@dxos/plugin-sheet 0.6.11 → 0.6.12-main.5cc132e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/{SheetContainer-U4H5D34A.mjs → SheetContainer-Y7ZMFBAP.mjs} +568 -109
- package/dist/lib/browser/SheetContainer-Y7ZMFBAP.mjs.map +7 -0
- package/dist/lib/browser/{chunk-D5AGLXJP.mjs → chunk-GNNVBNCX.mjs} +55 -47
- package/dist/lib/browser/chunk-GNNVBNCX.mjs.map +7 -0
- package/dist/lib/browser/{chunk-APHOLYUB.mjs → chunk-PGKZPKUD.mjs} +2 -2
- package/dist/lib/browser/chunk-VBF7YENS.mjs +8 -0
- package/dist/lib/browser/{chunk-FUAGSXA4.mjs → chunk-WUPTZUTX.mjs} +6 -3
- package/dist/lib/browser/chunk-WUPTZUTX.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +15 -6
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing.mjs +3 -3
- package/dist/lib/browser/types.mjs +1 -1
- package/dist/lib/node/{SheetContainer-AXQV3ZT5.cjs → SheetContainer-KEOKUKAQ.cjs} +509 -62
- package/dist/lib/node/SheetContainer-KEOKUKAQ.cjs.map +7 -0
- package/dist/lib/node/{chunk-PYXHNAAK.cjs → chunk-57PB2HPY.cjs} +5 -5
- package/dist/lib/node/{chunk-CN3RPESU.cjs → chunk-6LWBQAQZ.cjs} +9 -9
- package/dist/lib/node/{chunk-DSYKOI4E.cjs → chunk-VJU3NPUJ.cjs} +8 -5
- package/dist/lib/node/chunk-VJU3NPUJ.cjs.map +7 -0
- package/dist/lib/node/{chunk-5KKJ4NPP.cjs → chunk-ZRQZFV5T.cjs} +70 -57
- package/dist/lib/node/chunk-ZRQZFV5T.cjs.map +7 -0
- package/dist/lib/node/index.cjs +31 -23
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing.cjs +6 -6
- package/dist/lib/node/types.cjs +9 -9
- package/dist/lib/node/types.cjs.map +1 -1
- package/dist/lib/node-esm/SheetContainer-Y7ZMFBAP.mjs +2231 -0
- package/dist/lib/node-esm/SheetContainer-Y7ZMFBAP.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-GNNVBNCX.mjs +3243 -0
- package/dist/lib/node-esm/chunk-GNNVBNCX.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-JRL5LGCE.mjs +18 -0
- package/dist/lib/node-esm/chunk-JRL5LGCE.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-PGKZPKUD.mjs +175 -0
- package/dist/lib/node-esm/chunk-PGKZPKUD.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-VBF7YENS.mjs +8 -0
- package/dist/lib/node-esm/chunk-VBF7YENS.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-WUPTZUTX.mjs +85 -0
- package/dist/lib/node-esm/chunk-WUPTZUTX.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +257 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/meta.mjs +9 -0
- package/dist/lib/node-esm/meta.mjs.map +7 -0
- package/dist/lib/node-esm/testing.mjs +92 -0
- package/dist/lib/node-esm/testing.mjs.map +7 -0
- package/dist/lib/node-esm/types.mjs +22 -0
- package/dist/lib/node-esm/types.mjs.map +7 -0
- package/dist/types/src/SheetPlugin.d.ts.map +1 -1
- package/dist/types/src/components/Sheet/Sheet.d.ts.map +1 -1
- package/dist/types/src/components/Sheet/Sheet.stories.d.ts.map +1 -1
- package/dist/types/src/components/Sheet/decorations.d.ts +24 -0
- package/dist/types/src/components/Sheet/decorations.d.ts.map +1 -0
- package/dist/types/src/components/Sheet/formatting.d.ts.map +1 -1
- package/dist/types/src/components/Sheet/sheet-context.d.ts +2 -0
- package/dist/types/src/components/Sheet/sheet-context.d.ts.map +1 -1
- package/dist/types/src/components/Sheet/threads.d.ts +2 -0
- package/dist/types/src/components/Sheet/threads.d.ts.map +1 -0
- package/dist/types/src/components/SheetContainer.d.ts +2 -3
- package/dist/types/src/components/SheetContainer.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +19 -3
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +17 -12
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -2
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/model/index.d.ts +1 -0
- package/dist/types/src/model/index.d.ts.map +1 -1
- package/dist/types/src/model/model.d.ts +0 -16
- package/dist/types/src/model/model.d.ts.map +1 -1
- package/dist/types/src/model/util.d.ts +24 -0
- package/dist/types/src/model/util.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +17 -12
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +72 -2
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +36 -32
- package/src/SheetPlugin.tsx +8 -15
- package/src/components/CellEditor/extension.test.ts +1 -2
- package/src/components/ComputeGraph/graph.browser.test.ts +1 -2
- package/src/components/Sheet/Sheet.stories.tsx +5 -1
- package/src/components/Sheet/Sheet.tsx +45 -8
- package/src/components/Sheet/decorations.ts +62 -0
- package/src/components/Sheet/formatting.ts +3 -3
- package/src/components/Sheet/sheet-context.tsx +9 -1
- package/src/components/Sheet/threads.tsx +201 -0
- package/src/components/SheetContainer.tsx +72 -18
- package/src/components/Toolbar/Toolbar.tsx +54 -12
- package/src/model/index.ts +1 -0
- package/src/model/model.browser.test.ts +1 -2
- package/src/model/model.ts +9 -43
- package/src/model/types.test.ts +1 -2
- package/src/model/util.ts +67 -0
- package/src/translations.ts +6 -1
- package/src/types.ts +26 -3
- package/dist/lib/browser/SheetContainer-U4H5D34A.mjs.map +0 -7
- package/dist/lib/browser/chunk-D5AGLXJP.mjs.map +0 -7
- package/dist/lib/browser/chunk-FUAGSXA4.mjs.map +0 -7
- package/dist/lib/browser/chunk-NU4PBN33.mjs +0 -8
- package/dist/lib/node/SheetContainer-AXQV3ZT5.cjs.map +0 -7
- package/dist/lib/node/chunk-5KKJ4NPP.cjs.map +0 -7
- package/dist/lib/node/chunk-DSYKOI4E.cjs.map +0 -7
- /package/dist/lib/browser/{chunk-APHOLYUB.mjs.map → chunk-PGKZPKUD.mjs.map} +0 -0
- /package/dist/lib/browser/{chunk-NU4PBN33.mjs.map → chunk-VBF7YENS.mjs.map} +0 -0
- /package/dist/lib/node/{chunk-PYXHNAAK.cjs.map → chunk-57PB2HPY.cjs.map} +0 -0
- /package/dist/lib/node/{chunk-CN3RPESU.cjs.map → chunk-6LWBQAQZ.cjs.map} +0 -0
|
@@ -2,30 +2,84 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React from 'react';
|
|
5
|
+
import React, { useCallback } from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { useIntentDispatcher } from '@dxos/app-framework';
|
|
8
|
+
import { fullyQualifiedId } from '@dxos/react-client/echo';
|
|
9
|
+
import { useIsDirectlyAttended } from '@dxos/react-ui-attention';
|
|
10
|
+
import { focusRing, mx } from '@dxos/react-ui-theme';
|
|
9
11
|
|
|
10
12
|
import { Sheet, type SheetRootProps } from './Sheet';
|
|
13
|
+
import { Toolbar, type ToolbarAction } from './Toolbar';
|
|
14
|
+
|
|
15
|
+
// TODO(Zan): Factor out, copied this from MarkdownPlugin.
|
|
16
|
+
const attentionFragment = mx(
|
|
17
|
+
'group-focus-within/editor:attention-surface group-[[aria-current]]/editor:attention-surface',
|
|
18
|
+
'group-focus-within/editor:border-separator',
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// TODO(Zan): Factor out, copied this from MarkdownPlugin.
|
|
22
|
+
export const sectionToolbarLayout =
|
|
23
|
+
'bs-[--rail-action] bg-[--sticky-bg] sticky block-start-0 __-block-start-px transition-opacity';
|
|
24
|
+
|
|
25
|
+
const SheetContainer = ({ sheet, space, role, remoteFunctionUrl }: SheetRootProps & { role?: string }) => {
|
|
26
|
+
const dispatch = useIntentDispatcher();
|
|
27
|
+
|
|
28
|
+
const id = fullyQualifiedId(sheet);
|
|
29
|
+
const isDirectlyAttended = useIsDirectlyAttended(id);
|
|
30
|
+
|
|
31
|
+
// TODO(Zan): Centralise the toolbar action handler. Current implementation in stories.
|
|
32
|
+
const handleAction = useCallback(
|
|
33
|
+
(action: ToolbarAction) => {
|
|
34
|
+
switch (action.type) {
|
|
35
|
+
case 'comment': {
|
|
36
|
+
// TODO(Zan): We shouldn't hardcode the action ID.
|
|
37
|
+
void dispatch({
|
|
38
|
+
action: 'dxos.org/plugin/thread/action/create',
|
|
39
|
+
data: {
|
|
40
|
+
cursor: action.anchor,
|
|
41
|
+
name: action.cellContent,
|
|
42
|
+
subject: sheet,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
[sheet, dispatch],
|
|
49
|
+
);
|
|
11
50
|
|
|
12
|
-
const SheetContainer = ({
|
|
13
|
-
sheet,
|
|
14
|
-
space,
|
|
15
|
-
role,
|
|
16
|
-
remoteFunctionUrl,
|
|
17
|
-
}: SheetRootProps & { role?: string; coordinate?: LayoutCoordinate }) => {
|
|
18
51
|
return (
|
|
19
|
-
<div
|
|
20
|
-
role='none'
|
|
21
|
-
className={mx(
|
|
22
|
-
'flex',
|
|
23
|
-
role === 'article' && 'row-span-2',
|
|
24
|
-
role === 'section' && 'aspect-square border-y border-is border-separator',
|
|
25
|
-
)}
|
|
26
|
-
>
|
|
52
|
+
<div role='none' className={role === 'article' ? 'row-span-2 grid grid-rows-subgrid' : undefined}>
|
|
27
53
|
<Sheet.Root sheet={sheet} space={space} remoteFunctionUrl={remoteFunctionUrl}>
|
|
28
|
-
<
|
|
54
|
+
<div role='none' className={mx('flex flex-0 justify-center overflow-x-auto')}>
|
|
55
|
+
<Toolbar.Root
|
|
56
|
+
onAction={handleAction}
|
|
57
|
+
classNames={mx(
|
|
58
|
+
role === 'section'
|
|
59
|
+
? ['z-[2] group-focus-within/section:visible', !isDirectlyAttended && 'invisible', sectionToolbarLayout]
|
|
60
|
+
: 'group-focus-within/editor:border-separator group-[[aria-current]]/editor:border-separator',
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{/* TODO(Zan): Restore some of this functionality */}
|
|
64
|
+
{/* <Toolbar.Styles /> */}
|
|
65
|
+
{/* <Toolbar.Format /> */}
|
|
66
|
+
{/* <Toolbar.Alignment /> */}
|
|
67
|
+
<Toolbar.Separator />
|
|
68
|
+
<Toolbar.Actions />
|
|
69
|
+
</Toolbar.Root>
|
|
70
|
+
</div>
|
|
71
|
+
<div
|
|
72
|
+
role='none'
|
|
73
|
+
className={mx(
|
|
74
|
+
role === 'section' && 'aspect-square border-is border-bs border-be border-separator',
|
|
75
|
+
role === 'article' &&
|
|
76
|
+
'flex is-full overflow-hidden focus-visible:ring-inset row-span-1 data-[toolbar=disabled]:pbs-2 data-[toolbar=disabled]:row-span-2 border-bs border-separator',
|
|
77
|
+
focusRing,
|
|
78
|
+
attentionFragment,
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
<Sheet.Main />
|
|
82
|
+
</div>
|
|
29
83
|
</Sheet.Root>
|
|
30
84
|
</div>
|
|
31
85
|
);
|
|
@@ -23,22 +23,31 @@ import {
|
|
|
23
23
|
type ThemedClassName,
|
|
24
24
|
useTranslation,
|
|
25
25
|
} from '@dxos/react-ui';
|
|
26
|
+
import { nonNullable } from '@dxos/util';
|
|
26
27
|
|
|
27
28
|
import { ToolbarButton, ToolbarSeparator, ToolbarToggleButton } from './common';
|
|
28
29
|
import { SHEET_PLUGIN } from '../../meta';
|
|
30
|
+
import { addressToIndex } from '../../model';
|
|
29
31
|
import { type Formatting } from '../../types';
|
|
32
|
+
import { useSheetContext } from '../Sheet/sheet-context';
|
|
30
33
|
|
|
31
34
|
//
|
|
32
35
|
// Root
|
|
33
36
|
//
|
|
34
37
|
|
|
35
|
-
export type
|
|
38
|
+
export type ToolbarAction =
|
|
39
|
+
| { type: 'clear' }
|
|
40
|
+
| { type: 'highlight' }
|
|
41
|
+
| { type: 'left' }
|
|
42
|
+
| { type: 'center' }
|
|
43
|
+
| { type: 'right' }
|
|
44
|
+
| { type: 'date' }
|
|
45
|
+
| { type: 'currency' }
|
|
46
|
+
| { type: 'comment'; anchor: string; cellContent?: string };
|
|
36
47
|
|
|
37
|
-
export type
|
|
38
|
-
type: ToolbarActionType;
|
|
39
|
-
};
|
|
48
|
+
export type ToolbarActionType = ToolbarAction['type'];
|
|
40
49
|
|
|
41
|
-
export type ToolbarActionHandler = (
|
|
50
|
+
export type ToolbarActionHandler = (action: ToolbarAction) => void;
|
|
42
51
|
|
|
43
52
|
export type ToolbarProps = ThemedClassName<
|
|
44
53
|
PropsWithChildren<{
|
|
@@ -96,7 +105,7 @@ const Format = () => {
|
|
|
96
105
|
Icon={Icon}
|
|
97
106
|
// disabled={state?.blockType === 'codeblock'}
|
|
98
107
|
// onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
99
|
-
onClick={() => onAction?.({ type })}
|
|
108
|
+
onClick={() => onAction?.({ type: type as Exclude<typeof type, 'comment'> })}
|
|
100
109
|
>
|
|
101
110
|
{t(`toolbar ${type} label`)}
|
|
102
111
|
</ToolbarToggleButton>
|
|
@@ -127,7 +136,7 @@ const Alignment = () => {
|
|
|
127
136
|
Icon={Icon}
|
|
128
137
|
// disabled={state?.blockType === 'codeblock'}
|
|
129
138
|
// onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
130
|
-
onClick={() => onAction?.({ type })}
|
|
139
|
+
onClick={() => onAction?.({ type: type as Exclude<typeof type, 'comment'> })}
|
|
131
140
|
>
|
|
132
141
|
{t(`toolbar ${type} label`)}
|
|
133
142
|
</ToolbarToggleButton>
|
|
@@ -157,7 +166,7 @@ const Styles = () => {
|
|
|
157
166
|
Icon={Icon}
|
|
158
167
|
// disabled={state?.blockType === 'codeblock'}
|
|
159
168
|
// onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
160
|
-
onClick={() => onAction?.({ type })}
|
|
169
|
+
onClick={() => onAction?.({ type: type as Exclude<typeof type, 'comment'> })}
|
|
161
170
|
>
|
|
162
171
|
{t(`toolbar ${type} label`)}
|
|
163
172
|
</ToolbarToggleButton>
|
|
@@ -170,18 +179,51 @@ const Styles = () => {
|
|
|
170
179
|
// Actions
|
|
171
180
|
//
|
|
172
181
|
|
|
182
|
+
// TODO(Zan): Instead of taking props, can we access the state from sheet context?
|
|
173
183
|
const Actions = () => {
|
|
174
|
-
|
|
184
|
+
const { onAction } = useToolbarContext('Actions');
|
|
185
|
+
const { cursor, range, model } = useSheetContext();
|
|
175
186
|
const { t } = useTranslation(SHEET_PLUGIN);
|
|
187
|
+
|
|
188
|
+
const overlapsCommentAnchor = (model.sheet.threads ?? [])
|
|
189
|
+
.filter(nonNullable)
|
|
190
|
+
.filter((thread) => thread.status !== 'resolved')
|
|
191
|
+
.some((thread) => {
|
|
192
|
+
if (!cursor) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return addressToIndex(model.sheet, cursor) === thread.anchor;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const hasCursor = !!cursor;
|
|
199
|
+
const cursorOnly = hasCursor && !range && !overlapsCommentAnchor;
|
|
200
|
+
|
|
201
|
+
const tooltipLabelKey = !hasCursor
|
|
202
|
+
? 'no cursor label'
|
|
203
|
+
: overlapsCommentAnchor
|
|
204
|
+
? 'selection overlaps existing comment label'
|
|
205
|
+
: range
|
|
206
|
+
? 'comment ranges not supported label'
|
|
207
|
+
: 'comment label';
|
|
208
|
+
|
|
176
209
|
return (
|
|
177
210
|
<ToolbarButton
|
|
178
211
|
value='comment'
|
|
179
212
|
Icon={ChatText}
|
|
180
213
|
data-testid='editor.toolbar.comment'
|
|
181
|
-
|
|
182
|
-
|
|
214
|
+
onClick={() => {
|
|
215
|
+
if (!cursor) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
return onAction?.({
|
|
219
|
+
type: 'comment',
|
|
220
|
+
anchor: addressToIndex(model.sheet, cursor),
|
|
221
|
+
cellContent: model.getCellText(cursor),
|
|
222
|
+
});
|
|
223
|
+
}}
|
|
224
|
+
disabled={!cursorOnly || overlapsCommentAnchor}
|
|
183
225
|
>
|
|
184
|
-
{t(
|
|
226
|
+
{t(tooltipLabelKey)}
|
|
185
227
|
</ToolbarButton>
|
|
186
228
|
);
|
|
187
229
|
};
|
package/src/model/index.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { expect } from '
|
|
6
|
-
import { describe, test } from 'vitest';
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
7
6
|
|
|
8
7
|
import { SheetModel } from './model';
|
|
9
8
|
import { addressFromA1Notation, rangeFromA1Notation } from './types';
|
package/src/model/model.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { type FunctionType } from '@dxos/plugin-script/types';
|
|
|
17
17
|
|
|
18
18
|
import { defaultFunctions, type FunctionDefinition } from './functions';
|
|
19
19
|
import { addressFromA1Notation, addressToA1Notation, type CellAddress, type CellRange } from './types';
|
|
20
|
-
import { createIndices, RangeException, ReadonlyException } from './util';
|
|
20
|
+
import { addressFromIndex, addressToIndex, createIndices, RangeException, ReadonlyException } from './util';
|
|
21
21
|
import { type ComputeGraph } from '../components';
|
|
22
22
|
import { type CellScalarValue, type CellValue, type SheetType, ValueTypeEnum } from '../types';
|
|
23
23
|
|
|
@@ -186,7 +186,7 @@ export class SheetModel {
|
|
|
186
186
|
reset() {
|
|
187
187
|
this._graph.hf.clearSheet(this._sheetId);
|
|
188
188
|
Object.entries(this._sheet.cells).forEach(([key, { value }]) => {
|
|
189
|
-
const { column, row } = this.
|
|
189
|
+
const { column, row } = addressFromIndex(this._sheet, key);
|
|
190
190
|
if (typeof value === 'string' && value.charAt(0) === '=') {
|
|
191
191
|
value = this.mapFormulaBindingToFormula(this.mapFormulaBindingFromId(this.mapFormulaIndicesToRefs(value)));
|
|
192
192
|
}
|
|
@@ -220,7 +220,6 @@ export class SheetModel {
|
|
|
220
220
|
// Undoable actions.
|
|
221
221
|
// TODO(burdon): Group undoable methods; consistently update hf/sheet.
|
|
222
222
|
//
|
|
223
|
-
|
|
224
223
|
/**
|
|
225
224
|
* Clear range of values.
|
|
226
225
|
*/
|
|
@@ -229,7 +228,7 @@ export class SheetModel {
|
|
|
229
228
|
const values = this._iterRange(range, () => null);
|
|
230
229
|
this._graph.hf.setCellContents(toSimpleCellAddress(this._sheetId, topLeft), values);
|
|
231
230
|
this._iterRange(range, (cell) => {
|
|
232
|
-
const idx = this.
|
|
231
|
+
const idx = addressToIndex(this._sheet, cell);
|
|
233
232
|
delete this._sheet.cells[idx];
|
|
234
233
|
});
|
|
235
234
|
}
|
|
@@ -237,7 +236,7 @@ export class SheetModel {
|
|
|
237
236
|
cut(range: CellRange) {
|
|
238
237
|
this._graph.hf.cut(toModelRange(this._sheetId, range));
|
|
239
238
|
this._iterRange(range, (cell) => {
|
|
240
|
-
const idx = this.
|
|
239
|
+
const idx = addressToIndex(this._sheet, cell);
|
|
241
240
|
delete this._sheet.cells[idx];
|
|
242
241
|
});
|
|
243
242
|
}
|
|
@@ -252,7 +251,7 @@ export class SheetModel {
|
|
|
252
251
|
for (const change of changes) {
|
|
253
252
|
if (change instanceof ExportedCellChange) {
|
|
254
253
|
const { address, newValue } = change;
|
|
255
|
-
const idx = this.
|
|
254
|
+
const idx = addressToIndex(this._sheet, { row: address.row, column: address.col });
|
|
256
255
|
this._sheet.cells[idx] = { value: newValue };
|
|
257
256
|
}
|
|
258
257
|
}
|
|
@@ -278,7 +277,7 @@ export class SheetModel {
|
|
|
278
277
|
* Get value from sheet.
|
|
279
278
|
*/
|
|
280
279
|
getCellValue(cell: CellAddress): CellScalarValue {
|
|
281
|
-
const idx = this.
|
|
280
|
+
const idx = addressToIndex(this._sheet, cell);
|
|
282
281
|
return this._sheet.cells[idx]?.value ?? null;
|
|
283
282
|
}
|
|
284
283
|
|
|
@@ -356,7 +355,7 @@ export class SheetModel {
|
|
|
356
355
|
]);
|
|
357
356
|
|
|
358
357
|
// Insert into sheet.
|
|
359
|
-
const idx = this.
|
|
358
|
+
const idx = addressToIndex(this._sheet, cell);
|
|
360
359
|
if (value === undefined || value === null) {
|
|
361
360
|
delete this._sheet.cells[idx];
|
|
362
361
|
} else {
|
|
@@ -426,39 +425,6 @@ export class SheetModel {
|
|
|
426
425
|
// Indices.
|
|
427
426
|
//
|
|
428
427
|
|
|
429
|
-
/**
|
|
430
|
-
* E.g., "A1" => "x1@y1".
|
|
431
|
-
*/
|
|
432
|
-
addressToIndex(cell: CellAddress): CellIndex {
|
|
433
|
-
return `${this._sheet.columns[cell.column]}@${this._sheet.rows[cell.row]}`;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* E.g., "x1@y1" => "A1".
|
|
438
|
-
*/
|
|
439
|
-
addressFromIndex(idx: CellIndex): CellAddress {
|
|
440
|
-
const [column, row] = idx.split('@');
|
|
441
|
-
return {
|
|
442
|
-
column: this._sheet.columns.indexOf(column),
|
|
443
|
-
row: this._sheet.rows.indexOf(row),
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* E.g., "A1:B2" => "x1@y1:x2@y2".
|
|
449
|
-
*/
|
|
450
|
-
rangeToIndex(range: CellRange): string {
|
|
451
|
-
return [range.from, range.to ?? range.from].map((cell) => this.addressToIndex(cell)).join(':');
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* E.g., "x1@y1:x2@y2" => "A1:B2".
|
|
456
|
-
*/
|
|
457
|
-
rangeFromIndex(idx: string): CellRange {
|
|
458
|
-
const [from, to] = idx.split(':').map((idx) => this.addressFromIndex(idx));
|
|
459
|
-
return { from, to };
|
|
460
|
-
}
|
|
461
|
-
|
|
462
428
|
/**
|
|
463
429
|
* E.g., "HELLO()" => "EDGE("HELLO")".
|
|
464
430
|
*/
|
|
@@ -508,7 +474,7 @@ export class SheetModel {
|
|
|
508
474
|
mapFormulaRefsToIndices(formula: string): string {
|
|
509
475
|
invariant(formula.charAt(0) === '=');
|
|
510
476
|
return formula.replace(/([a-zA-Z]+)([0-9]+)/g, (match) => {
|
|
511
|
-
return this.
|
|
477
|
+
return addressToIndex(this._sheet, addressFromA1Notation(match));
|
|
512
478
|
});
|
|
513
479
|
}
|
|
514
480
|
|
|
@@ -518,7 +484,7 @@ export class SheetModel {
|
|
|
518
484
|
mapFormulaIndicesToRefs(formula: string): string {
|
|
519
485
|
invariant(formula.charAt(0) === '=');
|
|
520
486
|
return formula.replace(/([a-zA-Z0-9]+)@([a-zA-Z0-9]+)/g, (idx) => {
|
|
521
|
-
return addressToA1Notation(this.
|
|
487
|
+
return addressToA1Notation(addressFromIndex(this._sheet, idx));
|
|
522
488
|
});
|
|
523
489
|
}
|
|
524
490
|
|
package/src/model/types.test.ts
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { getIndices, sortByIndex, getIndicesBelow, getIndicesAbove, getIndicesBetween } from '@tldraw/indices';
|
|
6
|
-
import { expect } from '
|
|
7
|
-
import { describe, test } from 'vitest';
|
|
6
|
+
import { describe, expect, test } from 'vitest';
|
|
8
7
|
|
|
9
8
|
import { inRange, addressFromA1Notation, addressToA1Notation, rangeFromA1Notation, rangeToA1Notation } from './types';
|
|
10
9
|
|
package/src/model/util.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { randomBytes } from '@dxos/crypto';
|
|
6
6
|
|
|
7
|
+
import { type CellRange, type CellAddress } from '.';
|
|
8
|
+
import { type SheetType } from '../types';
|
|
9
|
+
|
|
7
10
|
// TODO(burdon): Factor out from dxos/protocols to new common package.
|
|
8
11
|
export class ApiError extends Error {}
|
|
9
12
|
export class ReadonlyException extends ApiError {}
|
|
@@ -25,6 +28,39 @@ export const createIndex = (length = 8): string => {
|
|
|
25
28
|
|
|
26
29
|
export const createIndices = (length: number): string[] => Array.from({ length }).map(() => createIndex());
|
|
27
30
|
|
|
31
|
+
/**
|
|
32
|
+
* E.g., "A1" => "CA2@CB3".
|
|
33
|
+
*/
|
|
34
|
+
export const addressToIndex = (sheet: SheetType, cell: CellAddress): string => {
|
|
35
|
+
return `${sheet.columns[cell.column]}@${sheet.rows[cell.row]}`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* E.g., "CA2@CB3" => "A1".
|
|
40
|
+
*/
|
|
41
|
+
export const addressFromIndex = (sheet: SheetType, idx: string): CellAddress => {
|
|
42
|
+
const [column, row] = idx.split('@');
|
|
43
|
+
return {
|
|
44
|
+
column: sheet.columns.indexOf(column),
|
|
45
|
+
row: sheet.rows.indexOf(row),
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* E.g., "A1:B2" => "CA2@CB3:CC4@CD5".
|
|
51
|
+
*/
|
|
52
|
+
export const rangeToIndex = (sheet: SheetType, range: CellRange): string => {
|
|
53
|
+
return [range.from, range.to ?? range.from].map((cell) => addressToIndex(sheet, cell)).join(':');
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* E.g., "CA2@CB3:CC4@CD5" => "A1:B2".
|
|
58
|
+
*/
|
|
59
|
+
export const rangeFromIndex = (sheet: SheetType, idx: string): CellRange => {
|
|
60
|
+
const [from, to] = idx.split(':').map((index) => addressFromIndex(sheet, index));
|
|
61
|
+
return { from, to };
|
|
62
|
+
};
|
|
63
|
+
|
|
28
64
|
// TODO(burdon): Factor out.
|
|
29
65
|
export const pickOne = <T>(values: T[]): T => values[Math.floor(Math.random() * values.length)];
|
|
30
66
|
export const pickSome = <T>(values: T[], n = 1): T[] => {
|
|
@@ -34,3 +70,34 @@ export const pickSome = <T>(values: T[], n = 1): T[] => {
|
|
|
34
70
|
}
|
|
35
71
|
return Array.from(result.values());
|
|
36
72
|
};
|
|
73
|
+
|
|
74
|
+
export const closest = (cursor: CellAddress, cells: CellAddress[]): CellAddress | undefined => {
|
|
75
|
+
let closestCell: CellAddress | undefined;
|
|
76
|
+
let closestDistance = Number.MAX_SAFE_INTEGER;
|
|
77
|
+
|
|
78
|
+
for (const cell of cells) {
|
|
79
|
+
const distance = Math.abs(cell.row - cursor.row) + Math.abs(cell.column - cursor.column);
|
|
80
|
+
if (distance < closestDistance) {
|
|
81
|
+
closestCell = cell;
|
|
82
|
+
closestDistance = distance;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return closestCell;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Compares the positions of two cell indexes in a sheet.
|
|
91
|
+
* Sorts primarily by row, then by column if rows are equal.
|
|
92
|
+
*/
|
|
93
|
+
export const compareIndexPositions = (sheet: SheetType, indexA: string, indexB: string): number => {
|
|
94
|
+
const { row: rowA, column: columnA } = addressFromIndex(sheet, indexA);
|
|
95
|
+
const { row: rowB, column: columnB } = addressFromIndex(sheet, indexB);
|
|
96
|
+
|
|
97
|
+
// Sort by row first, then by column.
|
|
98
|
+
if (rowA !== rowB) {
|
|
99
|
+
return rowA - rowB;
|
|
100
|
+
} else {
|
|
101
|
+
return columnA - columnB;
|
|
102
|
+
}
|
|
103
|
+
};
|
package/src/translations.ts
CHANGED
|
@@ -16,7 +16,12 @@ export default [
|
|
|
16
16
|
'toolbar left label': 'Align left',
|
|
17
17
|
'toolbar left center': 'Align center',
|
|
18
18
|
'toolbar left right': 'Align right',
|
|
19
|
+
'selection overlaps existing comment label': 'Selected cell already has a comment',
|
|
20
|
+
'comment label': 'Add comment',
|
|
21
|
+
'comment ranges not supported label': 'Commenting on ranges is not yet supported',
|
|
22
|
+
'no cursor label': 'Select a cell to comment',
|
|
23
|
+
'open comment for sheet cell': 'View comments for cell',
|
|
19
24
|
},
|
|
20
25
|
},
|
|
21
26
|
},
|
|
22
|
-
];
|
|
27
|
+
] as const;
|
package/src/types.ts
CHANGED
|
@@ -9,8 +9,9 @@ import type {
|
|
|
9
9
|
SurfaceProvides,
|
|
10
10
|
TranslationsProvides,
|
|
11
11
|
} from '@dxos/app-framework';
|
|
12
|
-
import { create, S, TypedObject } from '@dxos/echo-schema';
|
|
12
|
+
import { create, ref, S, TypedObject } from '@dxos/echo-schema';
|
|
13
13
|
import { type SchemaProvides } from '@dxos/plugin-client';
|
|
14
|
+
import { ThreadType } from '@dxos/plugin-space/types';
|
|
14
15
|
import { type StackProvides } from '@dxos/plugin-stack';
|
|
15
16
|
|
|
16
17
|
import { SHEET_PLUGIN } from './meta';
|
|
@@ -21,13 +22,24 @@ export enum SheetAction {
|
|
|
21
22
|
CREATE = `${SHEET_ACTION}/create`,
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
// TODO(Zan): Move this to the plugin-space plugin or another common location
|
|
26
|
+
// when we implement comments in sheets.
|
|
27
|
+
// This is currently duplicated in a few places.
|
|
28
|
+
type ThreadProvides<T> = {
|
|
29
|
+
thread: {
|
|
30
|
+
predicate: (obj: any) => obj is T;
|
|
31
|
+
createSort: (obj: T) => (anchorA: string | undefined, anchorB: string | undefined) => number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
24
35
|
export type SheetPluginProvides = SurfaceProvides &
|
|
25
36
|
IntentResolverProvides &
|
|
26
37
|
GraphBuilderProvides &
|
|
27
38
|
MetadataRecordsProvides &
|
|
28
39
|
TranslationsProvides &
|
|
29
40
|
SchemaProvides &
|
|
30
|
-
StackProvides
|
|
41
|
+
StackProvides &
|
|
42
|
+
ThreadProvides<SheetType>;
|
|
31
43
|
|
|
32
44
|
export type CellScalarValue = number | string | boolean | null;
|
|
33
45
|
|
|
@@ -105,8 +117,19 @@ export class SheetType extends TypedObject({ typename: 'dxos.org/type/SheetType'
|
|
|
105
117
|
|
|
106
118
|
// Cell formatting referenced by indexed range.
|
|
107
119
|
formatting: S.mutable(S.Record(S.String, S.mutable(Formatting))),
|
|
120
|
+
|
|
121
|
+
// Threads associated with the sheet
|
|
122
|
+
threads: S.optional(S.mutable(S.Array(ref(ThreadType)))),
|
|
108
123
|
}) {}
|
|
109
124
|
|
|
110
125
|
// TODO(burdon): Fix defaults.
|
|
111
126
|
export const createSheet = (title?: string): SheetType =>
|
|
112
|
-
create(SheetType, {
|
|
127
|
+
create(SheetType, {
|
|
128
|
+
title,
|
|
129
|
+
cells: {},
|
|
130
|
+
rows: [],
|
|
131
|
+
columns: [],
|
|
132
|
+
rowMeta: {},
|
|
133
|
+
columnMeta: {},
|
|
134
|
+
formatting: {},
|
|
135
|
+
});
|