@dxos/plugin-kanban 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8

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/LICENSE +102 -5
  2. package/PLUGIN.mdl +398 -0
  3. package/README.md +1 -1
  4. package/dist/lib/neutral/KanbanArticle-T4CPKAZH.mjs +132 -0
  5. package/dist/lib/neutral/KanbanArticle-T4CPKAZH.mjs.map +7 -0
  6. package/dist/lib/neutral/KanbanPlugin.mjs +44 -0
  7. package/dist/lib/neutral/KanbanPlugin.mjs.map +7 -0
  8. package/dist/lib/neutral/KanbanPlugin.node.mjs +27 -0
  9. package/dist/lib/neutral/KanbanPlugin.node.mjs.map +7 -0
  10. package/dist/lib/neutral/KanbanPlugin.workerd.mjs +21 -0
  11. package/dist/lib/neutral/KanbanPlugin.workerd.mjs.map +7 -0
  12. package/dist/lib/neutral/KanbanSettings-5WOS4CUE.mjs +83 -0
  13. package/dist/lib/neutral/KanbanSettings-5WOS4CUE.mjs.map +7 -0
  14. package/dist/lib/neutral/blueprint-definition-6DV3Q5MC.mjs +15 -0
  15. package/dist/lib/neutral/blueprint-definition-6DV3Q5MC.mjs.map +7 -0
  16. package/dist/lib/neutral/blueprints/index.mjs +8 -0
  17. package/dist/lib/neutral/capabilities/index.mjs +17 -0
  18. package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
  19. package/dist/lib/neutral/chunk-6ZHHQWO5.mjs +39 -0
  20. package/dist/lib/neutral/chunk-6ZHHQWO5.mjs.map +7 -0
  21. package/dist/lib/neutral/chunk-DAKIZO46.mjs +246 -0
  22. package/dist/lib/neutral/chunk-DAKIZO46.mjs.map +7 -0
  23. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  24. package/dist/lib/neutral/chunk-M5ISZWZU.mjs +8 -0
  25. package/dist/lib/neutral/chunk-M5ISZWZU.mjs.map +7 -0
  26. package/dist/lib/neutral/chunk-ZTQW5KQS.mjs +26 -0
  27. package/dist/lib/neutral/chunk-ZTQW5KQS.mjs.map +7 -0
  28. package/dist/lib/neutral/components/index.mjs +243 -0
  29. package/dist/lib/neutral/components/index.mjs.map +7 -0
  30. package/dist/lib/neutral/containers/index.mjs +11 -0
  31. package/dist/lib/neutral/containers/index.mjs.map +7 -0
  32. package/dist/lib/neutral/create-object-DKBSI46K.mjs +40 -0
  33. package/dist/lib/neutral/create-object-DKBSI46K.mjs.map +7 -0
  34. package/dist/lib/neutral/delete-card-VNAV3CZV.mjs +24 -0
  35. package/dist/lib/neutral/delete-card-VNAV3CZV.mjs.map +7 -0
  36. package/dist/lib/neutral/delete-card-field-XHOLGS6L.mjs +39 -0
  37. package/dist/lib/neutral/delete-card-field-XHOLGS6L.mjs.map +7 -0
  38. package/dist/lib/neutral/hooks/index.mjs +431 -0
  39. package/dist/lib/neutral/hooks/index.mjs.map +7 -0
  40. package/dist/lib/neutral/index.mjs +34 -0
  41. package/dist/lib/neutral/index.mjs.map +7 -0
  42. package/dist/lib/neutral/meta.json +1 -0
  43. package/dist/lib/neutral/meta.mjs +8 -0
  44. package/dist/lib/neutral/meta.mjs.map +7 -0
  45. package/dist/lib/neutral/operation-handler-B7IW6MXU.mjs +13 -0
  46. package/dist/lib/neutral/operation-handler-B7IW6MXU.mjs.map +7 -0
  47. package/dist/lib/neutral/operations/index.mjs +8 -0
  48. package/dist/lib/neutral/operations/index.mjs.map +7 -0
  49. package/dist/lib/neutral/plugin.mjs +16 -0
  50. package/dist/lib/neutral/plugin.mjs.map +7 -0
  51. package/dist/lib/neutral/react-surface-VZEVEJL5.mjs +91 -0
  52. package/dist/lib/neutral/react-surface-VZEVEJL5.mjs.map +7 -0
  53. package/dist/lib/neutral/restore-card-EKVEPATL.mjs +21 -0
  54. package/dist/lib/neutral/restore-card-EKVEPATL.mjs.map +7 -0
  55. package/dist/lib/neutral/restore-card-field-TQCTGGNO.mjs +37 -0
  56. package/dist/lib/neutral/restore-card-field-TQCTGGNO.mjs.map +7 -0
  57. package/dist/lib/neutral/testing/index.mjs +62 -0
  58. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  59. package/dist/lib/neutral/translations.mjs +44 -0
  60. package/dist/lib/neutral/translations.mjs.map +7 -0
  61. package/dist/lib/neutral/types/index.mjs +22 -0
  62. package/dist/lib/neutral/types/index.mjs.map +7 -0
  63. package/dist/lib/neutral/undo-mappings-6CHW6BOF.mjs +42 -0
  64. package/dist/lib/neutral/undo-mappings-6CHW6BOF.mjs.map +7 -0
  65. package/dist/types/src/KanbanPlugin.d.ts +3 -1
  66. package/dist/types/src/KanbanPlugin.d.ts.map +1 -1
  67. package/dist/types/src/KanbanPlugin.node.d.ts +4 -0
  68. package/dist/types/src/KanbanPlugin.node.d.ts.map +1 -0
  69. package/dist/types/src/KanbanPlugin.test.d.ts +2 -0
  70. package/dist/types/src/KanbanPlugin.test.d.ts.map +1 -0
  71. package/dist/types/src/KanbanPlugin.workerd.d.ts +4 -0
  72. package/dist/types/src/KanbanPlugin.workerd.d.ts.map +1 -0
  73. package/dist/types/src/blueprints/index.d.ts +2 -0
  74. package/dist/types/src/blueprints/index.d.ts.map +1 -0
  75. package/dist/types/src/blueprints/kanban-blueprint.d.ts +4 -0
  76. package/dist/types/src/blueprints/kanban-blueprint.d.ts.map +1 -0
  77. package/dist/types/src/capabilities/artifact-definition.d.ts +3 -2
  78. package/dist/types/src/capabilities/artifact-definition.d.ts.map +1 -1
  79. package/dist/types/src/capabilities/blueprint-definition.d.ts +6 -0
  80. package/dist/types/src/capabilities/blueprint-definition.d.ts.map +1 -0
  81. package/dist/types/src/capabilities/create-object.d.ts +11 -0
  82. package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
  83. package/dist/types/src/capabilities/index.d.ts +13 -2
  84. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  85. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  86. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  87. package/dist/types/src/capabilities/react-surface.d.ts +3 -2
  88. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  89. package/dist/types/src/capabilities/undo-mappings.d.ts +5 -0
  90. package/dist/types/src/capabilities/undo-mappings.d.ts.map +1 -0
  91. package/dist/types/src/components/KanbanBoard/KanbanBoard.d.ts +37 -0
  92. package/dist/types/src/components/KanbanBoard/KanbanBoard.d.ts.map +1 -0
  93. package/dist/types/src/components/KanbanBoard/KanbanBoard.stories.d.ts +72 -0
  94. package/dist/types/src/components/KanbanBoard/KanbanBoard.stories.d.ts.map +1 -0
  95. package/dist/types/src/components/KanbanBoard/KanbanCard.d.ts +9 -0
  96. package/dist/types/src/components/KanbanBoard/KanbanCard.d.ts.map +1 -0
  97. package/dist/types/src/components/KanbanBoard/KanbanColumn.d.ts +8 -0
  98. package/dist/types/src/components/KanbanBoard/KanbanColumn.d.ts.map +1 -0
  99. package/dist/types/src/components/KanbanBoard/context.d.ts +38 -0
  100. package/dist/types/src/components/KanbanBoard/context.d.ts.map +1 -0
  101. package/dist/types/src/components/KanbanBoard/index.d.ts +2 -0
  102. package/dist/types/src/components/KanbanBoard/index.d.ts.map +1 -0
  103. package/dist/types/src/components/index.d.ts +1 -2
  104. package/dist/types/src/components/index.d.ts.map +1 -1
  105. package/dist/types/src/containers/KanbanArticle/KanbanArticle.d.ts +6 -0
  106. package/dist/types/src/containers/KanbanArticle/KanbanArticle.d.ts.map +1 -0
  107. package/dist/types/src/containers/KanbanArticle/KanbanArticle.stories.d.ts +79 -0
  108. package/dist/types/src/containers/KanbanArticle/KanbanArticle.stories.d.ts.map +1 -0
  109. package/dist/types/src/containers/KanbanArticle/index.d.ts +2 -0
  110. package/dist/types/src/containers/KanbanArticle/index.d.ts.map +1 -0
  111. package/dist/types/src/containers/KanbanSettings/KanbanSettings.d.ts +13 -0
  112. package/dist/types/src/containers/KanbanSettings/KanbanSettings.d.ts.map +1 -0
  113. package/dist/types/src/containers/KanbanSettings/index.d.ts +2 -0
  114. package/dist/types/src/containers/KanbanSettings/index.d.ts.map +1 -0
  115. package/dist/types/src/containers/index.d.ts +4 -0
  116. package/dist/types/src/containers/index.d.ts.map +1 -0
  117. package/dist/types/src/hooks/index.d.ts +7 -0
  118. package/dist/types/src/hooks/index.d.ts.map +1 -0
  119. package/dist/types/src/hooks/useEchoChangeCallback.d.ts +13 -0
  120. package/dist/types/src/hooks/useEchoChangeCallback.d.ts.map +1 -0
  121. package/dist/types/src/hooks/useItemsProjection.d.ts +10 -0
  122. package/dist/types/src/hooks/useItemsProjection.d.ts.map +1 -0
  123. package/dist/types/src/hooks/useKanbanBoardModel.browser.test.d.ts +2 -0
  124. package/dist/types/src/hooks/useKanbanBoardModel.browser.test.d.ts.map +1 -0
  125. package/dist/types/src/hooks/useKanbanBoardModel.d.ts +16 -0
  126. package/dist/types/src/hooks/useKanbanBoardModel.d.ts.map +1 -0
  127. package/dist/types/src/hooks/useKanbanColumnEventHandler.d.ts +22 -0
  128. package/dist/types/src/hooks/useKanbanColumnEventHandler.d.ts.map +1 -0
  129. package/dist/types/src/hooks/useKanbanItemEventHandler.d.ts +19 -0
  130. package/dist/types/src/hooks/useKanbanItemEventHandler.d.ts.map +1 -0
  131. package/dist/types/src/hooks/useProjectionModel.d.ts +15 -0
  132. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  133. package/dist/types/src/index.d.ts +3 -1
  134. package/dist/types/src/index.d.ts.map +1 -1
  135. package/dist/types/src/meta.d.ts +2 -3
  136. package/dist/types/src/meta.d.ts.map +1 -1
  137. package/dist/types/src/operations/delete-card-field.d.ts +5 -0
  138. package/dist/types/src/operations/delete-card-field.d.ts.map +1 -0
  139. package/dist/types/src/operations/delete-card.d.ts +5 -0
  140. package/dist/types/src/operations/delete-card.d.ts.map +1 -0
  141. package/dist/types/src/operations/index.d.ts +3 -0
  142. package/dist/types/src/operations/index.d.ts.map +1 -0
  143. package/dist/types/src/operations/restore-card-field.d.ts +5 -0
  144. package/dist/types/src/operations/restore-card-field.d.ts.map +1 -0
  145. package/dist/types/src/operations/restore-card.d.ts +5 -0
  146. package/dist/types/src/operations/restore-card.d.ts.map +1 -0
  147. package/dist/types/src/playwright/board-manager.d.ts +5 -0
  148. package/dist/types/src/playwright/board-manager.d.ts.map +1 -0
  149. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  150. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  151. package/dist/types/src/playwright/smoke.spec.d.ts +2 -0
  152. package/dist/types/src/playwright/smoke.spec.d.ts.map +1 -0
  153. package/dist/types/src/plugin.d.ts +4 -0
  154. package/dist/types/src/plugin.d.ts.map +1 -0
  155. package/dist/types/src/testing/KanbanCardTileSimple.d.ts +7 -0
  156. package/dist/types/src/testing/KanbanCardTileSimple.d.ts.map +1 -0
  157. package/dist/types/src/testing/index.d.ts +2 -0
  158. package/dist/types/src/testing/index.d.ts.map +1 -0
  159. package/dist/types/src/translations.d.ts +50 -22
  160. package/dist/types/src/translations.d.ts.map +1 -1
  161. package/dist/types/src/types/Kanban.d.ts +94 -0
  162. package/dist/types/src/types/Kanban.d.ts.map +1 -0
  163. package/dist/types/src/types/KanbanOperation.d.ts +52 -0
  164. package/dist/types/src/types/KanbanOperation.d.ts.map +1 -0
  165. package/dist/types/src/types/constants.d.ts +6 -0
  166. package/dist/types/src/types/constants.d.ts.map +1 -0
  167. package/dist/types/src/types/index.d.ts +3 -1
  168. package/dist/types/src/types/index.d.ts.map +1 -1
  169. package/dist/types/src/types/schema.d.ts +18 -95
  170. package/dist/types/src/types/schema.d.ts.map +1 -1
  171. package/dist/types/src/types/types.d.ts +28 -0
  172. package/dist/types/src/types/types.d.ts.map +1 -1
  173. package/dist/types/src/util/arrangement.d.ts +72 -0
  174. package/dist/types/src/util/arrangement.d.ts.map +1 -0
  175. package/dist/types/src/util/arrangement.test.d.ts +2 -0
  176. package/dist/types/src/util/arrangement.test.d.ts.map +1 -0
  177. package/dist/types/src/util/index.d.ts +2 -0
  178. package/dist/types/src/util/index.d.ts.map +1 -0
  179. package/dist/types/tsconfig.tsbuildinfo +1 -1
  180. package/package.json +127 -55
  181. package/src/KanbanPlugin.node.ts +21 -0
  182. package/src/KanbanPlugin.test.ts +31 -0
  183. package/src/KanbanPlugin.tsx +24 -52
  184. package/src/KanbanPlugin.workerd.ts +18 -0
  185. package/src/blueprints/index.ts +5 -0
  186. package/src/blueprints/kanban-blueprint.ts +27 -0
  187. package/src/capabilities/artifact-definition.ts +119 -114
  188. package/src/capabilities/blueprint-definition.ts +19 -0
  189. package/src/capabilities/create-object.ts +40 -0
  190. package/src/capabilities/index.ts +16 -3
  191. package/src/capabilities/operation-handler.ts +14 -0
  192. package/src/capabilities/react-surface.tsx +90 -69
  193. package/src/capabilities/undo-mappings.ts +34 -0
  194. package/src/components/KanbanBoard/KanbanBoard.stories.tsx +145 -0
  195. package/src/components/KanbanBoard/KanbanBoard.tsx +164 -0
  196. package/src/components/KanbanBoard/KanbanCard.tsx +101 -0
  197. package/src/components/KanbanBoard/KanbanColumn.tsx +72 -0
  198. package/src/components/KanbanBoard/context.ts +54 -0
  199. package/src/components/KanbanBoard/index.ts +5 -0
  200. package/src/components/index.ts +1 -2
  201. package/src/containers/KanbanArticle/KanbanArticle.stories.tsx +277 -0
  202. package/src/containers/KanbanArticle/KanbanArticle.tsx +179 -0
  203. package/src/containers/KanbanArticle/index.ts +5 -0
  204. package/src/containers/KanbanSettings/KanbanSettings.tsx +94 -0
  205. package/src/containers/KanbanSettings/index.ts +5 -0
  206. package/src/containers/index.ts +8 -0
  207. package/src/hooks/index.ts +10 -0
  208. package/src/hooks/useEchoChangeCallback.ts +30 -0
  209. package/src/hooks/useItemsProjection.ts +44 -0
  210. package/src/hooks/useKanbanBoardModel.browser.test.ts +230 -0
  211. package/src/hooks/useKanbanBoardModel.ts +156 -0
  212. package/src/hooks/useKanbanColumnEventHandler.ts +106 -0
  213. package/src/hooks/useKanbanItemEventHandler.ts +133 -0
  214. package/src/hooks/useProjectionModel.ts +58 -0
  215. package/src/index.ts +3 -2
  216. package/src/meta.ts +27 -8
  217. package/src/operations/delete-card-field.ts +42 -0
  218. package/src/operations/delete-card.ts +23 -0
  219. package/src/operations/index.ts +10 -0
  220. package/src/operations/restore-card-field.ts +36 -0
  221. package/src/operations/restore-card.ts +21 -0
  222. package/src/playwright/board-manager.ts +13 -0
  223. package/src/playwright/playwright.config.ts +19 -0
  224. package/src/playwright/smoke.spec.ts +107 -0
  225. package/src/plugin.ts +11 -0
  226. package/src/testing/KanbanCardTileSimple.tsx +82 -0
  227. package/src/testing/index.ts +5 -0
  228. package/src/translations.ts +28 -20
  229. package/src/types/Kanban.ts +134 -0
  230. package/src/types/KanbanOperation.ts +79 -0
  231. package/src/types/constants.ts +9 -0
  232. package/src/types/index.ts +4 -1
  233. package/src/types/schema.ts +33 -45
  234. package/src/types/types.ts +35 -0
  235. package/src/util/arrangement.test.ts +217 -0
  236. package/src/util/arrangement.ts +177 -0
  237. package/src/util/index.ts +5 -0
  238. package/src/vite-env.d.ts +10 -0
  239. package/dist/lib/browser/chunk-5BR6HNHI.mjs +0 -108
  240. package/dist/lib/browser/chunk-5BR6HNHI.mjs.map +0 -7
  241. package/dist/lib/browser/index.mjs +0 -99
  242. package/dist/lib/browser/index.mjs.map +0 -7
  243. package/dist/lib/browser/intent-resolver-65UVYRI2.mjs +0 -111
  244. package/dist/lib/browser/intent-resolver-65UVYRI2.mjs.map +0 -7
  245. package/dist/lib/browser/meta.json +0 -1
  246. package/dist/lib/browser/react-surface-KX5U6PJ2.mjs +0 -256
  247. package/dist/lib/browser/react-surface-KX5U6PJ2.mjs.map +0 -7
  248. package/dist/lib/browser/types/index.mjs +0 -13
  249. package/dist/lib/node-esm/chunk-WLFZKRIJ.mjs +0 -110
  250. package/dist/lib/node-esm/chunk-WLFZKRIJ.mjs.map +0 -7
  251. package/dist/lib/node-esm/index.mjs +0 -100
  252. package/dist/lib/node-esm/index.mjs.map +0 -7
  253. package/dist/lib/node-esm/intent-resolver-R3MYQATZ.mjs +0 -112
  254. package/dist/lib/node-esm/intent-resolver-R3MYQATZ.mjs.map +0 -7
  255. package/dist/lib/node-esm/meta.json +0 -1
  256. package/dist/lib/node-esm/react-surface-BAMERRU5.mjs +0 -257
  257. package/dist/lib/node-esm/react-surface-BAMERRU5.mjs.map +0 -7
  258. package/dist/lib/node-esm/types/index.mjs +0 -14
  259. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  260. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  261. package/dist/types/src/components/KanbanContainer.d.ts +0 -7
  262. package/dist/types/src/components/KanbanContainer.d.ts.map +0 -1
  263. package/dist/types/src/components/KanbanContainer.stories.d.ts +0 -10
  264. package/dist/types/src/components/KanbanContainer.stories.d.ts.map +0 -1
  265. package/dist/types/src/components/KanbanViewEditor.d.ts +0 -8
  266. package/dist/types/src/components/KanbanViewEditor.d.ts.map +0 -1
  267. package/dist/types/src/types/kanban.d.ts +0 -11
  268. package/dist/types/src/types/kanban.d.ts.map +0 -1
  269. package/src/capabilities/intent-resolver.ts +0 -70
  270. package/src/components/KanbanContainer.stories.tsx +0 -189
  271. package/src/components/KanbanContainer.tsx +0 -95
  272. package/src/components/KanbanViewEditor.tsx +0 -60
  273. package/src/types/kanban.ts +0 -29
  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,277 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { RegistryContext } from '@effect-atom/atom-react';
6
+ import { type Decorator, type Meta, type StoryObj } from '@storybook/react-vite';
7
+ import * as Effect from 'effect/Effect';
8
+ import React, { useCallback, useContext, useMemo } from 'react';
9
+ import { expect, waitFor, within } from 'storybook/test';
10
+
11
+ import { withPluginManager } from '@dxos/app-framework/testing';
12
+ import { Surface } from '@dxos/app-framework/ui';
13
+ import { AppSurface } from '@dxos/app-toolkit/ui';
14
+ import { Filter, Obj, type QueryAST, Type, View } from '@dxos/echo';
15
+ import { type Mutable } from '@dxos/echo/Obj';
16
+ import { invariant } from '@dxos/invariant';
17
+ // `/plugin` entrypoints used here for the same reason as `corePlugins()` —
18
+ // see `@dxos/plugin-testing/src/core.ts` for the rationale.
19
+ import { ClientPlugin } from '@dxos/plugin-client/testing';
20
+ import { initializeIdentity } from '@dxos/plugin-client/testing';
21
+ import { PreviewPlugin } from '@dxos/plugin-preview/testing';
22
+ import { SpacePlugin } from '@dxos/plugin-space/testing';
23
+ import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
24
+ import { random } from '@dxos/random';
25
+ import { type Space, useQuery, useType, useSpaces } from '@dxos/react-client/echo';
26
+ import { ViewEditor } from '@dxos/react-ui-form';
27
+ import { Syntax } from '@dxos/react-ui-syntax-highlighter';
28
+ import { withLayout } from '@dxos/react-ui/testing';
29
+ import { ViewModel, getTypeURIFromQuery } from '@dxos/schema';
30
+ // TODO(wittjosiah): Replace with echo/testing.
31
+ import { Organization, Person } from '@dxos/types';
32
+
33
+ import { useProjectionModel } from '#hooks';
34
+ import { translations } from '#translations';
35
+ import { Kanban } from '#types';
36
+
37
+ import { KanbanPlugin } from '../../KanbanPlugin';
38
+
39
+ random.seed(0);
40
+
41
+ const createOrg = (status?: Organization.Organization['status']) => ({
42
+ name: random.commerce.productName(),
43
+ description: random.lorem.paragraph(),
44
+ image: random.image.url(),
45
+ website: random.internet.url(),
46
+ status: (status ?? random.helpers.arrayElement(Organization.StatusOptions).id) as Organization.Organization['status'],
47
+ });
48
+
49
+ //
50
+ // Story setup helpers.
51
+ //
52
+
53
+ type ClientSetupOptions = {
54
+ types?: Type.AnyEntity[];
55
+ onSpaceCreated?: (space: Space) => Promise<void>;
56
+ };
57
+
58
+ /**
59
+ * Creates the standard plugin manager decorator with client configuration.
60
+ * Includes KanbanPlugin so the Surface resolves to KanbanArticle.
61
+ */
62
+ const withKanbanPlugins = ({ types = [], onSpaceCreated }: ClientSetupOptions): Decorator =>
63
+ withPluginManager({
64
+ plugins: [
65
+ ...corePlugins(),
66
+ ClientPlugin({
67
+ types: [...types, View.View, Kanban.Kanban],
68
+ onClientInitialized: ({ client }) =>
69
+ Effect.gen(function* () {
70
+ yield* initializeIdentity(client);
71
+ const space = yield* Effect.promise(() => client.spaces.create());
72
+ yield* Effect.promise(() => space.waitUntilReady());
73
+ yield* Effect.promise(() => onSpaceCreated?.(space) ?? Promise.resolve());
74
+ }),
75
+ }),
76
+ PreviewPlugin(),
77
+ SpacePlugin({}),
78
+ StorybookPlugin({}),
79
+ KanbanPlugin(),
80
+ ],
81
+ });
82
+
83
+ /**
84
+ * Renders the first Kanban in the space via Surface (resolves to KanbanArticle),
85
+ * with a sidebar containing ViewEditor and Json filter.
86
+ */
87
+ const DefaultComponent = () => {
88
+ const registry = useContext(RegistryContext);
89
+ const spaces = useSpaces();
90
+ const space = spaces[spaces.length - 1];
91
+ const [kanban] = useQuery(space?.db, Filter.type(Kanban.Kanban));
92
+ const viewRef = kanban && kanban.spec.kind === 'view' ? kanban.spec.view : undefined;
93
+ const view = viewRef?.target;
94
+ const typeUri = view?.query ? getTypeURIFromQuery(view.query.ast) : undefined;
95
+ const type = useType(space?.db, typeUri);
96
+ const projection = useProjectionModel(type, kanban, registry);
97
+
98
+ const data = useMemo(() => (kanban ? { subject: kanban, attendableId: 'story' } : undefined), [kanban]);
99
+
100
+ const handleUpdateQuery = useCallback(
101
+ (newQuery: QueryAST.Query) => {
102
+ invariant(type);
103
+ invariant(view);
104
+ // NOTE: persisted Type.Type typename is immutable; only the view's
105
+ // query is updated here.
106
+ Obj.update(view, (view) => {
107
+ view.query.ast = newQuery as Mutable<QueryAST.Query>;
108
+ });
109
+ },
110
+ [view, type],
111
+ );
112
+
113
+ const handleDeleteField = useCallback(
114
+ (fieldId: string) => {
115
+ if (type && Type.getDatabase(type) != null && projection) {
116
+ projection.deleteFieldProjection(fieldId);
117
+ }
118
+ },
119
+ [type, projection],
120
+ );
121
+
122
+ if (!type || !view) {
123
+ return null;
124
+ }
125
+
126
+ return (
127
+ <div className='grow grid grid-cols-[1fr_350px] overflow-hidden h-full w-full'>
128
+ <Surface.Surface type={AppSurface.Article} data={data} limit={1} />
129
+ <div className='flex flex-col h-full overflow-hidden border-l border-separator'>
130
+ <ViewEditor
131
+ registry={space?.db.graph.registry}
132
+ type={type}
133
+ view={view}
134
+ onQueryChanged={handleUpdateQuery}
135
+ onDelete={type && Type.getDatabase(type) != null ? handleDeleteField : undefined}
136
+ />
137
+ <Syntax.Root data={{ view, schema: Type.getSchema(type) }}>
138
+ <Syntax.Content>
139
+ <Syntax.Filter />
140
+ <Syntax.Viewport>
141
+ <Syntax.Code classNames='text-xs' />
142
+ </Syntax.Viewport>
143
+ </Syntax.Content>
144
+ </Syntax.Root>
145
+ </div>
146
+ </div>
147
+ );
148
+ };
149
+
150
+ //
151
+ // Story definitions.
152
+ //
153
+
154
+ const meta = {
155
+ title: 'plugins/plugin-kanban/containers/Kanban',
156
+ component: DefaultComponent,
157
+ render: () => <DefaultComponent />,
158
+ decorators: [withLayout({ layout: 'fullscreen' })],
159
+ parameters: {
160
+ layout: 'fullscreen',
161
+ translations,
162
+ },
163
+ } satisfies Meta<typeof DefaultComponent>;
164
+
165
+ export default meta;
166
+
167
+ type Story = StoryObj<typeof meta>;
168
+
169
+ /**
170
+ * Default story using static runtime schema (immutable).
171
+ * Schema mutations are not allowed.
172
+ */
173
+ export const Default: Story = {
174
+ decorators: [
175
+ withKanbanPlugins({
176
+ types: [Organization.Organization, Person.Person],
177
+ onSpaceCreated: async (space) => {
178
+ const { view } = await ViewModel.makeFromDatabase({
179
+ db: space.db,
180
+ typename: Type.getTypename(Organization.Organization),
181
+ pivotFieldName: 'status',
182
+ });
183
+ const kanban = Kanban.make({ view });
184
+ space.db.add(kanban);
185
+
186
+ Array.from({ length: 10 }).map(() => {
187
+ return space.db.add(Obj.make(Organization.Organization, createOrg()));
188
+ });
189
+ },
190
+ }),
191
+ ],
192
+ play: async ({ canvasElement }) => {
193
+ const canvas = within(canvasElement);
194
+
195
+ // Wait for the kanban columns to render by finding the status tags.
196
+ // Organization.StatusOptions: prospect, qualified, active, commit, reject.
197
+ const activeTag = await canvas.findByText('Active', undefined, { timeout: 12_000 });
198
+ const prospectTag = await canvas.findByText('Prospect', undefined, { timeout: 12_000 });
199
+ const commitTag = await canvas.findByText('Commit', undefined, { timeout: 12_000 });
200
+
201
+ // Verify all expected columns are rendered.
202
+ await expect(activeTag).toBeTruthy();
203
+ await expect(prospectTag).toBeTruthy();
204
+ await expect(commitTag).toBeTruthy();
205
+
206
+ // Find the column containers (Board uses data-testid="board-column").
207
+ const activeColumn = activeTag.closest('[data-testid="board-column"]') as HTMLElement;
208
+ const prospectColumn = prospectTag.closest('[data-testid="board-column"]') as HTMLElement;
209
+ await expect(activeColumn).toBeTruthy();
210
+ await expect(prospectColumn).toBeTruthy();
211
+
212
+ // Wait for cards to render in the columns (Board items use data-testid="board-item").
213
+ const getColumnCards = (column: HTMLElement) =>
214
+ Array.from(column.querySelectorAll('[data-testid="board-item"]')) as HTMLElement[];
215
+
216
+ await waitFor(() => expect(getColumnCards(activeColumn).length).toBeGreaterThan(0));
217
+
218
+ // Verify cards are distributed across columns.
219
+ const activeCards = getColumnCards(activeColumn);
220
+ const prospectCards = getColumnCards(prospectColumn);
221
+ await expect(activeCards.length).toBeGreaterThan(0);
222
+ await expect(prospectCards.length).toBeGreaterThan(0);
223
+
224
+ // Verify cards have drag handles (Card.Header includes drag handle).
225
+ const firstActiveCard = activeCards[0];
226
+ const buttons = firstActiveCard.querySelectorAll('button');
227
+ await expect(buttons.length).toBeGreaterThan(0);
228
+
229
+ // Verify add-card action exists in columns (optional footer).
230
+ const activeAddItem = activeColumn.querySelector('[data-testid="board-column-add-item"]');
231
+ const prospectAddItem = prospectColumn.querySelector('[data-testid="board-column-add-item"]');
232
+ await expect(activeAddItem).toBeTruthy();
233
+ await expect(prospectAddItem).toBeTruthy();
234
+
235
+ // TODO(wittjosiah): Get drag & drop tests working.
236
+ // See packages/apps/composer-app/src/playwright/stack.spec.ts for reference.
237
+ },
238
+ };
239
+
240
+ /**
241
+ * Story variant that uses a database-stored Type.Type entity (mutable schema).
242
+ * This allows testing schema mutations like adding/removing fields.
243
+ */
244
+ // TODO(wittjosiah): Card previews (e.g., OrganizationCard) are type-specific and hard-coded.
245
+ // They don't use the projection to determine which fields to display, so deleting a field
246
+ // from the schema won't remove it from the card preview. To fix this, the type-specific
247
+ // cards in PreviewPlugin would need to accept and respect the projection prop.
248
+ export const MutableSchema: Story = {
249
+ decorators: [
250
+ withKanbanPlugins({
251
+ onSpaceCreated: async (space) => {
252
+ // Persist the schema in the database to make it mutable (stored Type.Type).
253
+ const type = await space.db.addType(Organization.Organization);
254
+
255
+ const { view } = await ViewModel.makeFromDatabase({
256
+ db: space.db,
257
+ // `db.addType` returns a persisted `Type.Type` entity; its typename lives in the
258
+ // type metadata, so read it via `Type.getTypename` rather than a `.typename` prop.
259
+ typename: Type.getTypename(type),
260
+ pivotFieldName: 'status',
261
+ });
262
+ const kanban = Kanban.make({ view });
263
+ space.db.add(kanban);
264
+
265
+ // Create test data using the registered schema.
266
+ const requiredOrgs = [
267
+ ...Array.from({ length: 2 }, () => createOrg('prospect')),
268
+ ...Array.from({ length: 5 }, () => createOrg('qualified')),
269
+ ...Array.from({ length: 1 }, () => createOrg('active')),
270
+ ...Array.from({ length: 1 }, () => createOrg('commit')),
271
+ ...Array.from({ length: 1 }, () => createOrg('reject')),
272
+ ];
273
+ requiredOrgs.forEach((org) => space.db.add(Obj.make(type, org)));
274
+ },
275
+ }),
276
+ ],
277
+ };
@@ -0,0 +1,179 @@
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 { useSchemaFilter, type AppSurface } from '@dxos/app-toolkit/ui';
11
+ import { Filter, Obj, Query, type Ref, Type } from '@dxos/echo';
12
+ import { useObject, useType } from '@dxos/react-client/echo';
13
+ import { Panel, Toolbar } from '@dxos/react-ui';
14
+ import { getTagFromQuery, getTypeURIFromQuery } from '@dxos/schema';
15
+
16
+ import { KanbanBoard } from '#components';
17
+ import { useEchoChangeCallback, useItemsProjection, useProjectionModel } from '#hooks';
18
+ import { KanbanOperation } from '#types';
19
+ import { Kanban } from '#types';
20
+
21
+ export type KanbanArticleProps = AppSurface.ObjectArticleProps<Kanban.Kanban>;
22
+
23
+ export const KanbanArticle = (props: KanbanArticleProps) => {
24
+ // Branch on `kanban.spec.kind`: view-variant runs a typename query through
25
+ // `useProjectionModel`; items-variant dereferences `kanban.spec.items` and
26
+ // uses a stub projection from `useItemsProjection`.
27
+ return Kanban.isKanbanItems(props.subject) ? (
28
+ <ItemsKanbanArticle {...props} subject={props.subject} />
29
+ ) : (
30
+ <ViewKanbanArticle {...props} />
31
+ );
32
+ };
33
+
34
+ const ViewKanbanArticle = ({ role, subject: object }: KanbanArticleProps) => {
35
+ const registry = useContext(RegistryContext);
36
+ const schemas = useCapabilities(AppCapabilities.Schema);
37
+ const db = Obj.getDatabase(object);
38
+ const { invokePromise } = useOperationInvoker();
39
+ const [view] = useObject(object.spec.kind === 'view' ? object.spec.view : undefined);
40
+ const typeUri = view?.query ? getTypeURIFromQuery(view.query.ast) : undefined;
41
+ const tag = view?.query ? getTagFromQuery(view.query.ast) : undefined;
42
+
43
+ const schemaFromDb = useType(db, typeUri);
44
+ const cardSchema = useMemo(
45
+ () => schemaFromDb ?? schemas.flat().find((schema) => Type.getURI(schema) === typeUri),
46
+ [schemaFromDb, schemas, typeUri],
47
+ );
48
+
49
+ const baseFilter = useSchemaFilter(cardSchema);
50
+ const items = useMemo(() => {
51
+ if (!db) {
52
+ return null;
53
+ }
54
+ const query = tag ? Query.select(baseFilter).select(Filter.tag(tag)) : Query.select(baseFilter);
55
+ return db.query(query).atom;
56
+ }, [db, baseFilter, tag]);
57
+
58
+ const projection = useProjectionModel(cardSchema, object, registry);
59
+ const change = useEchoChangeCallback(object);
60
+
61
+ const pivotFieldId = view?.projection?.pivotFieldId;
62
+ const columnFieldPath =
63
+ projection && pivotFieldId ? projection.tryGetFieldProjection(pivotFieldId)?.props.property : undefined;
64
+
65
+ const handleCardAdd = useCallback(
66
+ (columnValue: string | undefined) => {
67
+ if (db && cardSchema && columnFieldPath) {
68
+ const card = Obj.make(Type.assertObject(cardSchema), {
69
+ [columnFieldPath]: columnValue,
70
+ });
71
+ db.add(card);
72
+ return card.id;
73
+ }
74
+ },
75
+ [db, cardSchema, columnFieldPath],
76
+ );
77
+
78
+ const handleCardRemove = useCallback(
79
+ (card: { id: string }) => {
80
+ void invokePromise(KanbanOperation.DeleteCard, { card });
81
+ },
82
+ [invokePromise],
83
+ );
84
+
85
+ if (!object || !db || !items || !projection || !change) {
86
+ return null;
87
+ }
88
+
89
+ return (
90
+ <Panel.Root role={role}>
91
+ <Panel.Toolbar asChild>
92
+ <Toolbar.Root />
93
+ </Panel.Toolbar>
94
+ <KanbanBoard.Root
95
+ kanban={object}
96
+ projection={projection}
97
+ items={items}
98
+ change={change}
99
+ onCardAdd={handleCardAdd}
100
+ onCardRemove={handleCardRemove}
101
+ >
102
+ <Panel.Content asChild>
103
+ <KanbanBoard.Content />
104
+ </Panel.Content>
105
+ </KanbanBoard.Root>
106
+ </Panel.Root>
107
+ );
108
+ };
109
+
110
+ type ItemsKanbanArticleProps = Omit<KanbanArticleProps, 'subject'> & { subject: Kanban.KanbanItems };
111
+
112
+ const ItemsKanbanArticle = ({ role, subject: object }: ItemsKanbanArticleProps) => {
113
+ const db = Obj.getDatabase(object);
114
+ const projection = useItemsProjection(object);
115
+ const change = useEchoChangeCallback(object);
116
+
117
+ // TODO(wittjosiah): pass refs (not loaded objects) through to the kanban
118
+ // board and let `KanbanCard` subscribe to its own ref via `useObject`.
119
+ // Today this atom subscribes to *every* item — any one changing causes the
120
+ // container (and the model's per-column atoms) to recompute. With cards
121
+ // subscribing themselves, the container only needs the refs and the
122
+ // per-card render is independent. Requires:
123
+ // - `KanbanCard` to accept `Ref<Obj.Unknown>` as `data` and call
124
+ // `useObject(ref)` internally.
125
+ // - The model to handle a ref-bearing item shape (id from
126
+ // `ref.dxn.asEchoDXN()?.echoUri`) and use arrangement-only ordering
127
+ // for items-variant (no pivot-value fallback, since refs don't expose
128
+ // the pivot field without loading).
129
+ // - `Mosaic.isItem` to accept the ref wrapper alongside `Obj.isObject`.
130
+ const itemsAtom = useMemo(
131
+ () =>
132
+ Atom.make((get) => {
133
+ const out: Obj.Unknown[] = [];
134
+ for (const ref of object.spec.items as ReadonlyArray<Ref.Ref<Obj.Unknown>>) {
135
+ const target = get(Obj.atom(ref));
136
+ if (target == null) {
137
+ continue;
138
+ }
139
+ // Drop soft-deleted cards (e.g. Trello-closed cards). The ref
140
+ // stays in `spec.items` so arrangement is preserved, but the card
141
+ // shouldn't render.
142
+ if (Obj.isDeleted(target)) {
143
+ continue;
144
+ }
145
+ out.push(target as unknown as Obj.Unknown);
146
+ }
147
+ return out;
148
+ }),
149
+ [object.spec.items],
150
+ );
151
+
152
+ const handleCardRemove = useCallback(() => undefined, []);
153
+
154
+ if (!object || !db || !change) {
155
+ return null;
156
+ }
157
+
158
+ // TODO(wittjosiah): wire `onCardAdd` to the create-object flow so
159
+ // users can add items directly from the kanban (currently the column's
160
+ // "+" button is hidden because `onCardAdd` is undefined).
161
+ return (
162
+ <Panel.Root role={role}>
163
+ <Panel.Toolbar asChild>
164
+ <Toolbar.Root />
165
+ </Panel.Toolbar>
166
+ <KanbanBoard.Root
167
+ kanban={object}
168
+ projection={projection}
169
+ items={itemsAtom}
170
+ change={change}
171
+ onCardRemove={handleCardRemove}
172
+ >
173
+ <Panel.Content asChild>
174
+ <KanbanBoard.Content />
175
+ </Panel.Content>
176
+ </KanbanBoard.Root>
177
+ </Panel.Root>
178
+ );
179
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export { KanbanArticle as default } from './KanbanArticle';
@@ -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/Format';
11
+ import { useObject, useType } from '@dxos/react-client/echo';
12
+ import { Form, type FormFieldMap, SelectField } from '@dxos/react-ui-form';
13
+ import { getTypeURIFromQuery } 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 currentTypeUri = view?.query ? getTypeURIFromQuery(view.query.ast) : undefined;
34
+ const schema = useType(db, currentTypeUri);
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 KanbanArticle: ComponentType<any> = lazy(() => import('./KanbanArticle'));
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
+ };