@burdenoff/microfe-vibecontrols 2026.524.7 → 2026.524.8

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,273 +1,292 @@
1
1
  import { useTr as e } from "../../shared/hooks/useTr.js";
2
- import { useAvailableTags as t, useCreateVibecontrolsTag as n, useGetEntityTags as r, useSearchTags as ee, useTagEntity as i, useUntagEntity as te } from "../../hooks/useEntityTags.js";
3
- import { useCallback as a, useEffect as o, useMemo as s, useRef as ne, useState as c } from "react";
4
- import { Loader2 as l, Plus as u, Search as d, Tag as re, X as f } from "lucide-react";
5
- import { useApolloClient as ie } from "@apollo/client/react";
6
- import { jsx as p, jsxs as m } from "react/jsx-runtime";
2
+ import { useAvailableTags as t, useCreateVibecontrolsTag as n, useGetEntityTags as ee, useSearchTags as te, useTagEntity as ne, useUntagEntity as re } from "../../hooks/useEntityTags.js";
3
+ import { useCallback as r, useEffect as i, useMemo as a, useRef as o, useState as s } from "react";
4
+ import { Loader2 as c, Plus as l, Search as ie, Tag as ae, X as u } from "lucide-react";
5
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
7
6
  //#region src/components/shared/EntityTagPicker.tsx
8
- var ae = 4e3, oe = 250;
9
- function h({ entityType: h, entityId: g, onDraftChange: _, readOnly: v = !1 }) {
10
- let y = e(), b = ie(), x = !g, [S, C] = c(!1), [w, T] = c(""), [se, E] = c(""), [D, O] = c(null), [k, A] = c([]);
11
- o(() => {
12
- x && _?.(k.map((e) => e.id));
13
- }, [x, k]);
14
- let { data: j, loading: ce, refetch: M } = r(h, g ?? ""), { data: N, loading: P } = t(200, { skip: x && !S }), [F, { data: I, loading: L }] = ee(), [R, { loading: z }] = i(), [B, { loading: V }] = te(), [H, { loading: U }] = n(), W = s(() => j?.vibecontrolsEntityTags ?? [], [j]), G = s(() => {
15
- let e = N?.availableVibecontrolsTags ?? [];
7
+ var oe = 4e3, p = 250;
8
+ function m({ entityType: m, entityId: h, onDraftChange: se, readOnly: g = !1 }) {
9
+ let _ = e(), v = !h, [y, b] = s(!1), [x, S] = s(""), [ce, C] = s(""), [w, T] = s(null), [E, D] = s([]), [O, k] = s([]), A = o(0);
10
+ i(() => {
11
+ v && se?.(E.map((e) => e.id));
12
+ }, [v, E]);
13
+ let { data: j, loading: M, refetch: N } = ee(m, h ?? ""), { data: P, loading: F } = t(200, { skip: v && !y }), [I, { data: L, loading: R }] = te(), [z, { loading: B }] = ne(), [V, { loading: le }] = re(), [H, { loading: U }] = n();
14
+ i(() => {
15
+ v || j?.vibecontrolsEntityTags === void 0 || A.current > 0 || k(j.vibecontrolsEntityTags);
16
+ }, [v, j]);
17
+ let W = a(() => {
18
+ let e = P?.availableVibecontrolsTags ?? [];
16
19
  return new Map(e.map((e) => [e.id, e]));
17
- }, [N]), K = s(() => x ? k.map((e) => ({
20
+ }, [P]), G = a(() => v ? E.map((e) => ({
18
21
  key: e.id,
19
22
  tagId: e.id,
20
23
  label: e.label
21
- })) : W.map((e) => ({
24
+ })) : O.map((e) => ({
22
25
  key: e.id,
23
26
  tagId: e.tagId,
24
- label: e.tag?.label ?? G.get(e.tagId)?.label ?? y("vibecontrols.shared.entityTagPicker.untitledTag", "Untitled tag")
27
+ label: e.tagInfo?.label ?? W.get(e.tagId)?.label ?? _("vibecontrols.shared.entityTagPicker.untitledTag", "Untitled tag")
25
28
  })), [
26
- x,
27
- k,
29
+ v,
30
+ E,
31
+ O,
28
32
  W,
29
- G,
30
- y
31
- ]), q = s(() => new Set(K.map((e) => e.tagId)), [K]), J = s(() => (w ? I?.searchVibecontrolsTags : N?.availableVibecontrolsTags) ?? [], [
32
- w,
33
- I,
34
- N
35
- ]), Y = s(() => J.filter((e) => !q.has(e.id)), [J, q]);
36
- o(() => {
37
- if (!D) return;
38
- let e = setTimeout(() => O(null), ae);
33
+ _
34
+ ]), K = a(() => new Set(G.map((e) => e.tagId)), [G]), q = a(() => (x ? L?.searchVibecontrolsTags : P?.availableVibecontrolsTags) ?? [], [
35
+ x,
36
+ L,
37
+ P
38
+ ]), J = a(() => q.filter((e) => !K.has(e.id)), [q, K]);
39
+ i(() => {
40
+ if (!w) return;
41
+ let e = setTimeout(() => T(null), oe);
39
42
  return () => clearTimeout(e);
40
- }, [D]);
41
- let X = ne(null);
42
- o(() => () => {
43
- X.current && clearTimeout(X.current);
43
+ }, [w]);
44
+ let Y = o(null);
45
+ i(() => () => {
46
+ Y.current && clearTimeout(Y.current);
44
47
  }, []);
45
- let le = a((e) => {
46
- T(e), X.current && clearTimeout(X.current);
48
+ let ue = r((e) => {
49
+ S(e), Y.current && clearTimeout(Y.current);
47
50
  let t = e.trim();
48
51
  if (t.length < 2) {
49
- E("");
52
+ C("");
50
53
  return;
51
54
  }
52
- X.current = setTimeout(() => {
53
- E(t), F({ variables: {
55
+ Y.current = setTimeout(() => {
56
+ C(t), I({ variables: {
54
57
  query: t,
55
58
  limit: 10
56
59
  } });
57
- }, oe);
58
- }, [F]), Z = a(async (e) => {
59
- if (x) {
60
- A((t) => t.some((t) => t.id === e.id) ? t : [...t, e]), T(""), O(null);
60
+ }, p);
61
+ }, [I]), X = r(async (e) => {
62
+ if (v) {
63
+ D((t) => t.some((t) => t.id === e.id) ? t : [...t, e]), S(""), T(null);
61
64
  return;
62
65
  }
66
+ let t = `optimistic-${e.id}`, n = {
67
+ id: t,
68
+ tagId: e.id,
69
+ tagInfo: {
70
+ id: e.id,
71
+ key: e.key,
72
+ label: e.label,
73
+ color: e.color
74
+ },
75
+ entityType: m,
76
+ entityId: h ?? "",
77
+ source: "manual",
78
+ assignedBy: null,
79
+ assignedAt: null
80
+ };
81
+ k((t) => t.some((t) => t.tagId === e.id) ? t : [...t, n]), S(""), T(null), A.current++;
63
82
  try {
64
- await R({ variables: { input: {
65
- entityType: h,
66
- entityId: g ?? "",
83
+ let n = (await z({ variables: { input: {
84
+ entityType: m,
85
+ entityId: h ?? "",
67
86
  tagId: e.id
68
- } } }), b.cache.evict({
69
- id: "ROOT_QUERY",
70
- fieldName: "vibecontrolsEntityTags"
71
- }), b.cache.gc(), await M(), T(""), O(null);
87
+ } } })).data?.tagVibecontrolsEntity;
88
+ n && k((e) => e.map((e) => e.id === t ? n : e));
72
89
  } catch (e) {
73
- O(e instanceof Error ? e.message : y("vibecontrols.shared.entityTagPicker.failedToAddTag", "Failed to add tag"));
90
+ k((e) => e.filter((e) => e.id !== t)), T(e instanceof Error ? e.message : _("vibecontrols.shared.entityTagPicker.failedToAddTag", "Failed to add tag"));
91
+ } finally {
92
+ A.current--, N();
74
93
  }
75
94
  }, [
76
- x,
77
- R,
95
+ v,
96
+ z,
97
+ m,
78
98
  h,
79
- g,
80
- M,
81
- y
82
- ]), ue = a(async (e) => {
83
- if (x) {
84
- A((t) => t.filter((t) => t.id !== e)), O(null);
99
+ N,
100
+ _
101
+ ]), Z = r(async (e) => {
102
+ if (v) {
103
+ D((t) => t.filter((t) => t.id !== e)), T(null);
85
104
  return;
86
105
  }
106
+ k((t) => t.filter((t) => t.tagId !== e)), T(null), A.current++;
87
107
  try {
88
- await B({ variables: { input: {
89
- entityType: h,
90
- entityId: g ?? "",
108
+ await V({ variables: { input: {
109
+ entityType: m,
110
+ entityId: h ?? "",
91
111
  tagId: e
92
- } } }), b.cache.evict({
93
- id: "ROOT_QUERY",
94
- fieldName: "vibecontrolsEntityTags"
95
- }), b.cache.gc(), await M(), O(null);
112
+ } } });
96
113
  } catch (e) {
97
- O(e instanceof Error ? e.message : y("vibecontrols.shared.entityTagPicker.failedToRemoveTag", "Failed to remove tag"));
114
+ T(e instanceof Error ? e.message : _("vibecontrols.shared.entityTagPicker.failedToRemoveTag", "Failed to remove tag"));
115
+ } finally {
116
+ A.current--, N();
98
117
  }
99
118
  }, [
100
- x,
101
- B,
119
+ v,
120
+ V,
121
+ m,
102
122
  h,
103
- g,
104
- M,
105
- y
106
- ]), de = a(async (e) => {
123
+ N,
124
+ _
125
+ ]), de = r(async (e) => {
107
126
  let t = e.trim();
108
127
  if (t) try {
109
128
  let e = (await H({
110
129
  variables: { input: { label: t } },
111
130
  refetchQueries: ["EntityTagPickerAvailableTags"]
112
131
  })).data?.createVibecontrolsTag;
113
- if (!e) throw Error(y("vibecontrols.shared.entityTagPicker.failedToCreateTag", "Failed to create tag"));
114
- await Z({
132
+ if (!e) throw Error(_("vibecontrols.shared.entityTagPicker.failedToCreateTag", "Failed to create tag"));
133
+ await X({
115
134
  id: e.id,
116
135
  key: e.key,
117
136
  label: e.label,
118
137
  color: e.color ?? null
119
138
  });
120
139
  } catch (e) {
121
- O(e instanceof Error ? e.message : y("vibecontrols.shared.entityTagPicker.failedToCreateTag", "Failed to create tag"));
140
+ T(e instanceof Error ? e.message : _("vibecontrols.shared.entityTagPicker.failedToCreateTag", "Failed to create tag"));
122
141
  }
123
142
  }, [
124
143
  H,
125
- Z,
126
- y
127
- ]), Q = w.trim(), fe = s(() => {
144
+ X,
145
+ _
146
+ ]), Q = x.trim(), fe = a(() => {
128
147
  let e = Q.toLowerCase();
129
- return e ? J.some((t) => t.label.toLowerCase() === e) || K.some((t) => t.label.toLowerCase() === e) : !1;
148
+ return e ? q.some((t) => t.label.toLowerCase() === e) || G.some((t) => t.label.toLowerCase() === e) : !1;
130
149
  }, [
131
- J,
132
- K,
150
+ q,
151
+ G,
133
152
  Q
134
- ]), pe = se === Q && !L, $ = !v && Q.length >= 2 && pe && !fe;
135
- return ce && !x ? /* @__PURE__ */ m("div", {
153
+ ]), pe = ce === Q && !R, $ = !g && Q.length >= 2 && pe && !fe;
154
+ return M && !v ? /* @__PURE__ */ f("div", {
136
155
  className: "flex items-center gap-2 text-text-secondary text-sm",
137
156
  role: "status",
138
157
  "aria-live": "polite",
139
- children: [/* @__PURE__ */ p(l, {
158
+ children: [/* @__PURE__ */ d(c, {
140
159
  className: "h-4 w-4 animate-spin",
141
160
  "aria-hidden": "true"
142
- }), y("vibecontrols.shared.entityTagPicker.loadingTags", "Loading tags...")]
143
- }) : /* @__PURE__ */ m("div", {
161
+ }), _("vibecontrols.shared.entityTagPicker.loadingTags", "Loading tags...")]
162
+ }) : /* @__PURE__ */ f("div", {
144
163
  className: "space-y-3",
145
164
  children: [
146
- D && /* @__PURE__ */ m("div", {
165
+ w && /* @__PURE__ */ f("div", {
147
166
  role: "alert",
148
167
  className: "flex items-start gap-2 rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700",
149
- children: [/* @__PURE__ */ p("span", {
168
+ children: [/* @__PURE__ */ d("span", {
150
169
  className: "flex-1",
151
- children: D
152
- }), /* @__PURE__ */ p("button", {
170
+ children: w
171
+ }), /* @__PURE__ */ d("button", {
153
172
  type: "button",
154
- onClick: () => O(null),
155
- "aria-label": y("vibecontrols.shared.entityTagPicker.dismissError", "Dismiss error"),
173
+ onClick: () => T(null),
174
+ "aria-label": _("vibecontrols.shared.entityTagPicker.dismissError", "Dismiss error"),
156
175
  className: "text-red-700 hover:text-red-900",
157
- children: /* @__PURE__ */ p(f, {
176
+ children: /* @__PURE__ */ d(u, {
158
177
  className: "h-3.5 w-3.5",
159
178
  "aria-hidden": "true"
160
179
  })
161
180
  })]
162
181
  }),
163
- /* @__PURE__ */ m("div", {
182
+ /* @__PURE__ */ f("div", {
164
183
  className: "flex flex-wrap gap-2",
165
- "aria-label": y("vibecontrols.shared.entityTagPicker.assignedTags", "Assigned tags"),
184
+ "aria-label": _("vibecontrols.shared.entityTagPicker.assignedTags", "Assigned tags"),
166
185
  children: [
167
- K.length === 0 && !S && /* @__PURE__ */ p("p", {
186
+ G.length === 0 && !y && /* @__PURE__ */ d("p", {
168
187
  className: "text-text-secondary text-sm",
169
- children: y("vibecontrols.shared.entityTagPicker.noTagsAssigned", "No tags assigned")
188
+ children: _("vibecontrols.shared.entityTagPicker.noTagsAssigned", "No tags assigned")
170
189
  }),
171
- K.map((e) => /* @__PURE__ */ m("span", {
190
+ G.map((e) => /* @__PURE__ */ f("span", {
172
191
  className: "inline-flex items-center gap-1 px-3 py-1 bg-action-primary-bg/10 text-action-primary-bg text-sm rounded-full",
173
192
  children: [
174
- /* @__PURE__ */ p(re, {
193
+ /* @__PURE__ */ d(ae, {
175
194
  className: "h-3 w-3",
176
195
  "aria-hidden": "true"
177
196
  }),
178
197
  e.label,
179
- !v && /* @__PURE__ */ p("button", {
198
+ !g && /* @__PURE__ */ d("button", {
180
199
  type: "button",
181
- onClick: () => ue(e.tagId),
182
- disabled: V,
200
+ onClick: () => Z(e.tagId),
201
+ disabled: le,
183
202
  "aria-label": `Remove tag ${e.label}`,
184
203
  className: "ml-1 hover:text-red-500 transition-colors disabled:opacity-50",
185
- children: /* @__PURE__ */ p(f, {
204
+ children: /* @__PURE__ */ d(u, {
186
205
  className: "h-3 w-3",
187
206
  "aria-hidden": "true"
188
207
  })
189
208
  })
190
209
  ]
191
210
  }, e.key)),
192
- !v && !S && /* @__PURE__ */ m("button", {
211
+ !g && !y && /* @__PURE__ */ f("button", {
193
212
  type: "button",
194
- onClick: () => C(!0),
195
- "aria-label": y("vibecontrols.shared.entityTagPicker.addTag", "Add tag"),
213
+ onClick: () => b(!0),
214
+ "aria-label": _("vibecontrols.shared.entityTagPicker.addTag", "Add tag"),
196
215
  className: "inline-flex items-center gap-1 px-3 py-1 border border-dashed border-border-default text-text-secondary text-sm rounded-full hover:border-action-primary-bg hover:text-action-primary-bg transition-colors",
197
- children: [/* @__PURE__ */ p(u, {
216
+ children: [/* @__PURE__ */ d(l, {
198
217
  className: "h-3 w-3",
199
218
  "aria-hidden": "true"
200
- }), y("vibecontrols.shared.entityTagPicker.addTag", "Add Tag")]
219
+ }), _("vibecontrols.shared.entityTagPicker.addTag", "Add Tag")]
201
220
  })
202
221
  ]
203
222
  }),
204
- S && /* @__PURE__ */ m("div", {
223
+ y && /* @__PURE__ */ f("div", {
205
224
  className: "border border-border-default rounded-lg p-3 space-y-2 bg-bg-surface",
206
225
  children: [
207
- /* @__PURE__ */ m("div", {
226
+ /* @__PURE__ */ f("div", {
208
227
  className: "relative",
209
- children: [/* @__PURE__ */ p(d, {
228
+ children: [/* @__PURE__ */ d(ie, {
210
229
  className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary",
211
230
  "aria-hidden": "true"
212
- }), /* @__PURE__ */ p("input", {
231
+ }), /* @__PURE__ */ d("input", {
213
232
  type: "text",
214
- value: w,
215
- onChange: (e) => le(e.target.value),
216
- placeholder: y("vibecontrols.shared.entityTagPicker.searchPlaceholder", "Search tags..."),
233
+ value: x,
234
+ onChange: (e) => ue(e.target.value),
235
+ placeholder: _("vibecontrols.shared.entityTagPicker.searchPlaceholder", "Search tags..."),
217
236
  autoFocus: !0,
218
- "aria-label": y("vibecontrols.shared.entityTagPicker.searchAvailableTags", "Search available tags"),
237
+ "aria-label": _("vibecontrols.shared.entityTagPicker.searchAvailableTags", "Search available tags"),
219
238
  className: "w-full pl-9 pr-3 py-2 text-sm bg-bg-default border border-border-default rounded-md focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-text-primary placeholder:text-text-secondary"
220
239
  })]
221
240
  }),
222
- /* @__PURE__ */ m("div", {
241
+ /* @__PURE__ */ f("div", {
223
242
  className: "flex flex-wrap gap-2 max-h-32 overflow-y-auto",
224
- "aria-label": y("vibecontrols.shared.entityTagPicker.tagSuggestions", "Tag suggestions"),
243
+ "aria-label": _("vibecontrols.shared.entityTagPicker.tagSuggestions", "Tag suggestions"),
225
244
  children: [
226
- (L || P && !w) && /* @__PURE__ */ p("span", {
245
+ (R || F && !x) && /* @__PURE__ */ d("span", {
227
246
  className: "text-text-secondary text-xs",
228
- children: y("vibecontrols.shared.entityTagPicker.loading", "Loading...")
247
+ children: _("vibecontrols.shared.entityTagPicker.loading", "Loading...")
229
248
  }),
230
- !L && !P && Y.length === 0 && !$ && /* @__PURE__ */ p("span", {
249
+ !R && !F && J.length === 0 && !$ && /* @__PURE__ */ d("span", {
231
250
  className: "text-text-secondary text-xs",
232
- children: Q ? y("vibecontrols.shared.entityTagPicker.noMatchingTags", "No matching tags") : y("vibecontrols.shared.entityTagPicker.noAvailableTags", "No tags yet — type a name to create one")
251
+ children: Q ? _("vibecontrols.shared.entityTagPicker.noMatchingTags", "No matching tags") : _("vibecontrols.shared.entityTagPicker.noAvailableTags", "No tags yet — type a name to create one")
233
252
  }),
234
- Y.map((e) => /* @__PURE__ */ m("button", {
253
+ J.map((e) => /* @__PURE__ */ f("button", {
235
254
  type: "button",
236
- onClick: () => Z(e),
237
- disabled: z,
255
+ onClick: () => X(e),
256
+ disabled: B,
238
257
  "aria-label": `Add tag ${e.label}`,
239
258
  className: "inline-flex items-center gap-1 px-3 py-1 bg-bg-sunken text-text-primary text-sm rounded-full hover:bg-action-primary-bg/10 hover:text-action-primary-bg transition-colors cursor-pointer disabled:opacity-50",
240
- children: [/* @__PURE__ */ p(u, {
259
+ children: [/* @__PURE__ */ d(l, {
241
260
  className: "h-3 w-3",
242
261
  "aria-hidden": "true"
243
262
  }), e.label]
244
263
  }, e.id))
245
264
  ]
246
265
  }),
247
- $ && /* @__PURE__ */ m("button", {
266
+ $ && /* @__PURE__ */ f("button", {
248
267
  type: "button",
249
268
  onClick: () => de(Q),
250
- disabled: U || z,
251
- "aria-label": y("vibecontrols.shared.entityTagPicker.createTagAria", "Create tag {{label}}", { label: Q }),
269
+ disabled: U || B,
270
+ "aria-label": _("vibecontrols.shared.entityTagPicker.createTagAria", "Create tag {{label}}", { label: Q }),
252
271
  className: "inline-flex w-full items-center gap-1.5 px-3 py-2 border border-dashed border-action-primary-bg/50 text-action-primary-bg text-sm rounded-md hover:bg-action-primary-bg/10 transition-colors cursor-pointer disabled:opacity-50",
253
- children: [U || z ? /* @__PURE__ */ p(l, {
272
+ children: [U || B ? /* @__PURE__ */ d(c, {
254
273
  className: "h-3.5 w-3.5 animate-spin",
255
274
  "aria-hidden": "true"
256
- }) : /* @__PURE__ */ p(u, {
275
+ }) : /* @__PURE__ */ d(l, {
257
276
  className: "h-3.5 w-3.5",
258
277
  "aria-hidden": "true"
259
- }), /* @__PURE__ */ p("span", {
278
+ }), /* @__PURE__ */ d("span", {
260
279
  className: "truncate",
261
- children: y("vibecontrols.shared.entityTagPicker.createTag", "Create \"{{label}}\"", { label: Q })
280
+ children: _("vibecontrols.shared.entityTagPicker.createTag", "Create \"{{label}}\"", { label: Q })
262
281
  })]
263
282
  }),
264
- /* @__PURE__ */ p("button", {
283
+ /* @__PURE__ */ d("button", {
265
284
  type: "button",
266
285
  onClick: () => {
267
- C(!1), T("");
286
+ b(!1), S("");
268
287
  },
269
288
  className: "text-xs text-text-secondary hover:text-text-primary",
270
- children: y("vibecontrols.shared.entityTagPicker.done", "Done")
289
+ children: _("vibecontrols.shared.entityTagPicker.done", "Done")
271
290
  })
272
291
  ]
273
292
  })
@@ -275,6 +294,6 @@ function h({ entityType: h, entityId: g, onDraftChange: _, readOnly: v = !1 }) {
275
294
  });
276
295
  }
277
296
  //#endregion
278
- export { h as EntityTagPicker };
297
+ export { m as EntityTagPicker };
279
298
 
280
299
  //# sourceMappingURL=EntityTagPicker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"EntityTagPicker.js","names":[],"sources":["../../../src/components/shared/EntityTagPicker.tsx"],"sourcesContent":["/**\n * EntityTagPicker — Reusable tag picker for any VibeControls entity\n *\n * Two modes:\n * - Persisted (entityId provided): self-fetches assignments and writes\n * immediately via tag/untag mutations. Used on detail/edit surfaces.\n * - Draft (entityId omitted/empty): no queries or mutations — selections\n * are held locally and surfaced via `onDraftChange(tagIds)`. The parent\n * create form persists them after the entity is created (e.g. via\n * replaceVibecontrolsEntityTags with the new id). Used in create\n * forms/wizards where no entity id exists yet.\n *\n * Usage:\n * <EntityTagPicker entityType=\"VIBECONTROLS_VIBE\" entityId={vibe.id} />\n * <EntityTagPicker entityType=\"VIBECONTROLS_VIBE\" onDraftChange={setTagIds} />\n */\nimport { useState, useCallback, useMemo, useEffect, useRef } from 'react';\nimport { Tag, Plus, X, Search, Loader2 } from 'lucide-react';\nimport { useApolloClient } from '@apollo/client/react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n useGetEntityTags,\n useAvailableTags,\n useSearchTags,\n useTagEntity,\n useUntagEntity,\n useCreateVibecontrolsTag,\n} from '../../hooks/useEntityTags';\nimport type { AvailableTag, EntityTagAssignment } from '../../hooks/useEntityTags';\n\ninterface EntityTagPickerProps {\n entityType: string;\n /**\n * Existing entity id. Omit (or pass empty) to run in draft mode for\n * create forms — selections are emitted via `onDraftChange` instead of\n * being persisted immediately.\n */\n entityId?: string;\n /**\n * Draft-mode callback. Receives the current selected tag ids whenever\n * the draft selection changes. Only invoked when `entityId` is absent.\n */\n onDraftChange?: (tagIds: string[]) => void;\n readOnly?: boolean;\n}\n\nconst ERROR_DISMISS_MS = 4000;\nconst SEARCH_DEBOUNCE_MS = 250;\n\nexport function EntityTagPicker({\n entityType,\n entityId,\n onDraftChange,\n readOnly = false,\n}: EntityTagPickerProps) {\n const tr = useTr();\n const apolloClient = useApolloClient();\n const draftMode = !entityId;\n const [isAdding, setIsAdding] = useState(false);\n const [searchQuery, setSearchQuery] = useState('');\n // The trimmed query the most recent debounced search actually ran for.\n // Used to gate inline creation: we only offer \"Create\" once we have search\n // results that correspond to the current query (avoids offering Create\n // against stale/empty results while the debounced search is still pending).\n const [searchedQuery, setSearchedQuery] = useState('');\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n // Draft-mode local selection (no persistence until the parent flushes it).\n const [draftTags, setDraftTags] = useState<AvailableTag[]>([]);\n\n // In draft mode, propagate the local selection to the parent whenever it\n // changes — and reset it to [] on mount, so a fresh create form never\n // inherits tag ids left over from a previous create on the same page-level\n // hook. Emitting from an effect (rather than inside a setDraftTags updater)\n // keeps the updater pure — no \"setState while rendering\" warning — and lets\n // the handlers use functional updates, which avoids lost-update races when\n // async create/add flows overlap.\n useEffect(() => {\n if (draftMode) onDraftChange?.(draftTags.map((t) => t.id));\n // onDraftChange is a stable setter from the parent's page-level hook;\n // excluded to avoid re-emitting on unrelated parent re-renders.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [draftMode, draftTags]);\n\n const {\n data: tagsData,\n loading: tagsLoading,\n refetch: refetchTags,\n } = useGetEntityTags(entityType, entityId ?? '');\n // Fetch all available tags in persisted mode so we can resolve labels for\n // assigned tags when the federation doesn't return the nested tag object.\n // Also fetched when the add panel is open (suggestions list).\n const { data: availableData, loading: availableLoading } = useAvailableTags(200, {\n skip: draftMode && !isAdding,\n });\n const [searchTags, { data: searchData, loading: searchLoading }] = useSearchTags();\n const [tagEntity, { loading: tagging }] = useTagEntity();\n const [untagEntity, { loading: untagging }] = useUntagEntity();\n const [createTag, { loading: creatingTag }] = useCreateVibecontrolsTag();\n\n // Apollo Client v4 returns DeepPartialObject from useQuery data, but our\n // hook types declare full shapes — we cast back to the declared types since\n // the resolver is guaranteed to return non-null fields per the schema.\n const assignedTags = useMemo(\n () => (tagsData?.vibecontrolsEntityTags ?? []) as EntityTagAssignment[],\n [tagsData]\n );\n\n // Label lookup by tagId — fallback when federation doesn't return nested tag\n const availableTagMap = useMemo(() => {\n const tags = (availableData?.availableVibecontrolsTags as AvailableTag[] | undefined) ?? [];\n return new Map<string, AvailableTag>(tags.map((t) => [t.id, t]));\n }, [availableData]);\n\n // Unified chip list — draft selections in draft mode, persisted\n // assignments otherwise. `tagId` drives removal; `key` is render-stable.\n const chipItems = useMemo(\n () =>\n draftMode\n ? draftTags.map((t) => ({ key: t.id, tagId: t.id, label: t.label }))\n : assignedTags.map((a) => ({\n key: a.id,\n tagId: a.tagId,\n label:\n a.tag?.label ??\n availableTagMap.get(a.tagId)?.label ??\n tr('vibecontrols.shared.entityTagPicker.untitledTag', 'Untitled tag'),\n })),\n [draftMode, draftTags, assignedTags, availableTagMap, tr]\n );\n\n const assignedTagIds = useMemo(() => new Set(chipItems.map((c) => c.tagId)), [chipItems]);\n\n // Unfiltered tag options from the active source (search results when\n // searching, otherwise the available-tags list). Kept separate from\n // `suggestions` so the create affordance can detect an exact match against\n // ALL existing tags, including ones already assigned to this entity.\n const rawTagOptions: AvailableTag[] = useMemo(() => {\n const source = searchQuery\n ? (searchData?.searchVibecontrolsTags as AvailableTag[] | undefined)\n : (availableData?.availableVibecontrolsTags as AvailableTag[] | undefined);\n return source ?? [];\n }, [searchQuery, searchData, availableData]);\n\n const suggestions: AvailableTag[] = useMemo(\n () => rawTagOptions.filter((t) => !assignedTagIds.has(t.id)),\n [rawTagOptions, assignedTagIds]\n );\n\n // Auto-dismiss error toast after a few seconds\n useEffect(() => {\n if (!errorMessage) return;\n const timer = setTimeout(() => setErrorMessage(null), ERROR_DISMISS_MS);\n return () => clearTimeout(timer);\n }, [errorMessage]);\n\n // Debounce the search query so we don't fire a request on every keystroke.\n const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n useEffect(() => {\n return () => {\n if (searchTimerRef.current) clearTimeout(searchTimerRef.current);\n };\n }, []);\n\n const handleSearch = useCallback(\n (query: string) => {\n setSearchQuery(query);\n if (searchTimerRef.current) clearTimeout(searchTimerRef.current);\n const trimmed = query.trim();\n // No search runs for short queries, so there are no authoritative results\n // for this input — clear the marker to keep the Create button gated.\n if (trimmed.length < 2) {\n setSearchedQuery('');\n return;\n }\n searchTimerRef.current = setTimeout(() => {\n setSearchedQuery(trimmed);\n searchTags({ variables: { query: trimmed, limit: 10 } });\n }, SEARCH_DEBOUNCE_MS);\n },\n [searchTags]\n );\n\n const handleAddTag = useCallback(\n async (tag: AvailableTag) => {\n if (draftMode) {\n // Functional update so overlapping async flows (e.g. an in-flight\n // create plus a click on another suggestion) can't overwrite each\n // other from a stale snapshot. The draftTags-watching effect emits\n // onDraftChange for us.\n setDraftTags((prev) => (prev.some((t) => t.id === tag.id) ? prev : [...prev, tag]));\n setSearchQuery('');\n setErrorMessage(null);\n return;\n }\n try {\n await tagEntity({\n variables: { input: { entityType, entityId: entityId ?? '', tagId: tag.id } },\n });\n apolloClient.cache.evict({ id: 'ROOT_QUERY', fieldName: 'vibecontrolsEntityTags' });\n apolloClient.cache.gc();\n await refetchTags();\n setSearchQuery('');\n setErrorMessage(null);\n } catch (err) {\n setErrorMessage(\n err instanceof Error\n ? err.message\n : tr('vibecontrols.shared.entityTagPicker.failedToAddTag', 'Failed to add tag')\n );\n }\n },\n [draftMode, tagEntity, entityType, entityId, refetchTags, tr]\n );\n\n const handleRemoveTag = useCallback(\n async (tagId: string) => {\n if (draftMode) {\n setDraftTags((prev) => prev.filter((t) => t.id !== tagId));\n setErrorMessage(null);\n return;\n }\n try {\n await untagEntity({\n variables: { input: { entityType, entityId: entityId ?? '', tagId } },\n });\n apolloClient.cache.evict({ id: 'ROOT_QUERY', fieldName: 'vibecontrolsEntityTags' });\n apolloClient.cache.gc();\n await refetchTags();\n setErrorMessage(null);\n } catch (err) {\n setErrorMessage(\n err instanceof Error\n ? err.message\n : tr('vibecontrols.shared.entityTagPicker.failedToRemoveTag', 'Failed to remove tag')\n );\n }\n },\n [draftMode, untagEntity, entityType, entityId, refetchTags, tr]\n );\n\n // Inline tag creation. No vocabularyId is sent — the backend resolves/creates\n // a default workspace VibeControls vocabulary. The freshly created tag is\n // immediately selected via handleAddTag (draft → local; persisted → tagEntity).\n const handleCreateTag = useCallback(\n async (rawLabel: string) => {\n const label = rawLabel.trim();\n if (!label) return;\n try {\n // Refetch the available-tags list so the new tag shows up as a\n // suggestion on subsequent searches (not just as the selected chip).\n const result = await createTag({\n variables: { input: { label } },\n refetchQueries: ['EntityTagPickerAvailableTags'],\n });\n const created = result.data?.createVibecontrolsTag;\n if (!created) {\n throw new Error(\n tr('vibecontrols.shared.entityTagPicker.failedToCreateTag', 'Failed to create tag')\n );\n }\n await handleAddTag({\n id: created.id,\n key: created.key,\n label: created.label,\n color: created.color ?? null,\n });\n } catch (err) {\n setErrorMessage(\n err instanceof Error\n ? err.message\n : tr('vibecontrols.shared.entityTagPicker.failedToCreateTag', 'Failed to create tag')\n );\n }\n },\n [createTag, handleAddTag, tr]\n );\n\n // Offer inline creation when the typed label doesn't already exist (covers\n // both \"no tags yet\" and \"no match\" cases). Match against the unfiltered\n // options AND the already-selected chips — checking only `suggestions` would\n // miss tags already assigned to this entity (filtered out of suggestions),\n // wrongly offering \"Create\" for a label that already exists.\n const trimmedQuery = searchQuery.trim();\n const hasExactMatch = useMemo(() => {\n const q = trimmedQuery.toLowerCase();\n if (!q) return false;\n return (\n rawTagOptions.some((t) => t.label.toLowerCase() === q) ||\n chipItems.some((c) => c.label.toLowerCase() === q)\n );\n }, [rawTagOptions, chipItems, trimmedQuery]);\n // Only offer Create once the debounced search has actually run and settled\n // for the current query — otherwise rawTagOptions may be stale/empty and we\n // could offer Create for a label that already exists. (Search runs only for\n // queries of length >= 2, which also bounds inline-created tag labels.)\n const searchSettledForQuery = searchedQuery === trimmedQuery && !searchLoading;\n const canCreateTag =\n !readOnly && trimmedQuery.length >= 2 && searchSettledForQuery && !hasExactMatch;\n\n if (tagsLoading && !draftMode) {\n return (\n <div\n className=\"flex items-center gap-2 text-text-secondary text-sm\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n {tr('vibecontrols.shared.entityTagPicker.loadingTags', 'Loading tags...')}\n </div>\n );\n }\n\n return (\n <div className=\"space-y-3\">\n {errorMessage && (\n <div\n role=\"alert\"\n className=\"flex items-start gap-2 rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700\"\n >\n <span className=\"flex-1\">{errorMessage}</span>\n <button\n type=\"button\"\n onClick={() => setErrorMessage(null)}\n aria-label={tr('vibecontrols.shared.entityTagPicker.dismissError', 'Dismiss error')}\n className=\"text-red-700 hover:text-red-900\"\n >\n <X className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n </button>\n </div>\n )}\n\n <div\n className=\"flex flex-wrap gap-2\"\n aria-label={tr('vibecontrols.shared.entityTagPicker.assignedTags', 'Assigned tags')}\n >\n {chipItems.length === 0 && !isAdding && (\n <p className=\"text-text-secondary text-sm\">\n {tr('vibecontrols.shared.entityTagPicker.noTagsAssigned', 'No tags assigned')}\n </p>\n )}\n {chipItems.map((chip) => (\n <span\n key={chip.key}\n className=\"inline-flex items-center gap-1 px-3 py-1 bg-action-primary-bg/10 text-action-primary-bg text-sm rounded-full\"\n >\n <Tag className=\"h-3 w-3\" aria-hidden=\"true\" />\n {chip.label}\n {!readOnly && (\n <button\n type=\"button\"\n onClick={() => handleRemoveTag(chip.tagId)}\n disabled={untagging}\n aria-label={`Remove tag ${chip.label}`}\n className=\"ml-1 hover:text-red-500 transition-colors disabled:opacity-50\"\n >\n <X className=\"h-3 w-3\" aria-hidden=\"true\" />\n </button>\n )}\n </span>\n ))}\n {!readOnly && !isAdding && (\n <button\n type=\"button\"\n onClick={() => setIsAdding(true)}\n aria-label={tr('vibecontrols.shared.entityTagPicker.addTag', 'Add tag')}\n className=\"inline-flex items-center gap-1 px-3 py-1 border border-dashed border-border-default text-text-secondary text-sm rounded-full hover:border-action-primary-bg hover:text-action-primary-bg transition-colors\"\n >\n <Plus className=\"h-3 w-3\" aria-hidden=\"true\" />\n {tr('vibecontrols.shared.entityTagPicker.addTag', 'Add Tag')}\n </button>\n )}\n </div>\n\n {isAdding && (\n <div className=\"border border-border-default rounded-lg p-3 space-y-2 bg-bg-surface\">\n <div className=\"relative\">\n <Search\n className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => handleSearch(e.target.value)}\n placeholder={tr(\n 'vibecontrols.shared.entityTagPicker.searchPlaceholder',\n 'Search tags...'\n )}\n autoFocus\n aria-label={tr(\n 'vibecontrols.shared.entityTagPicker.searchAvailableTags',\n 'Search available tags'\n )}\n className=\"w-full pl-9 pr-3 py-2 text-sm bg-bg-default border border-border-default rounded-md focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div\n className=\"flex flex-wrap gap-2 max-h-32 overflow-y-auto\"\n aria-label={tr('vibecontrols.shared.entityTagPicker.tagSuggestions', 'Tag suggestions')}\n >\n {(searchLoading || (availableLoading && !searchQuery)) && (\n <span className=\"text-text-secondary text-xs\">\n {tr('vibecontrols.shared.entityTagPicker.loading', 'Loading...')}\n </span>\n )}\n {!searchLoading && !availableLoading && suggestions.length === 0 && !canCreateTag && (\n <span className=\"text-text-secondary text-xs\">\n {trimmedQuery\n ? tr('vibecontrols.shared.entityTagPicker.noMatchingTags', 'No matching tags')\n : tr(\n 'vibecontrols.shared.entityTagPicker.noAvailableTags',\n 'No tags yet — type a name to create one'\n )}\n </span>\n )}\n {suggestions.map((tag) => (\n <button\n key={tag.id}\n type=\"button\"\n onClick={() => handleAddTag(tag)}\n disabled={tagging}\n aria-label={`Add tag ${tag.label}`}\n className=\"inline-flex items-center gap-1 px-3 py-1 bg-bg-sunken text-text-primary text-sm rounded-full hover:bg-action-primary-bg/10 hover:text-action-primary-bg transition-colors cursor-pointer disabled:opacity-50\"\n >\n <Plus className=\"h-3 w-3\" aria-hidden=\"true\" />\n {tag.label}\n </button>\n ))}\n </div>\n {canCreateTag && (\n <button\n type=\"button\"\n onClick={() => handleCreateTag(trimmedQuery)}\n // Stay disabled through the follow-up assignment too: in persisted\n // mode handleCreateTag runs createTag (creatingTag) then tagEntity\n // (tagging); without `tagging` the button would re-enable between\n // the two, letting a second click duplicate the create.\n disabled={creatingTag || tagging}\n aria-label={tr(\n 'vibecontrols.shared.entityTagPicker.createTagAria',\n 'Create tag {{label}}',\n { label: trimmedQuery }\n )}\n className=\"inline-flex w-full items-center gap-1.5 px-3 py-2 border border-dashed border-action-primary-bg/50 text-action-primary-bg text-sm rounded-md hover:bg-action-primary-bg/10 transition-colors cursor-pointer disabled:opacity-50\"\n >\n {creatingTag || tagging ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Plus className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n )}\n <span className=\"truncate\">\n {tr('vibecontrols.shared.entityTagPicker.createTag', 'Create \"{{label}}\"', {\n label: trimmedQuery,\n })}\n </span>\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => {\n setIsAdding(false);\n setSearchQuery('');\n }}\n className=\"text-xs text-text-secondary hover:text-text-primary\"\n >\n {tr('vibecontrols.shared.entityTagPicker.done', 'Done')}\n </button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"EntityTagPicker.js","names":[],"sources":["../../../src/components/shared/EntityTagPicker.tsx"],"sourcesContent":["/**\n * EntityTagPicker — Reusable tag picker for any VibeControls entity\n *\n * Two modes:\n * - Persisted (entityId provided): self-fetches assignments and writes\n * immediately via tag/untag mutations. Used on detail/edit surfaces.\n * - Draft (entityId omitted/empty): no queries or mutations — selections\n * are held locally and surfaced via `onDraftChange(tagIds)`. The parent\n * create form persists them after the entity is created (e.g. via\n * replaceVibecontrolsEntityTags with the new id). Used in create\n * forms/wizards where no entity id exists yet.\n *\n * Usage:\n * <EntityTagPicker entityType=\"VIBECONTROLS_VIBE\" entityId={vibe.id} />\n * <EntityTagPicker entityType=\"VIBECONTROLS_VIBE\" onDraftChange={setTagIds} />\n */\nimport { useState, useCallback, useMemo, useEffect, useRef } from 'react';\nimport { Tag, Plus, X, Search, Loader2 } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n useGetEntityTags,\n useAvailableTags,\n useSearchTags,\n useTagEntity,\n useUntagEntity,\n useCreateVibecontrolsTag,\n} from '../../hooks/useEntityTags';\nimport type { AvailableTag, EntityTagAssignment } from '../../hooks/useEntityTags';\n\ninterface EntityTagPickerProps {\n entityType: string;\n /**\n * Existing entity id. Omit (or pass empty) to run in draft mode for\n * create forms — selections are emitted via `onDraftChange` instead of\n * being persisted immediately.\n */\n entityId?: string;\n /**\n * Draft-mode callback. Receives the current selected tag ids whenever\n * the draft selection changes. Only invoked when `entityId` is absent.\n */\n onDraftChange?: (tagIds: string[]) => void;\n readOnly?: boolean;\n}\n\nconst ERROR_DISMISS_MS = 4000;\nconst SEARCH_DEBOUNCE_MS = 250;\n\nexport function EntityTagPicker({\n entityType,\n entityId,\n onDraftChange,\n readOnly = false,\n}: EntityTagPickerProps) {\n const tr = useTr();\n const draftMode = !entityId;\n const [isAdding, setIsAdding] = useState(false);\n const [searchQuery, setSearchQuery] = useState('');\n // The trimmed query the most recent debounced search actually ran for.\n // Used to gate inline creation: we only offer \"Create\" once we have search\n // results that correspond to the current query (avoids offering Create\n // against stale/empty results while the debounced search is still pending).\n const [searchedQuery, setSearchedQuery] = useState('');\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n // Draft-mode local selection (no persistence until the parent flushes it).\n const [draftTags, setDraftTags] = useState<AvailableTag[]>([]);\n // Optimistic local list for persisted mode — updated immediately on add/remove\n // so the chip appears/disappears without waiting for a server refetch.\n const [localTags, setLocalTags] = useState<EntityTagAssignment[]>([]);\n // Counts in-flight add/remove mutations; prevents server refetch from\n // overwriting optimistic state before the mutation has completed.\n const inFlightCount = useRef(0);\n\n // In draft mode, propagate the local selection to the parent whenever it\n // changes — and reset it to [] on mount, so a fresh create form never\n // inherits tag ids left over from a previous create on the same page-level\n // hook. Emitting from an effect (rather than inside a setDraftTags updater)\n // keeps the updater pure — no \"setState while rendering\" warning — and lets\n // the handlers use functional updates, which avoids lost-update races when\n // async create/add flows overlap.\n useEffect(() => {\n if (draftMode) onDraftChange?.(draftTags.map((t) => t.id));\n // onDraftChange is a stable setter from the parent's page-level hook;\n // excluded to avoid re-emitting on unrelated parent re-renders.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [draftMode, draftTags]);\n\n const {\n data: tagsData,\n loading: tagsLoading,\n refetch: refetchTags,\n } = useGetEntityTags(entityType, entityId ?? '');\n // Fetch all available tags in persisted mode so we can resolve labels for\n // assigned tags when the federation doesn't return the nested tag object.\n // Also fetched when the add panel is open (suggestions list).\n const { data: availableData, loading: availableLoading } = useAvailableTags(200, {\n skip: draftMode && !isAdding,\n });\n const [searchTags, { data: searchData, loading: searchLoading }] = useSearchTags();\n const [tagEntity, { loading: tagging }] = useTagEntity();\n const [untagEntity, { loading: untagging }] = useUntagEntity();\n const [createTag, { loading: creatingTag }] = useCreateVibecontrolsTag();\n\n // Sync localTags from server data whenever it arrives (initial load or\n // background refetch), but only when no mutation is currently in flight —\n // otherwise we'd overwrite optimistic state mid-mutation.\n useEffect(() => {\n if (draftMode || tagsData?.vibecontrolsEntityTags === undefined) return;\n if (inFlightCount.current > 0) return;\n setLocalTags(tagsData.vibecontrolsEntityTags as EntityTagAssignment[]);\n }, [draftMode, tagsData]);\n\n // Label lookup by tagId — fallback when federation doesn't return nested tag\n const availableTagMap = useMemo(() => {\n const tags = (availableData?.availableVibecontrolsTags as AvailableTag[] | undefined) ?? [];\n return new Map<string, AvailableTag>(tags.map((t) => [t.id, t]));\n }, [availableData]);\n\n // Unified chip list — draft selections in draft mode, persisted\n // assignments otherwise. `tagId` drives removal; `key` is render-stable.\n // Uses localTags (optimistic) so chips appear/disappear immediately on\n // user action — server refetch syncs in the background via the effect above.\n const chipItems = useMemo(\n () =>\n draftMode\n ? draftTags.map((t) => ({ key: t.id, tagId: t.id, label: t.label }))\n : localTags.map((a) => ({\n key: a.id,\n tagId: a.tagId,\n label:\n a.tagInfo?.label ??\n availableTagMap.get(a.tagId)?.label ??\n tr('vibecontrols.shared.entityTagPicker.untitledTag', 'Untitled tag'),\n })),\n [draftMode, draftTags, localTags, availableTagMap, tr]\n );\n\n const assignedTagIds = useMemo(() => new Set(chipItems.map((c) => c.tagId)), [chipItems]);\n\n // Unfiltered tag options from the active source (search results when\n // searching, otherwise the available-tags list). Kept separate from\n // `suggestions` so the create affordance can detect an exact match against\n // ALL existing tags, including ones already assigned to this entity.\n const rawTagOptions: AvailableTag[] = useMemo(() => {\n const source = searchQuery\n ? (searchData?.searchVibecontrolsTags as AvailableTag[] | undefined)\n : (availableData?.availableVibecontrolsTags as AvailableTag[] | undefined);\n return source ?? [];\n }, [searchQuery, searchData, availableData]);\n\n const suggestions: AvailableTag[] = useMemo(\n () => rawTagOptions.filter((t) => !assignedTagIds.has(t.id)),\n [rawTagOptions, assignedTagIds]\n );\n\n // Auto-dismiss error toast after a few seconds\n useEffect(() => {\n if (!errorMessage) return;\n const timer = setTimeout(() => setErrorMessage(null), ERROR_DISMISS_MS);\n return () => clearTimeout(timer);\n }, [errorMessage]);\n\n // Debounce the search query so we don't fire a request on every keystroke.\n const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n useEffect(() => {\n return () => {\n if (searchTimerRef.current) clearTimeout(searchTimerRef.current);\n };\n }, []);\n\n const handleSearch = useCallback(\n (query: string) => {\n setSearchQuery(query);\n if (searchTimerRef.current) clearTimeout(searchTimerRef.current);\n const trimmed = query.trim();\n // No search runs for short queries, so there are no authoritative results\n // for this input — clear the marker to keep the Create button gated.\n if (trimmed.length < 2) {\n setSearchedQuery('');\n return;\n }\n searchTimerRef.current = setTimeout(() => {\n setSearchedQuery(trimmed);\n searchTags({ variables: { query: trimmed, limit: 10 } });\n }, SEARCH_DEBOUNCE_MS);\n },\n [searchTags]\n );\n\n const handleAddTag = useCallback(\n async (tag: AvailableTag) => {\n if (draftMode) {\n // Functional update so overlapping async flows (e.g. an in-flight\n // create plus a click on another suggestion) can't overwrite each\n // other from a stale snapshot. The draftTags-watching effect emits\n // onDraftChange for us.\n setDraftTags((prev) => (prev.some((t) => t.id === tag.id) ? prev : [...prev, tag]));\n setSearchQuery('');\n setErrorMessage(null);\n return;\n }\n // Optimistic: add chip immediately so the user sees it without waiting\n // for the mutation + refetch round-trip. A temporary id is used until\n // the server response replaces it with the real EntityTagAssignment id.\n const optimisticId = `optimistic-${tag.id}`;\n const optimistic: EntityTagAssignment = {\n id: optimisticId,\n tagId: tag.id,\n tagInfo: { id: tag.id, key: tag.key, label: tag.label, color: tag.color },\n entityType,\n entityId: entityId ?? '',\n source: 'manual',\n assignedBy: null,\n assignedAt: null,\n };\n setLocalTags((prev) => (prev.some((t) => t.tagId === tag.id) ? prev : [...prev, optimistic]));\n setSearchQuery('');\n setErrorMessage(null);\n inFlightCount.current++;\n try {\n const result = await tagEntity({\n variables: { input: { entityType, entityId: entityId ?? '', tagId: tag.id } },\n });\n // Swap the optimistic entry for the real server assignment\n const real = result.data?.tagVibecontrolsEntity as EntityTagAssignment | undefined;\n if (real) {\n setLocalTags((prev) => prev.map((t) => (t.id === optimisticId ? real : t)));\n }\n } catch (err) {\n // Roll back the optimistic add\n setLocalTags((prev) => prev.filter((t) => t.id !== optimisticId));\n setErrorMessage(\n err instanceof Error\n ? err.message\n : tr('vibecontrols.shared.entityTagPicker.failedToAddTag', 'Failed to add tag')\n );\n } finally {\n inFlightCount.current--;\n // Background sync: let Apollo reconcile cache with server state\n refetchTags();\n }\n },\n [draftMode, tagEntity, entityType, entityId, refetchTags, tr]\n );\n\n const handleRemoveTag = useCallback(\n async (tagId: string) => {\n if (draftMode) {\n setDraftTags((prev) => prev.filter((t) => t.id !== tagId));\n setErrorMessage(null);\n return;\n }\n // Optimistic: remove chip immediately without waiting for the server\n setLocalTags((prev) => prev.filter((t) => t.tagId !== tagId));\n setErrorMessage(null);\n inFlightCount.current++;\n try {\n await untagEntity({\n variables: { input: { entityType, entityId: entityId ?? '', tagId } },\n });\n } catch (err) {\n setErrorMessage(\n err instanceof Error\n ? err.message\n : tr('vibecontrols.shared.entityTagPicker.failedToRemoveTag', 'Failed to remove tag')\n );\n } finally {\n inFlightCount.current--;\n // Background sync: re-fetch so the local state converges with server\n // (also restores the chip if the mutation failed)\n refetchTags();\n }\n },\n [draftMode, untagEntity, entityType, entityId, refetchTags, tr]\n );\n\n // Inline tag creation. No vocabularyId is sent — the backend resolves/creates\n // a default workspace VibeControls vocabulary. The freshly created tag is\n // immediately selected via handleAddTag (draft → local; persisted → tagEntity).\n const handleCreateTag = useCallback(\n async (rawLabel: string) => {\n const label = rawLabel.trim();\n if (!label) return;\n try {\n // Refetch the available-tags list so the new tag shows up as a\n // suggestion on subsequent searches (not just as the selected chip).\n const result = await createTag({\n variables: { input: { label } },\n refetchQueries: ['EntityTagPickerAvailableTags'],\n });\n const created = result.data?.createVibecontrolsTag;\n if (!created) {\n throw new Error(\n tr('vibecontrols.shared.entityTagPicker.failedToCreateTag', 'Failed to create tag')\n );\n }\n await handleAddTag({\n id: created.id,\n key: created.key,\n label: created.label,\n color: created.color ?? null,\n });\n } catch (err) {\n setErrorMessage(\n err instanceof Error\n ? err.message\n : tr('vibecontrols.shared.entityTagPicker.failedToCreateTag', 'Failed to create tag')\n );\n }\n },\n [createTag, handleAddTag, tr]\n );\n\n // Offer inline creation when the typed label doesn't already exist (covers\n // both \"no tags yet\" and \"no match\" cases). Match against the unfiltered\n // options AND the already-selected chips — checking only `suggestions` would\n // miss tags already assigned to this entity (filtered out of suggestions),\n // wrongly offering \"Create\" for a label that already exists.\n const trimmedQuery = searchQuery.trim();\n const hasExactMatch = useMemo(() => {\n const q = trimmedQuery.toLowerCase();\n if (!q) return false;\n return (\n rawTagOptions.some((t) => t.label.toLowerCase() === q) ||\n chipItems.some((c) => c.label.toLowerCase() === q)\n );\n }, [rawTagOptions, chipItems, trimmedQuery]);\n // Only offer Create once the debounced search has actually run and settled\n // for the current query — otherwise rawTagOptions may be stale/empty and we\n // could offer Create for a label that already exists. (Search runs only for\n // queries of length >= 2, which also bounds inline-created tag labels.)\n const searchSettledForQuery = searchedQuery === trimmedQuery && !searchLoading;\n const canCreateTag =\n !readOnly && trimmedQuery.length >= 2 && searchSettledForQuery && !hasExactMatch;\n\n if (tagsLoading && !draftMode) {\n return (\n <div\n className=\"flex items-center gap-2 text-text-secondary text-sm\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n {tr('vibecontrols.shared.entityTagPicker.loadingTags', 'Loading tags...')}\n </div>\n );\n }\n\n return (\n <div className=\"space-y-3\">\n {errorMessage && (\n <div\n role=\"alert\"\n className=\"flex items-start gap-2 rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700\"\n >\n <span className=\"flex-1\">{errorMessage}</span>\n <button\n type=\"button\"\n onClick={() => setErrorMessage(null)}\n aria-label={tr('vibecontrols.shared.entityTagPicker.dismissError', 'Dismiss error')}\n className=\"text-red-700 hover:text-red-900\"\n >\n <X className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n </button>\n </div>\n )}\n\n <div\n className=\"flex flex-wrap gap-2\"\n aria-label={tr('vibecontrols.shared.entityTagPicker.assignedTags', 'Assigned tags')}\n >\n {chipItems.length === 0 && !isAdding && (\n <p className=\"text-text-secondary text-sm\">\n {tr('vibecontrols.shared.entityTagPicker.noTagsAssigned', 'No tags assigned')}\n </p>\n )}\n {chipItems.map((chip) => (\n <span\n key={chip.key}\n className=\"inline-flex items-center gap-1 px-3 py-1 bg-action-primary-bg/10 text-action-primary-bg text-sm rounded-full\"\n >\n <Tag className=\"h-3 w-3\" aria-hidden=\"true\" />\n {chip.label}\n {!readOnly && (\n <button\n type=\"button\"\n onClick={() => handleRemoveTag(chip.tagId)}\n disabled={untagging}\n aria-label={`Remove tag ${chip.label}`}\n className=\"ml-1 hover:text-red-500 transition-colors disabled:opacity-50\"\n >\n <X className=\"h-3 w-3\" aria-hidden=\"true\" />\n </button>\n )}\n </span>\n ))}\n {!readOnly && !isAdding && (\n <button\n type=\"button\"\n onClick={() => setIsAdding(true)}\n aria-label={tr('vibecontrols.shared.entityTagPicker.addTag', 'Add tag')}\n className=\"inline-flex items-center gap-1 px-3 py-1 border border-dashed border-border-default text-text-secondary text-sm rounded-full hover:border-action-primary-bg hover:text-action-primary-bg transition-colors\"\n >\n <Plus className=\"h-3 w-3\" aria-hidden=\"true\" />\n {tr('vibecontrols.shared.entityTagPicker.addTag', 'Add Tag')}\n </button>\n )}\n </div>\n\n {isAdding && (\n <div className=\"border border-border-default rounded-lg p-3 space-y-2 bg-bg-surface\">\n <div className=\"relative\">\n <Search\n className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => handleSearch(e.target.value)}\n placeholder={tr(\n 'vibecontrols.shared.entityTagPicker.searchPlaceholder',\n 'Search tags...'\n )}\n autoFocus\n aria-label={tr(\n 'vibecontrols.shared.entityTagPicker.searchAvailableTags',\n 'Search available tags'\n )}\n className=\"w-full pl-9 pr-3 py-2 text-sm bg-bg-default border border-border-default rounded-md focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div\n className=\"flex flex-wrap gap-2 max-h-32 overflow-y-auto\"\n aria-label={tr('vibecontrols.shared.entityTagPicker.tagSuggestions', 'Tag suggestions')}\n >\n {(searchLoading || (availableLoading && !searchQuery)) && (\n <span className=\"text-text-secondary text-xs\">\n {tr('vibecontrols.shared.entityTagPicker.loading', 'Loading...')}\n </span>\n )}\n {!searchLoading && !availableLoading && suggestions.length === 0 && !canCreateTag && (\n <span className=\"text-text-secondary text-xs\">\n {trimmedQuery\n ? tr('vibecontrols.shared.entityTagPicker.noMatchingTags', 'No matching tags')\n : tr(\n 'vibecontrols.shared.entityTagPicker.noAvailableTags',\n 'No tags yet — type a name to create one'\n )}\n </span>\n )}\n {suggestions.map((tag) => (\n <button\n key={tag.id}\n type=\"button\"\n onClick={() => handleAddTag(tag)}\n disabled={tagging}\n aria-label={`Add tag ${tag.label}`}\n className=\"inline-flex items-center gap-1 px-3 py-1 bg-bg-sunken text-text-primary text-sm rounded-full hover:bg-action-primary-bg/10 hover:text-action-primary-bg transition-colors cursor-pointer disabled:opacity-50\"\n >\n <Plus className=\"h-3 w-3\" aria-hidden=\"true\" />\n {tag.label}\n </button>\n ))}\n </div>\n {canCreateTag && (\n <button\n type=\"button\"\n onClick={() => handleCreateTag(trimmedQuery)}\n // Stay disabled through the follow-up assignment too: in persisted\n // mode handleCreateTag runs createTag (creatingTag) then tagEntity\n // (tagging); without `tagging` the button would re-enable between\n // the two, letting a second click duplicate the create.\n disabled={creatingTag || tagging}\n aria-label={tr(\n 'vibecontrols.shared.entityTagPicker.createTagAria',\n 'Create tag {{label}}',\n { label: trimmedQuery }\n )}\n className=\"inline-flex w-full items-center gap-1.5 px-3 py-2 border border-dashed border-action-primary-bg/50 text-action-primary-bg text-sm rounded-md hover:bg-action-primary-bg/10 transition-colors cursor-pointer disabled:opacity-50\"\n >\n {creatingTag || tagging ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Plus className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n )}\n <span className=\"truncate\">\n {tr('vibecontrols.shared.entityTagPicker.createTag', 'Create \"{{label}}\"', {\n label: trimmedQuery,\n })}\n </span>\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => {\n setIsAdding(false);\n setSearchQuery('');\n }}\n className=\"text-xs text-text-secondary hover:text-text-primary\"\n >\n {tr('vibecontrols.shared.entityTagPicker.done', 'Done')}\n </button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AA6CA,IAAM,KAAmB,KACnB,IAAqB;AAE3B,SAAgB,EAAgB,EAC9B,eACA,aACA,mBACA,cAAW,MACY;CACvB,IAAM,IAAK,GAAO,EACZ,IAAY,CAAC,GACb,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAa,KAAkB,EAAS,GAAG,EAK5C,CAAC,IAAe,KAAoB,EAAS,GAAG,EAChD,CAAC,GAAc,KAAmB,EAAwB,KAAK,EAE/D,CAAC,GAAW,KAAgB,EAAyB,EAAE,CAAC,EAGxD,CAAC,GAAW,KAAgB,EAAgC,EAAE,CAAC,EAG/D,IAAgB,EAAO,EAAE;AAS/B,SAAgB;AACd,EAAI,KAAW,KAAgB,EAAU,KAAK,MAAM,EAAE,GAAG,CAAC;IAIzD,CAAC,GAAW,EAAU,CAAC;CAE1B,IAAM,EACJ,MAAM,GACN,SAAS,GACT,SAAS,MACP,GAAiB,GAAY,KAAY,GAAG,EAI1C,EAAE,MAAM,GAAe,SAAS,MAAqB,EAAiB,KAAK,EAC/E,MAAM,KAAa,CAAC,GACrB,CAAC,EACI,CAAC,GAAY,EAAE,MAAM,GAAY,SAAS,OAAmB,IAAe,EAC5E,CAAC,GAAW,EAAE,SAAS,OAAa,IAAc,EAClD,CAAC,GAAa,EAAE,SAAS,QAAe,IAAgB,EACxD,CAAC,GAAW,EAAE,SAAS,OAAiB,GAA0B;AAKxE,SAAgB;AACV,OAAa,GAAU,2BAA2B,KAAA,KAClD,EAAc,UAAU,KAC5B,EAAa,EAAS,uBAAgD;IACrE,CAAC,GAAW,EAAS,CAAC;CAGzB,IAAM,IAAkB,QAAc;EACpC,IAAM,IAAQ,GAAe,6BAA4D,EAAE;AAC3F,SAAO,IAAI,IAA0B,EAAK,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/D,CAAC,EAAc,CAAC,EAMb,IAAY,QAEd,IACI,EAAU,KAAK,OAAO;EAAE,KAAK,EAAE;EAAI,OAAO,EAAE;EAAI,OAAO,EAAE;EAAO,EAAE,GAClE,EAAU,KAAK,OAAO;EACpB,KAAK,EAAE;EACP,OAAO,EAAE;EACT,OACE,EAAE,SAAS,SACX,EAAgB,IAAI,EAAE,MAAM,EAAE,SAC9B,EAAG,mDAAmD,eAAe;EACxE,EAAE,EACT;EAAC;EAAW;EAAW;EAAW;EAAiB;EAAG,CACvD,EAEK,IAAiB,QAAc,IAAI,IAAI,EAAU,KAAK,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,EAAU,CAAC,EAMnF,IAAgC,SACrB,IACV,GAAY,yBACZ,GAAe,8BACH,EAAE,EAClB;EAAC;EAAa;EAAY;EAAc,CAAC,EAEtC,IAA8B,QAC5B,EAAc,QAAQ,MAAM,CAAC,EAAe,IAAI,EAAE,GAAG,CAAC,EAC5D,CAAC,GAAe,EAAe,CAChC;AAGD,SAAgB;AACd,MAAI,CAAC,EAAc;EACnB,IAAM,IAAQ,iBAAiB,EAAgB,KAAK,EAAE,GAAiB;AACvE,eAAa,aAAa,EAAM;IAC/B,CAAC,EAAa,CAAC;CAGlB,IAAM,IAAiB,EAA6C,KAAK;AACzE,eACe;AACX,EAAI,EAAe,WAAS,aAAa,EAAe,QAAQ;IAEjE,EAAE,CAAC;CAEN,IAAM,KAAe,GAClB,MAAkB;AAEjB,EADA,EAAe,EAAM,EACjB,EAAe,WAAS,aAAa,EAAe,QAAQ;EAChE,IAAM,IAAU,EAAM,MAAM;AAG5B,MAAI,EAAQ,SAAS,GAAG;AACtB,KAAiB,GAAG;AACpB;;AAEF,IAAe,UAAU,iBAAiB;AAExC,GADA,EAAiB,EAAQ,EACzB,EAAW,EAAE,WAAW;IAAE,OAAO;IAAS,OAAO;IAAI,EAAE,CAAC;KACvD,EAAmB;IAExB,CAAC,EAAW,CACb,EAEK,IAAe,EACnB,OAAO,MAAsB;AAC3B,MAAI,GAAW;AAOb,GAFA,GAAc,MAAU,EAAK,MAAM,MAAM,EAAE,OAAO,EAAI,GAAG,GAAG,IAAO,CAAC,GAAG,GAAM,EAAI,CAAE,EACnF,EAAe,GAAG,EAClB,EAAgB,KAAK;AACrB;;EAKF,IAAM,IAAe,cAAc,EAAI,MACjC,IAAkC;GACtC,IAAI;GACJ,OAAO,EAAI;GACX,SAAS;IAAE,IAAI,EAAI;IAAI,KAAK,EAAI;IAAK,OAAO,EAAI;IAAO,OAAO,EAAI;IAAO;GACzE;GACA,UAAU,KAAY;GACtB,QAAQ;GACR,YAAY;GACZ,YAAY;GACb;AAID,EAHA,GAAc,MAAU,EAAK,MAAM,MAAM,EAAE,UAAU,EAAI,GAAG,GAAG,IAAO,CAAC,GAAG,GAAM,EAAW,CAAE,EAC7F,EAAe,GAAG,EAClB,EAAgB,KAAK,EACrB,EAAc;AACd,MAAI;GAKF,IAAM,KAJS,MAAM,EAAU,EAC7B,WAAW,EAAE,OAAO;IAAE;IAAY,UAAU,KAAY;IAAI,OAAO,EAAI;IAAI,EAAE,EAC9E,CAAC,EAEkB,MAAM;AAC1B,GAAI,KACF,GAAc,MAAS,EAAK,KAAK,MAAO,EAAE,OAAO,IAAe,IAAO,EAAG,CAAC;WAEtE,GAAK;AAGZ,GADA,GAAc,MAAS,EAAK,QAAQ,MAAM,EAAE,OAAO,EAAa,CAAC,EACjE,EACE,aAAe,QACX,EAAI,UACJ,EAAG,sDAAsD,oBAAoB,CAClF;YACO;AAGR,GAFA,EAAc,WAEd,GAAa;;IAGjB;EAAC;EAAW;EAAW;EAAY;EAAU;EAAa;EAAG,CAC9D,EAEK,IAAkB,EACtB,OAAO,MAAkB;AACvB,MAAI,GAAW;AAEb,GADA,GAAc,MAAS,EAAK,QAAQ,MAAM,EAAE,OAAO,EAAM,CAAC,EAC1D,EAAgB,KAAK;AACrB;;AAKF,EAFA,GAAc,MAAS,EAAK,QAAQ,MAAM,EAAE,UAAU,EAAM,CAAC,EAC7D,EAAgB,KAAK,EACrB,EAAc;AACd,MAAI;AACF,SAAM,EAAY,EAChB,WAAW,EAAE,OAAO;IAAE;IAAY,UAAU,KAAY;IAAI;IAAO,EAAE,EACtE,CAAC;WACK,GAAK;AACZ,KACE,aAAe,QACX,EAAI,UACJ,EAAG,yDAAyD,uBAAuB,CACxF;YACO;AAIR,GAHA,EAAc,WAGd,GAAa;;IAGjB;EAAC;EAAW;EAAa;EAAY;EAAU;EAAa;EAAG,CAChE,EAKK,KAAkB,EACtB,OAAO,MAAqB;EAC1B,IAAM,IAAQ,EAAS,MAAM;AACxB,QACL,KAAI;GAOF,IAAM,KAJS,MAAM,EAAU;IAC7B,WAAW,EAAE,OAAO,EAAE,UAAO,EAAE;IAC/B,gBAAgB,CAAC,+BAA+B;IACjD,CAAC,EACqB,MAAM;AAC7B,OAAI,CAAC,EACH,OAAU,MACR,EAAG,yDAAyD,uBAAuB,CACpF;AAEH,SAAM,EAAa;IACjB,IAAI,EAAQ;IACZ,KAAK,EAAQ;IACb,OAAO,EAAQ;IACf,OAAO,EAAQ,SAAS;IACzB,CAAC;WACK,GAAK;AACZ,KACE,aAAe,QACX,EAAI,UACJ,EAAG,yDAAyD,uBAAuB,CACxF;;IAGL;EAAC;EAAW;EAAc;EAAG,CAC9B,EAOK,IAAe,EAAY,MAAM,EACjC,KAAgB,QAAc;EAClC,IAAM,IAAI,EAAa,aAAa;AAEpC,SADK,IAEH,EAAc,MAAM,MAAM,EAAE,MAAM,aAAa,KAAK,EAAE,IACtD,EAAU,MAAM,MAAM,EAAE,MAAM,aAAa,KAAK,EAAE,GAHrC;IAKd;EAAC;EAAe;EAAW;EAAa,CAAC,EAKtC,KAAwB,OAAkB,KAAgB,CAAC,GAC3D,IACJ,CAAC,KAAY,EAAa,UAAU,KAAK,MAAyB,CAAC;AAerE,QAbI,KAAe,CAAC,IAEhB,kBAAC,OAAD;EACE,WAAU;EACV,MAAK;EACL,aAAU;YAHZ,CAKE,kBAAC,GAAD;GAAS,WAAU;GAAuB,eAAY;GAAS,CAAA,EAC9D,EAAG,mDAAmD,kBAAkB,CACrE;MAKR,kBAAC,OAAD;EAAK,WAAU;YAAf;GACG,KACC,kBAAC,OAAD;IACE,MAAK;IACL,WAAU;cAFZ,CAIE,kBAAC,QAAD;KAAM,WAAU;eAAU;KAAoB,CAAA,EAC9C,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAgB,KAAK;KACpC,cAAY,EAAG,oDAAoD,gBAAgB;KACnF,WAAU;eAEV,kBAAC,GAAD;MAAG,WAAU;MAAc,eAAY;MAAS,CAAA;KACzC,CAAA,CACL;;GAGR,kBAAC,OAAD;IACE,WAAU;IACV,cAAY,EAAG,oDAAoD,gBAAgB;cAFrF;KAIG,EAAU,WAAW,KAAK,CAAC,KAC1B,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAG,sDAAsD,mBAAmB;MAC3E,CAAA;KAEL,EAAU,KAAK,MACd,kBAAC,QAAD;MAEE,WAAU;gBAFZ;OAIE,kBAAC,IAAD;QAAK,WAAU;QAAU,eAAY;QAAS,CAAA;OAC7C,EAAK;OACL,CAAC,KACA,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAgB,EAAK,MAAM;QAC1C,UAAU;QACV,cAAY,cAAc,EAAK;QAC/B,WAAU;kBAEV,kBAAC,GAAD;SAAG,WAAU;SAAU,eAAY;SAAS,CAAA;QACrC,CAAA;OAEN;QAhBA,EAAK,IAgBL,CACP;KACD,CAAC,KAAY,CAAC,KACb,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAY,GAAK;MAChC,cAAY,EAAG,8CAA8C,UAAU;MACvE,WAAU;gBAJZ,CAME,kBAAC,GAAD;OAAM,WAAU;OAAU,eAAY;OAAS,CAAA,EAC9C,EAAG,8CAA8C,UAAU,CACrD;;KAEP;;GAEL,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,IAAD;OACE,WAAU;OACV,eAAY;OACZ,CAAA,EACF,kBAAC,SAAD;OACE,MAAK;OACL,OAAO;OACP,WAAW,MAAM,GAAa,EAAE,OAAO,MAAM;OAC7C,aAAa,EACX,yDACA,iBACD;OACD,WAAA;OACA,cAAY,EACV,2DACA,wBACD;OACD,WAAU;OACV,CAAA,CACE;;KACN,kBAAC,OAAD;MACE,WAAU;MACV,cAAY,EAAG,sDAAsD,kBAAkB;gBAFzF;QAII,KAAkB,KAAoB,CAAC,MACvC,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAG,+CAA+C,aAAa;QAC3D,CAAA;OAER,CAAC,KAAiB,CAAC,KAAoB,EAAY,WAAW,KAAK,CAAC,KACnE,kBAAC,QAAD;QAAM,WAAU;kBACb,IACG,EAAG,sDAAsD,mBAAmB,GAC5E,EACE,uDACA,0CACD;QACA,CAAA;OAER,EAAY,KAAK,MAChB,kBAAC,UAAD;QAEE,MAAK;QACL,eAAe,EAAa,EAAI;QAChC,UAAU;QACV,cAAY,WAAW,EAAI;QAC3B,WAAU;kBANZ,CAQE,kBAAC,GAAD;SAAM,WAAU;SAAU,eAAY;SAAS,CAAA,EAC9C,EAAI,MACE;UATF,EAAI,GASF,CACT;OACE;;KACL,KACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,GAAgB,EAAa;MAK5C,UAAU,KAAe;MACzB,cAAY,EACV,qDACA,wBACA,EAAE,OAAO,GAAc,CACxB;MACD,WAAU;gBAbZ,CAeG,KAAe,IACd,kBAAC,GAAD;OAAS,WAAU;OAA2B,eAAY;OAAS,CAAA,GAEnE,kBAAC,GAAD;OAAM,WAAU;OAAc,eAAY;OAAS,CAAA,EAErD,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAG,iDAAiD,wBAAsB,EACzE,OAAO,GACR,CAAC;OACG,CAAA,CACA;;KAEX,kBAAC,UAAD;MACE,MAAK;MACL,eAAe;AAEb,OADA,EAAY,GAAM,EAClB,EAAe,GAAG;;MAEpB,WAAU;gBAET,EAAG,4CAA4C,OAAO;MAChD,CAAA;KACL;;GAEJ"}
@@ -199,12 +199,15 @@ var n = {}, r = e`
199
199
  fragment EntityTagPickerAssignmentFields on EntityTagAssignment {
200
200
  id
201
201
  tagId
202
- tag {
202
+ tagInfo {
203
203
  id
204
204
  key
205
205
  label
206
206
  color
207
207
  }
208
+ tag {
209
+ id
210
+ }
208
211
  entityType
209
212
  entityId
210
213
  source