@burdenoff/microfe-planmagnet 2026.925.12 → 2026.925.13
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/export/ExportWorkItemsButton.d.ts +4 -0
- package/dist/planmagnet/export/ExportWorkItemsButton.d.ts.map +1 -0
- package/dist/planmagnet/export/ExportWorkItemsButton.js +117 -0
- package/dist/planmagnet/export/ExportWorkItemsButton.js.map +1 -0
- package/dist/planmagnet/export/workItemsCsv.d.ts +31 -0
- package/dist/planmagnet/export/workItemsCsv.d.ts.map +1 -0
- package/dist/planmagnet/export/workItemsCsv.js +32 -0
- package/dist/planmagnet/export/workItemsCsv.js.map +1 -0
- package/dist/planmagnet/pages/TasksTasksPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTasksPagePage.js +108 -103
- package/dist/planmagnet/pages/TasksTasksPagePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExportWorkItemsButton.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/export/ExportWorkItemsButton.tsx"],"names":[],"mappings":"AA2DA,wBAAgB,qBAAqB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,2CAkF1E"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { usePlanMagnet as e } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { downloadCsv as t, workItemsCsv as n } from "./workItemsCsv.js";
|
|
3
|
+
import { useState as r } from "react";
|
|
4
|
+
import { Download as i } from "lucide-react";
|
|
5
|
+
import { gql as a } from "@apollo/client";
|
|
6
|
+
import { useApolloClient as o } from "@apollo/client/react";
|
|
7
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
8
|
+
import { Button as l } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
//#region src/planmagnet/export/ExportWorkItemsButton.tsx
|
|
10
|
+
var u = a`
|
|
11
|
+
query ExportWorkItemsPage($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter, $after: String) {
|
|
12
|
+
planmagnetWorkItems(
|
|
13
|
+
workspaceId: $workspaceId
|
|
14
|
+
filter: $filter
|
|
15
|
+
pagination: { first: 100, after: $after }
|
|
16
|
+
) {
|
|
17
|
+
totalCount
|
|
18
|
+
pageInfo {
|
|
19
|
+
hasNextPage
|
|
20
|
+
endCursor
|
|
21
|
+
}
|
|
22
|
+
items {
|
|
23
|
+
key
|
|
24
|
+
title
|
|
25
|
+
type
|
|
26
|
+
status
|
|
27
|
+
priority
|
|
28
|
+
storyPoints
|
|
29
|
+
assigneeId
|
|
30
|
+
projectId
|
|
31
|
+
sprintId
|
|
32
|
+
dueDate
|
|
33
|
+
createdAt
|
|
34
|
+
updatedAt
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
`, d = a`
|
|
39
|
+
query ExportWorkItemsProjects($workspaceId: ID!) {
|
|
40
|
+
planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 100 }) {
|
|
41
|
+
items {
|
|
42
|
+
id
|
|
43
|
+
name
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
`, f = 200;
|
|
48
|
+
function p({ projectId: a }) {
|
|
49
|
+
let { workspaceId: p } = e(), m = o(), [h, g] = r({
|
|
50
|
+
busy: !1,
|
|
51
|
+
message: null,
|
|
52
|
+
error: !1
|
|
53
|
+
});
|
|
54
|
+
return /* @__PURE__ */ c("div", {
|
|
55
|
+
className: "flex items-center gap-2",
|
|
56
|
+
children: [/* @__PURE__ */ c(l, {
|
|
57
|
+
size: "sm",
|
|
58
|
+
variant: "outline",
|
|
59
|
+
onClick: async () => {
|
|
60
|
+
if (p) {
|
|
61
|
+
g({
|
|
62
|
+
busy: !0,
|
|
63
|
+
message: "Preparing export…",
|
|
64
|
+
error: !1
|
|
65
|
+
});
|
|
66
|
+
try {
|
|
67
|
+
let e = a ? { projectId: a } : void 0, r = [], i = null, o = 0;
|
|
68
|
+
for (let t = 0; t < f; t++) {
|
|
69
|
+
let t = await m.query({
|
|
70
|
+
query: u,
|
|
71
|
+
variables: {
|
|
72
|
+
workspaceId: p,
|
|
73
|
+
filter: e,
|
|
74
|
+
after: i
|
|
75
|
+
},
|
|
76
|
+
fetchPolicy: "network-only"
|
|
77
|
+
});
|
|
78
|
+
if (t.error) throw t.error;
|
|
79
|
+
let n = t.data?.planmagnetWorkItems;
|
|
80
|
+
if (!n) throw Error("The work items could not be read.");
|
|
81
|
+
if (o = n.totalCount, r.push(...n.items), !n.pageInfo.hasNextPage || !n.pageInfo.endCursor) break;
|
|
82
|
+
i = n.pageInfo.endCursor;
|
|
83
|
+
}
|
|
84
|
+
if (r.length < o) throw Error(`Only ${r.length} of ${o} work items could be read; nothing was exported.`);
|
|
85
|
+
let s = await m.query({
|
|
86
|
+
query: d,
|
|
87
|
+
variables: { workspaceId: p },
|
|
88
|
+
fetchPolicy: "network-only"
|
|
89
|
+
}), c = new Map((s.data?.planmagnetProjects.items ?? []).map((e) => [e.id, e.name]));
|
|
90
|
+
t(`planmagnet-work-items-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`, n(r, c)), g({
|
|
91
|
+
busy: !1,
|
|
92
|
+
message: `Exported ${r.length} work items.`,
|
|
93
|
+
error: !1
|
|
94
|
+
});
|
|
95
|
+
} catch (e) {
|
|
96
|
+
g({
|
|
97
|
+
busy: !1,
|
|
98
|
+
message: e instanceof Error ? e.message : "The export failed.",
|
|
99
|
+
error: !0
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
disabled: h.busy,
|
|
105
|
+
"data-testid": "export-work-items",
|
|
106
|
+
children: [/* @__PURE__ */ s(i, { className: "mr-1 h-4 w-4" }), h.busy ? "Exporting…" : "Export CSV"]
|
|
107
|
+
}), h.message && /* @__PURE__ */ s("span", {
|
|
108
|
+
role: h.error ? "alert" : "status",
|
|
109
|
+
className: `text-xs ${h.error ? "text-destructive" : "text-muted-foreground"}`,
|
|
110
|
+
children: h.message
|
|
111
|
+
})]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
export { p as ExportWorkItemsButton };
|
|
116
|
+
|
|
117
|
+
//# sourceMappingURL=ExportWorkItemsButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExportWorkItemsButton.js","names":[],"sources":["../../../src/planmagnet/export/ExportWorkItemsButton.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useApolloClient } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Download } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { downloadCsv, type ExportableWorkItem, workItemsCsv } from './workItemsCsv';\n\nconst PAGE = gql`\n query ExportWorkItemsPage($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter, $after: String) {\n planmagnetWorkItems(\n workspaceId: $workspaceId\n filter: $filter\n pagination: { first: 100, after: $after }\n ) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n key\n title\n type\n status\n priority\n storyPoints\n assigneeId\n projectId\n sprintId\n dueDate\n createdAt\n updatedAt\n }\n }\n }\n`;\ninterface WorkItemsPage {\n planmagnetWorkItems: {\n totalCount: number;\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n items: ExportableWorkItem[];\n };\n}\n\nconst PROJECTS = gql`\n query ExportWorkItemsProjects($workspaceId: ID!) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n }\n }\n`;\n\n/** Every page, not just the first 100 — an export that silently stops at a page is worse than none. */\nconst MAX_PAGES = 200;\n\nexport function ExportWorkItemsButton({ projectId }: { projectId?: string }) {\n const { workspaceId } = usePlanMagnet();\n const client = useApolloClient();\n const [state, setState] = useState<{ busy: boolean; message: string | null; error: boolean }>({\n busy: false,\n message: null,\n error: false,\n });\n\n const run = async () => {\n if (!workspaceId) return;\n setState({ busy: true, message: 'Preparing export…', error: false });\n try {\n const filter = projectId ? { projectId } : undefined;\n const items: ExportableWorkItem[] = [];\n let after: string | null = null;\n let total = 0;\n for (let page = 0; page < MAX_PAGES; page++) {\n const result: Awaited<ReturnType<typeof client.query<WorkItemsPage>>> =\n await client.query<WorkItemsPage>({\n query: PAGE,\n variables: { workspaceId, filter, after },\n fetchPolicy: 'network-only',\n });\n if (result.error) throw result.error;\n const list: WorkItemsPage['planmagnetWorkItems'] | undefined =\n result.data?.planmagnetWorkItems;\n if (!list) throw new Error('The work items could not be read.');\n total = list.totalCount;\n items.push(...list.items);\n if (!list.pageInfo.hasNextPage || !list.pageInfo.endCursor) break;\n after = list.pageInfo.endCursor;\n }\n if (items.length < total) {\n throw new Error(\n `Only ${items.length} of ${total} work items could be read; nothing was exported.`\n );\n }\n const projects = await client.query<{\n planmagnetProjects: { items: Array<{ id: string; name: string }> };\n }>({\n query: PROJECTS,\n variables: { workspaceId },\n fetchPolicy: 'network-only',\n });\n const names = new Map(\n (projects.data?.planmagnetProjects.items ?? []).map((p) => [p.id, p.name])\n );\n const stamp = new Date().toISOString().slice(0, 10);\n downloadCsv(`planmagnet-work-items-${stamp}.csv`, workItemsCsv(items, names));\n setState({ busy: false, message: `Exported ${items.length} work items.`, error: false });\n } catch (e) {\n setState({\n busy: false,\n message: e instanceof Error ? e.message : 'The export failed.',\n error: true,\n });\n }\n };\n\n return (\n <div className=\"flex items-center gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={run}\n disabled={state.busy}\n data-testid=\"export-work-items\"\n >\n <Download className=\"mr-1 h-4 w-4\" />\n {state.busy ? 'Exporting…' : 'Export CSV'}\n </Button>\n {state.message && (\n <span\n role={state.error ? 'alert' : 'status'}\n className={`text-xs ${state.error ? 'text-destructive' : 'text-muted-foreground'}`}\n >\n {state.message}\n </span>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;AAQA,IAAM,IAAO,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCV,IAAW,CAAG;;;;;;;;;GAYd,IAAY;AAElB,SAAgB,EAAsB,EAAE,gBAAqC;CAC3E,IAAM,EAAE,mBAAgB,GAAe,EACjC,IAAS,GAAiB,EAC1B,CAAC,GAAO,KAAY,EAAoE;EAC5F,MAAM;EACN,SAAS;EACT,OAAO;EACR,CAAC;AAqDF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD;GACE,MAAK;GACL,SAAQ;GACR,SAxDM,YAAY;AACjB,WACL;OAAS;MAAE,MAAM;MAAM,SAAS;MAAqB,OAAO;MAAO,CAAC;AACpE,SAAI;MACF,IAAM,IAAS,IAAY,EAAE,cAAW,GAAG,KAAA,GACrC,IAA8B,EAAE,EAClC,IAAuB,MACvB,IAAQ;AACZ,WAAK,IAAI,IAAO,GAAG,IAAO,GAAW,KAAQ;OAC3C,IAAM,IACJ,MAAM,EAAO,MAAqB;QAChC,OAAO;QACP,WAAW;SAAE;SAAa;SAAQ;SAAO;QACzC,aAAa;QACd,CAAC;AACJ,WAAI,EAAO,MAAO,OAAM,EAAO;OAC/B,IAAM,IACJ,EAAO,MAAM;AACf,WAAI,CAAC,EAAM,OAAU,MAAM,oCAAoC;AAG/D,WAFA,IAAQ,EAAK,YACb,EAAM,KAAK,GAAG,EAAK,MAAM,EACrB,CAAC,EAAK,SAAS,eAAe,CAAC,EAAK,SAAS,UAAW;AAC5D,WAAQ,EAAK,SAAS;;AAExB,UAAI,EAAM,SAAS,EACjB,OAAU,MACR,QAAQ,EAAM,OAAO,MAAM,EAAM,kDAClC;MAEH,IAAM,IAAW,MAAM,EAAO,MAE3B;OACD,OAAO;OACP,WAAW,EAAE,gBAAa;OAC1B,aAAa;OACd,CAAC,EACI,IAAQ,IAAI,KACf,EAAS,MAAM,mBAAmB,SAAS,EAAE,EAAE,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,CAC3E;AAGD,MADA,EAAY,0CADE,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,GAAG,GAAG,CACR,OAAO,EAAa,GAAO,EAAM,CAAC,EAC7E,EAAS;OAAE,MAAM;OAAO,SAAS,YAAY,EAAM,OAAO;OAAe,OAAO;OAAO,CAAC;cACjF,GAAG;AACV,QAAS;OACP,MAAM;OACN,SAAS,aAAa,QAAQ,EAAE,UAAU;OAC1C,OAAO;OACR,CAAC;;;;GAUA,UAAU,EAAM;GAChB,eAAY;aALd,CAOE,kBAAC,GAAD,EAAU,WAAU,gBAAiB,CAAA,EACpC,EAAM,OAAO,eAAe,aACtB;MACR,EAAM,WACL,kBAAC,QAAD;GACE,MAAM,EAAM,QAAQ,UAAU;GAC9B,WAAW,WAAW,EAAM,QAAQ,qBAAqB;aAExD,EAAM;GACF,CAAA,CAEL"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work items as CSV (P1.4 "data cannot leave the product").
|
|
3
|
+
*
|
|
4
|
+
* Built in the browser from the same API the pages use, so it exports exactly
|
|
5
|
+
* what the caller may read. The platform's Takeout-style export
|
|
6
|
+
* (wspace-export-svc) would need a PlanMagnet entity config in that shared
|
|
7
|
+
* service — held — so this is the in-product path.
|
|
8
|
+
*/
|
|
9
|
+
export interface ExportableWorkItem {
|
|
10
|
+
key: string;
|
|
11
|
+
title: string;
|
|
12
|
+
type: string;
|
|
13
|
+
status: string;
|
|
14
|
+
priority: string;
|
|
15
|
+
storyPoints: number | null;
|
|
16
|
+
assigneeId: string | null;
|
|
17
|
+
projectId: string;
|
|
18
|
+
sprintId: string | null;
|
|
19
|
+
dueDate: string | null;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* One CSV field. Quotes when needed (comma, quote, CR/LF) and neutralises
|
|
25
|
+
* spreadsheet formulas: a cell starting with = + - @ TAB or CR is executed by
|
|
26
|
+
* Excel/Sheets ("CSV injection"), so it is prefixed with a single quote.
|
|
27
|
+
*/
|
|
28
|
+
export declare function csvField(value: unknown): string;
|
|
29
|
+
export declare function workItemsCsv(items: ExportableWorkItem[], projectNames: Map<string, string>): string;
|
|
30
|
+
export declare function downloadCsv(filename: string, csv: string): void;
|
|
31
|
+
//# sourceMappingURL=workItemsCsv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workItemsCsv.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/export/workItemsCsv.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAmBD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAK/C;AAKD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,kBAAkB,EAAE,EAC3B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAChC,MAAM,CASR;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAS/D"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/planmagnet/export/workItemsCsv.ts
|
|
2
|
+
var e = [
|
|
3
|
+
["Key", (e) => e.key],
|
|
4
|
+
["Title", (e) => e.title],
|
|
5
|
+
["Type", (e) => e.type],
|
|
6
|
+
["Status", (e) => e.status],
|
|
7
|
+
["Priority", (e) => e.priority],
|
|
8
|
+
["Story points", (e) => e.storyPoints],
|
|
9
|
+
["Project", (e, t) => t],
|
|
10
|
+
["Assignee ID", (e) => e.assigneeId],
|
|
11
|
+
["Sprint ID", (e) => e.sprintId],
|
|
12
|
+
["Due date", (e) => e.dueDate],
|
|
13
|
+
["Created", (e) => e.createdAt],
|
|
14
|
+
["Updated", (e) => e.updatedAt]
|
|
15
|
+
];
|
|
16
|
+
function t(e) {
|
|
17
|
+
if (e == null) return "";
|
|
18
|
+
let t = String(e);
|
|
19
|
+
return /^[=+\-@\t\r]/.test(t) && (t = `'${t}`), /[",\r\n]/.test(t) ? `"${t.replace(/"/g, "\"\"")}"` : t;
|
|
20
|
+
}
|
|
21
|
+
var n = "";
|
|
22
|
+
function r(r, i) {
|
|
23
|
+
return `${n}${[e.map(([e]) => t(e)).join(","), ...r.map((n) => e.map(([, e]) => t(e(n, i.get(n.projectId) ?? n.projectId))).join(","))].join("\r\n")}\r\n`;
|
|
24
|
+
}
|
|
25
|
+
function i(e, t) {
|
|
26
|
+
let n = URL.createObjectURL(new Blob([t], { type: "text/csv;charset=utf-8" })), r = document.createElement("a");
|
|
27
|
+
r.href = n, r.download = e, document.body.appendChild(r), r.click(), r.remove(), URL.revokeObjectURL(n);
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { i as downloadCsv, r as workItemsCsv };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=workItemsCsv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workItemsCsv.js","names":[],"sources":["../../../src/planmagnet/export/workItemsCsv.ts"],"sourcesContent":["/**\n * Work items as CSV (P1.4 \"data cannot leave the product\").\n *\n * Built in the browser from the same API the pages use, so it exports exactly\n * what the caller may read. The platform's Takeout-style export\n * (wspace-export-svc) would need a PlanMagnet entity config in that shared\n * service — held — so this is the in-product path.\n */\n\nexport interface ExportableWorkItem {\n key: string;\n title: string;\n type: string;\n status: string;\n priority: string;\n storyPoints: number | null;\n assigneeId: string | null;\n projectId: string;\n sprintId: string | null;\n dueDate: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nconst COLUMNS: Array<\n [header: string, value: (item: ExportableWorkItem, project: string) => unknown]\n> = [\n ['Key', (i) => i.key],\n ['Title', (i) => i.title],\n ['Type', (i) => i.type],\n ['Status', (i) => i.status],\n ['Priority', (i) => i.priority],\n ['Story points', (i) => i.storyPoints],\n ['Project', (_, project) => project],\n ['Assignee ID', (i) => i.assigneeId],\n ['Sprint ID', (i) => i.sprintId],\n ['Due date', (i) => i.dueDate],\n ['Created', (i) => i.createdAt],\n ['Updated', (i) => i.updatedAt],\n];\n\n/**\n * One CSV field. Quotes when needed (comma, quote, CR/LF) and neutralises\n * spreadsheet formulas: a cell starting with = + - @ TAB or CR is executed by\n * Excel/Sheets (\"CSV injection\"), so it is prefixed with a single quote.\n */\nexport function csvField(value: unknown): string {\n if (value === null || value === undefined) return '';\n let text = String(value);\n if (/^[=+\\-@\\t\\r]/.test(text)) text = `'${text}`;\n return /[\",\\r\\n]/.test(text) ? `\"${text.replace(/\"/g, '\"\"')}\"` : text;\n}\n\n/** Makes Excel read the file as UTF-8. */\nconst UTF8_BOM = String.fromCharCode(0xfeff);\n\nexport function workItemsCsv(\n items: ExportableWorkItem[],\n projectNames: Map<string, string>\n): string {\n const header = COLUMNS.map(([name]) => csvField(name)).join(',');\n const rows = items.map((item) =>\n COLUMNS.map(([, value]) =>\n csvField(value(item, projectNames.get(item.projectId) ?? item.projectId))\n ).join(',')\n );\n // CRLF per RFC 4180; the BOM makes Excel read UTF-8 correctly.\n return `${UTF8_BOM}${[header, ...rows].join('\\r\\n')}\\r\\n`;\n}\n\nexport function downloadCsv(filename: string, csv: string): void {\n const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));\n const link = document.createElement('a');\n link.href = url;\n link.download = filename;\n document.body.appendChild(link);\n link.click();\n link.remove();\n URL.revokeObjectURL(url);\n}\n"],"mappings":";AAwBA,IAAM,IAEF;CACF,CAAC,QAAQ,MAAM,EAAE,IAAI;CACrB,CAAC,UAAU,MAAM,EAAE,MAAM;CACzB,CAAC,SAAS,MAAM,EAAE,KAAK;CACvB,CAAC,WAAW,MAAM,EAAE,OAAO;CAC3B,CAAC,aAAa,MAAM,EAAE,SAAS;CAC/B,CAAC,iBAAiB,MAAM,EAAE,YAAY;CACtC,CAAC,YAAY,GAAG,MAAY,EAAQ;CACpC,CAAC,gBAAgB,MAAM,EAAE,WAAW;CACpC,CAAC,cAAc,MAAM,EAAE,SAAS;CAChC,CAAC,aAAa,MAAM,EAAE,QAAQ;CAC9B,CAAC,YAAY,MAAM,EAAE,UAAU;CAC/B,CAAC,YAAY,MAAM,EAAE,UAAU;CAChC;AAOD,SAAgB,EAAS,GAAwB;AAC/C,KAAI,KAAU,KAA6B,QAAO;CAClD,IAAI,IAAO,OAAO,EAAM;AAExB,QADI,eAAe,KAAK,EAAK,KAAE,IAAO,IAAI,MACnC,WAAW,KAAK,EAAK,GAAG,IAAI,EAAK,QAAQ,MAAM,OAAK,CAAC,KAAK;;AAInE,IAAM,IAAW;AAEjB,SAAgB,EACd,GACA,GACQ;AAQR,QAAO,GAAG,IAAW,CAPN,EAAQ,KAAK,CAAC,OAAU,EAAS,EAAK,CAAC,CAAC,KAAK,IAAI,EAOlC,GANjB,EAAM,KAAK,MACtB,EAAQ,KAAK,GAAG,OACd,EAAS,EAAM,GAAM,EAAa,IAAI,EAAK,UAAU,IAAI,EAAK,UAAU,CAAC,CAC1E,CAAC,KAAK,IAAI,CACZ,CAEqC,CAAC,KAAK,OAAO,CAAC;;AAGtD,SAAgB,EAAY,GAAkB,GAAmB;CAC/D,IAAM,IAAM,IAAI,gBAAgB,IAAI,KAAK,CAAC,EAAI,EAAE,EAAE,MAAM,0BAA0B,CAAC,CAAC,EAC9E,IAAO,SAAS,cAAc,IAAI;AAMxC,CALA,EAAK,OAAO,GACZ,EAAK,WAAW,GAChB,SAAS,KAAK,YAAY,EAAK,EAC/B,EAAK,OAAO,EACZ,EAAK,QAAQ,EACb,IAAI,gBAAgB,EAAI"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TasksTasksPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TasksTasksPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TasksTasksPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TasksTasksPagePage.tsx"],"names":[],"mappings":"AAwIA,wBAAgB,kBAAkB,4CAyOjC"}
|
|
@@ -2,14 +2,15 @@ import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/Pla
|
|
|
2
2
|
import { PersonName as n } from "../people/People.js";
|
|
3
3
|
import { rowLinkProps as r } from "../utils/rowNavigation.js";
|
|
4
4
|
import { ProjectBoard as i } from "../board/ProjectBoard.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
5
|
+
import { ExportWorkItemsButton as a } from "../export/ExportWorkItemsButton.js";
|
|
6
|
+
import { useState as o } from "react";
|
|
7
|
+
import { Filter as s } from "lucide-react";
|
|
8
|
+
import { gql as c } from "@apollo/client";
|
|
9
|
+
import { useQuery as l } from "@apollo/client/react";
|
|
10
|
+
import { Fragment as u, jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
11
|
+
import { Card as p, CardContent as m } from "@burdenoff/fe-libs/ui";
|
|
11
12
|
//#region src/planmagnet/pages/TasksTasksPagePage.tsx
|
|
12
|
-
var
|
|
13
|
+
var h = c`
|
|
13
14
|
query GetWorkItems(
|
|
14
15
|
$workspaceId: ID!
|
|
15
16
|
$filter: PlanmagnetWorkItemFilter
|
|
@@ -40,7 +41,7 @@ var m = s`
|
|
|
40
41
|
totalCount
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
|
-
`,
|
|
44
|
+
`, g = c`
|
|
44
45
|
query GetWorkItemCounts($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {
|
|
45
46
|
planmagnetWorkItemCounts(workspaceId: $workspaceId, filter: $filter) {
|
|
46
47
|
total
|
|
@@ -50,7 +51,7 @@ var m = s`
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
`,
|
|
54
|
+
`, _ = 100, v = c`
|
|
54
55
|
query GetBoardProjects($workspaceId: ID!) {
|
|
55
56
|
planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 100 }) {
|
|
56
57
|
items {
|
|
@@ -59,7 +60,7 @@ var m = s`
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
|
-
`,
|
|
63
|
+
`, y = [
|
|
63
64
|
"BACKLOG",
|
|
64
65
|
"TODO",
|
|
65
66
|
"IN_PROGRESS",
|
|
@@ -67,7 +68,7 @@ var m = s`
|
|
|
67
68
|
"TESTING",
|
|
68
69
|
"DONE",
|
|
69
70
|
"CANCELLED"
|
|
70
|
-
],
|
|
71
|
+
], b = {
|
|
71
72
|
BACKLOG: "Backlog",
|
|
72
73
|
TODO: "To Do",
|
|
73
74
|
IN_PROGRESS: "In Progress",
|
|
@@ -75,7 +76,7 @@ var m = s`
|
|
|
75
76
|
TESTING: "Testing",
|
|
76
77
|
DONE: "Done",
|
|
77
78
|
CANCELLED: "Cancelled"
|
|
78
|
-
},
|
|
79
|
+
}, x = {
|
|
79
80
|
BACKLOG: "border-t-gray-400",
|
|
80
81
|
TODO: "border-t-blue-400",
|
|
81
82
|
IN_PROGRESS: "border-t-yellow-400",
|
|
@@ -83,46 +84,46 @@ var m = s`
|
|
|
83
84
|
TESTING: "border-t-teal-400",
|
|
84
85
|
DONE: "border-t-green-400",
|
|
85
86
|
CANCELLED: "border-t-muted-foreground"
|
|
86
|
-
},
|
|
87
|
+
}, S = {
|
|
87
88
|
LOW: "bg-gray-100 text-gray-700",
|
|
88
89
|
MEDIUM: "bg-blue-100 text-blue-700",
|
|
89
90
|
HIGH: "bg-orange-100 text-orange-700",
|
|
90
91
|
CRITICAL: "bg-red-100 text-red-700"
|
|
91
|
-
},
|
|
92
|
+
}, C = {
|
|
92
93
|
STORY: "bg-green-100 text-green-700",
|
|
93
94
|
BUG: "bg-red-100 text-red-700",
|
|
94
95
|
TASK: "bg-blue-100 text-blue-700",
|
|
95
96
|
EPIC: "bg-purple-100 text-purple-700",
|
|
96
97
|
SUBTASK: "bg-gray-100 text-gray-700"
|
|
97
98
|
};
|
|
98
|
-
function
|
|
99
|
-
let { workspaceId:
|
|
100
|
-
variables: { workspaceId:
|
|
101
|
-
skip: !
|
|
102
|
-
}),
|
|
103
|
-
...
|
|
104
|
-
...
|
|
105
|
-
}, [
|
|
99
|
+
function w() {
|
|
100
|
+
let { workspaceId: c } = e(), w = t(), [T, E] = o("all"), [D, O] = o("all"), [k, A] = o(null), { data: j } = l(v, {
|
|
101
|
+
variables: { workspaceId: c },
|
|
102
|
+
skip: !c
|
|
103
|
+
}), M = j?.planmagnetProjects?.items ?? [], N = k ?? M[0]?.id ?? "", P = M.find((e) => e.id === N)?.name ?? "", F = {
|
|
104
|
+
...T !== "all" && { priority: T.toUpperCase() },
|
|
105
|
+
...D !== "all" && { type: D.toUpperCase() }
|
|
106
|
+
}, [I, L] = o(!1), { data: R, loading: z, error: B, fetchMore: V } = l(h, {
|
|
106
107
|
variables: {
|
|
107
|
-
workspaceId:
|
|
108
|
-
filter:
|
|
109
|
-
pagination: { first:
|
|
108
|
+
workspaceId: c,
|
|
109
|
+
filter: F,
|
|
110
|
+
pagination: { first: _ }
|
|
110
111
|
},
|
|
111
|
-
skip: !
|
|
112
|
-
}), { data:
|
|
112
|
+
skip: !c || !!N
|
|
113
|
+
}), { data: H } = l(g, {
|
|
113
114
|
variables: {
|
|
114
|
-
workspaceId:
|
|
115
|
-
filter:
|
|
115
|
+
workspaceId: c,
|
|
116
|
+
filter: F
|
|
116
117
|
},
|
|
117
|
-
skip: !
|
|
118
|
-
}),
|
|
119
|
-
let e =
|
|
118
|
+
skip: !c
|
|
119
|
+
}), U = async () => {
|
|
120
|
+
let e = R?.planmagnetWorkItems?.pageInfo?.endCursor;
|
|
120
121
|
if (e) {
|
|
121
|
-
|
|
122
|
+
L(!0);
|
|
122
123
|
try {
|
|
123
|
-
await
|
|
124
|
+
await V({
|
|
124
125
|
variables: { pagination: {
|
|
125
|
-
first:
|
|
126
|
+
first: _,
|
|
126
127
|
after: e
|
|
127
128
|
} },
|
|
128
129
|
updateQuery: (e, { fetchMoreResult: t }) => ({ planmagnetWorkItems: {
|
|
@@ -131,65 +132,69 @@ function C() {
|
|
|
131
132
|
} })
|
|
132
133
|
});
|
|
133
134
|
} finally {
|
|
134
|
-
|
|
135
|
+
L(!1);
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
};
|
|
138
|
-
if (
|
|
139
|
+
if (z && !R) return /* @__PURE__ */ d("div", {
|
|
139
140
|
className: "flex items-center justify-center p-12",
|
|
140
|
-
children: /* @__PURE__ */
|
|
141
|
+
children: /* @__PURE__ */ d("div", { className: "animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" })
|
|
141
142
|
});
|
|
142
|
-
if (
|
|
143
|
+
if (B) return /* @__PURE__ */ f("div", {
|
|
143
144
|
className: "p-6 text-red-500",
|
|
144
|
-
children: ["Error: ",
|
|
145
|
+
children: ["Error: ", B.message]
|
|
145
146
|
});
|
|
146
|
-
let
|
|
147
|
-
return /* @__PURE__ */
|
|
147
|
+
let W = R?.planmagnetWorkItems?.items ?? [], G = H?.planmagnetWorkItemCounts, K = G?.total ?? R?.planmagnetWorkItems?.totalCount ?? W.length, q = (e) => G?.byStatus.find((t) => t.status === e)?.count ?? W.filter((t) => t.status === e).length, J = R?.planmagnetWorkItems?.pageInfo?.hasNextPage ?? !1;
|
|
148
|
+
return /* @__PURE__ */ f("div", {
|
|
148
149
|
className: "space-y-4 p-6",
|
|
149
|
-
children: [/* @__PURE__ */
|
|
150
|
+
children: [/* @__PURE__ */ d("div", {
|
|
150
151
|
className: "flex items-center justify-between",
|
|
151
|
-
children: /* @__PURE__ */
|
|
152
|
-
/* @__PURE__ */
|
|
152
|
+
children: /* @__PURE__ */ f("div", { children: [
|
|
153
|
+
/* @__PURE__ */ d("h1", {
|
|
153
154
|
className: "text-2xl font-semibold",
|
|
154
155
|
children: "Task Board"
|
|
155
156
|
}),
|
|
156
|
-
/* @__PURE__ */
|
|
157
|
+
/* @__PURE__ */ d("label", {
|
|
157
158
|
className: "sr-only",
|
|
158
159
|
htmlFor: "task-board-project",
|
|
159
160
|
children: "Project"
|
|
160
161
|
}),
|
|
161
|
-
/* @__PURE__ */
|
|
162
|
+
/* @__PURE__ */ f("select", {
|
|
162
163
|
id: "task-board-project",
|
|
163
164
|
className: "mt-2 rounded-md border border-input bg-background px-2 py-1 text-sm",
|
|
164
|
-
value:
|
|
165
|
-
onChange: (e) =>
|
|
166
|
-
children: [
|
|
165
|
+
value: N,
|
|
166
|
+
onChange: (e) => A(e.target.value),
|
|
167
|
+
children: [M.map((e) => /* @__PURE__ */ d("option", {
|
|
167
168
|
value: e.id,
|
|
168
169
|
children: e.name
|
|
169
|
-
}, e.id)), /* @__PURE__ */
|
|
170
|
+
}, e.id)), /* @__PURE__ */ d("option", {
|
|
170
171
|
value: "",
|
|
171
172
|
children: "All projects — status overview"
|
|
172
173
|
})]
|
|
173
174
|
}),
|
|
174
|
-
/* @__PURE__ */ d("
|
|
175
|
+
/* @__PURE__ */ d("div", {
|
|
176
|
+
className: "mt-2",
|
|
177
|
+
children: /* @__PURE__ */ d(a, { projectId: N || void 0 })
|
|
178
|
+
}),
|
|
179
|
+
/* @__PURE__ */ f("p", {
|
|
175
180
|
className: "text-sm text-muted-foreground",
|
|
176
181
|
"data-testid": "task-board-total",
|
|
177
182
|
children: [
|
|
178
|
-
|
|
183
|
+
K,
|
|
179
184
|
" tasks across all projects",
|
|
180
|
-
|
|
185
|
+
W.length < K && ` · showing ${W.length}`
|
|
181
186
|
]
|
|
182
187
|
})
|
|
183
188
|
] })
|
|
184
|
-
}),
|
|
185
|
-
projectId:
|
|
186
|
-
projectName:
|
|
187
|
-
}) : /* @__PURE__ */
|
|
188
|
-
/* @__PURE__ */
|
|
189
|
+
}), N ? /* @__PURE__ */ d(i, {
|
|
190
|
+
projectId: N,
|
|
191
|
+
projectName: P
|
|
192
|
+
}) : /* @__PURE__ */ f(u, { children: [
|
|
193
|
+
/* @__PURE__ */ f("div", {
|
|
189
194
|
className: "flex gap-4 items-center flex-wrap",
|
|
190
195
|
children: [
|
|
191
|
-
/* @__PURE__ */
|
|
192
|
-
/* @__PURE__ */
|
|
196
|
+
/* @__PURE__ */ d(s, { className: "h-4 w-4 text-muted-foreground" }),
|
|
197
|
+
/* @__PURE__ */ d("div", {
|
|
193
198
|
className: "flex gap-1",
|
|
194
199
|
children: [
|
|
195
200
|
"all",
|
|
@@ -197,13 +202,13 @@ function C() {
|
|
|
197
202
|
"medium",
|
|
198
203
|
"high",
|
|
199
204
|
"critical"
|
|
200
|
-
].map((e) => /* @__PURE__ */
|
|
201
|
-
onClick: () =>
|
|
202
|
-
className: `px-2 py-1 text-xs rounded capitalize ${
|
|
205
|
+
].map((e) => /* @__PURE__ */ d("button", {
|
|
206
|
+
onClick: () => E(e),
|
|
207
|
+
className: `px-2 py-1 text-xs rounded capitalize ${T === e ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`,
|
|
203
208
|
children: e
|
|
204
209
|
}, e))
|
|
205
210
|
}),
|
|
206
|
-
/* @__PURE__ */
|
|
211
|
+
/* @__PURE__ */ d("div", {
|
|
207
212
|
className: "flex gap-1",
|
|
208
213
|
children: [
|
|
209
214
|
"all",
|
|
@@ -211,77 +216,77 @@ function C() {
|
|
|
211
216
|
"bug",
|
|
212
217
|
"task",
|
|
213
218
|
"epic"
|
|
214
|
-
].map((e) => /* @__PURE__ */
|
|
215
|
-
onClick: () =>
|
|
216
|
-
className: `px-2 py-1 text-xs rounded capitalize ${
|
|
219
|
+
].map((e) => /* @__PURE__ */ d("button", {
|
|
220
|
+
onClick: () => O(e),
|
|
221
|
+
className: `px-2 py-1 text-xs rounded capitalize ${D === e ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`,
|
|
217
222
|
children: e
|
|
218
223
|
}, e))
|
|
219
224
|
})
|
|
220
225
|
]
|
|
221
226
|
}),
|
|
222
|
-
/* @__PURE__ */
|
|
227
|
+
/* @__PURE__ */ d("div", {
|
|
223
228
|
className: "grid grid-cols-7 gap-3 overflow-x-auto",
|
|
224
|
-
children:
|
|
225
|
-
let t =
|
|
226
|
-
return /* @__PURE__ */
|
|
229
|
+
children: y.map((e) => {
|
|
230
|
+
let t = W.filter((t) => t.status === e);
|
|
231
|
+
return /* @__PURE__ */ f("div", {
|
|
227
232
|
className: "min-w-[200px]",
|
|
228
|
-
children: [/* @__PURE__ */
|
|
229
|
-
className: `border-t-2 ${
|
|
230
|
-
children: /* @__PURE__ */
|
|
233
|
+
children: [/* @__PURE__ */ d("div", {
|
|
234
|
+
className: `border-t-2 ${x[e]} rounded-t-md p-2 bg-muted/50`,
|
|
235
|
+
children: /* @__PURE__ */ f("div", {
|
|
231
236
|
className: "flex justify-between items-center",
|
|
232
|
-
children: [/* @__PURE__ */
|
|
237
|
+
children: [/* @__PURE__ */ d("span", {
|
|
233
238
|
className: "text-sm font-medium",
|
|
234
|
-
children:
|
|
235
|
-
}), /* @__PURE__ */
|
|
239
|
+
children: b[e]
|
|
240
|
+
}), /* @__PURE__ */ d("span", {
|
|
236
241
|
className: "text-xs bg-muted rounded-full px-1.5",
|
|
237
242
|
"data-testid": `task-col-count-${e}`,
|
|
238
|
-
children:
|
|
243
|
+
children: q(e)
|
|
239
244
|
})]
|
|
240
245
|
})
|
|
241
|
-
}), /* @__PURE__ */
|
|
246
|
+
}), /* @__PURE__ */ d("div", {
|
|
242
247
|
className: "space-y-2 mt-2",
|
|
243
|
-
children: t.map((e) => /* @__PURE__ */
|
|
244
|
-
...r(() =>
|
|
245
|
-
children: /* @__PURE__ */
|
|
248
|
+
children: t.map((e) => /* @__PURE__ */ d(p, {
|
|
249
|
+
...r(() => w(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.id)}`), e.title),
|
|
250
|
+
children: /* @__PURE__ */ f(m, {
|
|
246
251
|
className: "p-3 space-y-2",
|
|
247
252
|
children: [
|
|
248
|
-
/* @__PURE__ */
|
|
253
|
+
/* @__PURE__ */ d("p", {
|
|
249
254
|
className: "text-sm font-medium leading-tight",
|
|
250
255
|
children: e.title
|
|
251
256
|
}),
|
|
252
|
-
/* @__PURE__ */
|
|
257
|
+
/* @__PURE__ */ d("p", {
|
|
253
258
|
className: "text-xs text-muted-foreground",
|
|
254
259
|
children: e.key
|
|
255
260
|
}),
|
|
256
|
-
/* @__PURE__ */
|
|
261
|
+
/* @__PURE__ */ f("div", {
|
|
257
262
|
className: "flex gap-1 flex-wrap",
|
|
258
|
-
children: [/* @__PURE__ */
|
|
259
|
-
className: `text-[10px] px-1.5 py-0.5 rounded-full capitalize ${
|
|
263
|
+
children: [/* @__PURE__ */ d("span", {
|
|
264
|
+
className: `text-[10px] px-1.5 py-0.5 rounded-full capitalize ${S[e.priority] ?? ""}`,
|
|
260
265
|
children: e.priority.toLowerCase()
|
|
261
|
-
}), /* @__PURE__ */
|
|
262
|
-
className: `text-[10px] px-1.5 py-0.5 rounded-full capitalize ${
|
|
266
|
+
}), /* @__PURE__ */ d("span", {
|
|
267
|
+
className: `text-[10px] px-1.5 py-0.5 rounded-full capitalize ${C[e.type] ?? ""}`,
|
|
263
268
|
children: e.type.toLowerCase()
|
|
264
269
|
})]
|
|
265
270
|
}),
|
|
266
|
-
/* @__PURE__ */
|
|
271
|
+
/* @__PURE__ */ f("div", {
|
|
267
272
|
className: "flex items-center justify-between",
|
|
268
|
-
children: [/* @__PURE__ */
|
|
273
|
+
children: [/* @__PURE__ */ d("div", {
|
|
269
274
|
className: "flex gap-1",
|
|
270
|
-
children: (e.labels ?? []).slice(0, 2).map((e) => /* @__PURE__ */
|
|
275
|
+
children: (e.labels ?? []).slice(0, 2).map((e) => /* @__PURE__ */ d("span", {
|
|
271
276
|
className: "text-[9px] px-1 py-0.5 rounded bg-muted",
|
|
272
277
|
children: e
|
|
273
278
|
}, e))
|
|
274
|
-
}), /* @__PURE__ */
|
|
279
|
+
}), /* @__PURE__ */ d("div", {
|
|
275
280
|
className: "flex items-center gap-1",
|
|
276
|
-
children: /* @__PURE__ */
|
|
281
|
+
children: /* @__PURE__ */ d("span", {
|
|
277
282
|
className: "text-[10px] font-medium bg-primary/10 text-primary rounded-full h-5 w-5 flex items-center justify-center",
|
|
278
283
|
children: e.storyPoints ?? 0
|
|
279
284
|
})
|
|
280
285
|
})]
|
|
281
286
|
}),
|
|
282
|
-
/* @__PURE__ */
|
|
287
|
+
/* @__PURE__ */ f("div", {
|
|
283
288
|
className: "flex justify-between text-[10px] text-muted-foreground",
|
|
284
|
-
children: [/* @__PURE__ */
|
|
289
|
+
children: [/* @__PURE__ */ d("span", { children: e.assigneeId ? /* @__PURE__ */ d(n, { id: e.assigneeId }) : "Unassigned" }), /* @__PURE__ */ d("span", { children: e.dueDate ? new Date(e.dueDate).toLocaleDateString() : "" })]
|
|
285
290
|
})
|
|
286
291
|
]
|
|
287
292
|
})
|
|
@@ -290,21 +295,21 @@ function C() {
|
|
|
290
295
|
}, e);
|
|
291
296
|
})
|
|
292
297
|
}),
|
|
293
|
-
|
|
298
|
+
J && /* @__PURE__ */ d("div", {
|
|
294
299
|
className: "flex justify-center",
|
|
295
|
-
children: /* @__PURE__ */
|
|
300
|
+
children: /* @__PURE__ */ d("button", {
|
|
296
301
|
type: "button",
|
|
297
|
-
onClick:
|
|
298
|
-
disabled:
|
|
302
|
+
onClick: U,
|
|
303
|
+
disabled: I,
|
|
299
304
|
className: "px-3 py-1.5 text-sm rounded-md bg-muted text-foreground hover:bg-muted/80 disabled:opacity-50",
|
|
300
305
|
"data-testid": "task-board-load-more",
|
|
301
|
-
children:
|
|
306
|
+
children: I ? "Loading…" : `Load more (${W.length} of ${K})`
|
|
302
307
|
})
|
|
303
308
|
})
|
|
304
309
|
] })]
|
|
305
310
|
});
|
|
306
311
|
}
|
|
307
312
|
//#endregion
|
|
308
|
-
export {
|
|
313
|
+
export { w as TasksTasksPagePage };
|
|
309
314
|
|
|
310
315
|
//# sourceMappingURL=TasksTasksPagePage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TasksTasksPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TasksTasksPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent } from '@burdenoff/fe-libs/ui';\nimport { Filter } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { rowLinkProps } from '../utils/rowNavigation';\nimport { PersonName } from '../people/People';\nimport { ProjectBoard } from '../board/ProjectBoard';\n\nconst GET_WORK_ITEMS = gql`\n query GetWorkItems(\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 type\n status\n priority\n key\n assigneeId\n reporterId\n sprintId\n storyPoints\n estimateMinutes\n dueDate\n labels\n tags\n createdAt\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n }\n }\n`;\n\n// Server-side counts (QA H2): the list returns at most 100 rows per page, so\n// counting the loaded page undercounted every workspace past 100 items.\nconst GET_WORK_ITEM_COUNTS = gql`\n query GetWorkItemCounts($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {\n planmagnetWorkItemCounts(workspaceId: $workspaceId, filter: $filter) {\n total\n byStatus {\n status\n count\n }\n }\n }\n`;\n\nconst PAGE_SIZE = 100;\n\nconst GET_BOARD_PROJECTS = gql`\n query GetBoardProjects($workspaceId: ID!) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n }\n }\n`;\n\ninterface WorkItemNode {\n id: string;\n title: string;\n type: string;\n status: string;\n priority: string;\n key: string;\n assigneeId: string | null;\n reporterId: string | null;\n sprintId: string | null;\n storyPoints: number | null;\n estimateMinutes: number | null;\n dueDate: string | null;\n labels: string[];\n tags: string[];\n createdAt: string;\n}\n\nconst columns = [\n 'BACKLOG',\n 'TODO',\n 'IN_PROGRESS',\n 'IN_REVIEW',\n 'TESTING',\n 'DONE',\n 'CANCELLED',\n] as const;\n\nconst colLabel: Record<string, string> = {\n BACKLOG: 'Backlog',\n TODO: 'To Do',\n IN_PROGRESS: 'In Progress',\n IN_REVIEW: 'In Review',\n TESTING: 'Testing',\n DONE: 'Done',\n CANCELLED: 'Cancelled',\n};\n\nconst colColor: Record<string, string> = {\n BACKLOG: 'border-t-gray-400',\n TODO: 'border-t-blue-400',\n IN_PROGRESS: 'border-t-yellow-400',\n IN_REVIEW: 'border-t-purple-400',\n TESTING: 'border-t-teal-400',\n DONE: 'border-t-green-400',\n CANCELLED: 'border-t-muted-foreground',\n};\n\nconst priorityColor: Record<string, string> = {\n LOW: 'bg-gray-100 text-gray-700',\n MEDIUM: 'bg-blue-100 text-blue-700',\n HIGH: 'bg-orange-100 text-orange-700',\n CRITICAL: 'bg-red-100 text-red-700',\n};\n\nconst typeColor: Record<string, string> = {\n STORY: 'bg-green-100 text-green-700',\n BUG: 'bg-red-100 text-red-700',\n TASK: 'bg-blue-100 text-blue-700',\n EPIC: 'bg-purple-100 text-purple-700',\n SUBTASK: 'bg-gray-100 text-gray-700',\n};\n\nexport function TasksTasksPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const [priorityFilter, setPriorityFilter] = useState<string>('all');\n const [typeFilter, setTypeFilter] = useState<string>('all');\n // P1.2: the Task Board is a project's real board (server-side placement,\n // drag-and-drop, WIP limits). null = not chosen yet -> the first project;\n // '' = the cross-project status overview, which is NOT a board.\n const [boardProject, setBoardProject] = useState<string | null>(null);\n const { data: projectsData } = useQuery(GET_BOARD_PROJECTS, {\n variables: { workspaceId },\n skip: !workspaceId,\n });\n const boardProjects = projectsData?.planmagnetProjects?.items ?? [];\n const selectedProject = boardProject ?? boardProjects[0]?.id ?? '';\n const selectedProjectName = boardProjects.find((p) => p.id === selectedProject)?.name ?? '';\n\n // Filters go to the server so the loaded items AND the counts both reflect\n // every matching item, not just the first page.\n const filter = {\n ...(priorityFilter !== 'all' && { priority: priorityFilter.toUpperCase() }),\n ...(typeFilter !== 'all' && { type: typeFilter.toUpperCase() }),\n };\n const [loadingMore, setLoadingMore] = useState(false);\n\n const { data, loading, error, fetchMore } = useQuery<{\n planmagnetWorkItems: {\n items: WorkItemNode[];\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n totalCount: number;\n };\n }>(GET_WORK_ITEMS, {\n variables: { workspaceId, filter, pagination: { first: PAGE_SIZE } },\n // The list only feeds the cross-project overview; a board loads its own cards.\n skip: !workspaceId || !!selectedProject,\n });\n const { data: countsData } = useQuery<{\n planmagnetWorkItemCounts: { total: number; byStatus: { status: string; count: number }[] };\n }>(GET_WORK_ITEM_COUNTS, { variables: { workspaceId, filter }, skip: !workspaceId });\n\n const loadMore = async () => {\n const after = data?.planmagnetWorkItems?.pageInfo?.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { first: PAGE_SIZE, after } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetWorkItems: {\n ...fetchMoreResult.planmagnetWorkItems,\n items: [\n ...prev.planmagnetWorkItems.items,\n ...fetchMoreResult.planmagnetWorkItems.items,\n ],\n },\n }),\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n // Only before the first load: a refetch or a filter change must not blank the board.\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 }\n\n if (error) {\n return <div className=\"p-6 text-red-500\">Error: {error.message}</div>;\n }\n\n const tasks = data?.planmagnetWorkItems?.items ?? [];\n\n const counts = countsData?.planmagnetWorkItemCounts;\n const total = counts?.total ?? data?.planmagnetWorkItems?.totalCount ?? tasks.length;\n const countFor = (status: string) =>\n counts?.byStatus.find((c) => c.status === status)?.count ??\n tasks.filter((t) => t.status === status).length;\n const hasMore = data?.planmagnetWorkItems?.pageInfo?.hasNextPage ?? false;\n\n return (\n <div className=\"space-y-4 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Task Board</h1>\n <label className=\"sr-only\" htmlFor=\"task-board-project\">\n Project\n </label>\n <select\n id=\"task-board-project\"\n className=\"mt-2 rounded-md border border-input bg-background px-2 py-1 text-sm\"\n value={selectedProject}\n onChange={(e) => setBoardProject(e.target.value)}\n >\n {boardProjects.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n <option value=\"\">All projects — status overview</option>\n </select>\n <p className=\"text-sm text-muted-foreground\" data-testid=\"task-board-total\">\n {total} tasks across all projects\n {tasks.length < total && ` · showing ${tasks.length}`}\n </p>\n </div>\n </div>\n {selectedProject ? (\n <ProjectBoard projectId={selectedProject} projectName={selectedProjectName} />\n ) : (\n <>\n <div className=\"flex gap-4 items-center flex-wrap\">\n <Filter className=\"h-4 w-4 text-muted-foreground\" />\n <div className=\"flex gap-1\">\n {['all', 'low', 'medium', 'high', 'critical'].map((p) => (\n <button\n key={p}\n onClick={() => setPriorityFilter(p)}\n className={`px-2 py-1 text-xs rounded capitalize ${priorityFilter === p ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n {p}\n </button>\n ))}\n </div>\n <div className=\"flex gap-1\">\n {['all', 'story', 'bug', 'task', 'epic'].map((t) => (\n <button\n key={t}\n onClick={() => setTypeFilter(t)}\n className={`px-2 py-1 text-xs rounded capitalize ${typeFilter === t ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n {t}\n </button>\n ))}\n </div>\n </div>\n <div className=\"grid grid-cols-7 gap-3 overflow-x-auto\">\n {columns.map((col) => {\n const colTasks = tasks.filter((t) => t.status === col);\n return (\n <div key={col} className=\"min-w-[200px]\">\n <div className={`border-t-2 ${colColor[col]} rounded-t-md p-2 bg-muted/50`}>\n <div className=\"flex justify-between items-center\">\n <span className=\"text-sm font-medium\">{colLabel[col]}</span>\n <span\n className=\"text-xs bg-muted rounded-full px-1.5\"\n data-testid={`task-col-count-${col}`}\n >\n {countFor(col)}\n </span>\n </div>\n </div>\n <div className=\"space-y-2 mt-2\">\n {colTasks.map((task) => (\n <Card\n key={task.id}\n {...rowLinkProps(\n () =>\n navigate(\n `/tasks/task-detail-page?taskId=${encodeURIComponent(task.id)}`\n ),\n task.title\n )}\n >\n <CardContent className=\"p-3 space-y-2\">\n <p className=\"text-sm font-medium leading-tight\">{task.title}</p>\n <p className=\"text-xs text-muted-foreground\">{task.key}</p>\n <div className=\"flex gap-1 flex-wrap\">\n <span\n className={`text-[10px] px-1.5 py-0.5 rounded-full capitalize ${priorityColor[task.priority] ?? ''}`}\n >\n {task.priority.toLowerCase()}\n </span>\n <span\n className={`text-[10px] px-1.5 py-0.5 rounded-full capitalize ${typeColor[task.type] ?? ''}`}\n >\n {task.type.toLowerCase()}\n </span>\n </div>\n <div className=\"flex items-center justify-between\">\n <div className=\"flex gap-1\">\n {(task.labels ?? []).slice(0, 2).map((l) => (\n <span key={l} className=\"text-[9px] px-1 py-0.5 rounded bg-muted\">\n {l}\n </span>\n ))}\n </div>\n <div className=\"flex items-center gap-1\">\n <span className=\"text-[10px] font-medium bg-primary/10 text-primary rounded-full h-5 w-5 flex items-center justify-center\">\n {task.storyPoints ?? 0}\n </span>\n </div>\n </div>\n <div className=\"flex justify-between text-[10px] text-muted-foreground\">\n <span>\n {task.assigneeId ? <PersonName id={task.assigneeId} /> : 'Unassigned'}\n </span>\n <span>\n {task.dueDate ? new Date(task.dueDate).toLocaleDateString() : ''}\n </span>\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n </div>\n );\n })}\n </div>\n {hasMore && (\n <div className=\"flex justify-center\">\n <button\n type=\"button\"\n onClick={loadMore}\n disabled={loadingMore}\n className=\"px-3 py-1.5 text-sm rounded-md bg-muted text-foreground hover:bg-muted/80 disabled:opacity-50\"\n data-testid=\"task-board-load-more\"\n >\n {loadingMore ? 'Loading…' : `Load more (${tasks.length} of ${total})`}\n </button>\n </div>\n )}\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TasksTasksPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TasksTasksPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent } from '@burdenoff/fe-libs/ui';\nimport { Filter } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { rowLinkProps } from '../utils/rowNavigation';\nimport { PersonName } from '../people/People';\nimport { ProjectBoard } from '../board/ProjectBoard';\nimport { ExportWorkItemsButton } from '../export/ExportWorkItemsButton';\n\nconst GET_WORK_ITEMS = gql`\n query GetWorkItems(\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 type\n status\n priority\n key\n assigneeId\n reporterId\n sprintId\n storyPoints\n estimateMinutes\n dueDate\n labels\n tags\n createdAt\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n }\n }\n`;\n\n// Server-side counts (QA H2): the list returns at most 100 rows per page, so\n// counting the loaded page undercounted every workspace past 100 items.\nconst GET_WORK_ITEM_COUNTS = gql`\n query GetWorkItemCounts($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {\n planmagnetWorkItemCounts(workspaceId: $workspaceId, filter: $filter) {\n total\n byStatus {\n status\n count\n }\n }\n }\n`;\n\nconst PAGE_SIZE = 100;\n\nconst GET_BOARD_PROJECTS = gql`\n query GetBoardProjects($workspaceId: ID!) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n }\n }\n`;\n\ninterface WorkItemNode {\n id: string;\n title: string;\n type: string;\n status: string;\n priority: string;\n key: string;\n assigneeId: string | null;\n reporterId: string | null;\n sprintId: string | null;\n storyPoints: number | null;\n estimateMinutes: number | null;\n dueDate: string | null;\n labels: string[];\n tags: string[];\n createdAt: string;\n}\n\nconst columns = [\n 'BACKLOG',\n 'TODO',\n 'IN_PROGRESS',\n 'IN_REVIEW',\n 'TESTING',\n 'DONE',\n 'CANCELLED',\n] as const;\n\nconst colLabel: Record<string, string> = {\n BACKLOG: 'Backlog',\n TODO: 'To Do',\n IN_PROGRESS: 'In Progress',\n IN_REVIEW: 'In Review',\n TESTING: 'Testing',\n DONE: 'Done',\n CANCELLED: 'Cancelled',\n};\n\nconst colColor: Record<string, string> = {\n BACKLOG: 'border-t-gray-400',\n TODO: 'border-t-blue-400',\n IN_PROGRESS: 'border-t-yellow-400',\n IN_REVIEW: 'border-t-purple-400',\n TESTING: 'border-t-teal-400',\n DONE: 'border-t-green-400',\n CANCELLED: 'border-t-muted-foreground',\n};\n\nconst priorityColor: Record<string, string> = {\n LOW: 'bg-gray-100 text-gray-700',\n MEDIUM: 'bg-blue-100 text-blue-700',\n HIGH: 'bg-orange-100 text-orange-700',\n CRITICAL: 'bg-red-100 text-red-700',\n};\n\nconst typeColor: Record<string, string> = {\n STORY: 'bg-green-100 text-green-700',\n BUG: 'bg-red-100 text-red-700',\n TASK: 'bg-blue-100 text-blue-700',\n EPIC: 'bg-purple-100 text-purple-700',\n SUBTASK: 'bg-gray-100 text-gray-700',\n};\n\nexport function TasksTasksPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const [priorityFilter, setPriorityFilter] = useState<string>('all');\n const [typeFilter, setTypeFilter] = useState<string>('all');\n // P1.2: the Task Board is a project's real board (server-side placement,\n // drag-and-drop, WIP limits). null = not chosen yet -> the first project;\n // '' = the cross-project status overview, which is NOT a board.\n const [boardProject, setBoardProject] = useState<string | null>(null);\n const { data: projectsData } = useQuery(GET_BOARD_PROJECTS, {\n variables: { workspaceId },\n skip: !workspaceId,\n });\n const boardProjects = projectsData?.planmagnetProjects?.items ?? [];\n const selectedProject = boardProject ?? boardProjects[0]?.id ?? '';\n const selectedProjectName = boardProjects.find((p) => p.id === selectedProject)?.name ?? '';\n\n // Filters go to the server so the loaded items AND the counts both reflect\n // every matching item, not just the first page.\n const filter = {\n ...(priorityFilter !== 'all' && { priority: priorityFilter.toUpperCase() }),\n ...(typeFilter !== 'all' && { type: typeFilter.toUpperCase() }),\n };\n const [loadingMore, setLoadingMore] = useState(false);\n\n const { data, loading, error, fetchMore } = useQuery<{\n planmagnetWorkItems: {\n items: WorkItemNode[];\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n totalCount: number;\n };\n }>(GET_WORK_ITEMS, {\n variables: { workspaceId, filter, pagination: { first: PAGE_SIZE } },\n // The list only feeds the cross-project overview; a board loads its own cards.\n skip: !workspaceId || !!selectedProject,\n });\n const { data: countsData } = useQuery<{\n planmagnetWorkItemCounts: { total: number; byStatus: { status: string; count: number }[] };\n }>(GET_WORK_ITEM_COUNTS, { variables: { workspaceId, filter }, skip: !workspaceId });\n\n const loadMore = async () => {\n const after = data?.planmagnetWorkItems?.pageInfo?.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { first: PAGE_SIZE, after } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetWorkItems: {\n ...fetchMoreResult.planmagnetWorkItems,\n items: [\n ...prev.planmagnetWorkItems.items,\n ...fetchMoreResult.planmagnetWorkItems.items,\n ],\n },\n }),\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n // Only before the first load: a refetch or a filter change must not blank the board.\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 }\n\n if (error) {\n return <div className=\"p-6 text-red-500\">Error: {error.message}</div>;\n }\n\n const tasks = data?.planmagnetWorkItems?.items ?? [];\n\n const counts = countsData?.planmagnetWorkItemCounts;\n const total = counts?.total ?? data?.planmagnetWorkItems?.totalCount ?? tasks.length;\n const countFor = (status: string) =>\n counts?.byStatus.find((c) => c.status === status)?.count ??\n tasks.filter((t) => t.status === status).length;\n const hasMore = data?.planmagnetWorkItems?.pageInfo?.hasNextPage ?? false;\n\n return (\n <div className=\"space-y-4 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Task Board</h1>\n <label className=\"sr-only\" htmlFor=\"task-board-project\">\n Project\n </label>\n <select\n id=\"task-board-project\"\n className=\"mt-2 rounded-md border border-input bg-background px-2 py-1 text-sm\"\n value={selectedProject}\n onChange={(e) => setBoardProject(e.target.value)}\n >\n {boardProjects.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n <option value=\"\">All projects — status overview</option>\n </select>\n <div className=\"mt-2\">\n <ExportWorkItemsButton projectId={selectedProject || undefined} />\n </div>\n <p className=\"text-sm text-muted-foreground\" data-testid=\"task-board-total\">\n {total} tasks across all projects\n {tasks.length < total && ` · showing ${tasks.length}`}\n </p>\n </div>\n </div>\n {selectedProject ? (\n <ProjectBoard projectId={selectedProject} projectName={selectedProjectName} />\n ) : (\n <>\n <div className=\"flex gap-4 items-center flex-wrap\">\n <Filter className=\"h-4 w-4 text-muted-foreground\" />\n <div className=\"flex gap-1\">\n {['all', 'low', 'medium', 'high', 'critical'].map((p) => (\n <button\n key={p}\n onClick={() => setPriorityFilter(p)}\n className={`px-2 py-1 text-xs rounded capitalize ${priorityFilter === p ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n {p}\n </button>\n ))}\n </div>\n <div className=\"flex gap-1\">\n {['all', 'story', 'bug', 'task', 'epic'].map((t) => (\n <button\n key={t}\n onClick={() => setTypeFilter(t)}\n className={`px-2 py-1 text-xs rounded capitalize ${typeFilter === t ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n {t}\n </button>\n ))}\n </div>\n </div>\n <div className=\"grid grid-cols-7 gap-3 overflow-x-auto\">\n {columns.map((col) => {\n const colTasks = tasks.filter((t) => t.status === col);\n return (\n <div key={col} className=\"min-w-[200px]\">\n <div className={`border-t-2 ${colColor[col]} rounded-t-md p-2 bg-muted/50`}>\n <div className=\"flex justify-between items-center\">\n <span className=\"text-sm font-medium\">{colLabel[col]}</span>\n <span\n className=\"text-xs bg-muted rounded-full px-1.5\"\n data-testid={`task-col-count-${col}`}\n >\n {countFor(col)}\n </span>\n </div>\n </div>\n <div className=\"space-y-2 mt-2\">\n {colTasks.map((task) => (\n <Card\n key={task.id}\n {...rowLinkProps(\n () =>\n navigate(\n `/tasks/task-detail-page?taskId=${encodeURIComponent(task.id)}`\n ),\n task.title\n )}\n >\n <CardContent className=\"p-3 space-y-2\">\n <p className=\"text-sm font-medium leading-tight\">{task.title}</p>\n <p className=\"text-xs text-muted-foreground\">{task.key}</p>\n <div className=\"flex gap-1 flex-wrap\">\n <span\n className={`text-[10px] px-1.5 py-0.5 rounded-full capitalize ${priorityColor[task.priority] ?? ''}`}\n >\n {task.priority.toLowerCase()}\n </span>\n <span\n className={`text-[10px] px-1.5 py-0.5 rounded-full capitalize ${typeColor[task.type] ?? ''}`}\n >\n {task.type.toLowerCase()}\n </span>\n </div>\n <div className=\"flex items-center justify-between\">\n <div className=\"flex gap-1\">\n {(task.labels ?? []).slice(0, 2).map((l) => (\n <span key={l} className=\"text-[9px] px-1 py-0.5 rounded bg-muted\">\n {l}\n </span>\n ))}\n </div>\n <div className=\"flex items-center gap-1\">\n <span className=\"text-[10px] font-medium bg-primary/10 text-primary rounded-full h-5 w-5 flex items-center justify-center\">\n {task.storyPoints ?? 0}\n </span>\n </div>\n </div>\n <div className=\"flex justify-between text-[10px] text-muted-foreground\">\n <span>\n {task.assigneeId ? <PersonName id={task.assigneeId} /> : 'Unassigned'}\n </span>\n <span>\n {task.dueDate ? new Date(task.dueDate).toLocaleDateString() : ''}\n </span>\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n </div>\n );\n })}\n </div>\n {hasMore && (\n <div className=\"flex justify-center\">\n <button\n type=\"button\"\n onClick={loadMore}\n disabled={loadingMore}\n className=\"px-3 py-1.5 text-sm rounded-md bg-muted text-foreground hover:bg-muted/80 disabled:opacity-50\"\n data-testid=\"task-board-load-more\"\n >\n {loadingMore ? 'Loading…' : `Load more (${tasks.length} of ${total})`}\n </button>\n </div>\n )}\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|