@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.
- package/dist/planmagnet/automation/AutomationRuleFields.d.ts.map +1 -1
- package/dist/planmagnet/automation/AutomationRuleFields.js +13 -12
- package/dist/planmagnet/automation/AutomationRuleFields.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.js +153 -63
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TemplatesTemplatesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TemplatesTemplatesPagePage.js +77 -70
- package/dist/planmagnet/pages/TemplatesTemplatesPagePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -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;
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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(
|
|
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(
|
|
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 {
|
|
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'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'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":";;;;;;;AAmCA,IAAM,IACJ,kGASI,IAAiB;AAEvB,SAAS,EAAkB,EACzB,WACA,eAIC;AACD,SAAQ,EAAO,MAAf;EACE,KAAK,iBACH,QAAO,kBAAC,GAAD;GAAgB,OAAO,EAAO;GAAiB;GAAY,CAAA;EACpE,KAAK,aACH,QACE,kBAAC,UAAD;GACE,WAAW;GACX,OAAO,EAAO;GACd,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;aAH3C,CAKE,kBAAC,UAAD;IAAQ,OAAM;cAAG;IAAwB,CAAA,EACxC,EAAmB,KAAK,MACvB,kBAAC,UAAD;IAAgB,OAAO;cACpB,EAAU,EAAE;IACN,EAFI,EAEJ,CACT,CACK;;EAEb,KAAK,eACH,QACE,kBAAC,UAAD;GACE,WAAW;GACX,OAAO,EAAO;GACd,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;aAH3C,CAKE,kBAAC,UAAD;IAAQ,OAAM;cAAG;IAA0B,CAAA,EAC1C,EAAqB,KAAK,MACzB,kBAAC,UAAD;IAAgB,OAAO;cACpB,EAAU,EAAE;IACN,EAFI,EAEJ,CACT,CACK;;EAGb,QACE,QACE,kBAAC,GAAD;GACE,cAAW;GACX,WAAW;GACX,aAAY;GACZ,OAAO,EAAO;GACd,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;GACzC,CAAA;;;AAMV,SAAgB,EAAc,EAC5B,YACA,eAIC;CACD,IAAM,KAAU,GAAe,MAC7B,EAAS,EAAQ,KAAK,GAAG,MAAO,MAAM,IAAQ;EAAE,GAAG;EAAG,GAAG;EAAO,GAAG,EAAG,CAAC,EACnE,KAAU,MAAkB,EAAS,EAAQ,QAAQ,GAAG,MAAM,MAAM,EAAM,CAAC;AAGjF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACG,EAAQ,KAAK,GAAQ,MAAU;IAK9B,IAAM,IAAS,EAAmC,SAAS,EAAO,KAAK,EACjE,IAAc,IAAQ,IAAe,CAAC,EAAO,MAAM,GAAG,EAAa;AACzE,WACE,kBAAC,OAAD;KAAiB,WAAU;eAA3B,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,UAAD;QACE,cAAW;QACX,WAAW,GAAG,EAAW;QACzB,OAAO,EAAO;QACd,WAAW,MACT,EAAO,GAAO;SACZ,MAAM,EAAE,OAAO;SACf,OAAO,EAAE,OAAO,UAAU,EAAO,OAAO,EAAO,QAAQ;SACxD,CAAC;kBAGH,EAAY,KAAK,MAChB,kBAAC,UAAD;SAAgB,OAAO;mBACpB,KAAS,MAAM,EAAO,OAAQ,EAAkB,MAAoB,IAAK;SACnE,EAFI,EAEJ,CACT;QACK,CAAA;OACT,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,GAAD;SAA2B;SAAQ,WAAW,MAAU,EAAO,GAAO,EAAE,UAAO,CAAC;SAAI,CAAA;QAChF,CAAA;OACN,kBAAC,GAAD;QACE,MAAK;QACL,MAAK;QACL,SAAQ;QACR,cAAW;QACX,eAAe,EAAO,EAAM;kBAE5B,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;QAClB,CAAA;OACL;SACL,CAAC,KACA,kBAAC,KAAD;MAAG,WAAU;gBAAgC;MAGzC,CAAA,CAEF;OAtCI,EAsCJ;KAER;GACF,kBAAC,GAAD;IACE,MAAK;IACL,MAAK;IACL,SAAQ;IACR,eAzDY,EAAS,CAAC,GAAG,GAAS;KAAE,MAAM;KAAkB,OAAO;KAAI,CAAC,CAAC;IA0DzE,eAAY;cALd,CAOE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,aAE1B;;GACR,EAAQ,WAAW,KAClB,kBAAC,KAAD;IAAG,WAAU;cAAgC;IAEzC,CAAA;GAEF;;;AAKV,SAAgB,EAAiB,EAC/B,eACA,eAIC;CACD,IAAM,KAAU,GAAe,MAC7B,EAAS,EAAW,KAAK,GAAG,MAAO,MAAM,IAAQ;EAAE,GAAG;EAAG,GAAG;EAAO,GAAG,EAAG,CAAC,EACtE,KAAU,MAAkB,EAAS,EAAW,QAAQ,GAAG,MAAM,MAAM,EAAM,CAAC;AAGpF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACG,EAAW,KAAK,GAAW,MAC1B,kBAAC,OAAD;IAAiB,WAAU;cAA3B;KACE,kBAAC,GAAD;MACE,cAAW;MACX,MAAK;MACL,WAAW,iBAAiB;MAC5B,aAAY;MACZ,OAAO,EAAU;MACjB,WAAW,MAAM,EAAO,GAAO,EAAE,OAAO,EAAE,OAAO,OAAO,CAAC;MACzD,CAAA;KACF,kBAAC,UAAD;MACE,cAAW;MACX,WAAW,GAAG,EAAW;MACzB,OAAO,EAAU;MACjB,WAAW,MAAM,EAAO,GAAO,EAAE,UAAU,EAAE,OAAO,OAA4B,CAAC;gBAEhF,EAAoB,KAAK,MACxB,kBAAC,UAAD;OAAiB,OAAO;iBACrB,EAAyB;OACnB,EAFI,EAEJ,CACT;MACK,CAAA;KACR,EAAU,aAAa,YACtB,kBAAC,GAAD;MACE,cAAW;MACX,WAAW,kBAAkB;MAC7B,aACE,EAAU,aAAa,QAAQ,EAAU,aAAa,UAClD,6BACA;MAEN,OAAO,EAAU;MACjB,WAAW,MAAM,EAAO,GAAO,EAAE,OAAO,EAAE,OAAO,OAAO,CAAC;MACzD,CAAA;KAEJ,kBAAC,GAAD;MACE,MAAK;MACL,MAAK;MACL,SAAQ;MACR,cAAW;MACX,eAAe,EAAO,EAAM;gBAE5B,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;MAClB,CAAA;KACL;MA3CI,EA2CJ,CACN;GACF,kBAAC,YAAD;IAAU,IAAG;cACV,EAAiB,KAAK,MACrB,kBAAC,UAAD,EAAgB,OAAO,GAAK,EAAf,EAAe,CAC5B;IACO,CAAA;GACX,kBAAC,GAAD;IACE,MAAK;IACL,MAAK;IACL,SAAQ;IACR,eA3DY,EAAS,CAAC,GAAG,GAAY;KAAE,OAAO;KAAU,UAAU;KAAM,OAAO;KAAI,CAAC,CAAC;IA4DrF,eAAY;cALd,CAOE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,gBAE1B;;GACR,EAAW,WAAW,KACrB,kBAAC,KAAD;IAAG,WAAU;cAAgC;IAEzC,CAAA;GAEF;;;AAKV,SAAgB,EAAqB,EACnC,SACA,aACA,mBAAgB,MAMf;AACD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;IAAO,SAAQ;IAAkB,WAAU;cAAsB;IAEzD,CAAA,EACR,kBAAC,GAAD;IACE,IAAG;IACH,WAAW,QAAQ;IACnB,OAAO,EAAK;IACZ,WAAW,MAAM,EAAS;KAAE,GAAG;KAAM,MAAM,EAAE,OAAO;KAAO,CAAC;IAC5D,aAAY;IACZ,CAAA,CACE,EAAA,CAAA;GACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;IAAO,SAAQ;IAAyB,WAAU;cAAsB;IAEhE,CAAA,EACR,kBAAC,YAAD;IACE,IAAG;IACH,WAAU;IACV,OAAO,EAAK;IACZ,WAAW,MAAM,EAAS;KAAE,GAAG;KAAM,aAAa,EAAE,OAAO;KAAO,CAAC;IACnE,aAAY;IACZ,CAAA,CACE,EAAA,CAAA;GACN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;KAAO,SAAQ;KAAqB,WAAU;eAAsB;KAE5D,CAAA,EACP,IACC,kBAAC,KAAD;KAAG,WAAU;eAAqC;KAE9C,CAAA,GAEJ,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MACE,IAAG;MACH,OAAO,EAAK;MACZ,WAAW,MAAc,EAAS;OAAE,GAAG;OAAM;OAAW,CAAC;MACzD,CAAA;KACE,CAAA,CAEJ,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA;KACE,kBAAC,SAAD;MAAO,SAAQ;MAAqB,WAAU;gBAAsB;MAE5D,CAAA;KACR,kBAAC,UAAD;MACE,IAAG;MACH,WAAW;MACX,OAAO,EAAK;MACZ,WAAW,MAAM,EAAS;OAAE,GAAG;OAAM,aAAa,EAAE,OAAO;OAAsB,CAAC;gBAEjF,EAAc,KAAK,MAClB,kBAAC,UAAD;OAAgB,OAAO;iBAAvB,CACG,EAAmB,IACnB,EAAwB,IAAI,EAAE,GAAG,KAAK,uBAChC;SAHI,EAGJ,CACT;MACK,CAAA;KACR,CAAC,EAAwB,IAAI,EAAK,YAAY,IAC7C,kBAAC,KAAD;MAAG,WAAU;gBAAb;OAAkD;OACa;OAC7D,kBAAC,QAAD;QAAM,WAAU;kBAAc;QAAc,CAAA;;OAC1C;;KAEF,EAAA,CAAA,CACF;;GACN,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,KAAD;KAAG,WAAU;eAAsB;KAAc,CAAA;IACjD,kBAAC,KAAD;KAAG,WAAU;eAAqC;KAE9C,CAAA;IACJ,kBAAC,GAAD;KACE,YAAY,EAAK;KACjB,WAAW,MAAe,EAAS;MAAE,GAAG;MAAM;MAAY,CAAC;KAC3D,CAAA;IACE,EAAA,CAAA;GACN,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,KAAD;KAAG,WAAU;eAAsB;KAAW,CAAA;IAC9C,kBAAC,KAAD;KAAG,WAAU;eAAqC;KAE9C,CAAA;IACJ,kBAAC,GAAD;KACE,SAAS,EAAK;KACd,WAAW,MAAY,EAAS;MAAE,GAAG;MAAM;MAAS,CAAC;KACrD,CAAA;IACE,EAAA,CAAA;GACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectsCriticalPathPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProjectsCriticalPathPagePage.tsx"],"names":[],"mappings":"
|
|
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 {
|
|
5
|
-
import { AlertTriangle as
|
|
6
|
-
import { gql as
|
|
7
|
-
import { useQuery as
|
|
8
|
-
import { jsx as
|
|
9
|
-
import { Badge as
|
|
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
|
|
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
|
-
`,
|
|
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
|
|
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
|
|
43
|
-
let
|
|
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:
|
|
46
|
-
projectId:
|
|
63
|
+
workspaceId: S ?? "",
|
|
64
|
+
projectId: c
|
|
47
65
|
},
|
|
48
|
-
skip: !
|
|
49
|
-
}), { data:
|
|
50
|
-
variables: { id:
|
|
51
|
-
skip: !
|
|
52
|
-
})
|
|
53
|
-
|
|
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__ */
|
|
104
|
+
children: [/* @__PURE__ */ d("div", { children: [/* @__PURE__ */ d("h1", {
|
|
56
105
|
className: "text-2xl font-semibold flex items-center gap-2",
|
|
57
|
-
children: [/* @__PURE__ */
|
|
58
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
110
|
+
})] }), /* @__PURE__ */ u(p, { children: /* @__PURE__ */ d(m, {
|
|
62
111
|
className: "pt-6 space-y-3",
|
|
63
112
|
children: [
|
|
64
|
-
/* @__PURE__ */
|
|
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__ */
|
|
118
|
+
/* @__PURE__ */ u(r, {
|
|
70
119
|
id: "critical-path-project",
|
|
71
|
-
value:
|
|
72
|
-
onChange:
|
|
120
|
+
value: w,
|
|
121
|
+
onChange: T,
|
|
73
122
|
noneLabel: "Choose a project…"
|
|
74
123
|
}),
|
|
75
|
-
/* @__PURE__ */
|
|
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: !
|
|
79
|
-
onClick: () =>
|
|
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 (
|
|
134
|
+
if (D && !E) return /* @__PURE__ */ u("div", {
|
|
86
135
|
className: "flex items-center justify-center p-12",
|
|
87
|
-
children: /* @__PURE__ */
|
|
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 (
|
|
138
|
+
if (O) return /* @__PURE__ */ d("div", {
|
|
90
139
|
className: "p-6 text-red-500",
|
|
91
|
-
children: ["Error computing critical path: ",
|
|
140
|
+
children: ["Error computing critical path: ", O.message]
|
|
92
141
|
});
|
|
93
|
-
let
|
|
94
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
146
|
+
/* @__PURE__ */ u("div", {
|
|
98
147
|
className: "flex items-center justify-between",
|
|
99
|
-
children: /* @__PURE__ */
|
|
148
|
+
children: /* @__PURE__ */ d("div", { children: [/* @__PURE__ */ d("h1", {
|
|
100
149
|
className: "text-2xl font-semibold flex items-center gap-2",
|
|
101
|
-
children: [/* @__PURE__ */
|
|
102
|
-
}), /* @__PURE__ */
|
|
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: [
|
|
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
|
-
|
|
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__ */
|
|
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
|
-
|
|
162
|
+
H.length,
|
|
114
163
|
" dependency cycle",
|
|
115
|
-
|
|
164
|
+
H.length === 1 ? "" : "s",
|
|
116
165
|
" detected."
|
|
117
166
|
]
|
|
118
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
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:
|
|
123
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
176
|
+
/* @__PURE__ */ d(p, { children: [/* @__PURE__ */ u(h, { children: /* @__PURE__ */ d(g, { children: [
|
|
128
177
|
"Path (",
|
|
129
|
-
|
|
178
|
+
A.length,
|
|
130
179
|
" item",
|
|
131
|
-
|
|
180
|
+
A.length === 1 ? "" : "s",
|
|
132
181
|
")"
|
|
133
|
-
] }) }), /* @__PURE__ */
|
|
182
|
+
] }) }), /* @__PURE__ */ d(m, {
|
|
134
183
|
className: "space-y-2",
|
|
135
|
-
children: [
|
|
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__ */
|
|
186
|
+
children: [/* @__PURE__ */ d("div", {
|
|
138
187
|
className: "flex items-center gap-3 min-w-0",
|
|
139
|
-
children: [/* @__PURE__ */
|
|
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__ */
|
|
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: () =>
|
|
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__ */
|
|
149
|
-
variant: (
|
|
197
|
+
}), /* @__PURE__ */ u(f, {
|
|
198
|
+
variant: (B[e.id] ?? 0) === 0 ? "default" : "secondary",
|
|
150
199
|
className: "shrink-0",
|
|
151
|
-
children: (
|
|
200
|
+
children: (B[e.id] ?? 0) === 0 ? "On critical path" : `${x(B[e.id] ?? 0)} slack`
|
|
152
201
|
})]
|
|
153
|
-
}, e.id)),
|
|
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 {
|
|
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'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'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'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":";;;;;;;;;;AAgBA,IAAM,IAAoB,CAAG;;;;;;;;;;;;;;;;GAkBvB,IAA4B,CAAG;;;;;;;GAS/B,IAAyB,KAMzB,IAA8B,CAAG;;;;;;;;;;;;;;;;;;;AAoBvC,SAAS,EAAc,GAA8B;AACnD,KAAI,KAAgB,EAAG,QAAO;CAG9B,IAAM,IAAiB,KAAK,MAAM,EAAa;AAC/C,KAAI,IAAiB,GAAI,QAAO,GAAG,KAAK,IAAI,GAAG,EAAe,CAAC;CAC/D,IAAM,IAAa,KAAK,MAAM,IAAiB,GAAG,EAC5C,IAAO,KAAK,MAAM,IAAa,GAAG,EAClC,IAAQ,IAAa;AAE3B,QADI,MAAS,IAAU,GAAG,EAAM,KACzB,IAAQ,IAAI,GAAG,EAAK,IAAI,EAAM,KAAK,GAAG,EAAK,MAAM,MAAS,IAAI,KAAK;;AAG5E,SAAgB,IAA+B;CAC7C,IAAM,IAAY,GAAa,EACzB,EAAE,mBAAgB,GAAe,EACjC,IAAW,GAAuB,EAClC,CAAC,GAAiB,KAAsB,EAAS,GAAG,EAEpD,EAAE,SAAM,YAAS,aAAU,EAc9B,GAAmB;EACpB,WAAW;GAAE,aAAa,KAAe;GAAI;GAAW;EACxD,MAAM,CAAC,KAAe,CAAC;EACxB,CAAC,EACI,EAAE,MAAM,MAAgB,EAE3B,GAA2B;EAAE,WAAW,EAAE,IAAI,GAAW;EAAE,MAAM,CAAC;EAAW,CAAC,EAE3E,IAAO,GAAM,uBAAuB,QAAQ,EAAE,EAC9C,IAAkB,EAAK,SAAS,GAChC,IAAU,EAAK,KAAK,MAAS,EAAK,GAAG,EAErC,EACJ,MAAM,GACN,SAAS,GACT,OAAO,GACP,WAAW,MACT,EAKD,GAA6B;EAC9B,WAAW;GACT,aAAa,KAAe;GAC5B,QAAQ;IAAE;IAAW,YAAY;IAAS;GAC1C,YAAY,EAAE,OAAO,GAAwB;GAC9C;EACD,MAAM,CAAC,KAAe,CAAC,KAAa,CAAC;EACtC,CAAC,EAEI,IAAgB,GAAW,oBAAoB,UAE/C,CAAC,GAAW,KAAgB,EAAwB,KAAK;AAqB/D,KApBA,QAAgB;AACd,MAAI,KAAa,CAAC,GAAe,eAAe,CAAC,EAAc,UAAW;EAC1E,IAAM,IAAQ,EAAc;AAC5B,IAAe;GACb,WAAW,EAAE,YAAY;IAAE,OAAO;IAAwB;IAAO,EAAE;GACnE,cAAc,GAAM,EAAE,0BAAuB,EAC3C,qBAAqB;IACnB,GAAG,EAAgB;IACnB,OAAO,CAAC,GAAG,EAAK,oBAAoB,OAAO,GAAG,EAAgB,oBAAoB,MAAM;IACzF,EACF;GACF,CAAC,CACC,MAAM,MAAW;GAEhB,IAAM,IAAW,GAAyD;AAC1E,GAAI,KAAS,EAAa,EAAQ,WAAW,gBAAgB;IAC7D,CACD,OAAO,MAAe,EAAa,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,CAAC,CAAC;IACjF;EAAC;EAAW,GAAe;EAAa,GAAe;EAAW;EAAe,CAAC,EAEjF,CAAC,EACH,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;GAAI,WAAU;aAAd,CACE,kBAAC,GAAD,EAAO,WAAU,wBAAyB,CAAA,EAAA,gBAEvC;MACL,kBAAC,KAAD;GAAG,WAAU;aAAgC;GAEzC,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GAAa,WAAU;aAAvB;IACE,kBAAC,SAAD;KAAO,SAAQ;KAAwB,WAAU;eAAsB;KAE/D,CAAA;IACR,kBAAC,GAAD;KACE,IAAG;KACH,OAAO;KACP,UAAU;KACV,WAAU;KACV,CAAA;IACF,kBAAC,UAAD;KACE,MAAK;KACL,WAAU;KACV,UAAU,CAAC;KACX,eACE,EAAS,gCAAgC,mBAAmB,EAAgB,GAAG;eAElF;KAEQ,CAAA;IACG;MACT,CAAA,CACH;;AAKV,KAAI,KAAW,CAAC,EACd,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD,EAAK,WAAU,kFAAmF,CAAA;EAC9F,CAAA;AAEV,KAAI,EACF,QAAO,kBAAC,OAAD;EAAK,WAAU;YAAf,CAAkC,mCAAgC,EAAM,QAAc;;CAE/F,IAAM,IAAQ,GAAM,uBAAuB,SAAS,EAAE,EAChD,IAAe,GAAM,uBAAuB,wBAAwB,GACpE,IAAS,GAAM,8BAA8B,EAAE,EAC/C,IAAc,GAAa,mBAAmB,MAC9C,IAAa,CAAC,GAAI,GAAW,oBAAoB,SAAS,EAAE,CAAE,CAAC,MAClE,GAAG,OAAO,EAAM,EAAE,OAAO,MAAM,EAAM,EAAE,OAAO,GAChD;AAED,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eAAd,CACE,kBAAC,GAAD,EAAO,WAAU,wBAAyB,CAAA,EAAA,gBAEvC;QACL,kBAAC,KAAD;KAAG,WAAU;eAAb,CACG,KAAe,gBAAe,2FAE7B;OACA,EAAA,CAAA;IACF,CAAA;GAEL,EAAO,SAAS,KACf,kBAAC,OAAD;IACE,MAAK;IACL,WAAU;cAFZ,CAIE,kBAAC,GAAD,EAAe,WAAU,2BAA4B,CAAA,EACrD,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG,EAAO;MAAO;MAAkB,EAAO,WAAW,IAAI,KAAK;MAAI;MAC9D;QACJ,kBAAC,KAAD,EAAA,UAAG,gLAIC,CAAA,CACA,EAAA,CAAA,CACF;;GAGR,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAW,kBAA0B,CAAA,EAC1B,CAAA,EACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,KAAD;IAAG,WAAU;cAAsB,EAAc,EAAa;IAAK,CAAA,EACnE,kBAAC,KAAD;IAAG,WAAU;cAAqC;IAE9C,CAAA,CACQ,EAAA,CAAA,CACT,EAAA,CAAA;GAEP,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAA;IAAW;IACF,EAAK;IAAO;IAAM,EAAK,WAAW,IAAI,KAAK;IAAI;IAC5C,EAAA,CAAA,EACD,CAAA,EACb,kBAAC,GAAD;IAAa,WAAU;cAAvB,CACG,EAAK,KAAK,GAAM,MACf,kBAAC,OAAD;KAEE,WAAU;eAFZ,CAIE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBACb,IAAQ;OACJ,CAAA,EACP,kBAAC,UAAD;OACE,MAAK;OACL,WAAU;OACV,eACE,EAAS,kCAAkC,mBAAmB,EAAK,GAAG,GAAG;iBAJ7E,CAOG,EAAK,MAAM,GAAG,EAAK,IAAI,KAAK,IAC5B,EAAK,SAAS,qBACR;SACL;SACN,kBAAC,GAAD;MACE,UAAU,EAAM,EAAK,OAAO,OAAO,IAAI,YAAY;MACnD,WAAU;iBAER,EAAM,EAAK,OAAO,OAAO,IACvB,qBACA,GAAG,EAAc,EAAM,EAAK,OAAO,EAAE,CAAC;MACpC,CAAA,CACJ;OA1BC,EAAK,GA0BN,CACN,EACD,EAAK,WAAW,KACf,kBAAC,KAAD;KAAG,WAAU;eAAyC;KAGlD,CAAA,CAEM;MACT,EAAA,CAAA;GAEN,KACC,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAA;IAAW;IACkB,EAAW;IAAO;IAC5C,EAAW,WAAW,IAAI,KAAK;IAAI;IAC1B,EAAA,CAAA,EACD,CAAA,EACb,kBAAC,GAAD;IAAa,WAAU;cAAvB;KACG,EAAW,KAAK,MACf,kBAAC,OAAD;MAEE,WAAU;gBAFZ,CAIE,kBAAC,UAAD;OACE,MAAK;OACL,WAAU;OACV,eACE,EAAS,kCAAkC,mBAAmB,EAAK,GAAG,GAAG;iBAJ7E,CAOG,EAAK,MAAM,GAAG,EAAK,IAAI,KAAK,IAC5B,EAAK,MACC;UACT,kBAAC,GAAD;OAAO,SAAQ;OAAY,WAAU;iBAArC,CACG,EAAc,EAAM,EAAK,OAAO,EAAE,EAAC,SAC9B;SACJ;QAhBC,EAAK,GAgBN,CACN;KACD,CAAC,KAAc,KACd,kBAAC,KAAD;MAAG,WAAU;MAAoC,MAAK;gBAAtD,CAA8D,yEACU,EACpE;;KAEL,KACC,kBAAC,KAAD;MAAG,WAAU;gBAAb,CAAiD,+DACkB,EAAW,QAC1E;;KAEL,CAAC,KAAc,KAAgB,EAAW,WAAW,KACpD,kBAAC,KAAD;MAAG,WAAU;gBAAyC;MAAY,CAAA;KAEnE,CAAC,KAAc,CAAC,KAAgB,EAAW,WAAW,KACrD,kBAAC,KAAD;MAAG,WAAU;gBAAyC;MAElD,CAAA;KAEM;MACT,EAAA,CAAA;GAEL"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TemplatesTemplatesPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TemplatesTemplatesPagePage.tsx"],"names":[],"mappings":"AA2DA,wBAAgB,0BAA0B,
|
|
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,
|
|
7
|
-
import { gql as
|
|
8
|
-
import { useQuery as
|
|
9
|
-
import { jsx as
|
|
10
|
-
import { Badge as
|
|
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
|
|
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
|
-
`,
|
|
41
|
-
function
|
|
42
|
-
let { workspaceId:
|
|
43
|
-
...
|
|
44
|
-
...
|
|
45
|
-
}, { data:
|
|
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:
|
|
48
|
-
filter:
|
|
49
|
-
pagination: { first:
|
|
47
|
+
workspaceId: p ?? "",
|
|
48
|
+
filter: j,
|
|
49
|
+
pagination: { first: S }
|
|
50
50
|
},
|
|
51
|
-
skip: !
|
|
51
|
+
skip: !p,
|
|
52
52
|
fetchPolicy: "cache-and-network"
|
|
53
|
-
}),
|
|
54
|
-
let e =
|
|
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
|
-
|
|
56
|
+
k(!0);
|
|
57
57
|
try {
|
|
58
|
-
await
|
|
58
|
+
await F({
|
|
59
59
|
variables: { pagination: {
|
|
60
|
-
first:
|
|
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
|
-
|
|
69
|
+
k(!1);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
|
-
return /* @__PURE__ */
|
|
73
|
+
return /* @__PURE__ */ g("div", {
|
|
74
74
|
className: "space-y-6 p-6",
|
|
75
75
|
"data-testid": "templates-page",
|
|
76
76
|
children: [
|
|
77
|
-
/* @__PURE__ */
|
|
77
|
+
/* @__PURE__ */ g("div", {
|
|
78
78
|
className: "flex flex-wrap items-center justify-between gap-3",
|
|
79
|
-
children: [/* @__PURE__ */
|
|
79
|
+
children: [/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("h1", {
|
|
80
80
|
className: "text-2xl font-semibold",
|
|
81
81
|
children: "Project templates"
|
|
82
|
-
}), /* @__PURE__ */
|
|
82
|
+
}), /* @__PURE__ */ h("p", {
|
|
83
83
|
className: "text-sm text-muted-foreground",
|
|
84
|
-
children:
|
|
85
|
-
})] }), /* @__PURE__ */
|
|
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__ */
|
|
87
|
+
/* @__PURE__ */ g("div", {
|
|
88
88
|
className: "flex flex-wrap items-center gap-3",
|
|
89
|
-
children: [/* @__PURE__ */
|
|
89
|
+
children: [/* @__PURE__ */ g("div", {
|
|
90
90
|
className: "relative w-full max-w-sm",
|
|
91
|
-
children: [/* @__PURE__ */
|
|
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:
|
|
96
|
-
onChange: (e) =>
|
|
95
|
+
value: w,
|
|
96
|
+
onChange: (e) => T(e.target.value)
|
|
97
97
|
})]
|
|
98
|
-
}), /* @__PURE__ */
|
|
98
|
+
}), /* @__PURE__ */ h("button", {
|
|
99
99
|
type: "button",
|
|
100
|
-
"aria-pressed":
|
|
101
|
-
onClick: () =>
|
|
102
|
-
className: `px-3 py-1.5 text-sm rounded-md ${
|
|
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
|
-
|
|
106
|
+
P && /* @__PURE__ */ g("p", {
|
|
107
107
|
role: "alert",
|
|
108
108
|
className: "text-sm text-destructive",
|
|
109
|
-
children: ["Templates could not be loaded: ",
|
|
109
|
+
children: ["Templates could not be loaded: ", P.message]
|
|
110
110
|
}),
|
|
111
|
-
|
|
111
|
+
N && !M && /* @__PURE__ */ h("p", {
|
|
112
112
|
className: "text-sm text-muted-foreground",
|
|
113
113
|
children: "Loading templates…"
|
|
114
114
|
}),
|
|
115
|
-
|
|
116
|
-
icon: /* @__PURE__ */
|
|
117
|
-
title:
|
|
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:
|
|
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__ */
|
|
128
|
+
/* @__PURE__ */ h("ul", {
|
|
122
129
|
className: "grid grid-cols-1 gap-3 md:grid-cols-2",
|
|
123
|
-
children:
|
|
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__ */
|
|
134
|
+
/* @__PURE__ */ g("div", {
|
|
128
135
|
className: "flex items-start justify-between gap-2",
|
|
129
|
-
children: [/* @__PURE__ */
|
|
136
|
+
children: [/* @__PURE__ */ g("div", {
|
|
130
137
|
className: "min-w-0",
|
|
131
|
-
children: [/* @__PURE__ */
|
|
138
|
+
children: [/* @__PURE__ */ h("p", {
|
|
132
139
|
className: "truncate text-sm font-medium",
|
|
133
140
|
children: e.name
|
|
134
|
-
}), /* @__PURE__ */
|
|
141
|
+
}), /* @__PURE__ */ h("p", {
|
|
135
142
|
className: "truncate text-xs text-muted-foreground",
|
|
136
143
|
children: e.description ?? "No description"
|
|
137
144
|
})]
|
|
138
|
-
}), /* @__PURE__ */
|
|
145
|
+
}), /* @__PURE__ */ g("div", {
|
|
139
146
|
className: "flex shrink-0 gap-1",
|
|
140
|
-
children: [e.isBuiltIn && /* @__PURE__ */
|
|
147
|
+
children: [e.isBuiltIn && /* @__PURE__ */ h(_, {
|
|
141
148
|
variant: "secondary",
|
|
142
149
|
children: "Built-in"
|
|
143
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
156
|
+
/* @__PURE__ */ g("div", {
|
|
150
157
|
className: "flex flex-wrap items-center gap-1",
|
|
151
|
-
children: [e.category && /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
180
|
+
children: [/* @__PURE__ */ g("div", {
|
|
174
181
|
className: "flex gap-2",
|
|
175
|
-
children: [/* @__PURE__ */
|
|
182
|
+
children: [/* @__PURE__ */ g(v, {
|
|
176
183
|
size: "sm",
|
|
177
184
|
variant: "outline",
|
|
178
|
-
onClick: () =>
|
|
185
|
+
onClick: () => C(n("/templates/edit-template-page", e.id)),
|
|
179
186
|
"data-testid": "template-edit",
|
|
180
|
-
children: [/* @__PURE__ */
|
|
181
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
193
|
+
}), /* @__PURE__ */ h(i, {
|
|
187
194
|
templateId: e.id,
|
|
188
195
|
templateName: e.name,
|
|
189
|
-
onApplied: (e) =>
|
|
196
|
+
onApplied: (e) => C(n("/projects/project-detail-page", e))
|
|
190
197
|
})]
|
|
191
198
|
})
|
|
192
199
|
]
|
|
193
200
|
}, e.id))
|
|
194
201
|
}),
|
|
195
|
-
|
|
202
|
+
I?.pageInfo.hasNextPage && /* @__PURE__ */ h(v, {
|
|
196
203
|
size: "sm",
|
|
197
204
|
variant: "outline",
|
|
198
|
-
onClick: () => void
|
|
199
|
-
disabled:
|
|
200
|
-
children:
|
|
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 {
|
|
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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"}
|