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 Sheet from '@theatre/core/sheets/Sheet'
|
||||
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 type {
|
||||
PathToProp,
|
||||
|
@ -58,6 +62,7 @@ export default class SheetObjectTemplate {
|
|||
readonly address: WithoutSheetInstance<SheetObjectAddress>
|
||||
readonly type: 'Theatre_SheetObjectTemplate' = 'Theatre_SheetObjectTemplate'
|
||||
protected _config: Atom<SheetObjectPropTypeConfig>
|
||||
readonly _actions: Atom<SheetObjectActionsConfig>
|
||||
readonly _cache = new SimpleCache()
|
||||
readonly project: Project
|
||||
|
||||
|
@ -69,14 +74,24 @@ export default class SheetObjectTemplate {
|
|||
return this._config.pointer
|
||||
}
|
||||
|
||||
get staticActions() {
|
||||
return this._actions.getState()
|
||||
}
|
||||
|
||||
get actionsPointer() {
|
||||
return this._actions.pointer
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly sheetTemplate: SheetTemplate,
|
||||
objectKey: ObjectAddressKey,
|
||||
nativeObject: unknown,
|
||||
config: SheetObjectPropTypeConfig,
|
||||
actions: SheetObjectActionsConfig,
|
||||
) {
|
||||
this.address = {...sheetTemplate.address, objectKey}
|
||||
this._config = new Atom(config)
|
||||
this._actions = new Atom(actions)
|
||||
this.project = sheetTemplate.project
|
||||
}
|
||||
|
||||
|
@ -93,6 +108,10 @@ export default class SheetObjectTemplate {
|
|||
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)
|
||||
*/
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import type Project from '@theatre/core/projects/Project'
|
||||
import Sequence from '@theatre/core/sequences/Sequence'
|
||||
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 type {SheetAddress} from '@theatre/shared/utils/addresses'
|
||||
import {Atom, valueDerivation} from '@theatre/dataverse'
|
||||
|
@ -55,11 +58,13 @@ export default class Sheet {
|
|||
objectKey: ObjectAddressKey,
|
||||
nativeObject: ObjectNativeObject,
|
||||
config: SheetObjectPropTypeConfig,
|
||||
actions: SheetObjectActionsConfig = {},
|
||||
): SheetObject {
|
||||
const objTemplate = this.template.getObjectTemplate(
|
||||
objectKey,
|
||||
nativeObject,
|
||||
config,
|
||||
actions,
|
||||
)
|
||||
|
||||
const object = objTemplate.createInstance(this, nativeObject, config)
|
||||
|
|
|
@ -8,7 +8,10 @@ import {Atom} from '@theatre/dataverse'
|
|||
import type {Pointer} from '@theatre/dataverse'
|
||||
import Sheet from './Sheet'
|
||||
import type {ObjectNativeObject} from './Sheet'
|
||||
import type {SheetObjectPropTypeConfig} from './TheatreSheet'
|
||||
import type {
|
||||
SheetObjectActionsConfig,
|
||||
SheetObjectPropTypeConfig,
|
||||
} from './TheatreSheet'
|
||||
import type {
|
||||
ObjectAddressKey,
|
||||
SheetId,
|
||||
|
@ -50,11 +53,18 @@ export default class SheetTemplate {
|
|||
objectKey: ObjectAddressKey,
|
||||
nativeObject: ObjectNativeObject,
|
||||
config: SheetObjectPropTypeConfig,
|
||||
actions: SheetObjectActionsConfig,
|
||||
): SheetObjectTemplate {
|
||||
let template = this._objectTemplates.getState()[objectKey]
|
||||
|
||||
if (!template) {
|
||||
template = new SheetObjectTemplate(this, objectKey, nativeObject, config)
|
||||
template = new SheetObjectTemplate(
|
||||
this,
|
||||
objectKey,
|
||||
nativeObject,
|
||||
config,
|
||||
actions,
|
||||
)
|
||||
this._objectTemplates.setIn([objectKey], template)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,10 +19,15 @@ import type {
|
|||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
||||
import type {ObjectAddressKey} from '@theatre/shared/utils/ids'
|
||||
import {notify} from '@theatre/shared/notify'
|
||||
import type {IStudio} from '@theatre/studio'
|
||||
|
||||
export type SheetObjectPropTypeConfig =
|
||||
PropTypeConfig_Compound<UnknownValidCompoundProps>
|
||||
|
||||
export type SheetObjectAction = (object: ISheetObject, studio: IStudio) => void
|
||||
|
||||
export type SheetObjectActionsConfig = Record<string, SheetObjectAction>
|
||||
|
||||
export interface ISheet {
|
||||
/**
|
||||
* 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 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
|
||||
*
|
||||
|
@ -75,6 +80,18 @@ export interface ISheet {
|
|||
* 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
|
||||
*
|
||||
* // 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>(
|
||||
|
@ -82,6 +99,7 @@ export interface ISheet {
|
|||
props: Props,
|
||||
options?: {
|
||||
reconfigure?: boolean
|
||||
actions?: SheetObjectActionsConfig
|
||||
},
|
||||
): ISheetObject<Props>
|
||||
|
||||
|
@ -120,7 +138,7 @@ export default class TheatreSheet implements ISheet {
|
|||
object<Props extends UnknownShorthandCompoundProps>(
|
||||
key: string,
|
||||
config: Props,
|
||||
opts?: {reconfigure?: boolean},
|
||||
opts?: {reconfigure?: boolean; actions?: SheetObjectActionsConfig},
|
||||
): ISheetObject<Props> {
|
||||
const internal = privateAPI(this)
|
||||
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
|
||||
} else {
|
||||
const sanitizedConfig = compound(config)
|
||||
|
@ -168,6 +193,7 @@ export default class TheatreSheet implements ISheet {
|
|||
sanitizedPath as ObjectAddressKey,
|
||||
nativeObject,
|
||||
sanitizedConfig,
|
||||
opts?.actions,
|
||||
)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
weakMapOfUnsanitizedProps.set(object as $FixMe, config)
|
||||
|
|
|
@ -5,6 +5,37 @@ import type {$FixMe} from '@theatre/shared/utils/types'
|
|||
import DeterminePropEditorForDetail from './DeterminePropEditorForDetail'
|
||||
import {useVal} from '@theatre/react'
|
||||
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<{
|
||||
/** TODO: add support for multiple objects (it would show their common props) */
|
||||
|
@ -12,17 +43,37 @@ const ObjectDetails: React.FC<{
|
|||
}> = ({objects}) => {
|
||||
const obj = objects[0]
|
||||
const config = useVal(obj.template.configPointer)
|
||||
const actions = useVal(obj.template.actionsPointer)
|
||||
|
||||
console.log(actions)
|
||||
|
||||
return (
|
||||
<DeterminePropEditorForDetail
|
||||
// we don't use the object's address as the key because if a user calls `sheet.detachObject(key)` and later
|
||||
// calls `sheet.object(key)` with the same key, we want to re-render the object details panel.
|
||||
key={uniqueKeyForAnyObject(obj)}
|
||||
obj={obj}
|
||||
pointerToProp={obj.propsP as Pointer<$FixMe>}
|
||||
propConfig={config}
|
||||
visualIndentation={1}
|
||||
/>
|
||||
<>
|
||||
<DeterminePropEditorForDetail
|
||||
// we don't use the object's address as the key because if a user calls `sheet.detachObject(key)` and later
|
||||
// calls `sheet.object(key)` with the same key, we want to re-render the object details panel.
|
||||
key={uniqueKeyForAnyObject(obj)}
|
||||
obj={obj}
|
||||
pointerToProp={obj.propsP as Pointer<$FixMe>}
|
||||
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