@dxos/plugin-navtree 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLUGIN.mdl +8 -6
- package/dist/lib/NavTreePlugin.mjs +40 -0
- package/dist/lib/NavTreePlugin.mjs.map +1 -0
- package/dist/lib/capabilities.mjs +11 -0
- package/dist/lib/capabilities.mjs.map +1 -0
- package/dist/lib/chunk-CommandsDialogContent.mjs +100 -0
- package/dist/lib/chunk-CommandsDialogContent.mjs.map +1 -0
- package/dist/lib/chunk-CommandsTrigger.mjs +27 -0
- package/dist/lib/chunk-CommandsTrigger.mjs.map +1 -0
- package/dist/lib/chunk-NavTreeDocumentTitle.mjs +17 -0
- package/dist/lib/chunk-NavTreeDocumentTitle.mjs.map +1 -0
- package/dist/lib/chunk-app-graph-builder.mjs +33 -0
- package/dist/lib/chunk-app-graph-builder.mjs.map +1 -0
- package/dist/lib/chunk-expose.mjs +31 -0
- package/dist/lib/chunk-expose.mjs.map +1 -0
- package/dist/lib/chunk-keyboard.mjs +49 -0
- package/dist/lib/chunk-keyboard.mjs.map +1 -0
- package/dist/lib/{neutral/chunk-IRQMJLRY.mjs → chunk-meta.mjs} +14 -23
- package/dist/lib/chunk-meta.mjs.map +1 -0
- package/dist/lib/chunk-operation-handler.mjs +11 -0
- package/dist/lib/chunk-operation-handler.mjs.map +1 -0
- package/dist/lib/chunk-react-surface.mjs +46 -0
- package/dist/lib/chunk-react-surface.mjs.map +1 -0
- package/dist/lib/chunk-state.mjs +106 -0
- package/dist/lib/chunk-state.mjs.map +1 -0
- package/dist/lib/chunk-types.mjs +26 -0
- package/dist/lib/chunk-types.mjs.map +1 -0
- package/dist/lib/chunk-util.mjs +51 -0
- package/dist/lib/chunk-util.mjs.map +1 -0
- package/dist/lib/components.mjs +723 -0
- package/dist/lib/components.mjs.map +1 -0
- package/dist/lib/containers.mjs +231 -0
- package/dist/lib/containers.mjs.map +1 -0
- package/dist/lib/hooks.mjs +162 -0
- package/dist/lib/hooks.mjs.map +1 -0
- package/dist/lib/index.mjs +4 -0
- package/dist/lib/meta.mjs +2 -0
- package/dist/lib/operations.mjs +7 -0
- package/dist/lib/operations.mjs.map +1 -0
- package/dist/lib/plugin.mjs +9 -0
- package/dist/lib/plugin.mjs.map +1 -0
- package/dist/lib/testing.mjs +397 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/lib/translations.mjs +17 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/lib/types.mjs +2 -0
- package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
- package/dist/types/src/capabilities/app-graph-builder.d.ts +1 -1
- package/dist/types/src/capabilities/keyboard.d.ts +1 -1
- package/dist/types/src/capabilities/nav-tree-view-state.d.ts +12 -0
- package/dist/types/src/capabilities/nav-tree-view-state.d.ts.map +1 -0
- package/dist/types/src/capabilities/nav-tree-view-state.test.d.ts +2 -0
- package/dist/types/src/capabilities/nav-tree-view-state.test.d.ts.map +1 -0
- package/dist/types/src/capabilities/operation-handler.d.ts +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
- package/dist/types/src/capabilities/state.d.ts +1 -1
- package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -1
- package/dist/types/src/components/NavTree/NavTree.stories.d.ts +3 -0
- package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
- package/dist/types/src/components/NavTreeContext/NavTreeContext.d.ts.map +1 -1
- package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts +1 -1
- package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
- package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
- package/dist/types/src/components/Sidebar/L1Panel.d.ts +5 -2
- package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
- package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -1
- package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts.map +1 -1
- package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts +2 -0
- package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts.map +1 -1
- package/dist/types/src/containers/CommandsTrigger/CommandsTrigger.d.ts +4 -1
- package/dist/types/src/containers/CommandsTrigger/CommandsTrigger.d.ts.map +1 -1
- package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts.map +1 -1
- package/dist/types/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.d.ts +5 -2
- package/dist/types/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.d.ts.map +1 -1
- package/dist/types/src/experimental/Tree.d.ts +0 -1
- package/dist/types/src/experimental/Tree.d.ts.map +1 -1
- package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
- package/dist/types/src/experimental/visit-nodes.d.ts +3 -0
- package/dist/types/src/experimental/visit-nodes.d.ts.map +1 -0
- package/dist/types/src/hooks/useActions.d.ts +3 -1
- package/dist/types/src/hooks/useActions.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/NavTreeCapabilities.d.ts +1 -1
- package/dist/types/src/types/NavTreeCapabilities.d.ts.map +1 -1
- package/dist/types/src/types/schema.d.ts +1 -1
- package/dist/types/src/types/schema.d.ts.map +1 -1
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +47 -52
- package/src/NavTreePlugin.tsx +3 -1
- package/src/capabilities/nav-tree-view-state.test.ts +55 -0
- package/src/capabilities/nav-tree-view-state.ts +19 -0
- package/src/capabilities/{react-surface.tsx → react-surface.ts} +8 -9
- package/src/capabilities/state.ts +47 -38
- package/src/components/NavTree/NavTree.stories.tsx +26 -5
- package/src/components/NavTree/NavTree.tsx +10 -13
- package/src/components/NavTreeItem/NavTreeItemAction.tsx +1 -1
- package/src/components/Sidebar/L0Menu.tsx +9 -3
- package/src/components/Sidebar/L1Panel.tsx +52 -20
- package/src/components/Sidebar/L1Tabs.tsx +6 -0
- package/src/containers/CommandsDialogContent/CommandsDialogContent.tsx +49 -44
- package/src/containers/CommandsTrigger/CommandsTrigger.tsx +2 -0
- package/src/containers/NavTreeContainer/NavTreeContainer.tsx +17 -18
- package/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.tsx +2 -0
- package/src/experimental/Tree.stories.tsx +2 -1
- package/src/experimental/Tree.tsx +2 -18
- package/src/experimental/visit-nodes.ts +27 -0
- package/src/hooks/useActions.ts +15 -4
- package/src/hooks/useNavTreeModel.ts +1 -1
- package/src/operations/expose.ts +2 -2
- package/src/testing/app-graph-builder.ts +1 -1
- package/src/translations.ts +3 -0
- package/src/types/NavTreeCapabilities.ts +1 -1
- package/src/types/schema.ts +1 -1
- package/src/util.ts +7 -9
- package/dist/lib/neutral/CommandsDialogContent-TEKBCQ3U.mjs +0 -112
- package/dist/lib/neutral/CommandsDialogContent-TEKBCQ3U.mjs.map +0 -7
- package/dist/lib/neutral/CommandsTrigger-HGCI22SP.mjs +0 -27
- package/dist/lib/neutral/CommandsTrigger-HGCI22SP.mjs.map +0 -7
- package/dist/lib/neutral/NavTreeContainer-ICDKC2GI.mjs +0 -12
- package/dist/lib/neutral/NavTreeContainer-ICDKC2GI.mjs.map +0 -7
- package/dist/lib/neutral/NavTreeDocumentTitle-RMERXZGK.mjs +0 -22
- package/dist/lib/neutral/NavTreeDocumentTitle-RMERXZGK.mjs.map +0 -7
- package/dist/lib/neutral/NavTreePlugin.mjs +0 -68
- package/dist/lib/neutral/NavTreePlugin.mjs.map +0 -7
- package/dist/lib/neutral/NavTreePlugin.node.mjs +0 -18
- package/dist/lib/neutral/NavTreePlugin.node.mjs.map +0 -7
- package/dist/lib/neutral/NavTreePlugin.workerd.mjs +0 -16
- package/dist/lib/neutral/NavTreePlugin.workerd.mjs.map +0 -7
- package/dist/lib/neutral/app-graph-builder-2DYAIROU.mjs +0 -42
- package/dist/lib/neutral/app-graph-builder-2DYAIROU.mjs.map +0 -7
- package/dist/lib/neutral/capabilities/index.mjs +0 -17
- package/dist/lib/neutral/capabilities/index.mjs.map +0 -7
- package/dist/lib/neutral/chunk-4RN6TH36.mjs +0 -292
- package/dist/lib/neutral/chunk-4RN6TH36.mjs.map +0 -7
- package/dist/lib/neutral/chunk-BAVI2K7A.mjs +0 -27
- package/dist/lib/neutral/chunk-BAVI2K7A.mjs.map +0 -7
- package/dist/lib/neutral/chunk-EAN4K6OL.mjs +0 -8
- package/dist/lib/neutral/chunk-EAN4K6OL.mjs.map +0 -7
- package/dist/lib/neutral/chunk-IRQMJLRY.mjs.map +0 -7
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
- package/dist/lib/neutral/chunk-JOSNCOV4.mjs +0 -79
- package/dist/lib/neutral/chunk-JOSNCOV4.mjs.map +0 -7
- package/dist/lib/neutral/components/index.mjs +0 -814
- package/dist/lib/neutral/components/index.mjs.map +0 -7
- package/dist/lib/neutral/containers/index.mjs +0 -20
- package/dist/lib/neutral/containers/index.mjs.map +0 -7
- package/dist/lib/neutral/expose-HIBA2NQG.mjs +0 -38
- package/dist/lib/neutral/expose-HIBA2NQG.mjs.map +0 -7
- package/dist/lib/neutral/hooks/index.mjs +0 -181
- package/dist/lib/neutral/hooks/index.mjs.map +0 -7
- package/dist/lib/neutral/index.mjs +0 -34
- package/dist/lib/neutral/index.mjs.map +0 -7
- package/dist/lib/neutral/keyboard-JM2HCF5N.mjs +0 -58
- package/dist/lib/neutral/keyboard-JM2HCF5N.mjs.map +0 -7
- package/dist/lib/neutral/meta.json +0 -1
- package/dist/lib/neutral/meta.mjs +0 -12
- package/dist/lib/neutral/meta.mjs.map +0 -7
- package/dist/lib/neutral/operation-handler-ORDOJUL2.mjs +0 -13
- package/dist/lib/neutral/operation-handler-ORDOJUL2.mjs.map +0 -7
- package/dist/lib/neutral/operations/index.mjs +0 -8
- package/dist/lib/neutral/operations/index.mjs.map +0 -7
- package/dist/lib/neutral/plugin.mjs +0 -16
- package/dist/lib/neutral/plugin.mjs.map +0 -7
- package/dist/lib/neutral/react-surface-QPMUEDZX.mjs +0 -48
- package/dist/lib/neutral/react-surface-QPMUEDZX.mjs.map +0 -7
- package/dist/lib/neutral/state-PDZU4PIP.mjs +0 -126
- package/dist/lib/neutral/state-PDZU4PIP.mjs.map +0 -7
- package/dist/lib/neutral/testing/index.mjs +0 -439
- package/dist/lib/neutral/testing/index.mjs.map +0 -7
- package/dist/lib/neutral/translations.mjs +0 -23
- package/dist/lib/neutral/translations.mjs.map +0 -7
- package/dist/lib/neutral/types/index.mjs +0 -10
- package/dist/lib/neutral/types/index.mjs.map +0 -7
- package/dist/types/src/NavTreePlugin.node.d.ts +0 -4
- package/dist/types/src/NavTreePlugin.node.d.ts.map +0 -1
- package/dist/types/src/NavTreePlugin.test.d.ts +0 -2
- package/dist/types/src/NavTreePlugin.test.d.ts.map +0 -1
- package/dist/types/src/NavTreePlugin.workerd.d.ts +0 -4
- package/dist/types/src/NavTreePlugin.workerd.d.ts.map +0 -1
- package/src/NavTreePlugin.node.ts +0 -17
- package/src/NavTreePlugin.test.ts +0 -27
- package/src/NavTreePlugin.workerd.ts +0 -16
- /package/src/components/NavTreeContext/{NavTreeContext.tsx → NavTreeContext.ts} +0 -0
|
@@ -6,11 +6,12 @@ import * as Option from 'effect/Option';
|
|
|
6
6
|
import React, { memo, useCallback, useMemo } from 'react';
|
|
7
7
|
|
|
8
8
|
import { Node } from '@dxos/app-graph';
|
|
9
|
-
import {
|
|
9
|
+
import { GraphPath } from '@dxos/app-toolkit';
|
|
10
10
|
import { useAppGraph } from '@dxos/app-toolkit/ui';
|
|
11
|
-
import { Graph
|
|
11
|
+
import { Graph } from '@dxos/plugin-graph';
|
|
12
|
+
import { useActionRunner, useEdges } from '@dxos/plugin-graph/hooks';
|
|
12
13
|
import { DensityProvider, IconButton, ScrollArea, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
13
|
-
import { Tree } from '@dxos/react-ui-list';
|
|
14
|
+
import { Empty, Tree } from '@dxos/react-ui-list';
|
|
14
15
|
import { Menu, type MenuItem } from '@dxos/react-ui-menu';
|
|
15
16
|
import { Tabs } from '@dxos/react-ui-tabs';
|
|
16
17
|
import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/ui-theme';
|
|
@@ -22,27 +23,38 @@ import { NAV_TREE_ITEM } from '../NavTree';
|
|
|
22
23
|
import { useNavTreeContext } from '../NavTreeContext';
|
|
23
24
|
import { NavTreeItemColumns } from '../NavTreeItem/NavTreeItemColumns';
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Delay before the unavailable-workspace message appears: a workspace whose space is still loading has
|
|
28
|
+
* no graph node yet and materializes into a real panel on its own, so it must not flash during that window.
|
|
29
|
+
*/
|
|
30
|
+
const RENDER_DELAY = '1s';
|
|
31
|
+
|
|
25
32
|
export type L1PanelProps = {
|
|
26
33
|
open?: boolean;
|
|
27
34
|
path: string[];
|
|
28
|
-
|
|
35
|
+
/** The tab's workspace id, which may name a workspace that has no graph node. */
|
|
36
|
+
id: string;
|
|
37
|
+
/** Absent when the workspace is not in the graph; the panel then renders the unavailable message. */
|
|
38
|
+
item?: Node.Node;
|
|
29
39
|
isCurrent: boolean;
|
|
30
40
|
onBack?: () => void;
|
|
31
41
|
};
|
|
32
42
|
|
|
33
43
|
/**
|
|
34
|
-
* Space or settings panel.
|
|
44
|
+
* Space or settings panel. Without an `item` — a link to a workspace this identity never had or which no
|
|
45
|
+
* longer exists, or persisted deck state pointing at one after a profile switch — the panel body is the
|
|
46
|
+
* unavailable-workspace message, so the sidebar is never blank.
|
|
35
47
|
*/
|
|
36
|
-
const
|
|
48
|
+
const L1PanelInner = ({ open, path, id, item, isCurrent, onBack }: L1PanelProps) => {
|
|
37
49
|
const { t } = useTranslation(meta.profile.key);
|
|
38
|
-
const title = toLocalizedString(item.properties.label, t);
|
|
39
|
-
const isActivated = useIsActivatedWorkspace(
|
|
50
|
+
const title = item ? toLocalizedString(item.properties.label, t) : t('workspace-unavailable.heading');
|
|
51
|
+
const isActivated = useIsActivatedWorkspace(id);
|
|
40
52
|
const shouldRenderContent = isCurrent || isActivated;
|
|
41
53
|
|
|
42
54
|
return (
|
|
43
55
|
<Tabs.Panel
|
|
44
|
-
key={
|
|
45
|
-
value={
|
|
56
|
+
key={id}
|
|
57
|
+
value={id}
|
|
46
58
|
classNames={[
|
|
47
59
|
'absolute inset-y-0 end-0',
|
|
48
60
|
'w-[calc(100%-var(--dx-l0-size))] lg:w-(--dx-l1-size) grid-cols-1 grid-rows-[var(--dx-rail-size)_1fr]',
|
|
@@ -51,18 +63,34 @@ const L1Panel$ = ({ open, path, item, isCurrent, onBack }: L1PanelProps) => {
|
|
|
51
63
|
]}
|
|
52
64
|
tabIndex={-1}
|
|
53
65
|
aria-label={title}
|
|
54
|
-
|
|
66
|
+
// An unavailable workspace has no tab in the rail, so the generated `aria-labelledby` would
|
|
67
|
+
// reference a missing element.
|
|
68
|
+
{...(!item && { 'aria-labelledby': undefined })}
|
|
69
|
+
{...(isCurrent && {
|
|
70
|
+
'data-testid': item ? 'navtree.workspace.visible' : 'navtree.workspace.unavailable',
|
|
71
|
+
})}
|
|
55
72
|
{...(!open && { inert: true })}
|
|
56
73
|
>
|
|
57
|
-
{shouldRenderContent &&
|
|
74
|
+
{shouldRenderContent &&
|
|
75
|
+
(item ? (
|
|
76
|
+
<L1PanelContent open={open} path={path} item={item} onBack={onBack} />
|
|
77
|
+
) : (
|
|
78
|
+
<Empty
|
|
79
|
+
label={t('workspace-unavailable.description')}
|
|
80
|
+
// Second grid row, so the message clears the rail exactly as the tree does, and hugging its
|
|
81
|
+
// top rather than stretching to the row's full height.
|
|
82
|
+
classNames='row-start-2 self-start animate-fade-in'
|
|
83
|
+
style={{ animationDelay: RENDER_DELAY, animationFillMode: 'backwards' }}
|
|
84
|
+
/>
|
|
85
|
+
))}
|
|
58
86
|
</Tabs.Panel>
|
|
59
87
|
);
|
|
60
88
|
};
|
|
61
89
|
|
|
62
90
|
/** Determines whether a workspace tab has been populated with real child content (i.e. expanded at least once). */
|
|
63
|
-
const useIsActivatedWorkspace = (
|
|
91
|
+
const useIsActivatedWorkspace = (id: string): boolean => {
|
|
64
92
|
const { graph } = useAppGraph();
|
|
65
|
-
const edges = useEdges(graph,
|
|
93
|
+
const edges = useEdges(graph, id);
|
|
66
94
|
|
|
67
95
|
return useMemo(() => {
|
|
68
96
|
const childIds = edges[Graph.relationKey('child')] ?? [];
|
|
@@ -79,13 +107,17 @@ const useIsActivatedWorkspace = (item: Node.Node): boolean => {
|
|
|
79
107
|
/**
|
|
80
108
|
* Mounted panel content for active or previously-visited tabs.
|
|
81
109
|
*/
|
|
82
|
-
const L1PanelContent = ({
|
|
110
|
+
const L1PanelContent = ({
|
|
111
|
+
path,
|
|
112
|
+
item,
|
|
113
|
+
onBack,
|
|
114
|
+
}: Pick<L1PanelProps, 'open' | 'path' | 'onBack'> & { item: Node.Node }) => {
|
|
83
115
|
const navTreeContext = useNavTreeContext();
|
|
84
116
|
|
|
85
117
|
return (
|
|
86
118
|
<DensityProvider density='md'>
|
|
87
119
|
<L1PanelHeader path={path} item={item} onBack={onBack} />
|
|
88
|
-
<ScrollArea.Root thin orientation='vertical'>
|
|
120
|
+
<ScrollArea.Root centered thin orientation='vertical'>
|
|
89
121
|
<ScrollArea.Viewport>
|
|
90
122
|
<Tree
|
|
91
123
|
classNames='pt-[2px]'
|
|
@@ -113,11 +145,11 @@ const L1PanelContent = ({ path, item, onBack }: Pick<L1PanelProps, 'open' | 'pat
|
|
|
113
145
|
/**
|
|
114
146
|
* Header row.
|
|
115
147
|
*/
|
|
116
|
-
const L1PanelHeader = ({ item, path, onBack }: Pick<L1PanelProps, '
|
|
148
|
+
const L1PanelHeader = ({ item, path, onBack }: Pick<L1PanelProps, 'path' | 'onBack'> & { item: Node.Node }) => {
|
|
117
149
|
const { t } = useTranslation(meta.profile.key);
|
|
118
150
|
const { renderItemEnd: ItemEnd } = useNavTreeContext();
|
|
119
151
|
const title = toLocalizedString(item.properties.label, t);
|
|
120
|
-
const backCapableWorkspace =
|
|
152
|
+
const backCapableWorkspace = GraphPath.isPinnedWorkspace(item.id);
|
|
121
153
|
|
|
122
154
|
const { menuActions, onAction } = useL1MenuActions({ item, path });
|
|
123
155
|
useLoadDescendents(item);
|
|
@@ -211,7 +243,7 @@ const MenuActions = ({
|
|
|
211
243
|
/**
|
|
212
244
|
* Builds the menu actions for the L1 panel header.
|
|
213
245
|
*/
|
|
214
|
-
const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, '
|
|
246
|
+
const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, 'path'> & { item: Node.Node }): L1MenuActions => {
|
|
215
247
|
const runAction = useActionRunner();
|
|
216
248
|
|
|
217
249
|
const menuActions = getListActions(useActions(item));
|
|
@@ -226,4 +258,4 @@ const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, 'item' | 'path'>):
|
|
|
226
258
|
return { menuActions, onAction };
|
|
227
259
|
};
|
|
228
260
|
|
|
229
|
-
export const L1Panel = memo(
|
|
261
|
+
export const L1Panel = memo(L1PanelInner);
|
|
@@ -19,6 +19,10 @@ export type L1TabsProps = Pick<L1PanelProps, 'open' | 'onBack'> & {
|
|
|
19
19
|
* Each workspace is an L1 tab.
|
|
20
20
|
*/
|
|
21
21
|
export const L1Tabs = ({ topLevelItems, currentItemId, onBack, open, path }: L1TabsProps) => {
|
|
22
|
+
// The current tab can name a workspace that is not in the graph, in which case it gets an item-less
|
|
23
|
+
// panel carrying the unavailable message rather than no panel at all (a blank sidebar).
|
|
24
|
+
const hasCurrentPanel = topLevelItems.some((item) => item.id === currentItemId && l0ItemType(item) === 'tab');
|
|
25
|
+
|
|
22
26
|
return (
|
|
23
27
|
<>
|
|
24
28
|
{topLevelItems.map((item) => {
|
|
@@ -27,6 +31,7 @@ export const L1Tabs = ({ topLevelItems, currentItemId, onBack, open, path }: L1T
|
|
|
27
31
|
return (
|
|
28
32
|
<L1Panel
|
|
29
33
|
key={item.id}
|
|
34
|
+
id={item.id}
|
|
30
35
|
item={item}
|
|
31
36
|
path={path}
|
|
32
37
|
open={open}
|
|
@@ -37,6 +42,7 @@ export const L1Tabs = ({ topLevelItems, currentItemId, onBack, open, path }: L1T
|
|
|
37
42
|
}
|
|
38
43
|
return null;
|
|
39
44
|
})}
|
|
45
|
+
{!hasCurrentPanel && <L1Panel key={currentItemId} id={currentItemId} path={path} open={open} isCurrent />}
|
|
40
46
|
</>
|
|
41
47
|
);
|
|
42
48
|
};
|
|
@@ -8,8 +8,9 @@ import { useOperationInvoker } from '@dxos/app-framework/ui';
|
|
|
8
8
|
import { LayoutOperation } from '@dxos/app-toolkit';
|
|
9
9
|
import { useAppGraph } from '@dxos/app-toolkit/ui';
|
|
10
10
|
import { Keyboard, keySymbols } from '@dxos/keyboard';
|
|
11
|
-
import { Graph, Node
|
|
12
|
-
import { useActions } from '@dxos/plugin-graph';
|
|
11
|
+
import { Graph, Node } from '@dxos/plugin-graph';
|
|
12
|
+
import { useActions } from '@dxos/plugin-graph/hooks';
|
|
13
|
+
import { useActionRunner } from '@dxos/plugin-graph/hooks';
|
|
13
14
|
import { Button, Dialog, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
14
15
|
import { SearchList, useSearchListResults } from '@dxos/react-ui-search';
|
|
15
16
|
import { osTranslations } from '@dxos/ui-theme';
|
|
@@ -71,51 +72,53 @@ export const CommandsDialogContent = forwardRef<HTMLDivElement, CommandsDialogCo
|
|
|
71
72
|
return (
|
|
72
73
|
<Dialog.Content ref={forwardedRef}>
|
|
73
74
|
<Dialog.Title srOnly>{t('commands-dialog.title', { ns: meta.profile.key })}</Dialog.Title>
|
|
74
|
-
<
|
|
75
|
-
<SearchList.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
<Dialog.Body>
|
|
76
|
+
<SearchList.Root onSearch={handleSearch}>
|
|
77
|
+
<SearchList.Input placeholder={t('command-list-input.placeholder')} />
|
|
78
|
+
<SearchList.Viewport>
|
|
79
|
+
{results.map((action) => {
|
|
80
|
+
const shortcut =
|
|
81
|
+
typeof action.properties.keyBinding === 'string'
|
|
82
|
+
? action.properties.keyBinding
|
|
83
|
+
: action.properties.keyBinding?.[getHostPlatform()];
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (Node.isActionGroup(action)) {
|
|
96
|
-
setSelected(action.id);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
85
|
+
return (
|
|
86
|
+
<SearchList.Item
|
|
87
|
+
value={action.id}
|
|
88
|
+
key={action.id}
|
|
89
|
+
label={toLocalizedString(action.properties.label, t)}
|
|
90
|
+
icon={action.properties.icon}
|
|
91
|
+
suffix={shortcut ? keySymbols(shortcut).join('') : undefined}
|
|
92
|
+
onSelect={() => {
|
|
93
|
+
if (action.properties.disabled) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
99
96
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const node = Graph.getConnections(graph, lookupId, Node.actionRelation('inbound'))[0];
|
|
104
|
-
if (node && Node.isAction(action)) {
|
|
105
|
-
void runAction(action, { parent: node, caller: KEY_BINDING });
|
|
97
|
+
if (Node.isActionGroup(action)) {
|
|
98
|
+
setSelected(action.id);
|
|
99
|
+
return;
|
|
106
100
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
101
|
+
|
|
102
|
+
void invokePromise(LayoutOperation.UpdateDialog, { state: false });
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
const lookupId = group?.id ?? action.id;
|
|
105
|
+
const node = Graph.getConnections(graph, lookupId, Node.actionRelation('inbound'))[0];
|
|
106
|
+
if (node && Node.isAction(action)) {
|
|
107
|
+
void runAction(action, { parent: node, caller: KEY_BINDING });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}}
|
|
111
|
+
classNames='flex items-center gap-2'
|
|
112
|
+
disabled={action.properties.disabled}
|
|
113
|
+
{...(action.properties?.testId && {
|
|
114
|
+
'data-testid': action.properties.testId,
|
|
115
|
+
})}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
</SearchList.Viewport>
|
|
120
|
+
</SearchList.Root>
|
|
121
|
+
</Dialog.Body>
|
|
119
122
|
<Dialog.ActionBar>
|
|
120
123
|
<Dialog.Close asChild>
|
|
121
124
|
<Button classNames='w-full'>{t('close.label', { ns: osTranslations })}</Button>
|
|
@@ -125,3 +128,5 @@ export const CommandsDialogContent = forwardRef<HTMLDivElement, CommandsDialogCo
|
|
|
125
128
|
);
|
|
126
129
|
},
|
|
127
130
|
);
|
|
131
|
+
|
|
132
|
+
CommandsDialogContent.displayName = 'CommandsDialogContent';
|
|
@@ -10,7 +10,8 @@ import React, { forwardRef, memo, useCallback, useEffect, useMemo, useRef } from
|
|
|
10
10
|
import { Surface, useOperationInvoker } from '@dxos/app-framework/ui';
|
|
11
11
|
import { LayoutOperation } from '@dxos/app-toolkit';
|
|
12
12
|
import { AppSurface, useAppGraph, useLayout } from '@dxos/app-toolkit/ui';
|
|
13
|
-
import { Graph, Node
|
|
13
|
+
import { Graph, Node } from '@dxos/plugin-graph';
|
|
14
|
+
import { useActionRunner } from '@dxos/plugin-graph/hooks';
|
|
14
15
|
import { useMediaQuery, useSidebars } from '@dxos/react-ui';
|
|
15
16
|
import { type TreeData, isTreeData } from '@dxos/react-ui-list';
|
|
16
17
|
import { arrayMove } from '@dxos/util';
|
|
@@ -102,15 +103,11 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
|
|
|
102
103
|
if (activeItems.length === 0) {
|
|
103
104
|
const [item] = getItems(graph, node).filter((node) => !Node.isActionLike(node));
|
|
104
105
|
if (item && item.data) {
|
|
105
|
-
|
|
106
|
-
void invokePromise(LayoutOperation.Set, { subject: [item.id] });
|
|
107
|
-
} else {
|
|
108
|
-
void invokePromise(LayoutOperation.Open, { subject: [item.id] });
|
|
109
|
-
}
|
|
106
|
+
void invokePromise(LayoutOperation.Open, { subject: [item.id] });
|
|
110
107
|
}
|
|
111
108
|
}
|
|
112
109
|
},
|
|
113
|
-
[invokePromise, graph
|
|
110
|
+
[invokePromise, graph],
|
|
114
111
|
);
|
|
115
112
|
|
|
116
113
|
const blockInstruction = useCallback(
|
|
@@ -129,7 +126,7 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
|
|
|
129
126
|
}, []);
|
|
130
127
|
|
|
131
128
|
const handleSelect = useCallback(
|
|
132
|
-
({ item: node, path, option }: { item: Node.Node; path: string[]; option: boolean }) => {
|
|
129
|
+
({ item: node, path, option, shift }: { item: Node.Node; path: string[]; option: boolean; shift: boolean }) => {
|
|
133
130
|
if (!node.data) {
|
|
134
131
|
return;
|
|
135
132
|
}
|
|
@@ -144,20 +141,20 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
|
|
|
144
141
|
|
|
145
142
|
const current = getItem(path).current;
|
|
146
143
|
if (!current) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
144
|
+
// Plain click navigates (the deck becomes this item); shift forces a new plank (see the Open
|
|
145
|
+
// handler, which upgrades any disposition to add when shift is held).
|
|
146
|
+
void invokePromise(LayoutOperation.Open, {
|
|
147
|
+
subject: [node.id],
|
|
148
|
+
disposition: 'solo',
|
|
149
|
+
modifiers: { shift },
|
|
150
|
+
});
|
|
152
151
|
} else if (option) {
|
|
153
152
|
void invokePromise(LayoutOperation.Close, { subject: [node.id] });
|
|
154
153
|
} else {
|
|
155
154
|
void invokePromise(LayoutOperation.ScrollIntoView, { subject: node.id });
|
|
156
155
|
}
|
|
157
156
|
|
|
158
|
-
const defaultAction = Graph.getActions(graph, node.id).find(
|
|
159
|
-
(action) => action.properties?.disposition === 'default',
|
|
160
|
-
);
|
|
157
|
+
const defaultAction = Graph.getActions(graph, node.id).find((action) => Node.hasDisposition(action, 'default'));
|
|
161
158
|
if (Node.isAction(defaultAction)) {
|
|
162
159
|
void runAction(defaultAction);
|
|
163
160
|
}
|
|
@@ -166,7 +163,7 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
|
|
|
166
163
|
void invokePromise(LayoutOperation.UpdateSidebar, { state: 'closed' });
|
|
167
164
|
}
|
|
168
165
|
},
|
|
169
|
-
[graph, invokePromise, getItem, runAction, isLg
|
|
166
|
+
[graph, invokePromise, getItem, runAction, isLg],
|
|
170
167
|
);
|
|
171
168
|
|
|
172
169
|
const handleBack = useCallback(() => void invokePromise(LayoutOperation.RevertWorkspace), [invokePromise]);
|
|
@@ -237,7 +234,7 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
|
|
|
237
234
|
const workspaceChildren = useAtomValue(graph.connections(tab, 'child'));
|
|
238
235
|
useEffect(() => {
|
|
239
236
|
for (const child of workspaceChildren) {
|
|
240
|
-
if (child
|
|
237
|
+
if (Node.hasDisposition(child, 'group')) {
|
|
241
238
|
setItem([Node.RootId, tab, child.id], 'open', true);
|
|
242
239
|
Graph.expand(graph, child.id, 'child');
|
|
243
240
|
}
|
|
@@ -283,3 +280,5 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
|
|
|
283
280
|
);
|
|
284
281
|
|
|
285
282
|
export const NavTreeContainer = memo(NavTreeContainer$);
|
|
283
|
+
|
|
284
|
+
NavTreeContainer.displayName = 'NavTreeContainer';
|
|
@@ -10,7 +10,8 @@ import { Icon, IconButton, Panel, ScrollArea } from '@dxos/react-ui';
|
|
|
10
10
|
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
11
|
import { mx } from '@dxos/ui-theme';
|
|
12
12
|
|
|
13
|
-
import { type ItemMap, Tree, type TreeNodeData, type TreeProps,
|
|
13
|
+
import { type ItemMap, Tree, type TreeNodeData, type TreeProps, visitor } from './Tree';
|
|
14
|
+
import { visitNodes } from './visit-nodes';
|
|
14
15
|
|
|
15
16
|
random.seed(1234);
|
|
16
17
|
|
|
@@ -7,6 +7,8 @@ import React, { type HTMLAttributes, type PropsWithChildren } from 'react';
|
|
|
7
7
|
import { type ClassNameValue, Icon, type Size } from '@dxos/react-ui';
|
|
8
8
|
import { mx } from '@dxos/ui-theme';
|
|
9
9
|
|
|
10
|
+
import { visitNodes } from './visit-nodes';
|
|
11
|
+
|
|
10
12
|
export const IconButton = ({
|
|
11
13
|
iconName,
|
|
12
14
|
classNames,
|
|
@@ -56,24 +58,6 @@ export function* visitor(node: TreeNodeData, open: ItemMap): Generator<TreeItera
|
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
export const visitNodes = <T,>(
|
|
60
|
-
node: TreeNodeData,
|
|
61
|
-
callback: (node: TreeNodeData, depth: number) => T,
|
|
62
|
-
depth = 0,
|
|
63
|
-
): T | undefined => {
|
|
64
|
-
const result = callback(node, depth);
|
|
65
|
-
if (result) {
|
|
66
|
-
return result;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (const child of node.children ?? []) {
|
|
70
|
-
const result = visitNodes(child, callback, depth + 1);
|
|
71
|
-
if (result) {
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
61
|
export type ItemMap<T = boolean> = Record<TreeNodeData['id'], T>;
|
|
78
62
|
|
|
79
63
|
export type TreeNodeProps = {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type TreeNodeData } from './Tree';
|
|
6
|
+
|
|
7
|
+
// Kept out of `Tree.tsx`: react-refresh only fast-refreshes a module whose
|
|
8
|
+
// exports are all components, so values exported beside them force a full page reload on
|
|
9
|
+
// every edit.
|
|
10
|
+
|
|
11
|
+
export const visitNodes = <T>(
|
|
12
|
+
node: TreeNodeData,
|
|
13
|
+
callback: (node: TreeNodeData, depth: number) => T,
|
|
14
|
+
depth = 0,
|
|
15
|
+
): T | undefined => {
|
|
16
|
+
const result = callback(node, depth);
|
|
17
|
+
if (result) {
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const child of node.children ?? []) {
|
|
22
|
+
const result = visitNodes(child, callback, depth + 1);
|
|
23
|
+
if (result) {
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
package/src/hooks/useActions.ts
CHANGED
|
@@ -5,19 +5,30 @@
|
|
|
5
5
|
import { useMemo } from 'react';
|
|
6
6
|
|
|
7
7
|
import { useAppGraph } from '@dxos/app-toolkit/ui';
|
|
8
|
-
import { Graph, Node
|
|
8
|
+
import { Graph, Node } from '@dxos/plugin-graph';
|
|
9
|
+
import { useActions as useGraphActions } from '@dxos/plugin-graph/hooks';
|
|
10
|
+
import { applyPresentation } from '@dxos/react-ui-menu';
|
|
9
11
|
|
|
10
12
|
import { type FlattenedActions } from '#types';
|
|
11
13
|
|
|
14
|
+
/** Dispositions rendered as navtree item (row/header) actions, most-primary first. */
|
|
15
|
+
const LIST_ITEM_DISPOSITIONS = ['list-item-primary', 'list-item'];
|
|
16
|
+
|
|
12
17
|
/**
|
|
13
18
|
* Flattens grouped actions and filters to the list-item dispositions rendered as
|
|
14
19
|
* navtree item actions. Shared by row (`NavTreeItemColumns`) and header (`L1Panel`)
|
|
15
|
-
* so both apply the same normalization rule.
|
|
20
|
+
* so both apply the same normalization rule. Applies each action's `presentation['list-item'
|
|
21
|
+
* | 'list-item-primary']` chrome override, if any, so an action multi-targeting the toolbar
|
|
22
|
+
* and the nav-tree can render appropriately in each.
|
|
16
23
|
*/
|
|
17
24
|
export const getListActions = ({ actions, groupedActions }: FlattenedActions): Node.Action[] =>
|
|
18
25
|
actions
|
|
19
26
|
.flatMap((action) => (Node.isAction(action) ? [action] : (groupedActions[action.id] ?? [])))
|
|
20
|
-
.filter((action) =>
|
|
27
|
+
.filter((action) => Node.hasDisposition(action, LIST_ITEM_DISPOSITIONS))
|
|
28
|
+
.map((action) => {
|
|
29
|
+
const surface = LIST_ITEM_DISPOSITIONS.find((disposition) => Node.hasDisposition(action, disposition));
|
|
30
|
+
return surface ? applyPresentation(action, surface) : action;
|
|
31
|
+
});
|
|
21
32
|
|
|
22
33
|
/** Returns flattened actions and grouped sub-actions for a given graph node. */
|
|
23
34
|
export const useActions = (node: Node.Node): FlattenedActions => {
|
|
@@ -27,7 +38,7 @@ export const useActions = (node: Node.Node): FlattenedActions => {
|
|
|
27
38
|
return useMemo(() => {
|
|
28
39
|
return actions.reduce(
|
|
29
40
|
(acc: FlattenedActions, arg) => {
|
|
30
|
-
if (arg
|
|
41
|
+
if (Node.hasDisposition(arg, 'item')) {
|
|
31
42
|
return acc;
|
|
32
43
|
}
|
|
33
44
|
|
package/src/operations/expose.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { AppCapabilities, LayoutOperation } from '@dxos/app-toolkit';
|
|
|
10
10
|
import { Operation } from '@dxos/compute';
|
|
11
11
|
import { log } from '@dxos/log';
|
|
12
12
|
import { Graph } from '@dxos/plugin-graph';
|
|
13
|
-
import {
|
|
13
|
+
import { Attention } from '@dxos/react-ui-attention';
|
|
14
14
|
|
|
15
15
|
import { NavTreeCapabilities } from '../types';
|
|
16
16
|
|
|
@@ -20,7 +20,7 @@ const handler: Operation.WithHandler<typeof LayoutOperation.Expose> = LayoutOper
|
|
|
20
20
|
const { graph } = yield* Capability.get(AppCapabilities.AppGraph);
|
|
21
21
|
const { getItem, setItem } = yield* Capability.get(NavTreeCapabilities.State);
|
|
22
22
|
|
|
23
|
-
const prefixes = expandAttendableId(subject);
|
|
23
|
+
const prefixes = Attention.expandAttendableId(subject);
|
|
24
24
|
|
|
25
25
|
for (const qualifiedId of prefixes) {
|
|
26
26
|
Graph.expand(graph, qualifiedId, 'child');
|
package/src/translations.ts
CHANGED
|
@@ -17,6 +17,9 @@ export const translations = [
|
|
|
17
17
|
'node-actions-menu-invoker.label': 'More options',
|
|
18
18
|
'tree-item-actions.label': 'More actions',
|
|
19
19
|
'button-back.button': 'Back to Space',
|
|
20
|
+
'workspace-unavailable.heading': 'Workspace unavailable',
|
|
21
|
+
'workspace-unavailable.description':
|
|
22
|
+
'You don’t have this workspace, or it no longer exists. Select one of your workspaces to continue.',
|
|
20
23
|
},
|
|
21
24
|
},
|
|
22
25
|
},
|
package/src/types/schema.ts
CHANGED
package/src/util.ts
CHANGED
|
@@ -64,15 +64,13 @@ export const resolveMigrationOperation = (
|
|
|
64
64
|
// TODO(wittjosiah): Move into node implementation?
|
|
65
65
|
export const sortActions = (actions: Node.Action[]): Node.Action[] =>
|
|
66
66
|
actions.sort((a, b) => {
|
|
67
|
-
|
|
67
|
+
const aPrimary = Node.hasDisposition(a, 'list-item-primary');
|
|
68
|
+
const bPrimary = Node.hasDisposition(b, 'list-item-primary');
|
|
69
|
+
if (aPrimary === bPrimary) {
|
|
68
70
|
return 0;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
return -1;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return 1;
|
|
73
|
+
return aPrimary ? -1 : 1;
|
|
76
74
|
});
|
|
77
75
|
|
|
78
76
|
export const getChildren = (
|
|
@@ -93,13 +91,13 @@ export const getChildren = (
|
|
|
93
91
|
* Determines whether a node should be visible based on its disposition.
|
|
94
92
|
*/
|
|
95
93
|
export const filterItems = (node: Node.Node, disposition?: string) => {
|
|
96
|
-
if (!disposition && node
|
|
94
|
+
if (!disposition && Node.hasDisposition(node, 'hidden')) {
|
|
97
95
|
return false;
|
|
98
96
|
} else if (!disposition) {
|
|
99
97
|
const action = Node.isAction(node);
|
|
100
|
-
return !action || node
|
|
98
|
+
return !action || Node.hasDisposition(node, 'item');
|
|
101
99
|
} else {
|
|
102
|
-
return node
|
|
100
|
+
return Node.hasDisposition(node, disposition);
|
|
103
101
|
}
|
|
104
102
|
};
|
|
105
103
|
|