@dxos/plugin-sheet 0.6.14-main.8b352a0 → 0.6.14-staging.3e2eaca
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-R65IDJHN.mjs → SheetContainer-P3NF5KEI.mjs} +49 -43
- package/dist/lib/browser/SheetContainer-P3NF5KEI.mjs.map +7 -0
- package/dist/lib/browser/{chunk-I2DKJ72A.mjs → chunk-4LKIURJA.mjs} +283 -84
- package/dist/lib/browser/chunk-4LKIURJA.mjs.map +7 -0
- package/dist/lib/browser/{chunk-5KVQ5IPW.mjs → chunk-BVUN7SHF.mjs} +4 -2
- package/dist/lib/browser/chunk-BVUN7SHF.mjs.map +7 -0
- package/dist/lib/browser/{chunk-D3QTX46O.mjs → chunk-RABELMEQ.mjs} +3 -2
- package/dist/lib/browser/{chunk-D3QTX46O.mjs.map → chunk-RABELMEQ.mjs.map} +3 -3
- package/dist/lib/browser/{chunk-KCYJSOFB.mjs → chunk-VMSX6Z4X.mjs} +51 -17
- package/dist/lib/browser/chunk-VMSX6Z4X.mjs.map +7 -0
- package/dist/lib/browser/{compute-graph-SJT67236.mjs → compute-graph-GGWUX644.mjs} +4 -4
- package/dist/lib/browser/index.mjs +38 -10
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/meta.mjs +1 -1
- package/dist/lib/browser/types.mjs +2 -2
- package/dist/lib/node/{SheetContainer-6BO4C5X2.cjs → SheetContainer-MPFKXY26.cjs} +67 -62
- package/dist/lib/node/SheetContainer-MPFKXY26.cjs.map +7 -0
- package/dist/lib/node/{chunk-QIFIGEKV.cjs → chunk-2ZVZI2KJ.cjs} +6 -5
- package/dist/lib/node/{chunk-QIFIGEKV.cjs.map → chunk-2ZVZI2KJ.cjs.map} +3 -3
- package/dist/lib/node/{chunk-2XJ5I4UF.cjs → chunk-AWKOWDMI.cjs} +8 -6
- package/dist/lib/node/chunk-AWKOWDMI.cjs.map +7 -0
- package/dist/lib/node/{chunk-JF5XNTF3.cjs → chunk-O7XR4R7Y.cjs} +61 -26
- package/dist/lib/node/chunk-O7XR4R7Y.cjs.map +7 -0
- package/dist/lib/node/{chunk-DEPJHN47.cjs → chunk-OO24XJBV.cjs} +318 -120
- package/dist/lib/node/chunk-OO24XJBV.cjs.map +7 -0
- package/dist/lib/node/{compute-graph-AQBDL7HO.cjs → compute-graph-KGWA2QLE.cjs} +21 -21
- package/dist/lib/node/{compute-graph-AQBDL7HO.cjs.map → compute-graph-KGWA2QLE.cjs.map} +2 -2
- package/dist/lib/node/index.cjs +66 -38
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.cjs +3 -3
- package/dist/lib/node/meta.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/types.cjs +7 -7
- package/dist/lib/node/types.cjs.map +1 -1
- package/dist/lib/node-esm/{SheetContainer-MJXC5E3P.mjs → SheetContainer-22IOAW3B.mjs} +49 -43
- package/dist/lib/node-esm/SheetContainer-22IOAW3B.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-VCYJWE3O.mjs → chunk-BM2Q3FFC.mjs} +3 -2
- package/dist/lib/node-esm/{chunk-VCYJWE3O.mjs.map → chunk-BM2Q3FFC.mjs.map} +3 -3
- package/dist/lib/node-esm/{chunk-25V7WY4R.mjs → chunk-BW36PM2Y.mjs} +283 -84
- package/dist/lib/node-esm/chunk-BW36PM2Y.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-XBEHKYO7.mjs → chunk-CR4K75EL.mjs} +51 -17
- package/dist/lib/node-esm/chunk-CR4K75EL.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-5TXLF6PL.mjs → chunk-UIBWRHW7.mjs} +4 -2
- package/dist/lib/node-esm/chunk-UIBWRHW7.mjs.map +7 -0
- package/dist/lib/node-esm/{compute-graph-FRCKXEYK.mjs → compute-graph-2SCZT7N5.mjs} +4 -4
- package/dist/lib/node-esm/index.mjs +38 -10
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/meta.mjs +1 -1
- package/dist/lib/node-esm/types.mjs +2 -2
- package/dist/types/src/SheetPlugin.d.ts.map +1 -1
- package/dist/types/src/components/FunctionEditor/FunctionEditor.d.ts.map +1 -1
- package/dist/types/src/components/GridSheet/GridSheet.d.ts.map +1 -1
- package/dist/types/src/components/GridSheet/util.d.ts +1 -0
- package/dist/types/src/components/GridSheet/util.d.ts.map +1 -1
- package/dist/types/src/components/SheetContainer/SheetContainer.d.ts.map +1 -1
- package/dist/types/src/components/SheetContainer/SheetContainer.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +5 -2
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/compute-graph/compute-graph.d.ts +2 -2
- package/dist/types/src/compute-graph/compute-graph.d.ts.map +1 -1
- package/dist/types/src/compute-graph/functions/async-function.d.ts.map +1 -1
- package/dist/types/src/compute-graph/functions/edge-function.d.ts.map +1 -1
- package/dist/types/src/defs/sheet-range-types.d.ts +1 -1
- package/dist/types/src/defs/sheet-range-types.d.ts.map +1 -1
- package/dist/types/src/defs/util.d.ts +1 -1
- package/dist/types/src/defs/util.d.ts.map +1 -1
- package/dist/types/src/meta.d.ts +1 -0
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/model/sheet-model.d.ts +12 -5
- package/dist/types/src/model/sheet-model.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +10 -5
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +33 -3
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +37 -36
- package/src/SheetPlugin.tsx +20 -0
- package/src/components/FunctionEditor/FunctionEditor.tsx +1 -5
- package/src/components/GridSheet/GridSheet.tsx +102 -38
- package/src/components/GridSheet/util.ts +19 -9
- package/src/components/SheetContainer/SheetContainer.stories.tsx +2 -0
- package/src/components/SheetContainer/SheetContainer.tsx +9 -8
- package/src/components/Toolbar/Toolbar.tsx +62 -53
- package/src/compute-graph/compute-graph.ts +22 -7
- package/src/compute-graph/functions/async-function.ts +1 -0
- package/src/compute-graph/functions/edge-function.ts +5 -3
- package/src/defs/sheet-range-types.ts +4 -2
- package/src/defs/util.ts +1 -0
- package/src/meta.ts +1 -0
- package/src/model/sheet-model.test.ts +5 -3
- package/src/model/sheet-model.ts +93 -21
- package/src/translations.ts +10 -5
- package/src/types.ts +20 -0
- package/dist/lib/browser/SheetContainer-R65IDJHN.mjs.map +0 -7
- package/dist/lib/browser/chunk-5KVQ5IPW.mjs.map +0 -7
- package/dist/lib/browser/chunk-I2DKJ72A.mjs.map +0 -7
- package/dist/lib/browser/chunk-KCYJSOFB.mjs.map +0 -7
- package/dist/lib/node/SheetContainer-6BO4C5X2.cjs.map +0 -7
- package/dist/lib/node/chunk-2XJ5I4UF.cjs.map +0 -7
- package/dist/lib/node/chunk-DEPJHN47.cjs.map +0 -7
- package/dist/lib/node/chunk-JF5XNTF3.cjs.map +0 -7
- package/dist/lib/node-esm/SheetContainer-MJXC5E3P.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-25V7WY4R.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-5TXLF6PL.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-XBEHKYO7.mjs.map +0 -7
- /package/dist/lib/browser/{compute-graph-SJT67236.mjs.map → compute-graph-GGWUX644.mjs.map} +0 -0
- /package/dist/lib/node-esm/{compute-graph-FRCKXEYK.mjs.map → compute-graph-2SCZT7N5.mjs.map} +0 -0
|
@@ -11,6 +11,7 @@ import { withTheme, withLayout } from '@dxos/storybook-utils';
|
|
|
11
11
|
|
|
12
12
|
import { SheetContainer } from './SheetContainer';
|
|
13
13
|
import { createTestCells, useTestSheet, withComputeGraphDecorator } from '../../testing';
|
|
14
|
+
import translations from '../../translations';
|
|
14
15
|
import { SheetType } from '../../types';
|
|
15
16
|
import { useComputeGraph } from '../ComputeGraph';
|
|
16
17
|
|
|
@@ -38,6 +39,7 @@ const meta: Meta = {
|
|
|
38
39
|
classNames: 'grid grid-cols-1 grid-rows-[min-content_minmax(0,1fr)_min-content]',
|
|
39
40
|
}),
|
|
40
41
|
],
|
|
42
|
+
parameters: { translations },
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
export default meta;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
7
|
import { type Space } from '@dxos/react-client/echo';
|
|
8
|
+
import { StackItem } from '@dxos/react-ui-stack';
|
|
8
9
|
|
|
9
10
|
import { type SheetType } from '../../types';
|
|
10
11
|
import { useComputeGraph } from '../ComputeGraph';
|
|
@@ -18,16 +19,16 @@ export const SheetContainer = ({ space, sheet, role }: { space: Space; sheet: Sh
|
|
|
18
19
|
|
|
19
20
|
return graph ? (
|
|
20
21
|
<SheetProvider sheet={sheet} graph={graph}>
|
|
21
|
-
<
|
|
22
|
-
<Toolbar.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
<StackItem.Content toolbar statusbar classNames='border-bs border-separator'>
|
|
23
|
+
<Toolbar.Root role={role}>
|
|
24
|
+
<Toolbar.Styles />
|
|
25
|
+
<Toolbar.Alignment />
|
|
26
|
+
<Toolbar.Separator />
|
|
27
|
+
<Toolbar.Actions />
|
|
28
|
+
</Toolbar.Root>
|
|
28
29
|
<GridSheet />
|
|
29
30
|
<FunctionEditor />
|
|
30
|
-
</
|
|
31
|
+
</StackItem.Content>
|
|
31
32
|
</SheetProvider>
|
|
32
33
|
) : null;
|
|
33
34
|
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { createContext } from '@radix-ui/react-context';
|
|
6
|
-
import React, { type PropsWithChildren, useCallback
|
|
6
|
+
import React, { type PropsWithChildren, useCallback } from 'react';
|
|
7
7
|
|
|
8
8
|
import { useIntentDispatcher } from '@dxos/app-framework';
|
|
9
9
|
import {
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
inRange,
|
|
29
29
|
rangeFromIndex,
|
|
30
30
|
rangeToIndex,
|
|
31
|
+
styleKey,
|
|
31
32
|
type StyleKey,
|
|
32
33
|
type StyleValue,
|
|
33
34
|
} from '../../defs';
|
|
@@ -89,10 +90,11 @@ type CommentAction = { key: CommentKey; value: CommentValue; cellContent?: strin
|
|
|
89
90
|
type StyleAction = { key: StyleKey; value: StyleValue };
|
|
90
91
|
|
|
91
92
|
export type ToolbarAction = StyleAction | AlignAction | CommentAction;
|
|
93
|
+
export type ToolbarActionAnnotated = ToolbarAction & { unset?: boolean };
|
|
92
94
|
|
|
93
95
|
export type ToolbarActionType = ToolbarAction['key'];
|
|
94
96
|
|
|
95
|
-
export type ToolbarActionHandler = (action:
|
|
97
|
+
export type ToolbarActionHandler = (action: ToolbarActionAnnotated) => void;
|
|
96
98
|
|
|
97
99
|
export type ToolbarProps = ThemedClassName<
|
|
98
100
|
PropsWithChildren<{
|
|
@@ -100,13 +102,9 @@ export type ToolbarProps = ThemedClassName<
|
|
|
100
102
|
}>
|
|
101
103
|
>;
|
|
102
104
|
|
|
103
|
-
const [ToolbarContextProvider, useToolbarContext] = createContext<{
|
|
104
|
-
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
// TODO(Zan): Factor out, copied this from MarkdownPlugin.
|
|
108
|
-
const sectionToolbarLayout =
|
|
109
|
-
'bs-[--rail-action] bg-[--sticky-bg] sticky block-start-0 __-block-start-px transition-opacity';
|
|
105
|
+
const [ToolbarContextProvider, useToolbarContext] = createContext<{
|
|
106
|
+
onAction: (action: ToolbarActionAnnotated) => void;
|
|
107
|
+
}>('Toolbar');
|
|
110
108
|
|
|
111
109
|
type Range = SheetType['ranges'][number];
|
|
112
110
|
|
|
@@ -117,13 +115,16 @@ const ToolbarRoot = ({ children, role, classNames }: ToolbarProps) => {
|
|
|
117
115
|
|
|
118
116
|
// TODO(Zan): Externalize the toolbar action handler. E.g., Toolbar/keys should both fire events.
|
|
119
117
|
const handleAction = useCallback(
|
|
120
|
-
(action:
|
|
118
|
+
(action: ToolbarActionAnnotated) => {
|
|
121
119
|
switch (action.key) {
|
|
122
120
|
case 'alignment':
|
|
123
|
-
if (
|
|
124
|
-
const index =
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
if (cursorFallbackRange) {
|
|
122
|
+
const index =
|
|
123
|
+
model.sheet.ranges?.findIndex(
|
|
124
|
+
(range) =>
|
|
125
|
+
range.key === action.key &&
|
|
126
|
+
inRange(rangeFromIndex(model.sheet, range.range), cursorFallbackRange.from),
|
|
127
|
+
) ?? -1;
|
|
127
128
|
const nextRangeEntity = {
|
|
128
129
|
range: rangeToIndex(model.sheet, cursorFallbackRange),
|
|
129
130
|
key: action.key,
|
|
@@ -131,14 +132,21 @@ const ToolbarRoot = ({ children, role, classNames }: ToolbarProps) => {
|
|
|
131
132
|
};
|
|
132
133
|
if (index < 0) {
|
|
133
134
|
model.sheet.ranges?.push(nextRangeEntity);
|
|
135
|
+
} else if (model.sheet.ranges![index].value === action.value) {
|
|
136
|
+
model.sheet.ranges?.splice(index, 1);
|
|
134
137
|
} else {
|
|
135
138
|
model.sheet.ranges?.splice(index, 1, nextRangeEntity);
|
|
136
139
|
}
|
|
137
140
|
}
|
|
138
141
|
break;
|
|
139
142
|
case 'style':
|
|
140
|
-
if (action.
|
|
141
|
-
const index = model.sheet.ranges?.findIndex(
|
|
143
|
+
if (action.unset) {
|
|
144
|
+
const index = model.sheet.ranges?.findIndex(
|
|
145
|
+
(range) =>
|
|
146
|
+
range.key === action.key &&
|
|
147
|
+
cursorFallbackRange &&
|
|
148
|
+
inRange(rangeFromIndex(model.sheet, range.range), cursorFallbackRange.from),
|
|
149
|
+
);
|
|
142
150
|
if (index >= 0) {
|
|
143
151
|
model.sheet.ranges?.splice(index, 1);
|
|
144
152
|
}
|
|
@@ -170,14 +178,7 @@ const ToolbarRoot = ({ children, role, classNames }: ToolbarProps) => {
|
|
|
170
178
|
|
|
171
179
|
return (
|
|
172
180
|
<ToolbarContextProvider onAction={handleAction}>
|
|
173
|
-
<NaturalToolbar.Root
|
|
174
|
-
classNames={[
|
|
175
|
-
...(role === 'section'
|
|
176
|
-
? ['z-[2] group-focus-within/section:visible', !hasAttention && 'invisible', sectionToolbarLayout]
|
|
177
|
-
: ['attention-surface']),
|
|
178
|
-
classNames,
|
|
179
|
-
]}
|
|
180
|
-
>
|
|
181
|
+
<NaturalToolbar.Root classNames={['pli-0.5', !hasAttention && 'opacity-20', classNames]}>
|
|
181
182
|
{children}
|
|
182
183
|
</NaturalToolbar.Root>
|
|
183
184
|
</ToolbarContextProvider>
|
|
@@ -207,20 +208,20 @@ const Alignment = () => {
|
|
|
207
208
|
const { onAction } = useToolbarContext('Alignment');
|
|
208
209
|
const { t } = useTranslation(SHEET_PLUGIN);
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
: undefined,
|
|
217
|
-
[cursor, model.sheet.ranges],
|
|
218
|
-
);
|
|
211
|
+
// TODO(thure): Can this O(n) call be memoized?
|
|
212
|
+
const value = cursor
|
|
213
|
+
? model.sheet.ranges?.findLast(
|
|
214
|
+
({ range, key }) => key === alignKey && inRange(rangeFromIndex(model.sheet, range), cursor),
|
|
215
|
+
)?.value
|
|
216
|
+
: undefined;
|
|
219
217
|
|
|
220
218
|
return (
|
|
221
219
|
<NaturalToolbar.ToggleGroup
|
|
222
220
|
type='single'
|
|
223
|
-
value={
|
|
221
|
+
value={
|
|
222
|
+
// TODO(thure): providing `undefined` leaves the last item active which was active rather than showing none.
|
|
223
|
+
value ?? 'never'
|
|
224
|
+
}
|
|
224
225
|
onValueChange={(value: AlignValue) => onAction?.({ key: alignKey, value })}
|
|
225
226
|
>
|
|
226
227
|
{alignmentOptions.map(({ value, icon }) => (
|
|
@@ -235,25 +236,27 @@ const Alignment = () => {
|
|
|
235
236
|
);
|
|
236
237
|
};
|
|
237
238
|
|
|
238
|
-
const styleOptions: ButtonProps<StyleValue>[] = [
|
|
239
|
+
const styleOptions: ButtonProps<StyleValue>[] = [
|
|
240
|
+
{ value: 'highlight', icon: 'ph--highlighter--regular' },
|
|
241
|
+
{ value: 'softwrap', icon: 'ph--paragraph--regular' },
|
|
242
|
+
];
|
|
239
243
|
|
|
240
244
|
const Styles = () => {
|
|
241
|
-
const {
|
|
245
|
+
const { cursorFallbackRange, model } = useSheetContext();
|
|
242
246
|
const { onAction } = useToolbarContext('Styles');
|
|
243
247
|
const { t } = useTranslation(SHEET_PLUGIN);
|
|
244
248
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
);
|
|
249
|
+
// TODO(thure): Can this O(n) call be memoized?
|
|
250
|
+
const activeValues = cursorFallbackRange
|
|
251
|
+
? model.sheet.ranges
|
|
252
|
+
?.filter(
|
|
253
|
+
({ range, key }) => key === 'style' && inRange(rangeFromIndex(model.sheet, range), cursorFallbackRange.from),
|
|
254
|
+
)
|
|
255
|
+
.reduce((acc, { value }) => {
|
|
256
|
+
acc.add(value);
|
|
257
|
+
return acc;
|
|
258
|
+
}, new Set())
|
|
259
|
+
: undefined;
|
|
257
260
|
|
|
258
261
|
return (
|
|
259
262
|
<>
|
|
@@ -262,10 +265,15 @@ const Styles = () => {
|
|
|
262
265
|
itemType='toggle'
|
|
263
266
|
key={value}
|
|
264
267
|
pressed={activeValues?.has(value)}
|
|
265
|
-
onPressedChange={(nextPressed: boolean) =>
|
|
268
|
+
onPressedChange={(nextPressed: boolean) => {
|
|
269
|
+
onAction?.({ key: 'style', value, unset: !nextPressed });
|
|
270
|
+
}}
|
|
266
271
|
icon={icon}
|
|
267
272
|
>
|
|
268
|
-
{t(
|
|
273
|
+
{t('toolbar action label', {
|
|
274
|
+
key: t(`range key ${styleKey} label`),
|
|
275
|
+
value: t(`range value ${value} label`),
|
|
276
|
+
})}
|
|
269
277
|
</ToolbarItem>
|
|
270
278
|
))}
|
|
271
279
|
</>
|
|
@@ -281,6 +289,7 @@ const Actions = () => {
|
|
|
281
289
|
const { cursorFallbackRange, cursor, model } = useSheetContext();
|
|
282
290
|
const { t } = useTranslation(SHEET_PLUGIN);
|
|
283
291
|
|
|
292
|
+
// TODO(thure): Can this O(n) call be memoized?
|
|
284
293
|
const overlapsCommentAnchor = (model.sheet.threads ?? [])
|
|
285
294
|
.filter(nonNullable)
|
|
286
295
|
.filter((thread) => thread.status !== 'resolved')
|
|
@@ -304,16 +313,16 @@ const Actions = () => {
|
|
|
304
313
|
icon='ph--chat-text--regular'
|
|
305
314
|
data-testid='editor.toolbar.comment'
|
|
306
315
|
onClick={() => {
|
|
307
|
-
if (!
|
|
316
|
+
if (!cursorFallbackRange) {
|
|
308
317
|
return;
|
|
309
318
|
}
|
|
310
319
|
return onAction?.({
|
|
311
320
|
key: 'comment',
|
|
312
321
|
value: rangeToIndex(model.sheet, cursorFallbackRange),
|
|
313
|
-
cellContent: model.getCellText(
|
|
322
|
+
cellContent: model.getCellText(cursorFallbackRange.from),
|
|
314
323
|
});
|
|
315
324
|
}}
|
|
316
|
-
disabled={!
|
|
325
|
+
disabled={!cursorFallbackRange || overlapsCommentAnchor}
|
|
317
326
|
>
|
|
318
327
|
{t(tooltipLabelKey)}
|
|
319
328
|
</ToolbarItem>
|
|
@@ -6,6 +6,7 @@ import { type Listeners } from 'hyperformula/typings/Emitter';
|
|
|
6
6
|
|
|
7
7
|
import { Event } from '@dxos/async';
|
|
8
8
|
import { type Space, Filter, fullyQualifiedId } from '@dxos/client/echo';
|
|
9
|
+
import { FQ_ID_LENGTH } from '@dxos/client/echo';
|
|
9
10
|
import { Resource } from '@dxos/context';
|
|
10
11
|
import { getTypename } from '@dxos/echo-schema';
|
|
11
12
|
import { invariant } from '@dxos/invariant';
|
|
@@ -26,8 +27,7 @@ import {
|
|
|
26
27
|
|
|
27
28
|
// TODO(burdon): Factor out compute-graph.
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
const OBJECT_ID_LENGTH = 60; // 33 (space id) + 1 (separator) + 26 (object id).
|
|
30
|
+
const UNKNOWN_BINDING = '__UNKNOWN__';
|
|
31
31
|
|
|
32
32
|
// TODO(burdon): Factory.
|
|
33
33
|
// export type ComputeNodeGenerator = <T>(obj: T) => ComputeNode;
|
|
@@ -44,7 +44,7 @@ export const parseSheetName = (name: string): Partial<ObjectRef> => {
|
|
|
44
44
|
return id ? { type, id } : { id: type };
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
export type ComputeGraphEvent = 'functionsUpdated';
|
|
47
|
+
export type ComputeGraphEvent = 'functionsUpdated' | 'valuesUpdated';
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Per-space compute and dependency graph.
|
|
@@ -64,7 +64,7 @@ export class ComputeGraph extends Resource {
|
|
|
64
64
|
public readonly update = new Event<{ type: ComputeGraphEvent }>();
|
|
65
65
|
|
|
66
66
|
// The context is passed to all functions.
|
|
67
|
-
public readonly context
|
|
67
|
+
public readonly context: FunctionContext;
|
|
68
68
|
|
|
69
69
|
constructor(
|
|
70
70
|
private readonly _hf: HyperFormula,
|
|
@@ -72,6 +72,15 @@ export class ComputeGraph extends Resource {
|
|
|
72
72
|
private readonly _options?: Partial<FunctionContextOptions>,
|
|
73
73
|
) {
|
|
74
74
|
super();
|
|
75
|
+
|
|
76
|
+
const contextOptions = {
|
|
77
|
+
...this._options,
|
|
78
|
+
onUpdate: (update) => {
|
|
79
|
+
this._options?.onUpdate?.(update);
|
|
80
|
+
this.update.emit({ type: 'valuesUpdated' });
|
|
81
|
+
},
|
|
82
|
+
} satisfies Partial<FunctionContextOptions>;
|
|
83
|
+
this.context = new FunctionContext(this._hf, this._space, contextOptions);
|
|
75
84
|
this._hf.updateConfig({ context: this.context });
|
|
76
85
|
|
|
77
86
|
// TODO(burdon): If debounce then aggregate changes.
|
|
@@ -209,9 +218,9 @@ export class ComputeGraph extends Resource {
|
|
|
209
218
|
* E.g., spaceId:objectId() => HELLO()
|
|
210
219
|
*/
|
|
211
220
|
mapFunctionBindingFromId(formula: string) {
|
|
212
|
-
|
|
221
|
+
const binding = formula.replace(/(\w+):([a-zA-Z0-9]+)\((.*)\)/g, (match, spaceId, objectId, args) => {
|
|
213
222
|
const id = `${spaceId}:${objectId}`;
|
|
214
|
-
if (id.length !==
|
|
223
|
+
if (id.length !== FQ_ID_LENGTH) {
|
|
215
224
|
return match;
|
|
216
225
|
}
|
|
217
226
|
|
|
@@ -219,9 +228,15 @@ export class ComputeGraph extends Resource {
|
|
|
219
228
|
if (fn?.binding) {
|
|
220
229
|
return `${fn.binding}(${args})`;
|
|
221
230
|
} else {
|
|
222
|
-
return
|
|
231
|
+
return UNKNOWN_BINDING;
|
|
223
232
|
}
|
|
224
233
|
});
|
|
234
|
+
|
|
235
|
+
if (binding.startsWith(`=${UNKNOWN_BINDING}`)) {
|
|
236
|
+
return undefined;
|
|
237
|
+
} else {
|
|
238
|
+
return binding;
|
|
239
|
+
}
|
|
225
240
|
}
|
|
226
241
|
|
|
227
242
|
protected override async _open() {
|
|
@@ -83,6 +83,7 @@ export class FunctionContext {
|
|
|
83
83
|
) {
|
|
84
84
|
this._options = defaultsDeep(_options ?? {}, defaultFunctionContextOptions);
|
|
85
85
|
this._onUpdate = debounce((update) => {
|
|
86
|
+
log('update', update);
|
|
86
87
|
// TODO(burdon): Better way to trigger recalculation? (NOTE: rebuildAndRecalculate resets the undo history.)
|
|
87
88
|
this._hf.resumeEvaluation();
|
|
88
89
|
this._options.onUpdate?.(update);
|
|
@@ -44,7 +44,7 @@ export class EdgeFunctionPlugin extends AsyncFunctionPlugin {
|
|
|
44
44
|
|
|
45
45
|
if (subscribe) {
|
|
46
46
|
const unsubscribe = effect(() => {
|
|
47
|
-
log
|
|
47
|
+
log('function changed', { fn });
|
|
48
48
|
const _ = fn?.version;
|
|
49
49
|
|
|
50
50
|
// TODO(wittjosiah): `ttl` should be 0 to force a recalculation when a new version is deployed.
|
|
@@ -56,13 +56,15 @@ export class EdgeFunctionPlugin extends AsyncFunctionPlugin {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
const path = getUserFunctionUrlInMetadata(getMeta(fn));
|
|
59
|
-
const
|
|
59
|
+
const response = await fetch(`${this.context.remoteFunctionUrl}${path}`, {
|
|
60
60
|
method: 'POST',
|
|
61
61
|
headers: { 'Content-Type': 'application/json' },
|
|
62
62
|
body: JSON.stringify({ args: args.filter(nonNullable) }),
|
|
63
63
|
});
|
|
64
|
+
const result = await response.text();
|
|
65
|
+
log('function executed', { result });
|
|
64
66
|
|
|
65
|
-
return
|
|
67
|
+
return result;
|
|
66
68
|
};
|
|
67
69
|
|
|
68
70
|
return this.runAsyncFunction(ast, state, handler(true), { ttl: FUNCTION_TTL });
|
|
@@ -16,7 +16,7 @@ export type CommentValue = string;
|
|
|
16
16
|
|
|
17
17
|
export const styleKey = 'style';
|
|
18
18
|
export type StyleKey = typeof styleKey;
|
|
19
|
-
export type StyleValue = 'highlight' | '
|
|
19
|
+
export type StyleValue = 'highlight' | 'softwrap';
|
|
20
20
|
|
|
21
21
|
// TODO(burdon): Reconcile with plugin-table.
|
|
22
22
|
export const cellClassNameForRange = ({ key, value }: SheetType['ranges'][number]): ClassNameValue => {
|
|
@@ -37,7 +37,9 @@ export const cellClassNameForRange = ({ key, value }: SheetType['ranges'][number
|
|
|
37
37
|
case styleKey:
|
|
38
38
|
switch (value) {
|
|
39
39
|
case 'highlight':
|
|
40
|
-
return 'bg-gridHighlight';
|
|
40
|
+
return '!bg-gridHighlight';
|
|
41
|
+
case 'softwrap':
|
|
42
|
+
return '!whitespace-normal';
|
|
41
43
|
default:
|
|
42
44
|
return undefined;
|
|
43
45
|
}
|
package/src/defs/util.ts
CHANGED
package/src/meta.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, onTestFinished, test } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { Trigger } from '@dxos/async';
|
|
8
8
|
import { FunctionType } from '@dxos/plugin-script/types';
|
|
@@ -36,13 +36,15 @@ describe('SheetModel', () => {
|
|
|
36
36
|
// Trigger waits for function invocation.
|
|
37
37
|
const trigger = new Trigger<CellScalarValue>();
|
|
38
38
|
model.setValue(addressFromA1Notation('A1'), '=TEST(100)');
|
|
39
|
-
|
|
39
|
+
// TODO(wittjosiah): Currently this fires twice, once for the binding loading & once for the function invocation.
|
|
40
|
+
const unsubscribe = model.update.on((update) => {
|
|
40
41
|
const { type } = update;
|
|
41
42
|
if (type === 'valuesUpdated') {
|
|
42
43
|
const value = model.getValue(addressFromA1Notation('A1'));
|
|
43
|
-
trigger.wake(value);
|
|
44
|
+
value && trigger.wake(value);
|
|
44
45
|
}
|
|
45
46
|
});
|
|
47
|
+
onTestFinished(() => unsubscribe());
|
|
46
48
|
|
|
47
49
|
// Initial value will be null.
|
|
48
50
|
const v1 = model.getValue(addressFromA1Notation('A1'));
|
package/src/model/sheet-model.ts
CHANGED
|
@@ -8,11 +8,10 @@ import { type SimpleDate, type SimpleDateTime } from 'hyperformula/typings/DateT
|
|
|
8
8
|
|
|
9
9
|
import { Event } from '@dxos/async';
|
|
10
10
|
import { Resource } from '@dxos/context';
|
|
11
|
-
import { getTypename } from '@dxos/echo-schema';
|
|
11
|
+
import { getTypename, FormatEnum, TypeEnum } from '@dxos/echo-schema';
|
|
12
12
|
import { invariant } from '@dxos/invariant';
|
|
13
13
|
import { PublicKey } from '@dxos/keys';
|
|
14
14
|
import { log } from '@dxos/log';
|
|
15
|
-
import { FieldValueType } from '@dxos/schema';
|
|
16
15
|
|
|
17
16
|
import { DetailedCellError, ExportedCellChange } from '#hyperformula';
|
|
18
17
|
import { type ComputeGraph, type ComputeNode, type ComputeNodeEvent, createSheetName } from '../compute-graph';
|
|
@@ -21,26 +20,29 @@ import {
|
|
|
21
20
|
addressFromIndex,
|
|
22
21
|
addressToA1Notation,
|
|
23
22
|
addressToIndex,
|
|
24
|
-
type CellAddress,
|
|
25
|
-
type CellRange,
|
|
26
23
|
initialize,
|
|
27
24
|
insertIndices,
|
|
28
25
|
isFormula,
|
|
26
|
+
type CellAddress,
|
|
27
|
+
type CellRange,
|
|
28
|
+
ReadonlyException,
|
|
29
29
|
MAX_COLUMNS,
|
|
30
30
|
MAX_ROWS,
|
|
31
|
-
ReadonlyException,
|
|
32
31
|
} from '../defs';
|
|
33
|
-
import { type CellScalarValue, type CellValue, type SheetType } from '../types';
|
|
32
|
+
import { type CellScalarValue, type CellValue, type SheetType, type RestoreAxis } from '../types';
|
|
34
33
|
|
|
35
34
|
// Map sheet types to system types.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
// https://hyperformula.handsontable.com/guide/types-of-values.html
|
|
36
|
+
// - https://github.com/handsontable/hyperformula/blob/master/src/Cell.ts (CellValueType)
|
|
37
|
+
// - https://github.com/handsontable/hyperformula/blob/master/src/interpreter/InterpreterValue.ts (NumberType)
|
|
38
|
+
const typeMap: Record<string, { type: TypeEnum; format?: FormatEnum }> = {
|
|
39
|
+
BOOLEAN: { type: TypeEnum.Boolean },
|
|
40
|
+
NUMBER_RAW: { type: TypeEnum.Number },
|
|
41
|
+
NUMBER_PERCENT: { type: TypeEnum.Number, format: FormatEnum.Percent },
|
|
42
|
+
NUMBER_CURRENCY: { type: TypeEnum.Number, format: FormatEnum.Currency },
|
|
43
|
+
NUMBER_DATETIME: { type: TypeEnum.String, format: FormatEnum.DateTime },
|
|
44
|
+
NUMBER_DATE: { type: TypeEnum.String, format: FormatEnum.Date },
|
|
45
|
+
NUMBER_TIME: { type: TypeEnum.String, format: FormatEnum.Time },
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
const getTopLeft = (range: CellRange): CellAddress => {
|
|
@@ -111,6 +113,12 @@ export class SheetModel extends Resource {
|
|
|
111
113
|
log('initialize', { id: this.id });
|
|
112
114
|
initialize(this._sheet);
|
|
113
115
|
|
|
116
|
+
this._graph.update.on((event) => {
|
|
117
|
+
if (event.type === 'functionsUpdated') {
|
|
118
|
+
this.reset();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
114
122
|
// TODO(burdon): SheetModel should extend ComputeNode and be constructed via the graph.
|
|
115
123
|
this._node = this._graph.getOrCreateNode(createSheetName({ type: getTypename(this._sheet)!, id: this._sheet.id }));
|
|
116
124
|
await this._node.open();
|
|
@@ -134,9 +142,14 @@ export class SheetModel extends Resource {
|
|
|
134
142
|
invariant(this._node);
|
|
135
143
|
const { col, row } = addressFromIndex(this._sheet, key);
|
|
136
144
|
if (isFormula(value)) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
145
|
+
const binding = this._graph.mapFunctionBindingFromId(this.mapFormulaIndicesToRefs(value));
|
|
146
|
+
if (binding) {
|
|
147
|
+
value = this._graph.mapFormulaToNative(binding);
|
|
148
|
+
} else {
|
|
149
|
+
// If binding is not found, render the cell as empty.
|
|
150
|
+
// This prevents the cell from momentarily rendering an error while the binding is being loaded.
|
|
151
|
+
value = '';
|
|
152
|
+
}
|
|
140
153
|
}
|
|
141
154
|
|
|
142
155
|
this._node.graph.hf.setCellContents({ sheet: this._node.sheetId, row, col }, value);
|
|
@@ -155,12 +168,68 @@ export class SheetModel extends Resource {
|
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
insertRows(i: number, n = 1) {
|
|
158
|
-
insertIndices(this._sheet.rows, i, n, MAX_ROWS);
|
|
171
|
+
const idx = insertIndices(this._sheet.rows, i, n, MAX_ROWS);
|
|
159
172
|
this.reset();
|
|
173
|
+
return idx;
|
|
160
174
|
}
|
|
161
175
|
|
|
162
176
|
insertColumns(i: number, n = 1) {
|
|
163
|
-
insertIndices(this._sheet.columns, i, n, MAX_COLUMNS);
|
|
177
|
+
const idx = insertIndices(this._sheet.columns, i, n, MAX_COLUMNS);
|
|
178
|
+
this.reset();
|
|
179
|
+
return idx;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
dropRow(rowIndex: string): RestoreAxis {
|
|
183
|
+
const range = {
|
|
184
|
+
from: addressFromIndex(this._sheet, `${this._sheet.columns[0]}@${rowIndex}`),
|
|
185
|
+
to: addressFromIndex(this._sheet, `${this._sheet.columns[this._sheet.columns.length - 1]}@${rowIndex}`),
|
|
186
|
+
};
|
|
187
|
+
const values = this.getCellValues(range).flat();
|
|
188
|
+
const index = this._sheet.rows.indexOf(rowIndex);
|
|
189
|
+
this.clear(range);
|
|
190
|
+
this._sheet.rows.splice(index, 1);
|
|
191
|
+
delete this._sheet.rowMeta[rowIndex];
|
|
192
|
+
this.reset();
|
|
193
|
+
return { axis: 'row', index, axisIndex: rowIndex, axisMeta: this._sheet.rowMeta[rowIndex], values };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
dropColumn(colIndex: string): RestoreAxis {
|
|
197
|
+
const range = {
|
|
198
|
+
from: addressFromIndex(this._sheet, `${colIndex}@${this._sheet.rows[0]}`),
|
|
199
|
+
to: addressFromIndex(this._sheet, `${colIndex}@${this._sheet.rows[this._sheet.rows.length - 1]}`),
|
|
200
|
+
};
|
|
201
|
+
const values = this.getCellValues(range).flat();
|
|
202
|
+
const index = this._sheet.columns.indexOf(colIndex);
|
|
203
|
+
this.clear(range);
|
|
204
|
+
this._sheet.columns.splice(index, 1);
|
|
205
|
+
delete this._sheet.columnMeta[colIndex];
|
|
206
|
+
this.reset();
|
|
207
|
+
return { axis: 'col', index, axisIndex: colIndex, axisMeta: this._sheet.rowMeta[colIndex], values };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
restoreRow({ index, axisIndex, axisMeta, values }: RestoreAxis) {
|
|
211
|
+
this._sheet.rows.splice(index, 0, axisIndex);
|
|
212
|
+
values.forEach((value, col) => {
|
|
213
|
+
if (value) {
|
|
214
|
+
this._sheet.cells[`${this._sheet.columns[col]}@${axisIndex}`] = { value };
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
if (axisMeta) {
|
|
218
|
+
this._sheet.rowMeta[axisIndex] = axisMeta;
|
|
219
|
+
}
|
|
220
|
+
this.reset();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
restoreColumn({ index, axisIndex, axisMeta, values }: RestoreAxis) {
|
|
224
|
+
this._sheet.columns.splice(index, 0, axisIndex);
|
|
225
|
+
values.forEach((value, row) => {
|
|
226
|
+
if (value) {
|
|
227
|
+
this._sheet.cells[`${axisIndex}@${this._sheet.rows[row]}`] = { value };
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
if (axisMeta) {
|
|
231
|
+
this._sheet.columnMeta[axisIndex] = axisMeta;
|
|
232
|
+
}
|
|
164
233
|
this.reset();
|
|
165
234
|
}
|
|
166
235
|
|
|
@@ -265,8 +334,11 @@ export class SheetModel extends Resource {
|
|
|
265
334
|
getValue(cell: CellAddress): CellScalarValue {
|
|
266
335
|
// Applies rounding and post-processing.
|
|
267
336
|
invariant(this._node);
|
|
268
|
-
const
|
|
337
|
+
const address = toSimpleCellAddress(this._node.sheetId, cell);
|
|
338
|
+
const value = this._node.graph.hf.getCellValue(address);
|
|
269
339
|
if (value instanceof DetailedCellError) {
|
|
340
|
+
// TODO(wittjosiah): Error details should be shown in cell `title`.
|
|
341
|
+
log.info('cell error', { cell, error: value });
|
|
270
342
|
return value.toString();
|
|
271
343
|
}
|
|
272
344
|
|
|
@@ -276,7 +348,7 @@ export class SheetModel extends Resource {
|
|
|
276
348
|
/**
|
|
277
349
|
* Get value type.
|
|
278
350
|
*/
|
|
279
|
-
|
|
351
|
+
getValueDescription(cell: CellAddress): { type: TypeEnum; format?: FormatEnum } | undefined {
|
|
280
352
|
invariant(this._node);
|
|
281
353
|
const addr = toSimpleCellAddress(this._node.sheetId, cell);
|
|
282
354
|
const type = this._node.graph.hf.getCellValueDetailedType(addr);
|
package/src/translations.ts
CHANGED
|
@@ -14,10 +14,13 @@ export default [
|
|
|
14
14
|
'create sheet section label': 'Create sheet',
|
|
15
15
|
'cell placeholder': 'Cell value...',
|
|
16
16
|
'range key alignment label': 'Align',
|
|
17
|
-
'range
|
|
18
|
-
'range value
|
|
19
|
-
'range value
|
|
20
|
-
'
|
|
17
|
+
'range key style label': 'Style',
|
|
18
|
+
'range value start label': 'Align left',
|
|
19
|
+
'range value center label': 'Align center',
|
|
20
|
+
'range value end label': 'Align right',
|
|
21
|
+
'range value softwrap label': 'Wrap text',
|
|
22
|
+
'range value highlight label': 'Highlight',
|
|
23
|
+
'toolbar action label': '{{value}}',
|
|
21
24
|
'selection overlaps existing comment label': 'Selected cell already has a comment',
|
|
22
25
|
'comment label': 'Add comment',
|
|
23
26
|
'comment ranges not supported label': 'Commenting on ranges is not yet supported',
|
|
@@ -30,7 +33,9 @@ export default [
|
|
|
30
33
|
'add row after label': 'Add row after',
|
|
31
34
|
'delete row label': 'Delete row',
|
|
32
35
|
'range list heading': 'Ranges',
|
|
33
|
-
'range title': '{{position}} — {{
|
|
36
|
+
'range title': '{{position}} — {{value}}',
|
|
37
|
+
'col dropped label': 'Deleted a column',
|
|
38
|
+
'row dropped label': 'Deleted a row',
|
|
34
39
|
},
|
|
35
40
|
},
|
|
36
41
|
},
|