@dxos/plugin-sheet 0.6.14-staging.9e90729 → 0.6.14-staging.c117501

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 (117) hide show
  1. package/dist/lib/browser/{SheetContainer-JBB7W52Y.mjs → SheetContainer-P3NF5KEI.mjs} +60 -48
  2. package/dist/lib/browser/SheetContainer-P3NF5KEI.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-XXDHBYZQ.mjs → chunk-4LKIURJA.mjs} +337 -122
  4. package/dist/lib/browser/chunk-4LKIURJA.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-3QWIMZZJ.mjs → chunk-BVUN7SHF.mjs} +5 -3
  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-2ZK3YMZG.mjs → chunk-VMSX6Z4X.mjs} +58 -20
  10. package/dist/lib/browser/chunk-VMSX6Z4X.mjs.map +7 -0
  11. package/dist/lib/browser/{compute-graph-ZQKB6QVP.mjs → compute-graph-GGWUX644.mjs} +4 -4
  12. package/dist/lib/browser/index.mjs +48 -21
  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-L37HUFCF.cjs → SheetContainer-MPFKXY26.cjs} +72 -63
  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-V7E5JZBD.cjs → chunk-AWKOWDMI.cjs} +9 -7
  22. package/dist/lib/node/chunk-AWKOWDMI.cjs.map +7 -0
  23. package/dist/lib/node/{chunk-QNFMTK3X.cjs → chunk-O7XR4R7Y.cjs} +69 -29
  24. package/dist/lib/node/chunk-O7XR4R7Y.cjs.map +7 -0
  25. package/dist/lib/node/{chunk-7AWAC3R3.cjs → chunk-OO24XJBV.cjs} +353 -139
  26. package/dist/lib/node/chunk-OO24XJBV.cjs.map +7 -0
  27. package/dist/lib/node/{compute-graph-RJB6OVW6.cjs → compute-graph-KGWA2QLE.cjs} +21 -21
  28. package/dist/lib/node/{compute-graph-RJB6OVW6.cjs.map → compute-graph-KGWA2QLE.cjs.map} +2 -2
  29. package/dist/lib/node/index.cjs +74 -46
  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-CLKFEM35.mjs → SheetContainer-22IOAW3B.mjs} +60 -48
  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-3JYJEOCF.mjs → chunk-BW36PM2Y.mjs} +337 -122
  41. package/dist/lib/node-esm/chunk-BW36PM2Y.mjs.map +7 -0
  42. package/dist/lib/node-esm/{chunk-JBTORSST.mjs → chunk-CR4K75EL.mjs} +58 -20
  43. package/dist/lib/node-esm/chunk-CR4K75EL.mjs.map +7 -0
  44. package/dist/lib/node-esm/{chunk-SPQFLYC2.mjs → chunk-UIBWRHW7.mjs} +5 -3
  45. package/dist/lib/node-esm/chunk-UIBWRHW7.mjs.map +7 -0
  46. package/dist/lib/node-esm/{compute-graph-VKRG526N.mjs → compute-graph-2SCZT7N5.mjs} +4 -4
  47. package/dist/lib/node-esm/index.mjs +48 -21
  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 +2 -2
  56. package/dist/types/src/components/GridSheet/util.d.ts.map +1 -1
  57. package/dist/types/src/components/RangeList/RangeList.d.ts +2 -4
  58. package/dist/types/src/components/RangeList/RangeList.d.ts.map +1 -1
  59. package/dist/types/src/components/SheetContainer/SheetContainer.d.ts +6 -3
  60. package/dist/types/src/components/SheetContainer/SheetContainer.d.ts.map +1 -1
  61. package/dist/types/src/components/SheetContainer/SheetContainer.stories.d.ts.map +1 -1
  62. package/dist/types/src/components/Toolbar/Toolbar.d.ts +6 -3
  63. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  64. package/dist/types/src/components/index.d.ts +4 -2
  65. package/dist/types/src/components/index.d.ts.map +1 -1
  66. package/dist/types/src/compute-graph/compute-graph.d.ts +2 -2
  67. package/dist/types/src/compute-graph/compute-graph.d.ts.map +1 -1
  68. package/dist/types/src/compute-graph/functions/async-function.d.ts.map +1 -1
  69. package/dist/types/src/compute-graph/functions/edge-function.d.ts.map +1 -1
  70. package/dist/types/src/defs/sheet-range-types.d.ts +2 -2
  71. package/dist/types/src/defs/sheet-range-types.d.ts.map +1 -1
  72. package/dist/types/src/defs/util.d.ts +1 -1
  73. package/dist/types/src/defs/util.d.ts.map +1 -1
  74. package/dist/types/src/integrations/thread-ranges.d.ts +1 -2
  75. package/dist/types/src/integrations/thread-ranges.d.ts.map +1 -1
  76. package/dist/types/src/meta.d.ts +1 -0
  77. package/dist/types/src/meta.d.ts.map +1 -1
  78. package/dist/types/src/model/sheet-model.d.ts +12 -5
  79. package/dist/types/src/model/sheet-model.d.ts.map +1 -1
  80. package/dist/types/src/translations.d.ts +12 -3
  81. package/dist/types/src/translations.d.ts.map +1 -1
  82. package/dist/types/src/types.d.ts +33 -3
  83. package/dist/types/src/types.d.ts.map +1 -1
  84. package/package.json +37 -36
  85. package/src/SheetPlugin.tsx +27 -7
  86. package/src/components/FunctionEditor/FunctionEditor.tsx +1 -5
  87. package/src/components/GridSheet/GridSheet.tsx +107 -42
  88. package/src/components/GridSheet/util.ts +26 -16
  89. package/src/components/RangeList/RangeList.tsx +27 -12
  90. package/src/components/SheetContainer/SheetContainer.stories.tsx +4 -2
  91. package/src/components/SheetContainer/SheetContainer.tsx +19 -12
  92. package/src/components/Toolbar/Toolbar.tsx +72 -59
  93. package/src/compute-graph/compute-graph.ts +22 -7
  94. package/src/compute-graph/functions/async-function.ts +1 -0
  95. package/src/compute-graph/functions/edge-function.ts +5 -3
  96. package/src/defs/sheet-range-types.ts +8 -6
  97. package/src/defs/util.ts +1 -0
  98. package/src/integrations/thread-ranges.ts +3 -3
  99. package/src/meta.ts +1 -0
  100. package/src/model/sheet-model.test.ts +5 -3
  101. package/src/model/sheet-model.ts +93 -21
  102. package/src/translations.ts +12 -3
  103. package/src/types.ts +21 -1
  104. package/dist/lib/browser/SheetContainer-JBB7W52Y.mjs.map +0 -7
  105. package/dist/lib/browser/chunk-2ZK3YMZG.mjs.map +0 -7
  106. package/dist/lib/browser/chunk-3QWIMZZJ.mjs.map +0 -7
  107. package/dist/lib/browser/chunk-XXDHBYZQ.mjs.map +0 -7
  108. package/dist/lib/node/SheetContainer-L37HUFCF.cjs.map +0 -7
  109. package/dist/lib/node/chunk-7AWAC3R3.cjs.map +0 -7
  110. package/dist/lib/node/chunk-QNFMTK3X.cjs.map +0 -7
  111. package/dist/lib/node/chunk-V7E5JZBD.cjs.map +0 -7
  112. package/dist/lib/node-esm/SheetContainer-CLKFEM35.mjs.map +0 -7
  113. package/dist/lib/node-esm/chunk-3JYJEOCF.mjs.map +0 -7
  114. package/dist/lib/node-esm/chunk-JBTORSST.mjs.map +0 -7
  115. package/dist/lib/node-esm/chunk-SPQFLYC2.mjs.map +0 -7
  116. /package/dist/lib/browser/{compute-graph-ZQKB6QVP.mjs.map → compute-graph-GGWUX644.mjs.map} +0 -0
  117. /package/dist/lib/node-esm/{compute-graph-VKRG526N.mjs.map → compute-graph-2SCZT7N5.mjs.map} +0 -0
@@ -3,23 +3,24 @@
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 {
10
10
  Icon,
11
+ type ThemedClassName,
11
12
  Toolbar as NaturalToolbar,
12
- Tooltip,
13
- type ToolbarToggleGroupItemProps as NaturalToolbarToggleGroupItemProps,
14
13
  type ToolbarButtonProps as NaturalToolbarButtonProps,
14
+ type ToolbarToggleGroupItemProps as NaturalToolbarToggleGroupItemProps,
15
15
  type ToolbarToggleProps as NaturalToolbarToggleProps,
16
- type ThemedClassName,
16
+ Tooltip,
17
17
  useTranslation,
18
18
  } from '@dxos/react-ui';
19
19
  import { useAttention } from '@dxos/react-ui-attention';
20
20
  import { nonNullable } from '@dxos/util';
21
21
 
22
22
  import {
23
+ alignKey,
23
24
  type AlignKey,
24
25
  type AlignValue,
25
26
  type CommentKey,
@@ -27,6 +28,7 @@ import {
27
28
  inRange,
28
29
  rangeFromIndex,
29
30
  rangeToIndex,
31
+ styleKey,
30
32
  type StyleKey,
31
33
  type StyleValue,
32
34
  } from '../../defs';
@@ -88,10 +90,11 @@ type CommentAction = { key: CommentKey; value: CommentValue; cellContent?: strin
88
90
  type StyleAction = { key: StyleKey; value: StyleValue };
89
91
 
90
92
  export type ToolbarAction = StyleAction | AlignAction | CommentAction;
93
+ export type ToolbarActionAnnotated = ToolbarAction & { unset?: boolean };
91
94
 
92
95
  export type ToolbarActionType = ToolbarAction['key'];
93
96
 
94
- export type ToolbarActionHandler = (action: ToolbarAction) => void;
97
+ export type ToolbarActionHandler = (action: ToolbarActionAnnotated) => void;
95
98
 
96
99
  export type ToolbarProps = ThemedClassName<
97
100
  PropsWithChildren<{
@@ -99,13 +102,9 @@ export type ToolbarProps = ThemedClassName<
99
102
  }>
100
103
  >;
101
104
 
102
- const [ToolbarContextProvider, useToolbarContext] = createContext<{ onAction: (action: ToolbarAction) => void }>(
103
- 'Toolbar',
104
- );
105
-
106
- // TODO(Zan): Factor out, copied this from MarkdownPlugin.
107
- const sectionToolbarLayout =
108
- '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');
109
108
 
110
109
  type Range = SheetType['ranges'][number];
111
110
 
@@ -116,13 +115,16 @@ const ToolbarRoot = ({ children, role, classNames }: ToolbarProps) => {
116
115
 
117
116
  // TODO(Zan): Externalize the toolbar action handler. E.g., Toolbar/keys should both fire events.
118
117
  const handleAction = useCallback(
119
- (action: ToolbarAction) => {
118
+ (action: ToolbarActionAnnotated) => {
120
119
  switch (action.key) {
121
- case 'align':
122
- if (cursor && cursorFallbackRange) {
123
- const index = model.sheet.ranges?.findIndex(
124
- (range) => range.key === action.key && inRange(rangeFromIndex(model.sheet, range.range), cursor),
125
- );
120
+ case 'alignment':
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;
126
128
  const nextRangeEntity = {
127
129
  range: rangeToIndex(model.sheet, cursorFallbackRange),
128
130
  key: action.key,
@@ -130,14 +132,21 @@ const ToolbarRoot = ({ children, role, classNames }: ToolbarProps) => {
130
132
  };
131
133
  if (index < 0) {
132
134
  model.sheet.ranges?.push(nextRangeEntity);
135
+ } else if (model.sheet.ranges![index].value === action.value) {
136
+ model.sheet.ranges?.splice(index, 1);
133
137
  } else {
134
138
  model.sheet.ranges?.splice(index, 1, nextRangeEntity);
135
139
  }
136
140
  }
137
141
  break;
138
142
  case 'style':
139
- if (action.value === 'unset') {
140
- 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
+ );
141
150
  if (index >= 0) {
142
151
  model.sheet.ranges?.splice(index, 1);
143
152
  }
@@ -169,14 +178,7 @@ const ToolbarRoot = ({ children, role, classNames }: ToolbarProps) => {
169
178
 
170
179
  return (
171
180
  <ToolbarContextProvider onAction={handleAction}>
172
- <NaturalToolbar.Root
173
- classNames={[
174
- ...(role === 'section'
175
- ? ['z-[2] group-focus-within/section:visible', !hasAttention && 'invisible', sectionToolbarLayout]
176
- : ['attention-surface']),
177
- classNames,
178
- ]}
179
- >
181
+ <NaturalToolbar.Root classNames={['pli-0.5', !hasAttention && 'opacity-20', classNames]}>
180
182
  {children}
181
183
  </NaturalToolbar.Root>
182
184
  </ToolbarContextProvider>
@@ -206,50 +208,55 @@ const Alignment = () => {
206
208
  const { onAction } = useToolbarContext('Alignment');
207
209
  const { t } = useTranslation(SHEET_PLUGIN);
208
210
 
209
- const value = useMemo(
210
- () =>
211
- cursor
212
- ? model.sheet.ranges?.find(
213
- ({ range, key }) => key === 'alignment' && inRange(rangeFromIndex(model.sheet, range), cursor),
214
- )?.value
215
- : undefined,
216
- [cursor, model.sheet.ranges],
217
- );
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;
218
217
 
219
218
  return (
220
219
  <NaturalToolbar.ToggleGroup
221
220
  type='single'
222
- value={value}
223
- onValueChange={(value: AlignValue) => onAction?.({ key: 'align', value })}
221
+ value={
222
+ // TODO(thure): providing `undefined` leaves the last item active which was active rather than showing none.
223
+ value ?? 'never'
224
+ }
225
+ onValueChange={(value: AlignValue) => onAction?.({ key: alignKey, value })}
224
226
  >
225
227
  {alignmentOptions.map(({ value, icon }) => (
226
228
  <ToolbarItem itemType='toggleGroupItem' key={value} value={value} icon={icon}>
227
- {t(`toolbar ${value} label`)}
229
+ {t('toolbar action label', {
230
+ key: t(`range key ${alignKey} label`),
231
+ value: t(`range value ${value} label`),
232
+ })}
228
233
  </ToolbarItem>
229
234
  ))}
230
235
  </NaturalToolbar.ToggleGroup>
231
236
  );
232
237
  };
233
238
 
234
- 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
+ ];
235
243
 
236
244
  const Styles = () => {
237
- const { cursor, model } = useSheetContext();
245
+ const { cursorFallbackRange, model } = useSheetContext();
238
246
  const { onAction } = useToolbarContext('Styles');
239
247
  const { t } = useTranslation(SHEET_PLUGIN);
240
248
 
241
- const activeValues = useMemo(
242
- () =>
243
- cursor
244
- ? model.sheet.ranges
245
- ?.filter(({ range, key }) => key === 'style' && inRange(rangeFromIndex(model.sheet, range), cursor))
246
- .reduce((acc, { value }) => {
247
- acc.add(value);
248
- return acc;
249
- }, new Set())
250
- : undefined,
251
- [cursor, model.sheet.ranges],
252
- );
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;
253
260
 
254
261
  return (
255
262
  <>
@@ -258,10 +265,15 @@ const Styles = () => {
258
265
  itemType='toggle'
259
266
  key={value}
260
267
  pressed={activeValues?.has(value)}
261
- onPressedChange={(nextPressed: boolean) => onAction?.({ key: 'style', value: nextPressed ? value : 'unset' })}
268
+ onPressedChange={(nextPressed: boolean) => {
269
+ onAction?.({ key: 'style', value, unset: !nextPressed });
270
+ }}
262
271
  icon={icon}
263
272
  >
264
- {t(`toolbar ${value} label`)}
273
+ {t('toolbar action label', {
274
+ key: t(`range key ${styleKey} label`),
275
+ value: t(`range value ${value} label`),
276
+ })}
265
277
  </ToolbarItem>
266
278
  ))}
267
279
  </>
@@ -277,6 +289,7 @@ const Actions = () => {
277
289
  const { cursorFallbackRange, cursor, model } = useSheetContext();
278
290
  const { t } = useTranslation(SHEET_PLUGIN);
279
291
 
292
+ // TODO(thure): Can this O(n) call be memoized?
280
293
  const overlapsCommentAnchor = (model.sheet.threads ?? [])
281
294
  .filter(nonNullable)
282
295
  .filter((thread) => thread.status !== 'resolved')
@@ -300,16 +313,16 @@ const Actions = () => {
300
313
  icon='ph--chat-text--regular'
301
314
  data-testid='editor.toolbar.comment'
302
315
  onClick={() => {
303
- if (!(cursorFallbackRange && cursor)) {
316
+ if (!cursorFallbackRange) {
304
317
  return;
305
318
  }
306
319
  return onAction?.({
307
320
  key: 'comment',
308
321
  value: rangeToIndex(model.sheet, cursorFallbackRange),
309
- cellContent: model.getCellText(cursor),
322
+ cellContent: model.getCellText(cursorFallbackRange.from),
310
323
  });
311
324
  }}
312
- disabled={!cursor || overlapsCommentAnchor}
325
+ disabled={!cursorFallbackRange || overlapsCommentAnchor}
313
326
  >
314
327
  {t(tooltipLabelKey)}
315
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 });
@@ -6,7 +6,7 @@ import { type ClassNameValue } from '@dxos/react-ui';
6
6
 
7
7
  import { type SheetType } from '../types';
8
8
 
9
- export const alignKey = 'align';
9
+ export const alignKey = 'alignment';
10
10
  export type AlignKey = typeof alignKey;
11
11
  export type AlignValue = 'start' | 'center' | 'end' | 'unset';
12
12
 
@@ -16,12 +16,12 @@ 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 => {
23
23
  switch (key) {
24
- case 'align':
24
+ case alignKey:
25
25
  switch (value) {
26
26
  case 'start':
27
27
  return 'text-start';
@@ -32,12 +32,14 @@ export const cellClassNameForRange = ({ key, value }: SheetType['ranges'][number
32
32
  default:
33
33
  return undefined;
34
34
  }
35
- case 'comment':
35
+ case commentKey:
36
36
  return 'bg-gridComment';
37
- case 'style':
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 = (
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type MutableRefObject, useCallback, useEffect, useMemo } from 'react';
5
+ import { useCallback, useEffect, useMemo } from 'react';
6
6
 
7
7
  import { type IntentResolver, LayoutAction, useIntentDispatcher, useIntentResolver } from '@dxos/app-framework';
8
8
  import { debounce } from '@dxos/async';
@@ -30,7 +30,7 @@ export const parseThreadAnchorAsCellRange = (cursor: string): CompleteCellRange
30
30
  }
31
31
  };
32
32
 
33
- export const useUpdateFocusedCellOnThreadSelection = (grid: MutableRefObject<DxGridElement | null>) => {
33
+ export const useUpdateFocusedCellOnThreadSelection = (grid: DxGridElement | null) => {
34
34
  const { model, setActiveRefs } = useSheetContext();
35
35
  const handleScrollIntoView: IntentResolver = useCallback(
36
36
  ({ action, data }) => {
@@ -42,7 +42,7 @@ export const useUpdateFocusedCellOnThreadSelection = (grid: MutableRefObject<DxG
42
42
  setActiveRefs(data.thread);
43
43
  // TODO(Zan): Everywhere we refer to the cursor in a thread context should change to `anchor`.
44
44
  const range = parseThreadAnchorAsCellRange(data.cursor);
45
- range && grid.current?.setFocus({ ...range.to, plane: 'grid' }, true);
45
+ range && grid?.setFocus({ ...range.to, plane: 'grid' }, true);
46
46
  }
47
47
  }
48
48
  },
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);