@burdenoff/microfe-workspaces 2026.530.2 → 2026.531.2
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/WorkspacesRoutes.js +19 -15
- package/dist/WorkspacesRoutes.js.map +1 -1
- package/dist/components/ProjectCard.js +2 -2
- package/dist/components/ProjectCard.js.map +1 -1
- package/dist/components/tags/ProjectTagsWidget.js +53 -57
- package/dist/components/tags/ProjectTagsWidget.js.map +1 -1
- package/dist/generated/wspace-operations.js +24 -9
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useMemberMutations.js +2 -2
- package/dist/hooks/useMemberMutations.js.map +1 -1
- package/dist/hooks/usePendingInvitations.js +29 -0
- package/dist/hooks/usePendingInvitations.js.map +1 -0
- package/dist/hooks/useProjectMutations.js +107 -6
- package/dist/hooks/useProjectMutations.js.map +1 -1
- package/dist/hooks/useProjects.js +1 -1
- package/dist/hooks/useProjects.js.map +1 -1
- package/dist/hooks/useWorkspaceMembers.js +1 -1
- package/dist/hooks/useWorkspaceMembers.js.map +1 -1
- package/dist/index.js +20 -18
- package/dist/pages/InviteAcceptancePage.js +68 -44
- package/dist/pages/InviteAcceptancePage.js.map +1 -1
- package/dist/pages/PendingInvitesPage.js +254 -0
- package/dist/pages/PendingInvitesPage.js.map +1 -0
- package/dist/pages/ProjectDetailPage.js +187 -161
- package/dist/pages/ProjectDetailPage.js.map +1 -1
- package/dist/pages/ProjectsListPage.js +2 -2
- package/dist/pages/ProjectsListPage.js.map +1 -1
- package/dist/pages/WorkspacesListPage.js +118 -102
- package/dist/pages/WorkspacesListPage.js.map +1 -1
- package/dist/utils/projectTags.js +28 -2
- package/dist/utils/projectTags.js.map +1 -1
- package/package.json +1 -1
package/dist/WorkspacesRoutes.js
CHANGED
|
@@ -5,14 +5,14 @@ import { FeatureGate as i } from "@burdenoff/fe-libs/shared/feature-flags";
|
|
|
5
5
|
import { jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
6
6
|
import { Navigate as s, Route as c, Routes as l } from "react-router-dom";
|
|
7
7
|
//#region src/WorkspacesRoutes.tsx
|
|
8
|
-
var u = r(() => import("./pages/CreateWorkspacePage.js").then((e) => ({ default: e.CreateWorkspacePage }))), d = r(() => import("./pages/WorkspaceDetailPage.js").then((e) => ({ default: e.WorkspaceDetailPage }))), f = r(() => import("./pages/WorkspaceSettingsPage.js").then((e) => ({ default: e.WorkspaceSettingsPage }))), p = r(() => import("./pages/ProjectsListPage.js").then((e) => ({ default: e.ProjectsListPage }))), m = r(() => import("./pages/ProjectDetailPage.js").then((e) => ({ default: e.ProjectDetailPage }))), h = r(() => import("./pages/MembersListPage.js").then((e) => ({ default: e.MembersListPage }))), g = r(() => import("./pages/ActivityListPage.js").then((e) => ({ default: e.ActivityListPage }))), _ = r(() => import("./pages/InviteAcceptancePage.js").then((e) => ({ default: e.InviteAcceptancePage })));
|
|
9
|
-
function
|
|
8
|
+
var u = r(() => import("./pages/CreateWorkspacePage.js").then((e) => ({ default: e.CreateWorkspacePage }))), d = r(() => import("./pages/WorkspaceDetailPage.js").then((e) => ({ default: e.WorkspaceDetailPage }))), f = r(() => import("./pages/WorkspaceSettingsPage.js").then((e) => ({ default: e.WorkspaceSettingsPage }))), p = r(() => import("./pages/ProjectsListPage.js").then((e) => ({ default: e.ProjectsListPage }))), m = r(() => import("./pages/ProjectDetailPage.js").then((e) => ({ default: e.ProjectDetailPage }))), h = r(() => import("./pages/MembersListPage.js").then((e) => ({ default: e.MembersListPage }))), g = r(() => import("./pages/ActivityListPage.js").then((e) => ({ default: e.ActivityListPage }))), _ = r(() => import("./pages/InviteAcceptancePage.js").then((e) => ({ default: e.InviteAcceptancePage }))), v = r(() => import("./pages/PendingInvitesPage.js").then((e) => ({ default: e.PendingInvitesPage })));
|
|
9
|
+
function y() {
|
|
10
10
|
return /* @__PURE__ */ a("div", {
|
|
11
11
|
className: "flex items-center justify-center h-64",
|
|
12
12
|
children: /* @__PURE__ */ a("div", { className: "animate-spin rounded-full size-8 border-b-2 border-primary" })
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
-
function
|
|
15
|
+
function b() {
|
|
16
16
|
return /* @__PURE__ */ a("div", {
|
|
17
17
|
className: "flex h-full items-center justify-center p-8 text-center",
|
|
18
18
|
children: /* @__PURE__ */ o("div", { children: [/* @__PURE__ */ a("h2", {
|
|
@@ -24,24 +24,24 @@ function y() {
|
|
|
24
24
|
})] })
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
|
-
function
|
|
27
|
+
function x(e, t, n = !1) {
|
|
28
28
|
return /* @__PURE__ */ a(i, {
|
|
29
29
|
flag: e,
|
|
30
30
|
defaultValue: n,
|
|
31
|
-
fallback: /* @__PURE__ */ a(
|
|
31
|
+
fallback: /* @__PURE__ */ a(b, {}),
|
|
32
32
|
children: t
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
-
function
|
|
35
|
+
function S() {
|
|
36
36
|
let { basePath: t = "/workspaces" } = e();
|
|
37
37
|
return /* @__PURE__ */ a(s, {
|
|
38
38
|
to: `${t}/list`,
|
|
39
39
|
replace: !0
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
-
function
|
|
42
|
+
function C() {
|
|
43
43
|
return /* @__PURE__ */ a(n, {
|
|
44
|
-
fallback: /* @__PURE__ */ a(
|
|
44
|
+
fallback: /* @__PURE__ */ a(y, {}),
|
|
45
45
|
children: /* @__PURE__ */ o(l, { children: [
|
|
46
46
|
/* @__PURE__ */ a(c, {
|
|
47
47
|
index: !0,
|
|
@@ -58,6 +58,10 @@ function S() {
|
|
|
58
58
|
path: "new",
|
|
59
59
|
element: /* @__PURE__ */ a(u, {})
|
|
60
60
|
}),
|
|
61
|
+
/* @__PURE__ */ a(c, {
|
|
62
|
+
path: "invites",
|
|
63
|
+
element: /* @__PURE__ */ a(v, {})
|
|
64
|
+
}),
|
|
61
65
|
/* @__PURE__ */ a(c, {
|
|
62
66
|
path: "invite/:invitationId",
|
|
63
67
|
element: /* @__PURE__ */ a(_, {})
|
|
@@ -68,7 +72,7 @@ function S() {
|
|
|
68
72
|
}),
|
|
69
73
|
/* @__PURE__ */ a(c, {
|
|
70
74
|
path: ":workspaceId/settings",
|
|
71
|
-
element:
|
|
75
|
+
element: x("workspaces.settings", /* @__PURE__ */ a(f, {}), !0)
|
|
72
76
|
}),
|
|
73
77
|
/* @__PURE__ */ a(c, {
|
|
74
78
|
path: "projects",
|
|
@@ -84,28 +88,28 @@ function S() {
|
|
|
84
88
|
}),
|
|
85
89
|
/* @__PURE__ */ a(c, {
|
|
86
90
|
path: "members",
|
|
87
|
-
element:
|
|
91
|
+
element: x("workspaces.members", /* @__PURE__ */ a(h, {}), !0)
|
|
88
92
|
}),
|
|
89
93
|
/* @__PURE__ */ a(c, {
|
|
90
94
|
path: ":workspaceId/members",
|
|
91
|
-
element:
|
|
95
|
+
element: x("workspaces.members", /* @__PURE__ */ a(h, {}), !0)
|
|
92
96
|
}),
|
|
93
97
|
/* @__PURE__ */ a(c, {
|
|
94
98
|
path: "activity",
|
|
95
|
-
element:
|
|
99
|
+
element: x("workspaces.activity", /* @__PURE__ */ a(g, {}))
|
|
96
100
|
}),
|
|
97
101
|
/* @__PURE__ */ a(c, {
|
|
98
102
|
path: ":workspaceId/activity",
|
|
99
|
-
element:
|
|
103
|
+
element: x("workspaces.activity", /* @__PURE__ */ a(g, {}))
|
|
100
104
|
}),
|
|
101
105
|
/* @__PURE__ */ a(c, {
|
|
102
106
|
path: "*",
|
|
103
|
-
element: /* @__PURE__ */ a(
|
|
107
|
+
element: /* @__PURE__ */ a(S, {})
|
|
104
108
|
})
|
|
105
109
|
] })
|
|
106
110
|
});
|
|
107
111
|
}
|
|
108
112
|
//#endregion
|
|
109
|
-
export {
|
|
113
|
+
export { C as WorkspacesRoutes };
|
|
110
114
|
|
|
111
115
|
//# sourceMappingURL=WorkspacesRoutes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkspacesRoutes.js","names":[],"sources":["../src/WorkspacesRoutes.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { Routes, Route, Navigate } from 'react-router-dom';\nimport { FeatureGate } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useWorkspacesContext } from './providers/WorkspacesProvider';\n\n// Eager load - main entry point\nimport { WorkspacesListPage } from './pages/WorkspacesListPage';\n\n// Lazy load - secondary pages\nconst CreateWorkspacePage = lazy(() =>\n import('./pages/CreateWorkspacePage').then((m) => ({ default: m.CreateWorkspacePage }))\n);\nconst WorkspaceDetailPage = lazy(() =>\n import('./pages/WorkspaceDetailPage').then((m) => ({ default: m.WorkspaceDetailPage }))\n);\nconst WorkspaceSettingsPage = lazy(() =>\n import('./pages/WorkspaceSettingsPage').then((m) => ({ default: m.WorkspaceSettingsPage }))\n);\nconst ProjectsListPage = lazy(() =>\n import('./pages/ProjectsListPage').then((m) => ({ default: m.ProjectsListPage }))\n);\nconst ProjectDetailPage = lazy(() =>\n import('./pages/ProjectDetailPage').then((m) => ({ default: m.ProjectDetailPage }))\n);\nconst MembersListPage = lazy(() =>\n import('./pages/MembersListPage').then((m) => ({ default: m.MembersListPage }))\n);\nconst ActivityListPage = lazy(() =>\n import('./pages/ActivityListPage').then((m) => ({ default: m.ActivityListPage }))\n);\nconst InviteAcceptancePage = lazy(() =>\n import('./pages/InviteAcceptancePage').then((m) => ({ default: m.InviteAcceptancePage }))\n);\n\n/**\n * Loading fallback for lazy-loaded components\n */\nfunction PageLoader() {\n return (\n <div className=\"flex items-center justify-center h-64\">\n <div className=\"animate-spin rounded-full size-8 border-b-2 border-primary\" />\n </div>\n );\n}\n\n/**\n * Fallback shown when a feature-gated route is disabled.\n */\nfunction FeatureDisabledFallback() {\n return (\n <div className=\"flex h-full items-center justify-center p-8 text-center\">\n <div>\n <h2 className=\"text-lg font-semibold mb-2 text-text-primary\">Feature Not Available</h2>\n <p className=\"text-sm text-text-secondary\">\n This feature is not currently available. Enable it under Feature Previews to access this\n functionality.\n </p>\n </div>\n </div>\n );\n}\n\n/**\n * Helper: wraps a page element in a FeatureGate.\n * defaultValue=false → hidden until explicitly enabled (unreleased feature).\n */\nfunction gated(flag: string, element: React.ReactNode, defaultValue = false): React.ReactElement {\n return (\n <FeatureGate flag={flag} defaultValue={defaultValue} fallback={<FeatureDisabledFallback />}>\n {element}\n </FeatureGate>\n );\n}\n\n/**\n * Catch-all redirect component that uses the basePath from context\n * to navigate to an absolute path, preventing infinite redirect loops.\n */\nfunction CatchAllRedirect() {\n const { basePath = '/workspaces' } = useWorkspacesContext();\n return <Navigate to={`${basePath}/list`} replace />;\n}\n\n/**\n * Internal routing for the Workspaces microfrontend.\n * Defines all routes and their corresponding page components.\n *\n * - WorkspacesListPage: Eager loaded (main entry point)\n * - All other pages: Lazy loaded for better performance\n * - Projects, Members, Activity: gated by feature flags (unreleased)\n */\nexport function WorkspacesRoutes() {\n return (\n <Suspense fallback={<PageLoader />}>\n <Routes>\n {/* Default redirect */}\n <Route index element={<Navigate to=\"list\" replace />} />\n\n {/* Workspaces List - Eager loaded */}\n <Route path=\"list\" element={<WorkspacesListPage />} />\n\n {/* Create Workspace (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"new\" element={<CreateWorkspacePage />} />\n\n {/* Invitation Acceptance (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"invite/:invitationId\" element={<InviteAcceptancePage />} />\n\n {/* Workspace Detail */}\n <Route path=\":workspaceId\" element={<WorkspaceDetailPage />} />\n <Route\n path=\":workspaceId/settings\"\n element={gated('workspaces.settings', <WorkspaceSettingsPage />, true)}\n />\n\n {/* Projects — core feature, never gated */}\n <Route path=\"projects\" element={<ProjectsListPage />} />\n <Route path=\":workspaceId/projects\" element={<ProjectsListPage />} />\n <Route path=\":workspaceId/projects/:projectId\" element={<ProjectDetailPage />} />\n\n {/* Members — gated by workspaces.members */}\n <Route path=\"members\" element={gated('workspaces.members', <MembersListPage />, true)} />\n <Route\n path=\":workspaceId/members\"\n element={gated('workspaces.members', <MembersListPage />, true)}\n />\n\n {/* Activity — gated by workspaces.activity */}\n <Route path=\"activity\" element={gated('workspaces.activity', <ActivityListPage />)} />\n <Route\n path=\":workspaceId/activity\"\n element={gated('workspaces.activity', <ActivityListPage />)}\n />\n\n {/* Catch-all redirect - uses absolute path to prevent infinite loops */}\n <Route path=\"*\" element={<CatchAllRedirect />} />\n </Routes>\n </Suspense>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WorkspacesRoutes.js","names":[],"sources":["../src/WorkspacesRoutes.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { Routes, Route, Navigate } from 'react-router-dom';\nimport { FeatureGate } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useWorkspacesContext } from './providers/WorkspacesProvider';\n\n// Eager load - main entry point\nimport { WorkspacesListPage } from './pages/WorkspacesListPage';\n\n// Lazy load - secondary pages\nconst CreateWorkspacePage = lazy(() =>\n import('./pages/CreateWorkspacePage').then((m) => ({ default: m.CreateWorkspacePage }))\n);\nconst WorkspaceDetailPage = lazy(() =>\n import('./pages/WorkspaceDetailPage').then((m) => ({ default: m.WorkspaceDetailPage }))\n);\nconst WorkspaceSettingsPage = lazy(() =>\n import('./pages/WorkspaceSettingsPage').then((m) => ({ default: m.WorkspaceSettingsPage }))\n);\nconst ProjectsListPage = lazy(() =>\n import('./pages/ProjectsListPage').then((m) => ({ default: m.ProjectsListPage }))\n);\nconst ProjectDetailPage = lazy(() =>\n import('./pages/ProjectDetailPage').then((m) => ({ default: m.ProjectDetailPage }))\n);\nconst MembersListPage = lazy(() =>\n import('./pages/MembersListPage').then((m) => ({ default: m.MembersListPage }))\n);\nconst ActivityListPage = lazy(() =>\n import('./pages/ActivityListPage').then((m) => ({ default: m.ActivityListPage }))\n);\nconst InviteAcceptancePage = lazy(() =>\n import('./pages/InviteAcceptancePage').then((m) => ({ default: m.InviteAcceptancePage }))\n);\nconst PendingInvitesPage = lazy(() =>\n import('./pages/PendingInvitesPage').then((m) => ({ default: m.PendingInvitesPage }))\n);\n\n/**\n * Loading fallback for lazy-loaded components\n */\nfunction PageLoader() {\n return (\n <div className=\"flex items-center justify-center h-64\">\n <div className=\"animate-spin rounded-full size-8 border-b-2 border-primary\" />\n </div>\n );\n}\n\n/**\n * Fallback shown when a feature-gated route is disabled.\n */\nfunction FeatureDisabledFallback() {\n return (\n <div className=\"flex h-full items-center justify-center p-8 text-center\">\n <div>\n <h2 className=\"text-lg font-semibold mb-2 text-text-primary\">Feature Not Available</h2>\n <p className=\"text-sm text-text-secondary\">\n This feature is not currently available. Enable it under Feature Previews to access this\n functionality.\n </p>\n </div>\n </div>\n );\n}\n\n/**\n * Helper: wraps a page element in a FeatureGate.\n * defaultValue=false → hidden until explicitly enabled (unreleased feature).\n */\nfunction gated(flag: string, element: React.ReactNode, defaultValue = false): React.ReactElement {\n return (\n <FeatureGate flag={flag} defaultValue={defaultValue} fallback={<FeatureDisabledFallback />}>\n {element}\n </FeatureGate>\n );\n}\n\n/**\n * Catch-all redirect component that uses the basePath from context\n * to navigate to an absolute path, preventing infinite redirect loops.\n */\nfunction CatchAllRedirect() {\n const { basePath = '/workspaces' } = useWorkspacesContext();\n return <Navigate to={`${basePath}/list`} replace />;\n}\n\n/**\n * Internal routing for the Workspaces microfrontend.\n * Defines all routes and their corresponding page components.\n *\n * - WorkspacesListPage: Eager loaded (main entry point)\n * - All other pages: Lazy loaded for better performance\n * - Projects, Members, Activity: gated by feature flags (unreleased)\n */\nexport function WorkspacesRoutes() {\n return (\n <Suspense fallback={<PageLoader />}>\n <Routes>\n {/* Default redirect */}\n <Route index element={<Navigate to=\"list\" replace />} />\n\n {/* Workspaces List - Eager loaded */}\n <Route path=\"list\" element={<WorkspacesListPage />} />\n\n {/* Create Workspace (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"new\" element={<CreateWorkspacePage />} />\n\n {/* Pending Invites (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"invites\" element={<PendingInvitesPage />} />\n\n {/* Invitation Acceptance (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"invite/:invitationId\" element={<InviteAcceptancePage />} />\n\n {/* Workspace Detail */}\n <Route path=\":workspaceId\" element={<WorkspaceDetailPage />} />\n <Route\n path=\":workspaceId/settings\"\n element={gated('workspaces.settings', <WorkspaceSettingsPage />, true)}\n />\n\n {/* Projects — core feature, never gated */}\n <Route path=\"projects\" element={<ProjectsListPage />} />\n <Route path=\":workspaceId/projects\" element={<ProjectsListPage />} />\n <Route path=\":workspaceId/projects/:projectId\" element={<ProjectDetailPage />} />\n\n {/* Members — gated by workspaces.members */}\n <Route path=\"members\" element={gated('workspaces.members', <MembersListPage />, true)} />\n <Route\n path=\":workspaceId/members\"\n element={gated('workspaces.members', <MembersListPage />, true)}\n />\n\n {/* Activity — gated by workspaces.activity */}\n <Route path=\"activity\" element={gated('workspaces.activity', <ActivityListPage />)} />\n <Route\n path=\":workspaceId/activity\"\n element={gated('workspaces.activity', <ActivityListPage />)}\n />\n\n {/* Catch-all redirect - uses absolute path to prevent infinite loops */}\n <Route path=\"*\" element={<CatchAllRedirect />} />\n </Routes>\n </Suspense>\n );\n}\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import { useScopedNavigation as n } from "../hooks/useScopedNavigation.js";
|
|
|
4
4
|
import { nativeConfirm as r, nativeImpact as i } from "../utils/nativeBridge.js";
|
|
5
5
|
import { useArchiveProject as a, useDeleteProject as o, useUnarchiveProject as s } from "../hooks/useProjectMutations.js";
|
|
6
6
|
import { formatDate as c } from "../utils/formatters.js";
|
|
7
|
-
import {
|
|
7
|
+
import { getProjectDisplayTags as l } from "../utils/projectTags.js";
|
|
8
8
|
import { Archive as u, FolderKanban as d, MoreVertical as f } from "lucide-react";
|
|
9
9
|
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
10
10
|
import { Badge as h, Button as g, Card as _, CardContent as v, CardHeader as y, CardTitle as b, DropdownMenu as x, DropdownMenuContent as S, DropdownMenuItem as C, DropdownMenuTrigger as w } from "@burdenoff/fe-libs/ui";
|
|
@@ -12,7 +12,7 @@ import { useNavigate as T } from "react-router-dom";
|
|
|
12
12
|
import { TagBadge as E } from "@burdenoff/fe-libs/tag";
|
|
13
13
|
//#region src/components/ProjectCard.tsx
|
|
14
14
|
function D({ project: D, selected: O = !1, onToggleSelect: k, onTagClick: A, canArchive: j = !1, canDelete: M = !1 }) {
|
|
15
|
-
let N = T(), { basePath: P = "/workspaces" } = e(), { withCurrentSearch: F } = n(), { mutate: I } = a(D.workspaceId), { mutate: L } = s(D.workspaceId), { mutate: R } = o(D.workspaceId), z = l(D
|
|
15
|
+
let N = T(), { basePath: P = "/workspaces" } = e(), { withCurrentSearch: F } = n(), { mutate: I } = a(D.workspaceId), { mutate: L } = s(D.workspaceId), { mutate: R } = o(D.workspaceId), z = l(D), B = () => {
|
|
16
16
|
N(F(`${P}/${D.workspaceId}/projects/${D.id}`));
|
|
17
17
|
};
|
|
18
18
|
return /* @__PURE__ */ m(_, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectCard.js","names":[],"sources":["../../src/components/ProjectCard.tsx"],"sourcesContent":["import type React from 'react';\nimport { FolderKanban, Archive, MoreVertical } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\n\nimport type { Project, ProjectTag } from '../types';\nimport { formatDate } from '../utils/formatters';\nimport { normalizeProjectTags } from '../utils/projectTags';\nimport { getProjectStatusClasses } from '../utils/tokenHelpers';\nimport {\n useArchiveProject,\n useUnarchiveProject,\n useDeleteProject,\n} from '../hooks/useProjectMutations';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { useScopedNavigation } from '../hooks/useScopedNavigation';\nimport { nativeConfirm, nativeImpact } from '../utils/nativeBridge';\n\ninterface ProjectCardProps {\n project: Project;\n selected?: boolean;\n onToggleSelect?: (checked: boolean) => void;\n onTagClick?: (tag: ProjectTag) => void;\n canArchive?: boolean;\n canDelete?: boolean;\n}\n\nexport function ProjectCard({\n project,\n selected = false,\n onToggleSelect,\n onTagClick,\n canArchive = false,\n canDelete = false,\n}: ProjectCardProps) {\n const navigate = useNavigate();\n const { basePath = '/workspaces' } = useWorkspacesContext();\n const { withCurrentSearch } = useScopedNavigation();\n const { mutate: archiveProject } = useArchiveProject(project.workspaceId);\n const { mutate: unarchiveProject } = useUnarchiveProject(project.workspaceId);\n const { mutate: deleteProject } = useDeleteProject(project.workspaceId);\n const projectTags = normalizeProjectTags(project.tags);\n\n const handleCardClick = () => {\n navigate(withCurrentSearch(`${basePath}/${project.workspaceId}/projects/${project.id}`));\n };\n\n const handleArchive = (e: React.MouseEvent) => {\n e.stopPropagation();\n if (project.archived) {\n unarchiveProject(project.id);\n } else {\n archiveProject(project.id);\n }\n };\n\n const handleDelete = async (e: React.MouseEvent) => {\n e.stopPropagation();\n void nativeImpact('medium');\n const native = await nativeConfirm({\n title: 'Delete project',\n message: `Are you sure you want to delete \"${project.name}\"?`,\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n const ok =\n native === null ? confirm(`Are you sure you want to delete \"${project.name}\"?`) : native;\n if (ok) {\n deleteProject(project.id);\n }\n };\n\n const handleCardKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n handleCardClick();\n }\n };\n\n return (\n <Card\n className={`cursor-pointer relative ${\n selected ? 'ring-2 ring-primary/25 border-primary/50' : ''\n }`}\n onClick={handleCardClick}\n onKeyDown={handleCardKeyDown}\n role=\"button\"\n tabIndex={0}\n aria-label={`Open project ${project.name}`}\n >\n <CardHeader>\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3 flex-1\">\n {onToggleSelect && (\n <input\n type=\"checkbox\"\n checked={selected}\n onChange={(event) => onToggleSelect(event.target.checked)}\n onClick={(event) => event.stopPropagation()}\n aria-label={`Select project ${project.name}`}\n className=\"mt-1 size-4 rounded border-border text-primary\"\n />\n )}\n <div className=\"p-3 rounded-lg bg-primary/10 transition-colors\">\n <FolderKanban className=\"text-primary\" size={24} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <CardTitle className=\"truncate\">{project.name}</CardTitle>\n <p className=\"truncate text-sm text-muted-foreground\">{project.key}</p>\n </div>\n </div>\n <DropdownMenu>\n <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>\n <Button variant=\"ghost\" size=\"sm\" className=\"size-8 p-0\">\n <MoreVertical size={16} />\n <span className=\"sr-only\">Open project actions</span>\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {canArchive && (\n <DropdownMenuItem onClick={handleArchive}>\n <Archive size={16} className=\"mr-2\" />\n {project.archived ? 'Unarchive' : 'Archive'}\n </DropdownMenuItem>\n )}\n {canDelete && (\n <DropdownMenuItem onClick={handleDelete} className=\"text-destructive\">\n Delete\n </DropdownMenuItem>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"flex items-center gap-2\">\n <Badge className={`border ${getProjectStatusClasses(project.status)}`}>\n {project.status}\n </Badge>\n {project.archived && (\n <Badge\n variant=\"outline\"\n className=\"border-workspace-archived-border text-workspace-archived-text\"\n >\n <Archive size={12} className=\"mr-1\" />\n Archived\n </Badge>\n )}\n </div>\n <div className=\"text-xs text-muted-foreground\">Created {formatDate(project.createdAt)}</div>\n {projectTags.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5\" aria-label=\"Project tags\">\n {projectTags.slice(0, 4).map((tag) => (\n <span\n key={tag.tagId}\n {...(onTagClick\n ? {\n role: 'button',\n tabIndex: 0,\n onClick: (event: React.MouseEvent<HTMLSpanElement>) => {\n event.stopPropagation();\n onTagClick(tag);\n },\n onKeyDown: (event: React.KeyboardEvent<HTMLSpanElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n event.stopPropagation();\n onTagClick(tag);\n }\n },\n }\n : {})}\n >\n <TagBadge\n id={tag.tagId}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n size=\"sm\"\n className={onTagClick ? 'cursor-pointer' : undefined}\n />\n </span>\n ))}\n {projectTags.length > 4 && (\n <Badge variant=\"outline\" className=\"text-xs\">\n +{projectTags.length - 4}\n </Badge>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProjectCard.js","names":[],"sources":["../../src/components/ProjectCard.tsx"],"sourcesContent":["import type React from 'react';\nimport { FolderKanban, Archive, MoreVertical } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\n\nimport type { Project, ProjectTag } from '../types';\nimport { formatDate } from '../utils/formatters';\nimport { getProjectDisplayTags } from '../utils/projectTags';\nimport { getProjectStatusClasses } from '../utils/tokenHelpers';\nimport {\n useArchiveProject,\n useUnarchiveProject,\n useDeleteProject,\n} from '../hooks/useProjectMutations';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { useScopedNavigation } from '../hooks/useScopedNavigation';\nimport { nativeConfirm, nativeImpact } from '../utils/nativeBridge';\n\ninterface ProjectCardProps {\n project: Project;\n selected?: boolean;\n onToggleSelect?: (checked: boolean) => void;\n onTagClick?: (tag: ProjectTag) => void;\n canArchive?: boolean;\n canDelete?: boolean;\n}\n\nexport function ProjectCard({\n project,\n selected = false,\n onToggleSelect,\n onTagClick,\n canArchive = false,\n canDelete = false,\n}: ProjectCardProps) {\n const navigate = useNavigate();\n const { basePath = '/workspaces' } = useWorkspacesContext();\n const { withCurrentSearch } = useScopedNavigation();\n const { mutate: archiveProject } = useArchiveProject(project.workspaceId);\n const { mutate: unarchiveProject } = useUnarchiveProject(project.workspaceId);\n const { mutate: deleteProject } = useDeleteProject(project.workspaceId);\n const projectTags = getProjectDisplayTags(project);\n\n const handleCardClick = () => {\n navigate(withCurrentSearch(`${basePath}/${project.workspaceId}/projects/${project.id}`));\n };\n\n const handleArchive = (e: React.MouseEvent) => {\n e.stopPropagation();\n if (project.archived) {\n unarchiveProject(project.id);\n } else {\n archiveProject(project.id);\n }\n };\n\n const handleDelete = async (e: React.MouseEvent) => {\n e.stopPropagation();\n void nativeImpact('medium');\n const native = await nativeConfirm({\n title: 'Delete project',\n message: `Are you sure you want to delete \"${project.name}\"?`,\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n const ok =\n native === null ? confirm(`Are you sure you want to delete \"${project.name}\"?`) : native;\n if (ok) {\n deleteProject(project.id);\n }\n };\n\n const handleCardKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n handleCardClick();\n }\n };\n\n return (\n <Card\n className={`cursor-pointer relative ${\n selected ? 'ring-2 ring-primary/25 border-primary/50' : ''\n }`}\n onClick={handleCardClick}\n onKeyDown={handleCardKeyDown}\n role=\"button\"\n tabIndex={0}\n aria-label={`Open project ${project.name}`}\n >\n <CardHeader>\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3 flex-1\">\n {onToggleSelect && (\n <input\n type=\"checkbox\"\n checked={selected}\n onChange={(event) => onToggleSelect(event.target.checked)}\n onClick={(event) => event.stopPropagation()}\n aria-label={`Select project ${project.name}`}\n className=\"mt-1 size-4 rounded border-border text-primary\"\n />\n )}\n <div className=\"p-3 rounded-lg bg-primary/10 transition-colors\">\n <FolderKanban className=\"text-primary\" size={24} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <CardTitle className=\"truncate\">{project.name}</CardTitle>\n <p className=\"truncate text-sm text-muted-foreground\">{project.key}</p>\n </div>\n </div>\n <DropdownMenu>\n <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>\n <Button variant=\"ghost\" size=\"sm\" className=\"size-8 p-0\">\n <MoreVertical size={16} />\n <span className=\"sr-only\">Open project actions</span>\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {canArchive && (\n <DropdownMenuItem onClick={handleArchive}>\n <Archive size={16} className=\"mr-2\" />\n {project.archived ? 'Unarchive' : 'Archive'}\n </DropdownMenuItem>\n )}\n {canDelete && (\n <DropdownMenuItem onClick={handleDelete} className=\"text-destructive\">\n Delete\n </DropdownMenuItem>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"flex items-center gap-2\">\n <Badge className={`border ${getProjectStatusClasses(project.status)}`}>\n {project.status}\n </Badge>\n {project.archived && (\n <Badge\n variant=\"outline\"\n className=\"border-workspace-archived-border text-workspace-archived-text\"\n >\n <Archive size={12} className=\"mr-1\" />\n Archived\n </Badge>\n )}\n </div>\n <div className=\"text-xs text-muted-foreground\">Created {formatDate(project.createdAt)}</div>\n {projectTags.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5\" aria-label=\"Project tags\">\n {projectTags.slice(0, 4).map((tag) => (\n <span\n key={tag.tagId}\n {...(onTagClick\n ? {\n role: 'button',\n tabIndex: 0,\n onClick: (event: React.MouseEvent<HTMLSpanElement>) => {\n event.stopPropagation();\n onTagClick(tag);\n },\n onKeyDown: (event: React.KeyboardEvent<HTMLSpanElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n event.stopPropagation();\n onTagClick(tag);\n }\n },\n }\n : {})}\n >\n <TagBadge\n id={tag.tagId}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n size=\"sm\"\n className={onTagClick ? 'cursor-pointer' : undefined}\n />\n </span>\n ))}\n {projectTags.length > 4 && (\n <Badge variant=\"outline\" className=\"text-xs\">\n +{projectTags.length - 4}\n </Badge>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAuCA,SAAgB,EAAY,EAC1B,YACA,cAAW,IACX,mBACA,eACA,gBAAa,IACb,eAAY,MACO;CACnB,IAAM,IAAW,GAAa,EACxB,EAAE,cAAW,kBAAkB,GAAsB,EACrD,EAAE,yBAAsB,GAAqB,EAC7C,EAAE,QAAQ,MAAmB,EAAkB,EAAQ,YAAY,EACnE,EAAE,QAAQ,MAAqB,EAAoB,EAAQ,YAAY,EACvE,EAAE,QAAQ,MAAkB,EAAiB,EAAQ,YAAY,EACjE,IAAc,EAAsB,EAAQ,EAE5C,UAAwB;AAC5B,IAAS,EAAkB,GAAG,EAAS,GAAG,EAAQ,YAAY,YAAY,EAAQ,KAAK,CAAC;;AAmC1F,QACE,kBAAC,GAAD;EACE,WAAW,2BACT,IAAW,6CAA6C;EAE1D,SAAS;EACT,YAbuB,MAA+C;AACxE,IAAI,EAAM,QAAQ,WAAW,EAAM,QAAQ,SACzC,EAAM,gBAAgB,EACtB,GAAiB;;EAWjB,MAAK;EACL,UAAU;EACV,cAAY,gBAAgB,EAAQ;YARtC,CAUE,kBAAC,GAAD,EAAA,UACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,KACC,kBAAC,SAAD;MACE,MAAK;MACL,SAAS;MACT,WAAW,MAAU,EAAe,EAAM,OAAO,QAAQ;MACzD,UAAU,MAAU,EAAM,iBAAiB;MAC3C,cAAY,kBAAkB,EAAQ;MACtC,WAAU;MACV,CAAA;KAEJ,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAc,WAAU;OAAe,MAAM;OAAM,CAAA;MAC/C,CAAA;KACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OAAW,WAAU;iBAAY,EAAQ;OAAiB,CAAA,EAC1D,kBAAC,KAAD;OAAG,WAAU;iBAA0C,EAAQ;OAAQ,CAAA,CACnE;;KACF;OACN,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;IAAqB,SAAA;IAAQ,UAAU,MAAM,EAAE,iBAAiB;cAC9D,kBAAC,GAAD;KAAQ,SAAQ;KAAQ,MAAK;KAAK,WAAU;eAA5C,CACE,kBAAC,GAAD,EAAc,MAAM,IAAM,CAAA,EAC1B,kBAAC,QAAD;MAAM,WAAU;gBAAU;MAA2B,CAAA,CAC9C;;IACW,CAAA,EACtB,kBAAC,GAAD;IAAqB,OAAM;cAA3B,CACG,KACC,kBAAC,GAAD;KAAkB,UAzET,MAAwB;AAE7C,MADA,EAAE,iBAAiB,EACf,EAAQ,WACV,EAAiB,EAAQ,GAAG,GAE5B,EAAe,EAAQ,GAAG;;eAoEhB,CACE,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAS,CAAA,EACrC,EAAQ,WAAW,cAAc,UACjB;QAEpB,KACC,kBAAC,GAAD;KAAkB,SAtEX,OAAO,MAAwB;AAE7C,MADL,EAAE,iBAAiB,EACd,EAAa,SAAS;MAC3B,IAAM,IAAS,MAAM,EAAc;OACjC,OAAO;OACP,SAAS,oCAAoC,EAAQ,KAAK;OAC1D,eAAe;OACf,mBAAmB;OACpB,CAAC;AAGF,OADE,MAAW,OAAO,QAAQ,oCAAoC,EAAQ,KAAK,IAAI,GAAG,MAElF,EAAc,EAAQ,GAAG;;KA0D0B,WAAU;eAAmB;KAEnD,CAAA,CAED;MACT,EAAA,CAAA,CACX;MACK,CAAA,EACb,kBAAC,GAAD;GAAa,WAAU;aAAvB;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAO,WAAW,UAAU,EAAwB,EAAQ,OAAO;gBAChE,EAAQ;MACH,CAAA,EACP,EAAQ,YACP,kBAAC,GAAD;MACE,SAAQ;MACR,WAAU;gBAFZ,CAIE,kBAAC,GAAD;OAAS,MAAM;OAAI,WAAU;OAAS,CAAA,EAAA,WAEhC;QAEN;;IACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CAA+C,YAAS,EAAW,EAAQ,UAAU,CAAO;;IAC3F,EAAY,SAAS,KACpB,kBAAC,OAAD;KAAK,WAAU;KAAyB,cAAW;eAAnD,CACG,EAAY,MAAM,GAAG,EAAE,CAAC,KAAK,MAC5B,kBAAC,QAAD;MAEE,GAAK,IACD;OACE,MAAM;OACN,UAAU;OACV,UAAU,MAA6C;AAErD,QADA,EAAM,iBAAiB,EACvB,EAAW,EAAI;;OAEjB,YAAY,MAAgD;AAC1D,SAAI,EAAM,QAAQ,WAAW,EAAM,QAAQ,SACzC,EAAM,gBAAgB,EACtB,EAAM,iBAAiB,EACvB,EAAW,EAAI;;OAGpB,GACD,EAAE;gBAEN,kBAAC,GAAD;OACE,IAAI,EAAI;OACR,OAAO,EAAI;OACX,OAAO,EAAI,SAAS,KAAA;OACpB,MAAM,EAAI,QAAQ,KAAA;OAClB,aAAa,EAAI,eAAe,KAAA;OAChC,MAAK;OACL,WAAW,IAAa,mBAAmB,KAAA;OAC3C,CAAA;MACG,EA5BA,EAAI,MA4BJ,CACP,EACD,EAAY,SAAS,KACpB,kBAAC,GAAD;MAAO,SAAQ;MAAU,WAAU;gBAAnC,CAA6C,KACzC,EAAY,SAAS,EACjB;QAEN;;IAEI;KACT"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { useCallback as e, useEffect as t, useRef as n, useState as r } from "react";
|
|
2
|
-
import { Loader2 as i, Plus as
|
|
3
|
-
import { jsx as
|
|
4
|
-
import { Command as
|
|
2
|
+
import { Loader2 as i, Plus as ee, Tag as a, X as te } from "lucide-react";
|
|
3
|
+
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
4
|
+
import { Command as ne, CommandEmpty as c, CommandGroup as l, CommandInput as u, CommandItem as d, CommandList as f, Popover as p, PopoverContent as m, PopoverTrigger as re } from "@burdenoff/fe-libs/ui";
|
|
5
5
|
import { cn as h } from "@burdenoff/fe-libs/shared/utils";
|
|
6
|
-
import { useGatewayClient as
|
|
6
|
+
import { useGatewayClient as ie } from "@burdenoff/fe-libs/shared/graphql";
|
|
7
7
|
import { gql as g } from "@apollo/client";
|
|
8
8
|
import { useLazyQuery as _, useMutation as v } from "@apollo/client/react";
|
|
9
|
-
import { TagBadge as
|
|
9
|
+
import { TagBadge as y } from "@burdenoff/fe-libs/tag";
|
|
10
10
|
//#region src/components/tags/ProjectTagsWidget.tsx
|
|
11
|
-
var
|
|
11
|
+
var b = g`
|
|
12
12
|
query ProjectTagsWidget_SearchTags($query: String!, $vocabularyId: ID, $limit: Int) {
|
|
13
13
|
searchTags(query: $query, vocabularyId: $vocabularyId, limit: $limit) {
|
|
14
14
|
id
|
|
@@ -26,7 +26,7 @@ var se = g`
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
`,
|
|
29
|
+
`, x = g`
|
|
30
30
|
query ProjectTagsWidget_GetVocabularyByKey($key: String!) {
|
|
31
31
|
getVocabularyByKey(key: $key) {
|
|
32
32
|
id
|
|
@@ -34,7 +34,7 @@ var se = g`
|
|
|
34
34
|
name
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
`,
|
|
37
|
+
`, ae = g`
|
|
38
38
|
mutation ProjectTagsWidget_CreateVocabulary($input: CreateVocabularyInput!) {
|
|
39
39
|
createVocabulary(input: $input) {
|
|
40
40
|
id
|
|
@@ -42,7 +42,7 @@ var se = g`
|
|
|
42
42
|
name
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
`,
|
|
45
|
+
`, oe = g`
|
|
46
46
|
mutation ProjectTagsWidget_CreateTag($input: CreateTagInput!) {
|
|
47
47
|
createTag(input: $input) {
|
|
48
48
|
id
|
|
@@ -59,19 +59,19 @@ var se = g`
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
`,
|
|
63
|
-
function
|
|
62
|
+
`, S = "general";
|
|
63
|
+
function C(e) {
|
|
64
64
|
if (!e || typeof e != "object") return !1;
|
|
65
65
|
let t = e;
|
|
66
66
|
return typeof t.id == "string" && typeof t.key == "string" && typeof t.label == "string";
|
|
67
67
|
}
|
|
68
|
-
function
|
|
69
|
-
let [
|
|
68
|
+
function w({ entityType: g, entityId: w, workspaceId: se, tags: T, onAdd: E, onRemove: D, allowCreate: O = !0, maxTags: k = 20, className: ce }) {
|
|
69
|
+
let [le, A] = r(!1), [j, M] = r(""), [N, P] = r(null), [F, I] = r(!1), [L, R] = r(), z = n(null), B = ie("workspace"), V = !se || !w, H = new Set(T.map((e) => e.id)), U = T.length < k, [W, { data: ue, loading: G }] = _(b, { client: B }), K = (ue?.searchTags ?? []).filter((e) => C(e) && !H.has(e.id)), [q, { loading: J }] = v(oe, { client: B }), [Y] = _(x, { client: B }), [X] = v(ae, { client: B }), Z = e(async () => {
|
|
70
70
|
if (L) return L;
|
|
71
|
-
let e = (await Y({ variables: { key:
|
|
71
|
+
let e = (await Y({ variables: { key: S } })).data?.getVocabularyByKey;
|
|
72
72
|
if (e) return R(e.id), e.id;
|
|
73
73
|
let t = (await X({ variables: { input: {
|
|
74
|
-
key:
|
|
74
|
+
key: S,
|
|
75
75
|
name: "General",
|
|
76
76
|
description: "Default vocabulary for general-purpose tags",
|
|
77
77
|
scope: "WORKSPACE",
|
|
@@ -83,7 +83,7 @@ function C({ entityType: g, entityId: C, workspaceId: w, tags: T, onAdd: E, onRe
|
|
|
83
83
|
L,
|
|
84
84
|
Y,
|
|
85
85
|
X
|
|
86
|
-
]),
|
|
86
|
+
]), de = e((e) => {
|
|
87
87
|
M(e), N && P(null), z.current && clearTimeout(z.current), e.trim() && (z.current = setTimeout(() => {
|
|
88
88
|
W({ variables: {
|
|
89
89
|
query: e,
|
|
@@ -115,7 +115,7 @@ function C({ entityType: g, entityId: C, workspaceId: w, tags: T, onAdd: E, onRe
|
|
|
115
115
|
E,
|
|
116
116
|
U,
|
|
117
117
|
F
|
|
118
|
-
]),
|
|
118
|
+
]), fe = e(async () => {
|
|
119
119
|
if (!(!O || !j.trim() || J)) {
|
|
120
120
|
P(null);
|
|
121
121
|
try {
|
|
@@ -129,7 +129,7 @@ function C({ entityType: g, entityId: C, workspaceId: w, tags: T, onAdd: E, onRe
|
|
|
129
129
|
label: e,
|
|
130
130
|
vocabularyId: await Z()
|
|
131
131
|
} } })).data?.createTag;
|
|
132
|
-
if (!
|
|
132
|
+
if (!C(n)) throw Error("No data returned");
|
|
133
133
|
await Q(n);
|
|
134
134
|
} catch (e) {
|
|
135
135
|
P(e instanceof Error ? e.message : "Failed to create tag");
|
|
@@ -142,100 +142,96 @@ function C({ entityType: g, entityId: C, workspaceId: w, tags: T, onAdd: E, onRe
|
|
|
142
142
|
Z,
|
|
143
143
|
q,
|
|
144
144
|
Q
|
|
145
|
-
]),
|
|
145
|
+
]), pe = e(async (e) => {
|
|
146
146
|
await D(e);
|
|
147
147
|
}, [D]);
|
|
148
148
|
if (V) return null;
|
|
149
149
|
let $ = !!(O && j.trim() && !G && K.length === 0);
|
|
150
|
-
return /* @__PURE__ */
|
|
151
|
-
className: h("flex flex-wrap items-center gap-1.5",
|
|
152
|
-
children: [T.map((e) => /* @__PURE__ */
|
|
150
|
+
return /* @__PURE__ */ s("div", {
|
|
151
|
+
className: h("flex flex-wrap items-center gap-1.5", ce),
|
|
152
|
+
children: [T.map((e) => /* @__PURE__ */ o(y, {
|
|
153
153
|
id: e.id,
|
|
154
154
|
label: e.label,
|
|
155
155
|
color: e.color ?? void 0,
|
|
156
156
|
icon: e.icon ?? void 0,
|
|
157
157
|
description: e.description ?? void 0,
|
|
158
158
|
removable: !0,
|
|
159
|
-
onRemove: () => void
|
|
159
|
+
onRemove: () => void pe(e.id),
|
|
160
160
|
size: "sm"
|
|
161
|
-
}, e.id)), U && /* @__PURE__ */
|
|
162
|
-
open:
|
|
161
|
+
}, e.id)), U && /* @__PURE__ */ s(p, {
|
|
162
|
+
open: le,
|
|
163
163
|
onOpenChange: A,
|
|
164
|
-
children: [/* @__PURE__ */
|
|
164
|
+
children: [/* @__PURE__ */ s(re, {
|
|
165
165
|
type: "button",
|
|
166
166
|
"aria-label": "Add tag",
|
|
167
167
|
className: h("inline-flex items-center gap-1 px-2 py-0.5 rounded-full border border-dashed", "border-border text-muted-foreground text-xs", "hover:border-foreground hover:text-foreground transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"),
|
|
168
|
-
children: [/* @__PURE__ */
|
|
169
|
-
}), /* @__PURE__ */
|
|
170
|
-
className: "w-
|
|
168
|
+
children: [/* @__PURE__ */ o(a, { className: "size-3" }), /* @__PURE__ */ o("span", { children: T.length > 0 ? "Add" : "Add tags" })]
|
|
169
|
+
}), /* @__PURE__ */ o(m, {
|
|
170
|
+
className: "z-50 w-72 max-w-[calc(100vw-2rem)] border-border/80 bg-popover p-0 shadow-xl",
|
|
171
171
|
align: "start",
|
|
172
172
|
side: "bottom",
|
|
173
|
-
sideOffset:
|
|
174
|
-
children: /* @__PURE__ */
|
|
173
|
+
sideOffset: 8,
|
|
174
|
+
children: /* @__PURE__ */ s(ne, {
|
|
175
175
|
shouldFilter: !1,
|
|
176
176
|
children: [
|
|
177
|
-
/* @__PURE__ */
|
|
178
|
-
className: "
|
|
179
|
-
children: [
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
className: "h-9 flex-1 border-0 bg-transparent px-2 text-sm outline-none placeholder:text-muted-foreground"
|
|
186
|
-
}),
|
|
187
|
-
G && /* @__PURE__ */ s(i, { className: "size-3.5 shrink-0 animate-spin text-muted-foreground" })
|
|
188
|
-
]
|
|
177
|
+
/* @__PURE__ */ s("div", {
|
|
178
|
+
className: "relative",
|
|
179
|
+
children: [/* @__PURE__ */ o(u, {
|
|
180
|
+
placeholder: "Search or create a tag...",
|
|
181
|
+
value: j,
|
|
182
|
+
onValueChange: de,
|
|
183
|
+
className: "h-10 border-0 bg-transparent pr-9 text-sm outline-none placeholder:text-muted-foreground"
|
|
184
|
+
}), G && /* @__PURE__ */ o(i, { className: "absolute right-3 top-1/2 size-3.5 -translate-y-1/2 animate-spin text-muted-foreground" })]
|
|
189
185
|
}),
|
|
190
|
-
/* @__PURE__ */
|
|
186
|
+
/* @__PURE__ */ s(f, {
|
|
191
187
|
className: "max-h-48 overflow-y-auto",
|
|
192
188
|
children: [
|
|
193
|
-
j.trim() === "" && /* @__PURE__ */
|
|
189
|
+
j.trim() === "" && /* @__PURE__ */ o(c, {
|
|
194
190
|
className: "py-4 text-center text-xs text-muted-foreground",
|
|
195
191
|
children: "Type to search or create a tag"
|
|
196
192
|
}),
|
|
197
|
-
K.length > 0 && /* @__PURE__ */
|
|
193
|
+
K.length > 0 && /* @__PURE__ */ o(l, { children: K.map((e) => /* @__PURE__ */ s(d, {
|
|
198
194
|
value: e.id,
|
|
199
195
|
onSelect: () => void Q(e),
|
|
200
196
|
className: "flex items-center gap-2 px-3 py-2 cursor-pointer",
|
|
201
197
|
disabled: F,
|
|
202
198
|
children: [
|
|
203
|
-
e.icon && /* @__PURE__ */
|
|
199
|
+
e.icon && /* @__PURE__ */ o("span", {
|
|
204
200
|
className: "text-sm",
|
|
205
201
|
children: e.icon
|
|
206
202
|
}),
|
|
207
|
-
/* @__PURE__ */
|
|
203
|
+
/* @__PURE__ */ o("span", {
|
|
208
204
|
className: "truncate text-sm",
|
|
209
205
|
style: e.color ? { color: e.color } : void 0,
|
|
210
206
|
children: e.label
|
|
211
207
|
}),
|
|
212
|
-
F && /* @__PURE__ */
|
|
208
|
+
F && /* @__PURE__ */ o(i, { className: "ml-auto size-3 animate-spin" })
|
|
213
209
|
]
|
|
214
210
|
}, e.id)) }),
|
|
215
|
-
$ && /* @__PURE__ */
|
|
211
|
+
$ && /* @__PURE__ */ o(l, { children: /* @__PURE__ */ s(d, {
|
|
216
212
|
value: `__create__${j}`,
|
|
217
|
-
onSelect: () => void
|
|
213
|
+
onSelect: () => void fe(),
|
|
218
214
|
className: "flex items-center gap-2 px-3 py-2 cursor-pointer text-primary",
|
|
219
215
|
disabled: J,
|
|
220
|
-
children: [J ? /* @__PURE__ */
|
|
216
|
+
children: [J ? /* @__PURE__ */ o(i, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ o(ee, { className: "size-3.5" }), /* @__PURE__ */ o("span", {
|
|
221
217
|
className: "text-sm truncate",
|
|
222
218
|
children: J ? "Creating..." : `Create "${j}"`
|
|
223
219
|
})]
|
|
224
220
|
}) }),
|
|
225
|
-
j.trim() !== "" && !G && K.length === 0 && !$ && /* @__PURE__ */
|
|
221
|
+
j.trim() !== "" && !G && K.length === 0 && !$ && /* @__PURE__ */ o(c, {
|
|
226
222
|
className: "py-4 text-center text-xs text-muted-foreground",
|
|
227
223
|
children: "No tags found"
|
|
228
224
|
}),
|
|
229
|
-
N && /* @__PURE__ */
|
|
225
|
+
N && /* @__PURE__ */ o("div", {
|
|
230
226
|
className: "px-3 py-2 text-xs text-destructive border-t border-border",
|
|
231
227
|
children: N
|
|
232
228
|
})
|
|
233
229
|
]
|
|
234
230
|
}),
|
|
235
|
-
!U && /* @__PURE__ */
|
|
231
|
+
!U && /* @__PURE__ */ s("div", {
|
|
236
232
|
className: "border-t border-border px-3 py-2 text-xs text-muted-foreground flex items-center gap-1",
|
|
237
233
|
children: [
|
|
238
|
-
/* @__PURE__ */
|
|
234
|
+
/* @__PURE__ */ o(te, { className: "size-3" }),
|
|
239
235
|
"Maximum ",
|
|
240
236
|
k,
|
|
241
237
|
" tags reached"
|
|
@@ -248,6 +244,6 @@ function C({ entityType: g, entityId: C, workspaceId: w, tags: T, onAdd: E, onRe
|
|
|
248
244
|
});
|
|
249
245
|
}
|
|
250
246
|
//#endregion
|
|
251
|
-
export {
|
|
247
|
+
export { w as ProjectTagsWidget };
|
|
252
248
|
|
|
253
249
|
//# sourceMappingURL=ProjectTagsWidget.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectTagsWidget.js","names":[],"sources":["../../../src/components/tags/ProjectTagsWidget.tsx"],"sourcesContent":["/**\n * ProjectTagsWidget\n *\n * Local replacement for `@burdenoff/fe-libs/tag` `TagsWidget`, used until\n * fe-libs ships a fix for the non-clickable `AddTagTrigger` (the published\n * trigger swallows the props Radix Slot tries to forward via\n * `<PopoverTrigger asChild>`, so the popover never opens).\n *\n * This component bypasses the broken trigger by rendering a regular\n * `<PopoverTrigger>` (no `asChild`) so Radix mounts the click-handling\n * button itself. Internals (search, create, assign, remove) mirror the\n * fe-libs `TagsWidget` so the data path through wspace-tags-svc is\n * identical.\n *\n * TODO(fe-libs): once `@burdenoff/fe-libs/tag` `AddTagTrigger` forwards\n * props/ref to the underlying button, delete this file and switch back\n * to `<TagsWidget>` from `@burdenoff/fe-libs/tag`.\n */\n\nimport { gql, type ApolloClient } from '@apollo/client';\nimport { useLazyQuery, useMutation } from '@apollo/client/react';\nimport { Loader2, Plus, Search, Tag as TagIcon, X } from 'lucide-react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { type TagItem } from '@burdenoff/fe-libs/tag';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { useGatewayClient } from '@burdenoff/fe-libs/shared/graphql';\nimport { cn } from '@burdenoff/fe-libs/shared/utils';\n\n// --- GraphQL ----------------------------------------------------------------\n\ninterface VocabResult {\n id: string;\n key: string;\n name: string;\n}\n\nconst SEARCH_TAGS = gql`\n query ProjectTagsWidget_SearchTags($query: String!, $vocabularyId: ID, $limit: Int) {\n searchTags(query: $query, vocabularyId: $vocabularyId, limit: $limit) {\n id\n key\n label\n value\n vocabularyId\n parentId\n hierarchyPath\n color\n icon\n description\n vocabulary {\n key\n }\n }\n }\n`;\n\nconst GET_VOCABULARY_BY_KEY = gql`\n query ProjectTagsWidget_GetVocabularyByKey($key: String!) {\n getVocabularyByKey(key: $key) {\n id\n key\n name\n }\n }\n`;\n\nconst CREATE_VOCABULARY = gql`\n mutation ProjectTagsWidget_CreateVocabulary($input: CreateVocabularyInput!) {\n createVocabulary(input: $input) {\n id\n key\n name\n }\n }\n`;\n\nconst CREATE_TAG = gql`\n mutation ProjectTagsWidget_CreateTag($input: CreateTagInput!) {\n createTag(input: $input) {\n id\n key\n label\n value\n vocabularyId\n parentId\n hierarchyPath\n color\n icon\n vocabulary {\n key\n }\n }\n }\n`;\n\nconst DEFAULT_VOCAB_KEY = 'general';\n\nfunction isTagItem(value: unknown): value is TagItem {\n if (!value || typeof value !== 'object') return false;\n const tag = value as Partial<Record<keyof TagItem, unknown>>;\n return typeof tag.id === 'string' && typeof tag.key === 'string' && typeof tag.label === 'string';\n}\n\n// --- Props ------------------------------------------------------------------\n\nexport interface ProjectTagsWidgetProps {\n /** Entity type recorded on the assigned-tag projection (e.g. \"Project\"). */\n entityType: string;\n /** Entity id (project id). */\n entityId: string;\n /** Workspace scope — widget renders nothing when empty. */\n workspaceId?: string;\n /** Controlled tag list owned by the parent. */\n tags: TagItem[];\n /** Persist a newly assigned tag on the entity. */\n onAdd: (tag: TagItem) => void | Promise<void>;\n /** Persist a tag removal on the entity. */\n onRemove: (tagId: string) => void | Promise<void>;\n /** Allow creating new tags on the fly. Default: true. */\n allowCreate?: boolean;\n /** Cap on assigned tags. Default 20. */\n maxTags?: number;\n className?: string;\n}\n\n// --- Component --------------------------------------------------------------\n\nexport function ProjectTagsWidget({\n entityType: _entityType,\n entityId,\n workspaceId,\n tags,\n onAdd,\n onRemove,\n allowCreate = true,\n maxTags = 20,\n className,\n}: ProjectTagsWidgetProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const [createError, setCreateError] = useState<string | null>(null);\n const [adding, setAdding] = useState(false);\n const [resolvedVocabId, setResolvedVocabId] = useState<string | undefined>();\n const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Bun may install Apollo twice when fe-libs is consumed as a package; the runtime client is compatible.\n const wsClient = useGatewayClient('workspace') as unknown as ApolloClient;\n\n const skip = !workspaceId || !entityId;\n\n const assignedTagIds = new Set(tags.map((t) => t.id));\n const canAddMore = tags.length < maxTags;\n\n const [runSearch, { data: searchData, loading: searching }] = useLazyQuery<{\n searchTags: TagItem[];\n }>(SEARCH_TAGS, { client: wsClient });\n\n const searchResults = (searchData?.searchTags ?? []).filter(\n (tag): tag is TagItem => isTagItem(tag) && !assignedTagIds.has(tag.id)\n );\n\n const [createTag, { loading: creating }] = useMutation<{\n createTag: TagItem;\n }>(CREATE_TAG, { client: wsClient });\n\n const [fetchVocabByKey] = useLazyQuery<{\n getVocabularyByKey: VocabResult | null;\n }>(GET_VOCABULARY_BY_KEY, { client: wsClient });\n\n const [createVocabulary] = useMutation<{\n createVocabulary: VocabResult;\n }>(CREATE_VOCABULARY, { client: wsClient });\n\n const resolveVocabId = useCallback(async (): Promise<string> => {\n if (resolvedVocabId) return resolvedVocabId;\n\n const res = await fetchVocabByKey({ variables: { key: DEFAULT_VOCAB_KEY } });\n const existing = res.data?.getVocabularyByKey;\n if (existing) {\n setResolvedVocabId(existing.id);\n return existing.id;\n }\n\n const created = await createVocabulary({\n variables: {\n input: {\n key: DEFAULT_VOCAB_KEY,\n name: 'General',\n description: 'Default vocabulary for general-purpose tags',\n scope: 'WORKSPACE',\n allowCustomTags: true,\n },\n },\n });\n const newVocab = created.data?.createVocabulary;\n if (!newVocab) throw new Error('Failed to create default vocabulary');\n setResolvedVocabId(newVocab.id);\n return newVocab.id;\n }, [resolvedVocabId, fetchVocabByKey, createVocabulary]);\n\n const handleSearchChange = useCallback(\n (value: string) => {\n setSearch(value);\n if (createError) setCreateError(null);\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (!value.trim()) return;\n debounceRef.current = setTimeout(() => {\n void runSearch({\n variables: { query: value, vocabularyId: resolvedVocabId, limit: 10 },\n });\n }, 250);\n },\n [runSearch, resolvedVocabId, createError]\n );\n\n useEffect(() => {\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n };\n }, []);\n\n const handleAssign = useCallback(\n async (tag: TagItem) => {\n if (!canAddMore || adding) return;\n setAdding(true);\n try {\n const normalizedTag: TagItem = {\n ...tag,\n vocabularyKey: tag.vocabularyKey ?? tag.vocabulary?.key ?? null,\n };\n await onAdd(normalizedTag);\n setSearch('');\n setOpen(false);\n } finally {\n setAdding(false);\n }\n },\n [onAdd, canAddMore, adding]\n );\n\n const handleCreate = useCallback(async () => {\n if (!allowCreate || !search.trim() || creating) return;\n setCreateError(null);\n try {\n const label = search.trim();\n const key = label\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n if (!key) {\n setCreateError('Must contain at least one letter or number');\n return;\n }\n const vocabId = await resolveVocabId();\n const res = await createTag({\n variables: { input: { key, label, vocabularyId: vocabId } },\n });\n const created = res.data?.createTag;\n if (!isTagItem(created)) throw new Error('No data returned');\n await handleAssign(created);\n } catch (err) {\n setCreateError(err instanceof Error ? err.message : 'Failed to create tag');\n }\n }, [allowCreate, search, creating, resolveVocabId, createTag, handleAssign]);\n\n const handleRemove = useCallback(\n async (tagId: string) => {\n await onRemove(tagId);\n },\n [onRemove]\n );\n\n if (skip) return null;\n\n const showCreateOption = Boolean(\n allowCreate && search.trim() && !searching && searchResults.length === 0\n );\n\n return (\n <div className={cn('flex flex-wrap items-center gap-1.5', className)}>\n {tags.map((tag) => (\n <TagBadge\n key={tag.id}\n id={tag.id}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n removable\n onRemove={() => void handleRemove(tag.id)}\n size=\"sm\"\n />\n ))}\n\n {canAddMore && (\n <Popover open={open} onOpenChange={setOpen}>\n {/*\n Intentionally do NOT use `asChild` here — the fe-libs\n `AddTagTrigger` swallows the props Radix forwards, leaving the\n trigger non-clickable. Letting Radix render its own button\n restores the click handler.\n */}\n <PopoverTrigger\n type=\"button\"\n aria-label=\"Add tag\"\n className={cn(\n 'inline-flex items-center gap-1 px-2 py-0.5 rounded-full border border-dashed',\n 'border-border text-muted-foreground text-xs',\n 'hover:border-foreground hover:text-foreground transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n )}\n >\n <TagIcon className=\"size-3\" />\n <span>{tags.length > 0 ? 'Add' : 'Add tags'}</span>\n </PopoverTrigger>\n\n <PopoverContent className=\"w-64 p-0\" align=\"start\" side=\"bottom\" sideOffset={4}>\n <Command shouldFilter={false}>\n <div className=\"flex items-center border-b border-border px-3\">\n <Search className=\"size-4 shrink-0 text-muted-foreground\" />\n <CommandInput\n placeholder=\"Search or create a tag...\"\n value={search}\n onValueChange={handleSearchChange}\n className=\"h-9 flex-1 border-0 bg-transparent px-2 text-sm outline-none placeholder:text-muted-foreground\"\n />\n {searching && (\n <Loader2 className=\"size-3.5 shrink-0 animate-spin text-muted-foreground\" />\n )}\n </div>\n\n <CommandList className=\"max-h-48 overflow-y-auto\">\n {search.trim() === '' && (\n <CommandEmpty className=\"py-4 text-center text-xs text-muted-foreground\">\n Type to search or create a tag\n </CommandEmpty>\n )}\n\n {searchResults.length > 0 && (\n <CommandGroup>\n {searchResults.map((tag) => (\n <CommandItem\n key={tag.id}\n value={tag.id}\n onSelect={() => void handleAssign(tag)}\n className=\"flex items-center gap-2 px-3 py-2 cursor-pointer\"\n disabled={adding}\n >\n {tag.icon && <span className=\"text-sm\">{tag.icon}</span>}\n <span\n className=\"truncate text-sm\"\n style={tag.color ? { color: tag.color } : undefined}\n >\n {tag.label}\n </span>\n {adding && <Loader2 className=\"ml-auto size-3 animate-spin\" />}\n </CommandItem>\n ))}\n </CommandGroup>\n )}\n\n {showCreateOption && (\n <CommandGroup>\n <CommandItem\n value={`__create__${search}`}\n onSelect={() => void handleCreate()}\n className=\"flex items-center gap-2 px-3 py-2 cursor-pointer text-primary\"\n disabled={creating}\n >\n {creating ? (\n <Loader2 className=\"size-3.5 animate-spin\" />\n ) : (\n <Plus className=\"size-3.5\" />\n )}\n <span className=\"text-sm truncate\">\n {creating ? 'Creating...' : `Create \"${search}\"`}\n </span>\n </CommandItem>\n </CommandGroup>\n )}\n\n {search.trim() !== '' &&\n !searching &&\n searchResults.length === 0 &&\n !showCreateOption && (\n <CommandEmpty className=\"py-4 text-center text-xs text-muted-foreground\">\n No tags found\n </CommandEmpty>\n )}\n\n {createError && (\n <div className=\"px-3 py-2 text-xs text-destructive border-t border-border\">\n {createError}\n </div>\n )}\n </CommandList>\n\n {!canAddMore && (\n <div className=\"border-t border-border px-3 py-2 text-xs text-muted-foreground flex items-center gap-1\">\n <X className=\"size-3\" />\n Maximum {maxTags} tags reached\n </div>\n )}\n </Command>\n </PopoverContent>\n </Popover>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProjectTagsWidget.js","names":[],"sources":["../../../src/components/tags/ProjectTagsWidget.tsx"],"sourcesContent":["/**\n * ProjectTagsWidget\n *\n * Local replacement for `@burdenoff/fe-libs/tag` `TagsWidget`, used until\n * fe-libs ships a fix for the non-clickable `AddTagTrigger` (the published\n * trigger swallows the props Radix Slot tries to forward via\n * `<PopoverTrigger asChild>`, so the popover never opens).\n *\n * This component bypasses the broken trigger by rendering a regular\n * `<PopoverTrigger>` (no `asChild`) so Radix mounts the click-handling\n * button itself. Internals (search, create, assign, remove) mirror the\n * fe-libs `TagsWidget` so the data path through wspace-tags-svc is\n * identical.\n *\n * TODO(fe-libs): once `@burdenoff/fe-libs/tag` `AddTagTrigger` forwards\n * props/ref to the underlying button, delete this file and switch back\n * to `<TagsWidget>` from `@burdenoff/fe-libs/tag`.\n */\n\nimport { gql, type ApolloClient } from '@apollo/client';\nimport { useLazyQuery, useMutation } from '@apollo/client/react';\nimport { Loader2, Plus, Tag as TagIcon, X } from 'lucide-react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { type TagItem } from '@burdenoff/fe-libs/tag';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { useGatewayClient } from '@burdenoff/fe-libs/shared/graphql';\nimport { cn } from '@burdenoff/fe-libs/shared/utils';\n\n// --- GraphQL ----------------------------------------------------------------\n\ninterface VocabResult {\n id: string;\n key: string;\n name: string;\n}\n\nconst SEARCH_TAGS = gql`\n query ProjectTagsWidget_SearchTags($query: String!, $vocabularyId: ID, $limit: Int) {\n searchTags(query: $query, vocabularyId: $vocabularyId, limit: $limit) {\n id\n key\n label\n value\n vocabularyId\n parentId\n hierarchyPath\n color\n icon\n description\n vocabulary {\n key\n }\n }\n }\n`;\n\nconst GET_VOCABULARY_BY_KEY = gql`\n query ProjectTagsWidget_GetVocabularyByKey($key: String!) {\n getVocabularyByKey(key: $key) {\n id\n key\n name\n }\n }\n`;\n\nconst CREATE_VOCABULARY = gql`\n mutation ProjectTagsWidget_CreateVocabulary($input: CreateVocabularyInput!) {\n createVocabulary(input: $input) {\n id\n key\n name\n }\n }\n`;\n\nconst CREATE_TAG = gql`\n mutation ProjectTagsWidget_CreateTag($input: CreateTagInput!) {\n createTag(input: $input) {\n id\n key\n label\n value\n vocabularyId\n parentId\n hierarchyPath\n color\n icon\n vocabulary {\n key\n }\n }\n }\n`;\n\nconst DEFAULT_VOCAB_KEY = 'general';\n\nfunction isTagItem(value: unknown): value is TagItem {\n if (!value || typeof value !== 'object') return false;\n const tag = value as Partial<Record<keyof TagItem, unknown>>;\n return typeof tag.id === 'string' && typeof tag.key === 'string' && typeof tag.label === 'string';\n}\n\n// --- Props ------------------------------------------------------------------\n\nexport interface ProjectTagsWidgetProps {\n /** Entity type recorded on the assigned-tag projection (e.g. \"Project\"). */\n entityType: string;\n /** Entity id (project id). */\n entityId: string;\n /** Workspace scope — widget renders nothing when empty. */\n workspaceId?: string;\n /** Controlled tag list owned by the parent. */\n tags: TagItem[];\n /** Persist a newly assigned tag on the entity. */\n onAdd: (tag: TagItem) => void | Promise<void>;\n /** Persist a tag removal on the entity. */\n onRemove: (tagId: string) => void | Promise<void>;\n /** Allow creating new tags on the fly. Default: true. */\n allowCreate?: boolean;\n /** Cap on assigned tags. Default 20. */\n maxTags?: number;\n className?: string;\n}\n\n// --- Component --------------------------------------------------------------\n\nexport function ProjectTagsWidget({\n entityType: _entityType,\n entityId,\n workspaceId,\n tags,\n onAdd,\n onRemove,\n allowCreate = true,\n maxTags = 20,\n className,\n}: ProjectTagsWidgetProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const [createError, setCreateError] = useState<string | null>(null);\n const [adding, setAdding] = useState(false);\n const [resolvedVocabId, setResolvedVocabId] = useState<string | undefined>();\n const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Bun may install Apollo twice when fe-libs is consumed as a package; the runtime client is compatible.\n const wsClient = useGatewayClient('workspace') as unknown as ApolloClient;\n\n const skip = !workspaceId || !entityId;\n\n const assignedTagIds = new Set(tags.map((t) => t.id));\n const canAddMore = tags.length < maxTags;\n\n const [runSearch, { data: searchData, loading: searching }] = useLazyQuery<{\n searchTags: TagItem[];\n }>(SEARCH_TAGS, { client: wsClient });\n\n const searchResults = (searchData?.searchTags ?? []).filter(\n (tag): tag is TagItem => isTagItem(tag) && !assignedTagIds.has(tag.id)\n );\n\n const [createTag, { loading: creating }] = useMutation<{\n createTag: TagItem;\n }>(CREATE_TAG, { client: wsClient });\n\n const [fetchVocabByKey] = useLazyQuery<{\n getVocabularyByKey: VocabResult | null;\n }>(GET_VOCABULARY_BY_KEY, { client: wsClient });\n\n const [createVocabulary] = useMutation<{\n createVocabulary: VocabResult;\n }>(CREATE_VOCABULARY, { client: wsClient });\n\n const resolveVocabId = useCallback(async (): Promise<string> => {\n if (resolvedVocabId) return resolvedVocabId;\n\n const res = await fetchVocabByKey({ variables: { key: DEFAULT_VOCAB_KEY } });\n const existing = res.data?.getVocabularyByKey;\n if (existing) {\n setResolvedVocabId(existing.id);\n return existing.id;\n }\n\n const created = await createVocabulary({\n variables: {\n input: {\n key: DEFAULT_VOCAB_KEY,\n name: 'General',\n description: 'Default vocabulary for general-purpose tags',\n scope: 'WORKSPACE',\n allowCustomTags: true,\n },\n },\n });\n const newVocab = created.data?.createVocabulary;\n if (!newVocab) throw new Error('Failed to create default vocabulary');\n setResolvedVocabId(newVocab.id);\n return newVocab.id;\n }, [resolvedVocabId, fetchVocabByKey, createVocabulary]);\n\n const handleSearchChange = useCallback(\n (value: string) => {\n setSearch(value);\n if (createError) setCreateError(null);\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (!value.trim()) return;\n debounceRef.current = setTimeout(() => {\n void runSearch({\n variables: { query: value, vocabularyId: resolvedVocabId, limit: 10 },\n });\n }, 250);\n },\n [runSearch, resolvedVocabId, createError]\n );\n\n useEffect(() => {\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n };\n }, []);\n\n const handleAssign = useCallback(\n async (tag: TagItem) => {\n if (!canAddMore || adding) return;\n setAdding(true);\n try {\n const normalizedTag: TagItem = {\n ...tag,\n vocabularyKey: tag.vocabularyKey ?? tag.vocabulary?.key ?? null,\n };\n await onAdd(normalizedTag);\n setSearch('');\n setOpen(false);\n } finally {\n setAdding(false);\n }\n },\n [onAdd, canAddMore, adding]\n );\n\n const handleCreate = useCallback(async () => {\n if (!allowCreate || !search.trim() || creating) return;\n setCreateError(null);\n try {\n const label = search.trim();\n const key = label\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n if (!key) {\n setCreateError('Must contain at least one letter or number');\n return;\n }\n const vocabId = await resolveVocabId();\n const res = await createTag({\n variables: { input: { key, label, vocabularyId: vocabId } },\n });\n const created = res.data?.createTag;\n if (!isTagItem(created)) throw new Error('No data returned');\n await handleAssign(created);\n } catch (err) {\n setCreateError(err instanceof Error ? err.message : 'Failed to create tag');\n }\n }, [allowCreate, search, creating, resolveVocabId, createTag, handleAssign]);\n\n const handleRemove = useCallback(\n async (tagId: string) => {\n await onRemove(tagId);\n },\n [onRemove]\n );\n\n if (skip) return null;\n\n const showCreateOption = Boolean(\n allowCreate && search.trim() && !searching && searchResults.length === 0\n );\n\n return (\n <div className={cn('flex flex-wrap items-center gap-1.5', className)}>\n {tags.map((tag) => (\n <TagBadge\n key={tag.id}\n id={tag.id}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n removable\n onRemove={() => void handleRemove(tag.id)}\n size=\"sm\"\n />\n ))}\n\n {canAddMore && (\n <Popover open={open} onOpenChange={setOpen}>\n {/*\n Intentionally do NOT use `asChild` here — the fe-libs\n `AddTagTrigger` swallows the props Radix forwards, leaving the\n trigger non-clickable. Letting Radix render its own button\n restores the click handler.\n */}\n <PopoverTrigger\n type=\"button\"\n aria-label=\"Add tag\"\n className={cn(\n 'inline-flex items-center gap-1 px-2 py-0.5 rounded-full border border-dashed',\n 'border-border text-muted-foreground text-xs',\n 'hover:border-foreground hover:text-foreground transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n )}\n >\n <TagIcon className=\"size-3\" />\n <span>{tags.length > 0 ? 'Add' : 'Add tags'}</span>\n </PopoverTrigger>\n\n <PopoverContent\n className=\"z-50 w-72 max-w-[calc(100vw-2rem)] border-border/80 bg-popover p-0 shadow-xl\"\n align=\"start\"\n side=\"bottom\"\n sideOffset={8}\n >\n <Command shouldFilter={false}>\n <div className=\"relative\">\n <CommandInput\n placeholder=\"Search or create a tag...\"\n value={search}\n onValueChange={handleSearchChange}\n className=\"h-10 border-0 bg-transparent pr-9 text-sm outline-none placeholder:text-muted-foreground\"\n />\n {searching && (\n <Loader2 className=\"absolute right-3 top-1/2 size-3.5 -translate-y-1/2 animate-spin text-muted-foreground\" />\n )}\n </div>\n\n <CommandList className=\"max-h-48 overflow-y-auto\">\n {search.trim() === '' && (\n <CommandEmpty className=\"py-4 text-center text-xs text-muted-foreground\">\n Type to search or create a tag\n </CommandEmpty>\n )}\n\n {searchResults.length > 0 && (\n <CommandGroup>\n {searchResults.map((tag) => (\n <CommandItem\n key={tag.id}\n value={tag.id}\n onSelect={() => void handleAssign(tag)}\n className=\"flex items-center gap-2 px-3 py-2 cursor-pointer\"\n disabled={adding}\n >\n {tag.icon && <span className=\"text-sm\">{tag.icon}</span>}\n <span\n className=\"truncate text-sm\"\n style={tag.color ? { color: tag.color } : undefined}\n >\n {tag.label}\n </span>\n {adding && <Loader2 className=\"ml-auto size-3 animate-spin\" />}\n </CommandItem>\n ))}\n </CommandGroup>\n )}\n\n {showCreateOption && (\n <CommandGroup>\n <CommandItem\n value={`__create__${search}`}\n onSelect={() => void handleCreate()}\n className=\"flex items-center gap-2 px-3 py-2 cursor-pointer text-primary\"\n disabled={creating}\n >\n {creating ? (\n <Loader2 className=\"size-3.5 animate-spin\" />\n ) : (\n <Plus className=\"size-3.5\" />\n )}\n <span className=\"text-sm truncate\">\n {creating ? 'Creating...' : `Create \"${search}\"`}\n </span>\n </CommandItem>\n </CommandGroup>\n )}\n\n {search.trim() !== '' &&\n !searching &&\n searchResults.length === 0 &&\n !showCreateOption && (\n <CommandEmpty className=\"py-4 text-center text-xs text-muted-foreground\">\n No tags found\n </CommandEmpty>\n )}\n\n {createError && (\n <div className=\"px-3 py-2 text-xs text-destructive border-t border-border\">\n {createError}\n </div>\n )}\n </CommandList>\n\n {!canAddMore && (\n <div className=\"border-t border-border px-3 py-2 text-xs text-muted-foreground flex items-center gap-1\">\n <X className=\"size-3\" />\n Maximum {maxTags} tags reached\n </div>\n )}\n </Command>\n </PopoverContent>\n </Popover>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|