@burdenoff/microfe-workspaces 2026.513.2 → 2026.518.1
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectsListPage.d.ts","sourceRoot":"","sources":["../../src/pages/ProjectsListPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ProjectsListPage.d.ts","sourceRoot":"","sources":["../../src/pages/ProjectsListPage.tsx"],"names":[],"mappings":"AAkCA,wBAAgB,gBAAgB,4CAymB/B"}
|
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
import { useWorkspacesStore as e } from "../store/workspacesStore.js";
|
|
2
2
|
import { tWithFallback as t } from "../utils/i18n.js";
|
|
3
3
|
import { useWorkspacesPageView as n } from "../hooks/useWorkspacesPageView.js";
|
|
4
|
-
import { nativeConfirm as
|
|
5
|
-
import {
|
|
6
|
-
import { useArchiveProject as
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
4
|
+
import { nativeConfirm as r, nativeImpact as i } from "../utils/nativeBridge.js";
|
|
5
|
+
import { useMyProjects as a } from "../hooks/useProjects.js";
|
|
6
|
+
import { useArchiveProject as o, useDeleteProject as s, useUnarchiveProject as c } from "../hooks/useProjectMutations.js";
|
|
7
|
+
import { normalizeProjectTags as ee } from "../utils/projectTags.js";
|
|
8
|
+
import { ProjectCard as l } from "../components/ProjectCard.js";
|
|
9
|
+
import { CreateProjectDialog as u } from "../components/CreateProjectDialog.js";
|
|
10
|
+
import { useWorkspacePermissions as d } from "../hooks/useWorkspacePermissions.js";
|
|
11
|
+
import { useEffect as f, useMemo as p, useState as m } from "react";
|
|
12
|
+
import { FolderKanban as te, Grid3x3 as ne, List as re, Plus as h, Search as ie } from "lucide-react";
|
|
13
|
+
import { Fragment as ae, jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
14
|
+
import { Button as v, Card as y, CardContent as b, Input as oe, Select as x, SelectContent as S, SelectItem as C, SelectTrigger as w, SelectValue as T, Skeleton as E } from "@burdenoff/fe-libs/ui";
|
|
15
|
+
import { useParams as se } from "react-router-dom";
|
|
16
|
+
import { useDebounce as ce } from "@burdenoff/fe-libs/shared/hooks";
|
|
17
|
+
import { useI18n as le } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
18
|
+
import { TagBadge as ue } from "@burdenoff/fe-libs/tag";
|
|
18
19
|
//#region src/pages/ProjectsListPage.tsx
|
|
19
|
-
function
|
|
20
|
-
let { t:
|
|
20
|
+
function D() {
|
|
21
|
+
let { t: D } = le(), { workspaceId: O } = se();
|
|
21
22
|
n("workspaces.projects", {
|
|
22
|
-
workspaceId:
|
|
23
|
+
workspaceId: O,
|
|
23
24
|
requireParams: !0
|
|
24
25
|
});
|
|
25
|
-
let { viewMode:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}, [
|
|
29
|
-
if (
|
|
30
|
-
|
|
26
|
+
let { viewMode: k, setViewMode: A, projectFilters: j, setProjectFilters: M, clearProjectFilters: de, selectedProjectIds: N, toggleProjectSelection: P, clearProjectSelection: F, openCreateProjectDialog: fe, isCreateProjectDialogOpen: pe } = e(), [I, L] = m(j.search || ""), [R, z] = m([]), [B, V] = m(!0), H = ce(I, 500);
|
|
27
|
+
f(() => {
|
|
28
|
+
M({ search: H });
|
|
29
|
+
}, [H, M]), f(() => {
|
|
30
|
+
if (R.length === 0) {
|
|
31
|
+
M({
|
|
31
32
|
tagIds: void 0,
|
|
32
33
|
tagHierarchyPaths: void 0,
|
|
33
34
|
matchAllTags: void 0,
|
|
@@ -35,193 +36,200 @@ function C() {
|
|
|
35
36
|
});
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
-
tagIds:
|
|
40
|
-
tagHierarchyPaths:
|
|
41
|
-
matchAllTags:
|
|
39
|
+
M({
|
|
40
|
+
tagIds: R.map((e) => e.tagId),
|
|
41
|
+
tagHierarchyPaths: R.map((e) => e.hierarchyPath).filter((e) => typeof e == "string" && e.length > 0),
|
|
42
|
+
matchAllTags: B,
|
|
42
43
|
includeDescendants: !0
|
|
43
44
|
});
|
|
44
45
|
}, [
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
R,
|
|
47
|
+
B,
|
|
48
|
+
M
|
|
49
|
+
]), f(() => () => F(), [F]);
|
|
50
|
+
let { data: U, isLoading: W, error: G } = a({
|
|
51
|
+
limit: 500,
|
|
52
|
+
offset: 0
|
|
53
|
+
}), K = p(() => {
|
|
54
|
+
let e = U?.items ?? [], t = (j.search ?? "").trim().toLowerCase(), n = j.tagIds ?? [], r = j.matchAllTags ?? !0, i = (e) => !O || e.workspaceId === O, a = (e) => !t || e.name.toLowerCase().includes(t) || e.key.toLowerCase().includes(t), o = (e) => !j.status || e.status === j.status, s = (e) => j.archived === void 0 || e.archived === j.archived, c = (e) => {
|
|
55
|
+
if (n.length === 0) return !0;
|
|
56
|
+
let t = new Set(ee(e.tags).map((e) => e.tagId));
|
|
57
|
+
return r ? n.every((e) => t.has(e)) : n.some((e) => t.has(e));
|
|
58
|
+
}, l = e.filter((e) => i(e) && a(e) && o(e) && s(e) && c(e)), u = U?.total ?? l.length, d = U?.hasMore ?? !1;
|
|
59
|
+
return {
|
|
60
|
+
items: l,
|
|
61
|
+
total: O || n.length > 0 ? l.length : u,
|
|
62
|
+
hasMore: d
|
|
63
|
+
};
|
|
64
|
+
}, [
|
|
65
|
+
U?.items,
|
|
66
|
+
U?.total,
|
|
67
|
+
U?.hasMore,
|
|
68
|
+
j,
|
|
47
69
|
O
|
|
48
|
-
]),
|
|
49
|
-
|
|
50
|
-
if (ye) return /* @__PURE__ */ p("div", {
|
|
70
|
+
]), { mutateAsync: me } = o(O), { mutateAsync: he } = c(O), { mutateAsync: ge } = s(O), q = p(() => K?.items.filter((e) => N.has(e.id)) ?? [], [K?.items, N]), J = p(() => q.filter((e) => !e.archived), [q]), Y = p(() => q.filter((e) => e.archived), [q]), X = (K?.items.length ?? 0) > 0 && (K?.items ?? []).every((e) => N.has(e.id)), _e = (K?.items ?? []).some((e) => N.has(e.id)) && !X, { canCreateProject: ve, canDeleteProject: Z, canArchiveProject: Q, isLoading: ye } = d();
|
|
71
|
+
if (ye) return /* @__PURE__ */ _("div", {
|
|
51
72
|
className: "space-y-4 p-6",
|
|
52
|
-
children: [/* @__PURE__ */
|
|
73
|
+
children: [/* @__PURE__ */ g(E, { className: "h-10 w-1/3" }), /* @__PURE__ */ _("div", {
|
|
53
74
|
className: "grid grid-cols-1 md:grid-cols-3 gap-4",
|
|
54
75
|
children: [
|
|
55
|
-
/* @__PURE__ */
|
|
56
|
-
/* @__PURE__ */
|
|
57
|
-
/* @__PURE__ */
|
|
76
|
+
/* @__PURE__ */ g(E, { className: "h-32" }),
|
|
77
|
+
/* @__PURE__ */ g(E, { className: "h-32" }),
|
|
78
|
+
/* @__PURE__ */ g(E, { className: "h-32" })
|
|
58
79
|
]
|
|
59
80
|
})]
|
|
60
81
|
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
children: /* @__PURE__ */ p("div", {
|
|
64
|
-
className: "text-center space-y-2",
|
|
65
|
-
children: [/* @__PURE__ */ f("h2", {
|
|
66
|
-
className: "text-lg font-semibold",
|
|
67
|
-
children: t(C, "projectsList.accessDenied", "Access Denied")
|
|
68
|
-
}), /* @__PURE__ */ f("p", {
|
|
69
|
-
className: "text-sm text-muted-foreground",
|
|
70
|
-
children: t(C, "projectsList.noPermissionToViewProjects", "You don't have permission to view projects.")
|
|
71
|
-
})]
|
|
72
|
-
})
|
|
73
|
-
});
|
|
74
|
-
let Q = () => {
|
|
75
|
-
N({});
|
|
76
|
-
}, be = (e) => {
|
|
77
|
-
if (e === "all") {
|
|
78
|
-
let e = { ...D };
|
|
79
|
-
delete e.status, O(e);
|
|
80
|
-
} else O({ status: e });
|
|
82
|
+
let be = () => {
|
|
83
|
+
fe({});
|
|
81
84
|
}, xe = (e) => {
|
|
82
85
|
if (e === "all") {
|
|
83
|
-
let e = { ...
|
|
84
|
-
delete e.
|
|
85
|
-
} else
|
|
86
|
+
let e = { ...j };
|
|
87
|
+
delete e.status, M(e);
|
|
88
|
+
} else M({ status: e });
|
|
89
|
+
}, Se = (e) => {
|
|
90
|
+
if (e === "all") {
|
|
91
|
+
let e = { ...j };
|
|
92
|
+
delete e.archived, M(e);
|
|
93
|
+
} else M({ archived: e === "true" });
|
|
86
94
|
}, $ = (e) => {
|
|
87
|
-
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
},
|
|
91
|
-
(
|
|
92
|
-
e !==
|
|
95
|
+
z((t) => t.some((t) => t.tagId === e.tagId) ? t.filter((t) => t.tagId !== e.tagId) : [...t, e]);
|
|
96
|
+
}, Ce = () => {
|
|
97
|
+
L(""), z([]), V(!0), de();
|
|
98
|
+
}, we = (e) => {
|
|
99
|
+
(K?.items ?? []).forEach((t) => {
|
|
100
|
+
e !== N.has(t.id) && P(t.id);
|
|
93
101
|
});
|
|
94
102
|
};
|
|
95
|
-
return /* @__PURE__ */
|
|
103
|
+
return /* @__PURE__ */ _("div", {
|
|
96
104
|
className: "space-y-4 sm:space-y-6 p-3 sm:p-6",
|
|
97
105
|
children: [
|
|
98
|
-
/* @__PURE__ */
|
|
106
|
+
/* @__PURE__ */ _("div", {
|
|
99
107
|
className: "flex flex-col sm:flex-row sm:items-center justify-between gap-4",
|
|
100
|
-
children: [/* @__PURE__ */
|
|
108
|
+
children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("h1", {
|
|
101
109
|
className: "text-2xl sm:text-3xl font-bold",
|
|
102
|
-
children: t(
|
|
103
|
-
}), /* @__PURE__ */
|
|
110
|
+
children: t(D, "projectsList.pageTitle", "Projects")
|
|
111
|
+
}), /* @__PURE__ */ g("p", {
|
|
104
112
|
className: "text-muted-foreground",
|
|
105
|
-
children: t(
|
|
106
|
-
})] }), /* @__PURE__ */
|
|
113
|
+
children: t(D, "projectsList.pageSubtitle", "Manage and organize your projects")
|
|
114
|
+
})] }), /* @__PURE__ */ _("div", {
|
|
107
115
|
className: "flex items-center gap-2",
|
|
108
|
-
children: [/* @__PURE__ */
|
|
116
|
+
children: [/* @__PURE__ */ _("div", {
|
|
109
117
|
className: "flex items-center gap-1 border rounded-lg p-1",
|
|
110
|
-
children: [/* @__PURE__ */
|
|
111
|
-
variant:
|
|
118
|
+
children: [/* @__PURE__ */ g(v, {
|
|
119
|
+
variant: k === "grid" ? "secondary" : "ghost",
|
|
112
120
|
size: "sm",
|
|
113
|
-
onClick: () =>
|
|
121
|
+
onClick: () => A("grid"),
|
|
114
122
|
className: "h-8 w-8 p-0",
|
|
115
|
-
"aria-label": t(
|
|
116
|
-
children: /* @__PURE__ */
|
|
117
|
-
}), /* @__PURE__ */
|
|
118
|
-
variant:
|
|
123
|
+
"aria-label": t(D, "projectsList.gridViewAriaLabel", "Show projects in grid view"),
|
|
124
|
+
children: /* @__PURE__ */ g(ne, { size: 16 })
|
|
125
|
+
}), /* @__PURE__ */ g(v, {
|
|
126
|
+
variant: k === "list" ? "secondary" : "ghost",
|
|
119
127
|
size: "sm",
|
|
120
|
-
onClick: () =>
|
|
128
|
+
onClick: () => A("list"),
|
|
121
129
|
className: "h-8 w-8 p-0",
|
|
122
|
-
"aria-label": t(
|
|
123
|
-
children: /* @__PURE__ */
|
|
130
|
+
"aria-label": t(D, "projectsList.listViewAriaLabel", "Show projects in list view"),
|
|
131
|
+
children: /* @__PURE__ */ g(re, { size: 16 })
|
|
124
132
|
})]
|
|
125
|
-
}),
|
|
126
|
-
onClick:
|
|
127
|
-
children: [/* @__PURE__ */
|
|
133
|
+
}), ve && /* @__PURE__ */ _(v, {
|
|
134
|
+
onClick: be,
|
|
135
|
+
children: [/* @__PURE__ */ g(h, { className: "h-4 w-4 sm:mr-2" }), /* @__PURE__ */ g("span", {
|
|
128
136
|
className: "hidden sm:inline",
|
|
129
|
-
children: t(
|
|
137
|
+
children: t(D, "projectsList.createProject", "Create Project")
|
|
130
138
|
})]
|
|
131
139
|
})]
|
|
132
140
|
})]
|
|
133
141
|
}),
|
|
134
|
-
/* @__PURE__ */
|
|
142
|
+
/* @__PURE__ */ _("div", {
|
|
135
143
|
className: "flex flex-col sm:flex-row gap-3",
|
|
136
144
|
children: [
|
|
137
|
-
/* @__PURE__ */
|
|
145
|
+
/* @__PURE__ */ _("div", {
|
|
138
146
|
className: "relative flex-1",
|
|
139
|
-
children: [/* @__PURE__ */
|
|
147
|
+
children: [/* @__PURE__ */ g(ie, {
|
|
140
148
|
className: "absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground",
|
|
141
149
|
size: 18
|
|
142
|
-
}), /* @__PURE__ */
|
|
143
|
-
placeholder: t(
|
|
144
|
-
value:
|
|
145
|
-
onChange: (e) =>
|
|
150
|
+
}), /* @__PURE__ */ g(oe, {
|
|
151
|
+
placeholder: t(D, "projectsList.searchPlaceholder", "Search projects..."),
|
|
152
|
+
value: I,
|
|
153
|
+
onChange: (e) => L(e.target.value),
|
|
146
154
|
className: "pl-[3.25rem]"
|
|
147
155
|
})]
|
|
148
156
|
}),
|
|
149
|
-
/* @__PURE__ */
|
|
150
|
-
value:
|
|
151
|
-
onValueChange:
|
|
152
|
-
children: [/* @__PURE__ */
|
|
157
|
+
/* @__PURE__ */ _(x, {
|
|
158
|
+
value: j.status || "all",
|
|
159
|
+
onValueChange: xe,
|
|
160
|
+
children: [/* @__PURE__ */ g(w, {
|
|
153
161
|
className: "w-full sm:w-[180px]",
|
|
154
|
-
children: /* @__PURE__ */
|
|
155
|
-
}), /* @__PURE__ */
|
|
156
|
-
/* @__PURE__ */
|
|
162
|
+
children: /* @__PURE__ */ g(T, { placeholder: t(D, "projectsList.filterByStatus", "Filter by status") })
|
|
163
|
+
}), /* @__PURE__ */ _(S, { children: [
|
|
164
|
+
/* @__PURE__ */ g(C, {
|
|
157
165
|
value: "all",
|
|
158
|
-
children: t(
|
|
166
|
+
children: t(D, "projectsList.statusAll", "All Statuses")
|
|
159
167
|
}),
|
|
160
|
-
/* @__PURE__ */
|
|
168
|
+
/* @__PURE__ */ g(C, {
|
|
161
169
|
value: "ACTIVE",
|
|
162
|
-
children: t(
|
|
170
|
+
children: t(D, "projectsList.statusActive", "Active")
|
|
163
171
|
}),
|
|
164
|
-
/* @__PURE__ */
|
|
172
|
+
/* @__PURE__ */ g(C, {
|
|
165
173
|
value: "INACTIVE",
|
|
166
|
-
children: t(
|
|
174
|
+
children: t(D, "projectsList.statusInactive", "Inactive")
|
|
167
175
|
})
|
|
168
176
|
] })]
|
|
169
177
|
}),
|
|
170
|
-
/* @__PURE__ */
|
|
171
|
-
value:
|
|
172
|
-
onValueChange:
|
|
173
|
-
children: [/* @__PURE__ */
|
|
178
|
+
/* @__PURE__ */ _(x, {
|
|
179
|
+
value: j.archived === void 0 ? "all" : j.archived ? "true" : "false",
|
|
180
|
+
onValueChange: Se,
|
|
181
|
+
children: [/* @__PURE__ */ g(w, {
|
|
174
182
|
className: "w-full sm:w-[180px]",
|
|
175
|
-
children: /* @__PURE__ */
|
|
176
|
-
}), /* @__PURE__ */
|
|
177
|
-
/* @__PURE__ */
|
|
183
|
+
children: /* @__PURE__ */ g(T, { placeholder: t(D, "projectsList.filterByArchive", "Filter by archive") })
|
|
184
|
+
}), /* @__PURE__ */ _(S, { children: [
|
|
185
|
+
/* @__PURE__ */ g(C, {
|
|
178
186
|
value: "all",
|
|
179
|
-
children: t(
|
|
187
|
+
children: t(D, "projectsList.archiveAll", "All Projects")
|
|
180
188
|
}),
|
|
181
|
-
/* @__PURE__ */
|
|
189
|
+
/* @__PURE__ */ g(C, {
|
|
182
190
|
value: "false",
|
|
183
|
-
children: t(
|
|
191
|
+
children: t(D, "projectsList.archiveActiveProjects", "Active Projects")
|
|
184
192
|
}),
|
|
185
|
-
/* @__PURE__ */
|
|
193
|
+
/* @__PURE__ */ g(C, {
|
|
186
194
|
value: "true",
|
|
187
|
-
children: t(
|
|
195
|
+
children: t(D, "projectsList.archiveArchived", "Archived")
|
|
188
196
|
})
|
|
189
197
|
] })]
|
|
190
198
|
}),
|
|
191
|
-
(
|
|
199
|
+
(j.search || j.status || j.archived !== void 0 || R.length > 0) && /* @__PURE__ */ g(v, {
|
|
192
200
|
variant: "outline",
|
|
193
|
-
onClick:
|
|
194
|
-
children: t(
|
|
201
|
+
onClick: Ce,
|
|
202
|
+
children: t(D, "projectsList.clearFilters", "Clear Filters")
|
|
195
203
|
})
|
|
196
204
|
]
|
|
197
205
|
}),
|
|
198
|
-
|
|
206
|
+
R.length > 0 && /* @__PURE__ */ _("div", {
|
|
199
207
|
className: "flex flex-col gap-3 rounded-lg border border-border bg-card px-4 py-3",
|
|
200
|
-
children: [/* @__PURE__ */
|
|
208
|
+
children: [/* @__PURE__ */ _("div", {
|
|
201
209
|
className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
|
|
202
|
-
children: [/* @__PURE__ */
|
|
210
|
+
children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("p", {
|
|
203
211
|
className: "text-sm font-medium",
|
|
204
|
-
children: t(
|
|
205
|
-
}), /* @__PURE__ */
|
|
212
|
+
children: t(D, "projectsList.activeTagFilters", "Active tag filters")
|
|
213
|
+
}), /* @__PURE__ */ g("p", {
|
|
206
214
|
className: "text-xs text-muted-foreground",
|
|
207
|
-
children: t(
|
|
208
|
-
})] }), /* @__PURE__ */
|
|
209
|
-
value:
|
|
210
|
-
onValueChange: (e) =>
|
|
211
|
-
children: [/* @__PURE__ */
|
|
215
|
+
children: t(D, "projectsList.activeTagFiltersDescription", "Click a project tag to add or remove it from the current filter set.")
|
|
216
|
+
})] }), /* @__PURE__ */ _(x, {
|
|
217
|
+
value: B ? "all" : "any",
|
|
218
|
+
onValueChange: (e) => V(e === "all"),
|
|
219
|
+
children: [/* @__PURE__ */ g(w, {
|
|
212
220
|
className: "w-full sm:w-[180px]",
|
|
213
|
-
children: /* @__PURE__ */
|
|
214
|
-
}), /* @__PURE__ */
|
|
221
|
+
children: /* @__PURE__ */ g(T, { placeholder: t(D, "projectsList.tagMatchMode", "Tag match mode") })
|
|
222
|
+
}), /* @__PURE__ */ _(S, { children: [/* @__PURE__ */ g(C, {
|
|
215
223
|
value: "all",
|
|
216
|
-
children: t(
|
|
217
|
-
}), /* @__PURE__ */
|
|
224
|
+
children: t(D, "projectsList.tagMatchAll", "Match all tags")
|
|
225
|
+
}), /* @__PURE__ */ g(C, {
|
|
218
226
|
value: "any",
|
|
219
|
-
children: t(
|
|
227
|
+
children: t(D, "projectsList.tagMatchAny", "Match any tag")
|
|
220
228
|
})] })]
|
|
221
229
|
})]
|
|
222
|
-
}), /* @__PURE__ */
|
|
230
|
+
}), /* @__PURE__ */ g("div", {
|
|
223
231
|
className: "flex flex-wrap gap-2",
|
|
224
|
-
children:
|
|
232
|
+
children: R.map((e) => /* @__PURE__ */ g(ue, {
|
|
225
233
|
id: e.tagId,
|
|
226
234
|
label: e.label,
|
|
227
235
|
color: e.color ?? void 0,
|
|
@@ -233,170 +241,170 @@ function C() {
|
|
|
233
241
|
}, e.tagId))
|
|
234
242
|
})]
|
|
235
243
|
}),
|
|
236
|
-
|
|
244
|
+
G && /* @__PURE__ */ g(y, {
|
|
237
245
|
className: "border-destructive",
|
|
238
|
-
children: /* @__PURE__ */
|
|
246
|
+
children: /* @__PURE__ */ g(b, {
|
|
239
247
|
className: "pt-6",
|
|
240
|
-
children: /* @__PURE__ */
|
|
248
|
+
children: /* @__PURE__ */ _("div", {
|
|
241
249
|
className: "flex flex-col items-center justify-center py-8 text-center",
|
|
242
|
-
children: [/* @__PURE__ */
|
|
250
|
+
children: [/* @__PURE__ */ g("p", {
|
|
243
251
|
className: "text-destructive font-semibold mb-2",
|
|
244
|
-
children: t(
|
|
245
|
-
}), /* @__PURE__ */
|
|
252
|
+
children: t(D, "projectsList.errorLoading", "Error loading projects")
|
|
253
|
+
}), /* @__PURE__ */ g("p", {
|
|
246
254
|
className: "text-sm text-muted-foreground",
|
|
247
|
-
children:
|
|
255
|
+
children: G instanceof Error ? G.message : t(D, "projectsList.unknownError", "An unknown error occurred")
|
|
248
256
|
})]
|
|
249
257
|
})
|
|
250
258
|
})
|
|
251
259
|
}),
|
|
252
|
-
|
|
260
|
+
W && /* @__PURE__ */ g("div", {
|
|
253
261
|
className: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4",
|
|
254
|
-
children: Array.from({ length: 6 }).map((e, t) => /* @__PURE__ */
|
|
262
|
+
children: Array.from({ length: 6 }).map((e, t) => /* @__PURE__ */ g(y, { children: /* @__PURE__ */ g(b, {
|
|
255
263
|
className: "pt-6",
|
|
256
|
-
children: /* @__PURE__ */
|
|
264
|
+
children: /* @__PURE__ */ _("div", {
|
|
257
265
|
className: "space-y-3",
|
|
258
266
|
children: [
|
|
259
|
-
/* @__PURE__ */
|
|
260
|
-
/* @__PURE__ */
|
|
261
|
-
/* @__PURE__ */
|
|
267
|
+
/* @__PURE__ */ g(E, { className: "h-12 w-12 rounded-lg" }),
|
|
268
|
+
/* @__PURE__ */ g(E, { className: "h-4 w-3/4" }),
|
|
269
|
+
/* @__PURE__ */ g(E, { className: "h-4 w-1/2" })
|
|
262
270
|
]
|
|
263
271
|
})
|
|
264
272
|
}) }, t))
|
|
265
273
|
}),
|
|
266
|
-
!
|
|
274
|
+
!W && !G && K?.items.length === 0 && /* @__PURE__ */ g(y, { children: /* @__PURE__ */ g(b, {
|
|
267
275
|
className: "pt-6",
|
|
268
|
-
children: /* @__PURE__ */
|
|
276
|
+
children: /* @__PURE__ */ _("div", {
|
|
269
277
|
className: "flex flex-col items-center justify-center py-12 text-center",
|
|
270
278
|
children: [
|
|
271
|
-
/* @__PURE__ */
|
|
279
|
+
/* @__PURE__ */ g("div", {
|
|
272
280
|
className: "rounded-full bg-muted p-6 mb-4",
|
|
273
|
-
children: /* @__PURE__ */
|
|
281
|
+
children: /* @__PURE__ */ g(te, {
|
|
274
282
|
size: 48,
|
|
275
283
|
className: "text-muted-foreground"
|
|
276
284
|
})
|
|
277
285
|
}),
|
|
278
|
-
/* @__PURE__ */
|
|
286
|
+
/* @__PURE__ */ g("h3", {
|
|
279
287
|
className: "text-lg font-semibold mb-2",
|
|
280
|
-
children: t(
|
|
288
|
+
children: t(D, "projectsList.noProjectsFound", "No projects found")
|
|
281
289
|
}),
|
|
282
|
-
/* @__PURE__ */
|
|
290
|
+
/* @__PURE__ */ g("p", {
|
|
283
291
|
className: "text-muted-foreground max-w-sm mb-4",
|
|
284
|
-
children:
|
|
292
|
+
children: j.search ? t(D, "projectsList.adjustSearchOrFilters", "Try adjusting your search or filters") : t(D, "projectsList.createFirstProject", "Create your first project to get started organizing your work")
|
|
285
293
|
}),
|
|
286
|
-
!
|
|
287
|
-
onClick:
|
|
288
|
-
children: [/* @__PURE__ */
|
|
294
|
+
!j.search && ve && /* @__PURE__ */ _(v, {
|
|
295
|
+
onClick: be,
|
|
296
|
+
children: [/* @__PURE__ */ g(h, {
|
|
289
297
|
size: 16,
|
|
290
298
|
className: "mr-2"
|
|
291
|
-
}), t(
|
|
299
|
+
}), t(D, "projectsList.createProject", "Create Project")]
|
|
292
300
|
})
|
|
293
301
|
]
|
|
294
302
|
})
|
|
295
303
|
}) }),
|
|
296
|
-
!
|
|
297
|
-
/* @__PURE__ */
|
|
304
|
+
!W && !G && K && K.items.length > 0 && /* @__PURE__ */ _(ae, { children: [
|
|
305
|
+
/* @__PURE__ */ _("div", {
|
|
298
306
|
className: "flex flex-col gap-3 rounded-lg border bg-card p-4 sm:flex-row sm:items-center sm:justify-between",
|
|
299
|
-
children: [/* @__PURE__ */
|
|
307
|
+
children: [/* @__PURE__ */ _("label", {
|
|
300
308
|
className: "flex items-center gap-2 text-sm text-foreground",
|
|
301
|
-
children: [/* @__PURE__ */
|
|
309
|
+
children: [/* @__PURE__ */ g("input", {
|
|
302
310
|
type: "checkbox",
|
|
303
|
-
checked:
|
|
311
|
+
checked: X,
|
|
304
312
|
ref: (e) => {
|
|
305
|
-
e && (e.indeterminate = _e && !
|
|
313
|
+
e && (e.indeterminate = _e && !X);
|
|
306
314
|
},
|
|
307
|
-
onChange: (e) =>
|
|
308
|
-
"aria-label": t(
|
|
315
|
+
onChange: (e) => we(e.target.checked),
|
|
316
|
+
"aria-label": t(D, "projectsList.selectAllAriaLabel", "Select all projects"),
|
|
309
317
|
className: "h-4 w-4 rounded border-border text-primary"
|
|
310
|
-
}), /* @__PURE__ */
|
|
311
|
-
}), /* @__PURE__ */
|
|
318
|
+
}), /* @__PURE__ */ g("span", { children: t(D, "projectsList.selectAllVisible", "Select all visible") })]
|
|
319
|
+
}), /* @__PURE__ */ _("div", {
|
|
312
320
|
className: "flex flex-wrap items-center gap-2",
|
|
313
321
|
children: [
|
|
314
|
-
/* @__PURE__ */
|
|
322
|
+
/* @__PURE__ */ _("span", {
|
|
315
323
|
className: "text-sm text-muted-foreground",
|
|
316
324
|
children: [
|
|
317
|
-
|
|
325
|
+
N.size,
|
|
318
326
|
" ",
|
|
319
|
-
t(
|
|
327
|
+
t(D, "projectsList.selected", "selected")
|
|
320
328
|
]
|
|
321
329
|
}),
|
|
322
|
-
|
|
330
|
+
N.size > 0 && /* @__PURE__ */ g(v, {
|
|
323
331
|
variant: "ghost",
|
|
324
332
|
size: "sm",
|
|
325
|
-
onClick:
|
|
326
|
-
children: t(
|
|
333
|
+
onClick: F,
|
|
334
|
+
children: t(D, "projectsList.clearSelection", "Clear selection")
|
|
327
335
|
}),
|
|
328
|
-
|
|
336
|
+
O && Q && /* @__PURE__ */ _(v, {
|
|
329
337
|
variant: "outline",
|
|
330
338
|
size: "sm",
|
|
331
339
|
onClick: async () => {
|
|
332
|
-
|
|
340
|
+
J.length !== 0 && (await Promise.allSettled(J.map((e) => me(e.id))), F());
|
|
333
341
|
},
|
|
334
|
-
disabled:
|
|
335
|
-
children: [t(
|
|
342
|
+
disabled: J.length === 0,
|
|
343
|
+
children: [t(D, "projectsList.archiveSelected", "Archive Selected"), J.length > 0 ? ` (${J.length})` : ""]
|
|
336
344
|
}),
|
|
337
|
-
|
|
345
|
+
O && Q && /* @__PURE__ */ _(v, {
|
|
338
346
|
variant: "outline",
|
|
339
347
|
size: "sm",
|
|
340
348
|
onClick: async () => {
|
|
341
|
-
|
|
349
|
+
Y.length !== 0 && (await Promise.allSettled(Y.map((e) => he(e.id))), F());
|
|
342
350
|
},
|
|
343
|
-
disabled:
|
|
344
|
-
children: [t(
|
|
351
|
+
disabled: Y.length === 0,
|
|
352
|
+
children: [t(D, "projectsList.unarchiveSelected", "Unarchive Selected"), Y.length > 0 ? ` (${Y.length})` : ""]
|
|
345
353
|
}),
|
|
346
|
-
|
|
354
|
+
O && Z && /* @__PURE__ */ _(v, {
|
|
347
355
|
variant: "outline",
|
|
348
356
|
size: "sm",
|
|
349
357
|
onClick: async () => {
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
let e = t(
|
|
358
|
+
if (q.length === 0) return;
|
|
359
|
+
i("medium");
|
|
360
|
+
let e = t(D, "projectsList.bulkDeleteConfirmPrefix", "Delete ") + q.length + " " + t(D, "projectsList.bulkDeleteConfirmProject", "selected project") + (q.length === 1 ? "" : t(D, "projectsList.bulkDeleteConfirmPluralSuffix", "s")) + t(D, "projectsList.bulkDeleteConfirmSuffix", "? This cannot be undone."), n = await r({
|
|
353
361
|
title: "Delete projects",
|
|
354
362
|
message: e,
|
|
355
363
|
okButtonTitle: "Delete",
|
|
356
364
|
cancelButtonTitle: "Cancel"
|
|
357
365
|
});
|
|
358
|
-
(n === null ? window.confirm(e) : n) && (await Promise.allSettled(
|
|
366
|
+
(n === null ? window.confirm(e) : n) && (await Promise.allSettled(q.map((e) => ge(e.id))), F());
|
|
359
367
|
},
|
|
360
|
-
disabled:
|
|
368
|
+
disabled: q.length === 0,
|
|
361
369
|
className: "text-destructive",
|
|
362
|
-
children: [t(
|
|
370
|
+
children: [t(D, "projectsList.deleteSelected", "Delete Selected"), q.length > 0 ? ` (${q.length})` : ""]
|
|
363
371
|
})
|
|
364
372
|
]
|
|
365
373
|
})]
|
|
366
374
|
}),
|
|
367
|
-
/* @__PURE__ */
|
|
368
|
-
className:
|
|
369
|
-
children:
|
|
375
|
+
/* @__PURE__ */ g("div", {
|
|
376
|
+
className: k === "list" ? "space-y-3" : "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4",
|
|
377
|
+
children: K.items.map((e) => /* @__PURE__ */ g(l, {
|
|
370
378
|
project: e,
|
|
371
|
-
selected:
|
|
372
|
-
canArchive:
|
|
373
|
-
canDelete:
|
|
379
|
+
selected: N.has(e.id),
|
|
380
|
+
canArchive: Q,
|
|
381
|
+
canDelete: Z,
|
|
374
382
|
onTagClick: $,
|
|
375
383
|
onToggleSelect: (t) => {
|
|
376
|
-
t !==
|
|
384
|
+
t !== N.has(e.id) && P(e.id);
|
|
377
385
|
}
|
|
378
386
|
}, e.id))
|
|
379
387
|
}),
|
|
380
|
-
|
|
388
|
+
K.total > K.items.length && /* @__PURE__ */ _("div", {
|
|
381
389
|
className: "text-center text-sm text-muted-foreground",
|
|
382
390
|
children: [
|
|
383
|
-
t(
|
|
391
|
+
t(D, "projectsList.showingPrefix", "Showing"),
|
|
384
392
|
" ",
|
|
385
|
-
|
|
393
|
+
K.items.length,
|
|
386
394
|
" ",
|
|
387
|
-
t(
|
|
395
|
+
t(D, "projectsList.showingOf", "of"),
|
|
388
396
|
" ",
|
|
389
|
-
|
|
397
|
+
K.total,
|
|
390
398
|
" ",
|
|
391
|
-
t(
|
|
399
|
+
t(D, "projectsList.showingProjects", "projects")
|
|
392
400
|
]
|
|
393
401
|
})
|
|
394
402
|
] }),
|
|
395
|
-
|
|
403
|
+
pe && /* @__PURE__ */ g(u, { workspaceId: O })
|
|
396
404
|
]
|
|
397
405
|
});
|
|
398
406
|
}
|
|
399
407
|
//#endregion
|
|
400
|
-
export {
|
|
408
|
+
export { D as ProjectsListPage };
|
|
401
409
|
|
|
402
410
|
//# sourceMappingURL=ProjectsListPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectsListPage.js","names":[],"sources":["../../src/pages/ProjectsListPage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { FolderKanban, Plus, Grid3x3, List, Search } from 'lucide-react';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport {\n Button,\n Input,\n Card,\n CardContent,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { useDebounce } from '@burdenoff/fe-libs/shared/hooks';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useWorkspacesStore } from '../store/workspacesStore';\nimport { nativeConfirm, nativeImpact } from '../utils/nativeBridge';\nimport { useProjects } from '../hooks/useProjects';\nimport {\n useArchiveProject,\n useDeleteProject,\n useUnarchiveProject,\n} from '../hooks/useProjectMutations';\nimport { ProjectCard } from '../components/ProjectCard';\nimport { CreateProjectDialog } from '../components/CreateProjectDialog';\nimport { useWorkspacePermissions } from '../hooks/useWorkspacePermissions';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\nimport { tWithFallback } from '../utils/i18n';\nimport type { ProjectTag, Workspace } from '../types';\n\nexport function ProjectsListPage() {\n const { t } = useI18n();\n\n const { workspaceId } = useParams<{ workspaceId: string }>();\n useWorkspacesPageView('workspaces.projects', { workspaceId, requireParams: true });\n const {\n viewMode,\n setViewMode,\n projectFilters,\n setProjectFilters,\n clearProjectFilters,\n selectedProjectIds,\n toggleProjectSelection,\n clearProjectSelection,\n openCreateProjectDialog,\n isCreateProjectDialogOpen,\n } = useWorkspacesStore();\n\n const [searchValue, setSearchValue] = useState(projectFilters.search || '');\n const [activeTagFilters, setActiveTagFilters] = useState<ProjectTag[]>([]);\n const [matchAllTagFilters, setMatchAllTagFilters] = useState(true);\n const debouncedSearch = useDebounce(searchValue, 500);\n\n // Update filters when debounced search changes\n useEffect(() => {\n setProjectFilters({ search: debouncedSearch });\n }, [debouncedSearch, setProjectFilters]);\n\n useEffect(() => {\n if (activeTagFilters.length === 0) {\n setProjectFilters({\n tagIds: undefined,\n tagHierarchyPaths: undefined,\n matchAllTags: undefined,\n includeDescendants: undefined,\n });\n return;\n }\n\n setProjectFilters({\n tagIds: activeTagFilters.map((tag) => tag.tagId),\n tagHierarchyPaths: activeTagFilters\n .map((tag) => tag.hierarchyPath)\n .filter((tagPath): tagPath is string => typeof tagPath === 'string' && tagPath.length > 0),\n matchAllTags: matchAllTagFilters,\n includeDescendants: true,\n });\n }, [activeTagFilters, matchAllTagFilters, setProjectFilters]);\n\n useEffect(() => {\n return () => clearProjectSelection();\n }, [clearProjectSelection]);\n\n const { data, isLoading, error } = useProjects(projectFilters, undefined, workspaceId);\n const { mutateAsync: archiveProject } = useArchiveProject(workspaceId);\n const { mutateAsync: unarchiveProject } = useUnarchiveProject(workspaceId);\n const { mutateAsync: deleteProject } = useDeleteProject(workspaceId);\n\n const selectedProjects = useMemo(\n () => data?.items.filter((project) => selectedProjectIds.has(project.id)) ?? [],\n [data?.items, selectedProjectIds]\n );\n\n const archivableProjects = useMemo(\n () => selectedProjects.filter((project) => !project.archived),\n [selectedProjects]\n );\n\n const unarchivableProjects = useMemo(\n () => selectedProjects.filter((project) => project.archived),\n [selectedProjects]\n );\n\n const allVisibleSelected =\n (data?.items.length ?? 0) > 0 &&\n (data?.items ?? []).every((project) => selectedProjectIds.has(project.id));\n const partiallySelected =\n (data?.items ?? []).some((project) => selectedProjectIds.has(project.id)) &&\n !allVisibleSelected;\n\n const {\n canListProjects,\n canCreateProject,\n canDeleteProject,\n canArchiveProject,\n isLoading: permissionsLoading,\n } = useWorkspacePermissions();\n\n if (permissionsLoading) {\n return (\n <div className=\"space-y-4 p-6\">\n <Skeleton className=\"h-10 w-1/3\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n </div>\n </div>\n );\n }\n\n if (!canListProjects) {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <h2 className=\"text-lg font-semibold\">\n {tWithFallback(t, 'projectsList.accessDenied', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-muted-foreground\">\n {tWithFallback(\n t,\n 'projectsList.noPermissionToViewProjects',\n \"You don't have permission to view projects.\"\n )}\n </p>\n </div>\n </div>\n );\n }\n\n const handleCreateProject = () => {\n openCreateProjectDialog({} as Workspace);\n };\n\n const handleStatusFilter = (value: string) => {\n if (value === 'all') {\n const nextFilters = { ...projectFilters };\n delete nextFilters.status;\n setProjectFilters(nextFilters);\n } else {\n setProjectFilters({ status: value as 'ACTIVE' | 'INACTIVE' | 'ARCHIVED' });\n }\n };\n\n const handleArchivedFilter = (value: string) => {\n if (value === 'all') {\n const nextFilters = { ...projectFilters };\n delete nextFilters.archived;\n setProjectFilters(nextFilters);\n } else {\n setProjectFilters({ archived: value === 'true' });\n }\n };\n\n const toggleTagFilter = (tag: ProjectTag) => {\n setActiveTagFilters((current) => {\n const exists = current.some((entry) => entry.tagId === tag.tagId);\n if (exists) {\n return current.filter((entry) => entry.tagId !== tag.tagId);\n }\n\n return [...current, tag];\n });\n };\n\n const clearAllProjectFilters = () => {\n setSearchValue('');\n setActiveTagFilters([]);\n setMatchAllTagFilters(true);\n clearProjectFilters();\n };\n\n const toggleAllProjects = (checked: boolean) => {\n (data?.items ?? []).forEach((project) => {\n const isSelected = selectedProjectIds.has(project.id);\n if (checked !== isSelected) {\n toggleProjectSelection(project.id);\n }\n });\n };\n\n const handleBulkArchive = async () => {\n if (archivableProjects.length === 0) return;\n await Promise.allSettled(archivableProjects.map((project) => archiveProject(project.id)));\n clearProjectSelection();\n };\n\n const handleBulkUnarchive = async () => {\n if (unarchivableProjects.length === 0) return;\n await Promise.allSettled(unarchivableProjects.map((project) => unarchiveProject(project.id)));\n clearProjectSelection();\n };\n\n const handleBulkDelete = async () => {\n if (selectedProjects.length === 0) return;\n void nativeImpact('medium');\n const message =\n tWithFallback(t, 'projectsList.bulkDeleteConfirmPrefix', 'Delete ') +\n selectedProjects.length +\n ' ' +\n tWithFallback(t, 'projectsList.bulkDeleteConfirmProject', 'selected project') +\n (selectedProjects.length === 1\n ? ''\n : tWithFallback(t, 'projectsList.bulkDeleteConfirmPluralSuffix', 's')) +\n tWithFallback(t, 'projectsList.bulkDeleteConfirmSuffix', '? This cannot be undone.');\n const native = await nativeConfirm({\n title: 'Delete projects',\n message,\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n const confirmed = native === null ? window.confirm(message) : native;\n if (!confirmed) return;\n\n await Promise.allSettled(selectedProjects.map((project) => deleteProject(project.id)));\n clearProjectSelection();\n };\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col sm:flex-row sm:items-center justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl sm:text-3xl font-bold\">\n {tWithFallback(t, 'projectsList.pageTitle', 'Projects')}\n </h1>\n <p className=\"text-muted-foreground\">\n {tWithFallback(t, 'projectsList.pageSubtitle', 'Manage and organize your projects')}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n {/* View mode toggle */}\n <div className=\"flex items-center gap-1 border rounded-lg p-1\">\n <Button\n variant={viewMode === 'grid' ? 'secondary' : 'ghost'}\n size=\"sm\"\n onClick={() => setViewMode('grid')}\n className=\"h-8 w-8 p-0\"\n aria-label={tWithFallback(\n t,\n 'projectsList.gridViewAriaLabel',\n 'Show projects in grid view'\n )}\n >\n <Grid3x3 size={16} />\n </Button>\n <Button\n variant={viewMode === 'list' ? 'secondary' : 'ghost'}\n size=\"sm\"\n onClick={() => setViewMode('list')}\n className=\"h-8 w-8 p-0\"\n aria-label={tWithFallback(\n t,\n 'projectsList.listViewAriaLabel',\n 'Show projects in list view'\n )}\n >\n <List size={16} />\n </Button>\n </div>\n {canCreateProject && (\n <Button onClick={handleCreateProject}>\n <Plus className=\"h-4 w-4 sm:mr-2\" />\n <span className=\"hidden sm:inline\">\n {tWithFallback(t, 'projectsList.createProject', 'Create Project')}\n </span>\n </Button>\n )}\n </div>\n </div>\n\n {/* Filters & Search */}\n <div className=\"flex flex-col sm:flex-row gap-3\">\n <div className=\"relative flex-1\">\n <Search\n className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground\"\n size={18}\n />\n <Input\n placeholder={tWithFallback(t, 'projectsList.searchPlaceholder', 'Search projects...')}\n value={searchValue}\n onChange={(e) => setSearchValue(e.target.value)}\n className=\"pl-[3.25rem]\"\n />\n </div>\n <Select value={projectFilters.status || 'all'} onValueChange={handleStatusFilter}>\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.filterByStatus', 'Filter by status')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.statusAll', 'All Statuses')}\n </SelectItem>\n <SelectItem value=\"ACTIVE\">\n {tWithFallback(t, 'projectsList.statusActive', 'Active')}\n </SelectItem>\n <SelectItem value=\"INACTIVE\">\n {tWithFallback(t, 'projectsList.statusInactive', 'Inactive')}\n </SelectItem>\n </SelectContent>\n </Select>\n <Select\n value={\n projectFilters.archived === undefined\n ? 'all'\n : projectFilters.archived\n ? 'true'\n : 'false'\n }\n onValueChange={handleArchivedFilter}\n >\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.filterByArchive', 'Filter by archive')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.archiveAll', 'All Projects')}\n </SelectItem>\n <SelectItem value=\"false\">\n {tWithFallback(t, 'projectsList.archiveActiveProjects', 'Active Projects')}\n </SelectItem>\n <SelectItem value=\"true\">\n {tWithFallback(t, 'projectsList.archiveArchived', 'Archived')}\n </SelectItem>\n </SelectContent>\n </Select>\n {(projectFilters.search ||\n projectFilters.status ||\n projectFilters.archived !== undefined ||\n activeTagFilters.length > 0) && (\n <Button variant=\"outline\" onClick={clearAllProjectFilters}>\n {tWithFallback(t, 'projectsList.clearFilters', 'Clear Filters')}\n </Button>\n )}\n </div>\n\n {activeTagFilters.length > 0 && (\n <div className=\"flex flex-col gap-3 rounded-lg border border-border bg-card px-4 py-3\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <p className=\"text-sm font-medium\">\n {tWithFallback(t, 'projectsList.activeTagFilters', 'Active tag filters')}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {tWithFallback(\n t,\n 'projectsList.activeTagFiltersDescription',\n 'Click a project tag to add or remove it from the current filter set.'\n )}\n </p>\n </div>\n <Select\n value={matchAllTagFilters ? 'all' : 'any'}\n onValueChange={(value) => setMatchAllTagFilters(value === 'all')}\n >\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.tagMatchMode', 'Tag match mode')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.tagMatchAll', 'Match all tags')}\n </SelectItem>\n <SelectItem value=\"any\">\n {tWithFallback(t, 'projectsList.tagMatchAny', 'Match any tag')}\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"flex flex-wrap gap-2\">\n {activeTagFilters.map((tag) => (\n <TagBadge\n key={tag.tagId}\n id={tag.tagId}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n removable\n onRemove={() => toggleTagFilter(tag)}\n size=\"sm\"\n />\n ))}\n </div>\n </div>\n )}\n\n {/* Error State */}\n {error && (\n <Card className=\"border-destructive\">\n <CardContent className=\"pt-6\">\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <p className=\"text-destructive font-semibold mb-2\">\n {tWithFallback(t, 'projectsList.errorLoading', 'Error loading projects')}\n </p>\n <p className=\"text-sm text-muted-foreground\">\n {error instanceof Error\n ? error.message\n : tWithFallback(t, 'projectsList.unknownError', 'An unknown error occurred')}\n </p>\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Loading State */}\n {isLoading && (\n <div className=\"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4\">\n {Array.from({ length: 6 }).map((_, i) => (\n <Card key={i}>\n <CardContent className=\"pt-6\">\n <div className=\"space-y-3\">\n <Skeleton className=\"h-12 w-12 rounded-lg\" />\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-1/2\" />\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n )}\n\n {/* Empty State */}\n {!isLoading && !error && data?.items.length === 0 && (\n <Card>\n <CardContent className=\"pt-6\">\n <div className=\"flex flex-col items-center justify-center py-12 text-center\">\n <div className=\"rounded-full bg-muted p-6 mb-4\">\n <FolderKanban size={48} className=\"text-muted-foreground\" />\n </div>\n <h3 className=\"text-lg font-semibold mb-2\">\n {tWithFallback(t, 'projectsList.noProjectsFound', 'No projects found')}\n </h3>\n <p className=\"text-muted-foreground max-w-sm mb-4\">\n {projectFilters.search\n ? tWithFallback(\n t,\n 'projectsList.adjustSearchOrFilters',\n 'Try adjusting your search or filters'\n )\n : tWithFallback(\n t,\n 'projectsList.createFirstProject',\n 'Create your first project to get started organizing your work'\n )}\n </p>\n {!projectFilters.search && canCreateProject && (\n <Button onClick={handleCreateProject}>\n <Plus size={16} className=\"mr-2\" />\n {tWithFallback(t, 'projectsList.createProject', 'Create Project')}\n </Button>\n )}\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Projects Grid */}\n {!isLoading && !error && data && data.items.length > 0 && (\n <>\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 sm:flex-row sm:items-center sm:justify-between\">\n <label className=\"flex items-center gap-2 text-sm text-foreground\">\n <input\n type=\"checkbox\"\n checked={allVisibleSelected}\n ref={(input) => {\n if (input) {\n input.indeterminate = partiallySelected && !allVisibleSelected;\n }\n }}\n onChange={(event) => toggleAllProjects(event.target.checked)}\n aria-label={tWithFallback(\n t,\n 'projectsList.selectAllAriaLabel',\n 'Select all projects'\n )}\n className=\"h-4 w-4 rounded border-border text-primary\"\n />\n <span>{tWithFallback(t, 'projectsList.selectAllVisible', 'Select all visible')}</span>\n </label>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm text-muted-foreground\">\n {selectedProjectIds.size} {tWithFallback(t, 'projectsList.selected', 'selected')}\n </span>\n {selectedProjectIds.size > 0 && (\n <Button variant=\"ghost\" size=\"sm\" onClick={clearProjectSelection}>\n {tWithFallback(t, 'projectsList.clearSelection', 'Clear selection')}\n </Button>\n )}\n {canArchiveProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkArchive}\n disabled={archivableProjects.length === 0}\n >\n {tWithFallback(t, 'projectsList.archiveSelected', 'Archive Selected')}\n {archivableProjects.length > 0 ? ` (${archivableProjects.length})` : ''}\n </Button>\n )}\n {canArchiveProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkUnarchive}\n disabled={unarchivableProjects.length === 0}\n >\n {tWithFallback(t, 'projectsList.unarchiveSelected', 'Unarchive Selected')}\n {unarchivableProjects.length > 0 ? ` (${unarchivableProjects.length})` : ''}\n </Button>\n )}\n {canDeleteProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkDelete}\n disabled={selectedProjects.length === 0}\n className=\"text-destructive\"\n >\n {tWithFallback(t, 'projectsList.deleteSelected', 'Delete Selected')}\n {selectedProjects.length > 0 ? ` (${selectedProjects.length})` : ''}\n </Button>\n )}\n </div>\n </div>\n\n <div\n className={\n viewMode === 'list'\n ? 'space-y-3'\n : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4'\n }\n >\n {data.items.map((project) => (\n <ProjectCard\n key={project.id}\n project={project}\n selected={selectedProjectIds.has(project.id)}\n canArchive={canArchiveProject}\n canDelete={canDeleteProject}\n onTagClick={toggleTagFilter}\n onToggleSelect={(checked) => {\n const isSelected = selectedProjectIds.has(project.id);\n if (checked !== isSelected) {\n toggleProjectSelection(project.id);\n }\n }}\n />\n ))}\n </div>\n\n {/* Pagination Info */}\n {data.total > data.items.length && (\n <div className=\"text-center text-sm text-muted-foreground\">\n {tWithFallback(t, 'projectsList.showingPrefix', 'Showing')} {data.items.length}{' '}\n {tWithFallback(t, 'projectsList.showingOf', 'of')} {data.total}{' '}\n {tWithFallback(t, 'projectsList.showingProjects', 'projects')}\n </div>\n )}\n </>\n )}\n\n {/* Create Project Dialog */}\n {isCreateProjectDialogOpen && <CreateProjectDialog workspaceId={workspaceId} />}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProjectsListPage.js","names":[],"sources":["../../src/pages/ProjectsListPage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { FolderKanban, Plus, Grid3x3, List, Search } from 'lucide-react';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport {\n Button,\n Input,\n Card,\n CardContent,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { useDebounce } from '@burdenoff/fe-libs/shared/hooks';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useWorkspacesStore } from '../store/workspacesStore';\nimport { nativeConfirm, nativeImpact } from '../utils/nativeBridge';\nimport { useMyProjects } from '../hooks/useProjects';\nimport {\n useArchiveProject,\n useDeleteProject,\n useUnarchiveProject,\n} from '../hooks/useProjectMutations';\nimport { ProjectCard } from '../components/ProjectCard';\nimport { CreateProjectDialog } from '../components/CreateProjectDialog';\nimport { useWorkspacePermissions } from '../hooks/useWorkspacePermissions';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\nimport { tWithFallback } from '../utils/i18n';\nimport { normalizeProjectTags } from '../utils/projectTags';\nimport type { ProjectTag, Workspace } from '../types';\n\nexport function ProjectsListPage() {\n const { t } = useI18n();\n\n const { workspaceId } = useParams<{ workspaceId: string }>();\n useWorkspacesPageView('workspaces.projects', { workspaceId, requireParams: true });\n const {\n viewMode,\n setViewMode,\n projectFilters,\n setProjectFilters,\n clearProjectFilters,\n selectedProjectIds,\n toggleProjectSelection,\n clearProjectSelection,\n openCreateProjectDialog,\n isCreateProjectDialogOpen,\n } = useWorkspacesStore();\n\n const [searchValue, setSearchValue] = useState(projectFilters.search || '');\n const [activeTagFilters, setActiveTagFilters] = useState<ProjectTag[]>([]);\n const [matchAllTagFilters, setMatchAllTagFilters] = useState(true);\n const debouncedSearch = useDebounce(searchValue, 500);\n\n // Update filters when debounced search changes\n useEffect(() => {\n setProjectFilters({ search: debouncedSearch });\n }, [debouncedSearch, setProjectFilters]);\n\n useEffect(() => {\n if (activeTagFilters.length === 0) {\n setProjectFilters({\n tagIds: undefined,\n tagHierarchyPaths: undefined,\n matchAllTags: undefined,\n includeDescendants: undefined,\n });\n return;\n }\n\n setProjectFilters({\n tagIds: activeTagFilters.map((tag) => tag.tagId),\n tagHierarchyPaths: activeTagFilters\n .map((tag) => tag.hierarchyPath)\n .filter((tagPath): tagPath is string => typeof tagPath === 'string' && tagPath.length > 0),\n matchAllTags: matchAllTagFilters,\n includeDescendants: true,\n });\n }, [activeTagFilters, matchAllTagFilters, setProjectFilters]);\n\n useEffect(() => {\n return () => clearProjectSelection();\n }, [clearProjectSelection]);\n\n // Membership-scoped query: backend filters by actorId, so no client-side\n // permission gate is needed. Page is shared by `/workspaces/projects` and\n // `/workspaces/:workspaceId/projects`; the latter narrows the list below.\n const PROJECTS_PAGE_SIZE = 500;\n const {\n data: myData,\n isLoading,\n error,\n } = useMyProjects({\n limit: PROJECTS_PAGE_SIZE,\n offset: 0,\n });\n\n const data = useMemo(() => {\n const all = myData?.items ?? [];\n const q = (projectFilters.search ?? '').trim().toLowerCase();\n const tagIds = projectFilters.tagIds ?? [];\n const matchAllTags = projectFilters.matchAllTags ?? true;\n\n const matchesWorkspace = (project: (typeof all)[number]) =>\n !workspaceId || project.workspaceId === workspaceId;\n const matchesSearch = (project: (typeof all)[number]) =>\n !q || project.name.toLowerCase().includes(q) || project.key.toLowerCase().includes(q);\n const matchesStatus = (project: (typeof all)[number]) =>\n !projectFilters.status || project.status === projectFilters.status;\n const matchesArchived = (project: (typeof all)[number]) =>\n typeof projectFilters.archived === 'undefined' ||\n project.archived === projectFilters.archived;\n const matchesTags = (project: (typeof all)[number]) => {\n if (tagIds.length === 0) return true;\n const projectTagIds = new Set(normalizeProjectTags(project.tags).map((t) => t.tagId));\n return matchAllTags\n ? tagIds.every((id) => projectTagIds.has(id))\n : tagIds.some((id) => projectTagIds.has(id));\n };\n\n const filtered = all.filter(\n (project) =>\n matchesWorkspace(project) &&\n matchesSearch(project) &&\n matchesStatus(project) &&\n matchesArchived(project) &&\n matchesTags(project)\n );\n\n // Surface the real backend totals so the \"Showing X of Y\" footer can flag\n // truncation. For narrowed views (workspace/tag filter), the backend total\n // doesn't reflect the post-filter count, so we report the filtered size as\n // total. But we MUST preserve `hasMore` from upstream — if the upstream\n // query was capped (hasMore=true), matches outside the first page are\n // silently hidden, and the footer needs to keep flagging that.\n const apiTotal = myData?.total ?? filtered.length;\n const apiHasMore = myData?.hasMore ?? false;\n const isNarrowed = !!workspaceId || tagIds.length > 0;\n return {\n items: filtered,\n total: isNarrowed ? filtered.length : apiTotal,\n hasMore: apiHasMore,\n };\n }, [myData?.items, myData?.total, myData?.hasMore, projectFilters, workspaceId]);\n\n const { mutateAsync: archiveProject } = useArchiveProject(workspaceId);\n const { mutateAsync: unarchiveProject } = useUnarchiveProject(workspaceId);\n const { mutateAsync: deleteProject } = useDeleteProject(workspaceId);\n\n const selectedProjects = useMemo(\n () => data?.items.filter((project) => selectedProjectIds.has(project.id)) ?? [],\n [data?.items, selectedProjectIds]\n );\n\n const archivableProjects = useMemo(\n () => selectedProjects.filter((project) => !project.archived),\n [selectedProjects]\n );\n\n const unarchivableProjects = useMemo(\n () => selectedProjects.filter((project) => project.archived),\n [selectedProjects]\n );\n\n const allVisibleSelected =\n (data?.items.length ?? 0) > 0 &&\n (data?.items ?? []).every((project) => selectedProjectIds.has(project.id));\n const partiallySelected =\n (data?.items ?? []).some((project) => selectedProjectIds.has(project.id)) &&\n !allVisibleSelected;\n\n const {\n canCreateProject,\n canDeleteProject,\n canArchiveProject,\n isLoading: permissionsLoading,\n } = useWorkspacePermissions();\n\n if (permissionsLoading) {\n return (\n <div className=\"space-y-4 p-6\">\n <Skeleton className=\"h-10 w-1/3\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n </div>\n </div>\n );\n }\n\n // No client-side list gate: this page calls `myProjects` which is\n // membership-scoped — the resolver filters by actorId on the backend, so the\n // data set returned IS the access list. If the user is in zero projects, the\n // page renders the empty state below. Per-action checks (canCreateProject,\n // canDeleteProject, canArchiveProject) remain in place to hide buttons the\n // user isn't authorized for on their workspace.\n\n const handleCreateProject = () => {\n openCreateProjectDialog({} as Workspace);\n };\n\n const handleStatusFilter = (value: string) => {\n if (value === 'all') {\n const nextFilters = { ...projectFilters };\n delete nextFilters.status;\n setProjectFilters(nextFilters);\n } else {\n setProjectFilters({ status: value as 'ACTIVE' | 'INACTIVE' | 'ARCHIVED' });\n }\n };\n\n const handleArchivedFilter = (value: string) => {\n if (value === 'all') {\n const nextFilters = { ...projectFilters };\n delete nextFilters.archived;\n setProjectFilters(nextFilters);\n } else {\n setProjectFilters({ archived: value === 'true' });\n }\n };\n\n const toggleTagFilter = (tag: ProjectTag) => {\n setActiveTagFilters((current) => {\n const exists = current.some((entry) => entry.tagId === tag.tagId);\n if (exists) {\n return current.filter((entry) => entry.tagId !== tag.tagId);\n }\n\n return [...current, tag];\n });\n };\n\n const clearAllProjectFilters = () => {\n setSearchValue('');\n setActiveTagFilters([]);\n setMatchAllTagFilters(true);\n clearProjectFilters();\n };\n\n const toggleAllProjects = (checked: boolean) => {\n (data?.items ?? []).forEach((project) => {\n const isSelected = selectedProjectIds.has(project.id);\n if (checked !== isSelected) {\n toggleProjectSelection(project.id);\n }\n });\n };\n\n const handleBulkArchive = async () => {\n if (archivableProjects.length === 0) return;\n await Promise.allSettled(archivableProjects.map((project) => archiveProject(project.id)));\n clearProjectSelection();\n };\n\n const handleBulkUnarchive = async () => {\n if (unarchivableProjects.length === 0) return;\n await Promise.allSettled(unarchivableProjects.map((project) => unarchiveProject(project.id)));\n clearProjectSelection();\n };\n\n const handleBulkDelete = async () => {\n if (selectedProjects.length === 0) return;\n void nativeImpact('medium');\n const message =\n tWithFallback(t, 'projectsList.bulkDeleteConfirmPrefix', 'Delete ') +\n selectedProjects.length +\n ' ' +\n tWithFallback(t, 'projectsList.bulkDeleteConfirmProject', 'selected project') +\n (selectedProjects.length === 1\n ? ''\n : tWithFallback(t, 'projectsList.bulkDeleteConfirmPluralSuffix', 's')) +\n tWithFallback(t, 'projectsList.bulkDeleteConfirmSuffix', '? This cannot be undone.');\n const native = await nativeConfirm({\n title: 'Delete projects',\n message,\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n const confirmed = native === null ? window.confirm(message) : native;\n if (!confirmed) return;\n\n await Promise.allSettled(selectedProjects.map((project) => deleteProject(project.id)));\n clearProjectSelection();\n };\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col sm:flex-row sm:items-center justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl sm:text-3xl font-bold\">\n {tWithFallback(t, 'projectsList.pageTitle', 'Projects')}\n </h1>\n <p className=\"text-muted-foreground\">\n {tWithFallback(t, 'projectsList.pageSubtitle', 'Manage and organize your projects')}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n {/* View mode toggle */}\n <div className=\"flex items-center gap-1 border rounded-lg p-1\">\n <Button\n variant={viewMode === 'grid' ? 'secondary' : 'ghost'}\n size=\"sm\"\n onClick={() => setViewMode('grid')}\n className=\"h-8 w-8 p-0\"\n aria-label={tWithFallback(\n t,\n 'projectsList.gridViewAriaLabel',\n 'Show projects in grid view'\n )}\n >\n <Grid3x3 size={16} />\n </Button>\n <Button\n variant={viewMode === 'list' ? 'secondary' : 'ghost'}\n size=\"sm\"\n onClick={() => setViewMode('list')}\n className=\"h-8 w-8 p-0\"\n aria-label={tWithFallback(\n t,\n 'projectsList.listViewAriaLabel',\n 'Show projects in list view'\n )}\n >\n <List size={16} />\n </Button>\n </div>\n {canCreateProject && (\n <Button onClick={handleCreateProject}>\n <Plus className=\"h-4 w-4 sm:mr-2\" />\n <span className=\"hidden sm:inline\">\n {tWithFallback(t, 'projectsList.createProject', 'Create Project')}\n </span>\n </Button>\n )}\n </div>\n </div>\n\n {/* Filters & Search */}\n <div className=\"flex flex-col sm:flex-row gap-3\">\n <div className=\"relative flex-1\">\n <Search\n className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground\"\n size={18}\n />\n <Input\n placeholder={tWithFallback(t, 'projectsList.searchPlaceholder', 'Search projects...')}\n value={searchValue}\n onChange={(e) => setSearchValue(e.target.value)}\n className=\"pl-[3.25rem]\"\n />\n </div>\n <Select value={projectFilters.status || 'all'} onValueChange={handleStatusFilter}>\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.filterByStatus', 'Filter by status')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.statusAll', 'All Statuses')}\n </SelectItem>\n <SelectItem value=\"ACTIVE\">\n {tWithFallback(t, 'projectsList.statusActive', 'Active')}\n </SelectItem>\n <SelectItem value=\"INACTIVE\">\n {tWithFallback(t, 'projectsList.statusInactive', 'Inactive')}\n </SelectItem>\n </SelectContent>\n </Select>\n <Select\n value={\n projectFilters.archived === undefined\n ? 'all'\n : projectFilters.archived\n ? 'true'\n : 'false'\n }\n onValueChange={handleArchivedFilter}\n >\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.filterByArchive', 'Filter by archive')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.archiveAll', 'All Projects')}\n </SelectItem>\n <SelectItem value=\"false\">\n {tWithFallback(t, 'projectsList.archiveActiveProjects', 'Active Projects')}\n </SelectItem>\n <SelectItem value=\"true\">\n {tWithFallback(t, 'projectsList.archiveArchived', 'Archived')}\n </SelectItem>\n </SelectContent>\n </Select>\n {(projectFilters.search ||\n projectFilters.status ||\n projectFilters.archived !== undefined ||\n activeTagFilters.length > 0) && (\n <Button variant=\"outline\" onClick={clearAllProjectFilters}>\n {tWithFallback(t, 'projectsList.clearFilters', 'Clear Filters')}\n </Button>\n )}\n </div>\n\n {activeTagFilters.length > 0 && (\n <div className=\"flex flex-col gap-3 rounded-lg border border-border bg-card px-4 py-3\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <p className=\"text-sm font-medium\">\n {tWithFallback(t, 'projectsList.activeTagFilters', 'Active tag filters')}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {tWithFallback(\n t,\n 'projectsList.activeTagFiltersDescription',\n 'Click a project tag to add or remove it from the current filter set.'\n )}\n </p>\n </div>\n <Select\n value={matchAllTagFilters ? 'all' : 'any'}\n onValueChange={(value) => setMatchAllTagFilters(value === 'all')}\n >\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.tagMatchMode', 'Tag match mode')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.tagMatchAll', 'Match all tags')}\n </SelectItem>\n <SelectItem value=\"any\">\n {tWithFallback(t, 'projectsList.tagMatchAny', 'Match any tag')}\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"flex flex-wrap gap-2\">\n {activeTagFilters.map((tag) => (\n <TagBadge\n key={tag.tagId}\n id={tag.tagId}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n removable\n onRemove={() => toggleTagFilter(tag)}\n size=\"sm\"\n />\n ))}\n </div>\n </div>\n )}\n\n {/* Error State */}\n {error && (\n <Card className=\"border-destructive\">\n <CardContent className=\"pt-6\">\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <p className=\"text-destructive font-semibold mb-2\">\n {tWithFallback(t, 'projectsList.errorLoading', 'Error loading projects')}\n </p>\n <p className=\"text-sm text-muted-foreground\">\n {error instanceof Error\n ? error.message\n : tWithFallback(t, 'projectsList.unknownError', 'An unknown error occurred')}\n </p>\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Loading State */}\n {isLoading && (\n <div className=\"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4\">\n {Array.from({ length: 6 }).map((_, i) => (\n <Card key={i}>\n <CardContent className=\"pt-6\">\n <div className=\"space-y-3\">\n <Skeleton className=\"h-12 w-12 rounded-lg\" />\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-1/2\" />\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n )}\n\n {/* Empty State */}\n {!isLoading && !error && data?.items.length === 0 && (\n <Card>\n <CardContent className=\"pt-6\">\n <div className=\"flex flex-col items-center justify-center py-12 text-center\">\n <div className=\"rounded-full bg-muted p-6 mb-4\">\n <FolderKanban size={48} className=\"text-muted-foreground\" />\n </div>\n <h3 className=\"text-lg font-semibold mb-2\">\n {tWithFallback(t, 'projectsList.noProjectsFound', 'No projects found')}\n </h3>\n <p className=\"text-muted-foreground max-w-sm mb-4\">\n {projectFilters.search\n ? tWithFallback(\n t,\n 'projectsList.adjustSearchOrFilters',\n 'Try adjusting your search or filters'\n )\n : tWithFallback(\n t,\n 'projectsList.createFirstProject',\n 'Create your first project to get started organizing your work'\n )}\n </p>\n {!projectFilters.search && canCreateProject && (\n <Button onClick={handleCreateProject}>\n <Plus size={16} className=\"mr-2\" />\n {tWithFallback(t, 'projectsList.createProject', 'Create Project')}\n </Button>\n )}\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Projects Grid */}\n {!isLoading && !error && data && data.items.length > 0 && (\n <>\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 sm:flex-row sm:items-center sm:justify-between\">\n <label className=\"flex items-center gap-2 text-sm text-foreground\">\n <input\n type=\"checkbox\"\n checked={allVisibleSelected}\n ref={(input) => {\n if (input) {\n input.indeterminate = partiallySelected && !allVisibleSelected;\n }\n }}\n onChange={(event) => toggleAllProjects(event.target.checked)}\n aria-label={tWithFallback(\n t,\n 'projectsList.selectAllAriaLabel',\n 'Select all projects'\n )}\n className=\"h-4 w-4 rounded border-border text-primary\"\n />\n <span>{tWithFallback(t, 'projectsList.selectAllVisible', 'Select all visible')}</span>\n </label>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm text-muted-foreground\">\n {selectedProjectIds.size} {tWithFallback(t, 'projectsList.selected', 'selected')}\n </span>\n {selectedProjectIds.size > 0 && (\n <Button variant=\"ghost\" size=\"sm\" onClick={clearProjectSelection}>\n {tWithFallback(t, 'projectsList.clearSelection', 'Clear selection')}\n </Button>\n )}\n {/* Bulk mutations are bound to a single workspaceId via the\n archive/unarchive/delete hooks. On the cross-workspace\n /workspaces/projects route (workspaceId is undefined),\n hide them — selections can span workspaces and the bound\n hook would fail authorization for projects outside the\n caller's context. The per-card actions in ProjectCard\n still work because they resolve per-project. */}\n {workspaceId && canArchiveProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkArchive}\n disabled={archivableProjects.length === 0}\n >\n {tWithFallback(t, 'projectsList.archiveSelected', 'Archive Selected')}\n {archivableProjects.length > 0 ? ` (${archivableProjects.length})` : ''}\n </Button>\n )}\n {workspaceId && canArchiveProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkUnarchive}\n disabled={unarchivableProjects.length === 0}\n >\n {tWithFallback(t, 'projectsList.unarchiveSelected', 'Unarchive Selected')}\n {unarchivableProjects.length > 0 ? ` (${unarchivableProjects.length})` : ''}\n </Button>\n )}\n {workspaceId && canDeleteProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkDelete}\n disabled={selectedProjects.length === 0}\n className=\"text-destructive\"\n >\n {tWithFallback(t, 'projectsList.deleteSelected', 'Delete Selected')}\n {selectedProjects.length > 0 ? ` (${selectedProjects.length})` : ''}\n </Button>\n )}\n </div>\n </div>\n\n <div\n className={\n viewMode === 'list'\n ? 'space-y-3'\n : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4'\n }\n >\n {data.items.map((project) => (\n <ProjectCard\n key={project.id}\n project={project}\n selected={selectedProjectIds.has(project.id)}\n canArchive={canArchiveProject}\n canDelete={canDeleteProject}\n onTagClick={toggleTagFilter}\n onToggleSelect={(checked) => {\n const isSelected = selectedProjectIds.has(project.id);\n if (checked !== isSelected) {\n toggleProjectSelection(project.id);\n }\n }}\n />\n ))}\n </div>\n\n {/* Pagination Info */}\n {data.total > data.items.length && (\n <div className=\"text-center text-sm text-muted-foreground\">\n {tWithFallback(t, 'projectsList.showingPrefix', 'Showing')} {data.items.length}{' '}\n {tWithFallback(t, 'projectsList.showingOf', 'of')} {data.total}{' '}\n {tWithFallback(t, 'projectsList.showingProjects', 'projects')}\n </div>\n )}\n </>\n )}\n\n {/* Create Project Dialog */}\n {isCreateProjectDialogOpen && <CreateProjectDialog workspaceId={workspaceId} />}\n </div>\n );\n}\n"],"mappings":"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"}
|