Make items in the outline menu collapsable (#367)
Co-authored-by: Clement Roche <rchclement@gmail.com> Co-authored-by: Cole Lawrence <cole@colelawrence.com> Co-authored-by: Aria <aria.minaei@gmail.com>
This commit is contained in:
parent
a727ee277b
commit
6558181dbb
9 changed files with 252 additions and 118 deletions
|
@ -1,6 +1,7 @@
|
||||||
import type {VoidFn} from '@theatre/shared/utils/types'
|
import type {VoidFn} from '@theatre/shared/utils/types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled, {css} from 'styled-components'
|
import styled, {css} from 'styled-components'
|
||||||
|
import noop from '@theatre/shared/utils/noop'
|
||||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
import {ChevronDown, Package} from '@theatre/studio/uiComponents/icons'
|
import {ChevronDown, Package} from '@theatre/studio/uiComponents/icons'
|
||||||
|
|
||||||
|
@ -62,15 +63,6 @@ const Header = styled(BaseHeader)`
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hit zone
|
|
||||||
&:before {
|
|
||||||
position: absolute;
|
|
||||||
inset: -1px 0;
|
|
||||||
display: block;
|
|
||||||
content: ' ';
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@supports not (backdrop-filter: blur()) {
|
@supports not (backdrop-filter: blur()) {
|
||||||
background: rgba(40, 43, 47, 0.95);
|
background: rgba(40, 43, 47, 0.95);
|
||||||
}
|
}
|
||||||
|
@ -105,16 +97,24 @@ const Head_IconContainer = styled.div`
|
||||||
opacity: 0.99;
|
opacity: 0.99;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Head_Icon_WithDescendants = styled.span<{isOpen: boolean}>`
|
const Head_Icon_WithDescendants = styled.span`
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
${Container}.collapsed & {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const ChildrenContainer = styled.ul`
|
const ChildrenContainer = styled.ul`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
|
${Container}.collapsed & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
type SelectionStatus =
|
type SelectionStatus =
|
||||||
|
@ -129,7 +129,18 @@ const BaseItem: React.FC<{
|
||||||
depth: number
|
depth: number
|
||||||
selectionStatus: SelectionStatus
|
selectionStatus: SelectionStatus
|
||||||
labelDecoration?: React.ReactNode
|
labelDecoration?: React.ReactNode
|
||||||
}> = ({label, children, depth, select, selectionStatus, labelDecoration}) => {
|
collapsed?: boolean
|
||||||
|
setIsCollapsed?: (v: boolean) => void
|
||||||
|
}> = ({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
depth,
|
||||||
|
select,
|
||||||
|
selectionStatus,
|
||||||
|
labelDecoration,
|
||||||
|
collapsed = false,
|
||||||
|
setIsCollapsed,
|
||||||
|
}) => {
|
||||||
const canContainChildren = children !== undefined
|
const canContainChildren = children !== undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -138,11 +149,18 @@ const BaseItem: React.FC<{
|
||||||
/* @ts-ignore */
|
/* @ts-ignore */
|
||||||
{'--depth': depth}
|
{'--depth': depth}
|
||||||
}
|
}
|
||||||
|
className={collapsed ? 'collapsed' : ''}
|
||||||
>
|
>
|
||||||
<Header className={selectionStatus} onClick={select}>
|
<Header className={selectionStatus} onClick={select ?? noop} data-header>
|
||||||
<Head_IconContainer>
|
<Head_IconContainer>
|
||||||
{canContainChildren ? (
|
{canContainChildren ? (
|
||||||
<Head_Icon_WithDescendants isOpen={true}>
|
<Head_Icon_WithDescendants
|
||||||
|
onClick={(evt) => {
|
||||||
|
evt.stopPropagation()
|
||||||
|
setIsCollapsed?.(!collapsed)
|
||||||
|
select?.()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ChevronDown />
|
<ChevronDown />
|
||||||
</Head_Icon_WithDescendants>
|
</Head_Icon_WithDescendants>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
|
||||||
import {getObjectNamespacePath} from './getObjectNamespacePath'
|
|
||||||
|
|
||||||
/** See {@link addToNamespace} for adding to the namespace, easily. */
|
|
||||||
export type NamespacedObjects = Map<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
object?: SheetObject
|
|
||||||
nested?: NamespacedObjects
|
|
||||||
}
|
|
||||||
>
|
|
||||||
|
|
||||||
export function addToNamespace(
|
|
||||||
mutObjects: NamespacedObjects,
|
|
||||||
object: SheetObject,
|
|
||||||
) {
|
|
||||||
_addToNamespace(mutObjects, getObjectNamespacePath(object), object)
|
|
||||||
}
|
|
||||||
function _addToNamespace(
|
|
||||||
mutObjects: NamespacedObjects,
|
|
||||||
path: string[],
|
|
||||||
object: SheetObject,
|
|
||||||
) {
|
|
||||||
console.assert(path.length > 0, 'expecting path to not be empty')
|
|
||||||
const [next, ...rest] = path
|
|
||||||
let existing = mutObjects.get(next)
|
|
||||||
if (!existing) {
|
|
||||||
existing = {
|
|
||||||
nested: undefined,
|
|
||||||
object: undefined,
|
|
||||||
}
|
|
||||||
mutObjects.set(next, existing)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rest.length === 0) {
|
|
||||||
console.assert(
|
|
||||||
!existing.object,
|
|
||||||
'expect not to have existing object with same name',
|
|
||||||
{existing, object},
|
|
||||||
)
|
|
||||||
existing.object = object
|
|
||||||
} else {
|
|
||||||
if (!existing.nested) {
|
|
||||||
existing.nested = new Map()
|
|
||||||
}
|
|
||||||
|
|
||||||
_addToNamespace(existing.nested, rest, object)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,8 +6,7 @@ import styled from 'styled-components'
|
||||||
import {ObjectItem} from './ObjectItem'
|
import {ObjectItem} from './ObjectItem'
|
||||||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
||||||
import BaseItem from '@theatre/studio/panels/OutlinePanel/BaseItem'
|
import BaseItem from '@theatre/studio/panels/OutlinePanel/BaseItem'
|
||||||
import type {NamespacedObjects} from './NamespacedObjects'
|
import {useCollapseStateInOutlinePanel} from '@theatre/studio/panels/OutlinePanel/outlinePanelUtils'
|
||||||
import {addToNamespace} from './NamespacedObjects'
|
|
||||||
|
|
||||||
export const Li = styled.li<{isSelected: boolean}>`
|
export const Li = styled.li<{isSelected: boolean}>`
|
||||||
color: ${(props) => (props.isSelected ? 'white' : 'hsl(1, 1%, 80%)')};
|
color: ${(props) => (props.isSelected ? 'white' : 'hsl(1, 1%, 80%)')};
|
||||||
|
@ -28,54 +27,155 @@ const ObjectsList: React.FC<{
|
||||||
addToNamespace(rootObject, object)
|
addToNamespace(rootObject, object)
|
||||||
})
|
})
|
||||||
|
|
||||||
return <NamespaceTree namespace={rootObject} visualIndentation={depth} />
|
return (
|
||||||
|
<NamespaceTree
|
||||||
|
namespace={rootObject}
|
||||||
|
visualIndentation={depth}
|
||||||
|
path={[]}
|
||||||
|
sheet={sheet}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}, [sheet, depth])
|
}, [sheet, depth])
|
||||||
}
|
}
|
||||||
|
|
||||||
function NamespaceTree(props: {
|
function NamespaceTree(props: {
|
||||||
namespace: NamespacedObjects
|
namespace: NamespacedObjects
|
||||||
visualIndentation: number
|
visualIndentation: number
|
||||||
|
path: string[]
|
||||||
|
sheet: Sheet
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{[...props.namespace.entries()].map(([label, {object, nested}]) => {
|
{[...props.namespace.entries()].map(([label, {object, nested}]) => {
|
||||||
const nestedChildrenElt = nested && (
|
|
||||||
<NamespaceTree
|
|
||||||
namespace={nested}
|
|
||||||
// Question: will there be key conflict if two components have the same labels?
|
|
||||||
key={'namespaceTree(' + label + ')'}
|
|
||||||
visualIndentation={props.visualIndentation + 1}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
const sameNameElt = object && (
|
|
||||||
<ObjectItem
|
|
||||||
depth={props.visualIndentation}
|
|
||||||
// key is useful for navigating react dev component tree
|
|
||||||
key={'objectPath(' + object.address.objectKey + ')'}
|
|
||||||
// object entries should not allow this to be undefined
|
|
||||||
sheetObject={object}
|
|
||||||
overrideLabel={label}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={`${label} - ${props.visualIndentation}`}>
|
<Namespace
|
||||||
{sameNameElt}
|
key={label}
|
||||||
{nestedChildrenElt && (
|
label={label}
|
||||||
<BaseItem
|
object={object}
|
||||||
selectionStatus="not-selectable"
|
nested={nested}
|
||||||
label={label}
|
visualIndentation={props.visualIndentation}
|
||||||
// key necessary for no duplicate keys (next to other React.Fragments)
|
path={props.path}
|
||||||
key={`baseItem(${label})`}
|
sheet={props.sheet}
|
||||||
depth={props.visualIndentation}
|
/>
|
||||||
children={nestedChildrenElt}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Namespace(props: {
|
||||||
|
nested?: NamespacedObjects
|
||||||
|
label: string
|
||||||
|
object?: SheetObject
|
||||||
|
visualIndentation: number
|
||||||
|
path: string[]
|
||||||
|
sheet: Sheet
|
||||||
|
}) {
|
||||||
|
const {nested, label, object, sheet} = props
|
||||||
|
const {collapsed, setCollapsed} = useCollapseStateInOutlinePanel({
|
||||||
|
type: 'namespace',
|
||||||
|
sheet,
|
||||||
|
path: props.path,
|
||||||
|
})
|
||||||
|
|
||||||
|
const nestedChildrenElt = nested && (
|
||||||
|
<NamespaceTree
|
||||||
|
namespace={nested}
|
||||||
|
path={[...props.path, label]}
|
||||||
|
// Question: will there be key conflict if two components have the same labels?
|
||||||
|
key={'namespaceTree(' + label + ')'}
|
||||||
|
visualIndentation={props.visualIndentation + 1}
|
||||||
|
sheet={sheet}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
const sameNameElt = object && (
|
||||||
|
<ObjectItem
|
||||||
|
depth={props.visualIndentation}
|
||||||
|
// key is useful for navigating react dev component tree
|
||||||
|
key={'objectPath(' + object.address.objectKey + ')'}
|
||||||
|
// object entries should not allow this to be undefined
|
||||||
|
sheetObject={object}
|
||||||
|
overrideLabel={label}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={`${label} - ${props.visualIndentation}`}>
|
||||||
|
{sameNameElt}
|
||||||
|
{nestedChildrenElt && (
|
||||||
|
<BaseItem
|
||||||
|
selectionStatus="not-selectable"
|
||||||
|
label={label}
|
||||||
|
// key necessary for no duplicate keys (next to other React.Fragments)
|
||||||
|
key={`baseItem(${label})`}
|
||||||
|
depth={props.visualIndentation}
|
||||||
|
children={nestedChildrenElt}
|
||||||
|
collapsed={collapsed}
|
||||||
|
setIsCollapsed={setCollapsed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default ObjectsList
|
export default ObjectsList
|
||||||
|
|
||||||
|
/** See {@link addToNamespace} for adding to the namespace, easily. */
|
||||||
|
type NamespacedObjects = Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
object?: SheetObject
|
||||||
|
nested?: NamespacedObjects
|
||||||
|
path: string[]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
function addToNamespace(
|
||||||
|
mutObjects: NamespacedObjects,
|
||||||
|
object: SheetObject,
|
||||||
|
path = getObjectNamespacePath(object),
|
||||||
|
) {
|
||||||
|
const [next, ...rest] = path
|
||||||
|
let existing = mutObjects.get(next)
|
||||||
|
if (!existing) {
|
||||||
|
existing = {
|
||||||
|
nested: undefined,
|
||||||
|
object: undefined,
|
||||||
|
path: [...path],
|
||||||
|
}
|
||||||
|
mutObjects.set(next, existing)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rest.length === 0) {
|
||||||
|
console.assert(
|
||||||
|
!existing.object,
|
||||||
|
'expect not to have existing object with same name',
|
||||||
|
{existing, object},
|
||||||
|
)
|
||||||
|
existing.object = object
|
||||||
|
} else {
|
||||||
|
if (!existing.nested) {
|
||||||
|
existing.nested = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
addToNamespace(existing.nested, object, rest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getObjectNamespacePath(object: SheetObject): string[] {
|
||||||
|
let existing = OBJECT_SPLITS_MEMO.get(object)
|
||||||
|
if (!existing) {
|
||||||
|
existing = object.address.objectKey.split(
|
||||||
|
RE_SPLIT_BY_SLASH_WITHOUT_WHITESPACE,
|
||||||
|
)
|
||||||
|
console.assert(existing.length > 0, 'expected not empty')
|
||||||
|
OBJECT_SPLITS_MEMO.set(object, existing)
|
||||||
|
}
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Relying on the fact we try to "sanitize paths" earlier.
|
||||||
|
* Go look for `sanifySlashedPath` in a `utils/slashedPaths.ts`.
|
||||||
|
*/
|
||||||
|
const RE_SPLIT_BY_SLASH_WITHOUT_WHITESPACE = /\s*\/\s*/g
|
||||||
|
const OBJECT_SPLITS_MEMO = new WeakMap<SheetObject, string[]>()
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
|
||||||
|
|
||||||
export function getObjectNamespacePath(object: SheetObject): string[] {
|
|
||||||
let existing = OBJECT_SPLITS_MEMO.get(object)
|
|
||||||
if (!existing) {
|
|
||||||
existing = object.address.objectKey.split(
|
|
||||||
RE_SPLIT_BY_SLASH_WITHOUT_WHITESPACE,
|
|
||||||
)
|
|
||||||
console.assert(existing.length > 0, 'expected not empty')
|
|
||||||
OBJECT_SPLITS_MEMO.set(object, existing)
|
|
||||||
}
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Relying on the fact we try to "sanitize paths" earlier.
|
|
||||||
* Go look for `sanifySlashedPath` in a `utils/slashedPaths.ts`.
|
|
||||||
*/
|
|
||||||
const RE_SPLIT_BY_SLASH_WITHOUT_WHITESPACE = /\s*\/\s*/g
|
|
||||||
const OBJECT_SPLITS_MEMO = new WeakMap<SheetObject, string[]>()
|
|
|
@ -7,6 +7,7 @@ import {usePrism} from '@theatre/react'
|
||||||
import {getOutlineSelection} from '@theatre/studio/selectors'
|
import {getOutlineSelection} from '@theatre/studio/selectors'
|
||||||
import {val} from '@theatre/dataverse'
|
import {val} from '@theatre/dataverse'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
import {useCollapseStateInOutlinePanel} from '@theatre/studio/panels/OutlinePanel/outlinePanelUtils'
|
||||||
|
|
||||||
const ConflictNotice = styled.div`
|
const ConflictNotice = styled.div`
|
||||||
color: #ff6363;
|
color: #ff6363;
|
||||||
|
@ -38,10 +39,14 @@ const ProjectListItem: React.FC<{
|
||||||
})
|
})
|
||||||
}, [project])
|
}, [project])
|
||||||
|
|
||||||
|
const {collapsed, setCollapsed} = useCollapseStateInOutlinePanel(project)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseItem
|
<BaseItem
|
||||||
depth={depth}
|
depth={depth}
|
||||||
label={project.address.projectId}
|
label={project.address.projectId}
|
||||||
|
setIsCollapsed={setCollapsed}
|
||||||
|
collapsed={collapsed}
|
||||||
labelDecoration={
|
labelDecoration={
|
||||||
hasConflict ? <ConflictNotice>Has Conflicts</ConflictNotice> : null
|
hasConflict ? <ConflictNotice>Has Conflicts</ConflictNotice> : null
|
||||||
}
|
}
|
||||||
|
@ -56,7 +61,7 @@ const ProjectListItem: React.FC<{
|
||||||
: 'not-selected'
|
: 'not-selected'
|
||||||
}
|
}
|
||||||
select={select}
|
select={select}
|
||||||
></BaseItem>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import styled from 'styled-components'
|
||||||
import ObjectsList from '@theatre/studio/panels/OutlinePanel/ObjectsList/ObjectsList'
|
import ObjectsList from '@theatre/studio/panels/OutlinePanel/ObjectsList/ObjectsList'
|
||||||
import BaseItem from '@theatre/studio/panels/OutlinePanel/BaseItem'
|
import BaseItem from '@theatre/studio/panels/OutlinePanel/BaseItem'
|
||||||
import type Sheet from '@theatre/core/sheets/Sheet'
|
import type Sheet from '@theatre/core/sheets/Sheet'
|
||||||
|
import {useCollapseStateInOutlinePanel} from '@theatre/studio/panels/OutlinePanel/outlinePanelUtils'
|
||||||
|
|
||||||
const Head = styled.div`
|
const Head = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -21,6 +22,8 @@ export const SheetInstanceItem: React.FC<{
|
||||||
depth: number
|
depth: number
|
||||||
sheet: Sheet
|
sheet: Sheet
|
||||||
}> = ({sheet, depth}) => {
|
}> = ({sheet, depth}) => {
|
||||||
|
const {collapsed, setCollapsed} = useCollapseStateInOutlinePanel(sheet)
|
||||||
|
|
||||||
const setSelectedSheet = useCallback(() => {
|
const setSelectedSheet = useCallback(() => {
|
||||||
getStudio()!.transaction(({stateEditors}) => {
|
getStudio()!.transaction(({stateEditors}) => {
|
||||||
stateEditors.studio.historic.panels.outline.selection.set([sheet])
|
stateEditors.studio.historic.panels.outline.selection.set([sheet])
|
||||||
|
@ -34,6 +37,8 @@ export const SheetInstanceItem: React.FC<{
|
||||||
<BaseItem
|
<BaseItem
|
||||||
depth={depth}
|
depth={depth}
|
||||||
select={setSelectedSheet}
|
select={setSelectedSheet}
|
||||||
|
setIsCollapsed={setCollapsed}
|
||||||
|
collapsed={collapsed}
|
||||||
selectionStatus={
|
selectionStatus={
|
||||||
selection.some((s) => s === sheet)
|
selection.some((s) => s === sheet)
|
||||||
? 'selected'
|
? 'selected'
|
||||||
|
@ -58,5 +63,5 @@ export const SheetInstanceItem: React.FC<{
|
||||||
</Body>
|
</Body>
|
||||||
</BaseItem>
|
</BaseItem>
|
||||||
)
|
)
|
||||||
}, [depth])
|
}, [depth, collapsed])
|
||||||
}
|
}
|
||||||
|
|
45
theatre/studio/src/panels/OutlinePanel/outlinePanelUtils.ts
Normal file
45
theatre/studio/src/panels/OutlinePanel/outlinePanelUtils.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import type Project from '@theatre/core/projects/Project'
|
||||||
|
import {useCallback} from 'react'
|
||||||
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
|
import {useVal} from '@theatre/react'
|
||||||
|
import type Sheet from '@theatre/core/sheets/Sheet'
|
||||||
|
|
||||||
|
export function useCollapseStateInOutlinePanel(
|
||||||
|
item: Project | Sheet | {type: 'namespace'; sheet: Sheet; path: string[]},
|
||||||
|
): {
|
||||||
|
collapsed: boolean
|
||||||
|
setCollapsed: (collapsed: boolean) => void
|
||||||
|
} {
|
||||||
|
const itemKey =
|
||||||
|
item.type === 'namespace'
|
||||||
|
? `namespace:${item.sheet.address.sheetId}:${item.path.join('/')}`
|
||||||
|
: item.type === 'Theatre_Project'
|
||||||
|
? 'project'
|
||||||
|
: item.type === 'Theatre_Sheet'
|
||||||
|
? `sheetInstance:${item.address.sheetId}:${item.address.sheetInstanceId}`
|
||||||
|
: 'unknown'
|
||||||
|
|
||||||
|
const projectId =
|
||||||
|
item.type === 'namespace'
|
||||||
|
? item.sheet.address.projectId
|
||||||
|
: item.address.projectId
|
||||||
|
|
||||||
|
const isCollapsed =
|
||||||
|
useVal(
|
||||||
|
getStudio().atomP.ahistoric.projects.stateByProjectId[projectId]
|
||||||
|
.collapsedItemsInOutline[itemKey],
|
||||||
|
) ?? false
|
||||||
|
|
||||||
|
const setCollapsed = useCallback(
|
||||||
|
(isCollapsed: boolean) => {
|
||||||
|
getStudio().transaction(({stateEditors}) => {
|
||||||
|
stateEditors.studio.ahistoric.projects.stateByProjectId.collapsedItemsInOutline.set(
|
||||||
|
{projectId, isCollapsed, itemKey: itemKey},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[itemKey],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {collapsed: isCollapsed, setCollapsed}
|
||||||
|
}
|
|
@ -455,6 +455,7 @@ namespace stateEditors {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace projects {
|
export namespace projects {
|
||||||
export namespace stateByProjectId {
|
export namespace stateByProjectId {
|
||||||
export function _ensure(p: ProjectAddress) {
|
export function _ensure(p: ProjectAddress) {
|
||||||
|
@ -468,6 +469,33 @@ namespace stateEditors {
|
||||||
return s.projects.stateByProjectId[p.projectId]!
|
return s.projects.stateByProjectId[p.projectId]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export namespace collapsedItemsInOutline {
|
||||||
|
export function _ensure(p: ProjectAddress) {
|
||||||
|
const projectState =
|
||||||
|
stateEditors.studio.ahistoric.projects.stateByProjectId._ensure(
|
||||||
|
p,
|
||||||
|
)
|
||||||
|
if (!projectState.collapsedItemsInOutline) {
|
||||||
|
projectState.collapsedItemsInOutline = {}
|
||||||
|
}
|
||||||
|
return projectState.collapsedItemsInOutline!
|
||||||
|
}
|
||||||
|
export function set(
|
||||||
|
p: ProjectAddress & {isCollapsed: boolean; itemKey: string},
|
||||||
|
) {
|
||||||
|
const collapsedItemsInOutline =
|
||||||
|
stateEditors.studio.ahistoric.projects.stateByProjectId.collapsedItemsInOutline._ensure(
|
||||||
|
p,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (p.isCollapsed) {
|
||||||
|
collapsedItemsInOutline[p.itemKey] = true
|
||||||
|
} else {
|
||||||
|
delete collapsedItemsInOutline[p.itemKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export namespace stateBySheetId {
|
export namespace stateBySheetId {
|
||||||
export function _ensure(p: WithoutSheetInstance<SheetAddress>) {
|
export function _ensure(p: WithoutSheetInstance<SheetAddress>) {
|
||||||
const projectState =
|
const projectState =
|
||||||
|
|
|
@ -45,6 +45,7 @@ export type StudioAhistoricState = {
|
||||||
stateByProjectId: StrictRecord<
|
stateByProjectId: StrictRecord<
|
||||||
ProjectId,
|
ProjectId,
|
||||||
{
|
{
|
||||||
|
collapsedItemsInOutline?: StrictRecord<string, boolean>
|
||||||
stateBySheetId: StrictRecord<
|
stateBySheetId: StrictRecord<
|
||||||
SheetId,
|
SheetId,
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue