@dxos/plugin-kanban 0.8.4-main.dedc0f3 → 0.8.4-main.dfabb4ec29

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 (275) hide show
  1. package/dist/lib/neutral/KanbanContainer-QK6LNCYT.mjs +132 -0
  2. package/dist/lib/neutral/KanbanContainer-QK6LNCYT.mjs.map +7 -0
  3. package/dist/lib/neutral/KanbanPlugin.mjs +36 -0
  4. package/dist/lib/neutral/KanbanPlugin.mjs.map +7 -0
  5. package/dist/lib/neutral/KanbanPlugin.node.mjs +27 -0
  6. package/dist/lib/neutral/KanbanPlugin.node.mjs.map +7 -0
  7. package/dist/lib/neutral/KanbanSettings-G6M47NSK.mjs +83 -0
  8. package/dist/lib/neutral/KanbanSettings-G6M47NSK.mjs.map +7 -0
  9. package/dist/lib/neutral/blueprint-definition-FHVIEKTQ.mjs +15 -0
  10. package/dist/lib/neutral/blueprint-definition-FHVIEKTQ.mjs.map +7 -0
  11. package/dist/lib/neutral/blueprints/index.mjs +8 -0
  12. package/dist/lib/neutral/capabilities/index.mjs +19 -0
  13. package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
  14. package/dist/lib/neutral/chunk-E65AME5F.mjs +255 -0
  15. package/dist/lib/neutral/chunk-E65AME5F.mjs.map +7 -0
  16. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  17. package/dist/lib/neutral/chunk-KDPM77BD.mjs +21 -0
  18. package/dist/lib/neutral/chunk-KDPM77BD.mjs.map +7 -0
  19. package/dist/lib/neutral/chunk-Z7O5CETK.mjs +8 -0
  20. package/dist/lib/neutral/chunk-Z7O5CETK.mjs.map +7 -0
  21. package/dist/lib/neutral/chunk-ZTQW5KQS.mjs +26 -0
  22. package/dist/lib/neutral/chunk-ZTQW5KQS.mjs.map +7 -0
  23. package/dist/lib/neutral/components/index.mjs +243 -0
  24. package/dist/lib/neutral/components/index.mjs.map +7 -0
  25. package/dist/lib/neutral/containers/index.mjs +11 -0
  26. package/dist/lib/neutral/containers/index.mjs.map +7 -0
  27. package/dist/lib/neutral/create-object-DKBSI46K.mjs +40 -0
  28. package/dist/lib/neutral/create-object-DKBSI46K.mjs.map +7 -0
  29. package/dist/lib/neutral/delete-card-QKT6OXGP.mjs +24 -0
  30. package/dist/lib/neutral/delete-card-QKT6OXGP.mjs.map +7 -0
  31. package/dist/lib/neutral/delete-card-field-XQKM7ZXE.mjs +42 -0
  32. package/dist/lib/neutral/delete-card-field-XQKM7ZXE.mjs.map +7 -0
  33. package/dist/lib/neutral/hooks/index.mjs +432 -0
  34. package/dist/lib/neutral/hooks/index.mjs.map +7 -0
  35. package/dist/lib/neutral/index.mjs +34 -0
  36. package/dist/lib/neutral/index.mjs.map +7 -0
  37. package/dist/lib/neutral/meta.json +1 -0
  38. package/dist/lib/neutral/meta.mjs +8 -0
  39. package/dist/lib/neutral/meta.mjs.map +7 -0
  40. package/dist/lib/neutral/migrations-4NS6H7U2.mjs +31 -0
  41. package/dist/lib/neutral/migrations-4NS6H7U2.mjs.map +7 -0
  42. package/dist/lib/neutral/operation-handler-B7IW6MXU.mjs +13 -0
  43. package/dist/lib/neutral/operation-handler-B7IW6MXU.mjs.map +7 -0
  44. package/dist/lib/neutral/operations/index.mjs +8 -0
  45. package/dist/lib/neutral/operations/index.mjs.map +7 -0
  46. package/dist/lib/neutral/plugin.mjs +16 -0
  47. package/dist/lib/neutral/plugin.mjs.map +7 -0
  48. package/dist/lib/neutral/react-surface-6RVSCHMJ.mjs +93 -0
  49. package/dist/lib/neutral/react-surface-6RVSCHMJ.mjs.map +7 -0
  50. package/dist/lib/neutral/restore-card-XW7AHMPO.mjs +21 -0
  51. package/dist/lib/neutral/restore-card-XW7AHMPO.mjs.map +7 -0
  52. package/dist/lib/neutral/restore-card-field-QOAUY3RJ.mjs +40 -0
  53. package/dist/lib/neutral/restore-card-field-QOAUY3RJ.mjs.map +7 -0
  54. package/dist/lib/neutral/testing/index.mjs +60 -0
  55. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  56. package/dist/lib/neutral/translations.mjs +44 -0
  57. package/dist/lib/neutral/translations.mjs.map +7 -0
  58. package/dist/lib/neutral/types/index.mjs +22 -0
  59. package/dist/lib/neutral/types/index.mjs.map +7 -0
  60. package/dist/lib/neutral/undo-mappings-6CHW6BOF.mjs +42 -0
  61. package/dist/lib/neutral/undo-mappings-6CHW6BOF.mjs.map +7 -0
  62. package/dist/types/src/KanbanPlugin.d.ts +3 -1
  63. package/dist/types/src/KanbanPlugin.d.ts.map +1 -1
  64. package/dist/types/src/KanbanPlugin.node.d.ts +4 -0
  65. package/dist/types/src/KanbanPlugin.node.d.ts.map +1 -0
  66. package/dist/types/src/KanbanPlugin.test.d.ts +2 -0
  67. package/dist/types/src/KanbanPlugin.test.d.ts.map +1 -0
  68. package/dist/types/src/blueprints/index.d.ts +2 -0
  69. package/dist/types/src/blueprints/index.d.ts.map +1 -0
  70. package/dist/types/src/blueprints/kanban-blueprint.d.ts +4 -0
  71. package/dist/types/src/blueprints/kanban-blueprint.d.ts.map +1 -0
  72. package/dist/types/src/capabilities/artifact-definition.d.ts +3 -2
  73. package/dist/types/src/capabilities/artifact-definition.d.ts.map +1 -1
  74. package/dist/types/src/capabilities/blueprint-definition.d.ts +5 -4
  75. package/dist/types/src/capabilities/blueprint-definition.d.ts.map +1 -1
  76. package/dist/types/src/capabilities/create-object.d.ts +11 -0
  77. package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
  78. package/dist/types/src/capabilities/index.d.ts +13 -3
  79. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  80. package/dist/types/src/capabilities/migrations.d.ts +5 -0
  81. package/dist/types/src/capabilities/migrations.d.ts.map +1 -0
  82. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  83. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  84. package/dist/types/src/capabilities/react-surface.d.ts +3 -2
  85. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  86. package/dist/types/src/capabilities/undo-mappings.d.ts +5 -0
  87. package/dist/types/src/capabilities/undo-mappings.d.ts.map +1 -0
  88. package/dist/types/src/components/KanbanBoard/KanbanBoard.d.ts +37 -0
  89. package/dist/types/src/components/KanbanBoard/KanbanBoard.d.ts.map +1 -0
  90. package/dist/types/src/components/KanbanBoard/KanbanBoard.stories.d.ts +72 -0
  91. package/dist/types/src/components/KanbanBoard/KanbanBoard.stories.d.ts.map +1 -0
  92. package/dist/types/src/components/KanbanBoard/KanbanCard.d.ts +9 -0
  93. package/dist/types/src/components/KanbanBoard/KanbanCard.d.ts.map +1 -0
  94. package/dist/types/src/components/KanbanBoard/KanbanColumn.d.ts +8 -0
  95. package/dist/types/src/components/KanbanBoard/KanbanColumn.d.ts.map +1 -0
  96. package/dist/types/src/components/KanbanBoard/context.d.ts +38 -0
  97. package/dist/types/src/components/KanbanBoard/context.d.ts.map +1 -0
  98. package/dist/types/src/components/KanbanBoard/index.d.ts +2 -0
  99. package/dist/types/src/components/KanbanBoard/index.d.ts.map +1 -0
  100. package/dist/types/src/components/index.d.ts +1 -2
  101. package/dist/types/src/components/index.d.ts.map +1 -1
  102. package/dist/types/src/containers/KanbanContainer/KanbanContainer.d.ts +6 -0
  103. package/dist/types/src/containers/KanbanContainer/KanbanContainer.d.ts.map +1 -0
  104. package/dist/types/src/containers/KanbanContainer/KanbanContainer.stories.d.ts +79 -0
  105. package/dist/types/src/containers/KanbanContainer/KanbanContainer.stories.d.ts.map +1 -0
  106. package/dist/types/src/containers/KanbanContainer/index.d.ts +2 -0
  107. package/dist/types/src/containers/KanbanContainer/index.d.ts.map +1 -0
  108. package/dist/types/src/containers/KanbanSettings/KanbanSettings.d.ts +13 -0
  109. package/dist/types/src/containers/KanbanSettings/KanbanSettings.d.ts.map +1 -0
  110. package/dist/types/src/containers/KanbanSettings/index.d.ts +2 -0
  111. package/dist/types/src/containers/KanbanSettings/index.d.ts.map +1 -0
  112. package/dist/types/src/containers/index.d.ts +4 -0
  113. package/dist/types/src/containers/index.d.ts.map +1 -0
  114. package/dist/types/src/hooks/index.d.ts +7 -0
  115. package/dist/types/src/hooks/index.d.ts.map +1 -0
  116. package/dist/types/src/hooks/useEchoChangeCallback.d.ts +13 -0
  117. package/dist/types/src/hooks/useEchoChangeCallback.d.ts.map +1 -0
  118. package/dist/types/src/hooks/useItemsProjection.d.ts +10 -0
  119. package/dist/types/src/hooks/useItemsProjection.d.ts.map +1 -0
  120. package/dist/types/src/hooks/useKanbanBoardModel.browser.test.d.ts +2 -0
  121. package/dist/types/src/hooks/useKanbanBoardModel.browser.test.d.ts.map +1 -0
  122. package/dist/types/src/hooks/useKanbanBoardModel.d.ts +16 -0
  123. package/dist/types/src/hooks/useKanbanBoardModel.d.ts.map +1 -0
  124. package/dist/types/src/hooks/useKanbanColumnEventHandler.d.ts +22 -0
  125. package/dist/types/src/hooks/useKanbanColumnEventHandler.d.ts.map +1 -0
  126. package/dist/types/src/hooks/useKanbanItemEventHandler.d.ts +19 -0
  127. package/dist/types/src/hooks/useKanbanItemEventHandler.d.ts.map +1 -0
  128. package/dist/types/src/hooks/useProjectionModel.d.ts +15 -0
  129. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  130. package/dist/types/src/index.d.ts +3 -1
  131. package/dist/types/src/index.d.ts.map +1 -1
  132. package/dist/types/src/meta.d.ts +2 -3
  133. package/dist/types/src/meta.d.ts.map +1 -1
  134. package/dist/types/src/operations/delete-card-field.d.ts +5 -0
  135. package/dist/types/src/operations/delete-card-field.d.ts.map +1 -0
  136. package/dist/types/src/operations/delete-card.d.ts +5 -0
  137. package/dist/types/src/operations/delete-card.d.ts.map +1 -0
  138. package/dist/types/src/operations/index.d.ts +3 -0
  139. package/dist/types/src/operations/index.d.ts.map +1 -0
  140. package/dist/types/src/operations/restore-card-field.d.ts +5 -0
  141. package/dist/types/src/operations/restore-card-field.d.ts.map +1 -0
  142. package/dist/types/src/operations/restore-card.d.ts +5 -0
  143. package/dist/types/src/operations/restore-card.d.ts.map +1 -0
  144. package/dist/types/src/playwright/board-manager.d.ts +5 -0
  145. package/dist/types/src/playwright/board-manager.d.ts.map +1 -0
  146. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  147. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  148. package/dist/types/src/playwright/smoke.spec.d.ts +2 -0
  149. package/dist/types/src/playwright/smoke.spec.d.ts.map +1 -0
  150. package/dist/types/src/plugin.d.ts +4 -0
  151. package/dist/types/src/plugin.d.ts.map +1 -0
  152. package/dist/types/src/testing/KanbanCardTileSimple.d.ts +7 -0
  153. package/dist/types/src/testing/KanbanCardTileSimple.d.ts.map +1 -0
  154. package/dist/types/src/testing/index.d.ts +2 -0
  155. package/dist/types/src/testing/index.d.ts.map +1 -0
  156. package/dist/types/src/translations.d.ts +50 -22
  157. package/dist/types/src/translations.d.ts.map +1 -1
  158. package/dist/types/src/types/Kanban.d.ts +109 -0
  159. package/dist/types/src/types/Kanban.d.ts.map +1 -0
  160. package/dist/types/src/types/KanbanOperation.d.ts +52 -0
  161. package/dist/types/src/types/KanbanOperation.d.ts.map +1 -0
  162. package/dist/types/src/types/constants.d.ts +6 -0
  163. package/dist/types/src/types/constants.d.ts.map +1 -0
  164. package/dist/types/src/types/index.d.ts +3 -0
  165. package/dist/types/src/types/index.d.ts.map +1 -1
  166. package/dist/types/src/types/migrations.test.d.ts +2 -0
  167. package/dist/types/src/types/migrations.test.d.ts.map +1 -0
  168. package/dist/types/src/types/schema.d.ts +18 -52
  169. package/dist/types/src/types/schema.d.ts.map +1 -1
  170. package/dist/types/src/types/types.d.ts +28 -0
  171. package/dist/types/src/types/types.d.ts.map +1 -1
  172. package/dist/types/src/util/arrangement.d.ts +72 -0
  173. package/dist/types/src/util/arrangement.d.ts.map +1 -0
  174. package/dist/types/src/util/arrangement.test.d.ts +2 -0
  175. package/dist/types/src/util/arrangement.test.d.ts.map +1 -0
  176. package/dist/types/src/util/index.d.ts +2 -0
  177. package/dist/types/src/util/index.d.ts.map +1 -0
  178. package/dist/types/tsconfig.tsbuildinfo +1 -1
  179. package/package.json +120 -57
  180. package/src/KanbanPlugin.node.ts +21 -0
  181. package/src/KanbanPlugin.test.ts +31 -0
  182. package/src/KanbanPlugin.tsx +29 -57
  183. package/src/blueprints/index.ts +5 -0
  184. package/src/blueprints/kanban-blueprint.ts +27 -0
  185. package/src/capabilities/artifact-definition.ts +117 -113
  186. package/src/capabilities/blueprint-definition.ts +13 -24
  187. package/src/capabilities/create-object.ts +40 -0
  188. package/src/capabilities/index.ts +12 -4
  189. package/src/capabilities/migrations.ts +35 -0
  190. package/src/capabilities/operation-handler.ts +14 -0
  191. package/src/capabilities/react-surface.tsx +79 -68
  192. package/src/capabilities/undo-mappings.ts +34 -0
  193. package/src/components/KanbanBoard/KanbanBoard.stories.tsx +146 -0
  194. package/src/components/KanbanBoard/KanbanBoard.tsx +164 -0
  195. package/src/components/KanbanBoard/KanbanCard.tsx +101 -0
  196. package/src/components/KanbanBoard/KanbanColumn.tsx +72 -0
  197. package/src/components/KanbanBoard/context.ts +54 -0
  198. package/src/components/KanbanBoard/index.ts +5 -0
  199. package/src/components/index.ts +1 -2
  200. package/src/containers/KanbanContainer/KanbanContainer.stories.tsx +277 -0
  201. package/src/containers/KanbanContainer/KanbanContainer.tsx +178 -0
  202. package/src/containers/KanbanContainer/index.ts +5 -0
  203. package/src/containers/KanbanSettings/KanbanSettings.tsx +94 -0
  204. package/src/containers/KanbanSettings/index.ts +5 -0
  205. package/src/containers/index.ts +8 -0
  206. package/src/hooks/index.ts +10 -0
  207. package/src/hooks/useEchoChangeCallback.ts +30 -0
  208. package/src/hooks/useItemsProjection.ts +44 -0
  209. package/src/hooks/useKanbanBoardModel.browser.test.ts +235 -0
  210. package/src/hooks/useKanbanBoardModel.ts +157 -0
  211. package/src/hooks/useKanbanColumnEventHandler.ts +106 -0
  212. package/src/hooks/useKanbanItemEventHandler.ts +133 -0
  213. package/src/hooks/useProjectionModel.ts +58 -0
  214. package/src/index.ts +3 -2
  215. package/src/meta.ts +9 -7
  216. package/src/operations/delete-card-field.ts +47 -0
  217. package/src/operations/delete-card.ts +23 -0
  218. package/src/operations/index.ts +10 -0
  219. package/src/operations/restore-card-field.ts +41 -0
  220. package/src/operations/restore-card.ts +21 -0
  221. package/src/playwright/board-manager.ts +13 -0
  222. package/src/playwright/playwright.config.ts +19 -0
  223. package/src/playwright/smoke.spec.ts +107 -0
  224. package/src/plugin.ts +11 -0
  225. package/src/testing/KanbanCardTileSimple.tsx +82 -0
  226. package/src/testing/index.ts +5 -0
  227. package/src/translations.ts +28 -20
  228. package/src/types/Kanban.ts +151 -0
  229. package/src/types/KanbanOperation.ts +67 -0
  230. package/src/types/constants.ts +9 -0
  231. package/src/types/index.ts +4 -0
  232. package/src/types/migrations.test.ts +83 -0
  233. package/src/types/schema.ts +33 -45
  234. package/src/types/types.ts +35 -0
  235. package/src/util/arrangement.test.ts +218 -0
  236. package/src/util/arrangement.ts +177 -0
  237. package/src/util/index.ts +5 -0
  238. package/dist/lib/browser/blueprint-definition-GFG7LX2C.mjs +0 -28
  239. package/dist/lib/browser/blueprint-definition-GFG7LX2C.mjs.map +0 -7
  240. package/dist/lib/browser/chunk-NCNNL74W.mjs +0 -82
  241. package/dist/lib/browser/chunk-NCNNL74W.mjs.map +0 -7
  242. package/dist/lib/browser/index.mjs +0 -106
  243. package/dist/lib/browser/index.mjs.map +0 -7
  244. package/dist/lib/browser/intent-resolver-KHOAFPNN.mjs +0 -111
  245. package/dist/lib/browser/intent-resolver-KHOAFPNN.mjs.map +0 -7
  246. package/dist/lib/browser/meta.json +0 -1
  247. package/dist/lib/browser/react-surface-UBRYXDKS.mjs +0 -253
  248. package/dist/lib/browser/react-surface-UBRYXDKS.mjs.map +0 -7
  249. package/dist/lib/browser/types/index.mjs +0 -11
  250. package/dist/lib/node-esm/blueprint-definition-MIMDXMUM.mjs +0 -30
  251. package/dist/lib/node-esm/blueprint-definition-MIMDXMUM.mjs.map +0 -7
  252. package/dist/lib/node-esm/chunk-5B3LKGA7.mjs +0 -84
  253. package/dist/lib/node-esm/chunk-5B3LKGA7.mjs.map +0 -7
  254. package/dist/lib/node-esm/index.mjs +0 -107
  255. package/dist/lib/node-esm/index.mjs.map +0 -7
  256. package/dist/lib/node-esm/intent-resolver-MKO5HVES.mjs +0 -112
  257. package/dist/lib/node-esm/intent-resolver-MKO5HVES.mjs.map +0 -7
  258. package/dist/lib/node-esm/meta.json +0 -1
  259. package/dist/lib/node-esm/react-surface-B2FPS52G.mjs +0 -254
  260. package/dist/lib/node-esm/react-surface-B2FPS52G.mjs.map +0 -7
  261. package/dist/lib/node-esm/types/index.mjs +0 -12
  262. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  263. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  264. package/dist/types/src/components/KanbanContainer.d.ts +0 -7
  265. package/dist/types/src/components/KanbanContainer.d.ts.map +0 -1
  266. package/dist/types/src/components/KanbanContainer.stories.d.ts +0 -41
  267. package/dist/types/src/components/KanbanContainer.stories.d.ts.map +0 -1
  268. package/dist/types/src/components/KanbanViewEditor.d.ts +0 -8
  269. package/dist/types/src/components/KanbanViewEditor.d.ts.map +0 -1
  270. package/src/capabilities/intent-resolver.ts +0 -71
  271. package/src/components/KanbanContainer.stories.tsx +0 -190
  272. package/src/components/KanbanContainer.tsx +0 -95
  273. package/src/components/KanbanViewEditor.tsx +0 -62
  274. /package/dist/lib/{browser/types → neutral/blueprints}/index.mjs.map +0 -0
  275. /package/dist/lib/{node-esm/types/index.mjs.map → neutral/chunk-J5LGTIGS.mjs.map} +0 -0
@@ -0,0 +1,178 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Atom, 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 AppSurface } from '@dxos/app-toolkit/ui';
11
+ import { Filter, Obj, Query, type Ref, Type } from '@dxos/echo';
12
+ import { AtomObj, 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, useItemsProjection, useProjectionModel } from '#hooks';
19
+ import { KanbanOperation } from '#types';
20
+ import { Kanban } from '#types';
21
+
22
+ export type KanbanContainerProps = AppSurface.ObjectArticleProps<Kanban.Kanban>;
23
+
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) => {
36
+ const registry = useContext(RegistryContext);
37
+ const schemas = useCapabilities(AppCapabilities.Schema);
38
+ const db = Obj.getDatabase(object);
39
+ const { invokePromise } = useOperationInvoker();
40
+ const [view] = useObject(object.spec.kind === 'view' ? object.spec.view : undefined);
41
+ const typename = view?.query ? getTypenameFromQuery(view.query.ast) : undefined;
42
+ const tag = view?.query ? getTagFromQuery(view.query.ast) : undefined;
43
+
44
+ const schemaFromDb = useSchema(db, typename);
45
+ const cardSchema = useMemo(
46
+ () => schemaFromDb ?? schemas.flat().find((schema) => Type.getTypename(schema) === typename),
47
+ [schemaFromDb, schemas, typename],
48
+ );
49
+
50
+ const items = useMemo(() => {
51
+ if (!db) {
52
+ return null;
53
+ }
54
+ const baseFilter = cardSchema ? Filter.type(cardSchema) : Filter.nothing();
55
+ const query = tag ? Query.select(baseFilter).select(Filter.tag(tag)) : Query.select(baseFilter);
56
+ return AtomQuery.make(db, query);
57
+ }, [db, cardSchema, tag]);
58
+
59
+ const projection = useProjectionModel(cardSchema, object, registry);
60
+ const change = useEchoChangeCallback(object);
61
+
62
+ const pivotFieldId = view?.projection?.pivotFieldId;
63
+ const columnFieldPath =
64
+ projection && pivotFieldId ? projection.tryGetFieldProjection(pivotFieldId)?.props.property : undefined;
65
+
66
+ const handleCardAdd = useCallback(
67
+ (columnValue: string | undefined) => {
68
+ if (db && cardSchema && columnFieldPath) {
69
+ const card = Obj.make(cardSchema, { [columnFieldPath]: columnValue });
70
+ db.add(card);
71
+ return card.id;
72
+ }
73
+ },
74
+ [db, cardSchema, columnFieldPath],
75
+ );
76
+
77
+ const handleCardRemove = useCallback(
78
+ (card: { id: string }) => {
79
+ void invokePromise(KanbanOperation.DeleteCard, { card });
80
+ },
81
+ [invokePromise],
82
+ );
83
+
84
+ if (!object || !db || !items || !projection || !change) {
85
+ return null;
86
+ }
87
+
88
+ return (
89
+ <Panel.Root role={role}>
90
+ <Panel.Toolbar asChild>
91
+ <Toolbar.Root />
92
+ </Panel.Toolbar>
93
+ <KanbanBoard.Root
94
+ kanban={object}
95
+ projection={projection}
96
+ items={items}
97
+ change={change}
98
+ onCardAdd={handleCardAdd}
99
+ onCardRemove={handleCardRemove}
100
+ >
101
+ <Panel.Content asChild>
102
+ <KanbanBoard.Content />
103
+ </Panel.Content>
104
+ </KanbanBoard.Root>
105
+ </Panel.Root>
106
+ );
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,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export { KanbanContainer as default } from './KanbanContainer';
@@ -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';
@@ -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 KanbanSettings: ComponentType<any> = lazy(() => import('./KanbanSettings'));
@@ -0,0 +1,10 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './useEchoChangeCallback';
6
+ export * from './useItemsProjection';
7
+ export * from './useKanbanBoardModel';
8
+ export * from './useKanbanColumnEventHandler';
9
+ export * from './useKanbanItemEventHandler';
10
+ 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.update(kanban, (kanban) => mutate(kanban)),
17
+ setItemField: (item, field, value) => {
18
+ Obj.update(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,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
+ };
@@ -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.update(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.update(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.update(kanban, (kanban) => {
229
+ kanban.arrangement.columns['b'] = { ids: [itemB.id] };
230
+ });
231
+ });
232
+
233
+ expect(itemsColAUpdateCount).toBe(0);
234
+ });
235
+ });