anton-bakker-feedback 0.4.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +171 -0
  2. package/dist/chunks/ingest-transport-DAA_pJNb.js +245 -0
  3. package/dist/chunks/ingest-transport-DAA_pJNb.js.map +1 -0
  4. package/dist/chunks/normalisers-Cbd-i4Jh.js +97 -0
  5. package/dist/chunks/normalisers-Cbd-i4Jh.js.map +1 -0
  6. package/dist/chunks/{observability-CfwHKObh.js → observability-DUK725zv.js} +3 -3
  7. package/dist/chunks/{observability-CfwHKObh.js.map → observability-DUK725zv.js.map} +1 -1
  8. package/dist/chunks/{submitResolver-DsfkrUHY.js → submitResolver-wfr4kbEM.js} +60 -39
  9. package/dist/chunks/submitResolver-wfr4kbEM.js.map +1 -0
  10. package/dist/core/index.d.ts +1 -0
  11. package/dist/core/index.d.ts.map +1 -1
  12. package/dist/core/ingest-transport.d.ts +97 -0
  13. package/dist/core/ingest-transport.d.ts.map +1 -1
  14. package/dist/core/normalisers.d.ts +29 -0
  15. package/dist/core/normalisers.d.ts.map +1 -0
  16. package/dist/core/observability.d.ts.map +1 -1
  17. package/dist/core/state-metrics.d.ts +19 -0
  18. package/dist/core/state-metrics.d.ts.map +1 -0
  19. package/dist/core/transforms.d.ts.map +1 -1
  20. package/dist/core/types.d.ts +93 -3
  21. package/dist/core/types.d.ts.map +1 -1
  22. package/dist/index.js +16 -98
  23. package/dist/index.js.map +1 -1
  24. package/dist/react/admin/ActivityTimeline.d.ts +9 -0
  25. package/dist/react/admin/ActivityTimeline.d.ts.map +1 -0
  26. package/dist/react/admin/AnnotationComposer.d.ts +30 -0
  27. package/dist/react/admin/AnnotationComposer.d.ts.map +1 -0
  28. package/dist/react/admin/FeedbackListPage.d.ts.map +1 -1
  29. package/dist/react/admin/FeedbackUpdatePage.d.ts +6 -0
  30. package/dist/react/admin/FeedbackUpdatePage.d.ts.map +1 -1
  31. package/dist/react/admin/LifecycleControls.d.ts +28 -0
  32. package/dist/react/admin/LifecycleControls.d.ts.map +1 -0
  33. package/dist/react/admin/__tests__/ActivityTimeline.test.d.ts +2 -0
  34. package/dist/react/admin/__tests__/ActivityTimeline.test.d.ts.map +1 -0
  35. package/dist/react/admin/__tests__/AnnotationComposer.test.d.ts +2 -0
  36. package/dist/react/admin/__tests__/AnnotationComposer.test.d.ts.map +1 -0
  37. package/dist/react/admin/__tests__/LifecycleControls.test.d.ts +2 -0
  38. package/dist/react/admin/__tests__/LifecycleControls.test.d.ts.map +1 -0
  39. package/dist/react/admin/hooks/can.d.ts +47 -0
  40. package/dist/react/admin/hooks/can.d.ts.map +1 -0
  41. package/dist/react/admin/hooks/useStatusTransition.d.ts +16 -2
  42. package/dist/react/admin/hooks/useStatusTransition.d.ts.map +1 -1
  43. package/dist/react/admin/index.d.ts +14 -1
  44. package/dist/react/admin/index.d.ts.map +1 -1
  45. package/dist/react/admin/index.js +312 -29
  46. package/dist/react/admin/index.js.map +1 -1
  47. package/dist/react/admin/types.d.ts +13 -0
  48. package/dist/react/admin/types.d.ts.map +1 -1
  49. package/dist/react/resolver/submitResolver.d.ts +10 -7
  50. package/dist/react/resolver/submitResolver.d.ts.map +1 -1
  51. package/dist/react.js +1 -1
  52. package/dist/server/index.d.ts +4 -5
  53. package/dist/server/index.d.ts.map +1 -1
  54. package/dist/server/listHandler.d.ts +15 -8
  55. package/dist/server/listHandler.d.ts.map +1 -1
  56. package/dist/server.js +22 -170
  57. package/dist/server.js.map +1 -1
  58. package/package.json +13 -1
  59. package/dist/chunks/submitResolver-DsfkrUHY.js.map +0 -1
  60. package/dist/server/FeedbackService.d.ts +0 -46
  61. package/dist/server/FeedbackService.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"useStatusTransition.d.ts","sourceRoot":"","sources":["../../../../src/react/admin/hooks/useStatusTransition.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAc7D,MAAM,WAAW,yBAAyB;IACxC,2EAA2E;IAC3E,cAAc,EAAE,cAAc,EAAE,CAAC;CAClC;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,cAAc,GAAG,yBAAyB,CAG5F"}
1
+ {"version":3,"file":"useStatusTransition.d.ts","sourceRoot":"","sources":["../../../../src/react/admin/hooks/useStatusTransition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,SAAS,cAAc,EAAE,CAAC,CAahG,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,GAAG,OAAO,CAGjF;AAED,MAAM,WAAW,yBAAyB;IACxC,2EAA2E;IAC3E,cAAc,EAAE,cAAc,EAAE,CAAC;CAClC;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,cAAc,GAAG,yBAAyB,CAG5F"}
@@ -1,7 +1,20 @@
1
1
  export { FeedbackListPage, StatusBadge, TypeBadge } from './FeedbackListPage.js';
2
2
  export { FeedbackDetailPage } from './FeedbackDetailPage.js';
3
+ /**
4
+ * @deprecated FeedbackUpdatePage is superseded by the enriched FeedbackDetailPage with appendActivity + transitionStatus.
5
+ * Use createIngestAnnotateClient + FeedbackDetailPage with the new IoC props instead.
6
+ * This export is kept for backward compatibility and will be removed in a future major version.
7
+ */
3
8
  export { FeedbackUpdatePage } from './FeedbackUpdatePage.js';
9
+ export { ActivityTimeline } from './ActivityTimeline.js';
10
+ export { AnnotationComposer } from './AnnotationComposer.js';
11
+ export { LifecycleControls } from './LifecycleControls.js';
4
12
  export { useStatusTransition } from './hooks/useStatusTransition.js';
13
+ export { can, CAPABILITIES } from './hooks/can.js';
14
+ export type { CapabilityKey, CapabilityUser, UserWithCapabilities } from './hooks/can.js';
5
15
  export type { AdminPageProps, FeedbackListPageProps, FeedbackDetailPageProps, FeedbackUpdatePageProps, } from './types.js';
6
- export type { StoredFeedbackItem, FeedbackStatus, FeedbackType, FeedbackScope, FeedbackSize, FeedbackEnvironment, Reporter, DialogueEntry, CorrectionEntry, FeedbackContext, } from '../../core/types.js';
16
+ export type { StoredFeedbackItem, FeedbackStatus, FeedbackType, FeedbackScope, FeedbackSize, FeedbackEnvironment, Reporter, DialogueEntry, CorrectionEntry, FeedbackContext, ActivityEntry, ActivityKind, ActivityActor, } from '../../core/types.js';
17
+ export type { FeedbackAnnotateClient, AnnotateResult, TransitionStatusOptions, } from '../../core/ingest-transport.js';
18
+ export { createIngestAnnotateClient } from '../../core/ingest-transport.js';
19
+ export { normaliseStoredItem } from '../../core/normalisers.js';
7
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/admin/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAGrE,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,QAAQ,EACR,aAAa,EACb,eAAe,EACf,eAAe,GAChB,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/admin/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D;;;;GAIG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAG3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAGrE,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAG1F,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,QAAQ,EACR,aAAa,EACb,eAAe,EACf,eAAe,EACf,aAAa,EACb,YAAY,EACZ,aAAa,GACd,MAAM,qBAAqB,CAAC;AAG7B,YAAY,EACV,sBAAsB,EACtB,cAAc,EACd,uBAAuB,GACxB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC"}
@@ -1,12 +1,21 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { ListPage, Badge, DetailPage, Button, DetailSection, DetailRow, UpdatePage, EnumField, TextField, TextareaField, SaveFooter } from "anton-bakker-tailwind-components";
4
+ import { d } from "../../chunks/ingest-transport-DAA_pJNb.js";
5
+ import { c } from "../../chunks/normalisers-Cbd-i4Jh.js";
4
6
  const STATUS_VARIANT = {
5
- open: "warning",
6
- approved: "default",
7
+ new: "warning",
8
+ triaged: "default",
9
+ assigned: "default",
10
+ in_progress: "default",
11
+ fixed: "success",
12
+ pending_retest: "warning",
13
+ verified: "success",
14
+ closed: "success",
7
15
  deferred: "secondary",
8
- resolved: "success",
9
- wontfix: "destructive"
16
+ rejected: "destructive",
17
+ duplicate: "secondary",
18
+ reopened: "warning"
10
19
  };
11
20
  const TYPE_VARIANT = {
12
21
  bug: "destructive",
@@ -21,7 +30,7 @@ function TypeBadge({ type }) {
21
30
  return /* @__PURE__ */ jsx(Badge, { variant: TYPE_VARIANT[type] ?? "secondary", children: type });
22
31
  }
23
32
  const FeedbackListPage = React.forwardRef(
24
- ({ items, loading = false, onNavigate, className, emptyStateMessage, currentUser, ...rest }, ref) => {
33
+ ({ items, loading = false, onNavigate, className, emptyStateMessage, currentUser: _currentUser }, ref) => {
25
34
  const [search, setSearch] = React.useState("");
26
35
  const [statusFilter, setStatusFilter] = React.useState("");
27
36
  const [typeFilter, setTypeFilter] = React.useState("");
@@ -135,7 +144,7 @@ function parseStructuredContent(content) {
135
144
  return { header, fields, raw: content };
136
145
  }
137
146
  const FeedbackDetailPage = React.forwardRef(
138
- ({ feedbackId, fetchItem, getScreenshotUrl, onNavigate, currentUser, className }, ref) => {
147
+ ({ feedbackId, fetchItem, getScreenshotUrl, onNavigate, currentUser: _currentUser, className }, ref) => {
139
148
  const [item, setItem] = React.useState(null);
140
149
  const [loading, setLoading] = React.useState(true);
141
150
  const [error, setError] = React.useState(null);
@@ -224,15 +233,22 @@ const FeedbackDetailPage = React.forwardRef(
224
233
  }
225
234
  );
226
235
  FeedbackDetailPage.displayName = "FeedbackDetailPage";
227
- const ALLOWED_TRANSITIONS = {
228
- open: ["approved", "deferred", "wontfix"],
229
- approved: ["open", "resolved", "deferred"],
230
- deferred: ["open", "approved", "wontfix"],
231
- resolved: ["open", "approved"],
232
- wontfix: ["open", "deferred"]
236
+ const QA_LIFECYCLE_TRANSITIONS = {
237
+ new: ["triaged"],
238
+ triaged: ["assigned", "deferred", "rejected", "duplicate"],
239
+ assigned: ["in_progress", "deferred"],
240
+ in_progress: ["fixed", "deferred"],
241
+ fixed: ["pending_retest"],
242
+ pending_retest: ["verified", "reopened"],
243
+ verified: ["closed", "reopened"],
244
+ closed: ["reopened"],
245
+ rejected: ["reopened"],
246
+ duplicate: ["reopened"],
247
+ reopened: ["triaged", "assigned", "in_progress"],
248
+ deferred: ["triaged"]
233
249
  };
234
250
  function useStatusTransition(currentStatus) {
235
- const allowedTargets = ALLOWED_TRANSITIONS[currentStatus] ?? [];
251
+ const allowedTargets = [...QA_LIFECYCLE_TRANSITIONS[currentStatus] ?? []];
236
252
  return { allowedTargets };
237
253
  }
238
254
  const STATUS_OPTIONS = [
@@ -266,7 +282,7 @@ const FeedbackUpdatePage = React.forwardRef(
266
282
  const [item, setItem] = React.useState(null);
267
283
  const [loading, setLoading] = React.useState(true);
268
284
  const [saving, setSaving] = React.useState(false);
269
- const [status, setStatus] = React.useState("open");
285
+ const [status, setStatus] = React.useState("new");
270
286
  const [type, setType] = React.useState("bug");
271
287
  const [scope, setScope] = React.useState("");
272
288
  const [size, setSize] = React.useState("");
@@ -300,6 +316,10 @@ const FeedbackUpdatePage = React.forwardRef(
300
316
  const isDirty = item && (status !== item.status || type !== item.type || scope !== (item.scope ?? "") || size !== (item.size ?? "") || assignedTo !== (item.assignedTo ?? "") || tags !== (item.tags?.join(", ") ?? ""));
301
317
  const handleSave = async () => {
302
318
  if (!item) return;
319
+ if (!updateItem) {
320
+ console.warn("FeedbackUpdatePage: updateItem prop not provided. This component is deprecated.");
321
+ return;
322
+ }
303
323
  setSaving(true);
304
324
  try {
305
325
  const patch = { lastModifiedAt: (/* @__PURE__ */ new Date()).toISOString() };
@@ -310,23 +330,10 @@ const FeedbackUpdatePage = React.forwardRef(
310
330
  if (assignedTo !== (item.assignedTo ?? "")) patch.assignedTo = assignedTo || void 0;
311
331
  const newTags = tags.split(",").map((t) => t.trim()).filter(Boolean);
312
332
  if (JSON.stringify(newTags) !== JSON.stringify(item.tags ?? [])) patch.tags = newTags;
313
- if (status === "resolved" && item.status !== "resolved") {
333
+ if (status === "closed" && item.status !== "closed") {
314
334
  patch.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
315
335
  patch.resolvedBy = currentUser?.userId ?? "unknown";
316
336
  }
317
- if (classificationChanged && correctionNote) {
318
- patch.corrections = [
319
- ...item.corrections ?? [],
320
- {
321
- field: [type !== item.type && "type", scope !== (item.scope ?? "") && "scope", size !== (item.size ?? "") && "size"].filter(Boolean).join(", "),
322
- before: { type: item.type, scope: item.scope, size: item.size },
323
- after: { type, scope: scope || void 0, size: size || void 0 },
324
- correctedBy: currentUser?.userId ?? "unknown",
325
- correctedAt: (/* @__PURE__ */ new Date()).toISOString(),
326
- reason: correctionNote
327
- }
328
- ];
329
- }
330
337
  await updateItem(feedbackId, patch);
331
338
  onNavigate(`/feedback/${feedbackId}`);
332
339
  } finally {
@@ -346,7 +353,7 @@ const FeedbackUpdatePage = React.forwardRef(
346
353
  Button,
347
354
  {
348
355
  onClick: handleSave,
349
- disabled: saving || !isDirty || !!classificationChanged && !correctionNote,
356
+ disabled: saving || !isDirty,
350
357
  children: saving ? "Saving…" : "Save changes"
351
358
  }
352
359
  )
@@ -441,12 +448,288 @@ const FeedbackUpdatePage = React.forwardRef(
441
448
  }
442
449
  );
443
450
  FeedbackUpdatePage.displayName = "FeedbackUpdatePage";
451
+ function ActivityTimeline({ activity, className }) {
452
+ if (activity.length === 0) {
453
+ return /* @__PURE__ */ jsx("div", { "data-testid": "activity-timeline", className, children: /* @__PURE__ */ jsx("div", { "data-testid": "activity-empty-state", children: "No activity yet." }) });
454
+ }
455
+ return /* @__PURE__ */ jsx("div", { "data-testid": "activity-timeline", className, children: activity.map((entry) => /* @__PURE__ */ jsxs("div", { "data-testid": "activity-entry", children: [
456
+ /* @__PURE__ */ jsx("span", { "data-testid": "activity-kind-badge", children: entry.kind.replace(/_/g, " ") }),
457
+ /* @__PURE__ */ jsx("span", { "data-testid": "activity-actor", children: entry.actor.displayName }),
458
+ /* @__PURE__ */ jsx("span", { "data-testid": "activity-timestamp", children: entry.occurredAt }),
459
+ entry.body !== void 0 && /* @__PURE__ */ jsx("span", { "data-testid": "activity-body", children: entry.body })
460
+ ] }, entry.id)) });
461
+ }
462
+ const AnnotationComposer = React.forwardRef(
463
+ (props, ref) => {
464
+ const { feedbackId, currentUser, can: can2, appendActivity, appSource, onAppended } = props;
465
+ const canComment = can2("feedback:comment");
466
+ const canAnnotate = can2("feedback:annotate");
467
+ const [body, setBody] = React.useState("");
468
+ const [conflictReason, setConflictReason] = React.useState(void 0);
469
+ const [submitting, setSubmitting] = React.useState(false);
470
+ if (!canComment && !canAnnotate) return null;
471
+ const submit = async (kind) => {
472
+ setSubmitting(true);
473
+ setConflictReason(void 0);
474
+ const entry = {
475
+ id: crypto.randomUUID(),
476
+ actor: { userId: currentUser.userId, displayName: currentUser.name ?? currentUser.userId },
477
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
478
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
479
+ source: appSource,
480
+ kind,
481
+ body,
482
+ schemaVersion: 1
483
+ };
484
+ const result = await appendActivity(feedbackId, entry);
485
+ if (result.success) {
486
+ setBody("");
487
+ setConflictReason(void 0);
488
+ onAppended?.();
489
+ } else {
490
+ setConflictReason(result.conflictReason ?? result.error ?? "Unknown error");
491
+ }
492
+ setSubmitting(false);
493
+ };
494
+ const isDisabled = body.trim().length === 0 || submitting;
495
+ return /* @__PURE__ */ jsxs("div", { ref, "data-testid": "annotation-composer", className: "space-y-3", children: [
496
+ /* @__PURE__ */ jsx(
497
+ "textarea",
498
+ {
499
+ "data-testid": "annotation-textarea",
500
+ value: body,
501
+ onChange: (e) => setBody(e.target.value),
502
+ placeholder: "Add a comment, note, question, or answer...",
503
+ className: "w-full min-h-[100px] p-3 border border-input rounded-md bg-background text-sm resize-y focus:outline-none focus:ring-2 focus:ring-ring"
504
+ }
505
+ ),
506
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
507
+ canComment && /* @__PURE__ */ jsx(
508
+ Button,
509
+ {
510
+ "data-testid": "annotation-comment-button",
511
+ disabled: isDisabled,
512
+ onClick: () => {
513
+ void submit("comment");
514
+ },
515
+ variant: "default",
516
+ size: "sm",
517
+ children: "Comment"
518
+ }
519
+ ),
520
+ canAnnotate && /* @__PURE__ */ jsxs(Fragment, { children: [
521
+ /* @__PURE__ */ jsx(
522
+ Button,
523
+ {
524
+ "data-testid": "annotation-note-button",
525
+ disabled: isDisabled,
526
+ onClick: () => {
527
+ void submit("note");
528
+ },
529
+ variant: "outline",
530
+ size: "sm",
531
+ children: "Note"
532
+ }
533
+ ),
534
+ /* @__PURE__ */ jsx(
535
+ Button,
536
+ {
537
+ "data-testid": "annotation-question-button",
538
+ disabled: isDisabled,
539
+ onClick: () => {
540
+ void submit("question");
541
+ },
542
+ variant: "outline",
543
+ size: "sm",
544
+ children: "Question"
545
+ }
546
+ ),
547
+ /* @__PURE__ */ jsx(
548
+ Button,
549
+ {
550
+ "data-testid": "annotation-answer-button",
551
+ disabled: isDisabled,
552
+ onClick: () => {
553
+ void submit("answer");
554
+ },
555
+ variant: "outline",
556
+ size: "sm",
557
+ children: "Answer"
558
+ }
559
+ )
560
+ ] })
561
+ ] }),
562
+ conflictReason !== void 0 && /* @__PURE__ */ jsx("div", { "data-testid": "annotation-conflict-reason", className: "text-sm text-destructive", children: conflictReason })
563
+ ] });
564
+ }
565
+ );
566
+ AnnotationComposer.displayName = "AnnotationComposer";
567
+ const CAPABILITY_MAP = {
568
+ triaged: "feedback:triage",
569
+ assigned: "feedback:assign",
570
+ in_progress: "feedback:assign",
571
+ fixed: "feedback:resolve",
572
+ pending_retest: "feedback:resolve",
573
+ verified: "feedback:verify",
574
+ closed: "feedback:resolve",
575
+ deferred: "feedback:triage",
576
+ rejected: "feedback:reject",
577
+ duplicate: "feedback:reject",
578
+ reopened: "feedback:reopen"
579
+ };
580
+ function formatStatusLabel(status) {
581
+ return status.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
582
+ }
583
+ const LifecycleControls = React.forwardRef(
584
+ (props, ref) => {
585
+ const { feedbackId, item, can: can2, transitionStatus, onTransitioned } = props;
586
+ const { allowedTargets } = useStatusTransition(item.status);
587
+ const authorizedTargets = allowedTargets.filter((target) => {
588
+ const cap = CAPABILITY_MAP[target];
589
+ return cap !== void 0 && can2(cap);
590
+ });
591
+ const [duplicateInputVisible, setDuplicateInputVisible] = React.useState(false);
592
+ const [duplicateOf, setDuplicateOf] = React.useState("");
593
+ const [conflictReason, setConflictReason] = React.useState(void 0);
594
+ const [submitting, setSubmitting] = React.useState(false);
595
+ if (authorizedTargets.length === 0) return null;
596
+ const handleTransition = async (target) => {
597
+ if (target === "duplicate") {
598
+ if (!duplicateInputVisible) {
599
+ setDuplicateInputVisible(true);
600
+ return;
601
+ }
602
+ if (duplicateOf.trim().length === 0) {
603
+ setConflictReason("Duplicate target ID is required");
604
+ return;
605
+ }
606
+ setSubmitting(true);
607
+ setConflictReason(void 0);
608
+ const opts2 = {
609
+ currentStatus: item.status,
610
+ duplicateOf,
611
+ duplicateReporters: item.reporters
612
+ };
613
+ const result2 = await transitionStatus(feedbackId, "duplicate", opts2);
614
+ if (result2.success) {
615
+ setDuplicateInputVisible(false);
616
+ setDuplicateOf("");
617
+ setConflictReason(void 0);
618
+ onTransitioned?.();
619
+ } else {
620
+ setConflictReason(result2.conflictReason ?? result2.error ?? "Unknown error");
621
+ }
622
+ setSubmitting(false);
623
+ return;
624
+ }
625
+ setSubmitting(true);
626
+ setConflictReason(void 0);
627
+ const opts = {
628
+ currentStatus: item.status
629
+ };
630
+ const result = await transitionStatus(feedbackId, target, opts);
631
+ if (result.success) {
632
+ setConflictReason(void 0);
633
+ onTransitioned?.();
634
+ } else {
635
+ setConflictReason(result.conflictReason ?? result.error ?? "Unknown error");
636
+ }
637
+ setSubmitting(false);
638
+ };
639
+ return /* @__PURE__ */ jsxs("div", { ref, "data-testid": "lifecycle-controls", className: "space-y-3", children: [
640
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 flex-wrap", children: authorizedTargets.map((target) => /* @__PURE__ */ jsx(
641
+ Button,
642
+ {
643
+ "data-testid": `lifecycle-${target}-button`,
644
+ onClick: () => {
645
+ void handleTransition(target);
646
+ },
647
+ disabled: submitting,
648
+ variant: target === "rejected" || target === "duplicate" ? "destructive" : "default",
649
+ size: "sm",
650
+ children: formatStatusLabel(target)
651
+ },
652
+ target
653
+ )) }),
654
+ duplicateInputVisible && /* @__PURE__ */ jsxs("div", { "data-testid": "duplicate-input-container", className: "space-y-2", children: [
655
+ /* @__PURE__ */ jsx(
656
+ "input",
657
+ {
658
+ "data-testid": "duplicate-of-input",
659
+ value: duplicateOf,
660
+ onChange: (e) => setDuplicateOf(e.target.value),
661
+ placeholder: "Canonical feedback ID (e.g., FB-2026-11-01-abc12345)",
662
+ className: "w-full p-2 border border-input rounded-md bg-background text-sm focus:outline-none focus:ring-2 focus:ring-ring"
663
+ }
664
+ ),
665
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
666
+ /* @__PURE__ */ jsx(
667
+ Button,
668
+ {
669
+ onClick: () => {
670
+ void handleTransition("duplicate");
671
+ },
672
+ disabled: submitting || duplicateOf.trim().length === 0,
673
+ size: "sm",
674
+ children: "Confirm Duplicate"
675
+ }
676
+ ),
677
+ /* @__PURE__ */ jsx(
678
+ Button,
679
+ {
680
+ onClick: () => {
681
+ setDuplicateInputVisible(false);
682
+ setDuplicateOf("");
683
+ setConflictReason(void 0);
684
+ },
685
+ disabled: submitting,
686
+ variant: "outline",
687
+ size: "sm",
688
+ children: "Cancel"
689
+ }
690
+ )
691
+ ] })
692
+ ] }),
693
+ conflictReason !== void 0 && /* @__PURE__ */ jsx("div", { "data-testid": "lifecycle-conflict-reason", className: "text-sm text-destructive", children: conflictReason })
694
+ ] });
695
+ }
696
+ );
697
+ LifecycleControls.displayName = "LifecycleControls";
698
+ const CAPABILITIES = {
699
+ "feedback:comment": "feedback:comment",
700
+ "feedback:annotate": "feedback:annotate",
701
+ "feedback:triage": "feedback:triage",
702
+ "feedback:assign": "feedback:assign",
703
+ "feedback:resolve": "feedback:resolve",
704
+ "feedback:verify": "feedback:verify",
705
+ "feedback:reopen": "feedback:reopen",
706
+ "feedback:reject": "feedback:reject",
707
+ "feedback:delete": "feedback:delete"
708
+ };
709
+ function can(capability, item, user) {
710
+ if (user.capabilities.has(capability)) {
711
+ return true;
712
+ }
713
+ const isSubmitter = user.userId === item.submittedBy;
714
+ const isReporter = item.reporters.some((r) => r.userId === user.userId);
715
+ if ((isSubmitter || isReporter) && (capability === "feedback:comment" || capability === "feedback:annotate")) {
716
+ return true;
717
+ }
718
+ return false;
719
+ }
444
720
  export {
721
+ ActivityTimeline,
722
+ AnnotationComposer,
723
+ CAPABILITIES,
445
724
  FeedbackDetailPage,
446
725
  FeedbackListPage,
447
726
  FeedbackUpdatePage,
727
+ LifecycleControls,
448
728
  StatusBadge,
449
729
  TypeBadge,
730
+ can,
731
+ d as createIngestAnnotateClient,
732
+ c as normaliseStoredItem,
450
733
  useStatusTransition
451
734
  };
452
735
  //# sourceMappingURL=index.js.map