@dxos/plugin-sheet 0.6.14-main.8b352a0 → 0.6.14-staging.8758a12

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 (103) hide show
  1. package/dist/lib/browser/{SheetContainer-R65IDJHN.mjs → SheetContainer-GPJOTYCI.mjs} +41 -30
  2. package/dist/lib/browser/SheetContainer-GPJOTYCI.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-5KVQ5IPW.mjs → chunk-BVUN7SHF.mjs} +4 -2
  4. package/dist/lib/browser/chunk-BVUN7SHF.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-I2DKJ72A.mjs → chunk-CYOQA6UK.mjs} +277 -79
  6. package/dist/lib/browser/chunk-CYOQA6UK.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-VSC6XF3M.cjs} +60 -50
  18. package/dist/lib/node/SheetContainer-VSC6XF3M.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-DEPJHN47.cjs → chunk-545PZPK3.cjs} +313 -116
  22. package/dist/lib/node/chunk-545PZPK3.cjs.map +7 -0
  23. package/dist/lib/node/{chunk-2XJ5I4UF.cjs → chunk-AWKOWDMI.cjs} +8 -6
  24. package/dist/lib/node/chunk-AWKOWDMI.cjs.map +7 -0
  25. package/dist/lib/node/{chunk-JF5XNTF3.cjs → chunk-O7XR4R7Y.cjs} +61 -26
  26. package/dist/lib/node/chunk-O7XR4R7Y.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-SJK25GKT.mjs} +41 -30
  37. package/dist/lib/node-esm/SheetContainer-SJK25GKT.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-XBEHKYO7.mjs → chunk-CR4K75EL.mjs} +51 -17
  41. package/dist/lib/node-esm/chunk-CR4K75EL.mjs.map +7 -0
  42. package/dist/lib/node-esm/{chunk-25V7WY4R.mjs → chunk-EZ6K2W62.mjs} +277 -79
  43. package/dist/lib/node-esm/chunk-EZ6K2W62.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/GridSheet/GridSheet.d.ts.map +1 -1
  54. package/dist/types/src/components/GridSheet/util.d.ts.map +1 -1
  55. package/dist/types/src/components/SheetContainer/SheetContainer.stories.d.ts.map +1 -1
  56. package/dist/types/src/components/Toolbar/Toolbar.d.ts +5 -2
  57. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  58. package/dist/types/src/compute-graph/compute-graph.d.ts +2 -2
  59. package/dist/types/src/compute-graph/compute-graph.d.ts.map +1 -1
  60. package/dist/types/src/compute-graph/functions/async-function.d.ts.map +1 -1
  61. package/dist/types/src/compute-graph/functions/edge-function.d.ts.map +1 -1
  62. package/dist/types/src/defs/sheet-range-types.d.ts +1 -1
  63. package/dist/types/src/defs/sheet-range-types.d.ts.map +1 -1
  64. package/dist/types/src/defs/util.d.ts +1 -1
  65. package/dist/types/src/defs/util.d.ts.map +1 -1
  66. package/dist/types/src/meta.d.ts +1 -0
  67. package/dist/types/src/meta.d.ts.map +1 -1
  68. package/dist/types/src/model/sheet-model.d.ts +12 -5
  69. package/dist/types/src/model/sheet-model.d.ts.map +1 -1
  70. package/dist/types/src/translations.d.ts +10 -5
  71. package/dist/types/src/translations.d.ts.map +1 -1
  72. package/dist/types/src/types.d.ts +33 -3
  73. package/dist/types/src/types.d.ts.map +1 -1
  74. package/package.json +36 -36
  75. package/src/SheetPlugin.tsx +20 -0
  76. package/src/components/GridSheet/GridSheet.tsx +96 -36
  77. package/src/components/GridSheet/util.ts +13 -7
  78. package/src/components/SheetContainer/SheetContainer.stories.tsx +2 -0
  79. package/src/components/Toolbar/Toolbar.tsx +62 -41
  80. package/src/compute-graph/compute-graph.ts +22 -7
  81. package/src/compute-graph/functions/async-function.ts +1 -0
  82. package/src/compute-graph/functions/edge-function.ts +5 -3
  83. package/src/defs/sheet-range-types.ts +4 -2
  84. package/src/defs/util.ts +1 -0
  85. package/src/meta.ts +1 -0
  86. package/src/model/sheet-model.test.ts +5 -3
  87. package/src/model/sheet-model.ts +93 -21
  88. package/src/translations.ts +10 -5
  89. package/src/types.ts +20 -0
  90. package/dist/lib/browser/SheetContainer-R65IDJHN.mjs.map +0 -7
  91. package/dist/lib/browser/chunk-5KVQ5IPW.mjs.map +0 -7
  92. package/dist/lib/browser/chunk-I2DKJ72A.mjs.map +0 -7
  93. package/dist/lib/browser/chunk-KCYJSOFB.mjs.map +0 -7
  94. package/dist/lib/node/SheetContainer-6BO4C5X2.cjs.map +0 -7
  95. package/dist/lib/node/chunk-2XJ5I4UF.cjs.map +0 -7
  96. package/dist/lib/node/chunk-DEPJHN47.cjs.map +0 -7
  97. package/dist/lib/node/chunk-JF5XNTF3.cjs.map +0 -7
  98. package/dist/lib/node-esm/SheetContainer-MJXC5E3P.mjs.map +0 -7
  99. package/dist/lib/node-esm/chunk-25V7WY4R.mjs.map +0 -7
  100. package/dist/lib/node-esm/chunk-5TXLF6PL.mjs.map +0 -7
  101. package/dist/lib/node-esm/chunk-XBEHKYO7.mjs.map +0 -7
  102. /package/dist/lib/browser/{compute-graph-SJT67236.mjs.map → compute-graph-GGWUX644.mjs.map} +0 -0
  103. /package/dist/lib/node-esm/{compute-graph-FRCKXEYK.mjs.map → compute-graph-2SCZT7N5.mjs.map} +0 -0
@@ -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,9 +102,9 @@ export type ToolbarProps = ThemedClassName<
100
102
  }>
101
103
  >;
102
104
 
103
- const [ToolbarContextProvider, useToolbarContext] = createContext<{ onAction: (action: ToolbarAction) => void }>(
104
- 'Toolbar',
105
- );
105
+ const [ToolbarContextProvider, useToolbarContext] = createContext<{
106
+ onAction: (action: ToolbarActionAnnotated) => void;
107
+ }>('Toolbar');
106
108
 
107
109
  // TODO(Zan): Factor out, copied this from MarkdownPlugin.
108
110
  const sectionToolbarLayout =
@@ -117,13 +119,16 @@ const ToolbarRoot = ({ children, role, classNames }: ToolbarProps) => {
117
119
 
118
120
  // TODO(Zan): Externalize the toolbar action handler. E.g., Toolbar/keys should both fire events.
119
121
  const handleAction = useCallback(
120
- (action: ToolbarAction) => {
122
+ (action: ToolbarActionAnnotated) => {
121
123
  switch (action.key) {
122
124
  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
- );
125
+ if (cursorFallbackRange) {
126
+ const index =
127
+ model.sheet.ranges?.findIndex(
128
+ (range) =>
129
+ range.key === action.key &&
130
+ inRange(rangeFromIndex(model.sheet, range.range), cursorFallbackRange.from),
131
+ ) ?? -1;
127
132
  const nextRangeEntity = {
128
133
  range: rangeToIndex(model.sheet, cursorFallbackRange),
129
134
  key: action.key,
@@ -131,14 +136,21 @@ const ToolbarRoot = ({ children, role, classNames }: ToolbarProps) => {
131
136
  };
132
137
  if (index < 0) {
133
138
  model.sheet.ranges?.push(nextRangeEntity);
139
+ } else if (model.sheet.ranges![index].value === action.value) {
140
+ model.sheet.ranges?.splice(index, 1);
134
141
  } else {
135
142
  model.sheet.ranges?.splice(index, 1, nextRangeEntity);
136
143
  }
137
144
  }
138
145
  break;
139
146
  case 'style':
140
- if (action.value === 'unset') {
141
- const index = model.sheet.ranges?.findIndex((range) => range.key === action.key);
147
+ if (action.unset) {
148
+ const index = model.sheet.ranges?.findIndex(
149
+ (range) =>
150
+ range.key === action.key &&
151
+ cursorFallbackRange &&
152
+ inRange(rangeFromIndex(model.sheet, range.range), cursorFallbackRange.from),
153
+ );
142
154
  if (index >= 0) {
143
155
  model.sheet.ranges?.splice(index, 1);
144
156
  }
@@ -172,6 +184,7 @@ const ToolbarRoot = ({ children, role, classNames }: ToolbarProps) => {
172
184
  <ToolbarContextProvider onAction={handleAction}>
173
185
  <NaturalToolbar.Root
174
186
  classNames={[
187
+ 'pli-0.5',
175
188
  ...(role === 'section'
176
189
  ? ['z-[2] group-focus-within/section:visible', !hasAttention && 'invisible', sectionToolbarLayout]
177
190
  : ['attention-surface']),
@@ -207,20 +220,20 @@ const Alignment = () => {
207
220
  const { onAction } = useToolbarContext('Alignment');
208
221
  const { t } = useTranslation(SHEET_PLUGIN);
209
222
 
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
- );
223
+ // TODO(thure): Can this O(n) call be memoized?
224
+ const value = cursor
225
+ ? model.sheet.ranges?.findLast(
226
+ ({ range, key }) => key === alignKey && inRange(rangeFromIndex(model.sheet, range), cursor),
227
+ )?.value
228
+ : undefined;
219
229
 
220
230
  return (
221
231
  <NaturalToolbar.ToggleGroup
222
232
  type='single'
223
- value={value}
233
+ value={
234
+ // TODO(thure): providing `undefined` leaves the last item active which was active rather than showing none.
235
+ value ?? 'never'
236
+ }
224
237
  onValueChange={(value: AlignValue) => onAction?.({ key: alignKey, value })}
225
238
  >
226
239
  {alignmentOptions.map(({ value, icon }) => (
@@ -235,25 +248,27 @@ const Alignment = () => {
235
248
  );
236
249
  };
237
250
 
238
- const styleOptions: ButtonProps<StyleValue>[] = [{ value: 'highlight', icon: 'ph--highlighter--regular' }];
251
+ const styleOptions: ButtonProps<StyleValue>[] = [
252
+ { value: 'highlight', icon: 'ph--highlighter--regular' },
253
+ { value: 'softwrap', icon: 'ph--paragraph--regular' },
254
+ ];
239
255
 
240
256
  const Styles = () => {
241
- const { cursor, model } = useSheetContext();
257
+ const { cursorFallbackRange, model } = useSheetContext();
242
258
  const { onAction } = useToolbarContext('Styles');
243
259
  const { t } = useTranslation(SHEET_PLUGIN);
244
260
 
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
- );
261
+ // TODO(thure): Can this O(n) call be memoized?
262
+ const activeValues = cursorFallbackRange
263
+ ? model.sheet.ranges
264
+ ?.filter(
265
+ ({ range, key }) => key === 'style' && inRange(rangeFromIndex(model.sheet, range), cursorFallbackRange.from),
266
+ )
267
+ .reduce((acc, { value }) => {
268
+ acc.add(value);
269
+ return acc;
270
+ }, new Set())
271
+ : undefined;
257
272
 
258
273
  return (
259
274
  <>
@@ -262,10 +277,15 @@ const Styles = () => {
262
277
  itemType='toggle'
263
278
  key={value}
264
279
  pressed={activeValues?.has(value)}
265
- onPressedChange={(nextPressed: boolean) => onAction?.({ key: 'style', value: nextPressed ? value : 'unset' })}
280
+ onPressedChange={(nextPressed: boolean) => {
281
+ onAction?.({ key: 'style', value, unset: !nextPressed });
282
+ }}
266
283
  icon={icon}
267
284
  >
268
- {t(`toolbar ${value} label`)}
285
+ {t('toolbar action label', {
286
+ key: t(`range key ${styleKey} label`),
287
+ value: t(`range value ${value} label`),
288
+ })}
269
289
  </ToolbarItem>
270
290
  ))}
271
291
  </>
@@ -281,6 +301,7 @@ const Actions = () => {
281
301
  const { cursorFallbackRange, cursor, model } = useSheetContext();
282
302
  const { t } = useTranslation(SHEET_PLUGIN);
283
303
 
304
+ // TODO(thure): Can this O(n) call be memoized?
284
305
  const overlapsCommentAnchor = (model.sheet.threads ?? [])
285
306
  .filter(nonNullable)
286
307
  .filter((thread) => thread.status !== 'resolved')
@@ -304,16 +325,16 @@ const Actions = () => {
304
325
  icon='ph--chat-text--regular'
305
326
  data-testid='editor.toolbar.comment'
306
327
  onClick={() => {
307
- if (!(cursorFallbackRange && cursor)) {
328
+ if (!cursorFallbackRange) {
308
329
  return;
309
330
  }
310
331
  return onAction?.({
311
332
  key: 'comment',
312
333
  value: rangeToIndex(model.sheet, cursorFallbackRange),
313
- cellContent: model.getCellText(cursor),
334
+ cellContent: model.getCellText(cursorFallbackRange.from),
314
335
  });
315
336
  }}
316
- disabled={!cursor || overlapsCommentAnchor}
337
+ disabled={!cursorFallbackRange || overlapsCommentAnchor}
317
338
  >
318
339
  {t(tooltipLabelKey)}
319
340
  </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
  },
package/src/types.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  MetadataRecordsProvides,
9
9
  SurfaceProvides,
10
10
  TranslationsProvides,
11
+ IntentData,
11
12
  } from '@dxos/app-framework';
12
13
  import { ref, S, TypedObject } from '@dxos/echo-schema';
13
14
  import { type SchemaProvides } from '@dxos/plugin-client';
@@ -15,13 +16,32 @@ import { type MarkdownExtensionProvides } from '@dxos/plugin-markdown';
15
16
  import { type SpaceInitProvides } from '@dxos/plugin-space';
16
17
  import { ThreadType } from '@dxos/plugin-space/types';
17
18
  import { type StackProvides } from '@dxos/plugin-stack';
19
+ import { type DxGridAxis } from '@dxos/react-ui-grid';
18
20
 
19
21
  import { SHEET_PLUGIN } from './meta';
22
+ import { type SheetModel } from './model';
20
23
 
21
24
  const SHEET_ACTION = `${SHEET_PLUGIN}/action`;
22
25
 
23
26
  export enum SheetAction {
24
27
  CREATE = `${SHEET_ACTION}/create`,
28
+ INSERT_AXIS = `${SHEET_ACTION}/axis-insert`,
29
+ DROP_AXIS = `${SHEET_ACTION}/axis-drop`,
30
+ }
31
+
32
+ export type RestoreAxis = {
33
+ axis: DxGridAxis;
34
+ axisIndex: string;
35
+ index: number;
36
+ axisMeta?: S.Schema.Type<typeof RowColumnMeta>;
37
+ values: CellScalarValue[];
38
+ };
39
+
40
+ export namespace SheetAction {
41
+ export type Create = IntentData<{ sheet: SheetType }>;
42
+ export type InsertAxis = IntentData<{ model: SheetModel; axis: DxGridAxis; index: number; count?: number }>;
43
+ export type DropAxis = IntentData<{ model: SheetModel; axis: DxGridAxis; axisIndex: string }>;
44
+ export type DropAxisRestore = IntentData<RestoreAxis & { model: SheetModel }>;
25
45
  }
26
46
 
27
47
  // TODO(Zan): Move this to the plugin-space plugin or another common location