@btst/stack 1.10.0 → 1.12.0
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.
- package/dist/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@dnd-kit/core/dist/core.esm.cjs +1 -1
- package/dist/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@dnd-kit/core/dist/core.esm.mjs +1 -1
- package/dist/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/sortable/dist/sortable.esm.cjs +77 -0
- package/dist/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/sortable/dist/sortable.esm.mjs +79 -3
- package/dist/node_modules/.pnpm/@radix-ui_react-avatar@1.1.11_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react_850cfbef1935a6e49a6ad6c93c7ca70d/node_modules/@radix-ui/react-avatar/dist/index.cjs +140 -0
- package/dist/node_modules/.pnpm/@radix-ui_react-avatar@1.1.11_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react_850cfbef1935a6e49a6ad6c93c7ca70d/node_modules/@radix-ui/react-avatar/dist/index.mjs +119 -0
- package/dist/node_modules/.pnpm/@radix-ui_react-context@1.1.3_@types_react@19.2.6_react@19.2.0/node_modules/@radix-ui/react-context/dist/index.cjs +80 -0
- package/dist/node_modules/.pnpm/@radix-ui_react-context@1.1.3_@types_react@19.2.6_react@19.2.0/node_modules/@radix-ui/react-context/dist/index.mjs +64 -0
- package/dist/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.2.6_react@19.2.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.cjs +18 -0
- package/dist/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.2.6_react@19.2.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.mjs +16 -0
- package/dist/packages/better-stack/src/plugins/kanban/api/plugin.cjs +846 -0
- package/dist/packages/better-stack/src/plugins/kanban/api/plugin.mjs +844 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/board-form.cjs +85 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/board-form.mjs +83 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/column-form.cjs +72 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/column-form.mjs +70 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/task-form.cjs +200 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/task-form.mjs +198 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/loading/board-skeleton.cjs +47 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/loading/board-skeleton.mjs +45 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/loading/boards-list-skeleton.cjs +30 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/loading/boards-list-skeleton.mjs +28 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/404-page.cjs +27 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/404-page.mjs +25 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/board-page.cjs +31 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/board-page.internal.cjs +458 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/board-page.internal.mjs +456 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/board-page.mjs +29 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/boards-list-page.cjs +30 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/boards-list-page.internal.cjs +72 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/boards-list-page.internal.mjs +70 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/boards-list-page.mjs +28 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/new-board-page.cjs +30 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/new-board-page.internal.cjs +51 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/new-board-page.internal.mjs +49 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/new-board-page.mjs +28 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/column-content.cjs +76 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/column-content.mjs +74 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/default-error.cjs +27 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/default-error.mjs +25 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/empty-state.cjs +32 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/empty-state.mjs +30 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/kanban-board.cjs +78 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/kanban-board.mjs +76 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/page-wrapper.cjs +15 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/page-wrapper.mjs +13 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/task-card.cjs +68 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/task-card.mjs +66 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/user-avatar.cjs +32 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/user-avatar.mjs +30 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/hooks/kanban-hooks.cjs +391 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/hooks/kanban-hooks.mjs +381 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/plugin.cjs +290 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/plugin.mjs +288 -0
- package/dist/packages/better-stack/src/plugins/kanban/db.cjs +125 -0
- package/dist/packages/better-stack/src/plugins/kanban/db.mjs +123 -0
- package/dist/packages/better-stack/src/plugins/kanban/schemas.cjs +117 -0
- package/dist/packages/better-stack/src/plugins/kanban/schemas.mjs +102 -0
- package/dist/packages/better-stack/src/plugins/kanban/utils.cjs +49 -0
- package/dist/packages/better-stack/src/plugins/kanban/utils.mjs +45 -0
- package/dist/packages/ui/src/components/avatar.cjs +58 -0
- package/dist/packages/ui/src/components/avatar.mjs +54 -0
- package/dist/packages/ui/src/components/command.cjs +3 -3
- package/dist/packages/ui/src/components/command.mjs +3 -3
- package/dist/packages/ui/src/components/form-builder/index.mjs +2 -2
- package/dist/packages/ui/src/components/kanban.cjs +835 -0
- package/dist/packages/ui/src/components/kanban.mjs +805 -0
- package/dist/packages/ui/src/components/minimal-tiptap/utils.cjs +15 -11
- package/dist/packages/ui/src/components/minimal-tiptap/utils.mjs +15 -11
- package/dist/packages/ui/src/components/popover.cjs +8 -3
- package/dist/packages/ui/src/components/popover.mjs +9 -4
- package/dist/packages/ui/src/components/search-select.cjs +75 -0
- package/dist/packages/ui/src/components/search-select.mjs +73 -0
- package/dist/packages/ui/src/components/ui-builder/index.cjs +9 -7
- package/dist/packages/ui/src/components/ui-builder/index.mjs +9 -7
- package/dist/packages/ui/src/components/ui-builder/internal/canvas/auto-frame.cjs +6 -3
- package/dist/packages/ui/src/components/ui-builder/internal/canvas/auto-frame.mjs +6 -3
- package/dist/packages/ui/src/components/ui-builder/internal/components/add-component-popover.cjs +228 -48
- package/dist/packages/ui/src/components/ui-builder/internal/components/add-component-popover.mjs +228 -48
- package/dist/packages/ui/src/components/ui-builder/internal/components/element-selector.cjs +1 -1
- package/dist/packages/ui/src/components/ui-builder/internal/components/element-selector.mjs +1 -1
- package/dist/packages/ui/src/components/ui-builder/internal/components/error-fallback.cjs +4 -2
- package/dist/packages/ui/src/components/ui-builder/internal/components/error-fallback.mjs +4 -2
- package/dist/packages/ui/src/components/ui-builder/internal/components/multi-select.cjs +6 -3
- package/dist/packages/ui/src/components/ui-builder/internal/components/multi-select.mjs +6 -3
- package/dist/packages/ui/src/components/ui-builder/internal/dnd/draggable-new-component.cjs +67 -0
- package/dist/packages/ui/src/components/ui-builder/internal/dnd/draggable-new-component.mjs +62 -0
- package/dist/packages/ui/src/components/ui-builder/internal/dnd/drop-zone.cjs +181 -37
- package/dist/packages/ui/src/components/ui-builder/internal/dnd/drop-zone.mjs +181 -38
- package/dist/packages/ui/src/components/ui-builder/internal/editor-panel.cjs +1 -1
- package/dist/packages/ui/src/components/ui-builder/internal/editor-panel.mjs +1 -1
- package/dist/packages/ui/src/components/ui-builder/internal/form-fields/classname-control/classname-group-control.cjs +1 -1
- package/dist/packages/ui/src/components/ui-builder/internal/form-fields/classname-control/classname-group-control.mjs +1 -1
- package/dist/packages/ui/src/components/ui-builder/internal/form-fields/classname-control/classname-item-control.cjs +9 -2
- package/dist/packages/ui/src/components/ui-builder/internal/form-fields/classname-control/classname-item-control.mjs +9 -2
- package/dist/packages/ui/src/components/ui-builder/internal/form-fields/iconname-field.cjs +3 -2
- package/dist/packages/ui/src/components/ui-builder/internal/form-fields/iconname-field.mjs +3 -2
- package/dist/packages/ui/src/components/ui-builder/internal/layers-panel.cjs +1 -1
- package/dist/packages/ui/src/components/ui-builder/internal/layers-panel.mjs +1 -1
- package/dist/packages/ui/src/components/ui-builder/internal/props-panel.cjs +17 -5
- package/dist/packages/ui/src/components/ui-builder/internal/props-panel.mjs +17 -5
- package/dist/packages/ui/src/components/ui-builder/internal/utils/render-utils.cjs +70 -16
- package/dist/packages/ui/src/components/ui-builder/internal/utils/render-utils.mjs +73 -20
- package/dist/packages/ui/src/lib/compose-refs.cjs +56 -0
- package/dist/packages/ui/src/lib/compose-refs.mjs +39 -0
- package/dist/packages/ui/src/lib/ui-builder/context/dnd-context-colission-utils.cjs +14 -9
- package/dist/packages/ui/src/lib/ui-builder/context/dnd-context-colission-utils.mjs +14 -9
- package/dist/packages/ui/src/lib/ui-builder/context/dnd-context.cjs +38 -10
- package/dist/packages/ui/src/lib/ui-builder/context/dnd-context.mjs +35 -11
- package/dist/packages/ui/src/lib/ui-builder/context/dnd-contexts.cjs +1 -0
- package/dist/packages/ui/src/lib/ui-builder/context/dnd-contexts.mjs +1 -0
- package/dist/packages/ui/src/lib/ui-builder/context/drag-overlay.cjs +7 -4
- package/dist/packages/ui/src/lib/ui-builder/context/drag-overlay.mjs +7 -4
- package/dist/packages/ui/src/lib/ui-builder/hooks/use-auto-scroll.cjs +4 -4
- package/dist/packages/ui/src/lib/ui-builder/hooks/use-auto-scroll.mjs +4 -4
- package/dist/packages/ui/src/lib/ui-builder/hooks/use-dnd-event-handlers.cjs +53 -16
- package/dist/packages/ui/src/lib/ui-builder/hooks/use-dnd-event-handlers.mjs +53 -16
- package/dist/packages/ui/src/lib/ui-builder/hooks/use-drop-validation.cjs +23 -7
- package/dist/packages/ui/src/lib/ui-builder/hooks/use-drop-validation.mjs +23 -7
- package/dist/packages/ui/src/lib/ui-builder/registry/form-field-overrides.cjs +110 -11
- package/dist/packages/ui/src/lib/ui-builder/registry/form-field-overrides.mjs +111 -13
- package/dist/packages/ui/src/lib/ui-builder/store/editor-store.cjs +3 -2
- package/dist/packages/ui/src/lib/ui-builder/store/editor-store.mjs +3 -2
- package/dist/packages/ui/src/lib/ui-builder/store/layer-store.cjs +53 -7
- package/dist/packages/ui/src/lib/ui-builder/store/layer-store.mjs +54 -8
- package/dist/packages/ui/src/lib/ui-builder/store/layer-utils.cjs +4 -3
- package/dist/packages/ui/src/lib/ui-builder/store/layer-utils.mjs +4 -3
- package/dist/packages/ui/src/lib/ui-builder/utils/variable-resolver.cjs +12 -0
- package/dist/packages/ui/src/lib/ui-builder/utils/variable-resolver.mjs +12 -1
- package/dist/plugins/blog/api/index.d.cts +1 -1
- package/dist/plugins/blog/api/index.d.mts +1 -1
- package/dist/plugins/blog/api/index.d.ts +1 -1
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +1 -1
- package/dist/plugins/blog/client/index.d.mts +1 -1
- package/dist/plugins/blog/client/index.d.ts +1 -1
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- package/dist/plugins/kanban/api/index.cjs +7 -0
- package/dist/plugins/kanban/api/index.d.cts +403 -0
- package/dist/plugins/kanban/api/index.d.mts +403 -0
- package/dist/plugins/kanban/api/index.d.ts +403 -0
- package/dist/plugins/kanban/api/index.mjs +1 -0
- package/dist/plugins/kanban/client/components/index.cjs +35 -0
- package/dist/plugins/kanban/client/components/index.d.cts +102 -0
- package/dist/plugins/kanban/client/components/index.d.mts +102 -0
- package/dist/plugins/kanban/client/components/index.d.ts +102 -0
- package/dist/plugins/kanban/client/components/index.mjs +15 -0
- package/dist/plugins/kanban/client/hooks/index.cjs +15 -0
- package/dist/plugins/kanban/client/hooks/index.d.cts +143 -0
- package/dist/plugins/kanban/client/hooks/index.d.mts +143 -0
- package/dist/plugins/kanban/client/hooks/index.d.ts +143 -0
- package/dist/plugins/kanban/client/hooks/index.mjs +1 -0
- package/dist/plugins/kanban/client/index.cjs +7 -0
- package/dist/plugins/kanban/client/index.d.cts +196 -0
- package/dist/plugins/kanban/client/index.d.mts +196 -0
- package/dist/plugins/kanban/client/index.d.ts +196 -0
- package/dist/plugins/kanban/client/index.mjs +1 -0
- package/dist/plugins/kanban/client.css +68 -0
- package/dist/plugins/kanban/query-keys.cjs +105 -0
- package/dist/plugins/kanban/query-keys.d.cts +59 -0
- package/dist/plugins/kanban/query-keys.d.mts +59 -0
- package/dist/plugins/kanban/query-keys.d.ts +59 -0
- package/dist/plugins/kanban/query-keys.mjs +103 -0
- package/dist/plugins/kanban/style.css +7 -0
- package/dist/plugins/ui-builder/client/components/index.d.cts +1 -1
- package/dist/plugins/ui-builder/client/components/index.d.mts +1 -1
- package/dist/plugins/ui-builder/client/components/index.d.ts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.cts +2 -2
- package/dist/plugins/ui-builder/client/hooks/index.d.mts +2 -2
- package/dist/plugins/ui-builder/client/hooks/index.d.ts +2 -2
- package/dist/plugins/ui-builder/client/index.d.cts +17 -7
- package/dist/plugins/ui-builder/client/index.d.mts +17 -7
- package/dist/plugins/ui-builder/client/index.d.ts +17 -7
- package/dist/plugins/ui-builder/index.d.cts +2 -2
- package/dist/plugins/ui-builder/index.d.mts +2 -2
- package/dist/plugins/ui-builder/index.d.ts +2 -2
- package/dist/plugins/ui-builder/style.css +6 -0
- package/dist/shared/{stack.BSM2cgoq.d.cts → stack.BYysGdHl.d.cts} +1 -1
- package/dist/shared/{stack.CqfZWfjJ.d.cts → stack.BdJFrdyt.d.cts} +8 -2
- package/dist/shared/{stack.e1FN86dE.d.mts → stack.ChVuHi5e.d.mts} +8 -2
- package/dist/shared/stack.DKDMI-QO.d.cts +70 -0
- package/dist/shared/stack.DKDMI-QO.d.mts +70 -0
- package/dist/shared/stack.DKDMI-QO.d.ts +70 -0
- package/dist/shared/{stack.CLtOoAqF.d.mts → stack.DYCFcnkL.d.mts} +1 -1
- package/dist/shared/{stack.MMntCVZZ.d.ts → stack.EhM4pmtN.d.ts} +8 -2
- package/dist/shared/stack.FeaWkglm.d.cts +190 -0
- package/dist/shared/stack.FeaWkglm.d.mts +190 -0
- package/dist/shared/stack.FeaWkglm.d.ts +190 -0
- package/dist/shared/{stack.BD1m-4yB.d.ts → stack.kFbDspnF.d.ts} +1 -1
- package/package.json +56 -2
- package/src/plugins/kanban/api/index.ts +6 -0
- package/src/plugins/kanban/api/plugin.ts +1245 -0
- package/src/plugins/kanban/client/components/forms/board-form.tsx +108 -0
- package/src/plugins/kanban/client/components/forms/column-form.tsx +97 -0
- package/src/plugins/kanban/client/components/forms/task-form.tsx +274 -0
- package/src/plugins/kanban/client/components/index.tsx +21 -0
- package/src/plugins/kanban/client/components/loading/board-skeleton.tsx +49 -0
- package/src/plugins/kanban/client/components/loading/boards-list-skeleton.tsx +34 -0
- package/src/plugins/kanban/client/components/loading/index.tsx +2 -0
- package/src/plugins/kanban/client/components/pages/404-page.tsx +28 -0
- package/src/plugins/kanban/client/components/pages/board-page.internal.tsx +575 -0
- package/src/plugins/kanban/client/components/pages/board-page.tsx +31 -0
- package/src/plugins/kanban/client/components/pages/boards-list-page.internal.tsx +101 -0
- package/src/plugins/kanban/client/components/pages/boards-list-page.tsx +26 -0
- package/src/plugins/kanban/client/components/pages/new-board-page.internal.tsx +65 -0
- package/src/plugins/kanban/client/components/pages/new-board-page.tsx +26 -0
- package/src/plugins/kanban/client/components/shared/column-content.tsx +108 -0
- package/src/plugins/kanban/client/components/shared/default-error.tsx +32 -0
- package/src/plugins/kanban/client/components/shared/empty-state.tsx +37 -0
- package/src/plugins/kanban/client/components/shared/kanban-board.tsx +87 -0
- package/src/plugins/kanban/client/components/shared/page-wrapper.tsx +20 -0
- package/src/plugins/kanban/client/components/shared/task-card.tsx +79 -0
- package/src/plugins/kanban/client/components/shared/user-avatar.tsx +63 -0
- package/src/plugins/kanban/client/hooks/index.tsx +11 -0
- package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +560 -0
- package/src/plugins/kanban/client/index.ts +8 -0
- package/src/plugins/kanban/client/localization/index.ts +28 -0
- package/src/plugins/kanban/client/localization/kanban-common.ts +69 -0
- package/src/plugins/kanban/client/localization/kanban-forms.ts +70 -0
- package/src/plugins/kanban/client/localization/kanban-list.ts +36 -0
- package/src/plugins/kanban/client/overrides.ts +145 -0
- package/src/plugins/kanban/client/plugin.tsx +463 -0
- package/src/plugins/kanban/client.css +68 -0
- package/src/plugins/kanban/db.ts +125 -0
- package/src/plugins/kanban/query-keys.ts +154 -0
- package/src/plugins/kanban/schemas.ts +143 -0
- package/src/plugins/kanban/style.css +7 -0
- package/src/plugins/kanban/types.ts +106 -0
- package/src/plugins/kanban/utils.ts +107 -0
- package/src/plugins/ui-builder/style.css +6 -0
- package/dist/shared/{stack.DLhzx1-D.d.mts → stack.CcI4sYJP.d.cts} +1 -1
- package/dist/shared/{stack.DLhzx1-D.d.ts → stack.CcI4sYJP.d.mts} +1 -1
- package/dist/shared/{stack.DLhzx1-D.d.cts → stack.CcI4sYJP.d.ts} +1 -1
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useMemo, useEffect } from "react";
|
|
4
|
+
import { ArrowLeft, Plus, Settings, Trash2, Pencil } from "lucide-react";
|
|
5
|
+
import { toast } from "sonner";
|
|
6
|
+
import { Button } from "@workspace/ui/components/button";
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuSeparator,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from "@workspace/ui/components/dropdown-menu";
|
|
14
|
+
import {
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogContent,
|
|
17
|
+
DialogDescription,
|
|
18
|
+
DialogHeader,
|
|
19
|
+
DialogTitle,
|
|
20
|
+
} from "@workspace/ui/components/dialog";
|
|
21
|
+
import {
|
|
22
|
+
AlertDialog,
|
|
23
|
+
AlertDialogAction,
|
|
24
|
+
AlertDialogCancel,
|
|
25
|
+
AlertDialogContent,
|
|
26
|
+
AlertDialogDescription,
|
|
27
|
+
AlertDialogFooter,
|
|
28
|
+
AlertDialogHeader,
|
|
29
|
+
AlertDialogTitle,
|
|
30
|
+
} from "@workspace/ui/components/alert-dialog";
|
|
31
|
+
import {
|
|
32
|
+
useSuspenseBoard,
|
|
33
|
+
useBoardMutations,
|
|
34
|
+
useColumnMutations,
|
|
35
|
+
useTaskMutations,
|
|
36
|
+
} from "../../hooks/kanban-hooks";
|
|
37
|
+
import { usePluginOverrides } from "@btst/stack/context";
|
|
38
|
+
import type { KanbanPluginOverrides } from "../../overrides";
|
|
39
|
+
import { KanbanBoard } from "../shared/kanban-board";
|
|
40
|
+
import { ColumnForm } from "../forms/column-form";
|
|
41
|
+
import { BoardForm } from "../forms/board-form";
|
|
42
|
+
import { TaskForm } from "../forms/task-form";
|
|
43
|
+
import { PageWrapper } from "../shared/page-wrapper";
|
|
44
|
+
import { EmptyState } from "../shared/empty-state";
|
|
45
|
+
import type { SerializedTask, SerializedColumn } from "../../../types";
|
|
46
|
+
|
|
47
|
+
interface BoardPageProps {
|
|
48
|
+
boardId: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type ModalState =
|
|
52
|
+
| { type: "none" }
|
|
53
|
+
| { type: "addColumn" }
|
|
54
|
+
| { type: "editColumn"; columnId: string }
|
|
55
|
+
| { type: "deleteColumn"; columnId: string }
|
|
56
|
+
| { type: "editBoard" }
|
|
57
|
+
| { type: "deleteBoard" }
|
|
58
|
+
| { type: "addTask"; columnId: string }
|
|
59
|
+
| { type: "editTask"; columnId: string; taskId: string };
|
|
60
|
+
|
|
61
|
+
export function BoardPage({ boardId }: BoardPageProps) {
|
|
62
|
+
const { data: board, error, refetch, isFetching } = useSuspenseBoard(boardId);
|
|
63
|
+
|
|
64
|
+
// Suspense hooks only throw on initial fetch, not refetch failures
|
|
65
|
+
if (error && !isFetching) {
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { Link: OverrideLink, navigate: overrideNavigate } =
|
|
70
|
+
usePluginOverrides<KanbanPluginOverrides>("kanban");
|
|
71
|
+
const navigate =
|
|
72
|
+
overrideNavigate ||
|
|
73
|
+
((path: string) => {
|
|
74
|
+
window.location.href = path;
|
|
75
|
+
});
|
|
76
|
+
const Link = OverrideLink || "a";
|
|
77
|
+
|
|
78
|
+
const { deleteBoard, isDeleting } = useBoardMutations();
|
|
79
|
+
const { deleteColumn, reorderColumns } = useColumnMutations();
|
|
80
|
+
const { deleteTask, moveTask, reorderTasks } = useTaskMutations();
|
|
81
|
+
|
|
82
|
+
const [modalState, setModalState] = useState<ModalState>({ type: "none" });
|
|
83
|
+
|
|
84
|
+
// Helper function to convert board columns to kanban state format
|
|
85
|
+
const computeKanbanData = useCallback(
|
|
86
|
+
(
|
|
87
|
+
columns: SerializedColumn[] | undefined,
|
|
88
|
+
): Record<string, SerializedTask[]> => {
|
|
89
|
+
if (!columns) return {};
|
|
90
|
+
return columns.reduce(
|
|
91
|
+
(acc, column) => {
|
|
92
|
+
acc[column.id] = column.tasks || [];
|
|
93
|
+
return acc;
|
|
94
|
+
},
|
|
95
|
+
{} as Record<string, SerializedTask[]>,
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
[],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Initialize kanbanState with data from board to avoid flash of empty state
|
|
102
|
+
// Using lazy initializer ensures we have the correct state on first render
|
|
103
|
+
const [kanbanState, setKanbanState] = useState<
|
|
104
|
+
Record<string, SerializedTask[]>
|
|
105
|
+
>(() => computeKanbanData(board?.columns));
|
|
106
|
+
|
|
107
|
+
// Keep kanbanState in sync when server data changes (e.g., after refetch)
|
|
108
|
+
const serverKanbanData = useMemo(
|
|
109
|
+
() => computeKanbanData(board?.columns),
|
|
110
|
+
[board?.columns, computeKanbanData],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
setKanbanState(serverKanbanData);
|
|
115
|
+
}, [serverKanbanData]);
|
|
116
|
+
|
|
117
|
+
const closeModal = useCallback(() => {
|
|
118
|
+
setModalState({ type: "none" });
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
const handleDeleteBoard = useCallback(async () => {
|
|
122
|
+
try {
|
|
123
|
+
await deleteBoard(boardId);
|
|
124
|
+
closeModal();
|
|
125
|
+
// Use both navigate and a fallback to ensure navigation works
|
|
126
|
+
// Some frameworks may have issues with router.push after mutations
|
|
127
|
+
navigate("/pages/kanban");
|
|
128
|
+
// Fallback: if navigate doesn't work, use window.location
|
|
129
|
+
if (typeof window !== "undefined") {
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
// Only redirect if we're still on the same page after 100ms
|
|
132
|
+
if (window.location.pathname.includes(boardId)) {
|
|
133
|
+
window.location.href = "/pages/kanban";
|
|
134
|
+
}
|
|
135
|
+
}, 100);
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const message =
|
|
139
|
+
error instanceof Error ? error.message : "Failed to delete board";
|
|
140
|
+
toast.error(message);
|
|
141
|
+
}
|
|
142
|
+
}, [deleteBoard, boardId, navigate, closeModal]);
|
|
143
|
+
|
|
144
|
+
const handleKanbanChange = useCallback(
|
|
145
|
+
async (newData: Record<string, SerializedTask[]>) => {
|
|
146
|
+
if (!board) return;
|
|
147
|
+
|
|
148
|
+
// Capture current state for change detection
|
|
149
|
+
// Note: We use a functional update to get the actual current state,
|
|
150
|
+
// avoiding stale closure issues with rapid successive operations
|
|
151
|
+
let previousState: Record<string, SerializedTask[]> = {};
|
|
152
|
+
setKanbanState((current) => {
|
|
153
|
+
previousState = current;
|
|
154
|
+
return newData;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Detect column reorder
|
|
159
|
+
const oldKeys = Object.keys(previousState);
|
|
160
|
+
const newKeys = Object.keys(newData);
|
|
161
|
+
const isColumnMove =
|
|
162
|
+
oldKeys.length === newKeys.length &&
|
|
163
|
+
oldKeys.join("") !== newKeys.join("");
|
|
164
|
+
|
|
165
|
+
if (isColumnMove) {
|
|
166
|
+
// Column reorder - use atomic batch endpoint with transaction support
|
|
167
|
+
await reorderColumns(board.id, newKeys);
|
|
168
|
+
} else {
|
|
169
|
+
// Task changes - detect cross-column moves and within-column reorders
|
|
170
|
+
const crossColumnMoves: Array<{
|
|
171
|
+
taskId: string;
|
|
172
|
+
targetColumnId: string;
|
|
173
|
+
targetOrder: number;
|
|
174
|
+
}> = [];
|
|
175
|
+
const columnsToReorder: Map<string, string[]> = new Map();
|
|
176
|
+
const targetColumnsOfCrossMove = new Set<string>();
|
|
177
|
+
|
|
178
|
+
for (const [columnId, tasks] of Object.entries(newData)) {
|
|
179
|
+
const oldTasks = previousState[columnId] || [];
|
|
180
|
+
let hasOrderChanges = false;
|
|
181
|
+
|
|
182
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
183
|
+
const task = tasks[i];
|
|
184
|
+
if (!task) continue;
|
|
185
|
+
|
|
186
|
+
if (task.columnId !== columnId) {
|
|
187
|
+
// Task moved from another column - needs cross-column move
|
|
188
|
+
crossColumnMoves.push({
|
|
189
|
+
taskId: task.id,
|
|
190
|
+
targetColumnId: columnId,
|
|
191
|
+
targetOrder: i,
|
|
192
|
+
});
|
|
193
|
+
targetColumnsOfCrossMove.add(columnId);
|
|
194
|
+
} else if (task.order !== i) {
|
|
195
|
+
// Task order changed within same column
|
|
196
|
+
hasOrderChanges = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check if tasks were removed from this column (moved elsewhere)
|
|
201
|
+
const newTaskIds = new Set(tasks.map((t) => t.id));
|
|
202
|
+
const tasksRemoved = oldTasks.some((t) => !newTaskIds.has(t.id));
|
|
203
|
+
|
|
204
|
+
// If order changes within column (not a target of cross-column move),
|
|
205
|
+
// use atomic reorder
|
|
206
|
+
if (
|
|
207
|
+
hasOrderChanges &&
|
|
208
|
+
!targetColumnsOfCrossMove.has(columnId) &&
|
|
209
|
+
!tasksRemoved
|
|
210
|
+
) {
|
|
211
|
+
columnsToReorder.set(
|
|
212
|
+
columnId,
|
|
213
|
+
tasks.map((t) => t.id),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Handle cross-column moves first (these need individual moveTask calls)
|
|
219
|
+
for (const move of crossColumnMoves) {
|
|
220
|
+
await moveTask(move.taskId, move.targetColumnId, move.targetOrder);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Then handle within-column reorders atomically
|
|
224
|
+
for (const [columnId, taskIds] of columnsToReorder) {
|
|
225
|
+
await reorderTasks(columnId, taskIds);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Reorder target columns of cross-column moves to fix order collisions
|
|
229
|
+
// The moveTask only sets the moved task's order, so other tasks need reordering
|
|
230
|
+
for (const targetColumnId of targetColumnsOfCrossMove) {
|
|
231
|
+
const tasks = newData[targetColumnId];
|
|
232
|
+
if (tasks) {
|
|
233
|
+
await reorderTasks(
|
|
234
|
+
targetColumnId,
|
|
235
|
+
tasks.map((t) => t.id),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Sync with server after successful mutations
|
|
242
|
+
refetch();
|
|
243
|
+
} catch (error) {
|
|
244
|
+
// On error, refetch from server to get the authoritative state.
|
|
245
|
+
// We avoid manual rollback to previousState because with rapid successive
|
|
246
|
+
// operations, the captured previousState may be stale - a later operation
|
|
247
|
+
// may have already updated the state, and reverting would incorrectly
|
|
248
|
+
// undo that operation too. The server is the source of truth.
|
|
249
|
+
refetch();
|
|
250
|
+
// Re-throw so error boundaries or toast handlers can catch it
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
[board, reorderColumns, moveTask, reorderTasks, refetch],
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const orderedColumns = useMemo(() => {
|
|
258
|
+
if (!board?.columns) return [];
|
|
259
|
+
const columnMap = new Map(board.columns.map((c) => [c.id, c]));
|
|
260
|
+
return Object.keys(kanbanState)
|
|
261
|
+
.map((columnId) => {
|
|
262
|
+
const column = columnMap.get(columnId);
|
|
263
|
+
if (!column) return null;
|
|
264
|
+
return {
|
|
265
|
+
...column,
|
|
266
|
+
tasks: kanbanState[columnId] || [],
|
|
267
|
+
};
|
|
268
|
+
})
|
|
269
|
+
.filter(
|
|
270
|
+
(c): c is SerializedColumn & { tasks: SerializedTask[] } => c !== null,
|
|
271
|
+
);
|
|
272
|
+
}, [board?.columns, kanbanState]);
|
|
273
|
+
|
|
274
|
+
// Board not found - only shown after data has loaded (not during loading)
|
|
275
|
+
if (!board) {
|
|
276
|
+
return (
|
|
277
|
+
<EmptyState
|
|
278
|
+
title="Board not found"
|
|
279
|
+
description="The board you're looking for doesn't exist or you don't have access to it."
|
|
280
|
+
action={
|
|
281
|
+
<Button onClick={() => navigate("/pages/kanban")}>
|
|
282
|
+
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
283
|
+
Back to Boards
|
|
284
|
+
</Button>
|
|
285
|
+
}
|
|
286
|
+
/>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<PageWrapper
|
|
292
|
+
data-testid="board-page"
|
|
293
|
+
className="flex flex-col items-center"
|
|
294
|
+
>
|
|
295
|
+
<div className="w-full flex items-center justify-between mb-8">
|
|
296
|
+
<div className="flex items-center gap-4">
|
|
297
|
+
<Link
|
|
298
|
+
href="/pages/kanban"
|
|
299
|
+
className="text-muted-foreground hover:text-foreground"
|
|
300
|
+
>
|
|
301
|
+
<ArrowLeft className="h-5 w-5" />
|
|
302
|
+
</Link>
|
|
303
|
+
<div>
|
|
304
|
+
<h1 className="text-3xl font-bold" data-testid="page-header">
|
|
305
|
+
{board.name}
|
|
306
|
+
</h1>
|
|
307
|
+
{board.description && (
|
|
308
|
+
<p className="text-muted-foreground mt-1">{board.description}</p>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
<DropdownMenu>
|
|
313
|
+
<DropdownMenuTrigger asChild>
|
|
314
|
+
<Button variant="outline">
|
|
315
|
+
<Settings className="mr-2 h-4 w-4" />
|
|
316
|
+
Actions
|
|
317
|
+
</Button>
|
|
318
|
+
</DropdownMenuTrigger>
|
|
319
|
+
<DropdownMenuContent align="end">
|
|
320
|
+
<DropdownMenuItem
|
|
321
|
+
onClick={() => setModalState({ type: "addColumn" })}
|
|
322
|
+
>
|
|
323
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
324
|
+
Add Column
|
|
325
|
+
</DropdownMenuItem>
|
|
326
|
+
<DropdownMenuItem
|
|
327
|
+
onClick={() => setModalState({ type: "editBoard" })}
|
|
328
|
+
>
|
|
329
|
+
<Pencil className="mr-2 h-4 w-4" />
|
|
330
|
+
Edit Board
|
|
331
|
+
</DropdownMenuItem>
|
|
332
|
+
<DropdownMenuSeparator />
|
|
333
|
+
<DropdownMenuItem
|
|
334
|
+
onClick={() => setModalState({ type: "deleteBoard" })}
|
|
335
|
+
className="text-red-600 focus:text-red-600"
|
|
336
|
+
>
|
|
337
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
338
|
+
Delete Board
|
|
339
|
+
</DropdownMenuItem>
|
|
340
|
+
</DropdownMenuContent>
|
|
341
|
+
</DropdownMenu>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{orderedColumns.length > 0 ? (
|
|
345
|
+
<KanbanBoard
|
|
346
|
+
columns={orderedColumns}
|
|
347
|
+
kanbanState={kanbanState}
|
|
348
|
+
onKanbanChange={handleKanbanChange}
|
|
349
|
+
onAddTask={(columnId) => setModalState({ type: "addTask", columnId })}
|
|
350
|
+
onEditTask={(columnId, taskId) =>
|
|
351
|
+
setModalState({ type: "editTask", columnId, taskId })
|
|
352
|
+
}
|
|
353
|
+
onEditColumn={(columnId) =>
|
|
354
|
+
setModalState({ type: "editColumn", columnId })
|
|
355
|
+
}
|
|
356
|
+
onDeleteColumn={(columnId) =>
|
|
357
|
+
setModalState({ type: "deleteColumn", columnId })
|
|
358
|
+
}
|
|
359
|
+
/>
|
|
360
|
+
) : (
|
|
361
|
+
<EmptyState
|
|
362
|
+
title="No columns yet"
|
|
363
|
+
description="Create your first column to start organizing tasks."
|
|
364
|
+
action={
|
|
365
|
+
<Button onClick={() => setModalState({ type: "addColumn" })}>
|
|
366
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
367
|
+
Add Column
|
|
368
|
+
</Button>
|
|
369
|
+
}
|
|
370
|
+
/>
|
|
371
|
+
)}
|
|
372
|
+
|
|
373
|
+
{/* Add Column Modal */}
|
|
374
|
+
<Dialog
|
|
375
|
+
open={modalState.type === "addColumn"}
|
|
376
|
+
onOpenChange={(open: boolean) => !open && closeModal()}
|
|
377
|
+
>
|
|
378
|
+
<DialogContent>
|
|
379
|
+
<DialogHeader>
|
|
380
|
+
<DialogTitle>Add Column</DialogTitle>
|
|
381
|
+
<DialogDescription>
|
|
382
|
+
Add a new column to this board.
|
|
383
|
+
</DialogDescription>
|
|
384
|
+
</DialogHeader>
|
|
385
|
+
<ColumnForm
|
|
386
|
+
boardId={boardId}
|
|
387
|
+
onClose={closeModal}
|
|
388
|
+
onSuccess={() => {
|
|
389
|
+
closeModal();
|
|
390
|
+
refetch();
|
|
391
|
+
}}
|
|
392
|
+
/>
|
|
393
|
+
</DialogContent>
|
|
394
|
+
</Dialog>
|
|
395
|
+
|
|
396
|
+
{/* Edit Column Modal */}
|
|
397
|
+
<Dialog
|
|
398
|
+
open={modalState.type === "editColumn"}
|
|
399
|
+
onOpenChange={(open: boolean) => !open && closeModal()}
|
|
400
|
+
>
|
|
401
|
+
<DialogContent>
|
|
402
|
+
<DialogHeader>
|
|
403
|
+
<DialogTitle>Edit Column</DialogTitle>
|
|
404
|
+
<DialogDescription>Update the column details.</DialogDescription>
|
|
405
|
+
</DialogHeader>
|
|
406
|
+
{modalState.type === "editColumn" && (
|
|
407
|
+
<ColumnForm
|
|
408
|
+
boardId={boardId}
|
|
409
|
+
columnId={modalState.columnId}
|
|
410
|
+
column={board.columns?.find((c) => c.id === modalState.columnId)}
|
|
411
|
+
onClose={closeModal}
|
|
412
|
+
onSuccess={() => {
|
|
413
|
+
closeModal();
|
|
414
|
+
refetch();
|
|
415
|
+
}}
|
|
416
|
+
/>
|
|
417
|
+
)}
|
|
418
|
+
</DialogContent>
|
|
419
|
+
</Dialog>
|
|
420
|
+
|
|
421
|
+
{/* Delete Column Modal */}
|
|
422
|
+
<AlertDialog
|
|
423
|
+
open={modalState.type === "deleteColumn"}
|
|
424
|
+
onOpenChange={(open: boolean) => !open && closeModal()}
|
|
425
|
+
>
|
|
426
|
+
<AlertDialogContent>
|
|
427
|
+
<AlertDialogHeader>
|
|
428
|
+
<AlertDialogTitle>Delete Column</AlertDialogTitle>
|
|
429
|
+
<AlertDialogDescription>
|
|
430
|
+
Are you sure you want to delete this column? All tasks in this
|
|
431
|
+
column will be permanently removed.
|
|
432
|
+
</AlertDialogDescription>
|
|
433
|
+
</AlertDialogHeader>
|
|
434
|
+
<AlertDialogFooter>
|
|
435
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
436
|
+
<AlertDialogAction
|
|
437
|
+
onClick={async () => {
|
|
438
|
+
if (modalState.type === "deleteColumn") {
|
|
439
|
+
try {
|
|
440
|
+
await deleteColumn(modalState.columnId);
|
|
441
|
+
closeModal();
|
|
442
|
+
refetch();
|
|
443
|
+
} catch (error) {
|
|
444
|
+
const message =
|
|
445
|
+
error instanceof Error
|
|
446
|
+
? error.message
|
|
447
|
+
: "Failed to delete column";
|
|
448
|
+
toast.error(message);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}}
|
|
452
|
+
className="bg-red-600 hover:bg-red-700"
|
|
453
|
+
>
|
|
454
|
+
Delete
|
|
455
|
+
</AlertDialogAction>
|
|
456
|
+
</AlertDialogFooter>
|
|
457
|
+
</AlertDialogContent>
|
|
458
|
+
</AlertDialog>
|
|
459
|
+
|
|
460
|
+
{/* Edit Board Modal */}
|
|
461
|
+
<Dialog
|
|
462
|
+
open={modalState.type === "editBoard"}
|
|
463
|
+
onOpenChange={(open: boolean) => !open && closeModal()}
|
|
464
|
+
>
|
|
465
|
+
<DialogContent>
|
|
466
|
+
<DialogHeader>
|
|
467
|
+
<DialogTitle>Edit Board</DialogTitle>
|
|
468
|
+
<DialogDescription>Update board details.</DialogDescription>
|
|
469
|
+
</DialogHeader>
|
|
470
|
+
<BoardForm
|
|
471
|
+
board={board}
|
|
472
|
+
onClose={closeModal}
|
|
473
|
+
onSuccess={() => {
|
|
474
|
+
closeModal();
|
|
475
|
+
refetch();
|
|
476
|
+
}}
|
|
477
|
+
/>
|
|
478
|
+
</DialogContent>
|
|
479
|
+
</Dialog>
|
|
480
|
+
|
|
481
|
+
{/* Delete Board Modal */}
|
|
482
|
+
<AlertDialog
|
|
483
|
+
open={modalState.type === "deleteBoard"}
|
|
484
|
+
onOpenChange={(open: boolean) => !open && closeModal()}
|
|
485
|
+
>
|
|
486
|
+
<AlertDialogContent>
|
|
487
|
+
<AlertDialogHeader>
|
|
488
|
+
<AlertDialogTitle>Delete Board</AlertDialogTitle>
|
|
489
|
+
<AlertDialogDescription>
|
|
490
|
+
Are you sure you want to delete this board? This action cannot be
|
|
491
|
+
undone. All columns and tasks will be permanently removed.
|
|
492
|
+
</AlertDialogDescription>
|
|
493
|
+
</AlertDialogHeader>
|
|
494
|
+
<AlertDialogFooter>
|
|
495
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
496
|
+
<Button
|
|
497
|
+
onClick={handleDeleteBoard}
|
|
498
|
+
disabled={isDeleting}
|
|
499
|
+
className="bg-red-600 hover:bg-red-700"
|
|
500
|
+
>
|
|
501
|
+
{isDeleting ? "Deleting..." : "Delete"}
|
|
502
|
+
</Button>
|
|
503
|
+
</AlertDialogFooter>
|
|
504
|
+
</AlertDialogContent>
|
|
505
|
+
</AlertDialog>
|
|
506
|
+
|
|
507
|
+
{/* Add Task Modal */}
|
|
508
|
+
<Dialog
|
|
509
|
+
open={modalState.type === "addTask"}
|
|
510
|
+
onOpenChange={(open: boolean) => !open && closeModal()}
|
|
511
|
+
>
|
|
512
|
+
<DialogContent className="max-w-3xl! max-h-screen overflow-y-auto">
|
|
513
|
+
<DialogHeader>
|
|
514
|
+
<DialogTitle>Add Task</DialogTitle>
|
|
515
|
+
<DialogDescription>Create a new task.</DialogDescription>
|
|
516
|
+
</DialogHeader>
|
|
517
|
+
{modalState.type === "addTask" && (
|
|
518
|
+
<TaskForm
|
|
519
|
+
columnId={modalState.columnId}
|
|
520
|
+
boardId={boardId}
|
|
521
|
+
columns={board.columns || []}
|
|
522
|
+
onClose={closeModal}
|
|
523
|
+
onSuccess={() => {
|
|
524
|
+
closeModal();
|
|
525
|
+
refetch();
|
|
526
|
+
}}
|
|
527
|
+
/>
|
|
528
|
+
)}
|
|
529
|
+
</DialogContent>
|
|
530
|
+
</Dialog>
|
|
531
|
+
|
|
532
|
+
{/* Edit Task Modal */}
|
|
533
|
+
<Dialog
|
|
534
|
+
open={modalState.type === "editTask"}
|
|
535
|
+
onOpenChange={(open: boolean) => !open && closeModal()}
|
|
536
|
+
>
|
|
537
|
+
<DialogContent className="max-w-3xl! max-h-screen overflow-y-auto">
|
|
538
|
+
<DialogHeader>
|
|
539
|
+
<DialogTitle>Edit Task</DialogTitle>
|
|
540
|
+
<DialogDescription>Update task details.</DialogDescription>
|
|
541
|
+
</DialogHeader>
|
|
542
|
+
{modalState.type === "editTask" && (
|
|
543
|
+
<TaskForm
|
|
544
|
+
columnId={modalState.columnId}
|
|
545
|
+
boardId={boardId}
|
|
546
|
+
taskId={modalState.taskId}
|
|
547
|
+
task={board.columns
|
|
548
|
+
?.find((c) => c.id === modalState.columnId)
|
|
549
|
+
?.tasks?.find((t) => t.id === modalState.taskId)}
|
|
550
|
+
columns={board.columns || []}
|
|
551
|
+
onClose={closeModal}
|
|
552
|
+
onSuccess={() => {
|
|
553
|
+
closeModal();
|
|
554
|
+
refetch();
|
|
555
|
+
}}
|
|
556
|
+
onDelete={async () => {
|
|
557
|
+
try {
|
|
558
|
+
await deleteTask(modalState.taskId);
|
|
559
|
+
closeModal();
|
|
560
|
+
refetch();
|
|
561
|
+
} catch (error) {
|
|
562
|
+
const message =
|
|
563
|
+
error instanceof Error
|
|
564
|
+
? error.message
|
|
565
|
+
: "Failed to delete task";
|
|
566
|
+
toast.error(message);
|
|
567
|
+
}
|
|
568
|
+
}}
|
|
569
|
+
/>
|
|
570
|
+
)}
|
|
571
|
+
</DialogContent>
|
|
572
|
+
</Dialog>
|
|
573
|
+
</PageWrapper>
|
|
574
|
+
);
|
|
575
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { lazy } from "react";
|
|
4
|
+
import { ComposedRoute } from "@btst/stack/client/components";
|
|
5
|
+
import { DefaultError } from "../shared/default-error";
|
|
6
|
+
import { BoardSkeleton } from "../loading/board-skeleton";
|
|
7
|
+
import { NotFoundPage } from "./404-page";
|
|
8
|
+
|
|
9
|
+
const BoardPage = lazy(() =>
|
|
10
|
+
import("./board-page.internal").then((m) => ({
|
|
11
|
+
default: m.BoardPage,
|
|
12
|
+
})),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
interface BoardPageComponentProps {
|
|
16
|
+
boardId: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function BoardPageComponent({ boardId }: BoardPageComponentProps) {
|
|
20
|
+
return (
|
|
21
|
+
<ComposedRoute
|
|
22
|
+
path={`/kanban/${boardId}`}
|
|
23
|
+
PageComponent={BoardPage}
|
|
24
|
+
ErrorComponent={DefaultError}
|
|
25
|
+
LoadingComponent={BoardSkeleton}
|
|
26
|
+
NotFoundComponent={NotFoundPage}
|
|
27
|
+
props={{ boardId }}
|
|
28
|
+
onError={(error) => console.error("BoardPage error:", error)}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Plus } from "lucide-react";
|
|
4
|
+
import { Button } from "@workspace/ui/components/button";
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from "@workspace/ui/components/card";
|
|
12
|
+
import { useSuspenseBoards } from "../../hooks/kanban-hooks";
|
|
13
|
+
import { usePluginOverrides } from "@btst/stack/context";
|
|
14
|
+
import type { KanbanPluginOverrides } from "../../overrides";
|
|
15
|
+
import { EmptyState } from "../shared/empty-state";
|
|
16
|
+
import { PageWrapper } from "../shared/page-wrapper";
|
|
17
|
+
import { format } from "date-fns";
|
|
18
|
+
|
|
19
|
+
export function BoardsListPage() {
|
|
20
|
+
const { data: boards, error, isFetching } = useSuspenseBoards();
|
|
21
|
+
|
|
22
|
+
// Suspense hooks only throw on initial fetch, not refetch failures
|
|
23
|
+
if (error && !isFetching) {
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
const { Link: OverrideLink, navigate: overrideNavigate } =
|
|
27
|
+
usePluginOverrides<KanbanPluginOverrides>("kanban");
|
|
28
|
+
const Link = OverrideLink || "a";
|
|
29
|
+
const navigate =
|
|
30
|
+
overrideNavigate ||
|
|
31
|
+
((path: string) => {
|
|
32
|
+
window.location.href = path;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const handleNewBoard = () => {
|
|
36
|
+
navigate("/pages/kanban/new");
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<PageWrapper data-testid="boards-list-page">
|
|
41
|
+
<div className="w-full flex items-center justify-between mb-8">
|
|
42
|
+
<div>
|
|
43
|
+
<h1 className="text-3xl font-bold" data-testid="page-header">
|
|
44
|
+
Kanban Boards
|
|
45
|
+
</h1>
|
|
46
|
+
<p className="text-muted-foreground mt-1">
|
|
47
|
+
Manage your projects and tasks
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
<Button onClick={handleNewBoard}>
|
|
51
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
52
|
+
New Board
|
|
53
|
+
</Button>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{boards.length > 0 ? (
|
|
57
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
58
|
+
{boards.map((board) => (
|
|
59
|
+
<Link
|
|
60
|
+
key={board.id}
|
|
61
|
+
href={`/pages/kanban/${board.id}`}
|
|
62
|
+
className="block group"
|
|
63
|
+
>
|
|
64
|
+
<Card className="h-full transition-shadow hover:shadow-md cursor-pointer">
|
|
65
|
+
<CardHeader>
|
|
66
|
+
<CardTitle className="group-hover:text-primary transition-colors">
|
|
67
|
+
{board.name}
|
|
68
|
+
</CardTitle>
|
|
69
|
+
{board.description && (
|
|
70
|
+
<CardDescription className="line-clamp-2">
|
|
71
|
+
{board.description}
|
|
72
|
+
</CardDescription>
|
|
73
|
+
)}
|
|
74
|
+
</CardHeader>
|
|
75
|
+
<CardContent>
|
|
76
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
77
|
+
<span>{board.columns?.length || 0} columns</span>
|
|
78
|
+
<span>
|
|
79
|
+
{format(new Date(board.createdAt), "MMM d, yyyy")}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
</CardContent>
|
|
83
|
+
</Card>
|
|
84
|
+
</Link>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
) : (
|
|
88
|
+
<EmptyState
|
|
89
|
+
title="No boards yet"
|
|
90
|
+
description="Create your first kanban board to start organizing your tasks."
|
|
91
|
+
action={
|
|
92
|
+
<Button onClick={handleNewBoard}>
|
|
93
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
94
|
+
Create Board
|
|
95
|
+
</Button>
|
|
96
|
+
}
|
|
97
|
+
/>
|
|
98
|
+
)}
|
|
99
|
+
</PageWrapper>
|
|
100
|
+
);
|
|
101
|
+
}
|