@dxos/plugin-deck 0.6.8-main.046e6cf
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/LICENSE +8 -0
- package/README.md +15 -0
- package/dist/lib/browser/chunk-YVHGFQQR.mjs +12 -0
- package/dist/lib/browser/chunk-YVHGFQQR.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1657 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/meta.mjs +9 -0
- package/dist/lib/browser/meta.mjs.map +7 -0
- package/dist/types/src/DeckPlugin.d.ts +15 -0
- package/dist/types/src/DeckPlugin.d.ts.map +1 -0
- package/dist/types/src/components/DeckContext.d.ts +8 -0
- package/dist/types/src/components/DeckContext.d.ts.map +1 -0
- package/dist/types/src/components/DeckLayout/ActiveNode.d.ts +5 -0
- package/dist/types/src/components/DeckLayout/ActiveNode.d.ts.map +1 -0
- package/dist/types/src/components/DeckLayout/ComplementarySidebar.d.ts +9 -0
- package/dist/types/src/components/DeckLayout/ComplementarySidebar.d.ts.map +1 -0
- package/dist/types/src/components/DeckLayout/ContentEmpty.d.ts +3 -0
- package/dist/types/src/components/DeckLayout/ContentEmpty.d.ts.map +1 -0
- package/dist/types/src/components/DeckLayout/DeckLayout.d.ts +25 -0
- package/dist/types/src/components/DeckLayout/DeckLayout.d.ts.map +1 -0
- package/dist/types/src/components/DeckLayout/Fallback.d.ts +3 -0
- package/dist/types/src/components/DeckLayout/Fallback.d.ts.map +1 -0
- package/dist/types/src/components/DeckLayout/Fullscreen.d.ts +5 -0
- package/dist/types/src/components/DeckLayout/Fullscreen.d.ts.map +1 -0
- package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts +14 -0
- package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts.map +1 -0
- package/dist/types/src/components/DeckLayout/Plank.d.ts +14 -0
- package/dist/types/src/components/DeckLayout/Plank.d.ts.map +1 -0
- package/dist/types/src/components/DeckLayout/PlankError.d.ts +14 -0
- package/dist/types/src/components/DeckLayout/PlankError.d.ts.map +1 -0
- package/dist/types/src/components/DeckLayout/PlankLoading.d.ts +3 -0
- package/dist/types/src/components/DeckLayout/PlankLoading.d.ts.map +1 -0
- package/dist/types/src/components/DeckLayout/Sidebar.d.ts +8 -0
- package/dist/types/src/components/DeckLayout/Sidebar.d.ts.map +1 -0
- package/dist/types/src/components/DeckLayout/Toast.d.ts +5 -0
- package/dist/types/src/components/DeckLayout/Toast.d.ts.map +1 -0
- package/dist/types/src/components/DeckLayout/constants.d.ts +3 -0
- package/dist/types/src/components/DeckLayout/constants.d.ts.map +1 -0
- package/dist/types/src/components/DeckLayout/index.d.ts +3 -0
- package/dist/types/src/components/DeckLayout/index.d.ts.map +1 -0
- package/dist/types/src/components/LayoutContext.d.ts +5 -0
- package/dist/types/src/components/LayoutContext.d.ts.map +1 -0
- package/dist/types/src/components/LayoutSettings.d.ts +6 -0
- package/dist/types/src/components/LayoutSettings.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +5 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +3 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useNode.d.ts +11 -0
- package/dist/types/src/hooks/useNode.d.ts.map +1 -0
- package/dist/types/src/hooks/useNodeActionExpander.d.ts +3 -0
- package/dist/types/src/hooks/useNodeActionExpander.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/layout.d.ts +25 -0
- package/dist/types/src/layout.d.ts.map +1 -0
- package/dist/types/src/layout.test.d.ts +2 -0
- package/dist/types/src/layout.test.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +7 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +41 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +16 -0
- package/dist/types/src/types.d.ts.map +1 -0
- package/dist/types/src/util/check-app-scheme.d.ts +2 -0
- package/dist/types/src/util/check-app-scheme.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +4 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/util/layout-parts.d.ts +7 -0
- package/dist/types/src/util/layout-parts.d.ts.map +1 -0
- package/dist/types/src/util/overscroll.d.ts +7 -0
- package/dist/types/src/util/overscroll.d.ts.map +1 -0
- package/package.json +76 -0
- package/src/DeckPlugin.tsx +629 -0
- package/src/components/DeckContext.ts +14 -0
- package/src/components/DeckLayout/ActiveNode.tsx +24 -0
- package/src/components/DeckLayout/ComplementarySidebar.tsx +58 -0
- package/src/components/DeckLayout/ContentEmpty.tsx +21 -0
- package/src/components/DeckLayout/DeckLayout.tsx +270 -0
- package/src/components/DeckLayout/Fallback.tsx +28 -0
- package/src/components/DeckLayout/Fullscreen.tsx +32 -0
- package/src/components/DeckLayout/NodePlankHeading.tsx +160 -0
- package/src/components/DeckLayout/Plank.tsx +142 -0
- package/src/components/DeckLayout/PlankError.tsx +64 -0
- package/src/components/DeckLayout/PlankLoading.tsx +15 -0
- package/src/components/DeckLayout/Sidebar.tsx +43 -0
- package/src/components/DeckLayout/Toast.tsx +48 -0
- package/src/components/DeckLayout/constants.ts +6 -0
- package/src/components/DeckLayout/index.ts +6 -0
- package/src/components/LayoutContext.ts +12 -0
- package/src/components/LayoutSettings.tsx +86 -0
- package/src/components/index.ts +8 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useNode.ts +40 -0
- package/src/hooks/useNodeActionExpander.ts +24 -0
- package/src/index.ts +9 -0
- package/src/layout.test.ts +380 -0
- package/src/layout.ts +245 -0
- package/src/meta.ts +10 -0
- package/src/translations.ts +47 -0
- package/src/types.ts +38 -0
- package/src/util/check-app-scheme.ts +21 -0
- package/src/util/index.ts +7 -0
- package/src/util/layout-parts.ts +12 -0
- package/src/util/overscroll.ts +97 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect, describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { type LayoutParts, type LayoutAdjustment, type LayoutEntry } from '@dxos/app-framework';
|
|
8
|
+
|
|
9
|
+
import { uriToActiveParts, activePartsToUri, incrementPlank, mergeLayoutParts, openEntry } from './layout';
|
|
10
|
+
|
|
11
|
+
describe('Layout URI parsing and formatting', () => {
|
|
12
|
+
test('uriToActiveParts parses a simple URI correctly', () => {
|
|
13
|
+
const uri = 'https://composer.space/main-id1~path1+id2_sidebar-id3';
|
|
14
|
+
const result = uriToActiveParts(uri);
|
|
15
|
+
expect(result).to.deep.equal({
|
|
16
|
+
main: [{ id: 'id1', path: 'path1' }, { id: 'id2' }],
|
|
17
|
+
sidebar: [{ id: 'id3' }],
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('activePartsToUri formats a simple object correctly', () => {
|
|
22
|
+
const activeParts: LayoutParts = {
|
|
23
|
+
main: [{ id: 'id1', path: 'path1' }, { id: 'id2' }],
|
|
24
|
+
sidebar: [{ id: 'id3' }],
|
|
25
|
+
};
|
|
26
|
+
const result = activePartsToUri(activeParts);
|
|
27
|
+
expect(result).to.equal('main-id1~path1+id2_sidebar-id3');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('activePartsToUri handles complex cases with multiple parts, and simple paths', () => {
|
|
31
|
+
const complexActiveParts: LayoutParts = {
|
|
32
|
+
main: [{ id: 'id1', path: 'path1' }, { id: 'id2' }, { id: 'id3', path: 'path3' }],
|
|
33
|
+
sidebar: [{ id: 'id4' }, { id: 'id5', path: 'path5' }],
|
|
34
|
+
complementary: [{ id: 'id6', path: 'path6' }, { id: 'id7' }],
|
|
35
|
+
};
|
|
36
|
+
const result = activePartsToUri(complexActiveParts);
|
|
37
|
+
expect(result).to.equal('main-id1~path1+id2+id3~path3_sidebar-id4+id5~path5_complementary-id6~path6+id7');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('Round trip: URI to object and back to URI', () => {
|
|
41
|
+
const originalUri = 'main-id1~path1+id2_sidebar-id3_complementary-id4~path4';
|
|
42
|
+
const activeParts = uriToActiveParts(originalUri);
|
|
43
|
+
const resultUri = activePartsToUri(activeParts);
|
|
44
|
+
expect(resultUri).to.equal(originalUri);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('Round trip: object to URI and back to object', () => {
|
|
48
|
+
const originalParts: LayoutParts = {
|
|
49
|
+
main: [{ id: 'id1', path: 'path1' }, { id: 'id2' }],
|
|
50
|
+
sidebar: [{ id: 'id3' }],
|
|
51
|
+
complementary: [{ id: 'id4', path: 'path4' }],
|
|
52
|
+
};
|
|
53
|
+
const uri = activePartsToUri(originalParts);
|
|
54
|
+
const resultParts = uriToActiveParts(`https://composer.space/${uri}`);
|
|
55
|
+
expect(resultParts).to.deep.equal(originalParts);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('uriToActiveParts handles missing parts', () => {
|
|
59
|
+
const uri = 'https://composer.space/main-id1~path1_sidebar-id2';
|
|
60
|
+
const result = uriToActiveParts(uri);
|
|
61
|
+
expect(result).to.deep.equal({
|
|
62
|
+
main: [{ id: 'id1', path: 'path1' }],
|
|
63
|
+
sidebar: [{ id: 'id2' }],
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('activePartsToUri excludes empty parts', () => {
|
|
68
|
+
const activeParts: LayoutParts = {
|
|
69
|
+
main: [{ id: 'id1', path: 'path1' }],
|
|
70
|
+
};
|
|
71
|
+
const result = activePartsToUri(activeParts);
|
|
72
|
+
expect(result).to.equal('main-id1~path1');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('Layout adjustment', () => {
|
|
77
|
+
test('adjustLayout moves an item left in the main part', () => {
|
|
78
|
+
const layout: LayoutParts = {
|
|
79
|
+
main: [{ id: 'id1' }, { id: 'id2' }, { id: 'id3' }],
|
|
80
|
+
sidebar: [{ id: 'sid1' }],
|
|
81
|
+
};
|
|
82
|
+
const adjustment: LayoutAdjustment = {
|
|
83
|
+
layoutCoordinate: { part: 'main', entryId: 'id2' },
|
|
84
|
+
type: 'increment-start',
|
|
85
|
+
};
|
|
86
|
+
const result = incrementPlank(layout, adjustment);
|
|
87
|
+
expect(result.main).to.deep.equal([{ id: 'id2' }, { id: 'id1' }, { id: 'id3' }]);
|
|
88
|
+
expect(result.sidebar).to.deep.equal([{ id: 'sid1' }]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('adjustLayout moves an item right in the main part', () => {
|
|
92
|
+
const layout: LayoutParts = {
|
|
93
|
+
main: [{ id: 'id1' }, { id: 'id2' }, { id: 'id3' }],
|
|
94
|
+
sidebar: [{ id: 'sid1' }],
|
|
95
|
+
};
|
|
96
|
+
const adjustment: LayoutAdjustment = {
|
|
97
|
+
layoutCoordinate: { part: 'main', entryId: 'id2' },
|
|
98
|
+
type: 'increment-end',
|
|
99
|
+
};
|
|
100
|
+
const result = incrementPlank(layout, adjustment);
|
|
101
|
+
expect(result.main).to.deep.equal([{ id: 'id1' }, { id: 'id3' }, { id: 'id2' }]);
|
|
102
|
+
expect(result.sidebar).to.deep.equal([{ id: 'sid1' }]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('adjustLayout does not move items in non-main parts', () => {
|
|
106
|
+
const layout: LayoutParts = {
|
|
107
|
+
main: [{ id: 'id1' }],
|
|
108
|
+
sidebar: [{ id: 'sid1' }, { id: 'sid2' }, { id: 'sid3' }],
|
|
109
|
+
};
|
|
110
|
+
const adjustment: LayoutAdjustment = {
|
|
111
|
+
layoutCoordinate: { part: 'sidebar', entryId: 'sid2' },
|
|
112
|
+
type: 'increment-end',
|
|
113
|
+
};
|
|
114
|
+
const result = incrementPlank(layout, adjustment);
|
|
115
|
+
expect(result).to.deep.equal(layout);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('adjustLayout does not move the first item left in main', () => {
|
|
119
|
+
const layout: LayoutParts = {
|
|
120
|
+
main: [{ id: 'id1' }, { id: 'id2' }],
|
|
121
|
+
};
|
|
122
|
+
const adjustment: LayoutAdjustment = {
|
|
123
|
+
layoutCoordinate: { part: 'main', entryId: 'id1' },
|
|
124
|
+
type: 'increment-start',
|
|
125
|
+
};
|
|
126
|
+
const result = incrementPlank(layout, adjustment);
|
|
127
|
+
expect(result).to.deep.equal(layout);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('adjustLayout does not move the last item right in main', () => {
|
|
131
|
+
const layout: LayoutParts = {
|
|
132
|
+
main: [{ id: 'id1' }, { id: 'id2' }],
|
|
133
|
+
};
|
|
134
|
+
const adjustment: LayoutAdjustment = {
|
|
135
|
+
layoutCoordinate: { part: 'main', entryId: 'id2' },
|
|
136
|
+
type: 'increment-end',
|
|
137
|
+
};
|
|
138
|
+
const result = incrementPlank(layout, adjustment);
|
|
139
|
+
expect(result).to.deep.equal(layout);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('adjustLayout handles non-existent slugId in main', () => {
|
|
143
|
+
const layout: LayoutParts = {
|
|
144
|
+
main: [{ id: 'id1' }, { id: 'id2' }],
|
|
145
|
+
};
|
|
146
|
+
const adjustment: LayoutAdjustment = {
|
|
147
|
+
layoutCoordinate: { part: 'main', entryId: 'id3' },
|
|
148
|
+
type: 'increment-start',
|
|
149
|
+
};
|
|
150
|
+
const result = incrementPlank(layout, adjustment);
|
|
151
|
+
expect(result).to.deep.equal(layout);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('adjustLayout preserves other parts when adjusting main', () => {
|
|
155
|
+
const layout: LayoutParts = {
|
|
156
|
+
main: [{ id: 'id1' }, { id: 'id2' }],
|
|
157
|
+
sidebar: [{ id: 'sid1' }],
|
|
158
|
+
complementary: [{ id: 'cid1' }],
|
|
159
|
+
};
|
|
160
|
+
const adjustment: LayoutAdjustment = {
|
|
161
|
+
layoutCoordinate: { part: 'main', entryId: 'id2' },
|
|
162
|
+
type: 'increment-start',
|
|
163
|
+
};
|
|
164
|
+
const result = incrementPlank(layout, adjustment);
|
|
165
|
+
expect(result.main).to.deep.equal([{ id: 'id2' }, { id: 'id1' }]);
|
|
166
|
+
expect(result.sidebar).to.deep.equal([{ id: 'sid1' }]);
|
|
167
|
+
expect(result.complementary).to.deep.equal([{ id: 'cid1' }]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('adjustLayout handles empty main part', () => {
|
|
171
|
+
const layout: LayoutParts = {
|
|
172
|
+
main: [],
|
|
173
|
+
sidebar: [{ id: 'sid1' }],
|
|
174
|
+
};
|
|
175
|
+
const adjustment: LayoutAdjustment = {
|
|
176
|
+
layoutCoordinate: { part: 'main', entryId: 'id1' },
|
|
177
|
+
type: 'increment-start',
|
|
178
|
+
};
|
|
179
|
+
const result = incrementPlank(layout, adjustment);
|
|
180
|
+
expect(result).to.deep.equal(layout);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('adjustLayout handles undefined main part', () => {
|
|
184
|
+
const layout: LayoutParts = {
|
|
185
|
+
sidebar: [{ id: 'sid1' }],
|
|
186
|
+
};
|
|
187
|
+
const adjustment: LayoutAdjustment = {
|
|
188
|
+
layoutCoordinate: { part: 'main', entryId: 'id1' },
|
|
189
|
+
type: 'increment-start',
|
|
190
|
+
};
|
|
191
|
+
const result = incrementPlank(layout, adjustment);
|
|
192
|
+
expect(result).to.deep.equal(layout);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('adjustLayout handles main part with only one item', () => {
|
|
196
|
+
const layout: LayoutParts = {
|
|
197
|
+
main: [{ id: 'id1' }],
|
|
198
|
+
};
|
|
199
|
+
const adjustment: LayoutAdjustment = {
|
|
200
|
+
layoutCoordinate: { part: 'main', entryId: 'id1' },
|
|
201
|
+
type: 'increment-end',
|
|
202
|
+
};
|
|
203
|
+
const result = incrementPlank(layout, adjustment);
|
|
204
|
+
expect(result).to.deep.equal(layout);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('Layout parts merging', () => {
|
|
209
|
+
test('merges two simple layout parts', () => {
|
|
210
|
+
const part1: LayoutParts = { main: [{ id: 'id1' }] };
|
|
211
|
+
const part2: LayoutParts = { sidebar: [{ id: 'id2' }] };
|
|
212
|
+
const result = mergeLayoutParts(part1, part2);
|
|
213
|
+
expect(result).to.deep.equal({
|
|
214
|
+
main: [{ id: 'id1' }],
|
|
215
|
+
sidebar: [{ id: 'id2' }],
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('replaces entries with the same id in the same part', () => {
|
|
220
|
+
const part1: LayoutParts = { main: [{ id: 'id1', path: 'path1' }] };
|
|
221
|
+
const part2: LayoutParts = { main: [{ id: 'id1', path: 'path2' }] };
|
|
222
|
+
const result = mergeLayoutParts(part1, part2);
|
|
223
|
+
expect(result).to.deep.equal({
|
|
224
|
+
main: [{ id: 'id1', path: 'path2' }],
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('merges multiple layout parts', () => {
|
|
229
|
+
const part1: LayoutParts = { main: [{ id: 'id1' }] };
|
|
230
|
+
const part2: LayoutParts = { sidebar: [{ id: 'id2' }] };
|
|
231
|
+
const part3: LayoutParts = { complementary: [{ id: 'id3' }] };
|
|
232
|
+
const result = mergeLayoutParts(part1, part2, part3);
|
|
233
|
+
expect(result).to.deep.equal({
|
|
234
|
+
main: [{ id: 'id1' }],
|
|
235
|
+
sidebar: [{ id: 'id2' }],
|
|
236
|
+
complementary: [{ id: 'id3' }],
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('handles empty layout parts', () => {
|
|
241
|
+
const part1: LayoutParts = { main: [{ id: 'id1' }] };
|
|
242
|
+
const part2: LayoutParts = {};
|
|
243
|
+
const result = mergeLayoutParts(part1, part2);
|
|
244
|
+
expect(result).to.deep.equal({
|
|
245
|
+
main: [{ id: 'id1' }],
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('merges parts with multiple entries', () => {
|
|
250
|
+
const part1: LayoutParts = { main: [{ id: 'id1' }, { id: 'id2' }] };
|
|
251
|
+
const part2: LayoutParts = { main: [{ id: 'id3' }], sidebar: [{ id: 'id4' }] };
|
|
252
|
+
const result = mergeLayoutParts(part1, part2);
|
|
253
|
+
expect(result).to.deep.equal({
|
|
254
|
+
main: [{ id: 'id1' }, { id: 'id2' }, { id: 'id3' }],
|
|
255
|
+
sidebar: [{ id: 'id4' }],
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('replaces entries with the same id and keeps unique entries', () => {
|
|
260
|
+
const part1: LayoutParts = {
|
|
261
|
+
main: [{ id: 'id1', path: 'path1' }, { id: 'id2' }],
|
|
262
|
+
sidebar: [{ id: 'id3' }],
|
|
263
|
+
};
|
|
264
|
+
const part2: LayoutParts = {
|
|
265
|
+
main: [{ id: 'id1', path: 'path2' }, { id: 'id4' }],
|
|
266
|
+
complementary: [{ id: 'id5' }],
|
|
267
|
+
};
|
|
268
|
+
const result = mergeLayoutParts(part1, part2);
|
|
269
|
+
expect(result).to.deep.equal({
|
|
270
|
+
main: [{ id: 'id1', path: 'path2' }, { id: 'id2' }, { id: 'id4' }],
|
|
271
|
+
sidebar: [{ id: 'id3' }],
|
|
272
|
+
complementary: [{ id: 'id5' }],
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('merges complex layout parts', () => {
|
|
277
|
+
const part1: LayoutParts = {
|
|
278
|
+
main: [{ id: 'id1', path: 'path1' }, { id: 'id2' }],
|
|
279
|
+
sidebar: [{ id: 'id3' }],
|
|
280
|
+
};
|
|
281
|
+
const part2: LayoutParts = {
|
|
282
|
+
main: [{ id: 'id1', path: 'path2' }, { id: 'id4' }],
|
|
283
|
+
complementary: [{ id: 'id5' }],
|
|
284
|
+
};
|
|
285
|
+
const part3: LayoutParts = {
|
|
286
|
+
sidebar: [{ id: 'id6' }],
|
|
287
|
+
fullScreen: [{ id: 'id7' }],
|
|
288
|
+
};
|
|
289
|
+
const result = mergeLayoutParts(part1, part2, part3);
|
|
290
|
+
expect(result).to.deep.equal({
|
|
291
|
+
main: [{ id: 'id1', path: 'path2' }, { id: 'id2' }, { id: 'id4' }],
|
|
292
|
+
sidebar: [{ id: 'id3' }, { id: 'id6' }],
|
|
293
|
+
complementary: [{ id: 'id5' }],
|
|
294
|
+
fullScreen: [{ id: 'id7' }],
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('handles merging with duplicate entries in the same part', () => {
|
|
299
|
+
const part1: LayoutParts = {
|
|
300
|
+
main: [{ id: 'id1' }, { id: 'id1' }, { id: 'id2' }],
|
|
301
|
+
};
|
|
302
|
+
const result = mergeLayoutParts(part1);
|
|
303
|
+
expect(result).to.deep.equal({
|
|
304
|
+
main: [{ id: 'id1' }, { id: 'id2' }],
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('preserves order of entries when merging', () => {
|
|
309
|
+
const part1: LayoutParts = { main: [{ id: 'id1' }, { id: 'id2' }] };
|
|
310
|
+
const part2: LayoutParts = { main: [{ id: 'id3' }, { id: 'id1' }] };
|
|
311
|
+
const result = mergeLayoutParts(part1, part2);
|
|
312
|
+
expect(result).to.deep.equal({
|
|
313
|
+
main: [{ id: 'id1' }, { id: 'id2' }, { id: 'id3' }],
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('openEntry', () => {
|
|
319
|
+
const initialLayout: LayoutParts = {
|
|
320
|
+
main: [
|
|
321
|
+
{ id: 'id1', path: 'path1' },
|
|
322
|
+
{ id: 'id2', path: 'path2' },
|
|
323
|
+
{ id: 'id3', path: 'path3' },
|
|
324
|
+
],
|
|
325
|
+
sidebar: [{ id: 'sid1', path: 'sidepath1' }],
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const newEntry: LayoutEntry = { id: 'new', path: 'newpath' };
|
|
329
|
+
|
|
330
|
+
test('adds entry to start of main without pivot', () => {
|
|
331
|
+
const result = openEntry(initialLayout, 'main', newEntry, { positioning: 'start' });
|
|
332
|
+
expect(result.main?.[0]).to.deep.equal(newEntry);
|
|
333
|
+
expect(result.main?.length).to.equal(4);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('adds entry to end of main without pivot', () => {
|
|
337
|
+
const result = openEntry(initialLayout, 'main', newEntry, { positioning: 'end' });
|
|
338
|
+
expect(result.main?.[result.main.length - 1]).to.deep.equal(newEntry);
|
|
339
|
+
expect(result.main?.length).to.equal(4);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('adds entry before pivot in main', () => {
|
|
343
|
+
const result = openEntry(initialLayout, 'main', newEntry, { positioning: 'start', pivotId: 'id2' });
|
|
344
|
+
expect(result.main?.[1]).to.deep.equal(newEntry);
|
|
345
|
+
expect(result.main?.length).to.equal(4);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('adds entry after pivot in main', () => {
|
|
349
|
+
const result = openEntry(initialLayout, 'main', newEntry, { positioning: 'end', pivotId: 'id2' });
|
|
350
|
+
expect(result.main?.[2]).to.deep.equal(newEntry);
|
|
351
|
+
expect(result.main?.length).to.equal(4);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('adds entry to start when pivot is not found', () => {
|
|
355
|
+
const result = openEntry(initialLayout, 'main', newEntry, { positioning: 'start', pivotId: 'nonexistent' });
|
|
356
|
+
expect(result.main?.[0]).to.deep.equal(newEntry);
|
|
357
|
+
expect(result.main?.length).to.equal(4);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('does not add duplicate entry to main', () => {
|
|
361
|
+
const result = openEntry(initialLayout, 'main', initialLayout.main![0], { positioning: 'start' });
|
|
362
|
+
expect(result.main).to.deep.equal(initialLayout.main);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('replaces entry in non-main part', () => {
|
|
366
|
+
const result = openEntry(initialLayout, 'sidebar', newEntry);
|
|
367
|
+
expect(result.sidebar).to.deep.equal([newEntry]);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('creates new part if it does not exist', () => {
|
|
371
|
+
const result = openEntry(initialLayout, 'complementary', newEntry);
|
|
372
|
+
expect(result.complementary).to.deep.equal([newEntry]);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('handles undefined main part', () => {
|
|
376
|
+
const layoutWithoutMain: LayoutParts = { sidebar: [{ id: 'sid1', path: 'sidepath1' }] };
|
|
377
|
+
const result = openEntry(layoutWithoutMain, 'main', newEntry);
|
|
378
|
+
expect(result.main).to.deep.equal([newEntry]);
|
|
379
|
+
});
|
|
380
|
+
});
|
package/src/layout.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
import { produce } from 'immer';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type LayoutAdjustment,
|
|
8
|
+
type LayoutCoordinate,
|
|
9
|
+
type LayoutEntry,
|
|
10
|
+
type LayoutParts,
|
|
11
|
+
SLUG_ENTRY_SEPARATOR,
|
|
12
|
+
SLUG_KEY_VALUE_SEPARATOR,
|
|
13
|
+
SLUG_LIST_SEPARATOR,
|
|
14
|
+
SLUG_PATH_SEPARATOR,
|
|
15
|
+
type LayoutPart,
|
|
16
|
+
} from '@dxos/app-framework';
|
|
17
|
+
|
|
18
|
+
import { type NewPlankPositioning } from './types';
|
|
19
|
+
|
|
20
|
+
// Part feature support
|
|
21
|
+
const partsThatSupportIncrement = ['main'] as LayoutPart[];
|
|
22
|
+
|
|
23
|
+
//
|
|
24
|
+
// --- Layout Parts Manipulation ----------------------------------------------
|
|
25
|
+
|
|
26
|
+
type OpenLayoutEntryOptions = { positioning?: NewPlankPositioning; pivotId?: string };
|
|
27
|
+
|
|
28
|
+
export const openEntry = (
|
|
29
|
+
layout: LayoutParts,
|
|
30
|
+
part: LayoutPart,
|
|
31
|
+
entry: LayoutEntry,
|
|
32
|
+
options?: OpenLayoutEntryOptions,
|
|
33
|
+
): LayoutParts => {
|
|
34
|
+
return produce(layout, (draft) => {
|
|
35
|
+
const layoutPart = draft[part];
|
|
36
|
+
// If the part doesn't exist, create it.
|
|
37
|
+
if (!layoutPart) {
|
|
38
|
+
draft[part] = [entry];
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (part === 'main') {
|
|
42
|
+
// Check that the entry is not already in the part
|
|
43
|
+
if (layoutPart.find((e) => e.id === entry.id)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const plankPositioning = options?.positioning ?? 'start';
|
|
48
|
+
const pivotId = options?.pivotId;
|
|
49
|
+
|
|
50
|
+
if (pivotId) {
|
|
51
|
+
const pivotIndex = layoutPart.findIndex((e) => e.id === pivotId);
|
|
52
|
+
if (pivotIndex !== -1) {
|
|
53
|
+
if (plankPositioning === 'start') {
|
|
54
|
+
layoutPart.splice(pivotIndex, 0, entry);
|
|
55
|
+
} else {
|
|
56
|
+
layoutPart.splice(pivotIndex + 1, 0, entry);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// If no pivot found or provided, fall back to original behavior
|
|
63
|
+
if (plankPositioning === 'start') {
|
|
64
|
+
layoutPart.unshift(entry);
|
|
65
|
+
} else {
|
|
66
|
+
layoutPart.push(entry);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// If the part is not main, we're going to replace the single entry in the part with the new entry.
|
|
70
|
+
draft[part] = [entry];
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const closeEntry = (layout: LayoutParts, layoutCoordinate: LayoutCoordinate): LayoutParts => {
|
|
76
|
+
return produce(layout, (draft) => {
|
|
77
|
+
const { part, entryId: slugId } = layoutCoordinate;
|
|
78
|
+
const layoutPart = draft[part];
|
|
79
|
+
if (!layoutPart) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const index = layoutPart.findIndex((entry) => entry.id === slugId);
|
|
84
|
+
if (index === -1) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// If there's only one entry in the layout part, remove the whole part from the layout.
|
|
89
|
+
if (layoutPart.length === 1) {
|
|
90
|
+
delete draft[part];
|
|
91
|
+
} else {
|
|
92
|
+
layoutPart.splice(index, 1);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const incrementPlank = (layout: LayoutParts, adjustment: LayoutAdjustment): LayoutParts => {
|
|
98
|
+
return produce(layout, (draft) => {
|
|
99
|
+
const { layoutCoordinate, type } = adjustment;
|
|
100
|
+
const { part, entryId } = layoutCoordinate;
|
|
101
|
+
|
|
102
|
+
// Only allow adjustments in the 'main' part
|
|
103
|
+
if (partsThatSupportIncrement.includes(part) === false) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const layoutPart = draft[part];
|
|
108
|
+
if (!layoutPart) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const index = layoutPart.findIndex((entry) => entry.id === entryId);
|
|
112
|
+
if (
|
|
113
|
+
index === -1 ||
|
|
114
|
+
(type === 'increment-start' && index === 0) ||
|
|
115
|
+
(type === 'increment-end' && index === layoutPart.length - 1)
|
|
116
|
+
) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (type === 'increment-start') {
|
|
121
|
+
// Swap the current item with the previous item.
|
|
122
|
+
[layoutPart[index - 1], layoutPart[index]] = [layoutPart[index], layoutPart[index - 1]];
|
|
123
|
+
} else if (type === 'increment-end') {
|
|
124
|
+
// Swap the current item with the next item.
|
|
125
|
+
[layoutPart[index], layoutPart[index + 1]] = [layoutPart[index + 1], layoutPart[index]];
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const removePart = (layout: LayoutParts, part: LayoutPart): LayoutParts => {
|
|
131
|
+
return produce(layout, (draft) => {
|
|
132
|
+
delete draft[part];
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const mergeLayoutParts = (...layoutParts: LayoutParts[]): LayoutParts => {
|
|
137
|
+
return layoutParts.reduce(
|
|
138
|
+
(merged, current) =>
|
|
139
|
+
produce(merged, (draft) => {
|
|
140
|
+
Object.entries(current).forEach(([part, entries]) => {
|
|
141
|
+
const typedPart = part as LayoutPart;
|
|
142
|
+
|
|
143
|
+
if (!draft[typedPart]) {
|
|
144
|
+
draft[typedPart] = [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const partEntries = draft[typedPart] as LayoutEntry[];
|
|
148
|
+
|
|
149
|
+
entries.forEach((entry) => {
|
|
150
|
+
const existingIndex = partEntries.findIndex((e) => e.id === entry.id);
|
|
151
|
+
if (existingIndex !== -1) {
|
|
152
|
+
partEntries[existingIndex] = entry;
|
|
153
|
+
} else {
|
|
154
|
+
partEntries.push(entry);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}),
|
|
159
|
+
{} as LayoutParts,
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
//
|
|
164
|
+
// --- URI Projection ---------------------------------------------------------
|
|
165
|
+
const parseLayoutEntry = (itemString: string): LayoutEntry => {
|
|
166
|
+
// Layout entries are in the form of 'id~path' or just 'id'
|
|
167
|
+
const [id, path] = itemString.split(SLUG_PATH_SEPARATOR);
|
|
168
|
+
const entry: LayoutEntry = { id };
|
|
169
|
+
if (path) {
|
|
170
|
+
entry.path = path;
|
|
171
|
+
}
|
|
172
|
+
return entry;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const uriToSoloPart = (uri: string): LayoutParts | undefined => {
|
|
176
|
+
// Now after the domain part, there will be a single ID with an optional path
|
|
177
|
+
const parts = uri.split('/');
|
|
178
|
+
const slug = parts[parts.length - 1]; // Take the last part of the URI
|
|
179
|
+
|
|
180
|
+
if (slug.length > 0) {
|
|
181
|
+
return {
|
|
182
|
+
solo: [parseLayoutEntry(slug)],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return undefined;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const soloPartToUri = (layout: LayoutParts): string => {
|
|
190
|
+
const soloPart = layout?.solo;
|
|
191
|
+
if (!soloPart || soloPart.length === 0) {
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const entry = soloPart[0];
|
|
196
|
+
return `${entry.id}${entry.path ? SLUG_PATH_SEPARATOR + entry.path : ''}`;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Converts a URI string into a LayoutParts object.
|
|
201
|
+
* @deprecated Keeping these as a reference for now. We should remove these once we're sure we don't need them.
|
|
202
|
+
*/
|
|
203
|
+
export const uriToActiveParts = (uri: string): LayoutParts => {
|
|
204
|
+
const parts = uri.split('/');
|
|
205
|
+
const slug = parts[parts.length - 1]; // Take the last part of the URI
|
|
206
|
+
|
|
207
|
+
if (!slug) {
|
|
208
|
+
return {}; // Return an empty object if the slug is empty
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return slug.split(SLUG_ENTRY_SEPARATOR).reduce((acc: LayoutParts, partDescriptor) => {
|
|
212
|
+
const [part, layoutEntry] = partDescriptor.split(SLUG_KEY_VALUE_SEPARATOR);
|
|
213
|
+
if (part && layoutEntry) {
|
|
214
|
+
// TODO(Zan): Remove this cast.
|
|
215
|
+
acc[part as LayoutPart] = layoutEntry.split(SLUG_LIST_SEPARATOR).map(parseLayoutEntry);
|
|
216
|
+
}
|
|
217
|
+
return acc;
|
|
218
|
+
}, {} as LayoutParts);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const formatLayoutEntry = ({ id, path }: LayoutEntry): string => {
|
|
222
|
+
// NOTE(Zan): Format = `[SOLO_INDICATOR] ID [PATH_SEPARATOR PATH]`.
|
|
223
|
+
let entry = '';
|
|
224
|
+
entry += id;
|
|
225
|
+
if (path) {
|
|
226
|
+
entry += `${SLUG_PATH_SEPARATOR}${path}`;
|
|
227
|
+
}
|
|
228
|
+
return entry;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const formatPartDescriptor = (part: LayoutPart, layoutEntries: LayoutEntry[]): string => {
|
|
232
|
+
const formattedEntries = layoutEntries.map(formatLayoutEntry).join(SLUG_LIST_SEPARATOR);
|
|
233
|
+
return `${part}${SLUG_KEY_VALUE_SEPARATOR}${formattedEntries}`;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Converts a LayoutParts object into a URI string.
|
|
238
|
+
* @deprecated Keeping these as a reference for now. We should remove these once we're sure we don't need them.
|
|
239
|
+
*/
|
|
240
|
+
export const activePartsToUri = (activeParts: LayoutParts): string => {
|
|
241
|
+
return Object.entries(activeParts)
|
|
242
|
+
.filter(([, layoutEntries]) => layoutEntries.length > 0) // Only include non-empty parts
|
|
243
|
+
.map(([part, layoutEntries]) => formatPartDescriptor(part as LayoutPart, layoutEntries))
|
|
244
|
+
.join(SLUG_ENTRY_SEPARATOR);
|
|
245
|
+
};
|
package/src/meta.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
import { DECK_PLUGIN } from './meta';
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
{
|
|
8
|
+
'en-US': {
|
|
9
|
+
[DECK_PLUGIN]: {
|
|
10
|
+
'main header label': 'Main header',
|
|
11
|
+
'open navigation sidebar label': 'Open navigation sidebar.',
|
|
12
|
+
'open complementary sidebar label': 'Open complementary sidebar.',
|
|
13
|
+
'open settings label': 'Show settings',
|
|
14
|
+
'plugin error message': 'Content failed to render.',
|
|
15
|
+
'content fallback message': 'Unsupported',
|
|
16
|
+
'content fallback description':
|
|
17
|
+
'No plugin had a response for the address you navigated to. Double-check the URL, and ensure you’ve enabled a plugin that supports the object.',
|
|
18
|
+
'toggle fullscreen label': 'Toggle fullscreen',
|
|
19
|
+
'settings show footer label': 'Show footer (experimental)',
|
|
20
|
+
'settings native redirect label': 'Enable native url redirect (experimental)',
|
|
21
|
+
'settings custom slots': 'Theme option (experimental)',
|
|
22
|
+
'settings new plank position start label': 'Start',
|
|
23
|
+
'settings new plank position end label': 'End',
|
|
24
|
+
'select new plank positioning placeholder': 'Select new plank positioning',
|
|
25
|
+
'select new plank positioning label': 'New plank positioning',
|
|
26
|
+
'undo available label': 'Click to undo previous action.',
|
|
27
|
+
'undo action label': 'Undo',
|
|
28
|
+
'undo action alt': 'Undo previous action',
|
|
29
|
+
'undo close label': 'Dismiss',
|
|
30
|
+
'open comments label': 'Open comments',
|
|
31
|
+
'error fallback message': 'Unable to open this item',
|
|
32
|
+
'plank heading fallback label': 'Untitled',
|
|
33
|
+
'actions menu label': 'Options',
|
|
34
|
+
'settings deck label': 'Disable Deck',
|
|
35
|
+
'reload required message': 'Reload required.',
|
|
36
|
+
'pending heading': 'Loading…',
|
|
37
|
+
'insert plank label': 'Open',
|
|
38
|
+
'solo plank label': 'Solo',
|
|
39
|
+
'settings overscroll label': 'Plank Overscrolling',
|
|
40
|
+
'select overscroll placeholder': 'Select plank overscrolling behavior',
|
|
41
|
+
'settings overscroll centering label': 'Centering',
|
|
42
|
+
'settings overscroll none label': 'None',
|
|
43
|
+
'settings flat deck': 'Flatten deck appearance',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
];
|