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

View file

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

View file

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

View file

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

View file

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