@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.
Files changed (109) hide show
  1. package/dist/lib/browser/{SheetContainer-R65IDJHN.mjs → SheetContainer-P3NF5KEI.mjs} +49 -43
  2. package/dist/lib/browser/SheetContainer-P3NF5KEI.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-I2DKJ72A.mjs → chunk-4LKIURJA.mjs} +283 -84
  4. package/dist/lib/browser/chunk-4LKIURJA.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-5KVQ5IPW.mjs → chunk-BVUN7SHF.mjs} +4 -2
  6. package/dist/lib/browser/chunk-BVUN7SHF.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-D3QTX46O.mjs → chunk-RABELMEQ.mjs} +3 -2
  8. package/dist/lib/browser/{chunk-D3QTX46O.mjs.map → chunk-RABELMEQ.mjs.map} +3 -3
  9. package/dist/lib/browser/{chunk-KCYJSOFB.mjs → chunk-VMSX6Z4X.mjs} +51 -17
  10. package/dist/lib/browser/chunk-VMSX6Z4X.mjs.map +7 -0
  11. package/dist/lib/browser/{compute-graph-SJT67236.mjs → compute-graph-GGWUX644.mjs} +4 -4
  12. package/dist/lib/browser/index.mjs +38 -10
  13. package/dist/lib/browser/index.mjs.map +3 -3
  14. package/dist/lib/browser/meta.json +1 -1
  15. package/dist/lib/browser/meta.mjs +1 -1
  16. package/dist/lib/browser/types.mjs +2 -2
  17. package/dist/lib/node/{SheetContainer-6BO4C5X2.cjs → SheetContainer-MPFKXY26.cjs} +67 -62
  18. package/dist/lib/node/SheetContainer-MPFKXY26.cjs.map +7 -0
  19. package/dist/lib/node/{chunk-QIFIGEKV.cjs → chunk-2ZVZI2KJ.cjs} +6 -5
  20. package/dist/lib/node/{chunk-QIFIGEKV.cjs.map → chunk-2ZVZI2KJ.cjs.map} +3 -3
  21. package/dist/lib/node/{chunk-2XJ5I4UF.cjs → chunk-AWKOWDMI.cjs} +8 -6
  22. package/dist/lib/node/chunk-AWKOWDMI.cjs.map +7 -0
  23. package/dist/lib/node/{chunk-JF5XNTF3.cjs → chunk-O7XR4R7Y.cjs} +61 -26
  24. package/dist/lib/node/chunk-O7XR4R7Y.cjs.map +7 -0
  25. package/dist/lib/node/{chunk-DEPJHN47.cjs → chunk-OO24XJBV.cjs} +318 -120
  26. package/dist/lib/node/chunk-OO24XJBV.cjs.map +7 -0
  27. package/dist/lib/node/{compute-graph-AQBDL7HO.cjs → compute-graph-KGWA2QLE.cjs} +21 -21
  28. package/dist/lib/node/{compute-graph-AQBDL7HO.cjs.map → compute-graph-KGWA2QLE.cjs.map} +2 -2
  29. package/dist/lib/node/index.cjs +66 -38
  30. package/dist/lib/node/index.cjs.map +3 -3
  31. package/dist/lib/node/meta.cjs +3 -3
  32. package/dist/lib/node/meta.cjs.map +1 -1
  33. package/dist/lib/node/meta.json +1 -1
  34. package/dist/lib/node/types.cjs +7 -7
  35. package/dist/lib/node/types.cjs.map +1 -1
  36. package/dist/lib/node-esm/{SheetContainer-MJXC5E3P.mjs → SheetContainer-22IOAW3B.mjs} +49 -43
  37. package/dist/lib/node-esm/SheetContainer-22IOAW3B.mjs.map +7 -0
  38. package/dist/lib/node-esm/{chunk-VCYJWE3O.mjs → chunk-BM2Q3FFC.mjs} +3 -2
  39. package/dist/lib/node-esm/{chunk-VCYJWE3O.mjs.map → chunk-BM2Q3FFC.mjs.map} +3 -3
  40. package/dist/lib/node-esm/{chunk-25V7WY4R.mjs → chunk-BW36PM2Y.mjs} +283 -84
  41. package/dist/lib/node-esm/chunk-BW36PM2Y.mjs.map +7 -0
  42. package/dist/lib/node-esm/{chunk-XBEHKYO7.mjs → chunk-CR4K75EL.mjs} +51 -17
  43. package/dist/lib/node-esm/chunk-CR4K75EL.mjs.map +7 -0
  44. package/dist/lib/node-esm/{chunk-5TXLF6PL.mjs → chunk-UIBWRHW7.mjs} +4 -2
  45. package/dist/lib/node-esm/chunk-UIBWRHW7.mjs.map +7 -0
  46. package/dist/lib/node-esm/{compute-graph-FRCKXEYK.mjs → compute-graph-2SCZT7N5.mjs} +4 -4
  47. package/dist/lib/node-esm/index.mjs +38 -10
  48. package/dist/lib/node-esm/index.mjs.map +3 -3
  49. package/dist/lib/node-esm/meta.json +1 -1
  50. package/dist/lib/node-esm/meta.mjs +1 -1
  51. package/dist/lib/node-esm/types.mjs +2 -2
  52. package/dist/types/src/SheetPlugin.d.ts.map +1 -1
  53. package/dist/types/src/components/FunctionEditor/FunctionEditor.d.ts.map +1 -1
  54. package/dist/types/src/components/GridSheet/GridSheet.d.ts.map +1 -1
  55. package/dist/types/src/components/GridSheet/util.d.ts +1 -0
  56. package/dist/types/src/components/GridSheet/util.d.ts.map +1 -1
  57. package/dist/types/src/components/SheetContainer/SheetContainer.d.ts.map +1 -1
  58. package/dist/types/src/components/SheetContainer/SheetContainer.stories.d.ts.map +1 -1
  59. package/dist/types/src/components/Toolbar/Toolbar.d.ts +5 -2
  60. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  61. package/dist/types/src/components/index.d.ts.map +1 -1
  62. package/dist/types/src/compute-graph/compute-graph.d.ts +2 -2
  63. package/dist/types/src/compute-graph/compute-graph.d.ts.map +1 -1
  64. package/dist/types/src/compute-graph/functions/async-function.d.ts.map +1 -1
  65. package/dist/types/src/compute-graph/functions/edge-function.d.ts.map +1 -1
  66. package/dist/types/src/defs/sheet-range-types.d.ts +1 -1
  67. package/dist/types/src/defs/sheet-range-types.d.ts.map +1 -1
  68. package/dist/types/src/defs/util.d.ts +1 -1
  69. package/dist/types/src/defs/util.d.ts.map +1 -1
  70. package/dist/types/src/meta.d.ts +1 -0
  71. package/dist/types/src/meta.d.ts.map +1 -1
  72. package/dist/types/src/model/sheet-model.d.ts +12 -5
  73. package/dist/types/src/model/sheet-model.d.ts.map +1 -1
  74. package/dist/types/src/translations.d.ts +10 -5
  75. package/dist/types/src/translations.d.ts.map +1 -1
  76. package/dist/types/src/types.d.ts +33 -3
  77. package/dist/types/src/types.d.ts.map +1 -1
  78. package/package.json +37 -36
  79. package/src/SheetPlugin.tsx +20 -0
  80. package/src/components/FunctionEditor/FunctionEditor.tsx +1 -5
  81. package/src/components/GridSheet/GridSheet.tsx +102 -38
  82. package/src/components/GridSheet/util.ts +19 -9
  83. package/src/components/SheetContainer/SheetContainer.stories.tsx +2 -0
  84. package/src/components/SheetContainer/SheetContainer.tsx +9 -8
  85. package/src/components/Toolbar/Toolbar.tsx +62 -53
  86. package/src/compute-graph/compute-graph.ts +22 -7
  87. package/src/compute-graph/functions/async-function.ts +1 -0
  88. package/src/compute-graph/functions/edge-function.ts +5 -3
  89. package/src/defs/sheet-range-types.ts +4 -2
  90. package/src/defs/util.ts +1 -0
  91. package/src/meta.ts +1 -0
  92. package/src/model/sheet-model.test.ts +5 -3
  93. package/src/model/sheet-model.ts +93 -21
  94. package/src/translations.ts +10 -5
  95. package/src/types.ts +20 -0
  96. package/dist/lib/browser/SheetContainer-R65IDJHN.mjs.map +0 -7
  97. package/dist/lib/browser/chunk-5KVQ5IPW.mjs.map +0 -7
  98. package/dist/lib/browser/chunk-I2DKJ72A.mjs.map +0 -7
  99. package/dist/lib/browser/chunk-KCYJSOFB.mjs.map +0 -7
  100. package/dist/lib/node/SheetContainer-6BO4C5X2.cjs.map +0 -7
  101. package/dist/lib/node/chunk-2XJ5I4UF.cjs.map +0 -7
  102. package/dist/lib/node/chunk-DEPJHN47.cjs.map +0 -7
  103. package/dist/lib/node/chunk-JF5XNTF3.cjs.map +0 -7
  104. package/dist/lib/node-esm/SheetContainer-MJXC5E3P.mjs.map +0 -7
  105. package/dist/lib/node-esm/chunk-25V7WY4R.mjs.map +0 -7
  106. package/dist/lib/node-esm/chunk-5TXLF6PL.mjs.map +0 -7
  107. package/dist/lib/node-esm/chunk-XBEHKYO7.mjs.map +0 -7
  108. /package/dist/lib/browser/{compute-graph-SJT67236.mjs.map → compute-graph-GGWUX644.mjs.map} +0 -0
  109. /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
- <Toolbar.Root role={role}>
22
- <Toolbar.Styles />
23
- <Toolbar.Alignment />
24
- <Toolbar.Separator />
25
- <Toolbar.Actions />
26
- </Toolbar.Root>
27
- <div role='none' className='border-bs border-separator grid cols-1 rows-[1fr_min-content] min-bs-0'>
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
- </div>
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, useMemo } from 'react';
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: ToolbarAction) => void;
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<{ onAction: (action: ToolbarAction) => void }>(
104
- 'Toolbar',
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: ToolbarAction) => {
118
+ (action: ToolbarActionAnnotated) => {
121
119
  switch (action.key) {
122
120
  case 'alignment':
123
- if (cursor && cursorFallbackRange) {
124
- const index = model.sheet.ranges?.findIndex(
125
- (range) => range.key === action.key && inRange(rangeFromIndex(model.sheet, range.range), cursor),
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.value === 'unset') {
141
- const index = model.sheet.ranges?.findIndex((range) => range.key === action.key);
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
- const value = useMemo(
211
- () =>
212
- cursor
213
- ? model.sheet.ranges?.find(
214
- ({ range, key }) => key === alignKey && inRange(rangeFromIndex(model.sheet, range), cursor),
215
- )?.value
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={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>[] = [{ value: 'highlight', icon: 'ph--highlighter--regular' }];
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 { cursor, model } = useSheetContext();
245
+ const { cursorFallbackRange, model } = useSheetContext();
242
246
  const { onAction } = useToolbarContext('Styles');
243
247
  const { t } = useTranslation(SHEET_PLUGIN);
244
248
 
245
- const activeValues = useMemo(
246
- () =>
247
- cursor
248
- ? model.sheet.ranges
249
- ?.filter(({ range, key }) => key === 'style' && inRange(rangeFromIndex(model.sheet, range), cursor))
250
- .reduce((acc, { value }) => {
251
- acc.add(value);
252
- return acc;
253
- }, new Set())
254
- : undefined,
255
- [cursor, model.sheet.ranges],
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) => onAction?.({ key: 'style', value: nextPressed ? value : 'unset' })}
268
+ onPressedChange={(nextPressed: boolean) => {
269
+ onAction?.({ key: 'style', value, unset: !nextPressed });
270
+ }}
266
271
  icon={icon}
267
272
  >
268
- {t(`toolbar ${value} label`)}
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 (!(cursorFallbackRange && cursor)) {
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(cursor),
322
+ cellContent: model.getCellText(cursorFallbackRange.from),
314
323
  });
315
324
  }}
316
- disabled={!cursor || overlapsCommentAnchor}
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
- // TODO(wittjosiah): Factor out.
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 = new FunctionContext(this._hf, this._space, this._options);
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
- return formula.replace(/(\w+):([a-zA-Z0-9]+)\((.*)\)/g, (match, spaceId, objectId, args) => {
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 !== OBJECT_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 match;
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.info('function changed', { fn });
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 result = await fetch(`${this.context.remoteFunctionUrl}${path}`, {
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 await result.text();
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' | 'unset';
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
@@ -47,6 +47,7 @@ export const insertIndices = (indices: string[], i: number, n: number, max: numb
47
47
 
48
48
  const idx = createIndices(n);
49
49
  indices.splice(i, 0, ...idx);
50
+ return idx;
50
51
  };
51
52
 
52
53
  export const initialize = (
package/src/meta.ts CHANGED
@@ -11,4 +11,5 @@ export default {
11
11
  name: 'Sheet',
12
12
  description: 'A simple spreadsheet plugin.',
13
13
  icon: 'ph--grid-nine--regular',
14
+ source: 'https://github.com/dxos/dxos/tree/main/packages/plugins/plugin-sheet',
14
15
  } satisfies PluginMeta;
@@ -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
- model.update.once((update) => {
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'));
@@ -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
- const typeMap: Record<string, FieldValueType> = {
37
- BOOLEAN: FieldValueType.Boolean,
38
- NUMBER_RAW: FieldValueType.Number,
39
- NUMBER_PERCENT: FieldValueType.Percent,
40
- NUMBER_CURRENCY: FieldValueType.Currency,
41
- NUMBER_DATETIME: FieldValueType.DateTime,
42
- NUMBER_DATE: FieldValueType.Date,
43
- NUMBER_TIME: FieldValueType.Time,
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
- value = this._graph.mapFormulaToNative(
138
- this._graph.mapFunctionBindingFromId(this.mapFormulaIndicesToRefs(value)),
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 value = this._node.graph.hf.getCellValue(toSimpleCellAddress(this._node.sheetId, cell));
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
- getValueType(cell: CellAddress): FieldValueType {
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);
@@ -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 value start label': 'left',
18
- 'range value center label': 'center',
19
- 'range value end label': 'right',
20
- 'toolbar action label': '{{key}} {{value}}',
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}} — {{key}}: {{value}}',
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
  },