From 71f08e171a413a7e4c2059708b092c579d322fca Mon Sep 17 00:00:00 2001 From: Andrew Prifer <2991360+AndrewPrifer@users.noreply.github.com> Date: Wed, 4 Jan 2023 16:00:46 +0100 Subject: [PATCH] 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 --- .../src/sheetObjects/SheetObjectTemplate.ts | 21 +++++- theatre/core/src/sheets/Sheet.ts | 7 +- theatre/core/src/sheets/SheetTemplate.ts | 14 +++- theatre/core/src/sheets/TheatreSheet.ts | 30 +++++++- .../src/panels/DetailPanel/ObjectDetails.tsx | 69 ++++++++++++++++--- 5 files changed, 126 insertions(+), 15 deletions(-) diff --git a/theatre/core/src/sheetObjects/SheetObjectTemplate.ts b/theatre/core/src/sheetObjects/SheetObjectTemplate.ts index 79ebfc8..cc8c492 100644 --- a/theatre/core/src/sheetObjects/SheetObjectTemplate.ts +++ b/theatre/core/src/sheetObjects/SheetObjectTemplate.ts @@ -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 readonly type: 'Theatre_SheetObjectTemplate' = 'Theatre_SheetObjectTemplate' protected _config: Atom + readonly _actions: Atom 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) */ diff --git a/theatre/core/src/sheets/Sheet.ts b/theatre/core/src/sheets/Sheet.ts index 1c2dab6..32b425f 100644 --- a/theatre/core/src/sheets/Sheet.ts +++ b/theatre/core/src/sheets/Sheet.ts @@ -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) diff --git a/theatre/core/src/sheets/SheetTemplate.ts b/theatre/core/src/sheets/SheetTemplate.ts index cae1cda..680cae7 100644 --- a/theatre/core/src/sheets/SheetTemplate.ts +++ b/theatre/core/src/sheets/SheetTemplate.ts @@ -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) } diff --git a/theatre/core/src/sheets/TheatreSheet.ts b/theatre/core/src/sheets/TheatreSheet.ts index 9f91fcf..5ab7b56 100644 --- a/theatre/core/src/sheets/TheatreSheet.ts +++ b/theatre/core/src/sheets/TheatreSheet.ts @@ -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 +export type SheetObjectAction = (object: ISheetObject, studio: IStudio) => void + +export type SheetObjectActionsConfig = Record + 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( @@ -82,6 +99,7 @@ export interface ISheet { props: Props, options?: { reconfigure?: boolean + actions?: SheetObjectActionsConfig }, ): ISheetObject @@ -120,7 +138,7 @@ export default class TheatreSheet implements ISheet { object( key: string, config: Props, - opts?: {reconfigure?: boolean}, + opts?: {reconfigure?: boolean; actions?: SheetObjectActionsConfig}, ): ISheetObject { 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) diff --git a/theatre/studio/src/panels/DetailPanel/ObjectDetails.tsx b/theatre/studio/src/panels/DetailPanel/ObjectDetails.tsx index 938731d..c518607 100644 --- a/theatre/studio/src/panels/DetailPanel/ObjectDetails.tsx +++ b/theatre/studio/src/panels/DetailPanel/ObjectDetails.tsx @@ -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 ( - } - propConfig={config} - visualIndentation={1} - /> + <> + } + propConfig={config} + visualIndentation={1} + /> + + {actions && + Object.entries(actions).map(([actionName, action]) => { + return ( + { + action(obj.publicApi, getStudio().publicApi) + }} + > + {actionName} + + ) + })} + + ) }