@btst/stack 1.11.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/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/lib/compose-refs.cjs +56 -0
- package/dist/packages/ui/src/lib/compose-refs.mjs +39 -0
- 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/style.css +6 -0
- 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.FeaWkglm.d.cts +190 -0
- package/dist/shared/stack.FeaWkglm.d.mts +190 -0
- package/dist/shared/stack.FeaWkglm.d.ts +190 -0
- 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.cts → stack.CcI4sYJP.d.cts} +1 -1
- package/dist/shared/{stack.DLhzx1-D.d.mts → stack.CcI4sYJP.d.mts} +1 -1
- package/dist/shared/{stack.DLhzx1-D.d.ts → stack.CcI4sYJP.d.ts} +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button } from "@workspace/ui/components/button";
|
|
5
|
+
import { Input } from "@workspace/ui/components/input";
|
|
6
|
+
import { Textarea } from "@workspace/ui/components/textarea";
|
|
7
|
+
import { Label } from "@workspace/ui/components/label";
|
|
8
|
+
import { useBoardMutations } from "../../hooks/kanban-hooks";
|
|
9
|
+
import type { SerializedBoard } from "../../../types";
|
|
10
|
+
|
|
11
|
+
interface BoardFormProps {
|
|
12
|
+
board?: SerializedBoard;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
onSuccess: (boardId: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function BoardForm({ board, onClose, onSuccess }: BoardFormProps) {
|
|
18
|
+
const isEditing = !!board;
|
|
19
|
+
const { createBoard, updateBoard, isCreating, isUpdating } =
|
|
20
|
+
useBoardMutations();
|
|
21
|
+
|
|
22
|
+
const [name, setName] = useState(board?.name || "");
|
|
23
|
+
const [description, setDescription] = useState(board?.description || "");
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
|
|
26
|
+
const isPending = isCreating || isUpdating;
|
|
27
|
+
|
|
28
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
setError(null);
|
|
31
|
+
|
|
32
|
+
if (!name.trim()) {
|
|
33
|
+
setError("Name is required");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
if (isEditing && board) {
|
|
39
|
+
await updateBoard(board.id, { name, description });
|
|
40
|
+
onSuccess(board.id);
|
|
41
|
+
} else {
|
|
42
|
+
const newBoard = await createBoard({ name, description });
|
|
43
|
+
if (newBoard?.id) {
|
|
44
|
+
onSuccess(newBoard.id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
54
|
+
<div className="space-y-2">
|
|
55
|
+
<Label htmlFor="name">Name *</Label>
|
|
56
|
+
<Input
|
|
57
|
+
id="name"
|
|
58
|
+
value={name}
|
|
59
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
60
|
+
setName(e.target.value)
|
|
61
|
+
}
|
|
62
|
+
placeholder="e.g., Project Alpha"
|
|
63
|
+
disabled={isPending}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="space-y-2">
|
|
68
|
+
<Label htmlFor="description">Description</Label>
|
|
69
|
+
<Textarea
|
|
70
|
+
id="description"
|
|
71
|
+
value={description}
|
|
72
|
+
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
73
|
+
setDescription(e.target.value)
|
|
74
|
+
}
|
|
75
|
+
placeholder="Describe your board..."
|
|
76
|
+
disabled={isPending}
|
|
77
|
+
rows={3}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{error && (
|
|
82
|
+
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
|
|
83
|
+
{error}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
<div className="flex gap-2 pt-2">
|
|
88
|
+
<Button type="submit" disabled={isPending}>
|
|
89
|
+
{isPending
|
|
90
|
+
? isEditing
|
|
91
|
+
? "Updating..."
|
|
92
|
+
: "Creating..."
|
|
93
|
+
: isEditing
|
|
94
|
+
? "Update Board"
|
|
95
|
+
: "Create Board"}
|
|
96
|
+
</Button>
|
|
97
|
+
<Button
|
|
98
|
+
type="button"
|
|
99
|
+
variant="outline"
|
|
100
|
+
onClick={onClose}
|
|
101
|
+
disabled={isPending}
|
|
102
|
+
>
|
|
103
|
+
Cancel
|
|
104
|
+
</Button>
|
|
105
|
+
</div>
|
|
106
|
+
</form>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button } from "@workspace/ui/components/button";
|
|
5
|
+
import { Input } from "@workspace/ui/components/input";
|
|
6
|
+
import { Label } from "@workspace/ui/components/label";
|
|
7
|
+
import { useColumnMutations } from "../../hooks/kanban-hooks";
|
|
8
|
+
import type { SerializedColumn } from "../../../types";
|
|
9
|
+
|
|
10
|
+
interface ColumnFormProps {
|
|
11
|
+
boardId: string;
|
|
12
|
+
columnId?: string;
|
|
13
|
+
column?: SerializedColumn;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
onSuccess: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ColumnForm({
|
|
19
|
+
boardId,
|
|
20
|
+
columnId,
|
|
21
|
+
column,
|
|
22
|
+
onClose,
|
|
23
|
+
onSuccess,
|
|
24
|
+
}: ColumnFormProps) {
|
|
25
|
+
const isEditing = !!columnId;
|
|
26
|
+
const { createColumn, updateColumn, isCreating, isUpdating } =
|
|
27
|
+
useColumnMutations();
|
|
28
|
+
|
|
29
|
+
const [title, setTitle] = useState(column?.title || "");
|
|
30
|
+
const [error, setError] = useState<string | null>(null);
|
|
31
|
+
|
|
32
|
+
const isPending = isCreating || isUpdating;
|
|
33
|
+
|
|
34
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
setError(null);
|
|
37
|
+
|
|
38
|
+
if (!title.trim()) {
|
|
39
|
+
setError("Title is required");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (isEditing && columnId) {
|
|
45
|
+
await updateColumn(columnId, { title });
|
|
46
|
+
} else {
|
|
47
|
+
await createColumn({ title, boardId });
|
|
48
|
+
}
|
|
49
|
+
onSuccess();
|
|
50
|
+
} catch (err) {
|
|
51
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
57
|
+
<div className="space-y-2">
|
|
58
|
+
<Label htmlFor="title">Title *</Label>
|
|
59
|
+
<Input
|
|
60
|
+
id="title"
|
|
61
|
+
value={title}
|
|
62
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
63
|
+
setTitle(e.target.value)
|
|
64
|
+
}
|
|
65
|
+
placeholder="e.g., To Do"
|
|
66
|
+
disabled={isPending}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{error && (
|
|
71
|
+
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
|
|
72
|
+
{error}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
<div className="flex gap-2 pt-2">
|
|
77
|
+
<Button type="submit" disabled={isPending}>
|
|
78
|
+
{isPending
|
|
79
|
+
? isEditing
|
|
80
|
+
? "Updating..."
|
|
81
|
+
: "Creating..."
|
|
82
|
+
: isEditing
|
|
83
|
+
? "Update Column"
|
|
84
|
+
: "Create Column"}
|
|
85
|
+
</Button>
|
|
86
|
+
<Button
|
|
87
|
+
type="button"
|
|
88
|
+
variant="outline"
|
|
89
|
+
onClick={onClose}
|
|
90
|
+
disabled={isPending}
|
|
91
|
+
>
|
|
92
|
+
Cancel
|
|
93
|
+
</Button>
|
|
94
|
+
</div>
|
|
95
|
+
</form>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Trash2 } from "lucide-react";
|
|
5
|
+
import { Button } from "@workspace/ui/components/button";
|
|
6
|
+
import { Input } from "@workspace/ui/components/input";
|
|
7
|
+
import { Label } from "@workspace/ui/components/label";
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from "@workspace/ui/components/select";
|
|
15
|
+
import { MinimalTiptapEditor } from "@workspace/ui/components/minimal-tiptap";
|
|
16
|
+
import SearchSelect from "@workspace/ui/components/search-select";
|
|
17
|
+
import { useTaskMutations, useSearchUsers } from "../../hooks/kanban-hooks";
|
|
18
|
+
import { PRIORITY_OPTIONS } from "../../../utils";
|
|
19
|
+
import type {
|
|
20
|
+
SerializedColumn,
|
|
21
|
+
SerializedTask,
|
|
22
|
+
Priority,
|
|
23
|
+
} from "../../../types";
|
|
24
|
+
|
|
25
|
+
interface TaskFormProps {
|
|
26
|
+
columnId: string;
|
|
27
|
+
boardId: string;
|
|
28
|
+
taskId?: string;
|
|
29
|
+
task?: SerializedTask;
|
|
30
|
+
columns: SerializedColumn[];
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
onSuccess: () => void;
|
|
33
|
+
onDelete?: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function TaskForm({
|
|
37
|
+
columnId,
|
|
38
|
+
boardId,
|
|
39
|
+
taskId,
|
|
40
|
+
task,
|
|
41
|
+
columns,
|
|
42
|
+
onClose,
|
|
43
|
+
onSuccess,
|
|
44
|
+
onDelete,
|
|
45
|
+
}: TaskFormProps) {
|
|
46
|
+
const isEditing = !!taskId;
|
|
47
|
+
const {
|
|
48
|
+
createTask,
|
|
49
|
+
updateTask,
|
|
50
|
+
moveTask,
|
|
51
|
+
isCreating,
|
|
52
|
+
isUpdating,
|
|
53
|
+
isDeleting,
|
|
54
|
+
isMoving,
|
|
55
|
+
} = useTaskMutations();
|
|
56
|
+
|
|
57
|
+
const [title, setTitle] = useState(task?.title || "");
|
|
58
|
+
const [description, setDescription] = useState(task?.description || "");
|
|
59
|
+
const [priority, setPriority] = useState<Priority>(
|
|
60
|
+
task?.priority || "MEDIUM",
|
|
61
|
+
);
|
|
62
|
+
const [selectedColumnId, setSelectedColumnId] = useState(
|
|
63
|
+
task?.columnId || columnId,
|
|
64
|
+
);
|
|
65
|
+
const [assigneeId, setAssigneeId] = useState<string>(task?.assigneeId || "");
|
|
66
|
+
const [error, setError] = useState<string | null>(null);
|
|
67
|
+
|
|
68
|
+
// Fetch available users for assignment
|
|
69
|
+
const { data: users = [] } = useSearchUsers("", boardId);
|
|
70
|
+
const userOptions = [
|
|
71
|
+
{ value: "", label: "Unassigned" },
|
|
72
|
+
...users.map((user) => ({ value: user.id, label: user.name })),
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const isPending = isCreating || isUpdating || isDeleting || isMoving;
|
|
76
|
+
|
|
77
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
setError(null);
|
|
80
|
+
|
|
81
|
+
if (!title.trim()) {
|
|
82
|
+
setError("Title is required");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
if (isEditing && taskId) {
|
|
88
|
+
const isColumnChanging =
|
|
89
|
+
task?.columnId && selectedColumnId !== task.columnId;
|
|
90
|
+
|
|
91
|
+
if (isColumnChanging) {
|
|
92
|
+
// When changing columns, we need two operations:
|
|
93
|
+
// 1. Update task properties (title, description, priority, assigneeId)
|
|
94
|
+
// 2. Move task to new column with proper order calculation
|
|
95
|
+
//
|
|
96
|
+
// To avoid partial failure confusion, we attempt both operations
|
|
97
|
+
// but provide clear messaging if one succeeds and the other fails.
|
|
98
|
+
|
|
99
|
+
// First update the task properties (title, description, priority, assigneeId)
|
|
100
|
+
// If this fails, nothing is saved and the outer catch handles it
|
|
101
|
+
await updateTask(taskId, {
|
|
102
|
+
title,
|
|
103
|
+
description,
|
|
104
|
+
priority,
|
|
105
|
+
assigneeId: assigneeId || null,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Then move the task to the new column with calculated order
|
|
109
|
+
// Place at the end of the destination column
|
|
110
|
+
try {
|
|
111
|
+
const targetColumn = columns.find((c) => c.id === selectedColumnId);
|
|
112
|
+
const targetTasks = targetColumn?.tasks || [];
|
|
113
|
+
const targetOrder =
|
|
114
|
+
targetTasks.length > 0
|
|
115
|
+
? Math.max(...targetTasks.map((t) => t.order)) + 1
|
|
116
|
+
: 0;
|
|
117
|
+
|
|
118
|
+
await moveTask(taskId, selectedColumnId, targetOrder);
|
|
119
|
+
} catch (moveErr) {
|
|
120
|
+
// Properties were saved but column move failed
|
|
121
|
+
// Provide specific error message about partial success
|
|
122
|
+
const moveErrorMsg =
|
|
123
|
+
moveErr instanceof Error ? moveErr.message : "Unknown error";
|
|
124
|
+
setError(
|
|
125
|
+
`Task properties were saved, but moving to the new column failed: ${moveErrorMsg}. ` +
|
|
126
|
+
`You can try dragging the task to the desired column.`,
|
|
127
|
+
);
|
|
128
|
+
// Don't call onSuccess since the operation wasn't fully completed
|
|
129
|
+
// but also don't throw - we want to show the specific error
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
// Same column - just update the task properties
|
|
134
|
+
await updateTask(taskId, {
|
|
135
|
+
title,
|
|
136
|
+
description,
|
|
137
|
+
priority,
|
|
138
|
+
columnId: selectedColumnId,
|
|
139
|
+
assigneeId: assigneeId || null,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
await createTask({
|
|
144
|
+
title,
|
|
145
|
+
description,
|
|
146
|
+
priority,
|
|
147
|
+
columnId: selectedColumnId,
|
|
148
|
+
assigneeId: assigneeId || undefined,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
onSuccess();
|
|
152
|
+
} catch (err) {
|
|
153
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
159
|
+
<div className="space-y-2">
|
|
160
|
+
<Label htmlFor="title">Title *</Label>
|
|
161
|
+
<Input
|
|
162
|
+
id="title"
|
|
163
|
+
value={title}
|
|
164
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
165
|
+
setTitle(e.target.value)
|
|
166
|
+
}
|
|
167
|
+
placeholder="e.g., Fix login bug"
|
|
168
|
+
disabled={isPending}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div className="grid grid-cols-2 gap-4">
|
|
173
|
+
<div className="space-y-2">
|
|
174
|
+
<Label htmlFor="priority">Priority</Label>
|
|
175
|
+
<Select
|
|
176
|
+
value={priority}
|
|
177
|
+
onValueChange={(v: string) => setPriority(v as Priority)}
|
|
178
|
+
>
|
|
179
|
+
<SelectTrigger>
|
|
180
|
+
<SelectValue placeholder="Select priority" />
|
|
181
|
+
</SelectTrigger>
|
|
182
|
+
<SelectContent>
|
|
183
|
+
{PRIORITY_OPTIONS.map((option) => (
|
|
184
|
+
<SelectItem key={option.value} value={option.value}>
|
|
185
|
+
{option.label}
|
|
186
|
+
</SelectItem>
|
|
187
|
+
))}
|
|
188
|
+
</SelectContent>
|
|
189
|
+
</Select>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div className="space-y-2">
|
|
193
|
+
<Label htmlFor="column">Column</Label>
|
|
194
|
+
<Select value={selectedColumnId} onValueChange={setSelectedColumnId}>
|
|
195
|
+
<SelectTrigger>
|
|
196
|
+
<SelectValue placeholder="Select column" />
|
|
197
|
+
</SelectTrigger>
|
|
198
|
+
<SelectContent>
|
|
199
|
+
{columns.map((col) => (
|
|
200
|
+
<SelectItem key={col.id} value={col.id}>
|
|
201
|
+
{col.title}
|
|
202
|
+
</SelectItem>
|
|
203
|
+
))}
|
|
204
|
+
</SelectContent>
|
|
205
|
+
</Select>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div className="space-y-2">
|
|
210
|
+
<Label htmlFor="assignee">Assignee</Label>
|
|
211
|
+
<SearchSelect
|
|
212
|
+
options={userOptions}
|
|
213
|
+
value={assigneeId}
|
|
214
|
+
onChange={setAssigneeId}
|
|
215
|
+
placeholder="Select assignee"
|
|
216
|
+
emptyMessage="No users found"
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div className="space-y-2">
|
|
221
|
+
<Label>Description</Label>
|
|
222
|
+
<MinimalTiptapEditor
|
|
223
|
+
value={description}
|
|
224
|
+
onChange={(value) =>
|
|
225
|
+
setDescription(typeof value === "string" ? value : "")
|
|
226
|
+
}
|
|
227
|
+
output="markdown"
|
|
228
|
+
placeholder="Describe the task..."
|
|
229
|
+
editable={!isPending}
|
|
230
|
+
className="min-h-[150px]"
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{error && (
|
|
235
|
+
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
|
|
236
|
+
{error}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
<div className="flex justify-between pt-2">
|
|
241
|
+
<div className="flex gap-2">
|
|
242
|
+
<Button type="submit" disabled={isPending}>
|
|
243
|
+
{isPending
|
|
244
|
+
? isEditing
|
|
245
|
+
? "Updating..."
|
|
246
|
+
: "Creating..."
|
|
247
|
+
: isEditing
|
|
248
|
+
? "Update Task"
|
|
249
|
+
: "Create Task"}
|
|
250
|
+
</Button>
|
|
251
|
+
<Button
|
|
252
|
+
type="button"
|
|
253
|
+
variant="outline"
|
|
254
|
+
onClick={onClose}
|
|
255
|
+
disabled={isPending}
|
|
256
|
+
>
|
|
257
|
+
Cancel
|
|
258
|
+
</Button>
|
|
259
|
+
</div>
|
|
260
|
+
{isEditing && onDelete && (
|
|
261
|
+
<Button
|
|
262
|
+
type="button"
|
|
263
|
+
variant="destructive"
|
|
264
|
+
onClick={onDelete}
|
|
265
|
+
disabled={isPending}
|
|
266
|
+
>
|
|
267
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
268
|
+
Delete
|
|
269
|
+
</Button>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
</form>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Pages
|
|
2
|
+
export { BoardsListPageComponent } from "./pages/boards-list-page";
|
|
3
|
+
export { BoardPageComponent } from "./pages/board-page";
|
|
4
|
+
export { NewBoardPageComponent } from "./pages/new-board-page";
|
|
5
|
+
export { NotFoundPage } from "./pages/404-page";
|
|
6
|
+
|
|
7
|
+
// Shared
|
|
8
|
+
export { PageWrapper } from "./shared/page-wrapper";
|
|
9
|
+
export { EmptyState } from "./shared/empty-state";
|
|
10
|
+
export { DefaultError } from "./shared/default-error";
|
|
11
|
+
export { KanbanBoard } from "./shared/kanban-board";
|
|
12
|
+
export { ColumnContent } from "./shared/column-content";
|
|
13
|
+
export { TaskCard } from "./shared/task-card";
|
|
14
|
+
|
|
15
|
+
// Forms
|
|
16
|
+
export { BoardForm } from "./forms/board-form";
|
|
17
|
+
export { ColumnForm } from "./forms/column-form";
|
|
18
|
+
export { TaskForm } from "./forms/task-form";
|
|
19
|
+
|
|
20
|
+
// Loading
|
|
21
|
+
export { BoardsListSkeleton, BoardSkeleton } from "./loading";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Skeleton } from "@workspace/ui/components/skeleton";
|
|
4
|
+
|
|
5
|
+
export function BoardSkeleton() {
|
|
6
|
+
return (
|
|
7
|
+
<div className="container mx-auto py-8 px-4">
|
|
8
|
+
<div className="w-full flex items-center justify-between mb-8">
|
|
9
|
+
<div className="flex items-center gap-4">
|
|
10
|
+
<Skeleton className="h-5 w-5" />
|
|
11
|
+
<div>
|
|
12
|
+
<Skeleton className="h-9 w-48" />
|
|
13
|
+
<Skeleton className="h-5 w-64 mt-2" />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<Skeleton className="h-10 w-28" />
|
|
17
|
+
</div>
|
|
18
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
19
|
+
{Array.from({ length: 3 }).map((_, colIdx) => (
|
|
20
|
+
<div
|
|
21
|
+
key={colIdx}
|
|
22
|
+
className="rounded-lg border bg-zinc-100 dark:bg-zinc-900 p-2.5"
|
|
23
|
+
>
|
|
24
|
+
<div className="flex items-center gap-2 mb-4">
|
|
25
|
+
<Skeleton className="h-8 w-8" />
|
|
26
|
+
<Skeleton className="h-6 w-24" />
|
|
27
|
+
<Skeleton className="h-5 w-8" />
|
|
28
|
+
</div>
|
|
29
|
+
<div className="space-y-2">
|
|
30
|
+
{Array.from({ length: 3 }).map((_, taskIdx) => (
|
|
31
|
+
<div key={taskIdx} className="rounded-md border bg-card p-3">
|
|
32
|
+
<div className="flex items-center gap-2 mb-2">
|
|
33
|
+
<Skeleton className="h-6 w-6" />
|
|
34
|
+
<Skeleton className="h-5 flex-1" />
|
|
35
|
+
<Skeleton className="h-5 w-16" />
|
|
36
|
+
</div>
|
|
37
|
+
<div className="flex items-center justify-between">
|
|
38
|
+
<Skeleton className="h-4 w-20" />
|
|
39
|
+
<Skeleton className="h-4 w-16" />
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Skeleton } from "@workspace/ui/components/skeleton";
|
|
4
|
+
import { Card, CardContent, CardHeader } from "@workspace/ui/components/card";
|
|
5
|
+
|
|
6
|
+
export function BoardsListSkeleton() {
|
|
7
|
+
return (
|
|
8
|
+
<div className="container mx-auto py-8 px-4">
|
|
9
|
+
<div className="w-full flex items-center justify-between mb-8">
|
|
10
|
+
<div>
|
|
11
|
+
<Skeleton className="h-9 w-48" />
|
|
12
|
+
<Skeleton className="h-5 w-64 mt-2" />
|
|
13
|
+
</div>
|
|
14
|
+
<Skeleton className="h-10 w-32" />
|
|
15
|
+
</div>
|
|
16
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
17
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
18
|
+
<Card key={i}>
|
|
19
|
+
<CardHeader>
|
|
20
|
+
<Skeleton className="h-6 w-3/4" />
|
|
21
|
+
<Skeleton className="h-4 w-full mt-2" />
|
|
22
|
+
</CardHeader>
|
|
23
|
+
<CardContent>
|
|
24
|
+
<div className="flex items-center justify-between">
|
|
25
|
+
<Skeleton className="h-4 w-20" />
|
|
26
|
+
<Skeleton className="h-4 w-24" />
|
|
27
|
+
</div>
|
|
28
|
+
</CardContent>
|
|
29
|
+
</Card>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@workspace/ui/components/button";
|
|
4
|
+
import { usePluginOverrides } from "@btst/stack/context";
|
|
5
|
+
import type { KanbanPluginOverrides } from "../../overrides";
|
|
6
|
+
|
|
7
|
+
export function NotFoundPage() {
|
|
8
|
+
const { navigate: overrideNavigate } =
|
|
9
|
+
usePluginOverrides<KanbanPluginOverrides>("kanban");
|
|
10
|
+
const navigate =
|
|
11
|
+
overrideNavigate ||
|
|
12
|
+
((path: string) => {
|
|
13
|
+
window.location.href = path;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className="flex min-h-[400px] flex-col items-center justify-center text-center"
|
|
19
|
+
data-testid="empty-state"
|
|
20
|
+
>
|
|
21
|
+
<h2 className="text-2xl font-bold mb-2">Page Not Found</h2>
|
|
22
|
+
<p className="text-muted-foreground mb-6">
|
|
23
|
+
The page you're looking for doesn't exist.
|
|
24
|
+
</p>
|
|
25
|
+
<Button onClick={() => navigate("/pages/kanban")}>Back to Boards</Button>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|