@dxos/plugin-kanban 0.6.8-main.046e6cf

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 (74) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +15 -0
  3. package/dist/lib/browser/KanbanMain-OVUL576T.mjs +444 -0
  4. package/dist/lib/browser/KanbanMain-OVUL576T.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-DMDAZVOX.mjs +21 -0
  6. package/dist/lib/browser/chunk-DMDAZVOX.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-LEPZRV4E.mjs +47 -0
  8. package/dist/lib/browser/chunk-LEPZRV4E.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +176 -0
  10. package/dist/lib/browser/index.mjs.map +7 -0
  11. package/dist/lib/browser/meta.json +1 -0
  12. package/dist/lib/browser/meta.mjs +9 -0
  13. package/dist/lib/browser/meta.mjs.map +7 -0
  14. package/dist/lib/browser/types/index.mjs +14 -0
  15. package/dist/lib/browser/types/index.mjs.map +7 -0
  16. package/dist/lib/node/KanbanMain-RSRZLAM5.cjs +453 -0
  17. package/dist/lib/node/KanbanMain-RSRZLAM5.cjs.map +7 -0
  18. package/dist/lib/node/chunk-CJTEPA5Z.cjs +54 -0
  19. package/dist/lib/node/chunk-CJTEPA5Z.cjs.map +7 -0
  20. package/dist/lib/node/chunk-RYK4NJNG.cjs +67 -0
  21. package/dist/lib/node/chunk-RYK4NJNG.cjs.map +7 -0
  22. package/dist/lib/node/index.cjs +192 -0
  23. package/dist/lib/node/index.cjs.map +7 -0
  24. package/dist/lib/node/meta.cjs +30 -0
  25. package/dist/lib/node/meta.cjs.map +7 -0
  26. package/dist/lib/node/meta.json +1 -0
  27. package/dist/lib/node/types/index.cjs +36 -0
  28. package/dist/lib/node/types/index.cjs.map +7 -0
  29. package/dist/types/src/KanbanPlugin.d.ts +4 -0
  30. package/dist/types/src/KanbanPlugin.d.ts.map +1 -0
  31. package/dist/types/src/components/KanbanBoard.d.ts +6 -0
  32. package/dist/types/src/components/KanbanBoard.d.ts.map +1 -0
  33. package/dist/types/src/components/KanbanCard.d.ts +9 -0
  34. package/dist/types/src/components/KanbanCard.d.ts.map +1 -0
  35. package/dist/types/src/components/KanbanColumn.d.ts +14 -0
  36. package/dist/types/src/components/KanbanColumn.d.ts.map +1 -0
  37. package/dist/types/src/components/KanbanMain.d.ts +7 -0
  38. package/dist/types/src/components/KanbanMain.d.ts.map +1 -0
  39. package/dist/types/src/components/index.d.ts +5 -0
  40. package/dist/types/src/components/index.d.ts.map +1 -0
  41. package/dist/types/src/components/util.d.ts +7 -0
  42. package/dist/types/src/components/util.d.ts.map +1 -0
  43. package/dist/types/src/index.d.ts +4 -0
  44. package/dist/types/src/index.d.ts.map +1 -0
  45. package/dist/types/src/meta.d.ts +15 -0
  46. package/dist/types/src/meta.d.ts.map +1 -0
  47. package/dist/types/src/sanity.test.d.ts +2 -0
  48. package/dist/types/src/sanity.test.d.ts.map +1 -0
  49. package/dist/types/src/stories/testing.d.ts +19 -0
  50. package/dist/types/src/stories/testing.d.ts.map +1 -0
  51. package/dist/types/src/translations.d.ts +20 -0
  52. package/dist/types/src/translations.d.ts.map +1 -0
  53. package/dist/types/src/types/index.d.ts +3 -0
  54. package/dist/types/src/types/index.d.ts.map +1 -0
  55. package/dist/types/src/types/kanban.d.ts +76 -0
  56. package/dist/types/src/types/kanban.d.ts.map +1 -0
  57. package/dist/types/src/types/types.d.ts +18 -0
  58. package/dist/types/src/types/types.d.ts.map +1 -0
  59. package/package.json +85 -0
  60. package/src/KanbanPlugin.tsx +112 -0
  61. package/src/components/KanbanBoard.tsx +195 -0
  62. package/src/components/KanbanCard.tsx +82 -0
  63. package/src/components/KanbanColumn.tsx +143 -0
  64. package/src/components/KanbanMain.tsx +37 -0
  65. package/src/components/index.ts +8 -0
  66. package/src/components/util.ts +38 -0
  67. package/src/index.ts +9 -0
  68. package/src/meta.tsx +19 -0
  69. package/src/sanity.test.ts +13 -0
  70. package/src/stories/testing.ts +29 -0
  71. package/src/translations.ts +26 -0
  72. package/src/types/index.ts +6 -0
  73. package/src/types/kanban.ts +22 -0
  74. package/src/types/types.ts +57 -0
@@ -0,0 +1,76 @@
1
+ import { Expando, ref, S } from '@dxos/echo-schema';
2
+ declare const KanbanItemType_base: import("@dxos/echo-schema").AbstractTypedObject<{
3
+ object?: import("@dxos/echo-schema").Ref<Expando>;
4
+ name?: string | undefined;
5
+ index?: string | undefined;
6
+ } & {
7
+ id: string;
8
+ }, S.Struct.Encoded<{
9
+ object: S.optional<ref<Expando>>;
10
+ name: S.optional<typeof S.String>;
11
+ index: S.optional<typeof S.String>;
12
+ }>>;
13
+ export declare class KanbanItemType extends KanbanItemType_base {
14
+ }
15
+ declare const KanbanColumnType_base: import("@dxos/echo-schema").AbstractTypedObject<{
16
+ name?: string | undefined;
17
+ index?: string | undefined;
18
+ items: import("@dxos/echo-schema").Ref<{
19
+ object?: import("@dxos/echo-schema").Ref<Expando>;
20
+ name?: string | undefined;
21
+ index?: string | undefined;
22
+ } & {
23
+ id: string;
24
+ }>[];
25
+ } & {
26
+ id: string;
27
+ }, S.Struct.Encoded<{
28
+ name: S.optional<typeof S.String>;
29
+ index: S.optional<typeof S.String>;
30
+ items: S.mutable<S.Array$<ref<{
31
+ object?: import("@dxos/echo-schema").Ref<Expando>;
32
+ name?: string | undefined;
33
+ index?: string | undefined;
34
+ } & {
35
+ id: string;
36
+ }>>>;
37
+ }>>;
38
+ export declare class KanbanColumnType extends KanbanColumnType_base {
39
+ }
40
+ declare const KanbanType_base: import("@dxos/echo-schema").AbstractTypedObject<{
41
+ name?: string | undefined;
42
+ columns: import("@dxos/echo-schema").Ref<{
43
+ name?: string | undefined;
44
+ index?: string | undefined;
45
+ items: import("@dxos/echo-schema").Ref<{
46
+ object?: import("@dxos/echo-schema").Ref<Expando>;
47
+ name?: string | undefined;
48
+ index?: string | undefined;
49
+ } & {
50
+ id: string;
51
+ }>[];
52
+ } & {
53
+ id: string;
54
+ }>[];
55
+ } & {
56
+ id: string;
57
+ }, S.Struct.Encoded<{
58
+ name: S.optional<typeof S.String>;
59
+ columns: S.mutable<S.Array$<ref<{
60
+ name?: string | undefined;
61
+ index?: string | undefined;
62
+ items: import("@dxos/echo-schema").Ref<{
63
+ object?: import("@dxos/echo-schema").Ref<Expando>;
64
+ name?: string | undefined;
65
+ index?: string | undefined;
66
+ } & {
67
+ id: string;
68
+ }>[];
69
+ } & {
70
+ id: string;
71
+ }>>>;
72
+ }>>;
73
+ export declare class KanbanType extends KanbanType_base {
74
+ }
75
+ export {};
76
+ //# sourceMappingURL=kanban.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kanban.d.ts","sourceRoot":"","sources":["../../../../src/types/kanban.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAe,MAAM,mBAAmB,CAAC;;;;;;;;;;;;AAEjE,qBAAa,cAAe,SAAQ,mBAIlC;CAAG;;;;;;;;;;;;;;;;;;;;;;;;AAEL,qBAAa,gBAAiB,SAAQ,qBAIpC;CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEL,qBAAa,UAAW,SAAQ,eAG9B;CAAG"}
@@ -0,0 +1,18 @@
1
+ import type { GraphBuilderProvides, IntentResolverProvides, MetadataRecordsProvides, SurfaceProvides, TranslationsProvides } from '@dxos/app-framework';
2
+ import { type SchemaProvides } from '@dxos/plugin-client';
3
+ import { type KanbanColumnType, type KanbanItemType, type KanbanType } from './kanban';
4
+ export declare enum KanbanAction {
5
+ CREATE = "dxos.org/plugin/kanban/action/create"
6
+ }
7
+ export type KanbanPluginProvides = SurfaceProvides & IntentResolverProvides & GraphBuilderProvides & MetadataRecordsProvides & TranslationsProvides & SchemaProvides;
8
+ export interface KanbanModel {
9
+ root: KanbanType;
10
+ createColumn(): KanbanColumnType;
11
+ createItem(column: KanbanColumnType): KanbanItemType;
12
+ }
13
+ export type Location = {
14
+ column: KanbanColumnType;
15
+ item?: KanbanItemType;
16
+ idx?: number;
17
+ };
18
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/types/types.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,eAAe,EACf,oBAAoB,EACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAcvF,oBAAY,YAAY;IACtB,MAAM,yCAA4B;CACnC;AAED,MAAM,MAAM,oBAAoB,GAAG,eAAe,GAChD,sBAAsB,GACtB,oBAAoB,GACpB,uBAAuB,GACvB,oBAAoB,GACpB,cAAc,CAAC;AAUjB,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,UAAU,CAAC;IACjB,YAAY,IAAI,gBAAgB,CAAC;IACjC,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,cAAc,CAAC;CACtD;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,gBAAgB,CAAC;IACzB,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC"}
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@dxos/plugin-kanban",
3
+ "version": "0.6.8-main.046e6cf",
4
+ "description": "Kanban DXOS Surface plugin",
5
+ "homepage": "https://dxos.org",
6
+ "bugs": "https://github.com/dxos/dxos/issues",
7
+ "license": "MIT",
8
+ "author": "DXOS.org",
9
+ "exports": {
10
+ ".": {
11
+ "browser": "./dist/lib/browser/index.mjs",
12
+ "node": {
13
+ "default": "./dist/lib/node/index.cjs"
14
+ },
15
+ "types": "./dist/types/src/index.d.ts"
16
+ },
17
+ "./meta": {
18
+ "browser": "./dist/lib/browser/meta.mjs",
19
+ "node": {
20
+ "default": "./dist/lib/node/meta.cjs"
21
+ },
22
+ "types": "./dist/types/src/meta.d.ts"
23
+ },
24
+ "./types": {
25
+ "browser": "./dist/lib/browser/types/index.mjs",
26
+ "node": {
27
+ "default": "./dist/lib/node/types/index.cjs"
28
+ },
29
+ "types": "./dist/types/src/types/index.d.ts"
30
+ }
31
+ },
32
+ "types": "dist/types/src/index.d.ts",
33
+ "typesVersions": {
34
+ "*": {
35
+ "meta": [
36
+ "dist/types/src/meta.d.ts"
37
+ ],
38
+ "types": [
39
+ "dist/types/src/types/index.d.ts"
40
+ ]
41
+ }
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "src"
46
+ ],
47
+ "dependencies": {
48
+ "@dnd-kit/core": "^6.0.5",
49
+ "@dnd-kit/modifiers": "^6.0.0",
50
+ "@dnd-kit/sortable": "^7.0.1",
51
+ "@dnd-kit/utilities": "^3.2.0",
52
+ "@preact/signals-core": "^1.6.0",
53
+ "@dxos/app-framework": "0.6.8-main.046e6cf",
54
+ "@dxos/async": "0.6.8-main.046e6cf",
55
+ "@dxos/echo-schema": "0.6.8-main.046e6cf",
56
+ "@dxos/plugin-client": "0.6.8-main.046e6cf",
57
+ "@dxos/plugin-graph": "0.6.8-main.046e6cf",
58
+ "@dxos/plugin-space": "0.6.8-main.046e6cf",
59
+ "@dxos/react-client": "0.6.8-main.046e6cf",
60
+ "@dxos/util": "0.6.8-main.046e6cf",
61
+ "@dxos/react-ui-editor": "0.6.8-main.046e6cf"
62
+ },
63
+ "devDependencies": {
64
+ "@phosphor-icons/react": "^2.1.5",
65
+ "@types/react": "~18.2.0",
66
+ "@types/react-dom": "~18.2.0",
67
+ "react": "~18.2.0",
68
+ "react-dom": "~18.2.0",
69
+ "vite": "^5.3.4",
70
+ "@dxos/random": "0.6.8-main.046e6cf",
71
+ "@dxos/react-ui-theme": "0.6.8-main.046e6cf",
72
+ "@dxos/storybook-utils": "0.6.8-main.046e6cf",
73
+ "@dxos/react-ui": "0.6.8-main.046e6cf"
74
+ },
75
+ "optionalDependencies": {
76
+ "@phosphor-icons/react": "^2.1.5",
77
+ "react": "^18.0.0",
78
+ "react-dom": "^18.0.0",
79
+ "@dxos/react-ui": "0.6.8-main.046e6cf",
80
+ "@dxos/react-ui-theme": "0.6.8-main.046e6cf"
81
+ },
82
+ "publishConfig": {
83
+ "access": "public"
84
+ }
85
+ }
@@ -0,0 +1,112 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type IconProps, Kanban } from '@phosphor-icons/react';
6
+ import React from 'react';
7
+
8
+ import { resolvePlugin, type PluginDefinition, parseIntentPlugin, NavigationAction } from '@dxos/app-framework';
9
+ import { create } from '@dxos/echo-schema';
10
+ import { parseClientPlugin } from '@dxos/plugin-client';
11
+ import { type ActionGroup, createExtension, isActionGroup } from '@dxos/plugin-graph';
12
+ import { SpaceAction } from '@dxos/plugin-space';
13
+ import { loadObjectReferences } from '@dxos/react-client/echo';
14
+
15
+ import { KanbanMain } from './components';
16
+ import meta, { KANBAN_PLUGIN } from './meta';
17
+ import translations from './translations';
18
+ import { KanbanColumnType, KanbanItemType, KanbanType } from './types';
19
+ import { KanbanAction, type KanbanPluginProvides } from './types';
20
+
21
+ export const KanbanPlugin = (): PluginDefinition<KanbanPluginProvides> => {
22
+ return {
23
+ meta,
24
+ provides: {
25
+ metadata: {
26
+ records: {
27
+ [KanbanType.typename]: {
28
+ placeholder: ['kanban title placeholder', { ns: KANBAN_PLUGIN }],
29
+ icon: (props: IconProps) => <Kanban {...props} />,
30
+ iconSymbol: 'ph--kanban--regular',
31
+ // TODO(wittjosiah): Move out of metadata.
32
+ loadReferences: (kanban: KanbanType) => loadObjectReferences(kanban, (kanban) => kanban.columns),
33
+ },
34
+ [KanbanColumnType.typename]: {
35
+ // TODO(wittjosiah): Move out of metadata.
36
+ loadReferences: (column: KanbanColumnType) => loadObjectReferences(column, (column) => column.items),
37
+ },
38
+ [KanbanItemType.typename]: {
39
+ // TODO(wittjosiah): Move out of metadata.
40
+ loadReferences: (item: KanbanItemType) => [], // loadObjectReferences(item, (item) => item.object),
41
+ },
42
+ },
43
+ },
44
+ echo: {
45
+ schema: [KanbanType, KanbanColumnType, KanbanItemType],
46
+ },
47
+ translations,
48
+ graph: {
49
+ builder: (plugins) => {
50
+ const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
51
+ const dispatch = resolvePlugin(plugins, parseIntentPlugin)?.provides.intent.dispatch;
52
+ if (!client || !dispatch) {
53
+ return [];
54
+ }
55
+
56
+ return createExtension({
57
+ id: KanbanAction.CREATE,
58
+ filter: (node): node is ActionGroup => isActionGroup(node) && node.id.startsWith(SpaceAction.ADD_OBJECT),
59
+ actions: ({ node }) => {
60
+ const id = node.id.split('/').at(-1);
61
+ const [spaceId, objectId] = id?.split(':') ?? [];
62
+ const space = client.spaces.get().find((space) => space.id === spaceId);
63
+ const object = objectId && space?.db.getObjectById(objectId);
64
+ const target = objectId ? object : space;
65
+ if (!target) {
66
+ return;
67
+ }
68
+
69
+ return [
70
+ {
71
+ id: `${KANBAN_PLUGIN}/create/${node.id}`,
72
+ data: async () => {
73
+ await dispatch([
74
+ { plugin: KANBAN_PLUGIN, action: KanbanAction.CREATE },
75
+ { action: SpaceAction.ADD_OBJECT, data: { target } },
76
+ { action: NavigationAction.OPEN },
77
+ ]);
78
+ },
79
+ properties: {
80
+ label: ['create kanban label', { ns: KANBAN_PLUGIN }],
81
+ icon: (props: IconProps) => <Kanban {...props} />,
82
+ iconSymbol: 'ph--kanban--regular',
83
+ testId: 'kanbanPlugin.createObject',
84
+ },
85
+ },
86
+ ];
87
+ },
88
+ });
89
+ },
90
+ },
91
+ surface: {
92
+ component: ({ data, role }) => {
93
+ switch (role) {
94
+ case 'main':
95
+ return data.active instanceof KanbanType ? <KanbanMain kanban={data.active} /> : null;
96
+ default:
97
+ return null;
98
+ }
99
+ },
100
+ },
101
+ intent: {
102
+ resolver: (intent) => {
103
+ switch (intent.action) {
104
+ case KanbanAction.CREATE: {
105
+ return { data: create(KanbanType, { columns: [] }) };
106
+ }
107
+ }
108
+ },
109
+ },
110
+ },
111
+ };
112
+ };
@@ -0,0 +1,195 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import {
6
+ DndContext,
7
+ type DragEndEvent,
8
+ type DragMoveEvent,
9
+ type DragOverEvent,
10
+ DragOverlay,
11
+ type DragStartEvent,
12
+ type Modifier,
13
+ MouseSensor,
14
+ useSensor,
15
+ } from '@dnd-kit/core';
16
+ import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable';
17
+ import React, { type FC, useEffect, useState } from 'react';
18
+
19
+ import { createSubscription } from '@dxos/react-client/echo';
20
+ import { arrayMove, nonNullable } from '@dxos/util';
21
+
22
+ import { KanbanCardComponent } from './KanbanCard';
23
+ import { type ItemsMapper, KanbanColumnComponent, KanbanColumnComponentPlaceholder } from './KanbanColumn';
24
+ import { findLocation, useSubscription } from './util';
25
+ import type { KanbanColumnType, KanbanItemType, Location, KanbanModel } from '../types';
26
+
27
+ // TODO(burdon): Touch sensors.
28
+ // TODO(burdon): Prevent browser nav back when swiping left/right.
29
+ // TODO(burdon): Consistently use FC?
30
+ export const KanbanBoard: FC<{ model: KanbanModel }> = ({ model }) => {
31
+ const kanban = model.root;
32
+ // TODO(wittjosiah): Remove?
33
+ useSubscription(kanban.columns);
34
+
35
+ // TODO(burdon): Remove since now uses ECHO.
36
+ const [_, setIter] = useState([]);
37
+
38
+ useEffect(() => {
39
+ const handle = createSubscription(() => setIter([]));
40
+ handle.update([kanban.columns]);
41
+ return () => handle.unsubscribe();
42
+ }, []);
43
+
44
+ const mouseSensor = useSensor(MouseSensor, {
45
+ activationConstraint: {
46
+ distance: 8,
47
+ },
48
+ });
49
+
50
+ // Dragging column.
51
+ // TODO(burdon): Dragging column causes flickering when dragging left to first column.
52
+ const [draggingColumn, setDraggingColumn] = useState<KanbanColumnType | undefined>();
53
+
54
+ // Dragging item.
55
+ const [draggingItem, setDraggingItem] = useState<{ source: Location; target?: Location }>();
56
+ // While dragging, temporarily remap which items should be visible inside each column.
57
+ const itemMapper: ItemsMapper = (column: string, items: KanbanItemType[]) => {
58
+ const { source, target } = draggingItem ?? {};
59
+ if (source && target) {
60
+ if (source?.column.id !== target?.column.id && (column === source?.column.id || column === target?.column.id)) {
61
+ const modified = [...items];
62
+ if (column === source.column.id) {
63
+ // Temporarily remove from old column.
64
+ modified.splice(source.idx!, 1);
65
+ } else if (column === target.column.id) {
66
+ // Temporarily insert into new column.
67
+ // TODO(burdon): Use ref to track item being temporarily moved.
68
+ modified.splice(target.idx ?? modified.length, 0, source.item!);
69
+ }
70
+
71
+ return modified;
72
+ }
73
+ }
74
+
75
+ return items;
76
+ };
77
+
78
+ const handleDragStart = ({ active }: DragStartEvent) => {
79
+ kanban.columns.filter(nonNullable).forEach((column) => {
80
+ if (column.id === active.id) {
81
+ setDraggingColumn(column);
82
+ } else {
83
+ const idx = column.items.filter(nonNullable).findIndex((item) => item.id === active.id);
84
+ if (idx !== -1) {
85
+ setDraggingItem({ source: { column, item: column.items![idx], idx } });
86
+ }
87
+ }
88
+ });
89
+ };
90
+
91
+ const handleDragMove = (event: DragMoveEvent) => {};
92
+
93
+ const handleDragOver = ({ active, over }: DragOverEvent) => {
94
+ if (draggingItem) {
95
+ const { source } = draggingItem;
96
+ const target = findLocation(kanban.columns.filter(nonNullable), over?.id as string);
97
+ if (active.id !== over?.id) {
98
+ setDraggingItem({ source, target });
99
+ }
100
+ }
101
+ };
102
+
103
+ // TODO(burdon): Call model to update.
104
+ const handleDragEnd = (event: DragEndEvent) => {
105
+ if (draggingColumn) {
106
+ const { active, over } = event;
107
+ const oldIndex = kanban.columns.filter(nonNullable).findIndex((column) => column.id === active.id);
108
+ const newIndex = kanban.columns.filter(nonNullable).findIndex((column) => column.id === over?.id);
109
+ arrayMove(kanban.columns, oldIndex, newIndex);
110
+ } else if (draggingItem) {
111
+ const { source, target } = draggingItem;
112
+ if (source.column.id === target!.column.id) {
113
+ if (target!.idx !== undefined) {
114
+ arrayMove(source.column.items!, source.idx!, target!.idx);
115
+ }
116
+ } else {
117
+ source.column.items!.splice(source.idx!, 1);
118
+ // TODO(burdon): Incorrect position when moving to new column.
119
+ target!.column.items!.splice(target!.idx ?? target!.column.items!.length, 0, source.item!);
120
+ }
121
+ }
122
+
123
+ setDraggingColumn(undefined);
124
+ setDraggingItem(undefined);
125
+ };
126
+
127
+ const handleDragCancel = () => {
128
+ setDraggingColumn(undefined);
129
+ setDraggingItem(undefined);
130
+ };
131
+
132
+ const handleCreateColumn = () => {
133
+ const column = model.createColumn();
134
+ kanban.columns.splice(kanban.columns.length, 0, column);
135
+ };
136
+
137
+ // TODO(burdon): Move to model.
138
+ const handleDeleteColumn = (id: string) => {
139
+ const index = kanban.columns.filter(nonNullable).findIndex((column) => column.id === id);
140
+ if (index >= 0) {
141
+ kanban.columns.splice(index, 1);
142
+ }
143
+ };
144
+
145
+ const customModifier: Modifier = ({ transform }) => {
146
+ if (draggingColumn) {
147
+ return {
148
+ ...transform,
149
+ y: 0,
150
+ };
151
+ } else {
152
+ return transform;
153
+ }
154
+ };
155
+
156
+ return (
157
+ <div className='flex overflow-x-scroll'>
158
+ <div className='flex m-4 space-x-4 snap-x'>
159
+ <DndContext
160
+ sensors={[mouseSensor]}
161
+ modifiers={[customModifier]}
162
+ onDragStart={handleDragStart}
163
+ onDragMove={handleDragMove}
164
+ onDragOver={handleDragOver}
165
+ onDragEnd={handleDragEnd}
166
+ onDragCancel={handleDragCancel}
167
+ >
168
+ <SortableContext
169
+ strategy={horizontalListSortingStrategy}
170
+ items={kanban.columns.filter(nonNullable).map(({ id }) => id!)}
171
+ >
172
+ {kanban.columns.filter(nonNullable).map((column) => (
173
+ <KanbanColumnComponent
174
+ key={column.id}
175
+ column={column}
176
+ itemMapper={itemMapper}
177
+ onCreate={(column: KanbanColumnType) => model.createItem(column)}
178
+ onDelete={() => handleDeleteColumn(column.id!)}
179
+ />
180
+ ))}
181
+ </SortableContext>
182
+
183
+ {/* Overlay required to drag across columns. */}
184
+ {draggingItem && (
185
+ <DragOverlay style={{ margin: 0 }}>
186
+ <KanbanCardComponent item={draggingItem.source.item!} onDelete={() => {}} />
187
+ </DragOverlay>
188
+ )}
189
+
190
+ {handleCreateColumn && <KanbanColumnComponentPlaceholder onAdd={handleCreateColumn} />}
191
+ </DndContext>
192
+ </div>
193
+ </div>
194
+ );
195
+ };
@@ -0,0 +1,82 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { useSortable } from '@dnd-kit/sortable';
6
+ import { CSS } from '@dnd-kit/utilities';
7
+ import { DotsSixVertical, X } from '@phosphor-icons/react';
8
+ import React, { type FC } from 'react';
9
+
10
+ import { createDocAccessor } from '@dxos/react-client/echo';
11
+ import { Button, useThemeContext, useTranslation } from '@dxos/react-ui';
12
+ import {
13
+ createBasicExtensions,
14
+ createDataExtensions,
15
+ createThemeExtensions,
16
+ useTextEditor,
17
+ } from '@dxos/react-ui-editor';
18
+ import { getSize, mx, attentionSurface, focusRing } from '@dxos/react-ui-theme';
19
+
20
+ import { KANBAN_PLUGIN } from '../meta';
21
+ import { type KanbanColumnType, type KanbanItemType } from '../types';
22
+
23
+ const DeleteItem = ({ onClick }: { onClick: () => void }) => {
24
+ const { t } = useTranslation(KANBAN_PLUGIN);
25
+ return (
26
+ <Button variant='ghost' onClick={onClick} classNames='plb-0 pli-0.5 -mlb-1'>
27
+ <span className='sr-only'>{t('delete item label')}</span>
28
+ <X className={getSize(4)} />
29
+ </Button>
30
+ );
31
+ };
32
+
33
+ export const KanbanCardComponent: FC<{
34
+ column?: KanbanColumnType;
35
+ item: KanbanItemType;
36
+ debug?: boolean;
37
+ onDelete?: () => void;
38
+ }> = ({ column, item, debug = false, onDelete }) => {
39
+ const { themeMode } = useThemeContext();
40
+ const { t } = useTranslation(KANBAN_PLUGIN);
41
+ const { isDragging, attributes, listeners, transform, transition, setNodeRef } = useSortable({
42
+ id: item.id,
43
+ data: { type: 'item', column },
44
+ });
45
+ const tx = transform ? Object.assign(transform, { scaleY: 1 }) : null;
46
+
47
+ const { parentRef, focusAttributes } = useTextEditor(
48
+ () => ({
49
+ initialValue: item.name,
50
+ extensions: [
51
+ createDataExtensions({ id: item.id, text: createDocAccessor(item, ['name']) }),
52
+ createBasicExtensions({ placeholder: t('item title placeholder') }),
53
+ createThemeExtensions({ themeMode }),
54
+ ],
55
+ }),
56
+ [item, themeMode],
57
+ );
58
+
59
+ return (
60
+ <div
61
+ ref={setNodeRef}
62
+ style={{ transform: CSS.Transform.toString(tx), transition }}
63
+ className={mx('flex grow', isDragging && 'border border-neutral-400 dark:border-neutral-800')}
64
+ >
65
+ <div className={mx('flex items-start grow p-1', attentionSurface, isDragging && 'opacity-10')}>
66
+ {/* TODO(burdon): Standardize height (and below); e.g., via toolbar. */}
67
+ <button className='flex h-[40px] items-center' {...attributes} {...listeners}>
68
+ <DotsSixVertical className={getSize(5)} />
69
+ </button>
70
+ <div className='flex flex-col grow pt-1'>
71
+ <div {...focusAttributes} className={mx(focusRing, 'rounded-sm p-1')} ref={parentRef} />
72
+ {debug && <div className='text-xs text-red-800'>{item.id.slice(0, 9)}</div>}
73
+ </div>
74
+ {onDelete && (
75
+ <div className='flex h-[40px] items-center'>
76
+ <DeleteItem onClick={onDelete} />
77
+ </div>
78
+ )}
79
+ </div>
80
+ </div>
81
+ );
82
+ };