@dxos/plugin-deck 0.7.4 → 0.7.5-main.499c70c

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 (36) hide show
  1. package/dist/lib/browser/chunk-ZC3K6C2W.mjs +37 -0
  2. package/dist/lib/browser/chunk-ZC3K6C2W.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +392 -420
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/types.mjs +6 -1
  7. package/dist/types/src/DeckPlugin.d.ts +1 -10
  8. package/dist/types/src/DeckPlugin.d.ts.map +1 -1
  9. package/dist/types/src/components/DeckLayout/ComplementarySidebar.d.ts.map +1 -1
  10. package/dist/types/src/components/DeckLayout/DeckLayout.d.ts.map +1 -1
  11. package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts.map +1 -1
  12. package/dist/types/src/components/DeckLayout/Plank.d.ts.map +1 -1
  13. package/dist/types/src/components/DeckLayout/PlankControls.d.ts +2 -2
  14. package/dist/types/src/components/DeckLayout/PlankControls.d.ts.map +1 -1
  15. package/dist/types/src/components/DeckLayout/Sidebar.d.ts.map +1 -1
  16. package/dist/types/src/components/DeckLayout/Toast.d.ts.map +1 -1
  17. package/dist/types/src/translations.d.ts +3 -0
  18. package/dist/types/src/translations.d.ts.map +1 -1
  19. package/dist/types/src/types.d.ts +16 -0
  20. package/dist/types/src/types.d.ts.map +1 -1
  21. package/dist/types/tsconfig.tsbuildinfo +1 -0
  22. package/package.json +29 -28
  23. package/src/DeckPlugin.tsx +222 -256
  24. package/src/components/DeckLayout/ActiveNode.tsx +1 -1
  25. package/src/components/DeckLayout/ComplementarySidebar.tsx +4 -3
  26. package/src/components/DeckLayout/DeckLayout.tsx +3 -7
  27. package/src/components/DeckLayout/Fullscreen.tsx +1 -1
  28. package/src/components/DeckLayout/NodePlankHeading.tsx +15 -29
  29. package/src/components/DeckLayout/Plank.tsx +7 -5
  30. package/src/components/DeckLayout/PlankControls.tsx +3 -5
  31. package/src/components/DeckLayout/Sidebar.tsx +4 -21
  32. package/src/components/DeckLayout/Toast.tsx +19 -6
  33. package/src/translations.ts +3 -0
  34. package/src/types.ts +15 -0
  35. package/dist/lib/browser/chunk-NIRHDTX4.mjs +0 -17
  36. package/dist/lib/browser/chunk-NIRHDTX4.mjs.map +0 -7
@@ -3,18 +3,18 @@
3
3
  //
4
4
 
5
5
  import { batch } from '@preact/signals-core';
6
+ import { pipe } from 'effect';
6
7
  import { setAutoFreeze } from 'immer';
7
8
  import React, { type PropsWithChildren } from 'react';
8
9
 
9
10
  import {
11
+ chain,
12
+ createIntent,
13
+ createResolver,
14
+ createSurface,
10
15
  filterPlugins,
11
- type GraphProvides,
12
16
  IntentAction,
13
- type IntentData,
14
17
  type IntentPluginProvides,
15
- isLayoutAdjustment,
16
- isLayoutMode,
17
- isLayoutParts,
18
18
  type Layout,
19
19
  LayoutAction,
20
20
  type LayoutEntry,
@@ -32,15 +32,15 @@ import {
32
32
  Toast as ToastSchema,
33
33
  } from '@dxos/app-framework';
34
34
  import { type UnsubscribeCallback } from '@dxos/async';
35
- import { getTypename } from '@dxos/echo-schema';
35
+ import { getTypename, S } from '@dxos/echo-schema';
36
36
  import { scheduledEffect } from '@dxos/echo-signals/core';
37
+ import { invariant } from '@dxos/invariant';
37
38
  import { create, isReactiveObject } from '@dxos/live-object';
38
39
  import { LocalStorageStore } from '@dxos/local-storage';
39
40
  import { log } from '@dxos/log';
40
41
  import { type AttentionPluginProvides, parseAttentionPlugin } from '@dxos/plugin-attention';
41
- import { createExtension, type Node } from '@dxos/plugin-graph';
42
- import { ObservabilityAction } from '@dxos/plugin-observability/meta';
43
- import { fullyQualifiedId } from '@dxos/react-client/echo';
42
+ import { createExtension, ROOT_ID, type Node } from '@dxos/plugin-graph';
43
+ import { ObservabilityAction } from '@dxos/plugin-observability/types';
44
44
  import { translations as stackTranslations } from '@dxos/react-ui-stack';
45
45
 
46
46
  import { DeckContext, type DeckContextType, DeckLayout, LayoutContext, LayoutSettings, NAV_ID } from './components';
@@ -56,6 +56,7 @@ import {
56
56
  import meta, { DECK_PLUGIN } from './meta';
57
57
  import translations from './translations';
58
58
  import {
59
+ DeckAction,
59
60
  type DeckPluginProvides,
60
61
  type DeckSettingsProps,
61
62
  type NewPlankPositioning,
@@ -75,25 +76,11 @@ const appScheme = 'composer://';
75
76
  // TODO(Zan): Move this to a more global location if we use immer more broadly.
76
77
  setAutoFreeze(false);
77
78
 
78
- //
79
- // Intents
80
- //
81
- const DECK_ACTION = 'dxos.org/plugin/deck';
82
-
83
- export enum DeckAction {
84
- UPDATE_PLANK_SIZE = `${DECK_ACTION}/update-plank-size`,
85
- }
86
-
87
- export namespace DeckAction {
88
- export type UpdatePlankSize = IntentData<{ id: string; size: number }>;
89
- }
90
-
91
79
  export const DeckPlugin = ({
92
80
  observability,
93
81
  }: {
94
82
  observability?: boolean;
95
83
  } = {}): PluginDefinition<DeckPluginProvides> => {
96
- let graphPlugin: Plugin<GraphProvides> | undefined;
97
84
  let intentPlugin: Plugin<IntentPluginProvides> | undefined;
98
85
  let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
99
86
  const unsubscriptionCallbacks = [] as (UnsubscribeCallback | undefined)[];
@@ -149,18 +136,18 @@ export const DeckPlugin = ({
149
136
  anchorId,
150
137
  dialogBlockAlign,
151
138
  dialogType,
152
- }: LayoutAction.SetLayout) => {
139
+ }: LayoutAction.SetLayout['input']) => {
153
140
  switch (element) {
154
141
  case 'sidebar': {
155
142
  layout.values.sidebarOpen = state ?? !layout.values.sidebarOpen;
156
- return { data: true };
143
+ break;
157
144
  }
158
145
 
159
146
  case 'complementary': {
160
147
  layout.values.complementarySidebarOpen = !!state;
161
148
  // TODO(thure): Hoist content into the c11y sidebar of Deck.
162
149
  // layout.values.complementarySidebarContent = component || subject ? { component, subject } : null;
163
- return { data: true };
150
+ break;
164
151
  }
165
152
 
166
153
  case 'dialog': {
@@ -168,32 +155,39 @@ export const DeckPlugin = ({
168
155
  layout.values.dialogContent = component ? { component, subject } : null;
169
156
  layout.values.dialogBlockAlign = dialogBlockAlign ?? 'center';
170
157
  layout.values.dialogType = dialogType;
171
- return { data: true };
158
+ break;
172
159
  }
173
160
 
174
161
  case 'popover': {
175
162
  layout.values.popoverOpen = state ?? Boolean(component);
176
163
  layout.values.popoverContent = component ? { component, subject } : null;
177
164
  layout.values.popoverAnchorId = anchorId;
178
- return { data: true };
165
+ break;
179
166
  }
180
167
 
181
168
  case 'toast': {
182
- if (ToastSchema.safeParse(subject).success) {
169
+ if (S.is(ToastSchema)(subject)) {
183
170
  layout.values.toasts = [...layout.values.toasts, subject];
184
- return { data: true };
185
171
  }
172
+ break;
186
173
  }
187
174
  }
188
175
  };
189
176
 
190
177
  /**
191
- * Update the active state and ensure that attention is on an active element.
178
+ * Update the active state and returns the id of the next attended plank.
192
179
  */
193
180
  const handleSetLocation = (next: LayoutParts) => {
194
181
  const part = layout.values.layoutMode === 'solo' ? 'solo' : 'main';
195
182
  const ids = openIds(next, [part]);
196
183
 
184
+ const current = openIds(location.values.active, [part]);
185
+ const removed = current.filter((id) => !ids.includes(id));
186
+ const closed = Array.from(new Set([...location.values.closed.filter((id) => !ids.includes(id)), ...removed]));
187
+
188
+ location.values.closed = closed;
189
+ location.values.active = next;
190
+
197
191
  if (attentionPlugin) {
198
192
  const attended = attentionPlugin.provides.attention.attended;
199
193
  const [attendedId] = Array.from(attended);
@@ -203,28 +197,17 @@ export const DeckPlugin = ({
203
197
  const attendedIndex = currentIds.indexOf(attendedId);
204
198
  // If outside of bounds, focus on the first/last plank, otherwise focus on the new plank in the same position.
205
199
  const index = attendedIndex === -1 ? 0 : attendedIndex >= ids.length ? ids.length - 1 : attendedIndex;
206
- const nextAttended = next[part]?.[index].id;
207
- void intentPlugin?.provides.intent.dispatch({
208
- action: LayoutAction.SCROLL_INTO_VIEW,
209
- data: { id: nextAttended },
210
- });
200
+ return next[part]?.[index].id;
211
201
  }
212
202
  }
213
-
214
- const current = openIds(location.values.active, [part]);
215
- const removed = current.filter((id) => !ids.includes(id));
216
- const closed = Array.from(new Set([...location.values.closed.filter((id) => !ids.includes(id)), ...removed]));
217
-
218
- location.values.closed = closed;
219
- location.values.active = next;
220
203
  };
221
204
 
222
205
  return {
223
206
  meta,
224
- ready: async (plugins) => {
225
- intentPlugin = resolvePlugin(plugins, parseIntentPlugin);
226
- graphPlugin = resolvePlugin(plugins, parseGraphPlugin);
207
+ ready: async ({ plugins }) => {
227
208
  attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
209
+ intentPlugin = resolvePlugin(plugins, parseIntentPlugin);
210
+ const dispatch = intentPlugin?.provides.intent.dispatchPromise;
228
211
 
229
212
  layout
230
213
  .prop({ key: 'layoutMode', type: LocalStorageStore.enum<LayoutMode>() })
@@ -267,13 +250,15 @@ export const DeckPlugin = ({
267
250
  const startingLayout = removePart(location.values.active, 'solo');
268
251
  const layoutFromUri = uriToSoloPart(pathname);
269
252
  if (!layoutFromUri) {
270
- handleSetLocation(startingLayout);
253
+ const toAttend = handleSetLocation(startingLayout);
271
254
  layout.values.layoutMode = 'deck';
255
+ await dispatch?.(createIntent(LayoutAction.ScrollIntoView, { id: toAttend }));
272
256
  return;
273
257
  }
274
258
 
275
- handleSetLocation(mergeLayoutParts(layoutFromUri, startingLayout));
259
+ const toAttend = handleSetLocation(mergeLayoutParts(layoutFromUri, startingLayout));
276
260
  layout.values.layoutMode = 'solo';
261
+ await dispatch?.(createIntent(LayoutAction.ScrollIntoView, { id: toAttend }));
277
262
  };
278
263
 
279
264
  await handleNavigation();
@@ -303,22 +288,24 @@ export const DeckPlugin = ({
303
288
  location: location.values,
304
289
  translations: [...translations, ...stackTranslations],
305
290
  graph: {
306
- builder: () => {
291
+ builder: (plugins) => {
292
+ const dispatch = resolvePlugin(plugins, parseIntentPlugin)?.provides.intent.dispatchPromise;
293
+ const attention = resolvePlugin(plugins, parseAttentionPlugin)?.provides.attention;
294
+
295
+ invariant(dispatch, 'Intent plugin is required for deck plugin.');
296
+ invariant(attention, 'Attention plugin is required for deck plugin.');
297
+
307
298
  // TODO(burdon): Root menu isn't visible so nothing bound.
308
299
  return createExtension({
309
300
  id: DECK_PLUGIN,
310
- // NOTE(Zan): This is currently disabled.
311
- // TODO(Zan): Fullscreen needs to know the active node and provide that to the layout part.
312
- filter: (node): node is Node<null> => false,
313
- actions: () => [
314
- {
315
- id: `${LayoutAction.SET_LAYOUT_MODE}/fullscreen`,
301
+ filter: (node): node is Node<null> => node.id === ROOT_ID,
302
+ actions: () => {
303
+ // NOTE(Zan): This is currently disabled.
304
+ // TODO(Zan): Fullscreen needs to know the active node and provide that to the layout part.
305
+ const _fullscreen = {
306
+ id: `${LayoutAction.SetLayoutMode._tag}/fullscreen`,
316
307
  data: async () => {
317
- await intentPlugin?.provides.intent.dispatch({
318
- plugin: DECK_PLUGIN,
319
- action: LayoutAction.SET_LAYOUT_MODE,
320
- data: { layoutMode: 'fullscreen' },
321
- });
308
+ await dispatch(createIntent(LayoutAction.SetLayoutMode, { layoutMode: 'fullscreen' }));
322
309
  },
323
310
  properties: {
324
311
  label: ['toggle fullscreen label', { ns: DECK_PLUGIN }],
@@ -328,8 +315,56 @@ export const DeckPlugin = ({
328
315
  windows: 'shift+ctrl+f',
329
316
  },
330
317
  },
331
- },
332
- ],
318
+ };
319
+
320
+ const closeCurrent = {
321
+ id: `${NavigationAction.Close._tag}/current`,
322
+ data: async () => {
323
+ const attended = attention.attended.at(-1);
324
+ if (attended) {
325
+ await dispatch(createIntent(NavigationAction.Close, { activeParts: { main: [attended] } }));
326
+ }
327
+ },
328
+ properties: {
329
+ label: ['close current label', { ns: DECK_PLUGIN }],
330
+ icon: 'ph--x--regular',
331
+ },
332
+ };
333
+
334
+ const closeOthers = {
335
+ id: `${NavigationAction.Close._tag}/others`,
336
+ data: async () => {
337
+ const attended = attention.attended.at(-1);
338
+ const ids = openIds(location.values.active, ['main']).filter((id) => id !== attended);
339
+ await dispatch(
340
+ createIntent(NavigationAction.Close, {
341
+ activeParts: { main: ids },
342
+ }),
343
+ );
344
+ },
345
+ properties: {
346
+ label: ['close others label', { ns: DECK_PLUGIN }],
347
+ icon: 'ph--x-square--regular',
348
+ },
349
+ };
350
+
351
+ const closeAll = {
352
+ id: `${NavigationAction.Close._tag}/all`,
353
+ data: async () => {
354
+ await dispatch(
355
+ createIntent(NavigationAction.Close, {
356
+ activeParts: { main: openIds(location.values.active, ['main']) },
357
+ }),
358
+ );
359
+ },
360
+ properties: {
361
+ label: ['close all label', { ns: DECK_PLUGIN }],
362
+ icon: 'ph--x-circle--regular',
363
+ },
364
+ };
365
+
366
+ return layout.values.layoutMode === 'deck' ? [closeCurrent, closeOthers, closeAll] : [];
367
+ },
333
368
  });
334
369
  },
335
370
  },
@@ -363,22 +398,43 @@ export const DeckPlugin = ({
363
398
  );
364
399
  },
365
400
  surface: {
366
- component: ({ data, role }) => {
367
- switch (role) {
368
- case 'settings':
369
- return data.plugin === meta.id ? <LayoutSettings settings={settings.values} /> : null;
370
- }
371
- return null;
372
- },
401
+ definitions: () =>
402
+ createSurface({
403
+ id: DECK_PLUGIN,
404
+ role: 'settings',
405
+ filter: (data): data is any => data.subject === DECK_PLUGIN,
406
+ component: () => <LayoutSettings settings={settings.values} />,
407
+ }),
373
408
  },
374
409
  intent: {
375
- resolver: (intent) => {
376
- switch (intent.action) {
377
- case LayoutAction.SET_LAYOUT: {
378
- return intent.data && handleSetLayout(intent.data as LayoutAction.SetLayout);
379
- }
380
-
381
- case LayoutAction.SET_LAYOUT_MODE: {
410
+ resolvers: ({ plugins }) => {
411
+ const graph = resolvePlugin(plugins, parseGraphPlugin)?.provides.graph;
412
+
413
+ return [
414
+ createResolver(DeckAction.UpdatePlankSize, (data) => {
415
+ deck.values.plankSizing[data.id] = data.size;
416
+ }),
417
+ createResolver(IntentAction.ShowUndo, (data) => {
418
+ // TODO(wittjosiah): Support undoing further back than the last action.
419
+ if (currentUndoId) {
420
+ layout.values.toasts = layout.values.toasts.filter((toast) => toast.id !== currentUndoId);
421
+ }
422
+ currentUndoId = `${IntentAction.ShowUndo._tag}-${Date.now()}`;
423
+ layout.values.toasts = [
424
+ ...layout.values.toasts,
425
+ {
426
+ id: currentUndoId,
427
+ title: data.message ?? ['undo available label', { ns: DECK_PLUGIN }],
428
+ duration: 10_000,
429
+ actionLabel: ['undo action label', { ns: DECK_PLUGIN }],
430
+ actionAlt: ['undo action alt', { ns: DECK_PLUGIN }],
431
+ closeLabel: ['undo close label', { ns: DECK_PLUGIN }],
432
+ onAction: () => intentPlugin?.provides.intent.undoPromise?.(),
433
+ },
434
+ ];
435
+ }),
436
+ createResolver(LayoutAction.SetLayout, handleSetLayout),
437
+ createResolver(LayoutAction.SetLayoutMode, (data) => {
382
438
  const setMode = (mode: LayoutMode) => {
383
439
  const main = openIds(location.values.active, ['main']);
384
440
  const solo = openIds(location.values.active, ['solo']);
@@ -394,72 +450,25 @@ export const DeckPlugin = ({
394
450
  layout.values.layoutMode = mode;
395
451
  };
396
452
 
397
- // TODO(wittjosiah): Update closed state.
398
453
  return batch(() => {
399
- if (!intent.data) {
400
- return;
401
- }
402
-
403
- if (intent.data?.revert) {
404
- setMode(layoutModeHistory.values.pop() ?? 'solo');
405
- return { data: true };
406
- }
407
-
408
- if (isLayoutMode(intent?.data?.layoutMode)) {
454
+ if ('layoutMode' in data) {
409
455
  layoutModeHistory.values.push(layout.values.layoutMode);
410
- setMode(intent.data.layoutMode);
456
+ setMode(data.layoutMode);
457
+ } else if (data.revert) {
458
+ setMode(layoutModeHistory.values.pop() ?? 'solo');
411
459
  } else {
412
- log.warn('Invalid layout mode', intent?.data?.layoutMode);
460
+ log.warn('Invalid layout mode', data);
413
461
  }
414
-
415
- return { data: true };
416
462
  });
417
- }
418
-
419
- case LayoutAction.SCROLL_INTO_VIEW: {
420
- layout.values.scrollIntoView = intent.data?.id ?? undefined;
421
- return { data: true };
422
- }
423
-
424
- case DeckAction.UPDATE_PLANK_SIZE: {
425
- const { id, size } = intent.data as DeckAction.UpdatePlankSize;
426
- deck.values.plankSizing[id] = size;
427
- return { data: true };
428
- }
429
-
430
- case IntentAction.SHOW_UNDO: {
431
- // TODO(wittjosiah): Support undoing further back than the last action.
432
- if (currentUndoId) {
433
- layout.values.toasts = layout.values.toasts.filter((toast) => toast.id !== currentUndoId);
434
- }
435
- currentUndoId = `${IntentAction.SHOW_UNDO}-${Date.now()}`;
436
- const title =
437
- // TODO(wittjosiah): How to handle chains better?
438
- intent.data?.results?.[0]?.result?.undoable?.message ??
439
- translations[0]['en-US']['dxos.org/plugin/deck']['undo available label'];
440
- layout.values.toasts = [
441
- ...layout.values.toasts,
442
- {
443
- id: currentUndoId,
444
- title,
445
- duration: 10_000,
446
- actionLabel: translations[0]['en-US']['dxos.org/plugin/deck']['undo action label'],
447
- actionAlt: translations[0]['en-US']['dxos.org/plugin/deck']['undo action alt'],
448
- closeLabel: translations[0]['en-US']['dxos.org/plugin/deck']['undo close label'],
449
- onAction: () => intentPlugin?.provides.intent.undo?.(),
450
- },
451
- ];
452
- return { data: true };
453
- }
454
-
455
- case NavigationAction.OPEN: {
463
+ }),
464
+ createResolver(LayoutAction.ScrollIntoView, ({ id }) => {
465
+ layout.values.scrollIntoView = id;
466
+ }),
467
+ // TODO(wittjosiah): Factor out navgiation from deck plugin.
468
+ createResolver(NavigationAction.Open, (data) => {
456
469
  const previouslyOpenIds = new Set<string>(openIds(location.values.active));
457
470
  const layoutMode = layout.values.layoutMode;
458
- batch(() => {
459
- if (!intent.data || !intent.data?.activeParts) {
460
- return;
461
- }
462
-
471
+ const toAttend = batch(() => {
463
472
  const processLayoutEntry = (partName: string, entryString: string, currentLayout: any) => {
464
473
  // TODO(burdon): Option to toggle?
465
474
  const toggle = false;
@@ -471,7 +480,7 @@ export const DeckPlugin = ({
471
480
  layoutMode === 'deck' &&
472
481
  effectivePart === 'main' &&
473
482
  currentLayout[effectivePart]?.some((entry: LayoutEntry) => entry.id === id) &&
474
- !intent.data?.noToggle
483
+ !data?.noToggle
475
484
  ) {
476
485
  // If we're in deck mode and the main part is already open, toggle it closed.
477
486
  return closeEntry(currentLayout, { part: effectivePart as LayoutPart, entryId: id });
@@ -483,7 +492,7 @@ export const DeckPlugin = ({
483
492
  };
484
493
 
485
494
  let newLayout = location.values.active;
486
- Object.entries(intent.data.activeParts).forEach(([partName, layoutEntries]) => {
495
+ Object.entries(data.activeParts).forEach(([partName, layoutEntries]) => {
487
496
  if (Array.isArray(layoutEntries)) {
488
497
  layoutEntries.forEach((activePartEntry: string) => {
489
498
  newLayout = processLayoutEntry(partName, activePartEntry, newLayout);
@@ -494,53 +503,34 @@ export const DeckPlugin = ({
494
503
  }
495
504
  });
496
505
 
497
- handleSetLocation(newLayout);
506
+ return handleSetLocation(newLayout);
498
507
  });
499
508
 
500
509
  const ids = openIds(location.values.active);
501
510
  const newlyOpen = ids.filter((i) => !previouslyOpenIds.has(i));
502
511
 
503
512
  return {
504
- data: { ids },
513
+ data: { open: ids },
505
514
  intents: [
506
- newlyOpen.length > 0
507
- ? [
508
- {
509
- action: LayoutAction.SCROLL_INTO_VIEW,
510
- data: { id: newlyOpen[0] },
511
- },
512
- ]
513
- : [],
514
- intent.data?.object
515
- ? [
516
- {
517
- action: NavigationAction.EXPOSE,
518
- data: { id: fullyQualifiedId(intent.data.object) },
519
- },
520
- ]
521
- : [],
522
- observability
515
+ createIntent(LayoutAction.ScrollIntoView, { id: newlyOpen[0] ?? toAttend }),
516
+ ...(toAttend ? [createIntent(NavigationAction.Expose, { id: toAttend })] : []),
517
+ ...(observability
523
518
  ? newlyOpen.map((id) => {
524
- const active = graphPlugin?.provides.graph.findNode(id)?.data;
519
+ const active = graph?.findNode(id)?.data;
525
520
  const typename = isReactiveObject(active) ? getTypename(active) : undefined;
526
- return {
527
- action: ObservabilityAction.SEND_EVENT,
528
- data: {
529
- name: 'navigation.activate',
530
- properties: {
531
- id,
532
- typename,
533
- },
521
+ return createIntent(ObservabilityAction.SendEvent, {
522
+ name: 'navigation.activate',
523
+ properties: {
524
+ id,
525
+ typename,
534
526
  },
535
- };
527
+ });
536
528
  })
537
- : [],
529
+ : []),
538
530
  ],
539
531
  };
540
- }
541
-
542
- case NavigationAction.ADD_TO_ACTIVE: {
543
- const data = intent.data as NavigationAction.AddToActive;
532
+ }),
533
+ createResolver(NavigationAction.AddToActive, (data) => {
544
534
  const layoutEntry = { id: data.id };
545
535
  const effectivePart = getEffectivePart(data.part, layout.values.layoutMode);
546
536
 
@@ -553,103 +543,79 @@ export const DeckPlugin = ({
553
543
 
554
544
  const intents = [];
555
545
  if (data.scrollIntoView && layout.values.layoutMode === 'deck') {
556
- intents.push([
557
- {
558
- action: LayoutAction.SCROLL_INTO_VIEW,
559
- data: { id: data.id },
560
- },
561
- ]);
546
+ intents.push(createIntent(LayoutAction.ScrollIntoView, { id: data.id }));
562
547
  }
563
548
 
564
- return { data: true, intents };
565
- }
566
-
567
- case NavigationAction.CLOSE: {
568
- return batch(() => {
569
- if (!intent.data) {
570
- return;
549
+ return { intents };
550
+ }),
551
+ createResolver(NavigationAction.Close, (data) => {
552
+ let newLayout = location.values.active;
553
+ const layoutMode = layout.values.layoutMode;
554
+ const intentParts = data.activeParts;
555
+ Object.keys(intentParts).forEach((partName: string) => {
556
+ const effectivePart = getEffectivePart(partName as LayoutPart, layoutMode);
557
+ const ids = intentParts[partName];
558
+ if (Array.isArray(ids)) {
559
+ ids.forEach((id: string) => {
560
+ newLayout = closeEntry(newLayout, { part: effectivePart, entryId: id });
561
+ });
562
+ } else {
563
+ // Legacy single string entry
564
+ newLayout = closeEntry(newLayout, { part: effectivePart, entryId: ids });
571
565
  }
572
- let newLayout = location.values.active;
573
- const layoutMode = layout.values.layoutMode;
574
- const intentParts = intent.data.activeParts;
575
- Object.keys(intentParts).forEach((partName: string) => {
576
- const effectivePart = getEffectivePart(partName as LayoutPart, layoutMode);
577
- const ids = intentParts[partName];
578
- if (Array.isArray(ids)) {
579
- ids.forEach((id: string) => {
580
- newLayout = closeEntry(newLayout, { part: effectivePart, entryId: id });
581
- });
582
- } else {
583
- // Legacy single string entry
584
- newLayout = closeEntry(newLayout, { part: effectivePart, entryId: ids });
585
- }
586
- });
587
-
588
- handleSetLocation(newLayout);
589
- return { data: true };
590
566
  });
591
- }
592
567
 
593
- // TODO(wittjosiah): Factor out.
594
- case NavigationAction.SET: {
568
+ const toAttend = handleSetLocation(newLayout);
569
+ return { intents: [createIntent(LayoutAction.ScrollIntoView, { id: toAttend })] };
570
+ }),
571
+ createResolver(NavigationAction.Set, (data) => {
595
572
  return batch(() => {
596
- if (isLayoutParts(intent.data?.activeParts)) {
597
- handleSetLocation(intent.data!.activeParts);
598
- }
599
- return { data: true };
573
+ const toAttend = handleSetLocation(data.activeParts);
574
+ return { intents: [createIntent(LayoutAction.ScrollIntoView, { id: toAttend })] };
600
575
  });
601
- }
602
-
603
- case NavigationAction.ADJUST: {
576
+ }),
577
+ createResolver(NavigationAction.Adjust, (adjustment) => {
604
578
  return batch(() => {
605
- if (isLayoutAdjustment(intent.data)) {
606
- const adjustment = intent.data;
607
- if (adjustment.type === 'increment-end' || adjustment.type === 'increment-start') {
608
- handleSetLocation(
609
- incrementPlank(location.values.active, {
610
- type: adjustment.type,
611
- layoutCoordinate: adjustment.layoutCoordinate,
612
- }),
613
- );
614
- }
579
+ if (adjustment.type === 'increment-end' || adjustment.type === 'increment-start') {
580
+ handleSetLocation(
581
+ incrementPlank(location.values.active, {
582
+ type: adjustment.type,
583
+ layoutCoordinate: adjustment.layoutCoordinate,
584
+ }),
585
+ );
586
+ }
615
587
 
616
- if (adjustment.type === 'solo') {
617
- const entryId = adjustment.layoutCoordinate.entryId;
618
- if (layout.values.layoutMode !== 'solo') {
619
- // Solo the entry.
620
- return {
621
- data: true,
622
- intents: [
623
- // NOTE: The order of these is important.
624
- [
625
- { action: NavigationAction.OPEN, data: { activeParts: { solo: [entryId] } } },
626
- { action: LayoutAction.SET_LAYOUT_MODE, data: { layoutMode: 'solo' } },
627
- ],
628
- ],
629
- };
630
- } else {
631
- // Un-solo the current entry.
632
- return {
633
- data: true,
634
- intents: [
635
- // NOTE: The order of these is important.
636
- [
637
- { action: LayoutAction.SET_LAYOUT_MODE, data: { layoutMode: 'deck' } },
638
- { action: NavigationAction.CLOSE, data: { activeParts: { solo: [entryId] } } },
639
- {
640
- action: NavigationAction.OPEN,
641
- data: { noToggle: true, activeParts: { main: [entryId] } },
642
- },
643
- { action: LayoutAction.SCROLL_INTO_VIEW, data: { id: entryId } },
644
- ],
645
- ],
646
- };
647
- }
588
+ if (adjustment.type === 'solo') {
589
+ const entryId = adjustment.layoutCoordinate.entryId;
590
+ if (layout.values.layoutMode !== 'solo') {
591
+ // Solo the entry.
592
+ return {
593
+ intents: [
594
+ // NOTE: The order of these is important.
595
+ pipe(
596
+ createIntent(NavigationAction.Open, { activeParts: { solo: [entryId] } }),
597
+ chain(LayoutAction.SetLayoutMode, { layoutMode: 'solo' }),
598
+ ),
599
+ ],
600
+ };
601
+ } else {
602
+ // Un-solo the current entry.
603
+ return {
604
+ intents: [
605
+ // NOTE: The order of these is important.
606
+ pipe(
607
+ createIntent(LayoutAction.SetLayoutMode, { layoutMode: 'deck' }),
608
+ chain(NavigationAction.Close, { activeParts: { solo: [entryId] } }),
609
+ chain(NavigationAction.Open, { activeParts: { main: [entryId] }, noToggle: true }),
610
+ chain(LayoutAction.ScrollIntoView, { id: entryId }),
611
+ ),
612
+ ],
613
+ };
648
614
  }
649
615
  }
650
616
  });
651
- }
652
- }
617
+ }),
618
+ ];
653
619
  },
654
620
  },
655
621
  },