Implement a way for users to be able to add buttons to the details panel (#372)
* Implement actions * Add action button styles * Add docs for actions
This commit is contained in:
parent
feb3ad34b8
commit
71f08e171a
5 changed files with 126 additions and 15 deletions
|
@ -1,7 +1,11 @@
|
||||||
import type Project from '@theatre/core/projects/Project'
|
import type Project from '@theatre/core/projects/Project'
|
||||||
import type Sheet from '@theatre/core/sheets/Sheet'
|
import type Sheet from '@theatre/core/sheets/Sheet'
|
||||||
import type SheetTemplate from '@theatre/core/sheets/SheetTemplate'
|
import type SheetTemplate from '@theatre/core/sheets/SheetTemplate'
|
||||||
import type {SheetObjectPropTypeConfig} from '@theatre/core/sheets/TheatreSheet'
|
import type {
|
||||||
|
SheetObjectAction,
|
||||||
|
SheetObjectActionsConfig,
|
||||||
|
SheetObjectPropTypeConfig,
|
||||||
|
} from '@theatre/core/sheets/TheatreSheet'
|
||||||
import {emptyArray} from '@theatre/shared/utils'
|
import {emptyArray} from '@theatre/shared/utils'
|
||||||
import type {
|
import type {
|
||||||
PathToProp,
|
PathToProp,
|
||||||
|
@ -58,6 +62,7 @@ export default class SheetObjectTemplate {
|
||||||
readonly address: WithoutSheetInstance<SheetObjectAddress>
|
readonly address: WithoutSheetInstance<SheetObjectAddress>
|
||||||
readonly type: 'Theatre_SheetObjectTemplate' = 'Theatre_SheetObjectTemplate'
|
readonly type: 'Theatre_SheetObjectTemplate' = 'Theatre_SheetObjectTemplate'
|
||||||
protected _config: Atom<SheetObjectPropTypeConfig>
|
protected _config: Atom<SheetObjectPropTypeConfig>
|
||||||
|
readonly _actions: Atom<SheetObjectActionsConfig>
|
||||||
readonly _cache = new SimpleCache()
|
readonly _cache = new SimpleCache()
|
||||||
readonly project: Project
|
readonly project: Project
|
||||||
|
|
||||||
|
@ -69,14 +74,24 @@ export default class SheetObjectTemplate {
|
||||||
return this._config.pointer
|
return this._config.pointer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get staticActions() {
|
||||||
|
return this._actions.getState()
|
||||||
|
}
|
||||||
|
|
||||||
|
get actionsPointer() {
|
||||||
|
return this._actions.pointer
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly sheetTemplate: SheetTemplate,
|
readonly sheetTemplate: SheetTemplate,
|
||||||
objectKey: ObjectAddressKey,
|
objectKey: ObjectAddressKey,
|
||||||
nativeObject: unknown,
|
nativeObject: unknown,
|
||||||
config: SheetObjectPropTypeConfig,
|
config: SheetObjectPropTypeConfig,
|
||||||
|
actions: SheetObjectActionsConfig,
|
||||||
) {
|
) {
|
||||||
this.address = {...sheetTemplate.address, objectKey}
|
this.address = {...sheetTemplate.address, objectKey}
|
||||||
this._config = new Atom(config)
|
this._config = new Atom(config)
|
||||||
|
this._actions = new Atom(actions)
|
||||||
this.project = sheetTemplate.project
|
this.project = sheetTemplate.project
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +108,10 @@ export default class SheetObjectTemplate {
|
||||||
this._config.setState(config)
|
this._config.setState(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerAction(name: string, action: SheetObjectAction) {
|
||||||
|
this._actions.setIn([name], action)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the default values (all defaults are read from the config)
|
* Returns the default values (all defaults are read from the config)
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import type Project from '@theatre/core/projects/Project'
|
import type Project from '@theatre/core/projects/Project'
|
||||||
import Sequence from '@theatre/core/sequences/Sequence'
|
import Sequence from '@theatre/core/sequences/Sequence'
|
||||||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
||||||
import type {SheetObjectPropTypeConfig} from '@theatre/core/sheets/TheatreSheet'
|
import type {
|
||||||
|
SheetObjectActionsConfig,
|
||||||
|
SheetObjectPropTypeConfig,
|
||||||
|
} from '@theatre/core/sheets/TheatreSheet'
|
||||||
import TheatreSheet from '@theatre/core/sheets/TheatreSheet'
|
import TheatreSheet from '@theatre/core/sheets/TheatreSheet'
|
||||||
import type {SheetAddress} from '@theatre/shared/utils/addresses'
|
import type {SheetAddress} from '@theatre/shared/utils/addresses'
|
||||||
import {Atom, valueDerivation} from '@theatre/dataverse'
|
import {Atom, valueDerivation} from '@theatre/dataverse'
|
||||||
|
@ -55,11 +58,13 @@ export default class Sheet {
|
||||||
objectKey: ObjectAddressKey,
|
objectKey: ObjectAddressKey,
|
||||||
nativeObject: ObjectNativeObject,
|
nativeObject: ObjectNativeObject,
|
||||||
config: SheetObjectPropTypeConfig,
|
config: SheetObjectPropTypeConfig,
|
||||||
|
actions: SheetObjectActionsConfig = {},
|
||||||
): SheetObject {
|
): SheetObject {
|
||||||
const objTemplate = this.template.getObjectTemplate(
|
const objTemplate = this.template.getObjectTemplate(
|
||||||
objectKey,
|
objectKey,
|
||||||
nativeObject,
|
nativeObject,
|
||||||
config,
|
config,
|
||||||
|
actions,
|
||||||
)
|
)
|
||||||
|
|
||||||
const object = objTemplate.createInstance(this, nativeObject, config)
|
const object = objTemplate.createInstance(this, nativeObject, config)
|
||||||
|
|
|
@ -8,7 +8,10 @@ import {Atom} from '@theatre/dataverse'
|
||||||
import type {Pointer} from '@theatre/dataverse'
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
import Sheet from './Sheet'
|
import Sheet from './Sheet'
|
||||||
import type {ObjectNativeObject} from './Sheet'
|
import type {ObjectNativeObject} from './Sheet'
|
||||||
import type {SheetObjectPropTypeConfig} from './TheatreSheet'
|
import type {
|
||||||
|
SheetObjectActionsConfig,
|
||||||
|
SheetObjectPropTypeConfig,
|
||||||
|
} from './TheatreSheet'
|
||||||
import type {
|
import type {
|
||||||
ObjectAddressKey,
|
ObjectAddressKey,
|
||||||
SheetId,
|
SheetId,
|
||||||
|
@ -50,11 +53,18 @@ export default class SheetTemplate {
|
||||||
objectKey: ObjectAddressKey,
|
objectKey: ObjectAddressKey,
|
||||||
nativeObject: ObjectNativeObject,
|
nativeObject: ObjectNativeObject,
|
||||||
config: SheetObjectPropTypeConfig,
|
config: SheetObjectPropTypeConfig,
|
||||||
|
actions: SheetObjectActionsConfig,
|
||||||
): SheetObjectTemplate {
|
): SheetObjectTemplate {
|
||||||
let template = this._objectTemplates.getState()[objectKey]
|
let template = this._objectTemplates.getState()[objectKey]
|
||||||
|
|
||||||
if (!template) {
|
if (!template) {
|
||||||
template = new SheetObjectTemplate(this, objectKey, nativeObject, config)
|
template = new SheetObjectTemplate(
|
||||||
|
this,
|
||||||
|
objectKey,
|
||||||
|
nativeObject,
|
||||||
|
config,
|
||||||
|
actions,
|
||||||
|
)
|
||||||
this._objectTemplates.setIn([objectKey], template)
|
this._objectTemplates.setIn([objectKey], template)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,15 @@ import type {
|
||||||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
||||||
import type {ObjectAddressKey} from '@theatre/shared/utils/ids'
|
import type {ObjectAddressKey} from '@theatre/shared/utils/ids'
|
||||||
import {notify} from '@theatre/shared/notify'
|
import {notify} from '@theatre/shared/notify'
|
||||||
|
import type {IStudio} from '@theatre/studio'
|
||||||
|
|
||||||
export type SheetObjectPropTypeConfig =
|
export type SheetObjectPropTypeConfig =
|
||||||
PropTypeConfig_Compound<UnknownValidCompoundProps>
|
PropTypeConfig_Compound<UnknownValidCompoundProps>
|
||||||
|
|
||||||
|
export type SheetObjectAction = (object: ISheetObject, studio: IStudio) => void
|
||||||
|
|
||||||
|
export type SheetObjectActionsConfig = Record<string, SheetObjectAction>
|
||||||
|
|
||||||
export interface ISheet {
|
export interface ISheet {
|
||||||
/**
|
/**
|
||||||
* All sheets have `sheet.type === 'Theatre_Sheet_PublicAPI'`
|
* All sheets have `sheet.type === 'Theatre_Sheet_PublicAPI'`
|
||||||
|
@ -46,7 +51,7 @@ export interface ISheet {
|
||||||
*
|
*
|
||||||
* @param key - Each object is identified by a key, which is a non-empty string
|
* @param key - Each object is identified by a key, which is a non-empty string
|
||||||
* @param props - The props of the object. See examples
|
* @param props - The props of the object. See examples
|
||||||
* @param options - (Optional) Provide `{reconfigure: true}` to reconfigure an existing object. Reac the example below for details.
|
* @param options - (Optional) Provide `{reconfigure: true}` to reconfigure an existing object, or `{actions: { ... }}` to add custom buttons to the UI. Read the example below for details.
|
||||||
*
|
*
|
||||||
* @returns An Object
|
* @returns An Object
|
||||||
*
|
*
|
||||||
|
@ -75,6 +80,18 @@ export interface ISheet {
|
||||||
* console.log(object.value.bar) // prints 0, since we've introduced this prop by reconfiguring the object
|
* console.log(object.value.bar) // prints 0, since we've introduced this prop by reconfiguring the object
|
||||||
*
|
*
|
||||||
* assert(obj === obj2) // passes, because reconfiguring the object returns the same object
|
* assert(obj === obj2) // passes, because reconfiguring the object returns the same object
|
||||||
|
*
|
||||||
|
* // you can add custom actions to an object:
|
||||||
|
* const obj = sheet.object("obj", {foo: 0}, {
|
||||||
|
* actions: {
|
||||||
|
* // This will display a button in the UI that will reset the value of `foo` to 0
|
||||||
|
* Reset: () => {
|
||||||
|
* studio.transaction((api) => {
|
||||||
|
* api.set(obj.props.foo, 0)
|
||||||
|
* })
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* })
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
object<Props extends UnknownShorthandCompoundProps>(
|
object<Props extends UnknownShorthandCompoundProps>(
|
||||||
|
@ -82,6 +99,7 @@ export interface ISheet {
|
||||||
props: Props,
|
props: Props,
|
||||||
options?: {
|
options?: {
|
||||||
reconfigure?: boolean
|
reconfigure?: boolean
|
||||||
|
actions?: SheetObjectActionsConfig
|
||||||
},
|
},
|
||||||
): ISheetObject<Props>
|
): ISheetObject<Props>
|
||||||
|
|
||||||
|
@ -120,7 +138,7 @@ export default class TheatreSheet implements ISheet {
|
||||||
object<Props extends UnknownShorthandCompoundProps>(
|
object<Props extends UnknownShorthandCompoundProps>(
|
||||||
key: string,
|
key: string,
|
||||||
config: Props,
|
config: Props,
|
||||||
opts?: {reconfigure?: boolean},
|
opts?: {reconfigure?: boolean; actions?: SheetObjectActionsConfig},
|
||||||
): ISheetObject<Props> {
|
): ISheetObject<Props> {
|
||||||
const internal = privateAPI(this)
|
const internal = privateAPI(this)
|
||||||
const sanitizedPath = validateAndSanitiseSlashedPathOrThrow(
|
const sanitizedPath = validateAndSanitiseSlashedPathOrThrow(
|
||||||
|
@ -161,6 +179,13 @@ export default class TheatreSheet implements ISheet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts?.actions) {
|
||||||
|
Object.entries(opts.actions).forEach(([key, action]) => {
|
||||||
|
existingObject.template.registerAction(key, action)
|
||||||
|
})
|
||||||
|
console.log('registered actions', opts.actions)
|
||||||
|
}
|
||||||
|
|
||||||
return existingObject.publicApi as $IntentionalAny
|
return existingObject.publicApi as $IntentionalAny
|
||||||
} else {
|
} else {
|
||||||
const sanitizedConfig = compound(config)
|
const sanitizedConfig = compound(config)
|
||||||
|
@ -168,6 +193,7 @@ export default class TheatreSheet implements ISheet {
|
||||||
sanitizedPath as ObjectAddressKey,
|
sanitizedPath as ObjectAddressKey,
|
||||||
nativeObject,
|
nativeObject,
|
||||||
sanitizedConfig,
|
sanitizedConfig,
|
||||||
|
opts?.actions,
|
||||||
)
|
)
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
weakMapOfUnsanitizedProps.set(object as $FixMe, config)
|
weakMapOfUnsanitizedProps.set(object as $FixMe, config)
|
||||||
|
|
|
@ -5,6 +5,37 @@ import type {$FixMe} from '@theatre/shared/utils/types'
|
||||||
import DeterminePropEditorForDetail from './DeterminePropEditorForDetail'
|
import DeterminePropEditorForDetail from './DeterminePropEditorForDetail'
|
||||||
import {useVal} from '@theatre/react'
|
import {useVal} from '@theatre/react'
|
||||||
import uniqueKeyForAnyObject from '@theatre/shared/utils/uniqueKeyForAnyObject'
|
import uniqueKeyForAnyObject from '@theatre/shared/utils/uniqueKeyForAnyObject'
|
||||||
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const ActionButtonContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ActionButton = styled.button`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
color: #a8a8a9;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
height: 28px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const ObjectDetails: React.FC<{
|
const ObjectDetails: React.FC<{
|
||||||
/** TODO: add support for multiple objects (it would show their common props) */
|
/** TODO: add support for multiple objects (it would show their common props) */
|
||||||
|
@ -12,17 +43,37 @@ const ObjectDetails: React.FC<{
|
||||||
}> = ({objects}) => {
|
}> = ({objects}) => {
|
||||||
const obj = objects[0]
|
const obj = objects[0]
|
||||||
const config = useVal(obj.template.configPointer)
|
const config = useVal(obj.template.configPointer)
|
||||||
|
const actions = useVal(obj.template.actionsPointer)
|
||||||
|
|
||||||
|
console.log(actions)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DeterminePropEditorForDetail
|
<>
|
||||||
// we don't use the object's address as the key because if a user calls `sheet.detachObject(key)` and later
|
<DeterminePropEditorForDetail
|
||||||
// calls `sheet.object(key)` with the same key, we want to re-render the object details panel.
|
// we don't use the object's address as the key because if a user calls `sheet.detachObject(key)` and later
|
||||||
key={uniqueKeyForAnyObject(obj)}
|
// calls `sheet.object(key)` with the same key, we want to re-render the object details panel.
|
||||||
obj={obj}
|
key={uniqueKeyForAnyObject(obj)}
|
||||||
pointerToProp={obj.propsP as Pointer<$FixMe>}
|
obj={obj}
|
||||||
propConfig={config}
|
pointerToProp={obj.propsP as Pointer<$FixMe>}
|
||||||
visualIndentation={1}
|
propConfig={config}
|
||||||
/>
|
visualIndentation={1}
|
||||||
|
/>
|
||||||
|
<ActionButtonContainer>
|
||||||
|
{actions &&
|
||||||
|
Object.entries(actions).map(([actionName, action]) => {
|
||||||
|
return (
|
||||||
|
<ActionButton
|
||||||
|
key={actionName}
|
||||||
|
onClick={() => {
|
||||||
|
action(obj.publicApi, getStudio().publicApi)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{actionName}
|
||||||
|
</ActionButton>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ActionButtonContainer>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue