@burdenoff/microfe-planmagnet 2026.928.10 → 2026.928.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"AutomationRuleFields.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/automation/AutomationRuleFields.tsx"],"names":[],"mappings":"AASA,OAAO,EAWL,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,uBAAuB,CAAC;AAE/B,+DAA+D;AAC/D,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,WAAW,CAAC;IACzB,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAoED,8GAA8G;AAC9G,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;CACxC,2CA0EA;AAED,8GAA8G;AAC9G,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,QAAQ,GACT,EAAE;IACD,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,QAAQ,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;CAC3C,2CA4EA;AAED,sGAAsG;AACtG,wBAAgB,oBAAoB,CAAC,EACnC,IAAI,EACJ,QAAQ,EACR,aAAqB,GACtB,EAAE;IACD,IAAI,EAAE,uBAAuB,CAAC;IAC9B,QAAQ,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,IAAI,CAAC;IAClD,0FAA0F;IAC1F,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,2CA6FA"}
1
+ {"version":3,"file":"AutomationRuleFields.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/automation/AutomationRuleFields.tsx"],"names":[],"mappings":"AASA,OAAO,EAWL,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,uBAAuB,CAAC;AAE/B,+DAA+D;AAC/D,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,WAAW,CAAC;IACzB,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAyED,8GAA8G;AAC9G,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;CACxC,2CA0EA;AAED,8GAA8G;AAC9G,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,QAAQ,GACT,EAAE;IACD,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,QAAQ,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;CAC3C,2CA4EA;AAED,sGAAsG;AACtG,wBAAgB,oBAAoB,CAAC,EACnC,IAAI,EACJ,QAAQ,EACR,aAAqB,GACtB,EAAE;IACD,IAAI,EAAE,uBAAuB,CAAC;IAC9B,QAAQ,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,IAAI,CAAC;IAClD,0FAA0F;IAC1F,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,2CA6FA"}
@@ -5,15 +5,15 @@ import { Plus as p, X as m } from "lucide-react";
5
5
  import { jsx as h, jsxs as g } from "react/jsx-runtime";
6
6
  import { Button as _, Input as v } from "@burdenoff/fe-libs/ui";
7
7
  //#region src/planmagnet/automation/AutomationRuleFields.tsx
8
- var y = "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60", b = "text-foreground";
9
- function x({ action: e, onChange: a }) {
8
+ var y = "rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60", b = `mt-1 w-full ${y}`, x = "text-foreground";
9
+ function S({ action: e, onChange: a }) {
10
10
  switch (e.type) {
11
11
  case "ASSIGN_TO_USER": return /* @__PURE__ */ h(t, {
12
12
  value: e.value,
13
13
  onChange: a
14
14
  });
15
15
  case "SET_STATUS": return /* @__PURE__ */ g("select", {
16
- className: y,
16
+ className: `${y} w-full`,
17
17
  value: e.value,
18
18
  onChange: (e) => a(e.target.value),
19
19
  children: [/* @__PURE__ */ h("option", {
@@ -25,7 +25,7 @@ function x({ action: e, onChange: a }) {
25
25
  }, e))]
26
26
  });
27
27
  case "SET_PRIORITY": return /* @__PURE__ */ g("select", {
28
- className: y,
28
+ className: `${y} w-full`,
29
29
  value: e.value,
30
30
  onChange: (e) => a(e.target.value),
31
31
  children: [/* @__PURE__ */ h("option", {
@@ -38,14 +38,14 @@ function x({ action: e, onChange: a }) {
38
38
  });
39
39
  default: return /* @__PURE__ */ h(v, {
40
40
  "aria-label": "Label",
41
- className: b,
41
+ className: x,
42
42
  placeholder: "e.g. needs-review",
43
43
  value: e.value,
44
44
  onChange: (e) => a(e.target.value)
45
45
  });
46
46
  }
47
47
  }
48
- function S({ actions: e, onChange: t }) {
48
+ function C({ actions: e, onChange: t }) {
49
49
  let n = (n, r) => t(e.map((e, t) => t === n ? {
50
50
  ...e,
51
51
  ...r
@@ -62,7 +62,7 @@ function S({ actions: e, onChange: t }) {
62
62
  children: [
63
63
  /* @__PURE__ */ h("select", {
64
64
  "aria-label": "Action type",
65
- className: `${y} mt-0 w-40 shrink-0`,
65
+ className: `${y} w-40 shrink-0`,
66
66
  value: e.type,
67
67
  onChange: (r) => n(t, {
68
68
  type: r.target.value,
@@ -75,7 +75,7 @@ function S({ actions: e, onChange: t }) {
75
75
  }),
76
76
  /* @__PURE__ */ h("div", {
77
77
  className: "min-w-0 flex-1",
78
- children: /* @__PURE__ */ h(x, {
78
+ children: /* @__PURE__ */ h(S, {
79
79
  action: e,
80
80
  onChange: (e) => n(t, { value: e })
81
81
  })
@@ -113,7 +113,7 @@ function S({ actions: e, onChange: t }) {
113
113
  ]
114
114
  });
115
115
  }
116
- function C({ conditions: e, onChange: t }) {
116
+ function w({ conditions: e, onChange: t }) {
117
117
  let n = (n, r) => t(e.map((e, t) => t === n ? {
118
118
  ...e,
119
119
  ...r
@@ -127,14 +127,14 @@ function C({ conditions: e, onChange: t }) {
127
127
  /* @__PURE__ */ h(v, {
128
128
  "aria-label": "Condition field",
129
129
  list: "automation-condition-fields",
130
- className: `w-36 shrink-0 ${b}`,
130
+ className: `w-36 shrink-0 ${x}`,
131
131
  placeholder: "field",
132
132
  value: e.field,
133
133
  onChange: (e) => n(t, { field: e.target.value })
134
134
  }),
135
135
  /* @__PURE__ */ h("select", {
136
136
  "aria-label": "Condition operator",
137
- className: `${y} mt-0 w-32 shrink-0`,
137
+ className: `${y} w-32 shrink-0`,
138
138
  value: e.operator,
139
139
  onChange: (e) => n(t, { operator: e.target.value }),
140
140
  children: l.map((e) => /* @__PURE__ */ h("option", {
@@ -144,7 +144,7 @@ function C({ conditions: e, onChange: t }) {
144
144
  }),
145
145
  e.operator !== "exists" && /* @__PURE__ */ h(v, {
146
146
  "aria-label": "Condition value",
147
- className: `min-w-0 flex-1 ${b}`,
147
+ className: `min-w-0 flex-1 ${x}`,
148
148
  placeholder: e.operator === "in" || e.operator === "notIn" ? "comma, separated, values" : "value",
149
149
  value: e.value,
150
150
  onChange: (e) => n(t, { value: e.target.value })
@@ -182,7 +182,7 @@ function C({ conditions: e, onChange: t }) {
182
182
  ]
183
183
  });
184
184
  }
185
- function w({ form: t, onChange: n, projectLocked: r = !1 }) {
185
+ function T({ form: t, onChange: n, projectLocked: r = !1 }) {
186
186
  return /* @__PURE__ */ g("div", {
187
187
  className: "space-y-4",
188
188
  children: [
@@ -192,7 +192,7 @@ function w({ form: t, onChange: n, projectLocked: r = !1 }) {
192
192
  children: "Name"
193
193
  }), /* @__PURE__ */ h(v, {
194
194
  id: "automation-name",
195
- className: `mt-1 ${b}`,
195
+ className: `mt-1 ${x}`,
196
196
  value: t.name,
197
197
  onChange: (e) => n({
198
198
  ...t,
@@ -241,7 +241,7 @@ function w({ form: t, onChange: n, projectLocked: r = !1 }) {
241
241
  }),
242
242
  /* @__PURE__ */ h("select", {
243
243
  id: "automation-trigger",
244
- className: y,
244
+ className: b,
245
245
  value: t.triggerType,
246
246
  onChange: (e) => n({
247
247
  ...t,
@@ -275,7 +275,7 @@ function w({ form: t, onChange: n, projectLocked: r = !1 }) {
275
275
  className: "mb-2 text-xs text-muted-foreground",
276
276
  children: "All conditions must match. Leave empty to match every event."
277
277
  }),
278
- /* @__PURE__ */ h(C, {
278
+ /* @__PURE__ */ h(w, {
279
279
  conditions: t.conditions,
280
280
  onChange: (e) => n({
281
281
  ...t,
@@ -292,7 +292,7 @@ function w({ form: t, onChange: n, projectLocked: r = !1 }) {
292
292
  className: "mb-2 text-xs text-muted-foreground",
293
293
  children: "Run in order when the conditions match."
294
294
  }),
295
- /* @__PURE__ */ h(S, {
295
+ /* @__PURE__ */ h(C, {
296
296
  actions: t.actions,
297
297
  onChange: (e) => n({
298
298
  ...t,
@@ -304,6 +304,6 @@ function w({ form: t, onChange: n, projectLocked: r = !1 }) {
304
304
  });
305
305
  }
306
306
  //#endregion
307
- export { w as AutomationRuleFields };
307
+ export { T as AutomationRuleFields };
308
308
 
309
309
  //# sourceMappingURL=AutomationRuleFields.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AutomationRuleFields.js","names":[],"sources":["../../../src/planmagnet/automation/AutomationRuleFields.tsx"],"sourcesContent":["import { Button, Input } from '@burdenoff/fe-libs/ui';\nimport { Plus, X } from 'lucide-react';\nimport {\n AssigneeSelect,\n WORK_ITEM_PRIORITIES,\n WORK_ITEM_STATUSES,\n enumLabel,\n} from '../controls/WorkItemControls';\nimport { ProjectSelect } from '../controls/Pickers';\nimport {\n ACTION_TYPES,\n ACTION_TYPE_LABEL,\n AUTOMATIC_TRIGGER_TYPES,\n CONDITION_FIELDS,\n CONDITION_OPERATORS,\n CONDITION_OPERATOR_LABEL,\n TRIGGER_TYPES,\n TRIGGER_TYPE_LABEL,\n type ActionType,\n type ConditionOperator,\n type RuleAction,\n type RuleCondition,\n type TriggerType,\n} from './automationRuleTypes';\n\n/** The editable shape both the create and edit pages share. */\nexport interface AutomationRuleFormState {\n name: string;\n description: string;\n projectId: string;\n triggerType: TriggerType;\n conditions: RuleCondition[];\n actions: RuleAction[];\n}\n\nconst fieldClass =\n 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\n// fe-libs' <Input> sets its own text color via `text-text-primary` (a semantic token from its own theme\n// scope), but every Input below sits in a row that also carries this file's OTHER token family\n// (`fieldClass` above uses `border-input`/`bg-background`, and the rest of this microfe standardizes on\n// `text-foreground`/`bg-background`/`border-input`). Passing this class merges via `cn()` (tailwind-merge),\n// which recognizes both `text-text-primary` and `text-foreground` as the same \"text color\" group and keeps\n// only the last one — so this pins every value/label an editor renders here to the token this microfe's own\n// pages already render correctly with, instead of leaving it to whichever token happens to win the cascade.\nconst inputTextClass = 'text-foreground';\n\nfunction ActionValueEditor({\n action,\n onChange,\n}: {\n action: RuleAction;\n onChange: (value: string) => void;\n}) {\n switch (action.type) {\n case 'ASSIGN_TO_USER':\n return <AssigneeSelect value={action.value} onChange={onChange} />;\n case 'SET_STATUS':\n return (\n <select\n className={fieldClass}\n value={action.value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">Choose a status</option>\n {WORK_ITEM_STATUSES.map((s) => (\n <option key={s} value={s}>\n {enumLabel(s)}\n </option>\n ))}\n </select>\n );\n case 'SET_PRIORITY':\n return (\n <select\n className={fieldClass}\n value={action.value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">Choose a priority</option>\n {WORK_ITEM_PRIORITIES.map((p) => (\n <option key={p} value={p}>\n {enumLabel(p)}\n </option>\n ))}\n </select>\n );\n case 'ADD_LABEL':\n default:\n return (\n <Input\n aria-label=\"Label\"\n className={inputTextClass}\n placeholder=\"e.g. needs-review\"\n value={action.value}\n onChange={(e) => onChange(e.target.value)}\n />\n );\n }\n}\n\n/** The actions builder: only the four types the engine can actually dispatch (see automationRuleTypes.ts). */\nexport function ActionsEditor({\n actions,\n onChange,\n}: {\n actions: RuleAction[];\n onChange: (next: RuleAction[]) => void;\n}) {\n const update = (index: number, patch: Partial<RuleAction>) =>\n onChange(actions.map((a, i) => (i === index ? { ...a, ...patch } : a)));\n const remove = (index: number) => onChange(actions.filter((_, i) => i !== index));\n const add = () => onChange([...actions, { type: 'ASSIGN_TO_USER', value: '' }]);\n\n return (\n <div className=\"space-y-2\">\n {actions.map((action, index) => {\n // A rule created before this UI existed (or via the API directly) can carry an action type this\n // builder does not offer (e.g. POST_COMMENT, which the engine never dispatches — see\n // automationRuleTypes.ts). Keep it selectable and visible rather than silently swapping it out from\n // under the user the moment they open the edit page.\n const known = (ACTION_TYPES as readonly string[]).includes(action.type);\n const typeOptions = known ? ACTION_TYPES : [action.type, ...ACTION_TYPES];\n return (\n <div key={index} className=\"space-y-1\">\n <div className=\"flex items-start gap-2\">\n <select\n aria-label=\"Action type\"\n className={`${fieldClass} mt-0 w-40 shrink-0`}\n value={action.type}\n onChange={(e) =>\n update(index, {\n type: e.target.value as ActionType,\n value: e.target.value === action.type ? action.value : '',\n })\n }\n >\n {typeOptions.map((t) => (\n <option key={t} value={t}>\n {known || t !== action.type ? (ACTION_TYPE_LABEL[t as ActionType] ?? t) : t}\n </option>\n ))}\n </select>\n <div className=\"min-w-0 flex-1\">\n <ActionValueEditor action={action} onChange={(value) => update(index, { value })} />\n </div>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n aria-label=\"Remove action\"\n onClick={() => remove(index)}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n {!known && (\n <p className=\"text-xs text-muted-foreground\">\n This action type is not run by the engine yet; the rule keeps it but never\n dispatches it.\n </p>\n )}\n </div>\n );\n })}\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={add}\n data-testid=\"automation-add-action\"\n >\n <Plus className=\"mr-1 h-4 w-4\" />\n Add action\n </Button>\n {actions.length === 0 && (\n <p className=\"text-xs text-muted-foreground\">\n A rule with no actions still records that it ran, but changes nothing.\n </p>\n )}\n </div>\n );\n}\n\n/** The conditions builder. A rule with no conditions always matches — the gate is opt-in (engine default). */\nexport function ConditionsEditor({\n conditions,\n onChange,\n}: {\n conditions: RuleCondition[];\n onChange: (next: RuleCondition[]) => void;\n}) {\n const update = (index: number, patch: Partial<RuleCondition>) =>\n onChange(conditions.map((c, i) => (i === index ? { ...c, ...patch } : c)));\n const remove = (index: number) => onChange(conditions.filter((_, i) => i !== index));\n const add = () => onChange([...conditions, { field: 'status', operator: 'eq', value: '' }]);\n\n return (\n <div className=\"space-y-2\">\n {conditions.map((condition, index) => (\n <div key={index} className=\"flex items-start gap-2\">\n <Input\n aria-label=\"Condition field\"\n list=\"automation-condition-fields\"\n className={`w-36 shrink-0 ${inputTextClass}`}\n placeholder=\"field\"\n value={condition.field}\n onChange={(e) => update(index, { field: e.target.value })}\n />\n <select\n aria-label=\"Condition operator\"\n className={`${fieldClass} mt-0 w-32 shrink-0`}\n value={condition.operator}\n onChange={(e) => update(index, { operator: e.target.value as ConditionOperator })}\n >\n {CONDITION_OPERATORS.map((op) => (\n <option key={op} value={op}>\n {CONDITION_OPERATOR_LABEL[op]}\n </option>\n ))}\n </select>\n {condition.operator !== 'exists' && (\n <Input\n aria-label=\"Condition value\"\n className={`min-w-0 flex-1 ${inputTextClass}`}\n placeholder={\n condition.operator === 'in' || condition.operator === 'notIn'\n ? 'comma, separated, values'\n : 'value'\n }\n value={condition.value}\n onChange={(e) => update(index, { value: e.target.value })}\n />\n )}\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n aria-label=\"Remove condition\"\n onClick={() => remove(index)}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n ))}\n <datalist id=\"automation-condition-fields\">\n {CONDITION_FIELDS.map((f) => (\n <option key={f} value={f} />\n ))}\n </datalist>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={add}\n data-testid=\"automation-add-condition\"\n >\n <Plus className=\"mr-1 h-4 w-4\" />\n Add condition\n </Button>\n {conditions.length === 0 && (\n <p className=\"text-xs text-muted-foreground\">\n No conditions: the rule matches every event.\n </p>\n )}\n </div>\n );\n}\n\n/** Name, description, project, trigger, conditions and actions — the fields create and edit share. */\nexport function AutomationRuleFields({\n form,\n onChange,\n projectLocked = false,\n}: {\n form: AutomationRuleFormState;\n onChange: (next: AutomationRuleFormState) => void;\n /** The edit page cannot change the project (the API accepts no `projectId` on update). */\n projectLocked?: boolean;\n}) {\n return (\n <div className=\"space-y-4\">\n <div>\n <label htmlFor=\"automation-name\" className=\"text-sm font-medium\">\n Name\n </label>\n <Input\n id=\"automation-name\"\n className={`mt-1 ${inputTextClass}`}\n value={form.name}\n onChange={(e) => onChange({ ...form, name: e.target.value })}\n placeholder=\"e.g. Assign new bugs to triage\"\n />\n </div>\n <div>\n <label htmlFor=\"automation-description\" className=\"text-sm font-medium\">\n Description\n </label>\n <textarea\n id=\"automation-description\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]\"\n value={form.description}\n onChange={(e) => onChange({ ...form, description: e.target.value })}\n placeholder=\"What this rule is for\"\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"automation-project\" className=\"text-sm font-medium\">\n Project\n </label>\n {projectLocked ? (\n <p className=\"mt-1 text-sm text-muted-foreground\">\n A rule&apos;s project cannot be changed after it is created.\n </p>\n ) : (\n <div className=\"mt-1\">\n <ProjectSelect\n id=\"automation-project\"\n value={form.projectId}\n onChange={(projectId) => onChange({ ...form, projectId })}\n />\n </div>\n )}\n </div>\n <div>\n <label htmlFor=\"automation-trigger\" className=\"text-sm font-medium\">\n Trigger\n </label>\n <select\n id=\"automation-trigger\"\n className={fieldClass}\n value={form.triggerType}\n onChange={(e) => onChange({ ...form, triggerType: e.target.value as TriggerType })}\n >\n {TRIGGER_TYPES.map((t) => (\n <option key={t} value={t}>\n {TRIGGER_TYPE_LABEL[t]}\n {AUTOMATIC_TRIGGER_TYPES.has(t) ? '' : ' (not yet automatic)'}\n </option>\n ))}\n </select>\n {!AUTOMATIC_TRIGGER_TYPES.has(form.triggerType) && (\n <p className=\"mt-1 text-xs text-muted-foreground\">\n This trigger is saved but does not fire on its own yet — use{' '}\n <span className=\"font-medium\">Run now</span> to run it against a work item.\n </p>\n )}\n </div>\n </div>\n <div>\n <p className=\"text-sm font-medium\">Conditions</p>\n <p className=\"mb-2 text-xs text-muted-foreground\">\n All conditions must match. Leave empty to match every event.\n </p>\n <ConditionsEditor\n conditions={form.conditions}\n onChange={(conditions) => onChange({ ...form, conditions })}\n />\n </div>\n <div>\n <p className=\"text-sm font-medium\">Actions</p>\n <p className=\"mb-2 text-xs text-muted-foreground\">\n Run in order when the conditions match.\n </p>\n <ActionsEditor\n actions={form.actions}\n onChange={(actions) => onChange({ ...form, actions })}\n />\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AutomationRuleFields.js","names":[],"sources":["../../../src/planmagnet/automation/AutomationRuleFields.tsx"],"sourcesContent":["import { Button, Input } from '@burdenoff/fe-libs/ui';\nimport { Plus, X } from 'lucide-react';\nimport {\n AssigneeSelect,\n WORK_ITEM_PRIORITIES,\n WORK_ITEM_STATUSES,\n enumLabel,\n} from '../controls/WorkItemControls';\nimport { ProjectSelect } from '../controls/Pickers';\nimport {\n ACTION_TYPES,\n ACTION_TYPE_LABEL,\n AUTOMATIC_TRIGGER_TYPES,\n CONDITION_FIELDS,\n CONDITION_OPERATORS,\n CONDITION_OPERATOR_LABEL,\n TRIGGER_TYPES,\n TRIGGER_TYPE_LABEL,\n type ActionType,\n type ConditionOperator,\n type RuleAction,\n type RuleCondition,\n type TriggerType,\n} from './automationRuleTypes';\n\n/** The editable shape both the create and edit pages share. */\nexport interface AutomationRuleFormState {\n name: string;\n description: string;\n projectId: string;\n triggerType: TriggerType;\n conditions: RuleCondition[];\n actions: RuleAction[];\n}\n\n// No width here: a native <select> does not go through tailwind-merge, so a `w-full` in a shared class and a\n// `w-32` added next to it BOTH land on the element and the stylesheet order decides — `w-full` won, the\n// operator / action-type selects took the whole row and squeezed the value input beside them to ~26px, so\n// nobody could see the value they typed. Rows size their own selects; `fieldClass` is the full-width form.\nconst fieldBaseClass =\n 'rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\nconst fieldClass = `mt-1 w-full ${fieldBaseClass}`;\n\n// fe-libs' <Input> sets its own text color via `text-text-primary` (a semantic token from its own theme\n// scope), but every Input below sits in a row that also carries this file's OTHER token family\n// (`fieldClass` above uses `border-input`/`bg-background`, and the rest of this microfe standardizes on\n// `text-foreground`/`bg-background`/`border-input`). Passing this class merges via `cn()` (tailwind-merge),\n// which recognizes both `text-text-primary` and `text-foreground` as the same \"text color\" group and keeps\n// only the last one — so this pins every value/label an editor renders here to the token this microfe's own\n// pages already render correctly with, instead of leaving it to whichever token happens to win the cascade.\nconst inputTextClass = 'text-foreground';\n\nfunction ActionValueEditor({\n action,\n onChange,\n}: {\n action: RuleAction;\n onChange: (value: string) => void;\n}) {\n switch (action.type) {\n case 'ASSIGN_TO_USER':\n return <AssigneeSelect value={action.value} onChange={onChange} />;\n case 'SET_STATUS':\n return (\n <select\n className={`${fieldBaseClass} w-full`}\n value={action.value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">Choose a status</option>\n {WORK_ITEM_STATUSES.map((s) => (\n <option key={s} value={s}>\n {enumLabel(s)}\n </option>\n ))}\n </select>\n );\n case 'SET_PRIORITY':\n return (\n <select\n className={`${fieldBaseClass} w-full`}\n value={action.value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">Choose a priority</option>\n {WORK_ITEM_PRIORITIES.map((p) => (\n <option key={p} value={p}>\n {enumLabel(p)}\n </option>\n ))}\n </select>\n );\n case 'ADD_LABEL':\n default:\n return (\n <Input\n aria-label=\"Label\"\n className={inputTextClass}\n placeholder=\"e.g. needs-review\"\n value={action.value}\n onChange={(e) => onChange(e.target.value)}\n />\n );\n }\n}\n\n/** The actions builder: only the four types the engine can actually dispatch (see automationRuleTypes.ts). */\nexport function ActionsEditor({\n actions,\n onChange,\n}: {\n actions: RuleAction[];\n onChange: (next: RuleAction[]) => void;\n}) {\n const update = (index: number, patch: Partial<RuleAction>) =>\n onChange(actions.map((a, i) => (i === index ? { ...a, ...patch } : a)));\n const remove = (index: number) => onChange(actions.filter((_, i) => i !== index));\n const add = () => onChange([...actions, { type: 'ASSIGN_TO_USER', value: '' }]);\n\n return (\n <div className=\"space-y-2\">\n {actions.map((action, index) => {\n // A rule created before this UI existed (or via the API directly) can carry an action type this\n // builder does not offer (e.g. POST_COMMENT, which the engine never dispatches — see\n // automationRuleTypes.ts). Keep it selectable and visible rather than silently swapping it out from\n // under the user the moment they open the edit page.\n const known = (ACTION_TYPES as readonly string[]).includes(action.type);\n const typeOptions = known ? ACTION_TYPES : [action.type, ...ACTION_TYPES];\n return (\n <div key={index} className=\"space-y-1\">\n <div className=\"flex items-start gap-2\">\n <select\n aria-label=\"Action type\"\n className={`${fieldBaseClass} w-40 shrink-0`}\n value={action.type}\n onChange={(e) =>\n update(index, {\n type: e.target.value as ActionType,\n value: e.target.value === action.type ? action.value : '',\n })\n }\n >\n {typeOptions.map((t) => (\n <option key={t} value={t}>\n {known || t !== action.type ? (ACTION_TYPE_LABEL[t as ActionType] ?? t) : t}\n </option>\n ))}\n </select>\n <div className=\"min-w-0 flex-1\">\n <ActionValueEditor action={action} onChange={(value) => update(index, { value })} />\n </div>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n aria-label=\"Remove action\"\n onClick={() => remove(index)}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n {!known && (\n <p className=\"text-xs text-muted-foreground\">\n This action type is not run by the engine yet; the rule keeps it but never\n dispatches it.\n </p>\n )}\n </div>\n );\n })}\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={add}\n data-testid=\"automation-add-action\"\n >\n <Plus className=\"mr-1 h-4 w-4\" />\n Add action\n </Button>\n {actions.length === 0 && (\n <p className=\"text-xs text-muted-foreground\">\n A rule with no actions still records that it ran, but changes nothing.\n </p>\n )}\n </div>\n );\n}\n\n/** The conditions builder. A rule with no conditions always matches — the gate is opt-in (engine default). */\nexport function ConditionsEditor({\n conditions,\n onChange,\n}: {\n conditions: RuleCondition[];\n onChange: (next: RuleCondition[]) => void;\n}) {\n const update = (index: number, patch: Partial<RuleCondition>) =>\n onChange(conditions.map((c, i) => (i === index ? { ...c, ...patch } : c)));\n const remove = (index: number) => onChange(conditions.filter((_, i) => i !== index));\n const add = () => onChange([...conditions, { field: 'status', operator: 'eq', value: '' }]);\n\n return (\n <div className=\"space-y-2\">\n {conditions.map((condition, index) => (\n <div key={index} className=\"flex items-start gap-2\">\n <Input\n aria-label=\"Condition field\"\n list=\"automation-condition-fields\"\n className={`w-36 shrink-0 ${inputTextClass}`}\n placeholder=\"field\"\n value={condition.field}\n onChange={(e) => update(index, { field: e.target.value })}\n />\n <select\n aria-label=\"Condition operator\"\n className={`${fieldBaseClass} w-32 shrink-0`}\n value={condition.operator}\n onChange={(e) => update(index, { operator: e.target.value as ConditionOperator })}\n >\n {CONDITION_OPERATORS.map((op) => (\n <option key={op} value={op}>\n {CONDITION_OPERATOR_LABEL[op]}\n </option>\n ))}\n </select>\n {condition.operator !== 'exists' && (\n <Input\n aria-label=\"Condition value\"\n className={`min-w-0 flex-1 ${inputTextClass}`}\n placeholder={\n condition.operator === 'in' || condition.operator === 'notIn'\n ? 'comma, separated, values'\n : 'value'\n }\n value={condition.value}\n onChange={(e) => update(index, { value: e.target.value })}\n />\n )}\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n aria-label=\"Remove condition\"\n onClick={() => remove(index)}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n ))}\n <datalist id=\"automation-condition-fields\">\n {CONDITION_FIELDS.map((f) => (\n <option key={f} value={f} />\n ))}\n </datalist>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={add}\n data-testid=\"automation-add-condition\"\n >\n <Plus className=\"mr-1 h-4 w-4\" />\n Add condition\n </Button>\n {conditions.length === 0 && (\n <p className=\"text-xs text-muted-foreground\">\n No conditions: the rule matches every event.\n </p>\n )}\n </div>\n );\n}\n\n/** Name, description, project, trigger, conditions and actions — the fields create and edit share. */\nexport function AutomationRuleFields({\n form,\n onChange,\n projectLocked = false,\n}: {\n form: AutomationRuleFormState;\n onChange: (next: AutomationRuleFormState) => void;\n /** The edit page cannot change the project (the API accepts no `projectId` on update). */\n projectLocked?: boolean;\n}) {\n return (\n <div className=\"space-y-4\">\n <div>\n <label htmlFor=\"automation-name\" className=\"text-sm font-medium\">\n Name\n </label>\n <Input\n id=\"automation-name\"\n className={`mt-1 ${inputTextClass}`}\n value={form.name}\n onChange={(e) => onChange({ ...form, name: e.target.value })}\n placeholder=\"e.g. Assign new bugs to triage\"\n />\n </div>\n <div>\n <label htmlFor=\"automation-description\" className=\"text-sm font-medium\">\n Description\n </label>\n <textarea\n id=\"automation-description\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]\"\n value={form.description}\n onChange={(e) => onChange({ ...form, description: e.target.value })}\n placeholder=\"What this rule is for\"\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"automation-project\" className=\"text-sm font-medium\">\n Project\n </label>\n {projectLocked ? (\n <p className=\"mt-1 text-sm text-muted-foreground\">\n A rule&apos;s project cannot be changed after it is created.\n </p>\n ) : (\n <div className=\"mt-1\">\n <ProjectSelect\n id=\"automation-project\"\n value={form.projectId}\n onChange={(projectId) => onChange({ ...form, projectId })}\n />\n </div>\n )}\n </div>\n <div>\n <label htmlFor=\"automation-trigger\" className=\"text-sm font-medium\">\n Trigger\n </label>\n <select\n id=\"automation-trigger\"\n className={fieldClass}\n value={form.triggerType}\n onChange={(e) => onChange({ ...form, triggerType: e.target.value as TriggerType })}\n >\n {TRIGGER_TYPES.map((t) => (\n <option key={t} value={t}>\n {TRIGGER_TYPE_LABEL[t]}\n {AUTOMATIC_TRIGGER_TYPES.has(t) ? '' : ' (not yet automatic)'}\n </option>\n ))}\n </select>\n {!AUTOMATIC_TRIGGER_TYPES.has(form.triggerType) && (\n <p className=\"mt-1 text-xs text-muted-foreground\">\n This trigger is saved but does not fire on its own yet — use{' '}\n <span className=\"font-medium\">Run now</span> to run it against a work item.\n </p>\n )}\n </div>\n </div>\n <div>\n <p className=\"text-sm font-medium\">Conditions</p>\n <p className=\"mb-2 text-xs text-muted-foreground\">\n All conditions must match. Leave empty to match every event.\n </p>\n <ConditionsEditor\n conditions={form.conditions}\n onChange={(conditions) => onChange({ ...form, conditions })}\n />\n </div>\n <div>\n <p className=\"text-sm font-medium\">Actions</p>\n <p className=\"mb-2 text-xs text-muted-foreground\">\n Run in order when the conditions match.\n </p>\n <ActionsEditor\n actions={form.actions}\n onChange={(actions) => onChange({ ...form, actions })}\n />\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProductsEditProductPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsEditProductPagePage.tsx"],"names":[],"mappings":"AAmHA,wBAAgB,2BAA2B,4CAqT1C"}
1
+ {"version":3,"file":"ProductsEditProductPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsEditProductPagePage.tsx"],"names":[],"mappings":"AAmHA,wBAAgB,2BAA2B,4CAkU1C"}
@@ -95,7 +95,7 @@ function O() {
95
95
  }, W = (e) => H(e), [G, K] = c({}), q = {
96
96
  ...B,
97
97
  ...G
98
- }, J = (e) => K(e), Y = Object.values(q).some((e) => e.trim() !== ""), X = q.name.trim() && q.unit.trim() && q.window.trim() && q.target.trim() !== "" && !Number.isNaN(Number(q.target)) && q.current.trim() !== "" && !Number.isNaN(Number(q.current)), Z = async () => {
98
+ }, J = (e) => K(e), Y = Object.values(q).some((e) => e.trim() !== ""), X = q.name.trim() && q.unit.trim() && q.window.trim() && q.target.trim() !== "" && !Number.isNaN(Number(q.target)) && q.current.trim() !== "" && !Number.isNaN(Number(q.current)), Z = U.vision.trim() === "", Q = U.name.trim() !== "" && !Z && (!Y || X), $ = async () => {
99
99
  F(null), L(!1);
100
100
  try {
101
101
  await n(M({ variables: {
@@ -147,8 +147,8 @@ function O() {
147
147
  className: "flex items-center gap-2",
148
148
  children: [/* @__PURE__ */ _(v, {
149
149
  size: "sm",
150
- onClick: () => void Z(),
151
- disabled: N || Y && !X,
150
+ onClick: () => void $(),
151
+ disabled: N || !Q,
152
152
  children: [N ? /* @__PURE__ */ g(u, { className: "mr-1 h-4 w-4 animate-spin" }) : /* @__PURE__ */ g(f, { className: "mr-1 h-4 w-4" }), "Save Changes"]
153
153
  }), /* @__PURE__ */ g(o, {
154
154
  productId: R.id,
@@ -196,17 +196,29 @@ function O() {
196
196
  description: e.target.value
197
197
  })
198
198
  })] }),
199
- /* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
200
- className: "text-sm font-medium",
201
- children: "Vision"
202
- }), /* @__PURE__ */ g("textarea", {
203
- className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[80px]",
204
- value: U.vision,
205
- onChange: (e) => W({
206
- ...U,
207
- vision: e.target.value
199
+ /* @__PURE__ */ _("div", { children: [
200
+ /* @__PURE__ */ g("label", {
201
+ htmlFor: "product-edit-vision",
202
+ className: "text-sm font-medium",
203
+ children: "Vision *"
204
+ }),
205
+ /* @__PURE__ */ g("textarea", {
206
+ id: "product-edit-vision",
207
+ className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[80px]",
208
+ value: U.vision,
209
+ "aria-invalid": Z,
210
+ "aria-describedby": Z ? "product-edit-vision-hint" : void 0,
211
+ onChange: (e) => W({
212
+ ...U,
213
+ vision: e.target.value
214
+ })
215
+ }),
216
+ Z && /* @__PURE__ */ g("p", {
217
+ id: "product-edit-vision-hint",
218
+ className: "mt-1 text-xs text-muted-foreground",
219
+ children: "Add a vision to save changes."
208
220
  })
209
- })] }),
221
+ ] }),
210
222
  /* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
211
223
  className: "text-sm font-medium",
212
224
  children: "Lifecycle Stage"
@@ -1 +1 @@
1
- {"version":3,"file":"ProductsEditProductPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProductsEditProductPagePage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery, useMutation } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Button, Input } from '@burdenoff/fe-libs/ui';\nimport { Save, ArrowLeft, Package, Loader2 } from 'lucide-react';\nimport { throwOnError } from '../utils/mutationResult';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { useEntityId } from '../hooks/useEntityId';\nimport { detailPath } from '../utils/rowNavigation';\nimport { PersonName } from '../people/People';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { ProductHeaderActions } from '../product/ProductControls';\n\n/**\n * Edit a Product's own fields (BOFF-8045): name, description, vision, lifecycle stage,\n * owner and North Star metric. Archive/unarchive and delete live in ProductHeaderActions\n * (shared with the detail page). Lifecycle stage has no enforced transition graph — any\n * stage to any stage is a valid save, deliberately (the owner decision only asks for the\n * field, not a workflow).\n */\n\nconst PRODUCT_LIFECYCLE_STAGES = [\n 'CONCEPT',\n 'DEVELOPMENT',\n 'BETA',\n 'LAUNCHED',\n 'MATURE',\n 'DEPRECATED',\n] as const;\ntype ProductLifecycleStage = (typeof PRODUCT_LIFECYCLE_STAGES)[number];\n\nconst GET_PRODUCT = gql`\n query GetProductForEdit($id: ID!) {\n planmagnetProduct(id: $id) {\n id\n workspaceId\n name\n description\n vision\n lifecycleStage\n ownerId\n archivedAt\n northStar {\n name\n unit\n target\n current\n window\n }\n createdAt\n updatedAt\n }\n }\n`;\n\nconst UPDATE_PRODUCT = gql`\n mutation UpdateProduct($id: ID!, $input: PlanmagnetUpdateProductInput!) {\n planmagnetUpdateProduct(id: $id, input: $input) {\n id\n name\n description\n vision\n lifecycleStage\n ownerId\n northStar {\n name\n unit\n target\n current\n window\n }\n updatedAt\n }\n }\n`;\n\ninterface ProductNorthStarFull {\n name: string;\n unit: string;\n target: number;\n current: number;\n window: string;\n}\n\ninterface ProductForEdit {\n id: string;\n workspaceId: string;\n name: string;\n description: string | null;\n vision: string;\n lifecycleStage: string;\n ownerId: string;\n archivedAt: string | null;\n northStar: ProductNorthStarFull | null;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface EditProductForm {\n name: string;\n description: string;\n vision: string;\n lifecycleStage: ProductLifecycleStage;\n}\n\ninterface NorthStarForm {\n name: string;\n unit: string;\n target: string;\n current: string;\n window: string;\n}\n\nconst EMPTY_NORTH_STAR: NorthStarForm = { name: '', unit: '', target: '', current: '', window: '' };\n\nexport function ProductsEditProductPagePage() {\n const navigate = usePlanMagnetNavigate();\n const productId = useEntityId('productId') ?? '';\n\n const { data, loading, error } = useQuery<{ planmagnetProduct: ProductForEdit | null }>(\n GET_PRODUCT,\n { variables: { id: productId }, skip: !productId }\n );\n\n const [updateProduct, { loading: saving }] = useMutation(UPDATE_PRODUCT, {\n refetchQueries: ['GetProducts', 'GetProductForEdit', 'GetProductDetail'],\n awaitRefetchQueries: true,\n });\n const [saveError, setSaveError] = useState<string | null>(null);\n const [saved, setSaved] = useState(false);\n\n const product = data?.planmagnetProduct;\n\n const defaultForm = useMemo<EditProductForm>(\n () => ({\n name: product?.name ?? '',\n description: product?.description ?? '',\n vision: product?.vision ?? '',\n lifecycleStage: (product?.lifecycleStage ?? 'CONCEPT') as ProductLifecycleStage,\n }),\n [product]\n );\n const defaultNorthStar = useMemo<NorthStarForm>(\n () =>\n product?.northStar\n ? {\n name: product.northStar.name,\n unit: product.northStar.unit,\n target: String(product.northStar.target),\n current: String(product.northStar.current),\n window: product.northStar.window,\n }\n : EMPTY_NORTH_STAR,\n [product]\n );\n\n const [formOverrides, setFormOverrides] = useState<Partial<EditProductForm>>({});\n const form: EditProductForm = { ...defaultForm, ...formOverrides };\n const setForm = (val: EditProductForm) => setFormOverrides(val);\n\n const [northStarOverrides, setNorthStarOverrides] = useState<Partial<NorthStarForm>>({});\n const northStar: NorthStarForm = { ...defaultNorthStar, ...northStarOverrides };\n const setNorthStar = (val: NorthStarForm) => setNorthStarOverrides(val);\n\n const northStarStarted = Object.values(northStar).some((v) => v.trim() !== '');\n const northStarComplete =\n northStar.name.trim() &&\n northStar.unit.trim() &&\n northStar.window.trim() &&\n northStar.target.trim() !== '' &&\n !Number.isNaN(Number(northStar.target)) &&\n northStar.current.trim() !== '' &&\n !Number.isNaN(Number(northStar.current));\n\n // An emptied description is cleared (null), not silently kept; success is confirmed.\n const handleSave = async () => {\n setSaveError(null);\n setSaved(false);\n try {\n await throwOnError(\n updateProduct({\n variables: {\n id: productId,\n input: {\n name: form.name,\n description: form.description.trim() || null,\n vision: form.vision,\n lifecycleStage: form.lifecycleStage,\n ...(northStarComplete\n ? {\n northStar: {\n name: northStar.name,\n unit: northStar.unit,\n target: Number(northStar.target),\n current: Number(northStar.current),\n window: northStar.window,\n },\n }\n : {}),\n },\n },\n })\n );\n setSaved(true);\n } catch (e) {\n setSaveError(e instanceof Error ? e.message : 'The product could not be saved.');\n }\n };\n\n // Only before the first load: a refetch must not blank the page (it\n // unmounts every component on it and loses their state).\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Failed to load product: {error.message}</div>;\n }\n\n if (!product) {\n return <div className=\"p-6 text-muted-foreground\">No product found.</div>;\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => navigate(detailPath('/products/product-detail-page', product.id))}\n >\n <ArrowLeft className=\"h-4 w-4\" />\n </Button>\n <div>\n <h1 className=\"text-2xl font-semibold\">Edit Product</h1>\n <p className=\"text-sm text-muted-foreground\">Editing {product.name}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <Button\n size=\"sm\"\n onClick={() => void handleSave()}\n disabled={saving || (northStarStarted && !northStarComplete)}\n >\n {saving ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"mr-1 h-4 w-4\" />\n )}\n Save Changes\n </Button>\n <ProductHeaderActions\n productId={product.id}\n name={product.name}\n archivedAt={product.archivedAt}\n />\n </div>\n </div>\n\n {saved && (\n <div role=\"status\" className=\"p-3 bg-muted text-status-success-text rounded-md text-sm\">\n Changes saved.\n </div>\n )}\n {saveError && (\n <div role=\"alert\" className=\"p-3 rounded-md bg-destructive/10 text-destructive text-sm\">\n {saveError}\n </div>\n )}\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Basic Information</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div>\n <label className=\"text-sm font-medium\">Product Name</label>\n <Input\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Description</label>\n <textarea\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[60px]\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Vision</label>\n <textarea\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[80px]\"\n value={form.vision}\n onChange={(e) => setForm({ ...form, vision: e.target.value })}\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Lifecycle Stage</label>\n <select\n aria-label=\"Lifecycle stage\"\n data-testid=\"product-lifecycle-stage\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm\"\n value={form.lifecycleStage}\n onChange={(e) =>\n setForm({ ...form, lifecycleStage: e.target.value as ProductLifecycleStage })\n }\n >\n {PRODUCT_LIFECYCLE_STAGES.map((stage) => (\n <option key={stage} value={stage}>\n {enumLabel(stage)}\n </option>\n ))}\n </select>\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>North Star metric</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label className=\"text-sm font-medium\">Metric name</label>\n <Input\n value={northStar.name}\n onChange={(e) => setNorthStar({ ...northStar, name: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Unit</label>\n <Input\n value={northStar.unit}\n onChange={(e) => setNorthStar({ ...northStar, unit: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Target</label>\n <Input\n type=\"number\"\n value={northStar.target}\n onChange={(e) => setNorthStar({ ...northStar, target: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Current</label>\n <Input\n type=\"number\"\n value={northStar.current}\n onChange={(e) => setNorthStar({ ...northStar, current: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div className=\"col-span-2\">\n <label className=\"text-sm font-medium\">Window</label>\n <Input\n value={northStar.window}\n onChange={(e) => setNorthStar({ ...northStar, window: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n </div>\n {northStarStarted && !northStarComplete && (\n <p className=\"text-xs text-destructive\">\n Fill in every North Star field (or clear them all) — name, unit, target, current\n and window are all required together.\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Product Info</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3 text-sm\">\n <div className=\"flex items-center gap-2\">\n <Package className=\"h-4 w-4 text-primary\" />\n <span className=\"font-medium\">{product.name}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Lifecycle stage</span>\n <span className=\"font-medium capitalize\">{enumLabel(product.lifecycleStage)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Status</span>\n <span className=\"font-medium\">{product.archivedAt ? 'Archived' : 'Active'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Owner</span>\n <span className=\"font-medium\">\n <PersonName id={product.ownerId} />\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Created</span>\n <span>{new Date(product.createdAt).toLocaleDateString()}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Updated</span>\n <span>{new Date(product.updatedAt).toLocaleDateString()}</span>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProductsEditProductPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProductsEditProductPagePage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery, useMutation } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Button, Input } from '@burdenoff/fe-libs/ui';\nimport { Save, ArrowLeft, Package, Loader2 } from 'lucide-react';\nimport { throwOnError } from '../utils/mutationResult';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { useEntityId } from '../hooks/useEntityId';\nimport { detailPath } from '../utils/rowNavigation';\nimport { PersonName } from '../people/People';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { ProductHeaderActions } from '../product/ProductControls';\n\n/**\n * Edit a Product's own fields (BOFF-8045): name, description, vision, lifecycle stage,\n * owner and North Star metric. Archive/unarchive and delete live in ProductHeaderActions\n * (shared with the detail page). Lifecycle stage has no enforced transition graph — any\n * stage to any stage is a valid save, deliberately (the owner decision only asks for the\n * field, not a workflow).\n */\n\nconst PRODUCT_LIFECYCLE_STAGES = [\n 'CONCEPT',\n 'DEVELOPMENT',\n 'BETA',\n 'LAUNCHED',\n 'MATURE',\n 'DEPRECATED',\n] as const;\ntype ProductLifecycleStage = (typeof PRODUCT_LIFECYCLE_STAGES)[number];\n\nconst GET_PRODUCT = gql`\n query GetProductForEdit($id: ID!) {\n planmagnetProduct(id: $id) {\n id\n workspaceId\n name\n description\n vision\n lifecycleStage\n ownerId\n archivedAt\n northStar {\n name\n unit\n target\n current\n window\n }\n createdAt\n updatedAt\n }\n }\n`;\n\nconst UPDATE_PRODUCT = gql`\n mutation UpdateProduct($id: ID!, $input: PlanmagnetUpdateProductInput!) {\n planmagnetUpdateProduct(id: $id, input: $input) {\n id\n name\n description\n vision\n lifecycleStage\n ownerId\n northStar {\n name\n unit\n target\n current\n window\n }\n updatedAt\n }\n }\n`;\n\ninterface ProductNorthStarFull {\n name: string;\n unit: string;\n target: number;\n current: number;\n window: string;\n}\n\ninterface ProductForEdit {\n id: string;\n workspaceId: string;\n name: string;\n description: string | null;\n vision: string;\n lifecycleStage: string;\n ownerId: string;\n archivedAt: string | null;\n northStar: ProductNorthStarFull | null;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface EditProductForm {\n name: string;\n description: string;\n vision: string;\n lifecycleStage: ProductLifecycleStage;\n}\n\ninterface NorthStarForm {\n name: string;\n unit: string;\n target: string;\n current: string;\n window: string;\n}\n\nconst EMPTY_NORTH_STAR: NorthStarForm = { name: '', unit: '', target: '', current: '', window: '' };\n\nexport function ProductsEditProductPagePage() {\n const navigate = usePlanMagnetNavigate();\n const productId = useEntityId('productId') ?? '';\n\n const { data, loading, error } = useQuery<{ planmagnetProduct: ProductForEdit | null }>(\n GET_PRODUCT,\n { variables: { id: productId }, skip: !productId }\n );\n\n const [updateProduct, { loading: saving }] = useMutation(UPDATE_PRODUCT, {\n refetchQueries: ['GetProducts', 'GetProductForEdit', 'GetProductDetail'],\n awaitRefetchQueries: true,\n });\n const [saveError, setSaveError] = useState<string | null>(null);\n const [saved, setSaved] = useState(false);\n\n const product = data?.planmagnetProduct;\n\n const defaultForm = useMemo<EditProductForm>(\n () => ({\n name: product?.name ?? '',\n description: product?.description ?? '',\n vision: product?.vision ?? '',\n lifecycleStage: (product?.lifecycleStage ?? 'CONCEPT') as ProductLifecycleStage,\n }),\n [product]\n );\n const defaultNorthStar = useMemo<NorthStarForm>(\n () =>\n product?.northStar\n ? {\n name: product.northStar.name,\n unit: product.northStar.unit,\n target: String(product.northStar.target),\n current: String(product.northStar.current),\n window: product.northStar.window,\n }\n : EMPTY_NORTH_STAR,\n [product]\n );\n\n const [formOverrides, setFormOverrides] = useState<Partial<EditProductForm>>({});\n const form: EditProductForm = { ...defaultForm, ...formOverrides };\n const setForm = (val: EditProductForm) => setFormOverrides(val);\n\n const [northStarOverrides, setNorthStarOverrides] = useState<Partial<NorthStarForm>>({});\n const northStar: NorthStarForm = { ...defaultNorthStar, ...northStarOverrides };\n const setNorthStar = (val: NorthStarForm) => setNorthStarOverrides(val);\n\n const northStarStarted = Object.values(northStar).some((v) => v.trim() !== '');\n const northStarComplete =\n northStar.name.trim() &&\n northStar.unit.trim() &&\n northStar.window.trim() &&\n northStar.target.trim() !== '' &&\n !Number.isNaN(Number(northStar.target)) &&\n northStar.current.trim() !== '' &&\n !Number.isNaN(Number(northStar.current));\n\n // Name and vision are required by the service (product.service assertProductInput). A product\n // created by the BOFF-8045 roadmap backfill starts with an empty vision, so gate Save here and say\n // why, instead of letting the first edit bounce off \"vision required\".\n const visionMissing = form.vision.trim() === '';\n const canSave =\n form.name.trim() !== '' && !visionMissing && (!northStarStarted || northStarComplete);\n\n // An emptied description is cleared (null), not silently kept; success is confirmed.\n const handleSave = async () => {\n setSaveError(null);\n setSaved(false);\n try {\n await throwOnError(\n updateProduct({\n variables: {\n id: productId,\n input: {\n name: form.name,\n description: form.description.trim() || null,\n vision: form.vision,\n lifecycleStage: form.lifecycleStage,\n ...(northStarComplete\n ? {\n northStar: {\n name: northStar.name,\n unit: northStar.unit,\n target: Number(northStar.target),\n current: Number(northStar.current),\n window: northStar.window,\n },\n }\n : {}),\n },\n },\n })\n );\n setSaved(true);\n } catch (e) {\n setSaveError(e instanceof Error ? e.message : 'The product could not be saved.');\n }\n };\n\n // Only before the first load: a refetch must not blank the page (it\n // unmounts every component on it and loses their state).\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Failed to load product: {error.message}</div>;\n }\n\n if (!product) {\n return <div className=\"p-6 text-muted-foreground\">No product found.</div>;\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => navigate(detailPath('/products/product-detail-page', product.id))}\n >\n <ArrowLeft className=\"h-4 w-4\" />\n </Button>\n <div>\n <h1 className=\"text-2xl font-semibold\">Edit Product</h1>\n <p className=\"text-sm text-muted-foreground\">Editing {product.name}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <Button size=\"sm\" onClick={() => void handleSave()} disabled={saving || !canSave}>\n {saving ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"mr-1 h-4 w-4\" />\n )}\n Save Changes\n </Button>\n <ProductHeaderActions\n productId={product.id}\n name={product.name}\n archivedAt={product.archivedAt}\n />\n </div>\n </div>\n\n {saved && (\n <div role=\"status\" className=\"p-3 bg-muted text-status-success-text rounded-md text-sm\">\n Changes saved.\n </div>\n )}\n {saveError && (\n <div role=\"alert\" className=\"p-3 rounded-md bg-destructive/10 text-destructive text-sm\">\n {saveError}\n </div>\n )}\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Basic Information</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div>\n <label className=\"text-sm font-medium\">Product Name</label>\n <Input\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Description</label>\n <textarea\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[60px]\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"product-edit-vision\" className=\"text-sm font-medium\">\n Vision *\n </label>\n <textarea\n id=\"product-edit-vision\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[80px]\"\n value={form.vision}\n aria-invalid={visionMissing}\n aria-describedby={visionMissing ? 'product-edit-vision-hint' : undefined}\n onChange={(e) => setForm({ ...form, vision: e.target.value })}\n />\n {visionMissing && (\n <p id=\"product-edit-vision-hint\" className=\"mt-1 text-xs text-muted-foreground\">\n Add a vision to save changes.\n </p>\n )}\n </div>\n <div>\n <label className=\"text-sm font-medium\">Lifecycle Stage</label>\n <select\n aria-label=\"Lifecycle stage\"\n data-testid=\"product-lifecycle-stage\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm\"\n value={form.lifecycleStage}\n onChange={(e) =>\n setForm({ ...form, lifecycleStage: e.target.value as ProductLifecycleStage })\n }\n >\n {PRODUCT_LIFECYCLE_STAGES.map((stage) => (\n <option key={stage} value={stage}>\n {enumLabel(stage)}\n </option>\n ))}\n </select>\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>North Star metric</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label className=\"text-sm font-medium\">Metric name</label>\n <Input\n value={northStar.name}\n onChange={(e) => setNorthStar({ ...northStar, name: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Unit</label>\n <Input\n value={northStar.unit}\n onChange={(e) => setNorthStar({ ...northStar, unit: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Target</label>\n <Input\n type=\"number\"\n value={northStar.target}\n onChange={(e) => setNorthStar({ ...northStar, target: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Current</label>\n <Input\n type=\"number\"\n value={northStar.current}\n onChange={(e) => setNorthStar({ ...northStar, current: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div className=\"col-span-2\">\n <label className=\"text-sm font-medium\">Window</label>\n <Input\n value={northStar.window}\n onChange={(e) => setNorthStar({ ...northStar, window: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n </div>\n {northStarStarted && !northStarComplete && (\n <p className=\"text-xs text-destructive\">\n Fill in every North Star field (or clear them all) — name, unit, target, current\n and window are all required together.\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Product Info</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3 text-sm\">\n <div className=\"flex items-center gap-2\">\n <Package className=\"h-4 w-4 text-primary\" />\n <span className=\"font-medium\">{product.name}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Lifecycle stage</span>\n <span className=\"font-medium capitalize\">{enumLabel(product.lifecycleStage)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Status</span>\n <span className=\"font-medium\">{product.archivedAt ? 'Archived' : 'Active'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Owner</span>\n <span className=\"font-medium\">\n <PersonName id={product.ownerId} />\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Created</span>\n <span>{new Date(product.createdAt).toLocaleDateString()}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Updated</span>\n <span>{new Date(product.updatedAt).toLocaleDateString()}</span>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TasksCreateTaskPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TasksCreateTaskPagePage.tsx"],"names":[],"mappings":"AAsEA,wBAAgB,uBAAuB,4CAqStC"}
1
+ {"version":3,"file":"TasksCreateTaskPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TasksCreateTaskPagePage.tsx"],"names":[],"mappings":"AAsEA,wBAAgB,uBAAuB,4CAgVtC"}