@burdenoff/microfe-workspaces 2026.830.1 → 2026.915.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.
- package/dist/components/CreateProjectDialog.js +65 -61
- package/dist/components/CreateProjectDialog.js.map +1 -1
- package/dist/components/CreateWorkspaceDialog.js +93 -90
- package/dist/components/CreateWorkspaceDialog.js.map +1 -1
- package/dist/components/InviteMemberDialog.js +122 -111
- package/dist/components/InviteMemberDialog.js.map +1 -1
- package/dist/components/MemberListItem.js +55 -58
- package/dist/components/MemberListItem.js.map +1 -1
- package/dist/components/ProjectCard.js +93 -92
- package/dist/components/ProjectCard.js.map +1 -1
- package/dist/components/ProjectMemberPickerDialog.js +78 -93
- package/dist/components/ProjectMemberPickerDialog.js.map +1 -1
- package/dist/components/RenameProjectDialog.js +47 -44
- package/dist/components/RenameProjectDialog.js.map +1 -1
- package/dist/components/WorkspaceCard.js +42 -43
- package/dist/components/WorkspaceCard.js.map +1 -1
- package/dist/components/WorkspacesErrorBoundary.js +43 -34
- package/dist/components/WorkspacesErrorBoundary.js.map +1 -1
- package/dist/components/tags/ProjectTagsWidget.js +75 -77
- package/dist/components/tags/ProjectTagsWidget.js.map +1 -1
- package/dist/hooks/useTr.js +14 -0
- package/dist/hooks/useTr.js.map +1 -0
- package/dist/index.js +2 -1
- package/dist/pages/ActivityListPage.js +3 -3
- package/dist/pages/CreateWorkspacePage.js +10 -10
- package/dist/pages/InviteAcceptancePage.js +49 -49
- package/dist/pages/InviteAcceptancePage.js.map +1 -1
- package/dist/pages/MembersListPage.js +6 -6
- package/dist/pages/PendingInvitesPage.js +3 -3
- package/dist/pages/ProjectDetailPage.js +4 -4
- package/dist/pages/ProjectDetailPage.js.map +1 -1
- package/dist/pages/ProjectMembersPage.js +3 -3
- package/dist/pages/ProjectsListPage.js +5 -5
- package/dist/pages/WorkspaceDetailPage.js +4 -4
- package/dist/pages/WorkspaceInvitePage.js +4 -4
- package/dist/pages/WorkspaceSettingsPage.js +7 -7
- package/dist/pages/WorkspacesListPage.js +9 -9
- package/dist/pages/dashboard/widgets/RecentActivityWidget.js +4 -4
- package/dist/pages/dashboard/widgets/StorageWidget.js +3 -3
- package/dist/pages/dashboard/widgets/WelcomeWidget.js +26 -16
- package/dist/pages/dashboard/widgets/WelcomeWidget.js.map +1 -1
- package/dist/translations/en.js +908 -0
- package/dist/translations/en.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useTr as e } from "../../hooks/useTr.js";
|
|
2
|
+
import { useCallback as t, useEffect as n, useRef as r, useState as i } from "react";
|
|
3
|
+
import { Loader2 as a, Plus as ee, Tag as te, X as ne } from "lucide-react";
|
|
3
4
|
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
4
|
-
import { Command as
|
|
5
|
-
import { cn as
|
|
6
|
-
import { useGatewayClient as
|
|
7
|
-
import { gql as
|
|
8
|
-
import { useLazyQuery as
|
|
9
|
-
import { TagBadge as
|
|
5
|
+
import { Command as re, CommandEmpty as c, CommandGroup as l, CommandInput as u, CommandItem as d, CommandList as f, Popover as p, PopoverContent as ie, PopoverTrigger as ae } from "@burdenoff/fe-libs/ui";
|
|
6
|
+
import { cn as m } from "@burdenoff/fe-libs/shared/utils";
|
|
7
|
+
import { useGatewayClient as oe } from "@burdenoff/fe-libs/shared/graphql";
|
|
8
|
+
import { gql as h } from "@apollo/client";
|
|
9
|
+
import { useLazyQuery as g, useMutation as _ } from "@apollo/client/react";
|
|
10
|
+
import { TagBadge as v } from "@burdenoff/fe-libs/tag";
|
|
10
11
|
//#region src/components/tags/ProjectTagsWidget.tsx
|
|
11
|
-
var
|
|
12
|
+
var y = h`
|
|
12
13
|
query ProjectTagsWidget_SearchTags($query: String!, $vocabularyId: ID, $limit: Int) {
|
|
13
14
|
searchTags(query: $query, vocabularyId: $vocabularyId, limit: $limit) {
|
|
14
15
|
id
|
|
@@ -26,7 +27,7 @@ var b = g`
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
|
-
`,
|
|
30
|
+
`, b = h`
|
|
30
31
|
query ProjectTagsWidget_GetVocabularyByKey($key: String!) {
|
|
31
32
|
getVocabularyByKey(key: $key) {
|
|
32
33
|
id
|
|
@@ -34,7 +35,7 @@ var b = g`
|
|
|
34
35
|
name
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
|
-
`,
|
|
38
|
+
`, se = h`
|
|
38
39
|
mutation ProjectTagsWidget_CreateVocabulary($input: CreateVocabularyInput!) {
|
|
39
40
|
createVocabulary(input: $input) {
|
|
40
41
|
id
|
|
@@ -42,7 +43,7 @@ var b = g`
|
|
|
42
43
|
name
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
|
-
`,
|
|
46
|
+
`, x = h`
|
|
46
47
|
mutation ProjectTagsWidget_CreateTag($input: CreateTagInput!) {
|
|
47
48
|
createTag(input: $input) {
|
|
48
49
|
id
|
|
@@ -65,11 +66,11 @@ function C(e) {
|
|
|
65
66
|
let t = e;
|
|
66
67
|
return typeof t.id == "string" && typeof t.key == "string" && typeof t.label == "string";
|
|
67
68
|
}
|
|
68
|
-
function w({ entityType:
|
|
69
|
-
let [
|
|
70
|
-
if (
|
|
69
|
+
function w({ entityType: h, entityId: w, workspaceId: ce, tags: T, onAdd: E, onRemove: D, allowCreate: O = !0, maxTags: k = 20, className: le }) {
|
|
70
|
+
let A = e(), [ue, j] = i(!1), [M, N] = i(""), [P, F] = i(null), [I, L] = i(!1), [R, z] = i(), B = r(null), V = oe("workspace"), H = !ce || !w, de = new Set(T.map((e) => e.id)), U = T.length < k, [W, { data: fe, loading: G }] = g(y, { client: V }), K = (fe?.searchTags ?? []).filter((e) => C(e) && !de.has(e.id)), [q, { loading: J }] = _(x, { client: V }), [Y] = g(b, { client: V }), [X] = _(se, { client: V }), Z = t(async () => {
|
|
71
|
+
if (R) return R;
|
|
71
72
|
let e = (await Y({ variables: { key: S } })).data?.getVocabularyByKey;
|
|
72
|
-
if (e) return
|
|
73
|
+
if (e) return z(e.id), e.id;
|
|
73
74
|
let t = (await X({ variables: { input: {
|
|
74
75
|
key: S,
|
|
75
76
|
name: "General",
|
|
@@ -77,51 +78,52 @@ function w({ entityType: g, entityId: w, workspaceId: se, tags: T, onAdd: E, onR
|
|
|
77
78
|
scope: "WORKSPACE",
|
|
78
79
|
allowCustomTags: !0
|
|
79
80
|
} } })).data?.createVocabulary;
|
|
80
|
-
if (!t) throw Error("Failed to create default vocabulary");
|
|
81
|
-
return
|
|
81
|
+
if (!t) throw Error(A("projectTagsWidget.createVocabularyError", "Failed to create default vocabulary"));
|
|
82
|
+
return z(t.id), t.id;
|
|
82
83
|
}, [
|
|
83
|
-
|
|
84
|
+
R,
|
|
84
85
|
Y,
|
|
85
|
-
X
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
X,
|
|
87
|
+
A
|
|
88
|
+
]), pe = t((e) => {
|
|
89
|
+
N(e), P && F(null), B.current && clearTimeout(B.current), e.trim() && (B.current = setTimeout(() => {
|
|
88
90
|
W({ variables: {
|
|
89
91
|
query: e,
|
|
90
|
-
vocabularyId:
|
|
92
|
+
vocabularyId: R,
|
|
91
93
|
limit: 10
|
|
92
94
|
} });
|
|
93
95
|
}, 250));
|
|
94
96
|
}, [
|
|
95
97
|
W,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
R,
|
|
99
|
+
P
|
|
98
100
|
]);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
n(() => () => {
|
|
102
|
+
B.current && clearTimeout(B.current);
|
|
101
103
|
}, []);
|
|
102
|
-
let Q =
|
|
103
|
-
if (!(!U ||
|
|
104
|
-
|
|
104
|
+
let Q = t(async (e) => {
|
|
105
|
+
if (!(!U || I)) {
|
|
106
|
+
L(!0);
|
|
105
107
|
try {
|
|
106
108
|
await E({
|
|
107
109
|
...e,
|
|
108
110
|
vocabularyKey: e.vocabularyKey ?? e.vocabulary?.key ?? null
|
|
109
|
-
}),
|
|
111
|
+
}), N(""), j(!1);
|
|
110
112
|
} finally {
|
|
111
|
-
|
|
113
|
+
L(!1);
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
116
|
}, [
|
|
115
117
|
E,
|
|
116
118
|
U,
|
|
117
|
-
|
|
118
|
-
]),
|
|
119
|
-
if (!(!O || !
|
|
120
|
-
|
|
119
|
+
I
|
|
120
|
+
]), me = t(async () => {
|
|
121
|
+
if (!(!O || !M.trim() || J)) {
|
|
122
|
+
F(null);
|
|
121
123
|
try {
|
|
122
|
-
let e =
|
|
124
|
+
let e = M.trim(), t = e.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
123
125
|
if (!t) {
|
|
124
|
-
|
|
126
|
+
F(A("projectTagsWidget.createValidationError", "Must contain at least one letter or number"));
|
|
125
127
|
return;
|
|
126
128
|
}
|
|
127
129
|
let n = (await q({ variables: { input: {
|
|
@@ -132,69 +134,70 @@ function w({ entityType: g, entityId: w, workspaceId: se, tags: T, onAdd: E, onR
|
|
|
132
134
|
if (!C(n)) throw Error("No data returned");
|
|
133
135
|
await Q(n);
|
|
134
136
|
} catch (e) {
|
|
135
|
-
|
|
137
|
+
F(e instanceof Error ? e.message : A("projectTagsWidget.createGenericError", "Failed to create tag"));
|
|
136
138
|
}
|
|
137
139
|
}
|
|
138
140
|
}, [
|
|
139
141
|
O,
|
|
140
|
-
|
|
142
|
+
M,
|
|
141
143
|
J,
|
|
142
144
|
Z,
|
|
143
145
|
q,
|
|
144
|
-
Q
|
|
145
|
-
|
|
146
|
+
Q,
|
|
147
|
+
A
|
|
148
|
+
]), he = t(async (e) => {
|
|
146
149
|
await D(e);
|
|
147
150
|
}, [D]);
|
|
148
|
-
if (
|
|
149
|
-
let $ = !!(O &&
|
|
151
|
+
if (H) return null;
|
|
152
|
+
let $ = !!(O && M.trim() && !G && K.length === 0);
|
|
150
153
|
return /* @__PURE__ */ s("div", {
|
|
151
|
-
className:
|
|
152
|
-
children: [T.map((e) => /* @__PURE__ */ o(
|
|
154
|
+
className: m("flex flex-wrap items-center gap-1.5", le),
|
|
155
|
+
children: [T.map((e) => /* @__PURE__ */ o(v, {
|
|
153
156
|
id: e.id,
|
|
154
157
|
label: e.label,
|
|
155
158
|
color: e.color ?? void 0,
|
|
156
159
|
icon: e.icon ?? void 0,
|
|
157
160
|
description: e.description ?? void 0,
|
|
158
161
|
removable: !0,
|
|
159
|
-
onRemove: () => void
|
|
162
|
+
onRemove: () => void he(e.id),
|
|
160
163
|
size: "sm"
|
|
161
164
|
}, e.id)), U && /* @__PURE__ */ s(p, {
|
|
162
|
-
open:
|
|
163
|
-
onOpenChange:
|
|
164
|
-
children: [/* @__PURE__ */ s(
|
|
165
|
+
open: ue,
|
|
166
|
+
onOpenChange: j,
|
|
167
|
+
children: [/* @__PURE__ */ s(ae, {
|
|
165
168
|
type: "button",
|
|
166
|
-
"aria-label": "Add tag",
|
|
167
|
-
className:
|
|
168
|
-
children: [/* @__PURE__ */ o(
|
|
169
|
-
}), /* @__PURE__ */ o(
|
|
169
|
+
"aria-label": A("projectTagsWidget.addTagAriaLabel", "Add tag"),
|
|
170
|
+
className: m("inline-flex items-center gap-1 px-2 py-0.5 rounded-full border border-dashed", "border-border-subtle text-text-secondary text-xs", "hover:border-foreground hover:text-text-primary transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]"),
|
|
171
|
+
children: [/* @__PURE__ */ o(te, { className: "size-3" }), /* @__PURE__ */ o("span", { children: T.length > 0 ? A("projectTagsWidget.add", "Add") : A("projectTagsWidget.addTags", "Add tags") })]
|
|
172
|
+
}), /* @__PURE__ */ o(ie, {
|
|
170
173
|
className: "z-50 w-72 max-w-[calc(100vw-2rem)] border-border-subtle/80 bg-popover p-0 shadow-xl",
|
|
171
174
|
align: "start",
|
|
172
175
|
side: "bottom",
|
|
173
176
|
sideOffset: 8,
|
|
174
|
-
children: /* @__PURE__ */ s(
|
|
177
|
+
children: /* @__PURE__ */ s(re, {
|
|
175
178
|
shouldFilter: !1,
|
|
176
179
|
children: [
|
|
177
180
|
/* @__PURE__ */ s("div", {
|
|
178
181
|
className: "relative",
|
|
179
182
|
children: [/* @__PURE__ */ o(u, {
|
|
180
|
-
placeholder: "Search or create a tag...",
|
|
181
|
-
value:
|
|
182
|
-
onValueChange:
|
|
183
|
+
placeholder: A("projectTagsWidget.searchPlaceholder", "Search or create a tag..."),
|
|
184
|
+
value: M,
|
|
185
|
+
onValueChange: pe,
|
|
183
186
|
className: "h-10 border-0 bg-transparent pr-9 text-sm outline-none placeholder:text-text-secondary"
|
|
184
|
-
}), G && /* @__PURE__ */ o(
|
|
187
|
+
}), G && /* @__PURE__ */ o(a, { className: "absolute right-3 top-1/2 size-3.5 -translate-y-1/2 animate-spin text-text-secondary" })]
|
|
185
188
|
}),
|
|
186
189
|
/* @__PURE__ */ s(f, {
|
|
187
190
|
className: "max-h-48 overflow-y-auto",
|
|
188
191
|
children: [
|
|
189
|
-
|
|
192
|
+
M.trim() === "" && /* @__PURE__ */ o(c, {
|
|
190
193
|
className: "py-4 text-center text-xs text-text-secondary",
|
|
191
|
-
children: "Type to search or create a tag"
|
|
194
|
+
children: A("projectTagsWidget.typeToSearch", "Type to search or create a tag")
|
|
192
195
|
}),
|
|
193
196
|
K.length > 0 && /* @__PURE__ */ o(l, { children: K.map((e) => /* @__PURE__ */ s(d, {
|
|
194
197
|
value: e.id,
|
|
195
198
|
onSelect: () => void Q(e),
|
|
196
199
|
className: "flex items-center gap-2 px-3 py-2 cursor-pointer",
|
|
197
|
-
disabled:
|
|
200
|
+
disabled: I,
|
|
198
201
|
children: [
|
|
199
202
|
e.icon && /* @__PURE__ */ o("span", {
|
|
200
203
|
className: "text-sm",
|
|
@@ -205,37 +208,32 @@ function w({ entityType: g, entityId: w, workspaceId: se, tags: T, onAdd: E, onR
|
|
|
205
208
|
style: e.color ? { color: e.color } : void 0,
|
|
206
209
|
children: e.label
|
|
207
210
|
}),
|
|
208
|
-
|
|
211
|
+
I && /* @__PURE__ */ o(a, { className: "ml-auto size-3 animate-spin" })
|
|
209
212
|
]
|
|
210
213
|
}, e.id)) }),
|
|
211
214
|
$ && /* @__PURE__ */ o(l, { children: /* @__PURE__ */ s(d, {
|
|
212
|
-
value: `__create__${
|
|
213
|
-
onSelect: () => void
|
|
215
|
+
value: `__create__${M}`,
|
|
216
|
+
onSelect: () => void me(),
|
|
214
217
|
className: "flex items-center gap-2 px-3 py-2 cursor-pointer text-primary",
|
|
215
218
|
disabled: J,
|
|
216
|
-
children: [J ? /* @__PURE__ */ o(
|
|
219
|
+
children: [J ? /* @__PURE__ */ o(a, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ o(ee, { className: "size-3.5" }), /* @__PURE__ */ o("span", {
|
|
217
220
|
className: "text-sm truncate",
|
|
218
|
-
children: J ? "Creating..." :
|
|
221
|
+
children: J ? A("projectTagsWidget.creating", "Creating...") : A("projectTagsWidget.create", "Create \"{{label}}\"", { label: M })
|
|
219
222
|
})]
|
|
220
223
|
}) }),
|
|
221
|
-
|
|
224
|
+
M.trim() !== "" && !G && K.length === 0 && !$ && /* @__PURE__ */ o(c, {
|
|
222
225
|
className: "py-4 text-center text-xs text-text-secondary",
|
|
223
|
-
children: "No tags found"
|
|
226
|
+
children: A("projectTagsWidget.noTagsFound", "No tags found")
|
|
224
227
|
}),
|
|
225
|
-
|
|
228
|
+
P && /* @__PURE__ */ o("div", {
|
|
226
229
|
className: "px-3 py-2 text-xs text-status-error-text border-t border-border-subtle",
|
|
227
|
-
children:
|
|
230
|
+
children: P
|
|
228
231
|
})
|
|
229
232
|
]
|
|
230
233
|
}),
|
|
231
234
|
!U && /* @__PURE__ */ s("div", {
|
|
232
235
|
className: "border-t border-border-subtle px-3 py-2 text-xs text-text-secondary flex items-center gap-1",
|
|
233
|
-
children: [
|
|
234
|
-
/* @__PURE__ */ o(te, { className: "size-3" }),
|
|
235
|
-
"Maximum ",
|
|
236
|
-
k,
|
|
237
|
-
" tags reached"
|
|
238
|
-
]
|
|
236
|
+
children: [/* @__PURE__ */ o(ne, { className: "size-3" }), A("projectTagsWidget.maxTagsReached", "Maximum {{maxTags}} tags reached", { maxTags: k })]
|
|
239
237
|
})
|
|
240
238
|
]
|
|
241
239
|
})
|
|
@@ -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, 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-subtle text-text-secondary text-xs',\n 'hover:border-foreground hover:text-text-primary transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-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-subtle/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-text-secondary\"\n />\n {searching && (\n <Loader2 className=\"absolute right-3 top-1/2 size-3.5 -translate-y-1/2 animate-spin text-text-secondary\" />\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-text-secondary\">\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-text-secondary\">\n No tags found\n </CommandEmpty>\n )}\n\n {createError && (\n <div className=\"px-3 py-2 text-xs text-status-error-text border-t border-border-subtle\">\n {createError}\n </div>\n )}\n </CommandList>\n\n {!canAddMore && (\n <div className=\"border-t border-border-subtle px-3 py-2 text-xs text-text-secondary 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';\nimport { useTr } from '../../hooks/useTr';\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 tr = useTr();\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)\n throw new Error(\n tr('projectTagsWidget.createVocabularyError', 'Failed to create default vocabulary')\n );\n setResolvedVocabId(newVocab.id);\n return newVocab.id;\n }, [resolvedVocabId, fetchVocabByKey, createVocabulary, tr]);\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(\n tr(\n 'projectTagsWidget.createValidationError',\n 'Must contain at least one letter or number'\n )\n );\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(\n err instanceof Error\n ? err.message\n : tr('projectTagsWidget.createGenericError', 'Failed to create tag')\n );\n }\n }, [allowCreate, search, creating, resolveVocabId, createTag, handleAssign, tr]);\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={tr('projectTagsWidget.addTagAriaLabel', '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-subtle text-text-secondary text-xs',\n 'hover:border-foreground hover:text-text-primary transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]'\n )}\n >\n <TagIcon className=\"size-3\" />\n <span>\n {tags.length > 0\n ? tr('projectTagsWidget.add', 'Add')\n : tr('projectTagsWidget.addTags', 'Add tags')}\n </span>\n </PopoverTrigger>\n\n <PopoverContent\n className=\"z-50 w-72 max-w-[calc(100vw-2rem)] border-border-subtle/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={tr(\n 'projectTagsWidget.searchPlaceholder',\n 'Search or create a tag...'\n )}\n value={search}\n onValueChange={handleSearchChange}\n className=\"h-10 border-0 bg-transparent pr-9 text-sm outline-none placeholder:text-text-secondary\"\n />\n {searching && (\n <Loader2 className=\"absolute right-3 top-1/2 size-3.5 -translate-y-1/2 animate-spin text-text-secondary\" />\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-text-secondary\">\n {tr('projectTagsWidget.typeToSearch', '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\n ? tr('projectTagsWidget.creating', 'Creating...')\n : tr('projectTagsWidget.create', 'Create \"{{label}}\"', {\n label: search,\n })}\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-text-secondary\">\n {tr('projectTagsWidget.noTagsFound', 'No tags found')}\n </CommandEmpty>\n )}\n\n {createError && (\n <div className=\"px-3 py-2 text-xs text-status-error-text border-t border-border-subtle\">\n {createError}\n </div>\n )}\n </CommandList>\n\n {!canAddMore && (\n <div className=\"border-t border-border-subtle px-3 py-2 text-xs text-text-secondary flex items-center gap-1\">\n <X className=\"size-3\" />\n {tr('projectTagsWidget.maxTagsReached', 'Maximum {{maxTags}} tags reached', {\n maxTags,\n })}\n </div>\n )}\n </Command>\n </PopoverContent>\n </Popover>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useCallback as e } from "react";
|
|
2
|
+
import { useI18n as t } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
3
|
+
//#region src/hooks/useTr.ts
|
|
4
|
+
function n() {
|
|
5
|
+
let { t: n } = t();
|
|
6
|
+
return e((e, t, r) => {
|
|
7
|
+
let i = n(e, void 0, r), a = i === e ? t : i;
|
|
8
|
+
return r ? Object.entries(r).reduce((e, [t, n]) => e.replace(RegExp(`\\{\\{${t}\\}\\}`, "g"), String(n)), a) : a;
|
|
9
|
+
}, [n]);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { n as useTr };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=useTr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTr.js","names":[],"sources":["../../src/hooks/useTr.ts"],"sourcesContent":["import { useCallback } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nexport type TFunction = (\n key: string,\n fallbackOrParams?: string | Record<string, string | number>,\n params?: Record<string, string | number>\n) => string;\nexport type TrFunction = (\n key: string,\n fallback: string,\n params?: Record<string, string | number>\n) => string;\n\n/**\n * Builds a translator function with fallback semantics for the given `t`.\n *\n * When `t(key)` returns the key itself (no translation found), the caller's\n * English `fallback` is used instead. Optional interpolation params use\n * `{{key}}` placeholders in both the translated and fallback strings.\n */\nexport function createTr(t: TFunction): TrFunction {\n return (key: string, fallback: string, params?: Record<string, string | number>): string => {\n const translated = t(key, undefined, params);\n const raw = translated === key ? fallback : translated;\n if (!params) return raw;\n return Object.entries(params).reduce(\n (str, [k, v]) => str.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), String(v)),\n raw\n );\n };\n}\n\n/**\n * useTr — translation hook with fallback semantics.\n *\n * Wraps `useI18n().t()` so that when a key has no translation (i.e. the\n * provider returns the key itself), the caller's English fallback is used\n * instead. This keeps consumer call sites consistent and avoids leaking\n * untranslated keys into the UI.\n *\n * Supports optional interpolation params using `{{key}}` placeholders.\n *\n * @example\n * const tr = useTr();\n * return <h1>{tr('workspaces.dashboard.title', 'Dashboard')}</h1>;\n * return <p>{tr('workspaces.delete.confirm', 'Delete {{count}} items?', { count: 5 })}</p>;\n */\nexport function useTr(): TrFunction {\n const { t } = useI18n();\n return useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string => {\n const translated = t(key, undefined, params);\n const raw = translated === key ? fallback : translated;\n if (!params) return raw;\n return Object.entries(params).reduce(\n (str, [k, v]) => str.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), String(v)),\n raw\n );\n },\n [t]\n );\n}\n"],"mappings":";;;AAgDA,SAAgB,IAAoB;CAClC,IAAM,EAAE,SAAM,GAAS;AACvB,QAAO,GACJ,GAAa,GAAkB,MAAqD;EACnF,IAAM,IAAa,EAAE,GAAK,KAAA,GAAW,EAAO,EACtC,IAAM,MAAe,IAAM,IAAW;AAE5C,SADK,IACE,OAAO,QAAQ,EAAO,CAAC,QAC3B,GAAK,CAAC,GAAG,OAAO,EAAI,QAAY,OAAO,SAAS,EAAE,SAAS,IAAI,EAAE,OAAO,EAAE,CAAC,EAC5E,EACD,GAJmB;IAMtB,CAAC,EAAE,CACJ"}
|
package/dist/index.js
CHANGED
|
@@ -24,4 +24,5 @@ import { CreateProjectDialog as he } from "./components/CreateProjectDialog.js";
|
|
|
24
24
|
import { MemberListItem as ge } from "./components/MemberListItem.js";
|
|
25
25
|
import { InviteMemberDialog as _e } from "./components/InviteMemberDialog.js";
|
|
26
26
|
import "./components/index.js";
|
|
27
|
-
|
|
27
|
+
import { enTranslations as ve } from "./translations/en.js";
|
|
28
|
+
export { he as CreateProjectDialog, ie as CreateWorkspaceDialog, y as DashboardPage, L as InviteAcceptancePage, _e as InviteMemberDialog, ge as MemberListItem, R as PendingInvitesPage, me as ProjectCard, _ as WorkspaceCard, U as WorkspaceInvitePage, n as WorkspacesErrorBoundary, e as WorkspacesProvider, v as WorkspacesRoot, ve as enTranslations, ae as formatDate, oe as formatDateTime, se as formatFileSize, ce as formatProjectStatus, le as formatRelativeTime, ue as formatWorkspaceStatus, de as getInitials, c as getMemberRoleClasses, l as getMemberRoleLabel, u as getProjectStatusClasses, d as getProjectStatusIconClass, f as getProjectStatusLabel, p as getWorkspaceStatusBgClass, m as getWorkspaceStatusClasses, h as getWorkspaceStatusIconClass, g as getWorkspaceStatusLabel, fe as toTitleCase, pe as truncate, E as useAcceptInvitation, D as useAddWorkspaceMember, Y as useArchiveProject, W as useArchiveWorkspace, O as useBulkCreateInvitations, k as useCancelInvitation, A as useCreateInvitation, X as useCreateProject, G as useCreateWorkspace, j as useDeleteInvitation, Z as useDeleteProject, K as useDeleteWorkspace, b as useInfiniteWorkspaceMembers, x as useInvitation, ne as useMyOrganizations, s as useMyPendingInvitations, z as useMyProjects, re as useMyTenants, i as useMyWorkspaces, B as useProject, V as useProjectMembersCount, H as useProjects, q as useReactivateWorkspace, M as useRejectInvitation, N as useRemoveUserFromWorkspace, P as useRemoveWorkspaceMember, F as useResendInvitation, Q as useTagProject, $ as useUnarchiveProject, ee as useUntagProject, I as useUpdateInvitation, te as useUpdateProject, J as useUpdateWorkspace, S as useUserWorkspaces, a as useWorkspace, C as useWorkspaceInvitations, w as useWorkspaceMember, T as useWorkspaceMembers, o as useWorkspaces, t as useWorkspacesContext, r as useWorkspacesStore };
|
|
@@ -5,11 +5,11 @@ import { formatDate as r } from "../utils/formatters.js";
|
|
|
5
5
|
import { useWorkspaceActivity as i } from "../hooks/useWorkspaceActivity.js";
|
|
6
6
|
import { jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
7
7
|
import { CardContent as s, CardHeader as c, CardTitle as l, GlassCard as u, IllustratedEmptyState as d, PagePurpose as f, Skeleton as p } from "@burdenoff/fe-libs/ui";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { useI18n as m } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
9
|
+
import { useParams as h } from "react-router";
|
|
10
10
|
//#region src/pages/ActivityListPage.tsx
|
|
11
11
|
function g() {
|
|
12
|
-
let { t: g } =
|
|
12
|
+
let { t: g } = m(), { workspaceId: _ } = h();
|
|
13
13
|
t("workspaces.activity", {
|
|
14
14
|
workspaceId: _,
|
|
15
15
|
requireParams: !0
|
|
@@ -9,20 +9,20 @@ import { useEffect as o, useMemo as s } from "react";
|
|
|
9
9
|
import { AlertCircle as c, ArrowLeft as l } from "lucide-react";
|
|
10
10
|
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
11
11
|
import { Button as f, CardContent as p, CardDescription as m, CardHeader as h, CardTitle as ne, GlassCard as g, PagePurpose as _, Skeleton as v } from "@burdenoff/fe-libs/ui";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
12
|
+
import { useI18n as y } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
13
|
+
import { Link as b, useNavigate as x } from "react-router";
|
|
14
|
+
import { useUpgradePrompt as S } from "@burdenoff/fe-libs/shared/hooks";
|
|
15
|
+
import { AccessDenied as C, UpgradeInlineBanner as w } from "@burdenoff/fe-libs/shared/components";
|
|
16
16
|
import { useForm as T, z as E } from "@burdenoff/fe-libs/form";
|
|
17
17
|
//#region src/pages/CreateWorkspacePage.tsx
|
|
18
18
|
function D() {
|
|
19
19
|
n("workspaces.create");
|
|
20
|
-
let { t: D } =
|
|
20
|
+
let { t: D } = y(), { isFree: O, approachingLimits: re, plan: ie } = S({ resource: "workspaces" }), k = re[0], A = !!k && typeof k.limit == "number" && k.current >= k.limit, j = O && !!k, M = s(() => E.object({
|
|
21
21
|
name: E.string().min(2, t(D, "createWorkspace.validation.nameMin", "Name must be at least 2 characters")).max(100),
|
|
22
22
|
slug: E.string().min(2, t(D, "createWorkspace.validation.slugMin", "Slug must be at least 2 characters")).max(50).regex(/^[a-z0-9-]+$/, t(D, "createWorkspace.validation.slugFormat", "Slug can only contain lowercase letters, numbers, and hyphens")),
|
|
23
23
|
tenantId: E.string().min(1, t(D, "createWorkspace.validation.tenantRequired", "Please select a tenant")),
|
|
24
24
|
organizationId: E.string().min(1, t(D, "createWorkspace.validation.organizationRequired", "Please select an organization"))
|
|
25
|
-
}), [D]), N =
|
|
25
|
+
}), [D]), N = x(), { basePath: P = "/" } = e(), { mutate: F, isPending: I } = ee(), { data: L, isLoading: R, error: z } = i(), { data: B, isLoading: V, error: H } = r(), U = s(() => L?.items ?? [], [L]), W = s(() => B?.items ?? [], [B]), G = s(() => U.find((e) => e.type === "SHARED" || e.slug === "burdenoff-shared")?.id ?? U[0]?.id ?? "", [U]), K = T({
|
|
26
26
|
schema: M,
|
|
27
27
|
defaultValues: {
|
|
28
28
|
name: "",
|
|
@@ -63,11 +63,11 @@ function D() {
|
|
|
63
63
|
}, oe = Y.size > 0, se = R || V || X, ce = !z && !H && W.length > 0;
|
|
64
64
|
return !se && !Z && ce && !oe ? /* @__PURE__ */ d("div", {
|
|
65
65
|
className: "p-6",
|
|
66
|
-
children: [/* @__PURE__ */ d(
|
|
66
|
+
children: [/* @__PURE__ */ d(b, {
|
|
67
67
|
to: `${P}/list`,
|
|
68
68
|
className: "inline-flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors mb-6",
|
|
69
69
|
children: [/* @__PURE__ */ u(l, { size: 20 }), t(D, "createWorkspace.backToWorkspaces", "Back to Workspaces")]
|
|
70
|
-
}), /* @__PURE__ */ u(
|
|
70
|
+
}), /* @__PURE__ */ u(C, {
|
|
71
71
|
variant: "card",
|
|
72
72
|
title: t(D, "createWorkspace.permissionRequiredTitle", "Permission Required"),
|
|
73
73
|
description: t(D, "createWorkspace.permissionRequiredDescription", "You don't have permission to create workspaces in any of your organizations."),
|
|
@@ -80,7 +80,7 @@ function D() {
|
|
|
80
80
|
}) : /* @__PURE__ */ d("div", {
|
|
81
81
|
className: "space-y-4 sm:space-y-6 p-3 sm:p-6",
|
|
82
82
|
children: [
|
|
83
|
-
/* @__PURE__ */ d(
|
|
83
|
+
/* @__PURE__ */ d(b, {
|
|
84
84
|
to: `${P}/list`,
|
|
85
85
|
className: "inline-flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors",
|
|
86
86
|
children: [/* @__PURE__ */ u(l, { size: 20 }), t(D, "createWorkspace.backToWorkspaces", "Back to Workspaces")]
|
|
@@ -91,7 +91,7 @@ function D() {
|
|
|
91
91
|
}),
|
|
92
92
|
j && /* @__PURE__ */ u("div", {
|
|
93
93
|
className: "max-w-2xl mx-auto w-full",
|
|
94
|
-
children: /* @__PURE__ */ u(
|
|
94
|
+
children: /* @__PURE__ */ u(w, {
|
|
95
95
|
resource: "workspaces",
|
|
96
96
|
current: k.current,
|
|
97
97
|
limit: k.limit ?? void 0,
|