@dxos/plugin-kanban 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef

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 (177) hide show
  1. package/dist/lib/browser/blueprints/index.mjs +1 -1
  2. package/dist/lib/browser/blueprints/index.mjs.map +3 -3
  3. package/dist/lib/browser/{chunk-A3PBV3S5.mjs → chunk-T32TEM55.mjs} +2 -2
  4. package/dist/lib/browser/chunk-T32TEM55.mjs.map +7 -0
  5. package/dist/lib/browser/{delete-card-VPNVIWOA.mjs → delete-card-7OSCNCW2.mjs} +4 -12
  6. package/dist/lib/browser/delete-card-7OSCNCW2.mjs.map +7 -0
  7. package/dist/lib/browser/{delete-card-field-4HHF2GYX.mjs → delete-card-field-NSB2RE3Z.mjs} +4 -12
  8. package/dist/lib/browser/delete-card-field-NSB2RE3Z.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +5 -97
  10. package/dist/lib/browser/index.mjs.map +4 -4
  11. package/dist/lib/browser/meta.json +1 -1
  12. package/dist/lib/browser/operations/index.mjs +3 -3
  13. package/dist/lib/browser/operations/index.mjs.map +3 -3
  14. package/dist/lib/browser/{restore-card-4GG2RYKR.mjs → restore-card-FO3WERIE.mjs} +4 -12
  15. package/dist/lib/browser/restore-card-FO3WERIE.mjs.map +7 -0
  16. package/dist/lib/browser/{restore-card-field-3T26ACYX.mjs → restore-card-field-U5XYEEOW.mjs} +4 -12
  17. package/dist/lib/browser/restore-card-field-U5XYEEOW.mjs.map +7 -0
  18. package/dist/lib/browser/translations.mjs +44 -0
  19. package/dist/lib/browser/translations.mjs.map +7 -0
  20. package/dist/lib/browser/types/index.mjs +70 -10
  21. package/dist/lib/browser/types/index.mjs.map +3 -3
  22. package/dist/lib/node-esm/blueprints/index.mjs +1 -1
  23. package/dist/lib/node-esm/blueprints/index.mjs.map +3 -3
  24. package/dist/lib/node-esm/{chunk-6LELYA2G.mjs → chunk-W2RNFBMZ.mjs} +2 -2
  25. package/dist/lib/node-esm/chunk-W2RNFBMZ.mjs.map +7 -0
  26. package/dist/lib/node-esm/{delete-card-5PW5OMFN.mjs → delete-card-ZIREL6HN.mjs} +4 -12
  27. package/dist/lib/node-esm/delete-card-ZIREL6HN.mjs.map +7 -0
  28. package/dist/lib/node-esm/{delete-card-field-KPJU2AQ3.mjs → delete-card-field-IPTEGVPP.mjs} +4 -12
  29. package/dist/lib/node-esm/delete-card-field-IPTEGVPP.mjs.map +7 -0
  30. package/dist/lib/node-esm/index.mjs +5 -97
  31. package/dist/lib/node-esm/index.mjs.map +4 -4
  32. package/dist/lib/node-esm/meta.json +1 -1
  33. package/dist/lib/node-esm/operations/index.mjs +3 -3
  34. package/dist/lib/node-esm/operations/index.mjs.map +3 -3
  35. package/dist/lib/node-esm/{restore-card-X2TKMU5A.mjs → restore-card-WJJ4YB7K.mjs} +4 -12
  36. package/dist/lib/node-esm/restore-card-WJJ4YB7K.mjs.map +7 -0
  37. package/dist/lib/node-esm/{restore-card-field-IUTL4RTR.mjs → restore-card-field-L24WJXAW.mjs} +4 -12
  38. package/dist/lib/node-esm/restore-card-field-L24WJXAW.mjs.map +7 -0
  39. package/dist/lib/node-esm/translations.mjs +45 -0
  40. package/dist/lib/node-esm/translations.mjs.map +7 -0
  41. package/dist/lib/node-esm/types/index.mjs +70 -10
  42. package/dist/lib/node-esm/types/index.mjs.map +3 -3
  43. package/dist/types/src/KanbanPlugin.d.ts +1 -0
  44. package/dist/types/src/KanbanPlugin.d.ts.map +1 -1
  45. package/dist/types/src/KanbanPlugin.node.d.ts +4 -0
  46. package/dist/types/src/KanbanPlugin.node.d.ts.map +1 -0
  47. package/dist/types/src/KanbanPlugin.test.d.ts +2 -0
  48. package/dist/types/src/KanbanPlugin.test.d.ts.map +1 -0
  49. package/dist/types/src/blueprints/kanban-blueprint.d.ts +2 -2
  50. package/dist/types/src/blueprints/kanban-blueprint.d.ts.map +1 -1
  51. package/dist/types/src/capabilities/artifact-definition.d.ts.map +1 -1
  52. package/dist/types/src/capabilities/blueprint-definition.d.ts +2 -2
  53. package/dist/types/src/capabilities/blueprint-definition.d.ts.map +1 -1
  54. package/dist/types/src/capabilities/index.d.ts +3 -2
  55. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  56. package/dist/types/src/capabilities/migrations.d.ts +5 -0
  57. package/dist/types/src/capabilities/migrations.d.ts.map +1 -0
  58. package/dist/types/src/capabilities/operation-handler.d.ts +1 -1
  59. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -1
  60. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  61. package/dist/types/src/capabilities/undo-mappings.d.ts.map +1 -1
  62. package/dist/types/src/components/KanbanBoard/KanbanBoard.d.ts +6 -6
  63. package/dist/types/src/components/KanbanBoard/KanbanBoard.d.ts.map +1 -1
  64. package/dist/types/src/components/KanbanBoard/KanbanBoard.stories.d.ts +24 -24
  65. package/dist/types/src/components/KanbanBoard/KanbanBoard.stories.d.ts.map +1 -1
  66. package/dist/types/src/components/KanbanBoard/KanbanCard.d.ts.map +1 -1
  67. package/dist/types/src/components/KanbanBoard/KanbanColumn.d.ts.map +1 -1
  68. package/dist/types/src/containers/KanbanContainer/KanbanContainer.d.ts +2 -2
  69. package/dist/types/src/containers/KanbanContainer/KanbanContainer.d.ts.map +1 -1
  70. package/dist/types/src/containers/KanbanContainer/KanbanContainer.stories.d.ts +24 -24
  71. package/dist/types/src/containers/KanbanContainer/KanbanContainer.stories.d.ts.map +1 -1
  72. package/dist/types/src/containers/KanbanSettings/KanbanSettings.d.ts +13 -0
  73. package/dist/types/src/containers/KanbanSettings/KanbanSettings.d.ts.map +1 -0
  74. package/dist/types/src/containers/KanbanSettings/index.d.ts +2 -0
  75. package/dist/types/src/containers/KanbanSettings/index.d.ts.map +1 -0
  76. package/dist/types/src/containers/index.d.ts +1 -1
  77. package/dist/types/src/containers/index.d.ts.map +1 -1
  78. package/dist/types/src/hooks/index.d.ts +1 -0
  79. package/dist/types/src/hooks/index.d.ts.map +1 -1
  80. package/dist/types/src/hooks/useEchoChangeCallback.d.ts.map +1 -1
  81. package/dist/types/src/hooks/useItemsProjection.d.ts +10 -0
  82. package/dist/types/src/hooks/useItemsProjection.d.ts.map +1 -0
  83. package/dist/types/src/hooks/useKanbanBoardModel.browser.test.d.ts +2 -0
  84. package/dist/types/src/hooks/useKanbanBoardModel.browser.test.d.ts.map +1 -0
  85. package/dist/types/src/hooks/useKanbanBoardModel.d.ts.map +1 -1
  86. package/dist/types/src/hooks/useKanbanColumnEventHandler.d.ts +1 -1
  87. package/dist/types/src/hooks/useKanbanColumnEventHandler.d.ts.map +1 -1
  88. package/dist/types/src/hooks/useKanbanItemEventHandler.d.ts +1 -1
  89. package/dist/types/src/hooks/useKanbanItemEventHandler.d.ts.map +1 -1
  90. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -1
  91. package/dist/types/src/index.d.ts +2 -1
  92. package/dist/types/src/index.d.ts.map +1 -1
  93. package/dist/types/src/operations/definitions.d.ts +2 -2
  94. package/dist/types/src/operations/definitions.d.ts.map +1 -1
  95. package/dist/types/src/operations/delete-card-field.d.ts +1 -1
  96. package/dist/types/src/operations/delete-card-field.d.ts.map +1 -1
  97. package/dist/types/src/operations/delete-card.d.ts +1 -1
  98. package/dist/types/src/operations/delete-card.d.ts.map +1 -1
  99. package/dist/types/src/operations/index.d.ts +1 -1
  100. package/dist/types/src/operations/index.d.ts.map +1 -1
  101. package/dist/types/src/operations/restore-card-field.d.ts +1 -1
  102. package/dist/types/src/operations/restore-card-field.d.ts.map +1 -1
  103. package/dist/types/src/operations/restore-card.d.ts +1 -1
  104. package/dist/types/src/operations/restore-card.d.ts.map +1 -1
  105. package/dist/types/src/playwright/board-manager.d.ts.map +1 -1
  106. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -1
  107. package/dist/types/src/translations.d.ts +24 -24
  108. package/dist/types/src/translations.d.ts.map +1 -1
  109. package/dist/types/src/types/Kanban.d.ts +78 -6
  110. package/dist/types/src/types/Kanban.d.ts.map +1 -1
  111. package/dist/types/src/types/constants.d.ts +3 -3
  112. package/dist/types/src/types/constants.d.ts.map +1 -1
  113. package/dist/types/src/types/migrations.test.d.ts +2 -0
  114. package/dist/types/src/types/migrations.test.d.ts.map +1 -0
  115. package/dist/types/src/types/schema.d.ts +15 -1
  116. package/dist/types/src/types/schema.d.ts.map +1 -1
  117. package/dist/types/src/types/types.d.ts +2 -2
  118. package/dist/types/src/util/arrangement.d.ts +7 -3
  119. package/dist/types/src/util/arrangement.d.ts.map +1 -1
  120. package/dist/types/tsconfig.tsbuildinfo +1 -1
  121. package/package.json +50 -50
  122. package/src/KanbanPlugin.node.ts +55 -0
  123. package/src/KanbanPlugin.test.ts +31 -0
  124. package/src/KanbanPlugin.tsx +11 -5
  125. package/src/blueprints/kanban-blueprint.ts +2 -3
  126. package/src/capabilities/artifact-definition.ts +1 -1
  127. package/src/capabilities/blueprint-definition.ts +2 -0
  128. package/src/capabilities/index.ts +3 -1
  129. package/src/capabilities/migrations.ts +35 -0
  130. package/src/capabilities/operation-handler.ts +1 -1
  131. package/src/capabilities/react-surface.tsx +16 -8
  132. package/src/components/KanbanBoard/KanbanBoard.stories.tsx +7 -3
  133. package/src/components/KanbanBoard/KanbanBoard.tsx +7 -2
  134. package/src/components/KanbanBoard/KanbanCard.tsx +15 -3
  135. package/src/components/KanbanBoard/KanbanColumn.tsx +7 -5
  136. package/src/containers/KanbanContainer/KanbanContainer.stories.tsx +21 -16
  137. package/src/containers/KanbanContainer/KanbanContainer.tsx +89 -7
  138. package/src/containers/KanbanSettings/KanbanSettings.tsx +94 -0
  139. package/src/containers/KanbanSettings/index.ts +5 -0
  140. package/src/containers/index.ts +1 -1
  141. package/src/hooks/index.ts +1 -0
  142. package/src/hooks/useEchoChangeCallback.ts +2 -2
  143. package/src/hooks/useItemsProjection.ts +44 -0
  144. package/src/hooks/{useKanbanBoardModel.test.ts → useKanbanBoardModel.browser.test.ts} +3 -3
  145. package/src/hooks/useKanbanBoardModel.ts +18 -5
  146. package/src/hooks/useProjectionModel.ts +2 -2
  147. package/src/index.ts +6 -2
  148. package/src/operations/definitions.ts +1 -1
  149. package/src/operations/delete-card-field.ts +1 -1
  150. package/src/operations/delete-card.ts +1 -1
  151. package/src/operations/index.ts +1 -1
  152. package/src/operations/restore-card-field.ts +1 -1
  153. package/src/operations/restore-card.ts +1 -1
  154. package/src/types/Kanban.ts +92 -12
  155. package/src/types/migrations.test.ts +83 -0
  156. package/src/types/schema.ts +19 -1
  157. package/src/types/types.ts +2 -2
  158. package/src/util/arrangement.test.ts +10 -0
  159. package/src/util/arrangement.ts +24 -14
  160. package/dist/lib/browser/chunk-A3PBV3S5.mjs.map +0 -7
  161. package/dist/lib/browser/delete-card-VPNVIWOA.mjs.map +0 -7
  162. package/dist/lib/browser/delete-card-field-4HHF2GYX.mjs.map +0 -7
  163. package/dist/lib/browser/restore-card-4GG2RYKR.mjs.map +0 -7
  164. package/dist/lib/browser/restore-card-field-3T26ACYX.mjs.map +0 -7
  165. package/dist/lib/node-esm/chunk-6LELYA2G.mjs.map +0 -7
  166. package/dist/lib/node-esm/delete-card-5PW5OMFN.mjs.map +0 -7
  167. package/dist/lib/node-esm/delete-card-field-KPJU2AQ3.mjs.map +0 -7
  168. package/dist/lib/node-esm/restore-card-X2TKMU5A.mjs.map +0 -7
  169. package/dist/lib/node-esm/restore-card-field-IUTL4RTR.mjs.map +0 -7
  170. package/dist/types/src/containers/KanbanViewEditor/KanbanViewEditor.d.ts +0 -6
  171. package/dist/types/src/containers/KanbanViewEditor/KanbanViewEditor.d.ts.map +0 -1
  172. package/dist/types/src/containers/KanbanViewEditor/index.d.ts +0 -2
  173. package/dist/types/src/containers/KanbanViewEditor/index.d.ts.map +0 -1
  174. package/dist/types/src/hooks/useKanbanBoardModel.test.d.ts +0 -2
  175. package/dist/types/src/hooks/useKanbanBoardModel.test.d.ts.map +0 -1
  176. package/src/containers/KanbanViewEditor/KanbanViewEditor.tsx +0 -63
  177. package/src/containers/KanbanViewEditor/index.ts +0 -5
@@ -2,31 +2,42 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { RegistryContext } from '@effect-atom/atom-react';
5
+ import { Atom, RegistryContext } from '@effect-atom/atom-react';
6
6
  import React, { useCallback, useContext, useMemo } from 'react';
7
7
 
8
8
  import { useCapabilities, useOperationInvoker } from '@dxos/app-framework/ui';
9
9
  import { AppCapabilities } from '@dxos/app-toolkit';
10
10
  import { type AppSurface } from '@dxos/app-toolkit/ui';
11
- import { Filter, Obj, Query, Type } from '@dxos/echo';
12
- import { AtomQuery } from '@dxos/echo-atom';
11
+ import { Filter, Obj, Query, type Ref, Type } from '@dxos/echo';
12
+ import { AtomObj, AtomQuery } from '@dxos/echo-atom';
13
13
  import { useObject, useSchema } from '@dxos/react-client/echo';
14
14
  import { Panel, Toolbar } from '@dxos/react-ui';
15
15
  import { getTagFromQuery, getTypenameFromQuery } from '@dxos/schema';
16
16
 
17
17
  import { KanbanBoard } from '#components';
18
- import { useEchoChangeCallback, useProjectionModel } from '#hooks';
18
+ import { useEchoChangeCallback, useItemsProjection, useProjectionModel } from '#hooks';
19
19
  import { KanbanOperation } from '#operations';
20
- import { type Kanban } from '#types';
20
+ import { Kanban } from '#types';
21
21
 
22
22
  export type KanbanContainerProps = AppSurface.ObjectArticleProps<Kanban.Kanban>;
23
23
 
24
- export const KanbanContainer = ({ role, subject: object }: KanbanContainerProps) => {
24
+ export const KanbanContainer = (props: KanbanContainerProps) => {
25
+ // Branch on `kanban.spec.kind`: view-variant runs a typename query through
26
+ // `useProjectionModel`; items-variant dereferences `kanban.spec.items` and
27
+ // uses a stub projection from `useItemsProjection`.
28
+ return Kanban.isKanbanItems(props.subject) ? (
29
+ <ItemsKanbanContainer {...props} subject={props.subject} />
30
+ ) : (
31
+ <ViewKanbanContainer {...props} />
32
+ );
33
+ };
34
+
35
+ const ViewKanbanContainer = ({ role, subject: object }: KanbanContainerProps) => {
25
36
  const registry = useContext(RegistryContext);
26
37
  const schemas = useCapabilities(AppCapabilities.Schema);
27
38
  const db = Obj.getDatabase(object);
28
39
  const { invokePromise } = useOperationInvoker();
29
- const [view] = useObject(object.view);
40
+ const [view] = useObject(object.spec.kind === 'view' ? object.spec.view : undefined);
30
41
  const typename = view?.query ? getTypenameFromQuery(view.query.ast) : undefined;
31
42
  const tag = view?.query ? getTagFromQuery(view.query.ast) : undefined;
32
43
 
@@ -94,3 +105,74 @@ export const KanbanContainer = ({ role, subject: object }: KanbanContainerProps)
94
105
  </Panel.Root>
95
106
  );
96
107
  };
108
+
109
+ type ItemsKanbanContainerProps = Omit<KanbanContainerProps, 'subject'> & { subject: Kanban.KanbanItems };
110
+
111
+ const ItemsKanbanContainer = ({ role, subject: object }: ItemsKanbanContainerProps) => {
112
+ const db = Obj.getDatabase(object);
113
+ const projection = useItemsProjection(object);
114
+ const change = useEchoChangeCallback(object);
115
+
116
+ // TODO(wittjosiah): pass refs (not loaded objects) through to the kanban
117
+ // board and let `KanbanCard` subscribe to its own ref via `useObject`.
118
+ // Today this atom subscribes to *every* item — any one changing causes the
119
+ // container (and the model's per-column atoms) to recompute. With cards
120
+ // subscribing themselves, the container only needs the refs and the
121
+ // per-card render is independent. Requires:
122
+ // - `KanbanCard` to accept `Ref<Obj.Unknown>` as `data` and call
123
+ // `useObject(ref)` internally.
124
+ // - The model to handle a ref-bearing item shape (id from
125
+ // `ref.dxn.asEchoDXN()?.echoId`) and use arrangement-only ordering
126
+ // for items-variant (no pivot-value fallback, since refs don't expose
127
+ // the pivot field without loading).
128
+ // - `Mosaic.isItem` to accept the ref wrapper alongside `Obj.isObject`.
129
+ const itemsAtom = useMemo(
130
+ () =>
131
+ Atom.make((get) => {
132
+ const out: Obj.Unknown[] = [];
133
+ for (const ref of object.spec.items as ReadonlyArray<Ref.Ref<Obj.Unknown>>) {
134
+ const target = get(AtomObj.make(ref));
135
+ if (target == null) {
136
+ continue;
137
+ }
138
+ // Drop soft-deleted cards (e.g. Trello-closed cards). The ref
139
+ // stays in `spec.items` so arrangement is preserved, but the card
140
+ // shouldn't render.
141
+ if (Obj.isDeleted(target)) {
142
+ continue;
143
+ }
144
+ out.push(target as unknown as Obj.Unknown);
145
+ }
146
+ return out;
147
+ }),
148
+ [object.spec.items],
149
+ );
150
+
151
+ const handleCardRemove = useCallback(() => undefined, []);
152
+
153
+ if (!object || !db || !change) {
154
+ return null;
155
+ }
156
+
157
+ // TODO(wittjosiah): wire `onCardAdd` to the create-object flow so
158
+ // users can add items directly from the kanban (currently the column's
159
+ // "+" button is hidden because `onCardAdd` is undefined).
160
+ return (
161
+ <Panel.Root role={role}>
162
+ <Panel.Toolbar asChild>
163
+ <Toolbar.Root />
164
+ </Panel.Toolbar>
165
+ <KanbanBoard.Root
166
+ kanban={object}
167
+ projection={projection}
168
+ items={itemsAtom}
169
+ change={change}
170
+ onCardRemove={handleCardRemove}
171
+ >
172
+ <Panel.Content asChild>
173
+ <KanbanBoard.Content />
174
+ </Panel.Content>
175
+ </KanbanBoard.Root>
176
+ </Panel.Root>
177
+ );
178
+ };
@@ -0,0 +1,94 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { RegistryContext } from '@effect-atom/atom-react';
6
+ import React, { useCallback, useContext, useMemo } from 'react';
7
+
8
+ import { type AppSurface } from '@dxos/app-toolkit/ui';
9
+ import { Obj } from '@dxos/echo';
10
+ import { Format } from '@dxos/echo/internal';
11
+ import { useObject, useSchema } from '@dxos/react-client/echo';
12
+ import { Form, type FormFieldMap, SelectField } from '@dxos/react-ui-form';
13
+ import { getTypenameFromQuery } from '@dxos/schema';
14
+
15
+ import { useProjectionModel } from '#hooks';
16
+ import { type Kanban, KanbanSettingsSchema, KanbanViewSettingsSchema, UNCATEGORIZED_VALUE } from '#types';
17
+
18
+ export type KanbanSettingsProps = AppSurface.ObjectPropertiesProps<Kanban.Kanban>;
19
+
20
+ /**
21
+ * Settings panel for a Kanban. Renders fields common to every kanban
22
+ * (currently the "Hide uncategorized column" toggle); for view-variant
23
+ * kanbans an additional "Column field" picker drives the View's pivot
24
+ * field. Items-variant kanbans use a hardcoded `spec.pivotField`, so that
25
+ * field is omitted there.
26
+ */
27
+ export const KanbanSettings = ({ subject: object }: KanbanSettingsProps) => {
28
+ const registry = useContext(RegistryContext);
29
+ const db = Obj.getDatabase(object);
30
+ const isView = object.spec.kind === 'view';
31
+ const [view, updateView] = useObject(object.spec.kind === 'view' ? object.spec.view : undefined);
32
+ const [, updateKanban] = useObject(object);
33
+ const currentTypename = view?.query ? getTypenameFromQuery(view.query.ast) : undefined;
34
+ const schema = useSchema(db, currentTypename);
35
+ const projection = useProjectionModel(schema, object, registry);
36
+
37
+ const fieldProjections = projection?.getFieldProjections() ?? [];
38
+ const selectFields = useMemo(
39
+ () =>
40
+ fieldProjections
41
+ .filter((field) => field.props.format === Format.TypeFormat.SingleSelect)
42
+ .map(({ field }) => ({ value: field.id, label: field.path })),
43
+ [fieldProjections],
44
+ );
45
+
46
+ const hideUncategorized = object.arrangement.columns[UNCATEGORIZED_VALUE]?.hidden ?? false;
47
+
48
+ const handleValuesChanged = useCallback(
49
+ (values: Partial<{ columnFieldId: string; hideUncategorized: boolean }>) => {
50
+ if (isView && values.columnFieldId != null) {
51
+ updateView((view) => {
52
+ view.projection.pivotFieldId = values.columnFieldId!;
53
+ });
54
+ }
55
+ if (values.hideUncategorized !== undefined) {
56
+ updateKanban((kanban) => {
57
+ const existing = kanban.arrangement.columns[UNCATEGORIZED_VALUE];
58
+ if (existing) {
59
+ existing.hidden = values.hideUncategorized;
60
+ } else {
61
+ kanban.arrangement.columns[UNCATEGORIZED_VALUE] = {
62
+ ids: [],
63
+ hidden: values.hideUncategorized,
64
+ };
65
+ }
66
+ });
67
+ }
68
+ },
69
+ [isView, updateView, updateKanban],
70
+ );
71
+
72
+ const initialValues = useMemo(
73
+ () => ({
74
+ ...(isView ? { columnFieldId: view?.projection.pivotFieldId } : {}),
75
+ hideUncategorized,
76
+ }),
77
+ [isView, view?.projection.pivotFieldId, hideUncategorized],
78
+ );
79
+
80
+ const fieldMap: FormFieldMap = useMemo(
81
+ () => ({ columnFieldId: (props) => <SelectField {...props} options={selectFields} /> }),
82
+ [selectFields],
83
+ );
84
+
85
+ // Schema is picked by `kanban.spec.kind` — they have different shapes,
86
+ // so cast for `Form.Root`'s single-schema prop.
87
+ const settingsSchema = (isView ? KanbanViewSettingsSchema : KanbanSettingsSchema) as any;
88
+
89
+ return (
90
+ <Form.Root schema={settingsSchema} values={initialValues} fieldMap={fieldMap} onValuesChanged={handleValuesChanged}>
91
+ <Form.FieldSet />
92
+ </Form.Root>
93
+ );
94
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export { KanbanSettings as default } from './KanbanSettings';
@@ -5,4 +5,4 @@
5
5
  import { type ComponentType, lazy } from 'react';
6
6
 
7
7
  export const KanbanContainer: ComponentType<any> = lazy(() => import('./KanbanContainer'));
8
- export const KanbanViewEditor: ComponentType<any> = lazy(() => import('./KanbanViewEditor'));
8
+ export const KanbanSettings: ComponentType<any> = lazy(() => import('./KanbanSettings'));
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './useEchoChangeCallback';
6
+ export * from './useItemsProjection';
6
7
  export * from './useKanbanBoardModel';
7
8
  export * from './useKanbanColumnEventHandler';
8
9
  export * from './useKanbanItemEventHandler';
@@ -13,9 +13,9 @@ import { type Kanban, type KanbanChangeCallback } from '#types';
13
13
  * Use this when the kanban and items are stored in the ECHO database.
14
14
  */
15
15
  export const createEchoChangeCallback = <T extends Obj.Unknown>(kanban: Kanban.Kanban): KanbanChangeCallback<T> => ({
16
- kanban: (mutate) => Obj.change(kanban, (kanban) => mutate(kanban)),
16
+ kanban: (mutate) => Obj.update(kanban, (kanban) => mutate(kanban)),
17
17
  setItemField: (item, field, value) => {
18
- Obj.change(item, (item: any) => {
18
+ Obj.update(item, (item: any) => {
19
19
  item[field] = value;
20
20
  });
21
21
  },
@@ -0,0 +1,44 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { Atom } from '@effect-atom/atom-react';
6
+ import { useMemo } from 'react';
7
+
8
+ import type { ProjectionModel } from '@dxos/schema';
9
+
10
+ import { type Kanban } from '#types';
11
+
12
+ /**
13
+ * Minimal `ProjectionModel` for `spec.kind === 'items'` (no View). Supplies `pivotField`
14
+ * and column options from `arrangement.columns` keys—written by sync so columns exist
15
+ * before refs hydrate. Stubs `getFieldProjections` / `getHiddenProperties` for shared
16
+ * board/card UI; hides the pivot on the card body (column shows it); Expando cards render title only.
17
+ */
18
+ export const useItemsProjection = (kanban: Kanban.KanbanItems): ProjectionModel => {
19
+ return useMemo(() => {
20
+ const pivotField = kanban.spec.pivotField;
21
+
22
+ const optionIds = Object.keys(kanban.arrangement?.columns ?? {});
23
+ const options = optionIds.map((id) => ({ id, title: id, color: 'neutral' as const }));
24
+
25
+ const fieldProjection: any = {
26
+ field: { id: pivotField, path: pivotField },
27
+ props: { property: pivotField, options },
28
+ };
29
+
30
+ const fields = Atom.make(() => [fieldProjection.field]);
31
+
32
+ const stub: Pick<ProjectionModel, 'tryGetFieldProjection' | 'getFieldProjections' | 'getHiddenProperties'> & {
33
+ fields: typeof fields;
34
+ } = {
35
+ fields,
36
+ tryGetFieldProjection: (id: string) => (id === pivotField ? fieldProjection : undefined),
37
+ getFieldProjections: () => [],
38
+ getHiddenProperties: () => [pivotField],
39
+ };
40
+
41
+ // TODO(wittjosiah): Refactor ProjectionModel to be an interface that we can fulfill.
42
+ return stub as unknown as ProjectionModel;
43
+ }, [kanban.arrangement?.columns, kanban.spec.pivotField]);
44
+ };
@@ -146,7 +146,7 @@ describe('useKanbanBoardModel', () => {
146
146
  expect(orderBefore).toEqual(['__uncategorized__', 'a', 'b']);
147
147
 
148
148
  act(() => {
149
- Obj.change(kanban, (kanban) => {
149
+ Obj.update(kanban, (kanban) => {
150
150
  kanban.arrangement.order = ['b', 'a'];
151
151
  });
152
152
  });
@@ -187,7 +187,7 @@ describe('useKanbanBoardModel', () => {
187
187
  });
188
188
 
189
189
  act(() => {
190
- Obj.change(kanban, (kanban) => {
190
+ Obj.update(kanban, (kanban) => {
191
191
  kanban.arrangement.columns['a'] = {
192
192
  ids: [item3.id, item1.id, item2.id],
193
193
  };
@@ -225,7 +225,7 @@ describe('useKanbanBoardModel', () => {
225
225
  });
226
226
 
227
227
  act(() => {
228
- Obj.change(kanban, (kanban) => {
228
+ Obj.update(kanban, (kanban) => {
229
229
  kanban.arrangement.columns['b'] = { ids: [itemB.id] };
230
230
  });
231
231
  });
@@ -38,14 +38,27 @@ export function useKanbanBoardModel<T extends BaseKanbanItem = BaseKanbanItem>(
38
38
  // Source atoms: reactive reads from the kanban object; items come from the passed-in atom (e.g. AtomQuery or in-memory).
39
39
  const arrangementAtom = useMemo(() => AtomObj.makeProperty(kanban, 'arrangement'), [kanban]);
40
40
  const viewSnapshotAtom = useMemo(
41
- () => (kanban?.view ? AtomObj.make(kanban.view) : Atom.make<undefined>(() => undefined)),
42
- [kanban?.view],
41
+ () =>
42
+ kanban?.spec?.kind === 'view' && kanban.spec.view
43
+ ? AtomObj.make(kanban.spec.view)
44
+ : Atom.make<undefined>(() => undefined),
45
+ [kanban?.spec],
43
46
  );
44
47
 
45
- /** Only changes when view.projection.pivotFieldId changes; keeps columns from firing on other view updates. */
48
+ /**
49
+ * Only changes when the discriminator-relevant pivot input changes.
50
+ * View-variant: derived from `view.projection.pivotFieldId`.
51
+ * Items-variant: the kanban's `spec.pivotField` (the property name itself acts as the field id).
52
+ */
46
53
  const pivotFieldIdAtom = useMemo(
47
- () => Atom.make((get) => get(viewSnapshotAtom)?.projection?.pivotFieldId as string | undefined),
48
- [viewSnapshotAtom],
54
+ () =>
55
+ Atom.make((get) => {
56
+ if (kanban?.spec.kind === 'items') {
57
+ return kanban.spec.pivotField;
58
+ }
59
+ return get(viewSnapshotAtom)?.projection?.pivotFieldId as string | undefined;
60
+ }),
61
+ [kanban?.spec, viewSnapshotAtom],
49
62
  );
50
63
 
51
64
  // Effective per-column ids: from kanban.arrangement.columns; empty when arrangement has no columns.
@@ -30,11 +30,11 @@ export const useProjectionModel = <S extends Type.AnyEntity>(
30
30
 
31
31
  useAsyncEffect(
32
32
  async (controller) => {
33
- if (!schema || !kanban) {
33
+ if (!schema || !kanban || kanban.spec.kind !== 'view') {
34
34
  return;
35
35
  }
36
36
  try {
37
- const view = await kanban.view.load();
37
+ const view = await kanban.spec.view.load();
38
38
  if (controller.signal.aborted) {
39
39
  return;
40
40
  }
package/src/index.ts CHANGED
@@ -2,6 +2,10 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './meta';
5
+ import { Plugin } from '@dxos/app-framework';
6
+
7
+ import { meta } from './meta';
6
8
 
7
- export * from './KanbanPlugin';
9
+ export const KanbanPlugin = Plugin.lazy(meta, () => import('#plugin'));
10
+
11
+ export * from './meta';
@@ -3,8 +3,8 @@
3
3
  import * as Schema from 'effect/Schema';
4
4
 
5
5
  import { Capability } from '@dxos/app-framework';
6
+ import { Operation } from '@dxos/compute';
6
7
  import { View } from '@dxos/echo';
7
- import { Operation } from '@dxos/operation';
8
8
 
9
9
  import { meta } from '#meta';
10
10
 
@@ -3,10 +3,10 @@
3
3
  import * as Effect from 'effect/Effect';
4
4
 
5
5
  import { Capabilities, Capability } from '@dxos/app-framework';
6
+ import { Operation } from '@dxos/compute';
6
7
  import { JsonSchema, Obj } from '@dxos/echo';
7
8
  import { type EchoSchema } from '@dxos/echo/internal';
8
9
  import { invariant } from '@dxos/invariant';
9
- import { Operation } from '@dxos/operation';
10
10
  import { ProjectionModel, createEchoChangeCallback, getTypenameFromQuery } from '@dxos/schema';
11
11
 
12
12
  import { DeleteCardField } from './definitions';
@@ -2,9 +2,9 @@
2
2
 
3
3
  import * as Effect from 'effect/Effect';
4
4
 
5
+ import { Operation } from '@dxos/compute';
5
6
  import { Obj } from '@dxos/echo';
6
7
  import { invariant } from '@dxos/invariant';
7
- import { Operation } from '@dxos/operation';
8
8
 
9
9
  import { DeleteCard } from './definitions';
10
10
 
@@ -1,6 +1,6 @@
1
1
  // Copyright 2025 DXOS.org
2
2
 
3
- import { OperationHandlerSet } from '@dxos/operation';
3
+ import { OperationHandlerSet } from '@dxos/compute';
4
4
 
5
5
  export * as KanbanOperation from './definitions';
6
6
 
@@ -3,10 +3,10 @@
3
3
  import * as Effect from 'effect/Effect';
4
4
 
5
5
  import { Capabilities, Capability } from '@dxos/app-framework';
6
+ import { Operation } from '@dxos/compute';
6
7
  import { JsonSchema, Obj } from '@dxos/echo';
7
8
  import { type EchoSchema } from '@dxos/echo/internal';
8
9
  import { invariant } from '@dxos/invariant';
9
- import { Operation } from '@dxos/operation';
10
10
  import { ProjectionModel, createEchoChangeCallback, getTypenameFromQuery } from '@dxos/schema';
11
11
 
12
12
  import { RestoreCardField } from './definitions';
@@ -2,9 +2,9 @@
2
2
 
3
3
  import * as Effect from 'effect/Effect';
4
4
 
5
+ import { Operation } from '@dxos/compute';
5
6
  import { Obj } from '@dxos/echo';
6
7
  import { invariant } from '@dxos/invariant';
7
- import { Operation } from '@dxos/operation';
8
8
 
9
9
  import { RestoreCard } from './definitions';
10
10
 
@@ -9,10 +9,15 @@ import { View } from '@dxos/echo';
9
9
  import { FormInputAnnotation, LabelAnnotation } from '@dxos/echo/internal';
10
10
  import { ViewAnnotation } from '@dxos/schema';
11
11
 
12
- /** Per-column entry (ids order, optional hidden). */
12
+ /**
13
+ * Per-column entry: ordered card ids plus an optional `hidden` flag that
14
+ * removes the column from the rendered board (and from the model's column
15
+ * list). Today only the uncategorized column is exposed in settings, but
16
+ * the data structure supports per-column hiding generally.
17
+ */
13
18
  const ArrangementColumnEntry = Schema.Struct({
14
19
  ids: Schema.Array(Obj.ID),
15
- hidden: Schema.optional(Schema.Boolean),
20
+ hidden: Schema.Boolean.pipe(Schema.optional),
16
21
  });
17
22
 
18
23
  /** Keyed by columnValue. */
@@ -29,43 +34,118 @@ export const Arrangement = Schema.Struct({
29
34
 
30
35
  export type Arrangement = Schema.Schema.Type<typeof Arrangement>;
31
36
 
32
- export const Kanban = Schema.Struct({
33
- name: Schema.optional(Schema.String),
37
+ /**
38
+ * v1: pre-existing Kanban shape. Retained as the source for the v1→v2 migration.
39
+ */
40
+ export const KanbanV1 = Schema.Struct({
41
+ name: Schema.String.pipe(Schema.optional),
42
+ view: Ref.Ref(View.View).pipe(FormInputAnnotation.set(false)),
43
+ arrangement: Arrangement,
44
+ }).pipe(
45
+ Type.object({
46
+ typename: 'org.dxos.type.kanban',
47
+ version: '0.1.0',
48
+ }),
49
+ );
34
50
 
51
+ //
52
+ // v2 — `spec` is a discriminated union of how items are sourced.
53
+ //
54
+ // Mirrors the canonical DXOS pattern (see `Trigger.Spec` in
55
+ // `@dxos/functions/src/types/Trigger.ts` and `Sequence.Source` in
56
+ // `@dxos/plugin-zen`): the `Type.object` schema is a flat `Schema.Struct`,
57
+ // and the discriminated union lives one level down as a single field whose
58
+ // variants are tagged with a `kind` literal.
59
+ //
60
+
61
+ /** View-variant: items come from running the View's query (the original behaviour). */
62
+ export const KanbanViewSpec = Schema.Struct({
63
+ kind: Schema.Literal('view').pipe(FormInputAnnotation.set(false)),
35
64
  view: Ref.Ref(View.View).pipe(FormInputAnnotation.set(false)),
65
+ });
66
+ export type KanbanViewSpec = Schema.Schema.Type<typeof KanbanViewSpec>;
67
+
68
+ /** Items-variant: kanban owns its items as an explicit ref array (used by externally-synced kanbans). */
69
+ export const KanbanItemsSpec = Schema.Struct({
70
+ kind: Schema.Literal('items').pipe(FormInputAnnotation.set(false)),
71
+ /** Property path on each item that drives column membership (e.g. `'listName'`). */
72
+ pivotField: Schema.String,
73
+ /** Items owned directly by the kanban. */
74
+ items: Schema.Array(Ref.Ref(Obj.Unknown)).pipe(FormInputAnnotation.set(false)),
75
+ });
76
+ export type KanbanItemsSpec = Schema.Schema.Type<typeof KanbanItemsSpec>;
77
+
78
+ /** Discriminated union of source specs. Distinguished by `kind`. */
79
+ export const KanbanSpec = Schema.Union(KanbanViewSpec, KanbanItemsSpec);
80
+ export type KanbanSpec = Schema.Schema.Type<typeof KanbanSpec>;
36
81
 
37
- /** Column display order and per-column card ids. */
82
+ export const Kanban = Schema.Struct({
83
+ name: Schema.String.pipe(Schema.optional),
38
84
  arrangement: Arrangement,
85
+ /** How this kanban sources its items. Discriminated by `spec.kind`. */
86
+ spec: KanbanSpec,
39
87
  }).pipe(
40
88
  Type.object({
41
89
  typename: 'org.dxos.type.kanban',
42
- version: '0.1.0',
90
+ version: '0.2.0',
43
91
  }),
44
92
  LabelAnnotation.set(['name']),
45
- ViewAnnotation.set(true),
93
+ ViewAnnotation.set(['spec', 'view']),
46
94
  Annotation.IconAnnotation.set({
47
95
  icon: 'ph--kanban--regular',
48
96
  hue: 'green',
49
97
  }),
50
98
  );
51
99
 
52
- /** Instance type; use Kanban.Kanban in type position so namespace has .Kanban as type and .KanbanSchema as schema. */
100
+ /** Instance type; narrow on `kanban.spec.kind` (or use the guards below). */
53
101
  export interface Kanban extends Schema.Schema.Type<typeof Kanban> {}
54
102
 
55
- type MakeProps = Omit<Partial<Obj.MakeProps<typeof Kanban>>, 'view'> & {
103
+ /** Narrowed view-variant kanban. */
104
+ export type KanbanView = Kanban & { spec: KanbanViewSpec };
105
+
106
+ /** Narrowed items-variant kanban. */
107
+ export type KanbanItems = Kanban & { spec: KanbanItemsSpec };
108
+
109
+ export const isKanbanView = (kanban: Kanban): kanban is KanbanView => kanban.spec.kind === 'view';
110
+ export const isKanbanItems = (kanban: Kanban): kanban is KanbanItems => kanban.spec.kind === 'items';
111
+
112
+ type MakeViewProps = {
113
+ name?: string;
56
114
  view: View.View;
115
+ arrangement?: Arrangement;
57
116
  };
58
117
 
59
118
  /**
60
- * Make a kanban as a view of a data set.
119
+ * Make a view-variant kanban (items sourced via the View's query).
61
120
  */
62
- export const make = (props: MakeProps): Kanban => {
121
+ export const make = (props: MakeViewProps): Kanban => {
63
122
  const { name, view, arrangement } = props;
64
123
  const order = arrangement?.order ?? [];
65
124
  const columns = arrangement?.columns ?? {};
66
125
  return Obj.make(Kanban, {
67
126
  name,
68
- view: Ref.make(view),
69
127
  arrangement: { order, columns },
128
+ spec: { kind: 'view' as const, view: Ref.make(view) },
129
+ });
130
+ };
131
+
132
+ type MakeItemsProps = {
133
+ name?: string;
134
+ arrangement?: Arrangement;
135
+ pivotField: string;
136
+ items?: ReadonlyArray<Ref.Ref<Obj.Unknown>>;
137
+ };
138
+
139
+ /**
140
+ * Make an items-variant kanban (items list owned by the kanban itself, e.g. populated by a sync integration).
141
+ */
142
+ export const makeItems = (props: MakeItemsProps): Kanban => {
143
+ const { name, arrangement, pivotField, items = [] } = props;
144
+ const order = arrangement?.order ?? [];
145
+ const columns = arrangement?.columns ?? {};
146
+ return Obj.make(Kanban, {
147
+ name,
148
+ arrangement: { order, columns },
149
+ spec: { kind: 'items' as const, pivotField, items },
70
150
  });
71
151
  };