@dxos/plugin-kanban 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6

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 (232) hide show
  1. package/dist/lib/browser/blueprints/index.mjs +23 -4
  2. package/dist/lib/browser/blueprints/index.mjs.map +4 -4
  3. package/dist/lib/browser/chunk-A3PBV3S5.mjs +105 -0
  4. package/dist/lib/browser/chunk-A3PBV3S5.mjs.map +7 -0
  5. package/dist/lib/browser/delete-card-VPNVIWOA.mjs +32 -0
  6. package/dist/lib/browser/delete-card-VPNVIWOA.mjs.map +7 -0
  7. package/dist/lib/browser/delete-card-field-4HHF2GYX.mjs +50 -0
  8. package/dist/lib/browser/delete-card-field-4HHF2GYX.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +84 -61
  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 +13 -0
  13. package/dist/lib/browser/operations/index.mjs.map +7 -0
  14. package/dist/lib/browser/restore-card-4GG2RYKR.mjs +29 -0
  15. package/dist/lib/browser/restore-card-4GG2RYKR.mjs.map +7 -0
  16. package/dist/lib/browser/restore-card-field-3T26ACYX.mjs +48 -0
  17. package/dist/lib/browser/restore-card-field-3T26ACYX.mjs.map +7 -0
  18. package/dist/lib/browser/types/index.mjs +94 -8
  19. package/dist/lib/browser/types/index.mjs.map +4 -4
  20. package/dist/lib/node-esm/blueprints/index.mjs +23 -4
  21. package/dist/lib/node-esm/blueprints/index.mjs.map +4 -4
  22. package/dist/lib/node-esm/chunk-6LELYA2G.mjs +106 -0
  23. package/dist/lib/node-esm/chunk-6LELYA2G.mjs.map +7 -0
  24. package/dist/lib/node-esm/delete-card-5PW5OMFN.mjs +33 -0
  25. package/dist/lib/node-esm/delete-card-5PW5OMFN.mjs.map +7 -0
  26. package/dist/lib/node-esm/delete-card-field-KPJU2AQ3.mjs +51 -0
  27. package/dist/lib/node-esm/delete-card-field-KPJU2AQ3.mjs.map +7 -0
  28. package/dist/lib/node-esm/index.mjs +84 -61
  29. package/dist/lib/node-esm/index.mjs.map +4 -4
  30. package/dist/lib/node-esm/meta.json +1 -1
  31. package/dist/lib/node-esm/operations/index.mjs +14 -0
  32. package/dist/lib/node-esm/operations/index.mjs.map +7 -0
  33. package/dist/lib/node-esm/restore-card-X2TKMU5A.mjs +30 -0
  34. package/dist/lib/node-esm/restore-card-X2TKMU5A.mjs.map +7 -0
  35. package/dist/lib/node-esm/restore-card-field-IUTL4RTR.mjs +49 -0
  36. package/dist/lib/node-esm/restore-card-field-IUTL4RTR.mjs.map +7 -0
  37. package/dist/lib/node-esm/types/index.mjs +94 -8
  38. package/dist/lib/node-esm/types/index.mjs.map +4 -4
  39. package/dist/types/src/KanbanPlugin.d.ts.map +1 -1
  40. package/dist/types/src/blueprints/index.d.ts +1 -1
  41. package/dist/types/src/blueprints/index.d.ts.map +1 -1
  42. package/dist/types/src/blueprints/kanban-blueprint.d.ts +3 -21
  43. package/dist/types/src/blueprints/kanban-blueprint.d.ts.map +1 -1
  44. package/dist/types/src/capabilities/{artifact-definition/artifact-definition.d.ts → artifact-definition.d.ts} +1 -1
  45. package/dist/types/src/capabilities/artifact-definition.d.ts.map +1 -0
  46. package/dist/types/src/capabilities/blueprint-definition.d.ts +6 -0
  47. package/dist/types/src/capabilities/blueprint-definition.d.ts.map +1 -0
  48. package/dist/types/src/capabilities/index.d.ts +6 -3
  49. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  50. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  51. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  52. package/dist/types/src/capabilities/react-surface.d.ts +5 -0
  53. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  54. package/dist/types/src/capabilities/undo-mappings.d.ts +5 -0
  55. package/dist/types/src/capabilities/undo-mappings.d.ts.map +1 -0
  56. package/dist/types/src/components/KanbanBoard/KanbanBoard.d.ts +65 -0
  57. package/dist/types/src/components/KanbanBoard/KanbanBoard.d.ts.map +1 -0
  58. package/dist/types/src/components/KanbanBoard/KanbanBoard.stories.d.ts +72 -0
  59. package/dist/types/src/components/KanbanBoard/KanbanBoard.stories.d.ts.map +1 -0
  60. package/dist/types/src/components/KanbanBoard/KanbanCard.d.ts +10 -0
  61. package/dist/types/src/components/KanbanBoard/KanbanCard.d.ts.map +1 -0
  62. package/dist/types/src/components/KanbanBoard/KanbanColumn.d.ts +9 -0
  63. package/dist/types/src/components/KanbanBoard/KanbanColumn.d.ts.map +1 -0
  64. package/dist/types/src/components/KanbanBoard/index.d.ts +2 -0
  65. package/dist/types/src/components/KanbanBoard/index.d.ts.map +1 -0
  66. package/dist/types/src/components/index.d.ts +1 -2
  67. package/dist/types/src/components/index.d.ts.map +1 -1
  68. package/dist/types/src/containers/KanbanContainer/KanbanContainer.d.ts +6 -0
  69. package/dist/types/src/containers/KanbanContainer/KanbanContainer.d.ts.map +1 -0
  70. package/dist/types/src/containers/KanbanContainer/KanbanContainer.stories.d.ts +79 -0
  71. package/dist/types/src/containers/KanbanContainer/KanbanContainer.stories.d.ts.map +1 -0
  72. package/dist/types/src/containers/KanbanContainer/index.d.ts +3 -0
  73. package/dist/types/src/containers/KanbanContainer/index.d.ts.map +1 -0
  74. package/dist/types/src/containers/KanbanViewEditor/KanbanViewEditor.d.ts +6 -0
  75. package/dist/types/src/containers/KanbanViewEditor/KanbanViewEditor.d.ts.map +1 -0
  76. package/dist/types/src/containers/KanbanViewEditor/index.d.ts +3 -0
  77. package/dist/types/src/containers/KanbanViewEditor/index.d.ts.map +1 -0
  78. package/dist/types/src/containers/index.d.ts +4 -0
  79. package/dist/types/src/containers/index.d.ts.map +1 -0
  80. package/dist/types/src/hooks/index.d.ts +6 -0
  81. package/dist/types/src/hooks/index.d.ts.map +1 -0
  82. package/dist/types/src/hooks/useEchoChangeCallback.d.ts +13 -0
  83. package/dist/types/src/hooks/useEchoChangeCallback.d.ts.map +1 -0
  84. package/dist/types/src/hooks/useKanbanBoardModel.d.ts +16 -0
  85. package/dist/types/src/hooks/useKanbanBoardModel.d.ts.map +1 -0
  86. package/dist/types/src/hooks/useKanbanBoardModel.test.d.ts +2 -0
  87. package/dist/types/src/hooks/useKanbanBoardModel.test.d.ts.map +1 -0
  88. package/dist/types/src/hooks/useKanbanColumnEventHandler.d.ts +22 -0
  89. package/dist/types/src/hooks/useKanbanColumnEventHandler.d.ts.map +1 -0
  90. package/dist/types/src/hooks/useKanbanItemEventHandler.d.ts +19 -0
  91. package/dist/types/src/hooks/useKanbanItemEventHandler.d.ts.map +1 -0
  92. package/dist/types/src/hooks/useProjectionModel.d.ts +15 -0
  93. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  94. package/dist/types/src/operations/definitions.d.ts +52 -0
  95. package/dist/types/src/operations/definitions.d.ts.map +1 -0
  96. package/dist/types/src/operations/delete-card-field.d.ts +5 -0
  97. package/dist/types/src/operations/delete-card-field.d.ts.map +1 -0
  98. package/dist/types/src/operations/delete-card.d.ts +5 -0
  99. package/dist/types/src/operations/delete-card.d.ts.map +1 -0
  100. package/dist/types/src/operations/index.d.ts +4 -0
  101. package/dist/types/src/operations/index.d.ts.map +1 -0
  102. package/dist/types/src/operations/restore-card-field.d.ts +5 -0
  103. package/dist/types/src/operations/restore-card-field.d.ts.map +1 -0
  104. package/dist/types/src/operations/restore-card.d.ts +5 -0
  105. package/dist/types/src/operations/restore-card.d.ts.map +1 -0
  106. package/dist/types/src/playwright/board-manager.d.ts +5 -0
  107. package/dist/types/src/playwright/board-manager.d.ts.map +1 -0
  108. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  109. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  110. package/dist/types/src/playwright/smoke.spec.d.ts +2 -0
  111. package/dist/types/src/playwright/smoke.spec.d.ts.map +1 -0
  112. package/dist/types/src/testing/KanbanCardTileSimple.d.ts +7 -0
  113. package/dist/types/src/testing/KanbanCardTileSimple.d.ts.map +1 -0
  114. package/dist/types/src/testing/index.d.ts +2 -0
  115. package/dist/types/src/testing/index.d.ts.map +1 -0
  116. package/dist/types/src/translations.d.ts +48 -32
  117. package/dist/types/src/translations.d.ts.map +1 -1
  118. package/dist/types/src/types/Kanban.d.ts +37 -0
  119. package/dist/types/src/types/Kanban.d.ts.map +1 -0
  120. package/dist/types/src/types/constants.d.ts +6 -0
  121. package/dist/types/src/types/constants.d.ts.map +1 -0
  122. package/dist/types/src/types/index.d.ts +2 -0
  123. package/dist/types/src/types/index.d.ts.map +1 -1
  124. package/dist/types/src/types/schema.d.ts +0 -103
  125. package/dist/types/src/types/schema.d.ts.map +1 -1
  126. package/dist/types/src/types/types.d.ts +28 -0
  127. package/dist/types/src/types/types.d.ts.map +1 -1
  128. package/dist/types/src/util/arrangement.d.ts +68 -0
  129. package/dist/types/src/util/arrangement.d.ts.map +1 -0
  130. package/dist/types/src/util/arrangement.test.d.ts +2 -0
  131. package/dist/types/src/util/arrangement.test.d.ts.map +1 -0
  132. package/dist/types/src/util/index.d.ts +2 -0
  133. package/dist/types/src/util/index.d.ts.map +1 -0
  134. package/dist/types/tsconfig.tsbuildinfo +1 -1
  135. package/package.json +64 -42
  136. package/src/KanbanPlugin.tsx +35 -23
  137. package/src/blueprints/index.ts +1 -1
  138. package/src/blueprints/kanban-blueprint.ts +12 -8
  139. package/src/capabilities/{artifact-definition/artifact-definition.ts → artifact-definition.ts} +10 -9
  140. package/src/capabilities/blueprint-definition.ts +17 -0
  141. package/src/capabilities/index.ts +10 -3
  142. package/src/capabilities/operation-handler.ts +14 -0
  143. package/src/capabilities/{react-surface/react-surface.tsx → react-surface.tsx} +15 -15
  144. package/src/capabilities/undo-mappings.ts +34 -0
  145. package/src/components/KanbanBoard/KanbanBoard.stories.tsx +142 -0
  146. package/src/components/KanbanBoard/KanbanBoard.tsx +193 -0
  147. package/src/components/KanbanBoard/KanbanCard.tsx +86 -0
  148. package/src/components/KanbanBoard/KanbanColumn.tsx +69 -0
  149. package/src/components/KanbanBoard/index.ts +5 -0
  150. package/src/components/index.ts +1 -2
  151. package/src/{components → containers/KanbanContainer}/KanbanContainer.stories.tsx +70 -87
  152. package/src/containers/KanbanContainer/KanbanContainer.tsx +96 -0
  153. package/src/containers/KanbanContainer/index.ts +7 -0
  154. package/src/{components → containers/KanbanViewEditor}/KanbanViewEditor.tsx +23 -19
  155. package/src/containers/KanbanViewEditor/index.ts +7 -0
  156. package/src/containers/index.ts +8 -0
  157. package/src/hooks/index.ts +9 -0
  158. package/src/hooks/useEchoChangeCallback.ts +30 -0
  159. package/src/hooks/useKanbanBoardModel.test.ts +235 -0
  160. package/src/hooks/useKanbanBoardModel.ts +143 -0
  161. package/src/hooks/useKanbanColumnEventHandler.ts +106 -0
  162. package/src/hooks/useKanbanItemEventHandler.ts +133 -0
  163. package/src/hooks/useProjectionModel.ts +58 -0
  164. package/src/meta.ts +1 -1
  165. package/src/operations/definitions.ts +63 -0
  166. package/src/operations/delete-card-field.ts +47 -0
  167. package/src/operations/delete-card.ts +23 -0
  168. package/src/operations/index.ts +12 -0
  169. package/src/operations/restore-card-field.ts +41 -0
  170. package/src/operations/restore-card.ts +21 -0
  171. package/src/playwright/board-manager.ts +13 -0
  172. package/src/playwright/playwright.config.ts +19 -0
  173. package/src/playwright/smoke.spec.ts +107 -0
  174. package/src/testing/KanbanCardTileSimple.tsx +82 -0
  175. package/src/testing/index.ts +5 -0
  176. package/src/translations.ts +26 -18
  177. package/src/types/Kanban.ts +71 -0
  178. package/src/types/constants.ts +9 -0
  179. package/src/types/index.ts +2 -0
  180. package/src/types/schema.ts +0 -76
  181. package/src/types/types.ts +35 -0
  182. package/src/util/arrangement.test.ts +208 -0
  183. package/src/util/arrangement.ts +167 -0
  184. package/src/util/index.ts +5 -0
  185. package/dist/lib/browser/blueprint-definition-T2544VMJ.mjs +0 -17
  186. package/dist/lib/browser/blueprint-definition-T2544VMJ.mjs.map +0 -7
  187. package/dist/lib/browser/chunk-L6N4ZDZ7.mjs +0 -35
  188. package/dist/lib/browser/chunk-L6N4ZDZ7.mjs.map +0 -7
  189. package/dist/lib/browser/chunk-XYQO4VL7.mjs +0 -150
  190. package/dist/lib/browser/chunk-XYQO4VL7.mjs.map +0 -7
  191. package/dist/lib/browser/operation-resolver-UEJHX42A.mjs +0 -162
  192. package/dist/lib/browser/operation-resolver-UEJHX42A.mjs.map +0 -7
  193. package/dist/lib/browser/react-surface-LFUJAPRL.mjs +0 -236
  194. package/dist/lib/browser/react-surface-LFUJAPRL.mjs.map +0 -7
  195. package/dist/lib/node-esm/blueprint-definition-APJQFSHJ.mjs +0 -18
  196. package/dist/lib/node-esm/blueprint-definition-APJQFSHJ.mjs.map +0 -7
  197. package/dist/lib/node-esm/chunk-NN6JMKIT.mjs +0 -152
  198. package/dist/lib/node-esm/chunk-NN6JMKIT.mjs.map +0 -7
  199. package/dist/lib/node-esm/chunk-ZHRMUKTF.mjs +0 -36
  200. package/dist/lib/node-esm/chunk-ZHRMUKTF.mjs.map +0 -7
  201. package/dist/lib/node-esm/operation-resolver-5RPWHZCF.mjs +0 -163
  202. package/dist/lib/node-esm/operation-resolver-5RPWHZCF.mjs.map +0 -7
  203. package/dist/lib/node-esm/react-surface-7TSGBRJL.mjs +0 -237
  204. package/dist/lib/node-esm/react-surface-7TSGBRJL.mjs.map +0 -7
  205. package/dist/types/src/capabilities/artifact-definition/artifact-definition.d.ts.map +0 -1
  206. package/dist/types/src/capabilities/artifact-definition/index.d.ts +0 -3
  207. package/dist/types/src/capabilities/artifact-definition/index.d.ts.map +0 -1
  208. package/dist/types/src/capabilities/blueprint-definition/blueprint-definition.d.ts +0 -9
  209. package/dist/types/src/capabilities/blueprint-definition/blueprint-definition.d.ts.map +0 -1
  210. package/dist/types/src/capabilities/blueprint-definition/index.d.ts +0 -3
  211. package/dist/types/src/capabilities/blueprint-definition/index.d.ts.map +0 -1
  212. package/dist/types/src/capabilities/operation-resolver/index.d.ts +0 -3
  213. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +0 -1
  214. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +0 -5
  215. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +0 -1
  216. package/dist/types/src/capabilities/react-surface/index.d.ts +0 -3
  217. package/dist/types/src/capabilities/react-surface/index.d.ts.map +0 -1
  218. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +0 -5
  219. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +0 -1
  220. package/dist/types/src/components/KanbanContainer.d.ts +0 -6
  221. package/dist/types/src/components/KanbanContainer.d.ts.map +0 -1
  222. package/dist/types/src/components/KanbanContainer.stories.d.ts +0 -75
  223. package/dist/types/src/components/KanbanContainer.stories.d.ts.map +0 -1
  224. package/dist/types/src/components/KanbanViewEditor.d.ts +0 -8
  225. package/dist/types/src/components/KanbanViewEditor.d.ts.map +0 -1
  226. package/src/capabilities/artifact-definition/index.ts +0 -7
  227. package/src/capabilities/blueprint-definition/blueprint-definition.ts +0 -23
  228. package/src/capabilities/blueprint-definition/index.ts +0 -7
  229. package/src/capabilities/operation-resolver/index.ts +0 -7
  230. package/src/capabilities/operation-resolver/operation-resolver.ts +0 -133
  231. package/src/capabilities/react-surface/index.ts +0 -7
  232. package/src/components/KanbanContainer.tsx +0 -86
@@ -0,0 +1,96 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { RegistryContext } from '@effect-atom/atom-react';
6
+ import React, { useCallback, useContext, useMemo } from 'react';
7
+
8
+ import { useCapabilities, useOperationInvoker } from '@dxos/app-framework/ui';
9
+ import { AppCapabilities } from '@dxos/app-toolkit';
10
+ import { type ObjectSurfaceProps } from '@dxos/app-toolkit/ui';
11
+ import { Filter, Obj, Query, Type } from '@dxos/echo';
12
+ import { AtomQuery } from '@dxos/echo-atom';
13
+ import { useObject, useSchema } from '@dxos/react-client/echo';
14
+ import { Panel, Toolbar } from '@dxos/react-ui';
15
+ import { getTagFromQuery, getTypenameFromQuery } from '@dxos/schema';
16
+
17
+ import { KanbanBoard } from '#components';
18
+ import { useEchoChangeCallback, useProjectionModel } from '#hooks';
19
+ import { type Kanban } from '#types';
20
+ import { KanbanOperation } from '#operations';
21
+
22
+ export type KanbanContainerProps = ObjectSurfaceProps<Kanban.Kanban>;
23
+
24
+ export const KanbanContainer = ({ role, subject: object }: KanbanContainerProps) => {
25
+ const registry = useContext(RegistryContext);
26
+ const schemas = useCapabilities(AppCapabilities.Schema);
27
+ const db = Obj.getDatabase(object);
28
+ const { invokePromise } = useOperationInvoker();
29
+ const [view] = useObject(object.view);
30
+ const typename = view?.query ? getTypenameFromQuery(view.query.ast) : undefined;
31
+ const tag = view?.query ? getTagFromQuery(view.query.ast) : undefined;
32
+
33
+ const schemaFromDb = useSchema(db, typename);
34
+ const cardSchema = useMemo(
35
+ () => schemaFromDb ?? schemas.flat().find((schema) => Type.getTypename(schema) === typename),
36
+ [schemaFromDb, schemas, typename],
37
+ );
38
+
39
+ const items = useMemo(() => {
40
+ if (!db) {
41
+ return null;
42
+ }
43
+ const baseFilter = cardSchema ? Filter.type(cardSchema) : Filter.nothing();
44
+ const query = tag ? Query.select(baseFilter).select(Filter.tag(tag)) : Query.select(baseFilter);
45
+ return AtomQuery.make(db, query);
46
+ }, [db, cardSchema, tag]);
47
+
48
+ const projection = useProjectionModel(cardSchema, object, registry);
49
+ const change = useEchoChangeCallback(object);
50
+
51
+ const pivotFieldId = view?.projection?.pivotFieldId;
52
+ const columnFieldPath =
53
+ projection && pivotFieldId ? projection.tryGetFieldProjection(pivotFieldId)?.props.property : undefined;
54
+
55
+ const handleCardAdd = useCallback(
56
+ (columnValue: string | undefined) => {
57
+ if (db && cardSchema && columnFieldPath) {
58
+ const card = Obj.make(cardSchema, { [columnFieldPath]: columnValue });
59
+ db.add(card);
60
+ return card.id;
61
+ }
62
+ },
63
+ [db, cardSchema, columnFieldPath],
64
+ );
65
+
66
+ const handleCardRemove = useCallback(
67
+ (card: { id: string }) => {
68
+ void invokePromise(KanbanOperation.DeleteCard, { card });
69
+ },
70
+ [invokePromise],
71
+ );
72
+
73
+ if (!object || !db || !items || !projection || !change) {
74
+ return null;
75
+ }
76
+
77
+ return (
78
+ <Panel.Root role={role}>
79
+ <Panel.Toolbar asChild>
80
+ <Toolbar.Root />
81
+ </Panel.Toolbar>
82
+ <KanbanBoard.Root
83
+ kanban={object}
84
+ projection={projection}
85
+ items={items}
86
+ change={change}
87
+ onCardAdd={handleCardAdd}
88
+ onCardRemove={handleCardRemove}
89
+ >
90
+ <Panel.Content asChild>
91
+ <KanbanBoard.Content />
92
+ </Panel.Content>
93
+ </KanbanBoard.Root>
94
+ </Panel.Root>
95
+ );
96
+ };
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { KanbanContainer } from './KanbanContainer';
6
+
7
+ export default KanbanContainer;
@@ -5,40 +5,44 @@
5
5
  import { RegistryContext } from '@effect-atom/atom-react';
6
6
  import React, { useCallback, useContext, useMemo } from 'react';
7
7
 
8
+ import { type ObjectSurfaceProps } from '@dxos/app-toolkit/ui';
8
9
  import { Obj } from '@dxos/echo';
9
10
  import { Format } from '@dxos/echo/internal';
10
- import { invariant } from '@dxos/invariant';
11
- import { useSchema } from '@dxos/react-client/echo';
11
+ import { useObject, useSchema } from '@dxos/react-client/echo';
12
12
  import { Form, type FormFieldMap, SelectField } from '@dxos/react-ui-form';
13
- import { useProjectionModel } from '@dxos/react-ui-kanban';
14
- import { type Kanban } from '@dxos/react-ui-kanban/types';
15
13
  import { getTypenameFromQuery } from '@dxos/schema';
16
14
 
17
- import { SettingsSchema } from '../types';
15
+ import { useProjectionModel } from '#hooks';
16
+ import { type Kanban, SettingsSchema } from '#types';
18
17
 
19
- type KanbanViewEditorProps = { object: Kanban.Kanban };
18
+ export type KanbanViewEditorProps = ObjectSurfaceProps<Kanban.Kanban>;
20
19
 
21
- export const KanbanViewEditor = ({ object }: KanbanViewEditorProps) => {
20
+ export const KanbanViewEditor = ({ subject: object }: KanbanViewEditorProps) => {
22
21
  const registry = useContext(RegistryContext);
23
22
  const db = Obj.getDatabase(object);
24
- const view = object.view.target;
23
+ const [view, updateView] = useObject(object.view);
25
24
  const currentTypename = view?.query ? getTypenameFromQuery(view.query.ast) : undefined;
26
25
  const schema = useSchema(db, currentTypename);
27
26
  const projection = useProjectionModel(schema, object, registry);
28
27
 
29
- const fieldProjections = projection?.getFieldProjections() || [];
30
- const selectFields = fieldProjections
31
- .filter((field) => field.props.format === Format.TypeFormat.SingleSelect)
32
- .map(({ field }) => ({ value: field.id, label: field.path }));
28
+ const fieldProjections = projection?.getFieldProjections() ?? [];
29
+ const selectFields = useMemo(
30
+ () =>
31
+ fieldProjections
32
+ .filter((field) => field.props.format === Format.TypeFormat.SingleSelect)
33
+ .map(({ field }) => ({ value: field.id, label: field.path })),
34
+ [fieldProjections],
35
+ );
33
36
 
34
- const handleSave = useCallback(
37
+ const handleValuesChanged = useCallback(
35
38
  (values: Partial<{ columnFieldId: string }>) => {
36
- invariant(view);
37
- Obj.change(view, (v) => {
38
- v.projection.pivotFieldId = values.columnFieldId;
39
- });
39
+ if (values.columnFieldId != null) {
40
+ updateView((view) => {
41
+ view.projection.pivotFieldId = values.columnFieldId;
42
+ });
43
+ }
40
44
  },
41
- [view],
45
+ [updateView],
42
46
  );
43
47
 
44
48
  const initialValues = useMemo(
@@ -52,7 +56,7 @@ export const KanbanViewEditor = ({ object }: KanbanViewEditorProps) => {
52
56
  );
53
57
 
54
58
  return (
55
- <Form.Root schema={SettingsSchema} values={initialValues} fieldMap={fieldMap} autoSave onSave={handleSave}>
59
+ <Form.Root schema={SettingsSchema} values={initialValues} fieldMap={fieldMap} onValuesChanged={handleValuesChanged}>
56
60
  <Form.FieldSet />
57
61
  </Form.Root>
58
62
  );
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { KanbanViewEditor } from './KanbanViewEditor';
6
+
7
+ export default KanbanViewEditor;
@@ -0,0 +1,8 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type ComponentType, lazy } from 'react';
6
+
7
+ export const KanbanContainer: ComponentType<any> = lazy(() => import('./KanbanContainer'));
8
+ export const KanbanViewEditor: ComponentType<any> = lazy(() => import('./KanbanViewEditor'));
@@ -0,0 +1,9 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './useEchoChangeCallback';
6
+ export * from './useKanbanBoardModel';
7
+ export * from './useKanbanColumnEventHandler';
8
+ export * from './useKanbanItemEventHandler';
9
+ export * from './useProjectionModel';
@@ -0,0 +1,30 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useMemo } from 'react';
6
+
7
+ import { Obj } from '@dxos/echo';
8
+
9
+ import { type Kanban, type KanbanChangeCallback } from '#types';
10
+
11
+ /**
12
+ * Creates a change callback for ECHO-backed kanban and items (plain function, no hooks).
13
+ * Use this when the kanban and items are stored in the ECHO database.
14
+ */
15
+ export const createEchoChangeCallback = <T extends Obj.Unknown>(kanban: Kanban.Kanban): KanbanChangeCallback<T> => ({
16
+ kanban: (mutate) => Obj.change(kanban, (kanban) => mutate(kanban)),
17
+ setItemField: (item, field, value) => {
18
+ Obj.change(item, (item: any) => {
19
+ item[field] = value;
20
+ });
21
+ },
22
+ });
23
+
24
+ /**
25
+ * Returns a memoized ECHO change callback for the given kanban.
26
+ * Returns null when kanban is undefined.
27
+ */
28
+ export const useEchoChangeCallback = <T extends Obj.Unknown = Obj.Unknown>(
29
+ kanban: Kanban.Kanban | undefined,
30
+ ): KanbanChangeCallback<T> | null => useMemo(() => (kanban ? createEchoChangeCallback<T>(kanban) : null), [kanban]);
@@ -0,0 +1,235 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Atom, Registry } from '@effect-atom/atom-react';
6
+ import { act, renderHook } from '@testing-library/react';
7
+ import * as Schema from 'effect/Schema';
8
+ import { beforeEach, describe, test } from 'vitest';
9
+
10
+ import { Filter, JsonSchema, Obj, Query, Type } from '@dxos/echo';
11
+ import { type View } from '@dxos/echo';
12
+ import { Format, FormatAnnotation, PropertyMetaAnnotationId } from '@dxos/echo/internal';
13
+ import { ObjectId } from '@dxos/keys';
14
+ import { ProjectionModel, ViewModel, createDirectChangeCallback } from '@dxos/schema';
15
+
16
+ import { Kanban } from '#types';
17
+
18
+ import { useKanbanBoardModel } from './useKanbanBoardModel';
19
+
20
+ // TODO(wittjosiah): Consider adding single-select to TestSchema.Task and using that instead.
21
+ const KanbanTaskSchema = Schema.Struct({
22
+ title: Schema.optional(Schema.String),
23
+ status: Schema.Literal('__uncategorized__', 'a', 'b').pipe(
24
+ FormatAnnotation.set(Format.TypeFormat.SingleSelect),
25
+ Schema.annotations({
26
+ title: 'Status',
27
+ [PropertyMetaAnnotationId]: {
28
+ singleSelect: {
29
+ options: [
30
+ { id: '__uncategorized__', title: 'Uncategorized', color: 'neutral' },
31
+ { id: 'a', title: 'A', color: 'blue' },
32
+ { id: 'b', title: 'B', color: 'green' },
33
+ ],
34
+ },
35
+ },
36
+ }),
37
+ Schema.optional,
38
+ ),
39
+ }).pipe(
40
+ Type.object({
41
+ typename: 'com.example.type.kanban-task',
42
+ version: '0.1.0',
43
+ }),
44
+ );
45
+
46
+ type KanbanTask = Schema.Schema.Type<typeof KanbanTaskSchema>;
47
+
48
+ describe('useKanbanBoardModel', () => {
49
+ let registry: Registry.Registry;
50
+ let view: View.View;
51
+ let kanban: Kanban.Kanban;
52
+ let projection: ProjectionModel;
53
+
54
+ beforeEach(() => {
55
+ registry = Registry.make();
56
+ const jsonSchema = JsonSchema.toJsonSchema(KanbanTaskSchema) as Parameters<typeof createDirectChangeCallback>[1];
57
+ view = ViewModel.make({
58
+ query: Query.select(Filter.type(KanbanTaskSchema)),
59
+ jsonSchema,
60
+ pivotFieldName: 'status',
61
+ });
62
+ kanban = Kanban.make({
63
+ view: view,
64
+ arrangement: {
65
+ order: ['a', 'b'],
66
+ columns: { a: { ids: [] }, b: { ids: [] } },
67
+ },
68
+ });
69
+ projection = new ProjectionModel({
70
+ registry,
71
+ view,
72
+ baseSchema: jsonSchema,
73
+ change: createDirectChangeCallback(view.projection, jsonSchema),
74
+ });
75
+ });
76
+
77
+ test('returns model with getColumns and getItems', ({ expect }) => {
78
+ const itemA = Obj.make(KanbanTaskSchema, { status: 'a' });
79
+ const itemB = Obj.make(KanbanTaskSchema, { status: 'b' });
80
+ const initialItems: KanbanTask[] = [itemA, itemB];
81
+ const itemsAtom = Atom.make<KanbanTask[]>(() => initialItems);
82
+
83
+ const { result } = renderHook(() => useKanbanBoardModel(kanban, projection, itemsAtom, registry));
84
+
85
+ const model = result.current;
86
+ expect(model.getColumnId).toBeDefined();
87
+ expect(model.getItemId).toBeDefined();
88
+ expect(model.isColumn).toBeDefined();
89
+ expect(model.isItem).toBeDefined();
90
+
91
+ const columns = model.getColumns();
92
+ expect(columns.length).toBeGreaterThanOrEqual(1);
93
+ const columnValues = columns.map((col) => col.columnValue);
94
+ expect(columnValues).toContain('a');
95
+ expect(columnValues).toContain('b');
96
+
97
+ const colA = columns.find((c) => c.columnValue === 'a');
98
+ const colB = columns.find((c) => c.columnValue === 'b');
99
+ expect(colA).toBeDefined();
100
+ expect(colB).toBeDefined();
101
+
102
+ const itemsA = model.getItems(colA!);
103
+ const itemsB = model.getItems(colB!);
104
+ expect(itemsA.map((i) => i.id)).toContain(itemA.id);
105
+ expect(itemsB.map((i) => i.id)).toContain(itemB.id);
106
+ });
107
+
108
+ test('getItems updates when itemsAtom source changes', ({ expect }) => {
109
+ const initialItem = Obj.make(KanbanTaskSchema, { status: 'a' });
110
+ const initialItems: KanbanTask[] = [initialItem];
111
+ const initialItemsAtom = Atom.make<KanbanTask[]>(() => initialItems);
112
+
113
+ const { result, rerender } = renderHook(
114
+ ({ itemsAtom }) => useKanbanBoardModel(kanban, projection, itemsAtom, registry),
115
+ { initialProps: { itemsAtom: initialItemsAtom } },
116
+ );
117
+
118
+ const columns = result.current.getColumns();
119
+ const colA = columns.find((c) => c.columnValue === 'a');
120
+ expect(colA).toBeDefined();
121
+ expect(result.current.getItems(colA!).length).toBe(1);
122
+ expect(result.current.getItems(colA!).map((i) => i.id)).toEqual([initialItem.id]);
123
+
124
+ const secondItem = Obj.make(KanbanTaskSchema, { status: 'a' });
125
+ const newItems: KanbanTask[] = [initialItem, secondItem];
126
+ const newItemsAtom = Atom.make<KanbanTask[]>(() => newItems);
127
+ act(() => {
128
+ rerender({ itemsAtom: newItemsAtom });
129
+ });
130
+
131
+ expect(result.current.getItems(colA!).length).toBe(2);
132
+ expect(result.current.getItems(colA!).map((i) => i.id)).toEqual([initialItem.id, secondItem.id]);
133
+ });
134
+
135
+ test('columns atom updates when kanban arrangement changes', ({ expect }) => {
136
+ const itemsAtom = Atom.make<KanbanTask[]>(() => []);
137
+ const { result } = renderHook(() => useKanbanBoardModel(kanban, projection, itemsAtom, registry));
138
+
139
+ let columnsUpdateCount = 0;
140
+ registry.subscribe(result.current.columns, () => {
141
+ columnsUpdateCount++;
142
+ });
143
+
144
+ const columnsBefore = result.current.getColumns();
145
+ const orderBefore = columnsBefore.map((c) => c.columnValue);
146
+ expect(orderBefore).toEqual(['__uncategorized__', 'a', 'b']);
147
+
148
+ act(() => {
149
+ Obj.change(kanban, (kanban) => {
150
+ kanban.arrangement.order = ['b', 'a'];
151
+ });
152
+ });
153
+
154
+ const columnsAfter = registry.get(result.current.columns) ?? [];
155
+ const orderAfter = columnsAfter.map((c) => c.columnValue);
156
+ expect(orderAfter).toEqual(['__uncategorized__', 'b', 'a']);
157
+ // TODO(wittjosiah): Try to reduce to 1.
158
+ expect(columnsUpdateCount).toBe(2);
159
+ });
160
+
161
+ test('getItems returns items in column ordered by arrangement ids', ({ expect }) => {
162
+ const item1 = Obj.make(KanbanTaskSchema, {
163
+ id: ObjectId.random(),
164
+ status: 'a',
165
+ });
166
+ const item2 = Obj.make(KanbanTaskSchema, {
167
+ id: ObjectId.random(),
168
+ status: 'a',
169
+ });
170
+ const item3 = Obj.make(KanbanTaskSchema, {
171
+ id: ObjectId.random(),
172
+ status: 'a',
173
+ });
174
+ const items: KanbanTask[] = [item1, item2, item3];
175
+ const itemsAtom = Atom.make<KanbanTask[]>(() => items);
176
+
177
+ const { result } = renderHook(() => useKanbanBoardModel(kanban, projection, itemsAtom, registry));
178
+
179
+ const columns = result.current.getColumns();
180
+ const colA = columns.find((c) => c.columnValue === 'a');
181
+ expect(colA).toBeDefined();
182
+ expect(result.current.getItems(colA!).length).toBe(3);
183
+
184
+ let itemsColAUpdateCount = 0;
185
+ registry.subscribe(result.current.items(colA!), () => {
186
+ itemsColAUpdateCount++;
187
+ });
188
+
189
+ act(() => {
190
+ Obj.change(kanban, (kanban) => {
191
+ kanban.arrangement.columns['a'] = {
192
+ ids: [item3.id, item1.id, item2.id],
193
+ };
194
+ });
195
+ });
196
+
197
+ const itemsAfter = result.current.getItems(colA!);
198
+ expect(itemsAfter.map((i) => i.id)).toEqual([item3.id, item1.id, item2.id]);
199
+ expect(itemsColAUpdateCount).toBe(1);
200
+ });
201
+
202
+ test('subscribing to one column items atom does not fire when another column changes', ({ expect }) => {
203
+ const itemA = Obj.make(KanbanTaskSchema, {
204
+ id: ObjectId.random(),
205
+ status: 'a',
206
+ });
207
+ const itemB = Obj.make(KanbanTaskSchema, {
208
+ id: ObjectId.random(),
209
+ status: 'b',
210
+ });
211
+ const items: KanbanTask[] = [itemA, itemB];
212
+ const itemsAtom = Atom.make<KanbanTask[]>(() => items);
213
+
214
+ const { result } = renderHook(() => useKanbanBoardModel(kanban, projection, itemsAtom, registry));
215
+
216
+ const columns = result.current.getColumns();
217
+ const colA = columns.find((c) => c.columnValue === 'a');
218
+ const colB = columns.find((c) => c.columnValue === 'b');
219
+ expect(colA).toBeDefined();
220
+ expect(colB).toBeDefined();
221
+
222
+ let itemsColAUpdateCount = 0;
223
+ registry.subscribe(result.current.items(colA!), () => {
224
+ itemsColAUpdateCount++;
225
+ });
226
+
227
+ act(() => {
228
+ Obj.change(kanban, (kanban) => {
229
+ kanban.arrangement.columns['b'] = { ids: [itemB.id] };
230
+ });
231
+ });
232
+
233
+ expect(itemsColAUpdateCount).toBe(0);
234
+ });
235
+ });
@@ -0,0 +1,143 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Atom, type Registry } from '@effect-atom/atom-react';
6
+ import { useMemo } from 'react';
7
+
8
+ import { Obj } from '@dxos/echo';
9
+ import { AtomObj } from '@dxos/echo-atom';
10
+ import type { BoardModel } from '@dxos/react-ui-mosaic';
11
+ import type { ProjectionModel } from '@dxos/schema';
12
+
13
+ import { type BaseKanbanItem, type ColumnStructure, type Kanban } from '#types';
14
+ import {
15
+ computeColumnStructure,
16
+ getOrderByColumnFromArrangement,
17
+ getOrderFromArrangement,
18
+ orderItemsInColumn,
19
+ } from '../util';
20
+
21
+ /**
22
+ * Builds a board model that maps kanban arrangement and projection onto columns and per-column items.
23
+ *
24
+ * @template T - Item type (must have id; defaults to BaseKanbanItem).
25
+ * @param kanban - Kanban object (arrangement, view).
26
+ * @param projection - ProjectionModel for pivot field and options.
27
+ * @param itemsAtom - Atom holding the full item list.
28
+ * @param registry - Registry for reading atom values.
29
+ * @returns BoardModel with columns atom, items family, and getColumns/getItems.
30
+ */
31
+ export function useKanbanBoardModel<T extends BaseKanbanItem = BaseKanbanItem>(
32
+ kanban: Kanban.Kanban,
33
+ projection: ProjectionModel,
34
+ itemsAtom: Atom.Atom<T[]>,
35
+ registry: Registry.Registry,
36
+ ): BoardModel<ColumnStructure, T> {
37
+ // Source atoms: reactive reads from the kanban object; items come from the passed-in atom (e.g. AtomQuery or in-memory).
38
+ const arrangementAtom = useMemo(() => AtomObj.makeProperty(kanban, 'arrangement'), [kanban]);
39
+ const viewSnapshotAtom = useMemo(
40
+ () => (kanban?.view ? AtomObj.make(kanban.view) : Atom.make<undefined>(() => undefined)),
41
+ [kanban?.view],
42
+ );
43
+
44
+ /** Only changes when view.projection.pivotFieldId changes; keeps columns from firing on other view updates. */
45
+ const pivotFieldIdAtom = useMemo(
46
+ () => Atom.make((get) => get(viewSnapshotAtom)?.projection?.pivotFieldId as string | undefined),
47
+ [viewSnapshotAtom],
48
+ );
49
+
50
+ // Effective per-column ids: from kanban.arrangement.columns; empty when arrangement has no columns.
51
+ const effectiveByColumnAtom = useMemo(
52
+ () => Atom.make((get) => getOrderByColumnFromArrangement(get(arrangementAtom))),
53
+ [arrangementAtom],
54
+ );
55
+
56
+ // Column structure: depends on pivotFieldId (not full view), arrangement, and projection so columns only fire when pivot or arrangement changes.
57
+ const columnsAtom = useMemo(
58
+ () =>
59
+ Atom.make((get) => {
60
+ const pivotFieldId = get(pivotFieldIdAtom);
61
+ if (pivotFieldId === undefined) {
62
+ return [];
63
+ }
64
+
65
+ get(projection.fields);
66
+ const fieldProj = projection.tryGetFieldProjection(pivotFieldId);
67
+ if (!fieldProj) {
68
+ return [];
69
+ }
70
+
71
+ const selectOptions = fieldProj.props.options ?? [];
72
+ if (selectOptions.length === 0) {
73
+ return [];
74
+ }
75
+
76
+ const arrangement = get(arrangementAtom);
77
+ const order = getOrderFromArrangement(arrangement);
78
+ const byColumn = getOrderByColumnFromArrangement(arrangement);
79
+ return computeColumnStructure(order, byColumn, selectOptions);
80
+ }),
81
+ [pivotFieldIdAtom, arrangementAtom, projection],
82
+ );
83
+
84
+ // Per-column slice of arrangement so each column’s items atom only depends on that column’s ids.
85
+ const columnArrangementAtomFamily = useMemo(
86
+ () =>
87
+ Atom.family<string, Atom.Atom<ColumnStructure>>((columnValue: string) =>
88
+ Atom.make((get) => {
89
+ const byColumn = get(effectiveByColumnAtom);
90
+ return {
91
+ columnValue,
92
+ ids: [...(byColumn[columnValue]?.ids ?? [])],
93
+ };
94
+ }),
95
+ ),
96
+ [effectiveByColumnAtom],
97
+ );
98
+
99
+ // Items for a single column: filter all items by pivot field, sort by this column’s ids, then append new items.
100
+ const itemsAtomFamily = useMemo(
101
+ () =>
102
+ Atom.family<string, Atom.Atom<T[]>>((columnValue: string) =>
103
+ Atom.make((get) => {
104
+ const columnArr = get(columnArrangementAtomFamily(columnValue));
105
+ const allItems = get(itemsAtom);
106
+ const pivotFieldId = get(pivotFieldIdAtom);
107
+
108
+ if (pivotFieldId === undefined) {
109
+ return [];
110
+ }
111
+
112
+ // TODO(wittjosiah): Try to narrow this down further.
113
+ get(projection.fields);
114
+ const fieldProj = projection.tryGetFieldProjection(pivotFieldId);
115
+ if (!fieldProj) {
116
+ return [];
117
+ }
118
+
119
+ const selectOptions = fieldProj.props.options ?? [];
120
+ const pivotPath = fieldProj.props.property;
121
+ const validColumnValues = new Set(selectOptions.map((opt) => opt.id));
122
+ return orderItemsInColumn(allItems, columnArr.ids, columnValue, pivotPath, validColumnValues);
123
+ }),
124
+ ),
125
+ [columnArrangementAtomFamily, itemsAtom, pivotFieldIdAtom, projection],
126
+ );
127
+
128
+ return useMemo(
129
+ () => ({
130
+ getColumnId: (data) => data.columnValue,
131
+ getItemId: (data) => (data as T).id,
132
+ isColumn: (obj): obj is ColumnStructure =>
133
+ typeof obj === 'object' && obj !== null && 'columnValue' in obj && 'ids' in obj,
134
+ // TODO(wittjosiah): This should be restricted to objects of the type of the kanban view.
135
+ isItem: (obj): obj is T => Obj.isObject(obj),
136
+ columns: columnsAtom,
137
+ items: (column) => itemsAtomFamily(column.columnValue),
138
+ getColumns: () => registry.get(columnsAtom) ?? [],
139
+ getItems: (column) => registry.get(itemsAtomFamily(column.columnValue)) ?? [],
140
+ }),
141
+ [columnsAtom, itemsAtomFamily, registry],
142
+ );
143
+ }