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:
Andrew Prifer 2023-01-04 16:00:46 +01:00 committed by GitHub
parent feb3ad34b8
commit 71f08e171a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 126 additions and 15 deletions

View file

@ -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)
*/

View file

@ -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)

View file

@ -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)
}

View file

@ -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)

View file

@ -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>
</>
)
}