@burdenoff/microfe-planmagnet 2026.928.7 → 2026.928.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 +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;AA0DD,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;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"}
@@ -5,8 +5,8 @@ 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";
9
- function b({ action: e, onChange: a }) {
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 }) {
10
10
  switch (e.type) {
11
11
  case "ASSIGN_TO_USER": return /* @__PURE__ */ h(t, {
12
12
  value: e.value,
@@ -38,13 +38,14 @@ function b({ action: e, onChange: a }) {
38
38
  });
39
39
  default: return /* @__PURE__ */ h(v, {
40
40
  "aria-label": "Label",
41
+ className: b,
41
42
  placeholder: "e.g. needs-review",
42
43
  value: e.value,
43
44
  onChange: (e) => a(e.target.value)
44
45
  });
45
46
  }
46
47
  }
47
- function x({ actions: e, onChange: t }) {
48
+ function S({ actions: e, onChange: t }) {
48
49
  let n = (n, r) => t(e.map((e, t) => t === n ? {
49
50
  ...e,
50
51
  ...r
@@ -74,7 +75,7 @@ function x({ actions: e, onChange: t }) {
74
75
  }),
75
76
  /* @__PURE__ */ h("div", {
76
77
  className: "min-w-0 flex-1",
77
- children: /* @__PURE__ */ h(b, {
78
+ children: /* @__PURE__ */ h(x, {
78
79
  action: e,
79
80
  onChange: (e) => n(t, { value: e })
80
81
  })
@@ -112,7 +113,7 @@ function x({ actions: e, onChange: t }) {
112
113
  ]
113
114
  });
114
115
  }
115
- function S({ conditions: e, onChange: t }) {
116
+ function C({ conditions: e, onChange: t }) {
116
117
  let n = (n, r) => t(e.map((e, t) => t === n ? {
117
118
  ...e,
118
119
  ...r
@@ -126,7 +127,7 @@ function S({ conditions: e, onChange: t }) {
126
127
  /* @__PURE__ */ h(v, {
127
128
  "aria-label": "Condition field",
128
129
  list: "automation-condition-fields",
129
- className: "w-36 shrink-0",
130
+ className: `w-36 shrink-0 ${b}`,
130
131
  placeholder: "field",
131
132
  value: e.field,
132
133
  onChange: (e) => n(t, { field: e.target.value })
@@ -143,7 +144,7 @@ function S({ conditions: e, onChange: t }) {
143
144
  }),
144
145
  e.operator !== "exists" && /* @__PURE__ */ h(v, {
145
146
  "aria-label": "Condition value",
146
- className: "min-w-0 flex-1",
147
+ className: `min-w-0 flex-1 ${b}`,
147
148
  placeholder: e.operator === "in" || e.operator === "notIn" ? "comma, separated, values" : "value",
148
149
  value: e.value,
149
150
  onChange: (e) => n(t, { value: e.target.value })
@@ -181,7 +182,7 @@ function S({ conditions: e, onChange: t }) {
181
182
  ]
182
183
  });
183
184
  }
184
- function C({ form: t, onChange: n, projectLocked: r = !1 }) {
185
+ function w({ form: t, onChange: n, projectLocked: r = !1 }) {
185
186
  return /* @__PURE__ */ g("div", {
186
187
  className: "space-y-4",
187
188
  children: [
@@ -191,7 +192,7 @@ function C({ form: t, onChange: n, projectLocked: r = !1 }) {
191
192
  children: "Name"
192
193
  }), /* @__PURE__ */ h(v, {
193
194
  id: "automation-name",
194
- className: "mt-1",
195
+ className: `mt-1 ${b}`,
195
196
  value: t.name,
196
197
  onChange: (e) => n({
197
198
  ...t,
@@ -274,7 +275,7 @@ function C({ form: t, onChange: n, projectLocked: r = !1 }) {
274
275
  className: "mb-2 text-xs text-muted-foreground",
275
276
  children: "All conditions must match. Leave empty to match every event."
276
277
  }),
277
- /* @__PURE__ */ h(S, {
278
+ /* @__PURE__ */ h(C, {
278
279
  conditions: t.conditions,
279
280
  onChange: (e) => n({
280
281
  ...t,
@@ -291,7 +292,7 @@ function C({ form: t, onChange: n, projectLocked: r = !1 }) {
291
292
  className: "mb-2 text-xs text-muted-foreground",
292
293
  children: "Run in order when the conditions match."
293
294
  }),
294
- /* @__PURE__ */ h(x, {
295
+ /* @__PURE__ */ h(S, {
295
296
  actions: t.actions,
296
297
  onChange: (e) => n({
297
298
  ...t,
@@ -303,6 +304,6 @@ function C({ form: t, onChange: n, projectLocked: r = !1 }) {
303
304
  });
304
305
  }
305
306
  //#endregion
306
- export { C as AutomationRuleFields };
307
+ export { w as AutomationRuleFields };
307
308
 
308
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\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 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\"\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\"\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\"\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\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 +1 @@
1
- {"version":3,"file":"ProjectsCriticalPathPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProjectsCriticalPathPagePage.tsx"],"names":[],"mappings":"AAwDA,wBAAgB,4BAA4B,4CAgL3C"}
1
+ {"version":3,"file":"ProjectsCriticalPathPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProjectsCriticalPathPagePage.tsx"],"names":[],"mappings":"AAkFA,wBAAgB,4BAA4B,4CAmR3C"}
@@ -1,14 +1,14 @@
1
1
  import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
2
2
  import { useEntityId as n } from "../hooks/useEntityId.js";
3
3
  import { ProjectSelect as r } from "../controls/Pickers.js";
4
- import { useState as i } from "react";
5
- import { AlertTriangle as a, Route as o } from "lucide-react";
6
- import { gql as s } from "@apollo/client";
7
- import { useQuery as c } from "@apollo/client/react";
8
- import { jsx as l, jsxs as u } from "react/jsx-runtime";
9
- import { Badge as d, Card as f, CardContent as p, CardHeader as m, CardTitle as h } from "@burdenoff/fe-libs/ui";
4
+ import { useEffect as i, useState as a } from "react";
5
+ import { AlertTriangle as o, Route as s } from "lucide-react";
6
+ import { gql as c } from "@apollo/client";
7
+ import { useQuery as l } from "@apollo/client/react";
8
+ import { jsx as u, jsxs as d } from "react/jsx-runtime";
9
+ import { Badge as f, Card as p, CardContent as m, CardHeader as h, CardTitle as g } from "@burdenoff/fe-libs/ui";
10
10
  //#region src/planmagnet/pages/ProjectsCriticalPathPagePage.tsx
11
- var g = s`
11
+ var _ = c`
12
12
  query GetProjectCriticalPath($workspaceId: ID!, $projectId: ID) {
13
13
  planmagnetCriticalPath(workspaceId: $workspaceId, projectId: $projectId) {
14
14
  totalDurationMinutes
@@ -24,141 +24,231 @@ var g = s`
24
24
  length
25
25
  }
26
26
  }
27
- `, _ = s`
27
+ `, v = c`
28
28
  query GetCriticalPathProject($id: ID!) {
29
29
  planmagnetProject(id: $id) {
30
30
  id
31
31
  name
32
32
  }
33
33
  }
34
+ `, y = 100, b = c`
35
+ query GetNonCriticalWorkItems(
36
+ $workspaceId: ID!
37
+ $filter: PlanmagnetWorkItemFilter
38
+ $pagination: PlanmagnetPaginationInput
39
+ ) {
40
+ planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: $pagination) {
41
+ items {
42
+ id
43
+ title
44
+ key
45
+ }
46
+ pageInfo {
47
+ hasNextPage
48
+ endCursor
49
+ }
50
+ }
51
+ }
34
52
  `;
35
- function v(e) {
53
+ function x(e) {
36
54
  if (e <= 0) return "0 days";
37
55
  let t = Math.round(e);
38
56
  if (t < 60) return `${Math.max(1, t)}m`;
39
57
  let n = Math.round(t / 60), r = Math.floor(n / 24), i = n % 24;
40
58
  return r === 0 ? `${i}h` : i > 0 ? `${r}d ${i}h` : `${r} day${r === 1 ? "" : "s"}`;
41
59
  }
42
- function y() {
43
- let s = n(), { workspaceId: y } = e(), b = t(), [x, S] = i(""), { data: C, loading: w, error: T } = c(g, {
60
+ function S() {
61
+ let c = n(), { workspaceId: S } = e(), C = t(), [w, T] = a(""), { data: E, loading: D, error: O } = l(_, {
44
62
  variables: {
45
- workspaceId: y ?? "",
46
- projectId: s
63
+ workspaceId: S ?? "",
64
+ projectId: c
47
65
  },
48
- skip: !y || !s
49
- }), { data: E } = c(_, {
50
- variables: { id: s },
51
- skip: !s
52
- });
53
- if (!s) return /* @__PURE__ */ u("div", {
66
+ skip: !S || !c
67
+ }), { data: k } = l(v, {
68
+ variables: { id: c },
69
+ skip: !c
70
+ }), A = E?.planmagnetCriticalPath.path ?? [], j = A.length > 0, M = A.map((e) => e.id), { data: N, loading: P, error: F, fetchMore: I } = l(b, {
71
+ variables: {
72
+ workspaceId: S ?? "",
73
+ filter: {
74
+ projectId: c,
75
+ excludeIds: M
76
+ },
77
+ pagination: { first: y }
78
+ },
79
+ skip: !S || !c || !j
80
+ }), L = N?.planmagnetWorkItems.pageInfo, [R, z] = a(null);
81
+ if (i(() => {
82
+ if (R || !L?.hasNextPage || !L.endCursor) return;
83
+ let e = L.endCursor;
84
+ I({
85
+ variables: { pagination: {
86
+ first: y,
87
+ after: e
88
+ } },
89
+ updateQuery: (e, { fetchMoreResult: t }) => ({ planmagnetWorkItems: {
90
+ ...t.planmagnetWorkItems,
91
+ items: [...e.planmagnetWorkItems.items, ...t.planmagnetWorkItems.items]
92
+ } })
93
+ }).then((e) => {
94
+ let t = e?.error;
95
+ t && z(t.message ?? "Unknown error");
96
+ }).catch((e) => z(e instanceof Error ? e.message : String(e)));
97
+ }, [
98
+ R,
99
+ L?.hasNextPage,
100
+ L?.endCursor,
101
+ I
102
+ ]), !c) return /* @__PURE__ */ d("div", {
54
103
  className: "space-y-6 p-6",
55
- children: [/* @__PURE__ */ u("div", { children: [/* @__PURE__ */ u("h1", {
104
+ children: [/* @__PURE__ */ d("div", { children: [/* @__PURE__ */ d("h1", {
56
105
  className: "text-2xl font-semibold flex items-center gap-2",
57
- children: [/* @__PURE__ */ l(o, { className: "h-5 w-5 text-primary" }), "Critical Path"]
58
- }), /* @__PURE__ */ l("p", {
106
+ children: [/* @__PURE__ */ u(s, { className: "h-5 w-5 text-primary" }), "Critical Path"]
107
+ }), /* @__PURE__ */ u("p", {
59
108
  className: "text-sm text-muted-foreground",
60
109
  children: "Choose a project to see its critical path."
61
- })] }), /* @__PURE__ */ l(f, { children: /* @__PURE__ */ u(p, {
110
+ })] }), /* @__PURE__ */ u(p, { children: /* @__PURE__ */ d(m, {
62
111
  className: "pt-6 space-y-3",
63
112
  children: [
64
- /* @__PURE__ */ l("label", {
113
+ /* @__PURE__ */ u("label", {
65
114
  htmlFor: "critical-path-project",
66
115
  className: "text-sm font-medium",
67
116
  children: "Project"
68
117
  }),
69
- /* @__PURE__ */ l(r, {
118
+ /* @__PURE__ */ u(r, {
70
119
  id: "critical-path-project",
71
- value: x,
72
- onChange: S,
120
+ value: w,
121
+ onChange: T,
73
122
  noneLabel: "Choose a project…"
74
123
  }),
75
- /* @__PURE__ */ l("button", {
124
+ /* @__PURE__ */ u("button", {
76
125
  type: "button",
77
126
  className: "text-sm text-primary hover:underline disabled:pointer-events-none disabled:opacity-50",
78
- disabled: !x,
79
- onClick: () => b(`/projects/critical-path-page/${encodeURIComponent(x)}`),
127
+ disabled: !w,
128
+ onClick: () => C(`/projects/critical-path-page/${encodeURIComponent(w)}`),
80
129
  children: "View critical path →"
81
130
  })
82
131
  ]
83
132
  }) })]
84
133
  });
85
- if (w && !C) return /* @__PURE__ */ l("div", {
134
+ if (D && !E) return /* @__PURE__ */ u("div", {
86
135
  className: "flex items-center justify-center p-12",
87
- children: /* @__PURE__ */ l("div", { className: "animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" })
136
+ children: /* @__PURE__ */ u("div", { className: "animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" })
88
137
  });
89
- if (T) return /* @__PURE__ */ u("div", {
138
+ if (O) return /* @__PURE__ */ d("div", {
90
139
  className: "p-6 text-red-500",
91
- children: ["Error computing critical path: ", T.message]
140
+ children: ["Error computing critical path: ", O.message]
92
141
  });
93
- let D = C?.planmagnetCriticalPath.path ?? [], O = C?.planmagnetCriticalPath.slack ?? {}, k = C?.planmagnetCriticalPath.totalDurationMinutes ?? 0, A = C?.planmagnetDependencyCycles ?? [], j = E?.planmagnetProject?.name;
94
- return /* @__PURE__ */ u("div", {
142
+ let B = E?.planmagnetCriticalPath.slack ?? {}, V = E?.planmagnetCriticalPath.totalDurationMinutes ?? 0, H = E?.planmagnetDependencyCycles ?? [], U = k?.planmagnetProject?.name, W = [...N?.planmagnetWorkItems.items ?? []].sort((e, t) => (B[e.id] ?? 0) - (B[t.id] ?? 0));
143
+ return /* @__PURE__ */ d("div", {
95
144
  className: "space-y-6 p-6",
96
145
  children: [
97
- /* @__PURE__ */ l("div", {
146
+ /* @__PURE__ */ u("div", {
98
147
  className: "flex items-center justify-between",
99
- children: /* @__PURE__ */ u("div", { children: [/* @__PURE__ */ u("h1", {
148
+ children: /* @__PURE__ */ d("div", { children: [/* @__PURE__ */ d("h1", {
100
149
  className: "text-2xl font-semibold flex items-center gap-2",
101
- children: [/* @__PURE__ */ l(o, { className: "h-5 w-5 text-primary" }), "Critical Path"]
102
- }), /* @__PURE__ */ u("p", {
150
+ children: [/* @__PURE__ */ u(s, { className: "h-5 w-5 text-primary" }), "Critical Path"]
151
+ }), /* @__PURE__ */ d("p", {
103
152
  className: "text-sm text-muted-foreground",
104
- children: [j ?? "This project", " — the longest chain of dependent work; a delay on any node here delays the whole chain."]
153
+ children: [U ?? "This project", " — the longest chain of dependent work; a delay on any node here delays the whole chain."]
105
154
  })] })
106
155
  }),
107
- A.length > 0 && /* @__PURE__ */ u("div", {
156
+ H.length > 0 && /* @__PURE__ */ d("div", {
108
157
  role: "alert",
109
158
  className: "flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive",
110
- children: [/* @__PURE__ */ l(a, { className: "h-4 w-4 mt-0.5 shrink-0" }), /* @__PURE__ */ u("div", { children: [/* @__PURE__ */ u("p", {
159
+ children: [/* @__PURE__ */ u(o, { className: "h-4 w-4 mt-0.5 shrink-0" }), /* @__PURE__ */ d("div", { children: [/* @__PURE__ */ d("p", {
111
160
  className: "font-medium",
112
161
  children: [
113
- A.length,
162
+ H.length,
114
163
  " dependency cycle",
115
- A.length === 1 ? "" : "s",
164
+ H.length === 1 ? "" : "s",
116
165
  " detected."
117
166
  ]
118
- }), /* @__PURE__ */ l("p", { children: "A cycle has no valid schedule, so the path below excludes the items involved. Break each cycle by removing or re-pointing one of its dependencies from the task detail page." })] })]
167
+ }), /* @__PURE__ */ u("p", { children: "A cycle has no valid schedule, so the path below excludes the items involved. Break each cycle by removing or re-pointing one of its dependencies from the task detail page." })] })]
119
168
  }),
120
- /* @__PURE__ */ u(f, { children: [/* @__PURE__ */ l(m, { children: /* @__PURE__ */ l(h, { children: "Total duration" }) }), /* @__PURE__ */ u(p, { children: [/* @__PURE__ */ l("p", {
169
+ /* @__PURE__ */ d(p, { children: [/* @__PURE__ */ u(h, { children: /* @__PURE__ */ u(g, { children: "Total duration" }) }), /* @__PURE__ */ d(m, { children: [/* @__PURE__ */ u("p", {
121
170
  className: "text-2xl font-bold",
122
- children: v(k)
123
- }), /* @__PURE__ */ l("p", {
171
+ children: x(V)
172
+ }), /* @__PURE__ */ u("p", {
124
173
  className: "text-xs text-muted-foreground mt-1",
125
174
  children: "Work item spans plus dependency lag/lead, summed along the longest chain."
126
175
  })] })] }),
127
- /* @__PURE__ */ u(f, { children: [/* @__PURE__ */ l(m, { children: /* @__PURE__ */ u(h, { children: [
176
+ /* @__PURE__ */ d(p, { children: [/* @__PURE__ */ u(h, { children: /* @__PURE__ */ d(g, { children: [
128
177
  "Path (",
129
- D.length,
178
+ A.length,
130
179
  " item",
131
- D.length === 1 ? "" : "s",
180
+ A.length === 1 ? "" : "s",
132
181
  ")"
133
- ] }) }), /* @__PURE__ */ u(p, {
182
+ ] }) }), /* @__PURE__ */ d(m, {
134
183
  className: "space-y-2",
135
- children: [D.map((e, t) => /* @__PURE__ */ u("div", {
184
+ children: [A.map((e, t) => /* @__PURE__ */ d("div", {
136
185
  className: "flex items-center justify-between gap-3 rounded-md border border-input p-3",
137
- children: [/* @__PURE__ */ u("div", {
186
+ children: [/* @__PURE__ */ d("div", {
138
187
  className: "flex items-center gap-3 min-w-0",
139
- children: [/* @__PURE__ */ l("span", {
188
+ children: [/* @__PURE__ */ u("span", {
140
189
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-medium text-primary-foreground",
141
190
  children: t + 1
142
- }), /* @__PURE__ */ u("button", {
191
+ }), /* @__PURE__ */ d("button", {
143
192
  type: "button",
144
193
  className: "min-w-0 truncate text-left text-sm font-medium hover:underline",
145
- onClick: () => b(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.id)}`),
194
+ onClick: () => C(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.id)}`),
146
195
  children: [e.key ? `${e.key} ` : "", e.title ?? "Untitled work item"]
147
196
  })]
148
- }), /* @__PURE__ */ l(d, {
149
- variant: (O[e.id] ?? 0) === 0 ? "default" : "secondary",
197
+ }), /* @__PURE__ */ u(f, {
198
+ variant: (B[e.id] ?? 0) === 0 ? "default" : "secondary",
150
199
  className: "shrink-0",
151
- children: (O[e.id] ?? 0) === 0 ? "On critical path" : `${v(O[e.id] ?? 0)} slack`
200
+ children: (B[e.id] ?? 0) === 0 ? "On critical path" : `${x(B[e.id] ?? 0)} slack`
152
201
  })]
153
- }, e.id)), D.length === 0 && /* @__PURE__ */ l("p", {
202
+ }, e.id)), A.length === 0 && /* @__PURE__ */ u("p", {
154
203
  className: "text-center text-muted-foreground py-8",
155
204
  children: "No schedulable dependencies yet. Add dependencies from a task's detail page to compute a critical path."
156
205
  })]
206
+ })] }),
207
+ j && /* @__PURE__ */ d(p, { children: [/* @__PURE__ */ u(h, { children: /* @__PURE__ */ d(g, { children: [
208
+ "Not on the critical path (",
209
+ W.length,
210
+ " item",
211
+ W.length === 1 ? "" : "s",
212
+ ")"
213
+ ] }) }), /* @__PURE__ */ d(m, {
214
+ className: "space-y-2",
215
+ children: [
216
+ W.map((e) => /* @__PURE__ */ d("div", {
217
+ className: "flex items-center justify-between gap-3 rounded-md border border-input p-3",
218
+ children: [/* @__PURE__ */ d("button", {
219
+ type: "button",
220
+ className: "min-w-0 truncate text-left text-sm font-medium hover:underline",
221
+ onClick: () => C(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.id)}`),
222
+ children: [e.key ? `${e.key} ` : "", e.title]
223
+ }), /* @__PURE__ */ d(f, {
224
+ variant: "secondary",
225
+ className: "shrink-0",
226
+ children: [x(B[e.id] ?? 0), " slack"]
227
+ })]
228
+ }, e.id)),
229
+ !F && R && /* @__PURE__ */ d("p", {
230
+ className: "text-center text-destructive py-4",
231
+ role: "alert",
232
+ children: ["Some work items could not be loaded, so this list may be incomplete: ", R]
233
+ }),
234
+ F && /* @__PURE__ */ d("p", {
235
+ className: "text-center text-destructive py-8",
236
+ children: ["The rest of this project's work items could not be loaded: ", F.message]
237
+ }),
238
+ !F && P && W.length === 0 && /* @__PURE__ */ u("p", {
239
+ className: "text-center text-muted-foreground py-8",
240
+ children: "Loading…"
241
+ }),
242
+ !F && !P && W.length === 0 && /* @__PURE__ */ u("p", {
243
+ className: "text-center text-muted-foreground py-8",
244
+ children: "Every scheduled work item in this project is on the critical path."
245
+ })
246
+ ]
157
247
  })] })
158
248
  ]
159
249
  });
160
250
  }
161
251
  //#endregion
162
- export { y as ProjectsCriticalPathPagePage };
252
+ export { S as ProjectsCriticalPathPagePage };
163
253
 
164
254
  //# sourceMappingURL=ProjectsCriticalPathPagePage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectsCriticalPathPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProjectsCriticalPathPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge } from '@burdenoff/fe-libs/ui';\nimport { Route, AlertTriangle } from 'lucide-react';\nimport { useEntityId } from '../hooks/useEntityId';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ProjectSelect } from '../controls/Pickers';\n\n/**\n * Critical path view for a project (BOFF-8047). planmagnetCriticalPath / planmagnetDependencyCycles were\n * served and prod-verified (duration-weighted CPM over WorkItemDependency, per the P0 decision that this type\n * is CPM-only) but had no UI. Reached from the sidebar (no project preselects one) and from a project's detail\n * page (\"Critical path\" button).\n */\n\nconst GET_CRITICAL_PATH = gql`\n query GetProjectCriticalPath($workspaceId: ID!, $projectId: ID) {\n planmagnetCriticalPath(workspaceId: $workspaceId, projectId: $projectId) {\n totalDurationMinutes\n slack\n path {\n id\n title\n key\n }\n }\n planmagnetDependencyCycles(workspaceId: $workspaceId, projectId: $projectId) {\n workItemIds\n length\n }\n }\n`;\n\nconst GET_CRITICAL_PATH_PROJECT = gql`\n query GetCriticalPathProject($id: ID!) {\n planmagnetProject(id: $id) {\n id\n name\n }\n }\n`;\n\nfunction durationLabel(totalMinutes: number): string {\n if (totalMinutes <= 0) return '0 days';\n // Round once, then split. Rounding the day remainder separately printed \"1d 24h\" for 47.5–47.9 h, and fractional\n // minutes (durations are elapsed ms / 60000) printed decimals.\n const roundedMinutes = Math.round(totalMinutes);\n if (roundedMinutes < 60) return `${Math.max(1, roundedMinutes)}m`;\n const totalHours = Math.round(roundedMinutes / 60);\n const days = Math.floor(totalHours / 24);\n const hours = totalHours % 24;\n if (days === 0) return `${hours}h`;\n return hours > 0 ? `${days}d ${hours}h` : `${days} day${days === 1 ? '' : 's'}`;\n}\n\nexport function ProjectsCriticalPathPagePage() {\n const projectId = useEntityId();\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const [pickerProjectId, setPickerProjectId] = useState('');\n\n const { data, loading, error } = useQuery<{\n planmagnetCriticalPath: {\n totalDurationMinutes: number;\n // The SDL doc comment on this field says \"days\" but the resolver\n // actually returns MINUTES (wspace-planmagnet-svc\n // src/modules/dependency/services/dependency-graph.ts:53, backed by\n // durationMinutes() in dependency.service.ts) — same unit as\n // totalDurationMinutes above. Feed values through durationLabel()\n // below, never render this number bare. SDL doc fix tracked\n // separately; do not \"fix\" this comment to match the SDL.\n slack: Record<string, number>;\n path: { id: string; title: string | null; key: string | null }[];\n };\n planmagnetDependencyCycles: { workItemIds: string[]; length: number }[];\n }>(GET_CRITICAL_PATH, {\n variables: { workspaceId: workspaceId ?? '', projectId },\n skip: !workspaceId || !projectId,\n });\n const { data: projectData } = useQuery<{\n planmagnetProject: { id: string; name: string } | null;\n }>(GET_CRITICAL_PATH_PROJECT, { variables: { id: projectId }, skip: !projectId });\n\n if (!projectId) {\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold flex items-center gap-2\">\n <Route className=\"h-5 w-5 text-primary\" />\n Critical Path\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n Choose a project to see its critical path.\n </p>\n </div>\n <Card>\n <CardContent className=\"pt-6 space-y-3\">\n <label htmlFor=\"critical-path-project\" className=\"text-sm font-medium\">\n Project\n </label>\n <ProjectSelect\n id=\"critical-path-project\"\n value={pickerProjectId}\n onChange={setPickerProjectId}\n noneLabel=\"Choose a project…\"\n />\n <button\n type=\"button\"\n className=\"text-sm text-primary hover:underline disabled:pointer-events-none disabled:opacity-50\"\n disabled={!pickerProjectId}\n onClick={() =>\n navigate(`/projects/critical-path-page/${encodeURIComponent(pickerProjectId)}`)\n }\n >\n View critical path →\n </button>\n </CardContent>\n </Card>\n </div>\n );\n }\n\n // Only before the first load: a refetch must not blank the page.\n if (loading && !data)\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n if (error)\n return <div className=\"p-6 text-red-500\">Error computing critical path: {error.message}</div>;\n\n const path = data?.planmagnetCriticalPath.path ?? [];\n const slack = data?.planmagnetCriticalPath.slack ?? {};\n const totalMinutes = data?.planmagnetCriticalPath.totalDurationMinutes ?? 0;\n const cycles = data?.planmagnetDependencyCycles ?? [];\n const projectName = projectData?.planmagnetProject?.name;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold flex items-center gap-2\">\n <Route className=\"h-5 w-5 text-primary\" />\n Critical Path\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {projectName ?? 'This project'} — the longest chain of dependent work; a delay on any\n node here delays the whole chain.\n </p>\n </div>\n </div>\n\n {cycles.length > 0 && (\n <div\n role=\"alert\"\n className=\"flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive\"\n >\n <AlertTriangle className=\"h-4 w-4 mt-0.5 shrink-0\" />\n <div>\n <p className=\"font-medium\">\n {cycles.length} dependency cycle{cycles.length === 1 ? '' : 's'} detected.\n </p>\n <p>\n A cycle has no valid schedule, so the path below excludes the items involved. Break\n each cycle by removing or re-pointing one of its dependencies from the task detail\n page.\n </p>\n </div>\n </div>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle>Total duration</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-2xl font-bold\">{durationLabel(totalMinutes)}</p>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Work item spans plus dependency lag/lead, summed along the longest chain.\n </p>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>\n Path ({path.length} item{path.length === 1 ? '' : 's'})\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {path.map((node, index) => (\n <div\n key={node.id}\n className=\"flex items-center justify-between gap-3 rounded-md border border-input p-3\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-medium text-primary-foreground\">\n {index + 1}\n </span>\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left text-sm font-medium hover:underline\"\n onClick={() =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(node.id)}`)\n }\n >\n {node.key ? `${node.key} ` : ''}\n {node.title ?? 'Untitled work item'}\n </button>\n </div>\n <Badge\n variant={(slack[node.id] ?? 0) === 0 ? 'default' : 'secondary'}\n className=\"shrink-0\"\n >\n {(slack[node.id] ?? 0) === 0\n ? 'On critical path'\n : `${durationLabel(slack[node.id] ?? 0)} slack`}\n </Badge>\n </div>\n ))}\n {path.length === 0 && (\n <p className=\"text-center text-muted-foreground py-8\">\n No schedulable dependencies yet. Add dependencies from a task&apos;s detail page to\n compute a critical path.\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProjectsCriticalPathPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProjectsCriticalPathPagePage.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge } from '@burdenoff/fe-libs/ui';\nimport { Route, AlertTriangle } from 'lucide-react';\nimport { useEntityId } from '../hooks/useEntityId';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ProjectSelect } from '../controls/Pickers';\n\n/**\n * Critical path view for a project (BOFF-8047). planmagnetCriticalPath / planmagnetDependencyCycles were\n * served and prod-verified (duration-weighted CPM over WorkItemDependency, per the P0 decision that this type\n * is CPM-only) but had no UI. Reached from the sidebar (no project preselects one) and from a project's detail\n * page (\"Critical path\" button).\n */\n\nconst GET_CRITICAL_PATH = gql`\n query GetProjectCriticalPath($workspaceId: ID!, $projectId: ID) {\n planmagnetCriticalPath(workspaceId: $workspaceId, projectId: $projectId) {\n totalDurationMinutes\n slack\n path {\n id\n title\n key\n }\n }\n planmagnetDependencyCycles(workspaceId: $workspaceId, projectId: $projectId) {\n workItemIds\n length\n }\n }\n`;\n\nconst GET_CRITICAL_PATH_PROJECT = gql`\n query GetCriticalPathProject($id: ID!) {\n planmagnetProject(id: $id) {\n id\n name\n }\n }\n`;\n\nconst NON_CRITICAL_PAGE_SIZE = 100;\n\n// Every work item in the project that the critical-path chain does NOT run through (excludeIds is the\n// path's own ids), so the slack map's per-node minutes have somewhere to show up. Paged like\n// ExportWorkItemsButton: a project can have more items than one page, and stopping at the first page\n// would silently drop items with real slack from the list below.\nconst GET_NON_CRITICAL_WORK_ITEMS = gql`\n query GetNonCriticalWorkItems(\n $workspaceId: ID!\n $filter: PlanmagnetWorkItemFilter\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: $pagination) {\n items {\n id\n title\n key\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n`;\n\nfunction durationLabel(totalMinutes: number): string {\n if (totalMinutes <= 0) return '0 days';\n // Round once, then split. Rounding the day remainder separately printed \"1d 24h\" for 47.5–47.9 h, and fractional\n // minutes (durations are elapsed ms / 60000) printed decimals.\n const roundedMinutes = Math.round(totalMinutes);\n if (roundedMinutes < 60) return `${Math.max(1, roundedMinutes)}m`;\n const totalHours = Math.round(roundedMinutes / 60);\n const days = Math.floor(totalHours / 24);\n const hours = totalHours % 24;\n if (days === 0) return `${hours}h`;\n return hours > 0 ? `${days}d ${hours}h` : `${days} day${days === 1 ? '' : 's'}`;\n}\n\nexport function ProjectsCriticalPathPagePage() {\n const projectId = useEntityId();\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const [pickerProjectId, setPickerProjectId] = useState('');\n\n const { data, loading, error } = useQuery<{\n planmagnetCriticalPath: {\n totalDurationMinutes: number;\n // The SDL doc comment on this field says \"days\" but the resolver\n // actually returns MINUTES (wspace-planmagnet-svc\n // src/modules/dependency/services/dependency-graph.ts:53, backed by\n // durationMinutes() in dependency.service.ts) — same unit as\n // totalDurationMinutes above. Feed values through durationLabel()\n // below, never render this number bare. SDL doc fix tracked\n // separately; do not \"fix\" this comment to match the SDL.\n slack: Record<string, number>;\n path: { id: string; title: string | null; key: string | null }[];\n };\n planmagnetDependencyCycles: { workItemIds: string[]; length: number }[];\n }>(GET_CRITICAL_PATH, {\n variables: { workspaceId: workspaceId ?? '', projectId },\n skip: !workspaceId || !projectId,\n });\n const { data: projectData } = useQuery<{\n planmagnetProject: { id: string; name: string } | null;\n }>(GET_CRITICAL_PATH_PROJECT, { variables: { id: projectId }, skip: !projectId });\n\n const path = data?.planmagnetCriticalPath.path ?? [];\n const hasCriticalPath = path.length > 0;\n const pathIds = path.map((node) => node.id);\n\n const {\n data: otherData,\n loading: otherLoading,\n error: otherError,\n fetchMore: fetchMoreOther,\n } = useQuery<{\n planmagnetWorkItems: {\n items: { id: string; title: string; key: string }[];\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n };\n }>(GET_NON_CRITICAL_WORK_ITEMS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: { projectId, excludeIds: pathIds },\n pagination: { first: NON_CRITICAL_PAGE_SIZE },\n },\n skip: !workspaceId || !projectId || !hasCriticalPath,\n });\n\n const otherPageInfo = otherData?.planmagnetWorkItems.pageInfo;\n // A failed later page must not leave a short list that reads as complete: remember it and say so.\n const [moreError, setMoreError] = useState<string | null>(null);\n useEffect(() => {\n if (moreError || !otherPageInfo?.hasNextPage || !otherPageInfo.endCursor) return;\n const after = otherPageInfo.endCursor;\n fetchMoreOther({\n variables: { pagination: { first: NON_CRITICAL_PAGE_SIZE, after } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetWorkItems: {\n ...fetchMoreResult.planmagnetWorkItems,\n items: [...prev.planmagnetWorkItems.items, ...fetchMoreResult.planmagnetWorkItems.items],\n },\n }),\n })\n .then((result) => {\n // fe-libs queries resolve with { error } instead of rejecting.\n const failure = (result as { error?: { message?: string } } | undefined)?.error;\n if (failure) setMoreError(failure.message ?? 'Unknown error');\n })\n .catch((e: unknown) => setMoreError(e instanceof Error ? e.message : String(e)));\n }, [moreError, otherPageInfo?.hasNextPage, otherPageInfo?.endCursor, fetchMoreOther]);\n\n if (!projectId) {\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold flex items-center gap-2\">\n <Route className=\"h-5 w-5 text-primary\" />\n Critical Path\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n Choose a project to see its critical path.\n </p>\n </div>\n <Card>\n <CardContent className=\"pt-6 space-y-3\">\n <label htmlFor=\"critical-path-project\" className=\"text-sm font-medium\">\n Project\n </label>\n <ProjectSelect\n id=\"critical-path-project\"\n value={pickerProjectId}\n onChange={setPickerProjectId}\n noneLabel=\"Choose a project…\"\n />\n <button\n type=\"button\"\n className=\"text-sm text-primary hover:underline disabled:pointer-events-none disabled:opacity-50\"\n disabled={!pickerProjectId}\n onClick={() =>\n navigate(`/projects/critical-path-page/${encodeURIComponent(pickerProjectId)}`)\n }\n >\n View critical path →\n </button>\n </CardContent>\n </Card>\n </div>\n );\n }\n\n // Only before the first load: a refetch must not blank the page.\n if (loading && !data)\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n if (error)\n return <div className=\"p-6 text-red-500\">Error computing critical path: {error.message}</div>;\n\n const slack = data?.planmagnetCriticalPath.slack ?? {};\n const totalMinutes = data?.planmagnetCriticalPath.totalDurationMinutes ?? 0;\n const cycles = data?.planmagnetDependencyCycles ?? [];\n const projectName = projectData?.planmagnetProject?.name;\n const otherItems = [...(otherData?.planmagnetWorkItems.items ?? [])].sort(\n (a, b) => (slack[a.id] ?? 0) - (slack[b.id] ?? 0)\n );\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold flex items-center gap-2\">\n <Route className=\"h-5 w-5 text-primary\" />\n Critical Path\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {projectName ?? 'This project'} — the longest chain of dependent work; a delay on any\n node here delays the whole chain.\n </p>\n </div>\n </div>\n\n {cycles.length > 0 && (\n <div\n role=\"alert\"\n className=\"flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive\"\n >\n <AlertTriangle className=\"h-4 w-4 mt-0.5 shrink-0\" />\n <div>\n <p className=\"font-medium\">\n {cycles.length} dependency cycle{cycles.length === 1 ? '' : 's'} detected.\n </p>\n <p>\n A cycle has no valid schedule, so the path below excludes the items involved. Break\n each cycle by removing or re-pointing one of its dependencies from the task detail\n page.\n </p>\n </div>\n </div>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle>Total duration</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-2xl font-bold\">{durationLabel(totalMinutes)}</p>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Work item spans plus dependency lag/lead, summed along the longest chain.\n </p>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>\n Path ({path.length} item{path.length === 1 ? '' : 's'})\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {path.map((node, index) => (\n <div\n key={node.id}\n className=\"flex items-center justify-between gap-3 rounded-md border border-input p-3\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-medium text-primary-foreground\">\n {index + 1}\n </span>\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left text-sm font-medium hover:underline\"\n onClick={() =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(node.id)}`)\n }\n >\n {node.key ? `${node.key} ` : ''}\n {node.title ?? 'Untitled work item'}\n </button>\n </div>\n <Badge\n variant={(slack[node.id] ?? 0) === 0 ? 'default' : 'secondary'}\n className=\"shrink-0\"\n >\n {(slack[node.id] ?? 0) === 0\n ? 'On critical path'\n : `${durationLabel(slack[node.id] ?? 0)} slack`}\n </Badge>\n </div>\n ))}\n {path.length === 0 && (\n <p className=\"text-center text-muted-foreground py-8\">\n No schedulable dependencies yet. Add dependencies from a task&apos;s detail page to\n compute a critical path.\n </p>\n )}\n </CardContent>\n </Card>\n\n {hasCriticalPath && (\n <Card>\n <CardHeader>\n <CardTitle>\n Not on the critical path ({otherItems.length} item\n {otherItems.length === 1 ? '' : 's'})\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {otherItems.map((node) => (\n <div\n key={node.id}\n className=\"flex items-center justify-between gap-3 rounded-md border border-input p-3\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left text-sm font-medium hover:underline\"\n onClick={() =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(node.id)}`)\n }\n >\n {node.key ? `${node.key} ` : ''}\n {node.title}\n </button>\n <Badge variant=\"secondary\" className=\"shrink-0\">\n {durationLabel(slack[node.id] ?? 0)} slack\n </Badge>\n </div>\n ))}\n {!otherError && moreError && (\n <p className=\"text-center text-destructive py-4\" role=\"alert\">\n Some work items could not be loaded, so this list may be incomplete: {moreError}\n </p>\n )}\n {otherError && (\n <p className=\"text-center text-destructive py-8\">\n The rest of this project&apos;s work items could not be loaded: {otherError.message}\n </p>\n )}\n {!otherError && otherLoading && otherItems.length === 0 && (\n <p className=\"text-center text-muted-foreground py-8\">Loading…</p>\n )}\n {!otherError && !otherLoading && otherItems.length === 0 && (\n <p className=\"text-center text-muted-foreground py-8\">\n Every scheduled work item in this project is on the critical path.\n </p>\n )}\n </CardContent>\n </Card>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TemplatesTemplatesPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TemplatesTemplatesPagePage.tsx"],"names":[],"mappings":"AA2DA,wBAAgB,0BAA0B,4CAsLzC"}
1
+ {"version":3,"file":"TemplatesTemplatesPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TemplatesTemplatesPagePage.tsx"],"names":[],"mappings":"AA2DA,wBAAgB,0BAA0B,4CAqMzC"}
@@ -3,13 +3,13 @@ import { detailPath as n, stopRowActivation as r } from "../utils/rowNavigation.
3
3
  import { ApplyTemplateDialog as i, NewTemplateFromProjectDialog as a } from "../create/CreateDialogs.js";
4
4
  import { TemplateDeleteButton as o } from "../controls/TemplateControls.js";
5
5
  import { useDeferredValue as s, useState as c } from "react";
6
- import { LayoutTemplate as l, Pencil as u, Search as d } from "lucide-react";
7
- import { gql as f } from "@apollo/client";
8
- import { useQuery as p } from "@apollo/client/react";
9
- import { jsx as m, jsxs as h } from "react/jsx-runtime";
10
- import { Badge as g, Button as _, EmptyState as v, Input as y } from "@burdenoff/fe-libs/ui";
6
+ import { LayoutTemplate as l, Pencil as u, Plus as d, Search as f } from "lucide-react";
7
+ import { gql as p } from "@apollo/client";
8
+ import { useQuery as m } from "@apollo/client/react";
9
+ import { jsx as h, jsxs as g } from "react/jsx-runtime";
10
+ import { Badge as _, Button as v, EmptyState as y, Input as b } from "@burdenoff/fe-libs/ui";
11
11
  //#region src/planmagnet/pages/TemplatesTemplatesPagePage.tsx
12
- var b = f`
12
+ var x = p`
13
13
  query TemplatesList(
14
14
  $workspaceId: ID!
15
15
  $filter: PlanmagnetProjectTemplateFilter
@@ -37,27 +37,27 @@ var b = f`
37
37
  }
38
38
  }
39
39
  }
40
- `, x = 50;
41
- function S() {
42
- let { workspaceId: f } = e(), S = t(), [C, w] = c(""), [T, E] = c(!1), [D, O] = c(!1), k = s(C.trim()), A = {
43
- ...k ? { search: k } : {},
44
- ...T ? { isPublished: !0 } : {}
45
- }, { data: j, loading: M, error: N, fetchMore: P } = p(b, {
40
+ `, S = 50;
41
+ function C() {
42
+ let { workspaceId: p } = e(), C = t(), [w, T] = c(""), [E, D] = c(!1), [O, k] = c(!1), A = s(w.trim()), j = {
43
+ ...A ? { search: A } : {},
44
+ ...E ? { isPublished: !0 } : {}
45
+ }, { data: M, loading: N, error: P, fetchMore: F } = m(x, {
46
46
  variables: {
47
- workspaceId: f ?? "",
48
- filter: A,
49
- pagination: { first: x }
47
+ workspaceId: p ?? "",
48
+ filter: j,
49
+ pagination: { first: S }
50
50
  },
51
- skip: !f,
51
+ skip: !p,
52
52
  fetchPolicy: "cache-and-network"
53
- }), F = j?.planmagnetProjectTemplates, I = F?.items ?? [], L = async () => {
54
- let e = F?.pageInfo.endCursor;
53
+ }), I = M?.planmagnetProjectTemplates, L = I?.items ?? [], R = A !== "" || E, z = !!I && L.length === 0 && !R, B = async () => {
54
+ let e = I?.pageInfo.endCursor;
55
55
  if (e) {
56
- O(!0);
56
+ k(!0);
57
57
  try {
58
- await P({
58
+ await F({
59
59
  variables: { pagination: {
60
- first: x,
60
+ first: S,
61
61
  after: e
62
62
  } },
63
63
  updateQuery: (e, { fetchMoreResult: t }) => ({ planmagnetProjectTemplates: {
@@ -66,99 +66,106 @@ function S() {
66
66
  } })
67
67
  });
68
68
  } finally {
69
- O(!1);
69
+ k(!1);
70
70
  }
71
71
  }
72
72
  };
73
- return /* @__PURE__ */ h("div", {
73
+ return /* @__PURE__ */ g("div", {
74
74
  className: "space-y-6 p-6",
75
75
  "data-testid": "templates-page",
76
76
  children: [
77
- /* @__PURE__ */ h("div", {
77
+ /* @__PURE__ */ g("div", {
78
78
  className: "flex flex-wrap items-center justify-between gap-3",
79
- children: [/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("h1", {
79
+ children: [/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("h1", {
80
80
  className: "text-2xl font-semibold",
81
81
  children: "Project templates"
82
- }), /* @__PURE__ */ m("p", {
82
+ }), /* @__PURE__ */ h("p", {
83
83
  className: "text-sm text-muted-foreground",
84
- children: F ? `${F.totalCount} template${F.totalCount === 1 ? "" : "s"}` : "Reusable project configurations — capture one from a project, or start a project from one"
85
- })] }), /* @__PURE__ */ m(a, { onCreated: (e) => S(n("/templates/edit-template-page", e)) })]
84
+ children: I ? `${I.totalCount} template${I.totalCount === 1 ? "" : "s"}` : "Reusable project configurations — capture one from a project, or start a project from one"
85
+ })] }), !z && /* @__PURE__ */ h(a, { onCreated: (e) => C(n("/templates/edit-template-page", e)) })]
86
86
  }),
87
- /* @__PURE__ */ h("div", {
87
+ /* @__PURE__ */ g("div", {
88
88
  className: "flex flex-wrap items-center gap-3",
89
- children: [/* @__PURE__ */ h("div", {
89
+ children: [/* @__PURE__ */ g("div", {
90
90
  className: "relative w-full max-w-sm",
91
- children: [/* @__PURE__ */ m(d, { className: "absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" }), /* @__PURE__ */ m(y, {
91
+ children: [/* @__PURE__ */ h(f, { className: "absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" }), /* @__PURE__ */ h(b, {
92
92
  "aria-label": "Search templates",
93
93
  placeholder: "Search by name or description",
94
94
  className: "pl-8",
95
- value: C,
96
- onChange: (e) => w(e.target.value)
95
+ value: w,
96
+ onChange: (e) => T(e.target.value)
97
97
  })]
98
- }), /* @__PURE__ */ m("button", {
98
+ }), /* @__PURE__ */ h("button", {
99
99
  type: "button",
100
- "aria-pressed": T,
101
- onClick: () => E((e) => !e),
102
- className: `px-3 py-1.5 text-sm rounded-md ${T ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted"}`,
100
+ "aria-pressed": E,
101
+ onClick: () => D((e) => !e),
102
+ className: `px-3 py-1.5 text-sm rounded-md ${E ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted"}`,
103
103
  children: "Published only"
104
104
  })]
105
105
  }),
106
- N && /* @__PURE__ */ h("p", {
106
+ P && /* @__PURE__ */ g("p", {
107
107
  role: "alert",
108
108
  className: "text-sm text-destructive",
109
- children: ["Templates could not be loaded: ", N.message]
109
+ children: ["Templates could not be loaded: ", P.message]
110
110
  }),
111
- M && !j && /* @__PURE__ */ m("p", {
111
+ N && !M && /* @__PURE__ */ h("p", {
112
112
  className: "text-sm text-muted-foreground",
113
113
  children: "Loading templates…"
114
114
  }),
115
- F && I.length === 0 && /* @__PURE__ */ m(v, {
116
- icon: /* @__PURE__ */ m(l, { className: "h-8 w-8 text-muted-foreground" }),
117
- title: k || T ? "No templates match" : "No templates yet",
115
+ I && L.length === 0 && /* @__PURE__ */ h(y, {
116
+ icon: /* @__PURE__ */ h(l, { className: "h-8 w-8 text-muted-foreground" }),
117
+ title: R ? "No templates match" : "No templates yet",
118
118
  description: "Save an existing project's setup as a template, then start new projects from it in one step.",
119
- action: k || T ? void 0 : /* @__PURE__ */ m(a, { onCreated: (e) => S(n("/templates/edit-template-page", e)) })
119
+ action: z ? /* @__PURE__ */ h(a, {
120
+ onCreated: (e) => C(n("/templates/edit-template-page", e)),
121
+ trigger: /* @__PURE__ */ g(v, {
122
+ size: "sm",
123
+ "data-testid": "new-template-empty-trigger",
124
+ children: [/* @__PURE__ */ h(d, { className: "mr-1 h-4 w-4" }), "New template"]
125
+ })
126
+ }) : void 0
120
127
  }),
121
- /* @__PURE__ */ m("ul", {
128
+ /* @__PURE__ */ h("ul", {
122
129
  className: "grid grid-cols-1 gap-3 md:grid-cols-2",
123
- children: I.map((e) => /* @__PURE__ */ h("li", {
130
+ children: L.map((e) => /* @__PURE__ */ g("li", {
124
131
  "data-testid": "template-row",
125
132
  className: "flex flex-col gap-2 rounded-md border p-3",
126
133
  children: [
127
- /* @__PURE__ */ h("div", {
134
+ /* @__PURE__ */ g("div", {
128
135
  className: "flex items-start justify-between gap-2",
129
- children: [/* @__PURE__ */ h("div", {
136
+ children: [/* @__PURE__ */ g("div", {
130
137
  className: "min-w-0",
131
- children: [/* @__PURE__ */ m("p", {
138
+ children: [/* @__PURE__ */ h("p", {
132
139
  className: "truncate text-sm font-medium",
133
140
  children: e.name
134
- }), /* @__PURE__ */ m("p", {
141
+ }), /* @__PURE__ */ h("p", {
135
142
  className: "truncate text-xs text-muted-foreground",
136
143
  children: e.description ?? "No description"
137
144
  })]
138
- }), /* @__PURE__ */ h("div", {
145
+ }), /* @__PURE__ */ g("div", {
139
146
  className: "flex shrink-0 gap-1",
140
- children: [e.isBuiltIn && /* @__PURE__ */ m(g, {
147
+ children: [e.isBuiltIn && /* @__PURE__ */ h(_, {
141
148
  variant: "secondary",
142
149
  children: "Built-in"
143
- }), /* @__PURE__ */ m(g, {
150
+ }), /* @__PURE__ */ h(_, {
144
151
  variant: e.isPublished ? "success" : "outline",
145
152
  children: e.isPublished ? "Published" : "Draft"
146
153
  })]
147
154
  })]
148
155
  }),
149
- /* @__PURE__ */ h("div", {
156
+ /* @__PURE__ */ g("div", {
150
157
  className: "flex flex-wrap items-center gap-1",
151
- children: [e.category && /* @__PURE__ */ m(g, {
158
+ children: [e.category && /* @__PURE__ */ h(_, {
152
159
  variant: "outline",
153
160
  className: "text-xs",
154
161
  children: e.category
155
- }), e.tags.map((e) => /* @__PURE__ */ m(g, {
162
+ }), e.tags.map((e) => /* @__PURE__ */ h(_, {
156
163
  variant: "outline",
157
164
  className: "text-xs",
158
165
  children: e
159
166
  }, e))]
160
167
  }),
161
- /* @__PURE__ */ h("p", {
168
+ /* @__PURE__ */ g("p", {
162
169
  className: "text-xs text-muted-foreground",
163
170
  children: [
164
171
  "Used ",
@@ -167,42 +174,42 @@ function S() {
167
174
  e.usageCount === 1 ? "" : "s"
168
175
  ]
169
176
  }),
170
- /* @__PURE__ */ h("div", {
177
+ /* @__PURE__ */ g("div", {
171
178
  className: "flex items-center justify-between gap-2 border-t pt-2",
172
179
  onClick: r,
173
- children: [/* @__PURE__ */ h("div", {
180
+ children: [/* @__PURE__ */ g("div", {
174
181
  className: "flex gap-2",
175
- children: [/* @__PURE__ */ h(_, {
182
+ children: [/* @__PURE__ */ g(v, {
176
183
  size: "sm",
177
184
  variant: "outline",
178
- onClick: () => S(n("/templates/edit-template-page", e.id)),
185
+ onClick: () => C(n("/templates/edit-template-page", e.id)),
179
186
  "data-testid": "template-edit",
180
- children: [/* @__PURE__ */ m(u, { className: "mr-1 h-4 w-4" }), "Edit"]
181
- }), /* @__PURE__ */ m(o, {
187
+ children: [/* @__PURE__ */ h(u, { className: "mr-1 h-4 w-4" }), "Edit"]
188
+ }), /* @__PURE__ */ h(o, {
182
189
  templateId: e.id,
183
190
  name: e.name,
184
191
  isBuiltIn: e.isBuiltIn
185
192
  })]
186
- }), /* @__PURE__ */ m(i, {
193
+ }), /* @__PURE__ */ h(i, {
187
194
  templateId: e.id,
188
195
  templateName: e.name,
189
- onApplied: (e) => S(n("/projects/project-detail-page", e))
196
+ onApplied: (e) => C(n("/projects/project-detail-page", e))
190
197
  })]
191
198
  })
192
199
  ]
193
200
  }, e.id))
194
201
  }),
195
- F?.pageInfo.hasNextPage && /* @__PURE__ */ m(_, {
202
+ I?.pageInfo.hasNextPage && /* @__PURE__ */ h(v, {
196
203
  size: "sm",
197
204
  variant: "outline",
198
- onClick: () => void L(),
199
- disabled: D,
200
- children: D ? "Loading…" : `Show more (${I.length} of ${F.totalCount})`
205
+ onClick: () => void B(),
206
+ disabled: O,
207
+ children: O ? "Loading…" : `Show more (${L.length} of ${I.totalCount})`
201
208
  })
202
209
  ]
203
210
  });
204
211
  }
205
212
  //#endregion
206
- export { S as TemplatesTemplatesPagePage };
213
+ export { C as TemplatesTemplatesPagePage };
207
214
 
208
215
  //# sourceMappingURL=TemplatesTemplatesPagePage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TemplatesTemplatesPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TemplatesTemplatesPagePage.tsx"],"sourcesContent":["import { useDeferredValue, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Badge, Button, EmptyState, Input } from '@burdenoff/fe-libs/ui';\nimport { LayoutTemplate, Pencil, Search } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ApplyTemplateDialog, NewTemplateFromProjectDialog } from '../create/CreateDialogs';\nimport { TemplateDeleteButton } from '../controls/TemplateControls';\nimport { detailPath, stopRowActivation } from '../utils/rowNavigation';\n\n/**\n * Project templates browse surface (BOFF-8047, capability register \"Project templates\": model/API/authz were\n * already real and prod-verified — `planmagnetProjectTemplates`/Create/Update/Delete/Apply/CreateFromProject —\n * gate 3 (UI) was the only gap; \"No UI anywhere\" until now.\n */\n\nconst TEMPLATES = gql`\n query TemplatesList(\n $workspaceId: ID!\n $filter: PlanmagnetProjectTemplateFilter\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetProjectTemplates(\n workspaceId: $workspaceId\n filter: $filter\n pagination: $pagination\n ) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n name\n description\n category\n isBuiltIn\n isPublished\n tags\n usageCount\n }\n }\n }\n`;\n\ninterface TemplateRow {\n id: string;\n name: string;\n description: string | null;\n category: string | null;\n isBuiltIn: boolean;\n isPublished: boolean;\n tags: string[];\n usageCount: number;\n}\n\nconst PAGE = 50;\n\nexport function TemplatesTemplatesPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const [search, setSearch] = useState('');\n const [publishedOnly, setPublishedOnly] = useState(false);\n const [loadingMore, setLoadingMore] = useState(false);\n const term = useDeferredValue(search.trim());\n\n const filter = {\n ...(term ? { search: term } : {}),\n ...(publishedOnly ? { isPublished: true } : {}),\n };\n\n const { data, loading, error, fetchMore } = useQuery<{\n planmagnetProjectTemplates: {\n totalCount: number;\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n items: TemplateRow[];\n };\n }>(TEMPLATES, {\n variables: { workspaceId: workspaceId ?? '', filter, pagination: { first: PAGE } },\n skip: !workspaceId,\n fetchPolicy: 'cache-and-network',\n });\n\n const list = data?.planmagnetProjectTemplates;\n const templates = list?.items ?? [];\n\n const loadMore = async () => {\n const after = list?.pageInfo.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { first: PAGE, after } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetProjectTemplates: {\n ...fetchMoreResult.planmagnetProjectTemplates,\n items: [\n ...prev.planmagnetProjectTemplates.items,\n ...fetchMoreResult.planmagnetProjectTemplates.items,\n ],\n },\n }),\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\" data-testid=\"templates-page\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Project templates</h1>\n <p className=\"text-sm text-muted-foreground\">\n {list\n ? `${list.totalCount} template${list.totalCount === 1 ? '' : 's'}`\n : 'Reusable project configurations — capture one from a project, or start a project from one'}\n </p>\n </div>\n <NewTemplateFromProjectDialog\n onCreated={(id) => navigate(detailPath('/templates/edit-template-page', id))}\n />\n </div>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"relative w-full max-w-sm\">\n <Search className=\"absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground\" />\n <Input\n aria-label=\"Search templates\"\n placeholder=\"Search by name or description\"\n className=\"pl-8\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n </div>\n <button\n type=\"button\"\n aria-pressed={publishedOnly}\n onClick={() => setPublishedOnly((v) => !v)}\n className={`px-3 py-1.5 text-sm rounded-md ${publishedOnly ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted'}`}\n >\n Published only\n </button>\n </div>\n\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n Templates could not be loaded: {error.message}\n </p>\n )}\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading templates…</p>}\n {list && templates.length === 0 && (\n <EmptyState\n icon={<LayoutTemplate className=\"h-8 w-8 text-muted-foreground\" />}\n title={term || publishedOnly ? 'No templates match' : 'No templates yet'}\n description=\"Save an existing project's setup as a template, then start new projects from it in one step.\"\n action={\n !(term || publishedOnly) ? (\n <NewTemplateFromProjectDialog\n onCreated={(id) => navigate(detailPath('/templates/edit-template-page', id))}\n />\n ) : undefined\n }\n />\n )}\n\n <ul className=\"grid grid-cols-1 gap-3 md:grid-cols-2\">\n {templates.map((template) => (\n <li\n key={template.id}\n data-testid=\"template-row\"\n className=\"flex flex-col gap-2 rounded-md border p-3\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium\">{template.name}</p>\n <p className=\"truncate text-xs text-muted-foreground\">\n {template.description ?? 'No description'}\n </p>\n </div>\n <div className=\"flex shrink-0 gap-1\">\n {template.isBuiltIn && <Badge variant=\"secondary\">Built-in</Badge>}\n <Badge variant={template.isPublished ? 'success' : 'outline'}>\n {template.isPublished ? 'Published' : 'Draft'}\n </Badge>\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-1\">\n {template.category && (\n <Badge variant=\"outline\" className=\"text-xs\">\n {template.category}\n </Badge>\n )}\n {template.tags.map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n Used {template.usageCount} time{template.usageCount === 1 ? '' : 's'}\n </p>\n <div\n className=\"flex items-center justify-between gap-2 border-t pt-2\"\n onClick={stopRowActivation}\n >\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate(detailPath('/templates/edit-template-page', template.id))}\n data-testid=\"template-edit\"\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n <TemplateDeleteButton\n templateId={template.id}\n name={template.name}\n isBuiltIn={template.isBuiltIn}\n />\n </div>\n <ApplyTemplateDialog\n templateId={template.id}\n templateName={template.name}\n onApplied={(projectId) =>\n navigate(detailPath('/projects/project-detail-page', projectId))\n }\n />\n </div>\n </li>\n ))}\n </ul>\n {list?.pageInfo.hasNextPage && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => void loadMore()} disabled={loadingMore}>\n {loadingMore ? 'Loading…' : `Show more (${templates.length} of ${list.totalCount})`}\n </Button>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TemplatesTemplatesPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TemplatesTemplatesPagePage.tsx"],"sourcesContent":["import { useDeferredValue, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Badge, Button, EmptyState, Input } from '@burdenoff/fe-libs/ui';\nimport { LayoutTemplate, Pencil, Plus, Search } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ApplyTemplateDialog, NewTemplateFromProjectDialog } from '../create/CreateDialogs';\nimport { TemplateDeleteButton } from '../controls/TemplateControls';\nimport { detailPath, stopRowActivation } from '../utils/rowNavigation';\n\n/**\n * Project templates browse surface (BOFF-8047, capability register \"Project templates\": model/API/authz were\n * already real and prod-verified — `planmagnetProjectTemplates`/Create/Update/Delete/Apply/CreateFromProject —\n * gate 3 (UI) was the only gap; \"No UI anywhere\" until now.\n */\n\nconst TEMPLATES = gql`\n query TemplatesList(\n $workspaceId: ID!\n $filter: PlanmagnetProjectTemplateFilter\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetProjectTemplates(\n workspaceId: $workspaceId\n filter: $filter\n pagination: $pagination\n ) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n name\n description\n category\n isBuiltIn\n isPublished\n tags\n usageCount\n }\n }\n }\n`;\n\ninterface TemplateRow {\n id: string;\n name: string;\n description: string | null;\n category: string | null;\n isBuiltIn: boolean;\n isPublished: boolean;\n tags: string[];\n usageCount: number;\n}\n\nconst PAGE = 50;\n\nexport function TemplatesTemplatesPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const [search, setSearch] = useState('');\n const [publishedOnly, setPublishedOnly] = useState(false);\n const [loadingMore, setLoadingMore] = useState(false);\n const term = useDeferredValue(search.trim());\n\n const filter = {\n ...(term ? { search: term } : {}),\n ...(publishedOnly ? { isPublished: true } : {}),\n };\n\n const { data, loading, error, fetchMore } = useQuery<{\n planmagnetProjectTemplates: {\n totalCount: number;\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n items: TemplateRow[];\n };\n }>(TEMPLATES, {\n variables: { workspaceId: workspaceId ?? '', filter, pagination: { first: PAGE } },\n skip: !workspaceId,\n fetchPolicy: 'cache-and-network',\n });\n\n const list = data?.planmagnetProjectTemplates;\n const templates = list?.items ?? [];\n const isFiltered = term !== '' || publishedOnly;\n // The EmptyState below only offers its own \"New template\" action once the list is confirmed empty AND\n // unfiltered (same gate as its `title`/`description`). Whenever that action is showing, drop the header's\n // button instead of duplicating it (BOFF-8047 prod verification: both rendered at once, sharing\n // data-testid=\"new-template-trigger\"). Keep the header's button for every OTHER state — loading, a\n // populated list, or a filtered-to-empty list, where the EmptyState offers no action of its own.\n const showsEmptyStateAction = !!list && templates.length === 0 && !isFiltered;\n\n const loadMore = async () => {\n const after = list?.pageInfo.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { first: PAGE, after } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetProjectTemplates: {\n ...fetchMoreResult.planmagnetProjectTemplates,\n items: [\n ...prev.planmagnetProjectTemplates.items,\n ...fetchMoreResult.planmagnetProjectTemplates.items,\n ],\n },\n }),\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\" data-testid=\"templates-page\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Project templates</h1>\n <p className=\"text-sm text-muted-foreground\">\n {list\n ? `${list.totalCount} template${list.totalCount === 1 ? '' : 's'}`\n : 'Reusable project configurations — capture one from a project, or start a project from one'}\n </p>\n </div>\n {!showsEmptyStateAction && (\n <NewTemplateFromProjectDialog\n onCreated={(id) => navigate(detailPath('/templates/edit-template-page', id))}\n />\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"relative w-full max-w-sm\">\n <Search className=\"absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground\" />\n <Input\n aria-label=\"Search templates\"\n placeholder=\"Search by name or description\"\n className=\"pl-8\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n </div>\n <button\n type=\"button\"\n aria-pressed={publishedOnly}\n onClick={() => setPublishedOnly((v) => !v)}\n className={`px-3 py-1.5 text-sm rounded-md ${publishedOnly ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted'}`}\n >\n Published only\n </button>\n </div>\n\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n Templates could not be loaded: {error.message}\n </p>\n )}\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading templates…</p>}\n {list && templates.length === 0 && (\n <EmptyState\n icon={<LayoutTemplate className=\"h-8 w-8 text-muted-foreground\" />}\n title={isFiltered ? 'No templates match' : 'No templates yet'}\n description=\"Save an existing project's setup as a template, then start new projects from it in one step.\"\n action={\n showsEmptyStateAction ? (\n <NewTemplateFromProjectDialog\n onCreated={(id) => navigate(detailPath('/templates/edit-template-page', id))}\n trigger={\n <Button size=\"sm\" data-testid=\"new-template-empty-trigger\">\n <Plus className=\"mr-1 h-4 w-4\" />\n New template\n </Button>\n }\n />\n ) : undefined\n }\n />\n )}\n\n <ul className=\"grid grid-cols-1 gap-3 md:grid-cols-2\">\n {templates.map((template) => (\n <li\n key={template.id}\n data-testid=\"template-row\"\n className=\"flex flex-col gap-2 rounded-md border p-3\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium\">{template.name}</p>\n <p className=\"truncate text-xs text-muted-foreground\">\n {template.description ?? 'No description'}\n </p>\n </div>\n <div className=\"flex shrink-0 gap-1\">\n {template.isBuiltIn && <Badge variant=\"secondary\">Built-in</Badge>}\n <Badge variant={template.isPublished ? 'success' : 'outline'}>\n {template.isPublished ? 'Published' : 'Draft'}\n </Badge>\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-1\">\n {template.category && (\n <Badge variant=\"outline\" className=\"text-xs\">\n {template.category}\n </Badge>\n )}\n {template.tags.map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n Used {template.usageCount} time{template.usageCount === 1 ? '' : 's'}\n </p>\n <div\n className=\"flex items-center justify-between gap-2 border-t pt-2\"\n onClick={stopRowActivation}\n >\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate(detailPath('/templates/edit-template-page', template.id))}\n data-testid=\"template-edit\"\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n <TemplateDeleteButton\n templateId={template.id}\n name={template.name}\n isBuiltIn={template.isBuiltIn}\n />\n </div>\n <ApplyTemplateDialog\n templateId={template.id}\n templateName={template.name}\n onApplied={(projectId) =>\n navigate(detailPath('/projects/project-detail-page', projectId))\n }\n />\n </div>\n </li>\n ))}\n </ul>\n {list?.pageInfo.hasNextPage && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => void loadMore()} disabled={loadingMore}>\n {loadingMore ? 'Loading…' : `Show more (${templates.length} of ${list.totalCount})`}\n </Button>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-planmagnet",
3
- "version": "2026.928.7",
3
+ "version": "2026.928.8",
4
4
  "description": "Burdenoff PlanMagnet Microfrontend",
5
5
  "type": "module",
6
6
  "files": [