@dxos/plugin-sheet 0.6.11-staging.e6894a4 → 0.6.12-main.5cc132e
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/dist/lib/browser/{SheetContainer-4XOKHKKZ.mjs → SheetContainer-Y7ZMFBAP.mjs} +582 -121
- package/dist/lib/browser/SheetContainer-Y7ZMFBAP.mjs.map +7 -0
- package/dist/lib/browser/{chunk-P7SSL3EG.mjs → chunk-GNNVBNCX.mjs} +61 -53
- package/dist/lib/browser/chunk-GNNVBNCX.mjs.map +7 -0
- package/dist/lib/browser/{chunk-FWGRE3EG.mjs → chunk-PGKZPKUD.mjs} +2 -2
- package/dist/lib/browser/chunk-VBF7YENS.mjs +8 -0
- package/dist/lib/browser/{chunk-FUAGSXA4.mjs → chunk-WUPTZUTX.mjs} +6 -3
- package/dist/lib/browser/chunk-WUPTZUTX.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +29 -18
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing.mjs +3 -3
- package/dist/lib/browser/types.mjs +1 -1
- package/dist/lib/node/{SheetContainer-IQT6TR4Z.cjs → SheetContainer-KEOKUKAQ.cjs} +528 -79
- package/dist/lib/node/SheetContainer-KEOKUKAQ.cjs.map +7 -0
- package/dist/lib/node/{chunk-5EPCDAZC.cjs → chunk-57PB2HPY.cjs} +5 -5
- package/dist/lib/node/{chunk-727C6YNP.cjs → chunk-6LWBQAQZ.cjs} +9 -9
- package/dist/lib/node/{chunk-DSYKOI4E.cjs → chunk-VJU3NPUJ.cjs} +8 -5
- package/dist/lib/node/chunk-VJU3NPUJ.cjs.map +7 -0
- package/dist/lib/node/{chunk-SVAIIXWQ.cjs → chunk-ZRQZFV5T.cjs} +76 -63
- package/dist/lib/node/chunk-ZRQZFV5T.cjs.map +7 -0
- package/dist/lib/node/index.cjs +43 -33
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing.cjs +6 -6
- package/dist/lib/node/types.cjs +9 -9
- package/dist/lib/node/types.cjs.map +1 -1
- package/dist/lib/node-esm/SheetContainer-Y7ZMFBAP.mjs +2231 -0
- package/dist/lib/node-esm/SheetContainer-Y7ZMFBAP.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-GNNVBNCX.mjs +3243 -0
- package/dist/lib/node-esm/chunk-GNNVBNCX.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-JRL5LGCE.mjs +18 -0
- package/dist/lib/node-esm/chunk-JRL5LGCE.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-PGKZPKUD.mjs +175 -0
- package/dist/lib/node-esm/chunk-PGKZPKUD.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-VBF7YENS.mjs +8 -0
- package/dist/lib/node-esm/chunk-VBF7YENS.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-WUPTZUTX.mjs +85 -0
- package/dist/lib/node-esm/chunk-WUPTZUTX.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +257 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/meta.mjs +9 -0
- package/dist/lib/node-esm/meta.mjs.map +7 -0
- package/dist/lib/node-esm/testing.mjs +92 -0
- package/dist/lib/node-esm/testing.mjs.map +7 -0
- package/dist/lib/node-esm/types.mjs +22 -0
- package/dist/lib/node-esm/types.mjs.map +7 -0
- package/dist/types/src/SheetPlugin.d.ts.map +1 -1
- package/dist/types/src/components/Sheet/Sheet.d.ts.map +1 -1
- package/dist/types/src/components/Sheet/Sheet.stories.d.ts.map +1 -1
- package/dist/types/src/components/Sheet/decorations.d.ts +24 -0
- package/dist/types/src/components/Sheet/decorations.d.ts.map +1 -0
- package/dist/types/src/components/Sheet/formatting.d.ts.map +1 -1
- package/dist/types/src/components/Sheet/sheet-context.d.ts +2 -0
- package/dist/types/src/components/Sheet/sheet-context.d.ts.map +1 -1
- package/dist/types/src/components/Sheet/threads.d.ts +2 -0
- package/dist/types/src/components/Sheet/threads.d.ts.map +1 -0
- package/dist/types/src/components/SheetContainer.d.ts +2 -3
- package/dist/types/src/components/SheetContainer.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +19 -3
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +17 -12
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -2
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/model/index.d.ts +1 -0
- package/dist/types/src/model/index.d.ts.map +1 -1
- package/dist/types/src/model/model.d.ts +0 -16
- package/dist/types/src/model/model.d.ts.map +1 -1
- package/dist/types/src/model/util.d.ts +24 -0
- package/dist/types/src/model/util.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +17 -12
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +72 -2
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +36 -32
- package/src/SheetPlugin.tsx +19 -20
- package/src/components/CellEditor/extension.test.ts +1 -2
- package/src/components/ComputeGraph/graph.browser.test.ts +1 -2
- package/src/components/Sheet/Sheet.stories.tsx +11 -9
- package/src/components/Sheet/Sheet.tsx +57 -29
- package/src/components/Sheet/decorations.ts +62 -0
- package/src/components/Sheet/formatting.ts +3 -3
- package/src/components/Sheet/sheet-context.tsx +9 -1
- package/src/components/Sheet/threads.tsx +201 -0
- package/src/components/SheetContainer.tsx +72 -20
- package/src/components/Toolbar/Toolbar.stories.tsx +5 -10
- package/src/components/Toolbar/Toolbar.tsx +54 -12
- package/src/model/index.ts +1 -0
- package/src/model/model.browser.test.ts +1 -2
- package/src/model/model.ts +11 -46
- package/src/model/types.test.ts +1 -2
- package/src/model/util.ts +67 -0
- package/src/translations.ts +6 -1
- package/src/types.ts +26 -3
- package/dist/lib/browser/SheetContainer-4XOKHKKZ.mjs.map +0 -7
- package/dist/lib/browser/chunk-FUAGSXA4.mjs.map +0 -7
- package/dist/lib/browser/chunk-P7SSL3EG.mjs.map +0 -7
- package/dist/lib/browser/chunk-YPU3R7FA.mjs +0 -8
- package/dist/lib/node/SheetContainer-IQT6TR4Z.cjs.map +0 -7
- package/dist/lib/node/chunk-DSYKOI4E.cjs.map +0 -7
- package/dist/lib/node/chunk-SVAIIXWQ.cjs.map +0 -7
- /package/dist/lib/browser/{chunk-FWGRE3EG.mjs.map → chunk-PGKZPKUD.mjs.map} +0 -0
- /package/dist/lib/browser/{chunk-YPU3R7FA.mjs.map → chunk-VBF7YENS.mjs.map} +0 -0
- /package/dist/lib/node/{chunk-5EPCDAZC.cjs.map → chunk-57PB2HPY.cjs.map} +0 -0
- /package/dist/lib/node/{chunk-727C6YNP.cjs.map → chunk-6LWBQAQZ.cjs.map} +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { effect } from '@preact/signals-core';
|
|
6
|
+
import React, { useCallback, useEffect, useMemo } from 'react';
|
|
7
|
+
|
|
8
|
+
import { type IntentResolver, LayoutAction, useIntentDispatcher, useIntentResolver } from '@dxos/app-framework';
|
|
9
|
+
import { debounce } from '@dxos/async';
|
|
10
|
+
import { fullyQualifiedId } from '@dxos/react-client/echo';
|
|
11
|
+
import { Icon, useTranslation } from '@dxos/react-ui';
|
|
12
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
13
|
+
|
|
14
|
+
import { type Decoration } from './decorations';
|
|
15
|
+
import { useSheetContext } from './sheet-context';
|
|
16
|
+
import { SHEET_PLUGIN } from '../../meta';
|
|
17
|
+
import { addressFromIndex, addressToIndex, type CellAddress, closest } from '../../model';
|
|
18
|
+
|
|
19
|
+
const CommentIndicator = () => {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
role='none'
|
|
23
|
+
className='absolute top-0 right-0 w-0 h-0 border-t-8 border-l-8 border-t-cmCommentSurface border-l-transparent'
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ThreadedCellWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
29
|
+
const dispatch = useIntentDispatcher();
|
|
30
|
+
const [isHovered, setIsHovered] = React.useState(true);
|
|
31
|
+
const { t } = useTranslation(SHEET_PLUGIN);
|
|
32
|
+
|
|
33
|
+
const handleClick = React.useCallback(
|
|
34
|
+
(_event: React.MouseEvent) => {
|
|
35
|
+
void dispatch({ action: LayoutAction.SET_LAYOUT, data: { element: 'complementary', state: true } });
|
|
36
|
+
},
|
|
37
|
+
[dispatch],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
role='none'
|
|
43
|
+
className={mx('relative h-full is-full')}
|
|
44
|
+
onMouseEnter={() => {
|
|
45
|
+
setIsHovered(true);
|
|
46
|
+
}}
|
|
47
|
+
onMouseLeave={() => {
|
|
48
|
+
setIsHovered(false);
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<CommentIndicator />
|
|
52
|
+
{isHovered && (
|
|
53
|
+
<div className='absolute inset-0 flex items-center justify-end pr-1'>
|
|
54
|
+
<button
|
|
55
|
+
className='ch-button text-xs min-bs-0 p-1'
|
|
56
|
+
onClick={handleClick}
|
|
57
|
+
aria-label={t('open comment for sheet cell')}
|
|
58
|
+
>
|
|
59
|
+
<Icon icon='ph--chat--regular' aria-hidden={true} />
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
{children}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const createThreadDecoration = (cellIndex: string, threadId: string, sheetId: string): Decoration => {
|
|
69
|
+
return {
|
|
70
|
+
type: 'comment',
|
|
71
|
+
cellIndex,
|
|
72
|
+
decorate: (props) => <ThreadedCellWrapper {...props} />,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const useUpdateCursorOnThreadSelection = () => {
|
|
77
|
+
const { setCursor, model } = useSheetContext();
|
|
78
|
+
|
|
79
|
+
const handleScrollIntoView: IntentResolver = useCallback(
|
|
80
|
+
({ action, data }) => {
|
|
81
|
+
switch (action) {
|
|
82
|
+
case LayoutAction.SCROLL_INTO_VIEW: {
|
|
83
|
+
if (!data?.id || data?.cursor === undefined || data?.id !== fullyQualifiedId(model.sheet)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// TODO(Zan): Everywhere we refer to the cursor in a thread context should change to `anchor`.
|
|
88
|
+
const cellAddress = addressFromIndex(model.sheet, data.cursor);
|
|
89
|
+
setCursor(cellAddress);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[model.sheet, setCursor],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
useIntentResolver(SHEET_PLUGIN, handleScrollIntoView);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const useSelectThreadOnCursorChange = () => {
|
|
100
|
+
const { cursor, model } = useSheetContext();
|
|
101
|
+
const dispatch = useIntentDispatcher();
|
|
102
|
+
|
|
103
|
+
const activeThreads = useMemo(
|
|
104
|
+
() =>
|
|
105
|
+
model.sheet.threads?.filter(
|
|
106
|
+
(thread): thread is NonNullable<typeof thread> => !!thread && thread.status === 'active',
|
|
107
|
+
) ?? [],
|
|
108
|
+
[JSON.stringify(model.sheet.threads)],
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const activeThreadAddresses = useMemo(
|
|
112
|
+
() =>
|
|
113
|
+
activeThreads
|
|
114
|
+
.map((thread) => thread.anchor)
|
|
115
|
+
.filter((anchor): anchor is NonNullable<typeof anchor> => anchor !== undefined)
|
|
116
|
+
.map((anchor) => addressFromIndex(model.sheet, anchor)),
|
|
117
|
+
[activeThreads, model.sheet],
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const selectClosestThread = useCallback(
|
|
121
|
+
(cellAddress: CellAddress) => {
|
|
122
|
+
if (!cellAddress || !activeThreads) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const closestThreadAnchor = closest(cellAddress, activeThreadAddresses);
|
|
127
|
+
if (closestThreadAnchor) {
|
|
128
|
+
const closestThread = activeThreads.find(
|
|
129
|
+
(thread) => thread && thread.anchor === addressToIndex(model.sheet, closestThreadAnchor),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (closestThread) {
|
|
133
|
+
void dispatch([
|
|
134
|
+
{ action: 'dxos.org/plugin/thread/action/select', data: { current: fullyQualifiedId(closestThread) } },
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
[dispatch, activeThreads, activeThreadAddresses, model.sheet],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const debounced = useMemo(() => {
|
|
143
|
+
return debounce((cursor: CellAddress) => requestAnimationFrame(() => selectClosestThread(cursor)), 50);
|
|
144
|
+
}, [selectClosestThread]);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (!cursor) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
debounced(cursor);
|
|
151
|
+
}, [cursor, selectClosestThread]);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const useThreadDecorations = () => {
|
|
155
|
+
const { decorations, model } = useSheetContext();
|
|
156
|
+
const sheet = useMemo(() => model.sheet, [model.sheet]);
|
|
157
|
+
const sheetId = useMemo(() => fullyQualifiedId(sheet), [sheet]);
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const unsubscribe = effect(() => {
|
|
161
|
+
const activeThreadAnchors = new Set<string>();
|
|
162
|
+
if (!sheet.threads) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Process active threads
|
|
167
|
+
for (const thread of sheet.threads) {
|
|
168
|
+
if (!thread || thread.anchor === undefined || thread.status === 'resolved') {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
activeThreadAnchors.add(thread.anchor);
|
|
173
|
+
const index = thread.anchor;
|
|
174
|
+
|
|
175
|
+
// Add decoration only if it doesn't already exist
|
|
176
|
+
const existingDecorations = decorations.getDecorationsForCell(index);
|
|
177
|
+
if (!existingDecorations || !existingDecorations.some((d) => d.type === 'comment')) {
|
|
178
|
+
decorations.addDecoration(index, createThreadDecoration(index, thread.id, sheetId));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Remove decorations for resolved or deleted threads
|
|
183
|
+
for (const decoration of decorations.getAllDecorations()) {
|
|
184
|
+
if (decoration.type !== 'comment') {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!activeThreadAnchors.has(decoration.cellIndex)) {
|
|
189
|
+
decorations.removeDecoration(decoration.cellIndex, 'comment');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
return () => unsubscribe();
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const useThreads = () => {
|
|
198
|
+
useUpdateCursorOnThreadSelection();
|
|
199
|
+
useSelectThreadOnCursorChange();
|
|
200
|
+
useThreadDecorations();
|
|
201
|
+
};
|
|
@@ -2,32 +2,84 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React from 'react';
|
|
5
|
+
import React, { useCallback } from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { useIntentDispatcher } from '@dxos/app-framework';
|
|
8
|
+
import { fullyQualifiedId } from '@dxos/react-client/echo';
|
|
9
|
+
import { useIsDirectlyAttended } from '@dxos/react-ui-attention';
|
|
10
|
+
import { focusRing, mx } from '@dxos/react-ui-theme';
|
|
9
11
|
|
|
10
12
|
import { Sheet, type SheetRootProps } from './Sheet';
|
|
13
|
+
import { Toolbar, type ToolbarAction } from './Toolbar';
|
|
14
|
+
|
|
15
|
+
// TODO(Zan): Factor out, copied this from MarkdownPlugin.
|
|
16
|
+
const attentionFragment = mx(
|
|
17
|
+
'group-focus-within/editor:attention-surface group-[[aria-current]]/editor:attention-surface',
|
|
18
|
+
'group-focus-within/editor:border-separator',
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// TODO(Zan): Factor out, copied this from MarkdownPlugin.
|
|
22
|
+
export const sectionToolbarLayout =
|
|
23
|
+
'bs-[--rail-action] bg-[--sticky-bg] sticky block-start-0 __-block-start-px transition-opacity';
|
|
24
|
+
|
|
25
|
+
const SheetContainer = ({ sheet, space, role, remoteFunctionUrl }: SheetRootProps & { role?: string }) => {
|
|
26
|
+
const dispatch = useIntentDispatcher();
|
|
27
|
+
|
|
28
|
+
const id = fullyQualifiedId(sheet);
|
|
29
|
+
const isDirectlyAttended = useIsDirectlyAttended(id);
|
|
30
|
+
|
|
31
|
+
// TODO(Zan): Centralise the toolbar action handler. Current implementation in stories.
|
|
32
|
+
const handleAction = useCallback(
|
|
33
|
+
(action: ToolbarAction) => {
|
|
34
|
+
switch (action.type) {
|
|
35
|
+
case 'comment': {
|
|
36
|
+
// TODO(Zan): We shouldn't hardcode the action ID.
|
|
37
|
+
void dispatch({
|
|
38
|
+
action: 'dxos.org/plugin/thread/action/create',
|
|
39
|
+
data: {
|
|
40
|
+
cursor: action.anchor,
|
|
41
|
+
name: action.cellContent,
|
|
42
|
+
subject: sheet,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
[sheet, dispatch],
|
|
49
|
+
);
|
|
11
50
|
|
|
12
|
-
const SheetContainer = ({
|
|
13
|
-
sheet,
|
|
14
|
-
space,
|
|
15
|
-
role,
|
|
16
|
-
coordinate = { part: 'main', entryId: '' },
|
|
17
|
-
remoteFunctionUrl,
|
|
18
|
-
}: SheetRootProps & { role?: string; coordinate?: LayoutCoordinate }) => {
|
|
19
51
|
return (
|
|
20
|
-
<div
|
|
21
|
-
role='none'
|
|
22
|
-
className={mx(
|
|
23
|
-
'flex',
|
|
24
|
-
role === 'article' && 'row-span-2', // TODO(burdon): Container with toolbar.
|
|
25
|
-
role === 'section' && 'aspect-square border-y border-is border-separator',
|
|
26
|
-
coordinate.part !== 'solo' && 'border-is border-separator',
|
|
27
|
-
)}
|
|
28
|
-
>
|
|
52
|
+
<div role='none' className={role === 'article' ? 'row-span-2 grid grid-rows-subgrid' : undefined}>
|
|
29
53
|
<Sheet.Root sheet={sheet} space={space} remoteFunctionUrl={remoteFunctionUrl}>
|
|
30
|
-
<
|
|
54
|
+
<div role='none' className={mx('flex flex-0 justify-center overflow-x-auto')}>
|
|
55
|
+
<Toolbar.Root
|
|
56
|
+
onAction={handleAction}
|
|
57
|
+
classNames={mx(
|
|
58
|
+
role === 'section'
|
|
59
|
+
? ['z-[2] group-focus-within/section:visible', !isDirectlyAttended && 'invisible', sectionToolbarLayout]
|
|
60
|
+
: 'group-focus-within/editor:border-separator group-[[aria-current]]/editor:border-separator',
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{/* TODO(Zan): Restore some of this functionality */}
|
|
64
|
+
{/* <Toolbar.Styles /> */}
|
|
65
|
+
{/* <Toolbar.Format /> */}
|
|
66
|
+
{/* <Toolbar.Alignment /> */}
|
|
67
|
+
<Toolbar.Separator />
|
|
68
|
+
<Toolbar.Actions />
|
|
69
|
+
</Toolbar.Root>
|
|
70
|
+
</div>
|
|
71
|
+
<div
|
|
72
|
+
role='none'
|
|
73
|
+
className={mx(
|
|
74
|
+
role === 'section' && 'aspect-square border-is border-bs border-be border-separator',
|
|
75
|
+
role === 'article' &&
|
|
76
|
+
'flex is-full overflow-hidden focus-visible:ring-inset row-span-1 data-[toolbar=disabled]:pbs-2 data-[toolbar=disabled]:row-span-2 border-bs border-separator',
|
|
77
|
+
focusRing,
|
|
78
|
+
attentionFragment,
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
<Sheet.Main />
|
|
82
|
+
</div>
|
|
31
83
|
</Sheet.Root>
|
|
32
84
|
</div>
|
|
33
85
|
);
|
|
@@ -6,29 +6,24 @@ import '@dxos-theme';
|
|
|
6
6
|
|
|
7
7
|
import React from 'react';
|
|
8
8
|
|
|
9
|
-
import { Tooltip } from '@dxos/react-ui';
|
|
10
9
|
import { textBlockWidth } from '@dxos/react-ui-theme';
|
|
11
|
-
import { withTheme } from '@dxos/storybook-utils';
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
12
11
|
|
|
13
12
|
import { Toolbar } from './Toolbar';
|
|
14
13
|
import translations from '../../translations';
|
|
15
14
|
|
|
16
15
|
const Story = () => {
|
|
17
16
|
return (
|
|
18
|
-
<
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
<Toolbar.Alignment />
|
|
22
|
-
</Toolbar.Root>
|
|
23
|
-
</div>
|
|
24
|
-
</Tooltip.Provider>
|
|
17
|
+
<Toolbar.Root classNames={textBlockWidth}>
|
|
18
|
+
<Toolbar.Alignment />
|
|
19
|
+
</Toolbar.Root>
|
|
25
20
|
);
|
|
26
21
|
};
|
|
27
22
|
|
|
28
23
|
export default {
|
|
29
24
|
title: 'plugin-sheet/Toolbar',
|
|
30
25
|
component: Toolbar,
|
|
31
|
-
decorators: [withTheme],
|
|
26
|
+
decorators: [withTheme, withLayout({ tooltips: true })],
|
|
32
27
|
parameters: { translations, layout: 'fullscreen' },
|
|
33
28
|
render: (args: any) => <Story {...args} />,
|
|
34
29
|
};
|
|
@@ -23,22 +23,31 @@ import {
|
|
|
23
23
|
type ThemedClassName,
|
|
24
24
|
useTranslation,
|
|
25
25
|
} from '@dxos/react-ui';
|
|
26
|
+
import { nonNullable } from '@dxos/util';
|
|
26
27
|
|
|
27
28
|
import { ToolbarButton, ToolbarSeparator, ToolbarToggleButton } from './common';
|
|
28
29
|
import { SHEET_PLUGIN } from '../../meta';
|
|
30
|
+
import { addressToIndex } from '../../model';
|
|
29
31
|
import { type Formatting } from '../../types';
|
|
32
|
+
import { useSheetContext } from '../Sheet/sheet-context';
|
|
30
33
|
|
|
31
34
|
//
|
|
32
35
|
// Root
|
|
33
36
|
//
|
|
34
37
|
|
|
35
|
-
export type
|
|
38
|
+
export type ToolbarAction =
|
|
39
|
+
| { type: 'clear' }
|
|
40
|
+
| { type: 'highlight' }
|
|
41
|
+
| { type: 'left' }
|
|
42
|
+
| { type: 'center' }
|
|
43
|
+
| { type: 'right' }
|
|
44
|
+
| { type: 'date' }
|
|
45
|
+
| { type: 'currency' }
|
|
46
|
+
| { type: 'comment'; anchor: string; cellContent?: string };
|
|
36
47
|
|
|
37
|
-
export type
|
|
38
|
-
type: ToolbarActionType;
|
|
39
|
-
};
|
|
48
|
+
export type ToolbarActionType = ToolbarAction['type'];
|
|
40
49
|
|
|
41
|
-
export type ToolbarActionHandler = (
|
|
50
|
+
export type ToolbarActionHandler = (action: ToolbarAction) => void;
|
|
42
51
|
|
|
43
52
|
export type ToolbarProps = ThemedClassName<
|
|
44
53
|
PropsWithChildren<{
|
|
@@ -96,7 +105,7 @@ const Format = () => {
|
|
|
96
105
|
Icon={Icon}
|
|
97
106
|
// disabled={state?.blockType === 'codeblock'}
|
|
98
107
|
// onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
99
|
-
onClick={() => onAction?.({ type })}
|
|
108
|
+
onClick={() => onAction?.({ type: type as Exclude<typeof type, 'comment'> })}
|
|
100
109
|
>
|
|
101
110
|
{t(`toolbar ${type} label`)}
|
|
102
111
|
</ToolbarToggleButton>
|
|
@@ -127,7 +136,7 @@ const Alignment = () => {
|
|
|
127
136
|
Icon={Icon}
|
|
128
137
|
// disabled={state?.blockType === 'codeblock'}
|
|
129
138
|
// onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
130
|
-
onClick={() => onAction?.({ type })}
|
|
139
|
+
onClick={() => onAction?.({ type: type as Exclude<typeof type, 'comment'> })}
|
|
131
140
|
>
|
|
132
141
|
{t(`toolbar ${type} label`)}
|
|
133
142
|
</ToolbarToggleButton>
|
|
@@ -157,7 +166,7 @@ const Styles = () => {
|
|
|
157
166
|
Icon={Icon}
|
|
158
167
|
// disabled={state?.blockType === 'codeblock'}
|
|
159
168
|
// onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
160
|
-
onClick={() => onAction?.({ type })}
|
|
169
|
+
onClick={() => onAction?.({ type: type as Exclude<typeof type, 'comment'> })}
|
|
161
170
|
>
|
|
162
171
|
{t(`toolbar ${type} label`)}
|
|
163
172
|
</ToolbarToggleButton>
|
|
@@ -170,18 +179,51 @@ const Styles = () => {
|
|
|
170
179
|
// Actions
|
|
171
180
|
//
|
|
172
181
|
|
|
182
|
+
// TODO(Zan): Instead of taking props, can we access the state from sheet context?
|
|
173
183
|
const Actions = () => {
|
|
174
|
-
|
|
184
|
+
const { onAction } = useToolbarContext('Actions');
|
|
185
|
+
const { cursor, range, model } = useSheetContext();
|
|
175
186
|
const { t } = useTranslation(SHEET_PLUGIN);
|
|
187
|
+
|
|
188
|
+
const overlapsCommentAnchor = (model.sheet.threads ?? [])
|
|
189
|
+
.filter(nonNullable)
|
|
190
|
+
.filter((thread) => thread.status !== 'resolved')
|
|
191
|
+
.some((thread) => {
|
|
192
|
+
if (!cursor) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return addressToIndex(model.sheet, cursor) === thread.anchor;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const hasCursor = !!cursor;
|
|
199
|
+
const cursorOnly = hasCursor && !range && !overlapsCommentAnchor;
|
|
200
|
+
|
|
201
|
+
const tooltipLabelKey = !hasCursor
|
|
202
|
+
? 'no cursor label'
|
|
203
|
+
: overlapsCommentAnchor
|
|
204
|
+
? 'selection overlaps existing comment label'
|
|
205
|
+
: range
|
|
206
|
+
? 'comment ranges not supported label'
|
|
207
|
+
: 'comment label';
|
|
208
|
+
|
|
176
209
|
return (
|
|
177
210
|
<ToolbarButton
|
|
178
211
|
value='comment'
|
|
179
212
|
Icon={ChatText}
|
|
180
213
|
data-testid='editor.toolbar.comment'
|
|
181
|
-
|
|
182
|
-
|
|
214
|
+
onClick={() => {
|
|
215
|
+
if (!cursor) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
return onAction?.({
|
|
219
|
+
type: 'comment',
|
|
220
|
+
anchor: addressToIndex(model.sheet, cursor),
|
|
221
|
+
cellContent: model.getCellText(cursor),
|
|
222
|
+
});
|
|
223
|
+
}}
|
|
224
|
+
disabled={!cursorOnly || overlapsCommentAnchor}
|
|
183
225
|
>
|
|
184
|
-
{t(
|
|
226
|
+
{t(tooltipLabelKey)}
|
|
185
227
|
</ToolbarButton>
|
|
186
228
|
);
|
|
187
229
|
};
|
package/src/model/index.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { expect } from '
|
|
6
|
-
import { describe, test } from 'vitest';
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
7
6
|
|
|
8
7
|
import { SheetModel } from './model';
|
|
9
8
|
import { addressFromA1Notation, rangeFromA1Notation } from './types';
|
package/src/model/model.ts
CHANGED
|
@@ -17,13 +17,12 @@ import { type FunctionType } from '@dxos/plugin-script/types';
|
|
|
17
17
|
|
|
18
18
|
import { defaultFunctions, type FunctionDefinition } from './functions';
|
|
19
19
|
import { addressFromA1Notation, addressToA1Notation, type CellAddress, type CellRange } from './types';
|
|
20
|
-
import { createIndices, RangeException, ReadonlyException } from './util';
|
|
20
|
+
import { addressFromIndex, addressToIndex, createIndices, RangeException, ReadonlyException } from './util';
|
|
21
21
|
import { type ComputeGraph } from '../components';
|
|
22
22
|
import { type CellScalarValue, type CellValue, type SheetType, ValueTypeEnum } from '../types';
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const DEFAULT_COLUMNS = 26 * 2;
|
|
24
|
+
const DEFAULT_ROWS = 100;
|
|
25
|
+
const DEFAULT_COLUMNS = 26;
|
|
27
26
|
|
|
28
27
|
export type CellIndex = string;
|
|
29
28
|
|
|
@@ -187,7 +186,7 @@ export class SheetModel {
|
|
|
187
186
|
reset() {
|
|
188
187
|
this._graph.hf.clearSheet(this._sheetId);
|
|
189
188
|
Object.entries(this._sheet.cells).forEach(([key, { value }]) => {
|
|
190
|
-
const { column, row } = this.
|
|
189
|
+
const { column, row } = addressFromIndex(this._sheet, key);
|
|
191
190
|
if (typeof value === 'string' && value.charAt(0) === '=') {
|
|
192
191
|
value = this.mapFormulaBindingToFormula(this.mapFormulaBindingFromId(this.mapFormulaIndicesToRefs(value)));
|
|
193
192
|
}
|
|
@@ -221,7 +220,6 @@ export class SheetModel {
|
|
|
221
220
|
// Undoable actions.
|
|
222
221
|
// TODO(burdon): Group undoable methods; consistently update hf/sheet.
|
|
223
222
|
//
|
|
224
|
-
|
|
225
223
|
/**
|
|
226
224
|
* Clear range of values.
|
|
227
225
|
*/
|
|
@@ -230,7 +228,7 @@ export class SheetModel {
|
|
|
230
228
|
const values = this._iterRange(range, () => null);
|
|
231
229
|
this._graph.hf.setCellContents(toSimpleCellAddress(this._sheetId, topLeft), values);
|
|
232
230
|
this._iterRange(range, (cell) => {
|
|
233
|
-
const idx = this.
|
|
231
|
+
const idx = addressToIndex(this._sheet, cell);
|
|
234
232
|
delete this._sheet.cells[idx];
|
|
235
233
|
});
|
|
236
234
|
}
|
|
@@ -238,7 +236,7 @@ export class SheetModel {
|
|
|
238
236
|
cut(range: CellRange) {
|
|
239
237
|
this._graph.hf.cut(toModelRange(this._sheetId, range));
|
|
240
238
|
this._iterRange(range, (cell) => {
|
|
241
|
-
const idx = this.
|
|
239
|
+
const idx = addressToIndex(this._sheet, cell);
|
|
242
240
|
delete this._sheet.cells[idx];
|
|
243
241
|
});
|
|
244
242
|
}
|
|
@@ -253,7 +251,7 @@ export class SheetModel {
|
|
|
253
251
|
for (const change of changes) {
|
|
254
252
|
if (change instanceof ExportedCellChange) {
|
|
255
253
|
const { address, newValue } = change;
|
|
256
|
-
const idx = this.
|
|
254
|
+
const idx = addressToIndex(this._sheet, { row: address.row, column: address.col });
|
|
257
255
|
this._sheet.cells[idx] = { value: newValue };
|
|
258
256
|
}
|
|
259
257
|
}
|
|
@@ -279,7 +277,7 @@ export class SheetModel {
|
|
|
279
277
|
* Get value from sheet.
|
|
280
278
|
*/
|
|
281
279
|
getCellValue(cell: CellAddress): CellScalarValue {
|
|
282
|
-
const idx = this.
|
|
280
|
+
const idx = addressToIndex(this._sheet, cell);
|
|
283
281
|
return this._sheet.cells[idx]?.value ?? null;
|
|
284
282
|
}
|
|
285
283
|
|
|
@@ -357,7 +355,7 @@ export class SheetModel {
|
|
|
357
355
|
]);
|
|
358
356
|
|
|
359
357
|
// Insert into sheet.
|
|
360
|
-
const idx = this.
|
|
358
|
+
const idx = addressToIndex(this._sheet, cell);
|
|
361
359
|
if (value === undefined || value === null) {
|
|
362
360
|
delete this._sheet.cells[idx];
|
|
363
361
|
} else {
|
|
@@ -427,39 +425,6 @@ export class SheetModel {
|
|
|
427
425
|
// Indices.
|
|
428
426
|
//
|
|
429
427
|
|
|
430
|
-
/**
|
|
431
|
-
* E.g., "A1" => "x1@y1".
|
|
432
|
-
*/
|
|
433
|
-
addressToIndex(cell: CellAddress): CellIndex {
|
|
434
|
-
return `${this._sheet.columns[cell.column]}@${this._sheet.rows[cell.row]}`;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* E.g., "x1@y1" => "A1".
|
|
439
|
-
*/
|
|
440
|
-
addressFromIndex(idx: CellIndex): CellAddress {
|
|
441
|
-
const [column, row] = idx.split('@');
|
|
442
|
-
return {
|
|
443
|
-
column: this._sheet.columns.indexOf(column),
|
|
444
|
-
row: this._sheet.rows.indexOf(row),
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* E.g., "A1:B2" => "x1@y1:x2@y2".
|
|
450
|
-
*/
|
|
451
|
-
rangeToIndex(range: CellRange): string {
|
|
452
|
-
return [range.from, range.to ?? range.from].map((cell) => this.addressToIndex(cell)).join(':');
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* E.g., "x1@y1:x2@y2" => "A1:B2".
|
|
457
|
-
*/
|
|
458
|
-
rangeFromIndex(idx: string): CellRange {
|
|
459
|
-
const [from, to] = idx.split(':').map((idx) => this.addressFromIndex(idx));
|
|
460
|
-
return { from, to };
|
|
461
|
-
}
|
|
462
|
-
|
|
463
428
|
/**
|
|
464
429
|
* E.g., "HELLO()" => "EDGE("HELLO")".
|
|
465
430
|
*/
|
|
@@ -509,7 +474,7 @@ export class SheetModel {
|
|
|
509
474
|
mapFormulaRefsToIndices(formula: string): string {
|
|
510
475
|
invariant(formula.charAt(0) === '=');
|
|
511
476
|
return formula.replace(/([a-zA-Z]+)([0-9]+)/g, (match) => {
|
|
512
|
-
return this.
|
|
477
|
+
return addressToIndex(this._sheet, addressFromA1Notation(match));
|
|
513
478
|
});
|
|
514
479
|
}
|
|
515
480
|
|
|
@@ -519,7 +484,7 @@ export class SheetModel {
|
|
|
519
484
|
mapFormulaIndicesToRefs(formula: string): string {
|
|
520
485
|
invariant(formula.charAt(0) === '=');
|
|
521
486
|
return formula.replace(/([a-zA-Z0-9]+)@([a-zA-Z0-9]+)/g, (idx) => {
|
|
522
|
-
return addressToA1Notation(this.
|
|
487
|
+
return addressToA1Notation(addressFromIndex(this._sheet, idx));
|
|
523
488
|
});
|
|
524
489
|
}
|
|
525
490
|
|
package/src/model/types.test.ts
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { getIndices, sortByIndex, getIndicesBelow, getIndicesAbove, getIndicesBetween } from '@tldraw/indices';
|
|
6
|
-
import { expect } from '
|
|
7
|
-
import { describe, test } from 'vitest';
|
|
6
|
+
import { describe, expect, test } from 'vitest';
|
|
8
7
|
|
|
9
8
|
import { inRange, addressFromA1Notation, addressToA1Notation, rangeFromA1Notation, rangeToA1Notation } from './types';
|
|
10
9
|
|