@dxos/plugin-kanban 0.8.4-main.ae835ea → 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 (219) hide show
  1. package/dist/lib/browser/blueprints/index.mjs +27 -0
  2. package/dist/lib/browser/blueprints/index.mjs.map +7 -0
  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/chunk-J5LGTIGS.mjs +10 -0
  6. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
  7. package/dist/lib/browser/delete-card-VPNVIWOA.mjs +32 -0
  8. package/dist/lib/browser/delete-card-VPNVIWOA.mjs.map +7 -0
  9. package/dist/lib/browser/delete-card-field-4HHF2GYX.mjs +50 -0
  10. package/dist/lib/browser/delete-card-field-4HHF2GYX.mjs.map +7 -0
  11. package/dist/lib/browser/index.mjs +101 -85
  12. package/dist/lib/browser/index.mjs.map +4 -4
  13. package/dist/lib/browser/meta.json +1 -1
  14. package/dist/lib/browser/operations/index.mjs +13 -0
  15. package/dist/lib/browser/operations/index.mjs.map +7 -0
  16. package/dist/lib/browser/restore-card-4GG2RYKR.mjs +29 -0
  17. package/dist/lib/browser/restore-card-4GG2RYKR.mjs.map +7 -0
  18. package/dist/lib/browser/restore-card-field-3T26ACYX.mjs +48 -0
  19. package/dist/lib/browser/restore-card-field-3T26ACYX.mjs.map +7 -0
  20. package/dist/lib/browser/types/index.mjs +95 -6
  21. package/dist/lib/browser/types/index.mjs.map +4 -4
  22. package/dist/lib/node-esm/blueprints/index.mjs +28 -0
  23. package/dist/lib/node-esm/blueprints/index.mjs.map +7 -0
  24. package/dist/lib/node-esm/chunk-6LELYA2G.mjs +106 -0
  25. package/dist/lib/node-esm/chunk-6LELYA2G.mjs.map +7 -0
  26. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  27. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  28. package/dist/lib/node-esm/delete-card-5PW5OMFN.mjs +33 -0
  29. package/dist/lib/node-esm/delete-card-5PW5OMFN.mjs.map +7 -0
  30. package/dist/lib/node-esm/delete-card-field-KPJU2AQ3.mjs +51 -0
  31. package/dist/lib/node-esm/delete-card-field-KPJU2AQ3.mjs.map +7 -0
  32. package/dist/lib/node-esm/index.mjs +101 -85
  33. package/dist/lib/node-esm/index.mjs.map +4 -4
  34. package/dist/lib/node-esm/meta.json +1 -1
  35. package/dist/lib/node-esm/operations/index.mjs +14 -0
  36. package/dist/lib/node-esm/operations/index.mjs.map +7 -0
  37. package/dist/lib/node-esm/restore-card-X2TKMU5A.mjs +30 -0
  38. package/dist/lib/node-esm/restore-card-X2TKMU5A.mjs.map +7 -0
  39. package/dist/lib/node-esm/restore-card-field-IUTL4RTR.mjs +49 -0
  40. package/dist/lib/node-esm/restore-card-field-IUTL4RTR.mjs.map +7 -0
  41. package/dist/lib/node-esm/types/index.mjs +95 -6
  42. package/dist/lib/node-esm/types/index.mjs.map +4 -4
  43. package/dist/types/src/KanbanPlugin.d.ts +2 -1
  44. package/dist/types/src/KanbanPlugin.d.ts.map +1 -1
  45. package/dist/types/src/blueprints/index.d.ts +2 -0
  46. package/dist/types/src/blueprints/index.d.ts.map +1 -0
  47. package/dist/types/src/blueprints/kanban-blueprint.d.ts +4 -0
  48. package/dist/types/src/blueprints/kanban-blueprint.d.ts.map +1 -0
  49. package/dist/types/src/capabilities/artifact-definition.d.ts +3 -2
  50. package/dist/types/src/capabilities/artifact-definition.d.ts.map +1 -1
  51. package/dist/types/src/capabilities/blueprint-definition.d.ts +5 -4
  52. package/dist/types/src/capabilities/blueprint-definition.d.ts.map +1 -1
  53. package/dist/types/src/capabilities/index.d.ts +6 -3
  54. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  55. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  56. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  57. package/dist/types/src/capabilities/react-surface.d.ts +3 -2
  58. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  59. package/dist/types/src/capabilities/undo-mappings.d.ts +5 -0
  60. package/dist/types/src/capabilities/undo-mappings.d.ts.map +1 -0
  61. package/dist/types/src/components/KanbanBoard/KanbanBoard.d.ts +65 -0
  62. package/dist/types/src/components/KanbanBoard/KanbanBoard.d.ts.map +1 -0
  63. package/dist/types/src/components/KanbanBoard/KanbanBoard.stories.d.ts +72 -0
  64. package/dist/types/src/components/KanbanBoard/KanbanBoard.stories.d.ts.map +1 -0
  65. package/dist/types/src/components/KanbanBoard/KanbanCard.d.ts +10 -0
  66. package/dist/types/src/components/KanbanBoard/KanbanCard.d.ts.map +1 -0
  67. package/dist/types/src/components/KanbanBoard/KanbanColumn.d.ts +9 -0
  68. package/dist/types/src/components/KanbanBoard/KanbanColumn.d.ts.map +1 -0
  69. package/dist/types/src/components/KanbanBoard/index.d.ts +2 -0
  70. package/dist/types/src/components/KanbanBoard/index.d.ts.map +1 -0
  71. package/dist/types/src/components/index.d.ts +1 -2
  72. package/dist/types/src/components/index.d.ts.map +1 -1
  73. package/dist/types/src/containers/KanbanContainer/KanbanContainer.d.ts +6 -0
  74. package/dist/types/src/containers/KanbanContainer/KanbanContainer.d.ts.map +1 -0
  75. package/dist/types/src/containers/KanbanContainer/KanbanContainer.stories.d.ts +79 -0
  76. package/dist/types/src/containers/KanbanContainer/KanbanContainer.stories.d.ts.map +1 -0
  77. package/dist/types/src/containers/KanbanContainer/index.d.ts +3 -0
  78. package/dist/types/src/containers/KanbanContainer/index.d.ts.map +1 -0
  79. package/dist/types/src/containers/KanbanViewEditor/KanbanViewEditor.d.ts +6 -0
  80. package/dist/types/src/containers/KanbanViewEditor/KanbanViewEditor.d.ts.map +1 -0
  81. package/dist/types/src/containers/KanbanViewEditor/index.d.ts +3 -0
  82. package/dist/types/src/containers/KanbanViewEditor/index.d.ts.map +1 -0
  83. package/dist/types/src/containers/index.d.ts +4 -0
  84. package/dist/types/src/containers/index.d.ts.map +1 -0
  85. package/dist/types/src/hooks/index.d.ts +6 -0
  86. package/dist/types/src/hooks/index.d.ts.map +1 -0
  87. package/dist/types/src/hooks/useEchoChangeCallback.d.ts +13 -0
  88. package/dist/types/src/hooks/useEchoChangeCallback.d.ts.map +1 -0
  89. package/dist/types/src/hooks/useKanbanBoardModel.d.ts +16 -0
  90. package/dist/types/src/hooks/useKanbanBoardModel.d.ts.map +1 -0
  91. package/dist/types/src/hooks/useKanbanBoardModel.test.d.ts +2 -0
  92. package/dist/types/src/hooks/useKanbanBoardModel.test.d.ts.map +1 -0
  93. package/dist/types/src/hooks/useKanbanColumnEventHandler.d.ts +22 -0
  94. package/dist/types/src/hooks/useKanbanColumnEventHandler.d.ts.map +1 -0
  95. package/dist/types/src/hooks/useKanbanItemEventHandler.d.ts +19 -0
  96. package/dist/types/src/hooks/useKanbanItemEventHandler.d.ts.map +1 -0
  97. package/dist/types/src/hooks/useProjectionModel.d.ts +15 -0
  98. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  99. package/dist/types/src/meta.d.ts +2 -2
  100. package/dist/types/src/meta.d.ts.map +1 -1
  101. package/dist/types/src/operations/definitions.d.ts +52 -0
  102. package/dist/types/src/operations/definitions.d.ts.map +1 -0
  103. package/dist/types/src/operations/delete-card-field.d.ts +5 -0
  104. package/dist/types/src/operations/delete-card-field.d.ts.map +1 -0
  105. package/dist/types/src/operations/delete-card.d.ts +5 -0
  106. package/dist/types/src/operations/delete-card.d.ts.map +1 -0
  107. package/dist/types/src/operations/index.d.ts +4 -0
  108. package/dist/types/src/operations/index.d.ts.map +1 -0
  109. package/dist/types/src/operations/restore-card-field.d.ts +5 -0
  110. package/dist/types/src/operations/restore-card-field.d.ts.map +1 -0
  111. package/dist/types/src/operations/restore-card.d.ts +5 -0
  112. package/dist/types/src/operations/restore-card.d.ts.map +1 -0
  113. package/dist/types/src/playwright/board-manager.d.ts +5 -0
  114. package/dist/types/src/playwright/board-manager.d.ts.map +1 -0
  115. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  116. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  117. package/dist/types/src/playwright/smoke.spec.d.ts +2 -0
  118. package/dist/types/src/playwright/smoke.spec.d.ts.map +1 -0
  119. package/dist/types/src/testing/KanbanCardTileSimple.d.ts +7 -0
  120. package/dist/types/src/testing/KanbanCardTileSimple.d.ts.map +1 -0
  121. package/dist/types/src/testing/index.d.ts +2 -0
  122. package/dist/types/src/testing/index.d.ts.map +1 -0
  123. package/dist/types/src/translations.d.ts +50 -22
  124. package/dist/types/src/translations.d.ts.map +1 -1
  125. package/dist/types/src/types/Kanban.d.ts +37 -0
  126. package/dist/types/src/types/Kanban.d.ts.map +1 -0
  127. package/dist/types/src/types/constants.d.ts +6 -0
  128. package/dist/types/src/types/constants.d.ts.map +1 -0
  129. package/dist/types/src/types/index.d.ts +2 -0
  130. package/dist/types/src/types/index.d.ts.map +1 -1
  131. package/dist/types/src/types/schema.d.ts +3 -51
  132. package/dist/types/src/types/schema.d.ts.map +1 -1
  133. package/dist/types/src/types/types.d.ts +28 -0
  134. package/dist/types/src/types/types.d.ts.map +1 -1
  135. package/dist/types/src/util/arrangement.d.ts +68 -0
  136. package/dist/types/src/util/arrangement.d.ts.map +1 -0
  137. package/dist/types/src/util/arrangement.test.d.ts +2 -0
  138. package/dist/types/src/util/arrangement.test.d.ts.map +1 -0
  139. package/dist/types/src/util/index.d.ts +2 -0
  140. package/dist/types/src/util/index.d.ts.map +1 -0
  141. package/dist/types/tsconfig.tsbuildinfo +1 -1
  142. package/package.json +84 -46
  143. package/src/KanbanPlugin.tsx +49 -56
  144. package/src/blueprints/index.ts +5 -0
  145. package/src/blueprints/kanban-blueprint.ts +28 -0
  146. package/src/capabilities/artifact-definition.ts +115 -112
  147. package/src/capabilities/blueprint-definition.ts +11 -24
  148. package/src/capabilities/index.ts +9 -4
  149. package/src/capabilities/operation-handler.ts +14 -0
  150. package/src/capabilities/react-surface.tsx +70 -68
  151. package/src/capabilities/undo-mappings.ts +34 -0
  152. package/src/components/KanbanBoard/KanbanBoard.stories.tsx +142 -0
  153. package/src/components/KanbanBoard/KanbanBoard.tsx +193 -0
  154. package/src/components/KanbanBoard/KanbanCard.tsx +86 -0
  155. package/src/components/KanbanBoard/KanbanColumn.tsx +69 -0
  156. package/src/components/KanbanBoard/index.ts +5 -0
  157. package/src/components/index.ts +1 -2
  158. package/src/containers/KanbanContainer/KanbanContainer.stories.tsx +269 -0
  159. package/src/containers/KanbanContainer/KanbanContainer.tsx +96 -0
  160. package/src/containers/KanbanContainer/index.ts +7 -0
  161. package/src/containers/KanbanViewEditor/KanbanViewEditor.tsx +63 -0
  162. package/src/containers/KanbanViewEditor/index.ts +7 -0
  163. package/src/containers/index.ts +8 -0
  164. package/src/hooks/index.ts +9 -0
  165. package/src/hooks/useEchoChangeCallback.ts +30 -0
  166. package/src/hooks/useKanbanBoardModel.test.ts +235 -0
  167. package/src/hooks/useKanbanBoardModel.ts +143 -0
  168. package/src/hooks/useKanbanColumnEventHandler.ts +106 -0
  169. package/src/hooks/useKanbanItemEventHandler.ts +133 -0
  170. package/src/hooks/useProjectionModel.ts +58 -0
  171. package/src/meta.ts +3 -3
  172. package/src/operations/definitions.ts +63 -0
  173. package/src/operations/delete-card-field.ts +47 -0
  174. package/src/operations/delete-card.ts +23 -0
  175. package/src/operations/index.ts +12 -0
  176. package/src/operations/restore-card-field.ts +41 -0
  177. package/src/operations/restore-card.ts +21 -0
  178. package/src/playwright/board-manager.ts +13 -0
  179. package/src/playwright/playwright.config.ts +19 -0
  180. package/src/playwright/smoke.spec.ts +107 -0
  181. package/src/testing/KanbanCardTileSimple.tsx +82 -0
  182. package/src/testing/index.ts +5 -0
  183. package/src/translations.ts +28 -20
  184. package/src/types/Kanban.ts +71 -0
  185. package/src/types/constants.ts +9 -0
  186. package/src/types/index.ts +2 -0
  187. package/src/types/schema.ts +14 -44
  188. package/src/types/types.ts +35 -0
  189. package/src/util/arrangement.test.ts +208 -0
  190. package/src/util/arrangement.ts +167 -0
  191. package/src/util/index.ts +5 -0
  192. package/dist/lib/browser/blueprint-definition-UYVX622Q.mjs +0 -28
  193. package/dist/lib/browser/blueprint-definition-UYVX622Q.mjs.map +0 -7
  194. package/dist/lib/browser/chunk-3UDST345.mjs +0 -85
  195. package/dist/lib/browser/chunk-3UDST345.mjs.map +0 -7
  196. package/dist/lib/browser/intent-resolver-VVBNS2TO.mjs +0 -111
  197. package/dist/lib/browser/intent-resolver-VVBNS2TO.mjs.map +0 -7
  198. package/dist/lib/browser/react-surface-FNXJ6VJX.mjs +0 -255
  199. package/dist/lib/browser/react-surface-FNXJ6VJX.mjs.map +0 -7
  200. package/dist/lib/node-esm/blueprint-definition-42P47FUY.mjs +0 -30
  201. package/dist/lib/node-esm/blueprint-definition-42P47FUY.mjs.map +0 -7
  202. package/dist/lib/node-esm/chunk-JBOARUAT.mjs +0 -87
  203. package/dist/lib/node-esm/chunk-JBOARUAT.mjs.map +0 -7
  204. package/dist/lib/node-esm/intent-resolver-ACN7UALP.mjs +0 -112
  205. package/dist/lib/node-esm/intent-resolver-ACN7UALP.mjs.map +0 -7
  206. package/dist/lib/node-esm/react-surface-ZHYHCV5N.mjs +0 -256
  207. package/dist/lib/node-esm/react-surface-ZHYHCV5N.mjs.map +0 -7
  208. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  209. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  210. package/dist/types/src/components/KanbanContainer.d.ts +0 -7
  211. package/dist/types/src/components/KanbanContainer.d.ts.map +0 -1
  212. package/dist/types/src/components/KanbanContainer.stories.d.ts +0 -41
  213. package/dist/types/src/components/KanbanContainer.stories.d.ts.map +0 -1
  214. package/dist/types/src/components/KanbanViewEditor.d.ts +0 -8
  215. package/dist/types/src/components/KanbanViewEditor.d.ts.map +0 -1
  216. package/src/capabilities/intent-resolver.ts +0 -71
  217. package/src/components/KanbanContainer.stories.tsx +0 -193
  218. package/src/components/KanbanContainer.tsx +0 -95
  219. package/src/components/KanbanViewEditor.tsx +0 -64
@@ -2,5 +2,4 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './KanbanContainer';
6
- export * from './KanbanViewEditor';
5
+ export * from './KanbanBoard';
@@ -0,0 +1,269 @@
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 { Obj, type QueryAST, Type } from '@dxos/echo';
14
+ import { View } from '@dxos/echo';
15
+ import { type Mutable } from '@dxos/echo/internal';
16
+ import { invariant } from '@dxos/invariant';
17
+ import { ClientPlugin } from '@dxos/plugin-client';
18
+ import { initializeIdentity } from '@dxos/plugin-client/testing';
19
+ import { PreviewPlugin } from '@dxos/plugin-preview';
20
+ import { SpacePlugin } from '@dxos/plugin-space';
21
+ import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
22
+ import { faker } from '@dxos/random';
23
+ import { Filter, type Space, useQuery, useSchema, useSpaces } from '@dxos/react-client/echo';
24
+ import { withLayout } from '@dxos/react-ui/testing';
25
+ import { ViewEditor } from '@dxos/react-ui-form';
26
+ import { Json } from '@dxos/react-ui-syntax-highlighter';
27
+ import { ViewModel, getTypenameFromQuery } from '@dxos/schema';
28
+ // TODO(wittjosiah): Replace with echo/testing.
29
+ import { Organization, Person } from '@dxos/types';
30
+
31
+ import { useProjectionModel } from '#hooks';
32
+ import { KanbanPlugin } from '../../KanbanPlugin';
33
+ import { translations } from '../../translations';
34
+ import { Kanban } from '#types';
35
+
36
+ faker.seed(0);
37
+
38
+ const createOrg = (status?: Organization.Organization['status']) => ({
39
+ name: faker.commerce.productName(),
40
+ description: faker.lorem.paragraph(),
41
+ image: faker.image.url(),
42
+ website: faker.internet.url(),
43
+ status: (status ?? faker.helpers.arrayElement(Organization.StatusOptions).id) as Organization.Organization['status'],
44
+ });
45
+
46
+ //
47
+ // Story setup helpers.
48
+ //
49
+
50
+ type ClientSetupOptions = {
51
+ types?: Type.AnyEntity[];
52
+ onSpaceCreated?: (space: Space) => Promise<void>;
53
+ };
54
+
55
+ /**
56
+ * Creates the standard plugin manager decorator with client configuration.
57
+ * Includes KanbanPlugin so the Surface resolves to KanbanContainer.
58
+ */
59
+ const withKanbanPlugins = ({ types = [], onSpaceCreated }: ClientSetupOptions): Decorator =>
60
+ withPluginManager({
61
+ plugins: [
62
+ ...corePlugins(),
63
+ ClientPlugin({
64
+ types: [...types, View.View, Kanban.Kanban],
65
+ onClientInitialized: ({ client }) =>
66
+ Effect.gen(function* () {
67
+ yield* initializeIdentity(client);
68
+ const space = yield* Effect.promise(() => client.spaces.create());
69
+ yield* Effect.promise(() => space.waitUntilReady());
70
+ yield* Effect.promise(() => onSpaceCreated?.(space) ?? Promise.resolve());
71
+ }),
72
+ }),
73
+ PreviewPlugin(),
74
+ SpacePlugin({}),
75
+ StorybookPlugin({}),
76
+ KanbanPlugin(),
77
+ ],
78
+ });
79
+
80
+ /**
81
+ * Renders the first Kanban in the space via Surface (resolves to KanbanContainer),
82
+ * with a sidebar containing ViewEditor and Json filter.
83
+ */
84
+ const DefaultComponent = () => {
85
+ const registry = useContext(RegistryContext);
86
+ const spaces = useSpaces();
87
+ const space = spaces[spaces.length - 1];
88
+ const [kanban] = useQuery(space?.db, Filter.type(Kanban.Kanban));
89
+ const typename = kanban?.view.target?.query ? getTypenameFromQuery(kanban.view.target.query.ast) : undefined;
90
+ const schema = useSchema(space?.db, typename);
91
+ const projection = useProjectionModel(schema, kanban, registry);
92
+
93
+ const data = useMemo(() => (kanban ? { subject: kanban } : {}), [kanban]);
94
+
95
+ const handleUpdateQuery = useCallback(
96
+ (newQuery: QueryAST.Query) => {
97
+ invariant(schema);
98
+ invariant(kanban?.view.target);
99
+ if (Type.isMutable(schema)) {
100
+ schema.updateTypename(getTypenameFromQuery(newQuery));
101
+ }
102
+ Obj.change(kanban.view.target, (view) => {
103
+ view.query.ast = newQuery as Mutable<QueryAST.Query>;
104
+ });
105
+ },
106
+ [kanban, schema],
107
+ );
108
+
109
+ const handleDeleteField = useCallback(
110
+ (fieldId: string) => {
111
+ if (schema && Type.isMutable(schema) && projection) {
112
+ projection.deleteFieldProjection(fieldId);
113
+ }
114
+ },
115
+ [schema, projection],
116
+ );
117
+
118
+ if (!schema || !kanban?.view.target) {
119
+ return null;
120
+ }
121
+
122
+ return (
123
+ <div className='grow grid grid-cols-[1fr_350px] overflow-hidden h-full w-full'>
124
+ <Surface.Surface role='article' data={data} limit={1} />
125
+ <div className='flex flex-col h-full overflow-hidden border-l border-separator'>
126
+ <ViewEditor
127
+ registry={space?.db.schemaRegistry}
128
+ schema={schema}
129
+ view={kanban.view.target}
130
+ onQueryChanged={handleUpdateQuery}
131
+ onDelete={schema && Type.isMutable(schema) ? handleDeleteField : undefined}
132
+ />
133
+ <Json.Root data={{ view: kanban.view.target, schema }}>
134
+ <Json.Content>
135
+ <Json.Filter />
136
+ <Json.Data classNames='text-xs' />
137
+ </Json.Content>
138
+ </Json.Root>
139
+ </div>
140
+ </div>
141
+ );
142
+ };
143
+
144
+ //
145
+ // Story definitions.
146
+ //
147
+
148
+ const meta = {
149
+ title: 'plugins/plugin-kanban/containers/Kanban',
150
+ component: DefaultComponent,
151
+ render: () => <DefaultComponent />,
152
+ decorators: [withLayout({ layout: 'fullscreen' })],
153
+ parameters: {
154
+ layout: 'fullscreen',
155
+ translations,
156
+ },
157
+ } satisfies Meta<typeof DefaultComponent>;
158
+
159
+ export default meta;
160
+
161
+ type Story = StoryObj<typeof meta>;
162
+
163
+ /**
164
+ * Default story using static runtime schema (immutable).
165
+ * Schema mutations are not allowed.
166
+ */
167
+ export const Default: Story = {
168
+ decorators: [
169
+ withKanbanPlugins({
170
+ types: [Organization.Organization, Person.Person],
171
+ onSpaceCreated: async (space) => {
172
+ const { view } = await ViewModel.makeFromDatabase({
173
+ db: space.db,
174
+ typename: Organization.Organization.typename,
175
+ pivotFieldName: 'status',
176
+ });
177
+ const kanban = Kanban.make({ view });
178
+ space.db.add(kanban);
179
+
180
+ Array.from({ length: 10 }).map(() => {
181
+ return space.db.add(Obj.make(Organization.Organization, createOrg()));
182
+ });
183
+ },
184
+ }),
185
+ ],
186
+ play: async ({ canvasElement }) => {
187
+ const canvas = within(canvasElement);
188
+
189
+ // Wait for the kanban columns to render by finding the status tags.
190
+ // Organization.StatusOptions: prospect, qualified, active, commit, reject.
191
+ const activeTag = await canvas.findByText('Active', undefined, { timeout: 30_000 });
192
+ const prospectTag = await canvas.findByText('Prospect', undefined, { timeout: 10_000 });
193
+ const commitTag = await canvas.findByText('Commit', undefined, { timeout: 10_000 });
194
+
195
+ // Verify all expected columns are rendered.
196
+ await expect(activeTag).toBeTruthy();
197
+ await expect(prospectTag).toBeTruthy();
198
+ await expect(commitTag).toBeTruthy();
199
+
200
+ // Find the column containers (Board uses data-testid="board-column").
201
+ const activeColumn = activeTag.closest('[data-testid="board-column"]') as HTMLElement;
202
+ const prospectColumn = prospectTag.closest('[data-testid="board-column"]') as HTMLElement;
203
+ await expect(activeColumn).toBeTruthy();
204
+ await expect(prospectColumn).toBeTruthy();
205
+
206
+ // Wait for cards to render in the columns (Board items use data-testid="board-item").
207
+ const getColumnCards = (column: HTMLElement) =>
208
+ Array.from(column.querySelectorAll('[data-testid="board-item"]')) as HTMLElement[];
209
+
210
+ await waitFor(() => expect(getColumnCards(activeColumn).length).toBeGreaterThan(0));
211
+
212
+ // Verify cards are distributed across columns.
213
+ const activeCards = getColumnCards(activeColumn);
214
+ const prospectCards = getColumnCards(prospectColumn);
215
+ await expect(activeCards.length).toBeGreaterThan(0);
216
+ await expect(prospectCards.length).toBeGreaterThan(0);
217
+
218
+ // Verify cards have drag handles (Card.Toolbar includes drag handle).
219
+ const firstActiveCard = activeCards[0];
220
+ const buttons = firstActiveCard.querySelectorAll('button');
221
+ await expect(buttons.length).toBeGreaterThan(0);
222
+
223
+ // Verify add-card action exists in columns (optional footer).
224
+ const activeAddItem = activeColumn.querySelector('[data-testid="board-column-add-item"]');
225
+ const prospectAddItem = prospectColumn.querySelector('[data-testid="board-column-add-item"]');
226
+ await expect(activeAddItem).toBeTruthy();
227
+ await expect(prospectAddItem).toBeTruthy();
228
+
229
+ // TODO(wittjosiah): Get drag & drop tests working.
230
+ // See packages/apps/composer-app/src/playwright/stack.spec.ts for reference.
231
+ },
232
+ };
233
+
234
+ /**
235
+ * Story variant that uses a mutable database schema (EchoSchema).
236
+ * This allows testing schema mutations like adding/removing fields.
237
+ */
238
+ // TODO(wittjosiah): Card previews (e.g., OrganizationCard) are type-specific and hard-coded.
239
+ // They don't use the projection to determine which fields to display, so deleting a field
240
+ // from the schema won't remove it from the card preview. To fix this, the type-specific
241
+ // cards in PreviewPlugin would need to accept and respect the projection prop.
242
+ export const MutableSchema: Story = {
243
+ decorators: [
244
+ withKanbanPlugins({
245
+ onSpaceCreated: async (space) => {
246
+ // Register schema in the database to make it mutable (EchoSchema).
247
+ const [schema] = await space.db.schemaRegistry.register([Organization.Organization]);
248
+
249
+ const { view } = await ViewModel.makeFromDatabase({
250
+ db: space.db,
251
+ typename: schema.typename,
252
+ pivotFieldName: 'status',
253
+ });
254
+ const kanban = Kanban.make({ view });
255
+ space.db.add(kanban);
256
+
257
+ // Create test data using the registered schema.
258
+ const requiredOrgs = [
259
+ ...Array.from({ length: 2 }, () => createOrg('prospect')),
260
+ ...Array.from({ length: 5 }, () => createOrg('qualified')),
261
+ ...Array.from({ length: 1 }, () => createOrg('active')),
262
+ ...Array.from({ length: 1 }, () => createOrg('commit')),
263
+ ...Array.from({ length: 1 }, () => createOrg('reject')),
264
+ ];
265
+ requiredOrgs.forEach((org) => space.db.add(Obj.make(schema, org)));
266
+ },
267
+ }),
268
+ ],
269
+ };
@@ -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;
@@ -0,0 +1,63 @@
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 ObjectSurfaceProps } 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, SettingsSchema } from '#types';
17
+
18
+ export type KanbanViewEditorProps = ObjectSurfaceProps<Kanban.Kanban>;
19
+
20
+ export const KanbanViewEditor = ({ subject: object }: KanbanViewEditorProps) => {
21
+ const registry = useContext(RegistryContext);
22
+ const db = Obj.getDatabase(object);
23
+ const [view, updateView] = useObject(object.view);
24
+ const currentTypename = view?.query ? getTypenameFromQuery(view.query.ast) : undefined;
25
+ const schema = useSchema(db, currentTypename);
26
+ const projection = useProjectionModel(schema, object, registry);
27
+
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
+ );
36
+
37
+ const handleValuesChanged = useCallback(
38
+ (values: Partial<{ columnFieldId: string }>) => {
39
+ if (values.columnFieldId != null) {
40
+ updateView((view) => {
41
+ view.projection.pivotFieldId = values.columnFieldId;
42
+ });
43
+ }
44
+ },
45
+ [updateView],
46
+ );
47
+
48
+ const initialValues = useMemo(
49
+ () => ({ columnFieldId: view?.projection.pivotFieldId }),
50
+ [view?.projection.pivotFieldId],
51
+ );
52
+
53
+ const fieldMap: FormFieldMap = useMemo(
54
+ () => ({ columnFieldId: (props) => <SelectField {...props} options={selectFields} /> }),
55
+ [selectFields],
56
+ );
57
+
58
+ return (
59
+ <Form.Root schema={SettingsSchema} values={initialValues} fieldMap={fieldMap} onValuesChanged={handleValuesChanged}>
60
+ <Form.FieldSet />
61
+ </Form.Root>
62
+ );
63
+ };
@@ -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]);