@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,26 @@
|
|
|
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 { BoardsListSkeleton } from "../loading/boards-list-skeleton";
|
|
7
|
+
import { NotFoundPage } from "./404-page";
|
|
8
|
+
|
|
9
|
+
const BoardsListPage = lazy(() =>
|
|
10
|
+
import("./boards-list-page.internal").then((m) => ({
|
|
11
|
+
default: m.BoardsListPage,
|
|
12
|
+
})),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export function BoardsListPageComponent() {
|
|
16
|
+
return (
|
|
17
|
+
<ComposedRoute
|
|
18
|
+
path="/kanban"
|
|
19
|
+
PageComponent={BoardsListPage}
|
|
20
|
+
ErrorComponent={DefaultError}
|
|
21
|
+
LoadingComponent={BoardsListSkeleton}
|
|
22
|
+
NotFoundComponent={NotFoundPage}
|
|
23
|
+
onError={(error) => console.error("BoardsListPage error:", error)}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowLeft } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from "@workspace/ui/components/card";
|
|
11
|
+
import { usePluginOverrides } from "@btst/stack/context";
|
|
12
|
+
import type { KanbanPluginOverrides } from "../../overrides";
|
|
13
|
+
import { BoardForm } from "../forms/board-form";
|
|
14
|
+
import { PageWrapper } from "../shared/page-wrapper";
|
|
15
|
+
|
|
16
|
+
export function NewBoardPage() {
|
|
17
|
+
const { Link: OverrideLink, navigate: overrideNavigate } =
|
|
18
|
+
usePluginOverrides<KanbanPluginOverrides>("kanban");
|
|
19
|
+
const navigate =
|
|
20
|
+
overrideNavigate ||
|
|
21
|
+
((path: string) => {
|
|
22
|
+
window.location.href = path;
|
|
23
|
+
});
|
|
24
|
+
const Link = OverrideLink || "a";
|
|
25
|
+
|
|
26
|
+
const handleSuccess = (boardId: string) => {
|
|
27
|
+
navigate(`/pages/kanban/${boardId}`);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<PageWrapper data-testid="new-board-page">
|
|
32
|
+
<div className="flex items-center gap-4 mb-8">
|
|
33
|
+
<Link
|
|
34
|
+
href="/pages/kanban"
|
|
35
|
+
className="text-muted-foreground hover:text-foreground"
|
|
36
|
+
>
|
|
37
|
+
<ArrowLeft className="h-5 w-5" />
|
|
38
|
+
</Link>
|
|
39
|
+
<div>
|
|
40
|
+
<h1 className="text-3xl font-bold" data-testid="page-header">
|
|
41
|
+
Create New Board
|
|
42
|
+
</h1>
|
|
43
|
+
<p className="text-muted-foreground mt-1">
|
|
44
|
+
Set up a new kanban board for your project
|
|
45
|
+
</p>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<Card className="max-w-2xl">
|
|
50
|
+
<CardHeader>
|
|
51
|
+
<CardTitle>Board Details</CardTitle>
|
|
52
|
+
<CardDescription>
|
|
53
|
+
Enter the details for your new kanban board.
|
|
54
|
+
</CardDescription>
|
|
55
|
+
</CardHeader>
|
|
56
|
+
<CardContent>
|
|
57
|
+
<BoardForm
|
|
58
|
+
onClose={() => navigate("/pages/kanban")}
|
|
59
|
+
onSuccess={handleSuccess}
|
|
60
|
+
/>
|
|
61
|
+
</CardContent>
|
|
62
|
+
</Card>
|
|
63
|
+
</PageWrapper>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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 { BoardsListSkeleton } from "../loading/boards-list-skeleton";
|
|
7
|
+
import { NotFoundPage } from "./404-page";
|
|
8
|
+
|
|
9
|
+
const NewBoardPage = lazy(() =>
|
|
10
|
+
import("./new-board-page.internal").then((m) => ({
|
|
11
|
+
default: m.NewBoardPage,
|
|
12
|
+
})),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export function NewBoardPageComponent() {
|
|
16
|
+
return (
|
|
17
|
+
<ComposedRoute
|
|
18
|
+
path="/kanban/new"
|
|
19
|
+
PageComponent={NewBoardPage}
|
|
20
|
+
ErrorComponent={DefaultError}
|
|
21
|
+
LoadingComponent={BoardsListSkeleton}
|
|
22
|
+
NotFoundComponent={NotFoundPage}
|
|
23
|
+
onError={(error) => console.error("NewBoardPage error:", error)}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo } from "react";
|
|
4
|
+
import { GripVertical, MoreVertical, Pencil, Plus, Trash2 } from "lucide-react";
|
|
5
|
+
import { Button } from "@workspace/ui/components/button";
|
|
6
|
+
import { Badge } from "@workspace/ui/components/badge";
|
|
7
|
+
import * as Kanban from "@workspace/ui/components/kanban";
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuItem,
|
|
12
|
+
DropdownMenuSeparator,
|
|
13
|
+
DropdownMenuTrigger,
|
|
14
|
+
} from "@workspace/ui/components/dropdown-menu";
|
|
15
|
+
import { TaskCard } from "./task-card";
|
|
16
|
+
import type { SerializedColumn, SerializedTask } from "../../../types";
|
|
17
|
+
|
|
18
|
+
interface ColumnContentProps {
|
|
19
|
+
column: SerializedColumn & { tasks: SerializedTask[] };
|
|
20
|
+
onAddTask: () => void;
|
|
21
|
+
onEditTask: (taskId: string) => void;
|
|
22
|
+
onEditColumn: () => void;
|
|
23
|
+
onDeleteColumn: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ColumnContentComponent({
|
|
27
|
+
column,
|
|
28
|
+
onAddTask,
|
|
29
|
+
onEditTask,
|
|
30
|
+
onEditColumn,
|
|
31
|
+
onDeleteColumn,
|
|
32
|
+
}: ColumnContentProps) {
|
|
33
|
+
const hasTasks = column.tasks && column.tasks.length > 0;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Kanban.Column key={column.id} value={column.id}>
|
|
37
|
+
<div className="flex items-center">
|
|
38
|
+
<Kanban.ColumnHandle asChild>
|
|
39
|
+
<Button variant="ghost" size="icon">
|
|
40
|
+
<GripVertical className="h-4 w-4" />
|
|
41
|
+
</Button>
|
|
42
|
+
</Kanban.ColumnHandle>
|
|
43
|
+
<div className="flex items-center gap-2 flex-1">
|
|
44
|
+
<span className="font-bold text-lg line-clamp-1 flex-1 text-left">
|
|
45
|
+
{column.title}
|
|
46
|
+
</span>
|
|
47
|
+
<Badge variant="outline" className="pointer-events-none rounded-sm">
|
|
48
|
+
{column.tasks?.length || 0}
|
|
49
|
+
</Badge>
|
|
50
|
+
</div>
|
|
51
|
+
<DropdownMenu>
|
|
52
|
+
<DropdownMenuTrigger asChild>
|
|
53
|
+
<Button variant="ghost" size="icon">
|
|
54
|
+
<MoreVertical className="h-4 w-4" />
|
|
55
|
+
</Button>
|
|
56
|
+
</DropdownMenuTrigger>
|
|
57
|
+
<DropdownMenuContent align="end">
|
|
58
|
+
<DropdownMenuItem onClick={onEditColumn}>
|
|
59
|
+
<Pencil className="mr-2 h-4 w-4" />
|
|
60
|
+
Edit Column
|
|
61
|
+
</DropdownMenuItem>
|
|
62
|
+
<DropdownMenuItem onClick={onAddTask}>
|
|
63
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
64
|
+
Add Task
|
|
65
|
+
</DropdownMenuItem>
|
|
66
|
+
<DropdownMenuSeparator />
|
|
67
|
+
<DropdownMenuItem
|
|
68
|
+
onClick={onDeleteColumn}
|
|
69
|
+
className="text-red-600 focus:text-red-600"
|
|
70
|
+
>
|
|
71
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
72
|
+
Delete Column
|
|
73
|
+
</DropdownMenuItem>
|
|
74
|
+
</DropdownMenuContent>
|
|
75
|
+
</DropdownMenu>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="p-0.5 space-y-2">
|
|
78
|
+
{hasTasks ? (
|
|
79
|
+
column.tasks.map((task) => (
|
|
80
|
+
<TaskCard
|
|
81
|
+
key={task.id}
|
|
82
|
+
task={task}
|
|
83
|
+
onClick={() => onEditTask(task.id)}
|
|
84
|
+
/>
|
|
85
|
+
))
|
|
86
|
+
) : (
|
|
87
|
+
<div className="flex flex-col items-center justify-center py-1 md:py-8 text-center">
|
|
88
|
+
<div className="rounded-full bg-muted p-4 mb-3 hidden md:block">
|
|
89
|
+
<Plus className="h-5 w-5 text-muted-foreground" />
|
|
90
|
+
</div>
|
|
91
|
+
<div className="space-y-1 mb-2 md:space-y-2 md:mb-4">
|
|
92
|
+
<p className="text-sm text-muted-foreground">No tasks yet</p>
|
|
93
|
+
<p className="text-xs text-muted-foreground">
|
|
94
|
+
Add a task to get started
|
|
95
|
+
</p>
|
|
96
|
+
</div>
|
|
97
|
+
<Button onClick={onAddTask} size="sm">
|
|
98
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
99
|
+
Add Task
|
|
100
|
+
</Button>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
</Kanban.Column>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const ColumnContent = memo(ColumnContentComponent);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AlertCircle, RefreshCw } from "lucide-react";
|
|
4
|
+
import { Button } from "@workspace/ui/components/button";
|
|
5
|
+
|
|
6
|
+
interface DefaultErrorProps {
|
|
7
|
+
error?: Error;
|
|
8
|
+
reset?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DefaultError({ error, reset }: DefaultErrorProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
className="flex flex-col items-center justify-center py-12 text-center"
|
|
15
|
+
data-testid="error-placeholder"
|
|
16
|
+
>
|
|
17
|
+
<div className="rounded-full bg-destructive/10 p-6 mb-4">
|
|
18
|
+
<AlertCircle className="h-8 w-8 text-destructive" />
|
|
19
|
+
</div>
|
|
20
|
+
<h3 className="text-lg font-semibold mb-2">Something went wrong</h3>
|
|
21
|
+
<p className="text-muted-foreground max-w-md mb-4">
|
|
22
|
+
{error?.message || "An unexpected error occurred. Please try again."}
|
|
23
|
+
</p>
|
|
24
|
+
{reset && (
|
|
25
|
+
<Button onClick={reset} variant="outline">
|
|
26
|
+
<RefreshCw className="mr-2 h-4 w-4" />
|
|
27
|
+
Try Again
|
|
28
|
+
</Button>
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { cn } from "@workspace/ui/lib/utils";
|
|
5
|
+
|
|
6
|
+
interface EmptyStateProps {
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
action?: ReactNode;
|
|
10
|
+
icon?: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function EmptyState({
|
|
15
|
+
title,
|
|
16
|
+
description,
|
|
17
|
+
action,
|
|
18
|
+
icon,
|
|
19
|
+
className,
|
|
20
|
+
}: EmptyStateProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className={cn(
|
|
24
|
+
"flex flex-col items-center justify-center py-12 text-center",
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
data-testid="empty-state"
|
|
28
|
+
>
|
|
29
|
+
{icon && <div className="rounded-full bg-muted p-6 mb-4">{icon}</div>}
|
|
30
|
+
<h3 className="text-lg font-semibold mb-2">{title}</h3>
|
|
31
|
+
{description && (
|
|
32
|
+
<p className="text-muted-foreground max-w-md mb-4">{description}</p>
|
|
33
|
+
)}
|
|
34
|
+
{action && <div>{action}</div>}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo, useMemo } from "react";
|
|
4
|
+
import * as Kanban from "@workspace/ui/components/kanban";
|
|
5
|
+
import { cn } from "@workspace/ui/lib/utils";
|
|
6
|
+
import { ColumnContent } from "./column-content";
|
|
7
|
+
import type { SerializedColumn, SerializedTask } from "../../../types";
|
|
8
|
+
|
|
9
|
+
interface KanbanBoardProps {
|
|
10
|
+
columns: (SerializedColumn & { tasks: SerializedTask[] })[];
|
|
11
|
+
kanbanState: Record<string, SerializedTask[]>;
|
|
12
|
+
onKanbanChange: (newData: Record<string, SerializedTask[]>) => void;
|
|
13
|
+
onAddTask: (columnId: string) => void;
|
|
14
|
+
onEditTask: (columnId: string, taskId: string) => void;
|
|
15
|
+
onEditColumn: (columnId: string) => void;
|
|
16
|
+
onDeleteColumn: (columnId: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function KanbanBoardComponent({
|
|
20
|
+
columns,
|
|
21
|
+
kanbanState,
|
|
22
|
+
onKanbanChange,
|
|
23
|
+
onAddTask,
|
|
24
|
+
onEditTask,
|
|
25
|
+
onEditColumn,
|
|
26
|
+
onDeleteColumn,
|
|
27
|
+
}: KanbanBoardProps) {
|
|
28
|
+
const orderedColumns = useMemo(() => {
|
|
29
|
+
const columnMap = new Map(columns.map((c) => [c.id, c]));
|
|
30
|
+
return Object.keys(kanbanState)
|
|
31
|
+
.map((columnId) => {
|
|
32
|
+
const column = columnMap.get(columnId);
|
|
33
|
+
if (!column) return null;
|
|
34
|
+
return {
|
|
35
|
+
...column,
|
|
36
|
+
tasks: kanbanState[columnId] || [],
|
|
37
|
+
};
|
|
38
|
+
})
|
|
39
|
+
.filter(
|
|
40
|
+
(c): c is SerializedColumn & { tasks: SerializedTask[] } => c !== null,
|
|
41
|
+
);
|
|
42
|
+
}, [columns, kanbanState]);
|
|
43
|
+
|
|
44
|
+
const mdClass = useMemo(() => {
|
|
45
|
+
const gridClassMap: Record<number, string> = {
|
|
46
|
+
1: "md:grid-cols-1",
|
|
47
|
+
2: "md:grid-cols-2",
|
|
48
|
+
3: "md:grid-cols-3",
|
|
49
|
+
4: "md:grid-cols-4",
|
|
50
|
+
5: "md:grid-cols-5",
|
|
51
|
+
6: "md:grid-cols-6",
|
|
52
|
+
};
|
|
53
|
+
return gridClassMap[orderedColumns.length] || "md:grid-cols-6";
|
|
54
|
+
}, [orderedColumns.length]);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Kanban.Root
|
|
58
|
+
orientation="horizontal"
|
|
59
|
+
value={kanbanState}
|
|
60
|
+
onValueChange={onKanbanChange}
|
|
61
|
+
getItemValue={(item: SerializedTask) => item.id}
|
|
62
|
+
>
|
|
63
|
+
<Kanban.Board
|
|
64
|
+
className={cn(
|
|
65
|
+
"flex flex-col gap-4 md:auto-rows-fr md:grid-cols-1 md:grid min-h-[400px]",
|
|
66
|
+
mdClass,
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
{orderedColumns.map((column) => (
|
|
70
|
+
<ColumnContent
|
|
71
|
+
key={column.id}
|
|
72
|
+
column={column}
|
|
73
|
+
onAddTask={() => onAddTask(column.id)}
|
|
74
|
+
onEditTask={(taskId) => onEditTask(column.id, taskId)}
|
|
75
|
+
onEditColumn={() => onEditColumn(column.id)}
|
|
76
|
+
onDeleteColumn={() => onDeleteColumn(column.id)}
|
|
77
|
+
/>
|
|
78
|
+
))}
|
|
79
|
+
</Kanban.Board>
|
|
80
|
+
<Kanban.Overlay>
|
|
81
|
+
<div className="size-full rounded-md bg-primary/10" />
|
|
82
|
+
</Kanban.Overlay>
|
|
83
|
+
</Kanban.Root>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const KanbanBoard = memo(KanbanBoardComponent);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode, HTMLAttributes } from "react";
|
|
4
|
+
import { cn } from "@workspace/ui/lib/utils";
|
|
5
|
+
|
|
6
|
+
interface PageWrapperProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function PageWrapper({
|
|
11
|
+
children,
|
|
12
|
+
className,
|
|
13
|
+
...props
|
|
14
|
+
}: PageWrapperProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className={cn("container mx-auto py-8 px-4", className)} {...props}>
|
|
17
|
+
{children}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo } from "react";
|
|
4
|
+
import { GripVertical } from "lucide-react";
|
|
5
|
+
import { Button } from "@workspace/ui/components/button";
|
|
6
|
+
import { Badge } from "@workspace/ui/components/badge";
|
|
7
|
+
import * as Kanban from "@workspace/ui/components/kanban";
|
|
8
|
+
import { format } from "date-fns";
|
|
9
|
+
import type { SerializedTask } from "../../../types";
|
|
10
|
+
import { getPriorityConfig } from "../../../utils";
|
|
11
|
+
import { useResolveUser } from "../../hooks/kanban-hooks";
|
|
12
|
+
import { UserAvatar } from "./user-avatar";
|
|
13
|
+
|
|
14
|
+
interface TaskCardProps {
|
|
15
|
+
task: SerializedTask;
|
|
16
|
+
onClick: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function TaskCardComponent({ task, onClick }: TaskCardProps) {
|
|
20
|
+
const priorityConfig = getPriorityConfig(task.priority);
|
|
21
|
+
const { data: assignee } = useResolveUser(task.assigneeId);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Kanban.Item value={task.id} asChild>
|
|
25
|
+
<div
|
|
26
|
+
className="rounded-md border bg-card p-3 shadow-xs cursor-pointer hover:shadow-md transition-shadow"
|
|
27
|
+
onClick={onClick}
|
|
28
|
+
>
|
|
29
|
+
<div className="flex flex-col gap-2">
|
|
30
|
+
<div className="flex items-center gap-2">
|
|
31
|
+
<Kanban.ItemHandle asChild>
|
|
32
|
+
<Button
|
|
33
|
+
variant="ghost"
|
|
34
|
+
size="icon"
|
|
35
|
+
className="h-6 w-6"
|
|
36
|
+
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
|
37
|
+
>
|
|
38
|
+
<GripVertical className="h-3 w-3" />
|
|
39
|
+
</Button>
|
|
40
|
+
</Kanban.ItemHandle>
|
|
41
|
+
<span
|
|
42
|
+
className="line-clamp-1 font-medium text-base flex-1 text-left cursor-pointer hover:text-primary"
|
|
43
|
+
title={task.title}
|
|
44
|
+
>
|
|
45
|
+
{task.title}
|
|
46
|
+
</span>
|
|
47
|
+
<Badge
|
|
48
|
+
variant={priorityConfig.variant}
|
|
49
|
+
className={`pointer-events-none h-5 rounded-sm px-1.5 text-[11px] capitalize ${priorityConfig.className}`}
|
|
50
|
+
>
|
|
51
|
+
{priorityConfig.label}
|
|
52
|
+
</Badge>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="flex items-center justify-between text-muted-foreground text-xs">
|
|
56
|
+
{task.assigneeId ? (
|
|
57
|
+
<div className="flex items-center gap-1.5">
|
|
58
|
+
<UserAvatar user={assignee ?? null} size="sm" />
|
|
59
|
+
<span className="line-clamp-1">
|
|
60
|
+
{assignee?.name || "Assigned"}
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
) : (
|
|
64
|
+
<div className="flex items-center gap-1.5">
|
|
65
|
+
<UserAvatar user={null} size="sm" />
|
|
66
|
+
<span className="line-clamp-1">Unassigned</span>
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
<time className="tabular-nums">
|
|
70
|
+
{format(new Date(task.createdAt), "MMM d")}
|
|
71
|
+
</time>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</Kanban.Item>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const TaskCard = memo(TaskCardComponent);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo } from "react";
|
|
4
|
+
import { User } from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
Avatar,
|
|
7
|
+
AvatarImage,
|
|
8
|
+
AvatarFallback,
|
|
9
|
+
} from "@workspace/ui/components/avatar";
|
|
10
|
+
import type { KanbanUser } from "../../overrides";
|
|
11
|
+
|
|
12
|
+
interface UserAvatarProps {
|
|
13
|
+
user: KanbanUser | null;
|
|
14
|
+
size?: "sm" | "default" | "lg";
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get initials from a user's name
|
|
20
|
+
*/
|
|
21
|
+
function getInitials(name: string): string {
|
|
22
|
+
const parts = name.trim().split(/\s+/);
|
|
23
|
+
if (parts.length === 1) {
|
|
24
|
+
return parts[0]?.charAt(0).toUpperCase() || "";
|
|
25
|
+
}
|
|
26
|
+
return (
|
|
27
|
+
(parts[0]?.charAt(0) || "") + (parts[parts.length - 1]?.charAt(0) || "")
|
|
28
|
+
).toUpperCase();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* UserAvatar component displays a user's avatar or initials fallback
|
|
33
|
+
* Uses the shadcn Avatar component with Radix primitives
|
|
34
|
+
*/
|
|
35
|
+
function UserAvatarComponent({
|
|
36
|
+
user,
|
|
37
|
+
size = "sm",
|
|
38
|
+
className,
|
|
39
|
+
}: UserAvatarProps) {
|
|
40
|
+
// No user - show placeholder icon
|
|
41
|
+
if (!user) {
|
|
42
|
+
return (
|
|
43
|
+
<Avatar size={size} className={className} title="Unassigned">
|
|
44
|
+
<AvatarFallback>
|
|
45
|
+
<User className="size-3 group-data-[size=default]/avatar:size-4 group-data-[size=lg]/avatar:size-5" />
|
|
46
|
+
</AvatarFallback>
|
|
47
|
+
</Avatar>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const initials = getInitials(user.name);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Avatar size={size} className={className} title={user.name}>
|
|
55
|
+
{user.avatarUrl && <AvatarImage src={user.avatarUrl} alt={user.name} />}
|
|
56
|
+
<AvatarFallback className="bg-primary/10 text-primary font-medium">
|
|
57
|
+
{initials}
|
|
58
|
+
</AvatarFallback>
|
|
59
|
+
</Avatar>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const UserAvatar = memo(UserAvatarComponent);
|