@burdenoff/microfe-assethandler 2026.820.1 → 2026.820.3
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/assethandler/pages/KnowledgeBasePage.d.ts.map +1 -1
- package/dist/assethandler/pages/KnowledgeBasePage.js +394 -287
- package/dist/assethandler/pages/KnowledgeBasePage.js.map +1 -1
- package/dist/assethandler/utils/knowledgeBase.d.ts +17 -0
- package/dist/assethandler/utils/knowledgeBase.d.ts.map +1 -0
- package/dist/assethandler/utils/knowledgeBase.js +17 -0
- package/dist/assethandler/utils/knowledgeBase.js.map +1 -0
- package/dist/test/setup.d.ts +2 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/package.json +9 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KnowledgeBasePage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/KnowledgeBasePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"KnowledgeBasePage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/KnowledgeBasePage.tsx"],"names":[],"mappings":"AA6iBA,wBAAgB,iBAAiB,gCAoUhC"}
|
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
1
|
+
import { useAssetHandlerStore as e } from "../store/assethandlerStore.js";
|
|
2
|
+
import { seriesColor as t } from "../components/charts/chartTheme.js";
|
|
3
|
+
import { useAssetHandlerNavigate as n } from "../hooks/useAssetHandlerNavigate.js";
|
|
4
|
+
import { cn as r } from "../utils/cn.js";
|
|
5
|
+
import { notify as i } from "../utils/toast.js";
|
|
6
|
+
import { BarsChart as a } from "../components/charts/BarsChart.js";
|
|
7
|
+
import { DonutChart as o } from "../components/charts/DonutChart.js";
|
|
8
|
+
import { Pagination as s, usePagination as c } from "../components/Pagination.js";
|
|
9
|
+
import { ChartCard as l } from "../components/ChartCard.js";
|
|
10
|
+
import { SectionHeader as u } from "../components/SectionHeader.js";
|
|
10
11
|
import "../components/index.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
12
|
+
import { formatAssetOption as d, formatIncidentOption as f, parseTagsInput as ee } from "../utils/knowledgeBase.js";
|
|
13
|
+
import { useMemo as p, useState as m } from "react";
|
|
14
|
+
import { BookOpen as h, ExternalLink as g, Eye as _, Link2 as v, Pencil as y, Plus as b, Search as x, User as S, X as C } from "lucide-react";
|
|
15
|
+
import { gql as w } from "@apollo/client";
|
|
16
|
+
import { useMutation as T, useQuery as E } from "@apollo/client/react";
|
|
17
|
+
import { Fragment as D, jsx as O, jsxs as k } from "react/jsx-runtime";
|
|
18
|
+
import { Button as A, GlassCard as j, IllustratedEmptyState as M, Input as N, Label as P, PagePurpose as F, ResponsiveDialog as I, ResponsiveDialogContent as L, ResponsiveDialogDescription as R, ResponsiveDialogFooter as te, ResponsiveDialogHeader as z, ResponsiveDialogTitle as B, Select as V, SelectContent as H, SelectItem as U, SelectTrigger as W, SelectValue as G, Textarea as K } from "@burdenoff/fe-libs/ui";
|
|
17
19
|
//#region src/assethandler/pages/KnowledgeBasePage.tsx
|
|
18
|
-
var
|
|
20
|
+
var ne = w`
|
|
19
21
|
query AssetHandlerKnowledgeArticles {
|
|
20
22
|
knowledgeArticles {
|
|
21
23
|
id
|
|
@@ -35,18 +37,25 @@ var H = y`
|
|
|
35
37
|
relatedIncidentId
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
|
-
`,
|
|
40
|
+
`, q = w`
|
|
39
41
|
mutation AssetHandlerCreateKnowledgeArticle($input: CreateKnowledgeArticleInput!) {
|
|
40
42
|
createKnowledgeArticle(input: $input) {
|
|
41
43
|
id
|
|
42
44
|
title
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
|
-
`,
|
|
47
|
+
`, J = w`
|
|
48
|
+
mutation AssetHandlerUpdateKnowledgeArticle($id: ID!, $input: UpdateKnowledgeArticleInput!) {
|
|
49
|
+
updateKnowledgeArticle(id: $id, input: $input) {
|
|
50
|
+
id
|
|
51
|
+
title
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`, re = [
|
|
46
55
|
"DRAFT",
|
|
47
56
|
"PUBLISHED",
|
|
48
57
|
"ARCHIVED"
|
|
49
|
-
],
|
|
58
|
+
], Y = {
|
|
50
59
|
PUBLISHED: {
|
|
51
60
|
label: "Published",
|
|
52
61
|
className: "bg-status-success-bg-subtle text-status-success-text"
|
|
@@ -59,193 +68,285 @@ var H = y`
|
|
|
59
68
|
label: "Archived",
|
|
60
69
|
className: "bg-status-warning-bg-subtle text-status-warning-text"
|
|
61
70
|
}
|
|
62
|
-
},
|
|
71
|
+
}, X = (e) => e.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""), Z = (e) => e ? new Date(e).toLocaleDateString(void 0, {
|
|
63
72
|
year: "numeric",
|
|
64
73
|
month: "short",
|
|
65
74
|
day: "numeric"
|
|
66
75
|
}) : "—";
|
|
67
|
-
function
|
|
76
|
+
function ie(e) {
|
|
68
77
|
if (e.summary && e.summary.trim()) return e.summary.trim();
|
|
69
78
|
let t = e.content.replace(/\s+/g, " ").trim();
|
|
70
79
|
return t.length > 160 ? `${t.slice(0, 160)}…` : t;
|
|
71
80
|
}
|
|
72
|
-
function
|
|
81
|
+
function Q(e) {
|
|
73
82
|
return (e.replace(/[^a-zA-Z0-9]/g, "").slice(0, 2) || "?").toUpperCase();
|
|
74
83
|
}
|
|
75
|
-
function
|
|
76
|
-
let t =
|
|
84
|
+
function $({ status: e }) {
|
|
85
|
+
let t = Y[e] ?? {
|
|
77
86
|
label: e,
|
|
78
87
|
className: "bg-bg-sunken text-text-secondary"
|
|
79
88
|
};
|
|
80
|
-
return /* @__PURE__ */
|
|
81
|
-
className:
|
|
89
|
+
return /* @__PURE__ */ O("span", {
|
|
90
|
+
className: r("inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium", t.className),
|
|
82
91
|
children: t.label
|
|
83
92
|
});
|
|
84
93
|
}
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
a(""), s(""), l(!1), f(""), m(""), g(""), v("PUBLISHED");
|
|
88
|
-
}, [x, { loading: S }] = b(U, {
|
|
89
|
-
onCompleted: (e) => {
|
|
90
|
-
r.success("Article published", `${e.createKnowledgeArticle.title} is now available.`), y(), t(!1), n();
|
|
91
|
-
},
|
|
92
|
-
onError: (e) => r.error("Could not publish article", e.message)
|
|
93
|
-
}), E = (e) => {
|
|
94
|
-
a(e), c || s(K(e));
|
|
95
|
-
};
|
|
96
|
-
return /* @__PURE__ */ C(A, {
|
|
94
|
+
function ae({ open: e, onOpenChange: t, article: n, onSaved: r }) {
|
|
95
|
+
return /* @__PURE__ */ O(I, {
|
|
97
96
|
open: e,
|
|
98
97
|
onOpenChange: t,
|
|
99
|
-
children: /* @__PURE__ */
|
|
98
|
+
children: /* @__PURE__ */ O(L, {
|
|
100
99
|
className: "max-h-[85vh] overflow-y-auto sm:max-w-2xl",
|
|
100
|
+
children: e && /* @__PURE__ */ O(oe, {
|
|
101
|
+
article: n,
|
|
102
|
+
onOpenChange: t,
|
|
103
|
+
onSaved: r
|
|
104
|
+
}, n?.id ?? "new")
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function oe({ article: t, onOpenChange: n, onSaved: r }) {
|
|
109
|
+
let a = !!t, { assets: o, incidents: s } = e(), [c, l] = m(t?.title ?? ""), [u, h] = m(t?.slug ?? ""), [g, _] = m(a), [v, y] = m(t?.summary ?? ""), [b, x] = m(t?.content ?? ""), [S, w] = m(t ? t.tags.join(", ") : ""), [E, j] = m(t?.status ?? "PUBLISHED"), [M, F] = m(t?.relatedAssetId ?? ""), [I, L] = m(t?.relatedIncidentId ?? ""), ne = () => {
|
|
110
|
+
l(""), h(""), _(!1), y(""), x(""), w(""), j("PUBLISHED"), F(""), L("");
|
|
111
|
+
}, [Y, { loading: Z }] = T(q, {
|
|
112
|
+
onCompleted: (e) => {
|
|
113
|
+
i.success("Article published", `${e.createKnowledgeArticle.title} is now available.`), ne(), n(!1), r();
|
|
114
|
+
},
|
|
115
|
+
onError: (e) => i.error("Could not publish article", e.message)
|
|
116
|
+
}), [ie, { loading: Q }] = T(J, {
|
|
117
|
+
refetchQueries: ["AssetHandlerKnowledgeArticles"],
|
|
118
|
+
onCompleted: (e) => {
|
|
119
|
+
i.success("Article updated", `${e.updateKnowledgeArticle.title} has been saved.`), n(!1), r();
|
|
120
|
+
},
|
|
121
|
+
onError: (e) => i.error("Could not update article", e.message)
|
|
122
|
+
}), $ = p(() => o.find((e) => e.id === M), [o, M]), ae = p(() => s.find((e) => e.id === I), [s, I]), oe = (e) => {
|
|
123
|
+
l(e), g || h(X(e));
|
|
124
|
+
};
|
|
125
|
+
return /* @__PURE__ */ k(D, { children: [
|
|
126
|
+
/* @__PURE__ */ k(z, { children: [/* @__PURE__ */ O(B, { children: a ? "Edit article" : "New Article" }), /* @__PURE__ */ O(R, { children: a ? "Update the runbook or fix so the team has the latest guidance." : "Capture a runbook or fix so your team can find it later." })] }),
|
|
127
|
+
/* @__PURE__ */ k("div", {
|
|
128
|
+
className: "space-y-4",
|
|
101
129
|
children: [
|
|
102
|
-
/* @__PURE__ */
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
/* @__PURE__ */
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
})]
|
|
122
|
-
})]
|
|
123
|
-
}),
|
|
124
|
-
/* @__PURE__ */ w("div", {
|
|
125
|
-
className: "space-y-2",
|
|
126
|
-
children: [/* @__PURE__ */ C(k, { children: "Summary" }), /* @__PURE__ */ C(O, {
|
|
127
|
-
placeholder: "One-line description",
|
|
128
|
-
value: u,
|
|
129
|
-
onChange: (e) => f(e.target.value)
|
|
130
|
-
})]
|
|
131
|
-
}),
|
|
132
|
-
/* @__PURE__ */ w("div", {
|
|
133
|
-
className: "space-y-2",
|
|
134
|
-
children: [/* @__PURE__ */ C(k, { children: "Body *" }), /* @__PURE__ */ C(V, {
|
|
135
|
-
placeholder: "Write the full article body — steps, context, links…",
|
|
136
|
-
value: p,
|
|
137
|
-
onChange: (e) => m(e.target.value),
|
|
138
|
-
rows: 8
|
|
139
|
-
})]
|
|
140
|
-
}),
|
|
141
|
-
/* @__PURE__ */ w("div", {
|
|
142
|
-
className: "grid gap-4 md:grid-cols-2",
|
|
143
|
-
children: [/* @__PURE__ */ w("div", {
|
|
144
|
-
className: "space-y-2",
|
|
145
|
-
children: [/* @__PURE__ */ C(k, { children: "Category tags" }), /* @__PURE__ */ C(O, {
|
|
146
|
-
placeholder: "Runbooks, Fixes, Procedures…",
|
|
147
|
-
value: h,
|
|
148
|
-
onChange: (e) => g(e.target.value)
|
|
149
|
-
})]
|
|
150
|
-
}), /* @__PURE__ */ w("div", {
|
|
151
|
-
className: "space-y-2",
|
|
152
|
-
children: [/* @__PURE__ */ C(k, { children: "Status" }), /* @__PURE__ */ w(I, {
|
|
153
|
-
value: _,
|
|
154
|
-
onValueChange: (e) => v(e),
|
|
155
|
-
children: [/* @__PURE__ */ C(z, { children: /* @__PURE__ */ C(B, {}) }), /* @__PURE__ */ C(L, { children: W.map((e) => /* @__PURE__ */ C(R, {
|
|
156
|
-
value: e,
|
|
157
|
-
children: e
|
|
158
|
-
}, e)) })]
|
|
159
|
-
})]
|
|
160
|
-
})]
|
|
161
|
-
})
|
|
162
|
-
]
|
|
130
|
+
/* @__PURE__ */ k("div", {
|
|
131
|
+
className: "grid gap-4 md:grid-cols-2",
|
|
132
|
+
children: [/* @__PURE__ */ k("div", {
|
|
133
|
+
className: "space-y-2",
|
|
134
|
+
children: [/* @__PURE__ */ O(P, { children: "Title *" }), /* @__PURE__ */ O(N, {
|
|
135
|
+
value: c,
|
|
136
|
+
onChange: (e) => oe(e.target.value)
|
|
137
|
+
})]
|
|
138
|
+
}), /* @__PURE__ */ k("div", {
|
|
139
|
+
className: "space-y-2",
|
|
140
|
+
children: [/* @__PURE__ */ O(P, { children: "Slug *" }), /* @__PURE__ */ O(N, {
|
|
141
|
+
value: u,
|
|
142
|
+
disabled: a,
|
|
143
|
+
onChange: (e) => {
|
|
144
|
+
_(!0), h(e.target.value);
|
|
145
|
+
}
|
|
146
|
+
})]
|
|
147
|
+
})]
|
|
163
148
|
}),
|
|
164
|
-
/* @__PURE__ */
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
149
|
+
/* @__PURE__ */ k("div", {
|
|
150
|
+
className: "space-y-2",
|
|
151
|
+
children: [/* @__PURE__ */ O(P, { children: "Summary" }), /* @__PURE__ */ O(N, {
|
|
152
|
+
placeholder: "One-line description",
|
|
153
|
+
value: v,
|
|
154
|
+
onChange: (e) => y(e.target.value)
|
|
155
|
+
})]
|
|
156
|
+
}),
|
|
157
|
+
/* @__PURE__ */ k("div", {
|
|
158
|
+
className: "space-y-2",
|
|
159
|
+
children: [/* @__PURE__ */ O(P, { children: "Body *" }), /* @__PURE__ */ O(K, {
|
|
160
|
+
placeholder: "Write the full article body — steps, context, links…",
|
|
161
|
+
value: b,
|
|
162
|
+
onChange: (e) => x(e.target.value),
|
|
163
|
+
rows: 8
|
|
164
|
+
})]
|
|
165
|
+
}),
|
|
166
|
+
/* @__PURE__ */ k("div", {
|
|
167
|
+
className: "grid gap-4 md:grid-cols-2",
|
|
168
|
+
children: [/* @__PURE__ */ k("div", {
|
|
169
|
+
className: "space-y-2",
|
|
170
|
+
children: [/* @__PURE__ */ O(P, { children: "Category tags" }), /* @__PURE__ */ O(N, {
|
|
171
|
+
placeholder: "Runbooks, Fixes, Procedures…",
|
|
172
|
+
value: S,
|
|
173
|
+
onChange: (e) => w(e.target.value)
|
|
174
|
+
})]
|
|
175
|
+
}), /* @__PURE__ */ k("div", {
|
|
176
|
+
className: "space-y-2",
|
|
177
|
+
children: [/* @__PURE__ */ O(P, { children: "Status" }), /* @__PURE__ */ k(V, {
|
|
178
|
+
value: E,
|
|
179
|
+
onValueChange: j,
|
|
180
|
+
children: [/* @__PURE__ */ O(W, { children: /* @__PURE__ */ O(G, {}) }), /* @__PURE__ */ O(H, { children: re.map((e) => /* @__PURE__ */ O(U, {
|
|
181
|
+
value: e,
|
|
182
|
+
children: e
|
|
183
|
+
}, e)) })]
|
|
184
|
+
})]
|
|
185
|
+
})]
|
|
186
|
+
}),
|
|
187
|
+
/* @__PURE__ */ k("div", {
|
|
188
|
+
className: "grid gap-4 md:grid-cols-2",
|
|
189
|
+
children: [/* @__PURE__ */ k("div", {
|
|
190
|
+
className: "space-y-2",
|
|
191
|
+
children: [
|
|
192
|
+
/* @__PURE__ */ O(P, { children: "Related asset" }),
|
|
193
|
+
/* @__PURE__ */ k(V, {
|
|
194
|
+
value: M,
|
|
195
|
+
onValueChange: F,
|
|
196
|
+
children: [/* @__PURE__ */ O(W, { children: /* @__PURE__ */ O(G, { placeholder: "Select an asset" }) }), /* @__PURE__ */ k(H, { children: [/* @__PURE__ */ O(U, {
|
|
197
|
+
value: "",
|
|
198
|
+
children: "None"
|
|
199
|
+
}), o.map((e) => /* @__PURE__ */ O(U, {
|
|
200
|
+
value: e.id,
|
|
201
|
+
children: d(e)
|
|
202
|
+
}, e.id))] })]
|
|
203
|
+
}),
|
|
204
|
+
$ ? /* @__PURE__ */ O("div", {
|
|
205
|
+
className: "flex flex-wrap gap-2",
|
|
206
|
+
children: /* @__PURE__ */ k("span", {
|
|
207
|
+
className: "inline-flex items-center gap-1 rounded-full bg-action-primary-bg/10 px-2 py-0.5 text-xs font-medium text-primary",
|
|
208
|
+
children: [d($), /* @__PURE__ */ O("button", {
|
|
209
|
+
type: "button",
|
|
210
|
+
onClick: () => F(""),
|
|
211
|
+
className: "rounded-full p-0.5 hover:bg-action-primary-bg/20",
|
|
212
|
+
"aria-label": "Clear related asset",
|
|
213
|
+
children: /* @__PURE__ */ O(C, { className: "h-3 w-3" })
|
|
214
|
+
})]
|
|
215
|
+
})
|
|
216
|
+
}) : null
|
|
217
|
+
]
|
|
218
|
+
}), /* @__PURE__ */ k("div", {
|
|
219
|
+
className: "space-y-2",
|
|
220
|
+
children: [
|
|
221
|
+
/* @__PURE__ */ O(P, { children: "Related incident" }),
|
|
222
|
+
/* @__PURE__ */ k(V, {
|
|
223
|
+
value: I,
|
|
224
|
+
onValueChange: L,
|
|
225
|
+
children: [/* @__PURE__ */ O(W, { children: /* @__PURE__ */ O(G, { placeholder: "Select an incident" }) }), /* @__PURE__ */ k(H, { children: [/* @__PURE__ */ O(U, {
|
|
226
|
+
value: "",
|
|
227
|
+
children: "None"
|
|
228
|
+
}), s.map((e) => /* @__PURE__ */ O(U, {
|
|
229
|
+
value: e.id,
|
|
230
|
+
children: f(e)
|
|
231
|
+
}, e.id))] })]
|
|
232
|
+
}),
|
|
233
|
+
ae ? /* @__PURE__ */ O("div", {
|
|
234
|
+
className: "flex flex-wrap gap-2",
|
|
235
|
+
children: /* @__PURE__ */ k("span", {
|
|
236
|
+
className: "inline-flex items-center gap-1 rounded-full bg-action-primary-bg/10 px-2 py-0.5 text-xs font-medium text-primary",
|
|
237
|
+
children: [f(ae), /* @__PURE__ */ O("button", {
|
|
238
|
+
type: "button",
|
|
239
|
+
onClick: () => L(""),
|
|
240
|
+
className: "rounded-full p-0.5 hover:bg-action-primary-bg/20",
|
|
241
|
+
"aria-label": "Clear related incident",
|
|
242
|
+
children: /* @__PURE__ */ O(C, { className: "h-3 w-3" })
|
|
243
|
+
})]
|
|
244
|
+
})
|
|
245
|
+
}) : null
|
|
246
|
+
]
|
|
247
|
+
})]
|
|
248
|
+
})
|
|
192
249
|
]
|
|
193
|
-
})
|
|
194
|
-
|
|
250
|
+
}),
|
|
251
|
+
/* @__PURE__ */ k(te, { children: [/* @__PURE__ */ O(A, {
|
|
252
|
+
variant: "outline",
|
|
253
|
+
onClick: () => n(!1),
|
|
254
|
+
disabled: Z || Q,
|
|
255
|
+
children: "Cancel"
|
|
256
|
+
}), /* @__PURE__ */ O(A, {
|
|
257
|
+
onClick: () => {
|
|
258
|
+
if (!c.trim() || !u.trim()) {
|
|
259
|
+
i.warning("Missing details", "Title and slug are required.");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (!b.trim()) {
|
|
263
|
+
i.warning("Body required", "Write the article body before publishing.");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
let e = ee(S), n = {
|
|
267
|
+
title: c.trim(),
|
|
268
|
+
summary: v.trim() || null,
|
|
269
|
+
content: b.trim(),
|
|
270
|
+
tags: e,
|
|
271
|
+
status: E,
|
|
272
|
+
relatedAssetId: M || null,
|
|
273
|
+
relatedIncidentId: I || null
|
|
274
|
+
};
|
|
275
|
+
a && t ? ie({ variables: {
|
|
276
|
+
id: t.id,
|
|
277
|
+
input: n
|
|
278
|
+
} }) : Y({ variables: { input: {
|
|
279
|
+
...n,
|
|
280
|
+
slug: u.trim()
|
|
281
|
+
} } });
|
|
282
|
+
},
|
|
283
|
+
disabled: Z || Q,
|
|
284
|
+
children: a ? Q ? "Saving…" : "Save changes" : Z ? "Publishing…" : "Publish article"
|
|
285
|
+
})] })
|
|
286
|
+
] });
|
|
195
287
|
}
|
|
196
|
-
function
|
|
197
|
-
let
|
|
198
|
-
return /* @__PURE__ */
|
|
288
|
+
function se({ article: e, onOpenChange: t, onEdit: r }) {
|
|
289
|
+
let i = n();
|
|
290
|
+
return /* @__PURE__ */ O(I, {
|
|
199
291
|
open: !!e,
|
|
200
|
-
onOpenChange:
|
|
201
|
-
children: /* @__PURE__ */
|
|
292
|
+
onOpenChange: t,
|
|
293
|
+
children: /* @__PURE__ */ O(L, {
|
|
202
294
|
className: "max-h-[85vh] overflow-y-auto sm:max-w-2xl",
|
|
203
|
-
children: e ? /* @__PURE__ */
|
|
204
|
-
/* @__PURE__ */
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
/* @__PURE__ */
|
|
213
|
-
className: "
|
|
214
|
-
children:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
295
|
+
children: e ? /* @__PURE__ */ k(D, { children: [
|
|
296
|
+
/* @__PURE__ */ k(z, { children: [/* @__PURE__ */ k("div", {
|
|
297
|
+
className: "flex flex-wrap items-center gap-2",
|
|
298
|
+
children: [/* @__PURE__ */ O($, { status: e.status }), e.tags.map((e) => /* @__PURE__ */ O("span", {
|
|
299
|
+
className: "rounded-full bg-action-primary-bg/10 px-2 py-0.5 text-xs font-medium text-primary",
|
|
300
|
+
children: e
|
|
301
|
+
}, e))]
|
|
302
|
+
}), /* @__PURE__ */ k("div", {
|
|
303
|
+
className: "flex items-start justify-between gap-4",
|
|
304
|
+
children: [/* @__PURE__ */ k("div", {
|
|
305
|
+
className: "min-w-0 flex-1",
|
|
306
|
+
children: [/* @__PURE__ */ O(B, {
|
|
307
|
+
className: "mt-1",
|
|
308
|
+
children: e.title
|
|
309
|
+
}), /* @__PURE__ */ k(R, { children: [
|
|
310
|
+
"Updated ",
|
|
311
|
+
Z(e.updatedAt),
|
|
312
|
+
" · ",
|
|
313
|
+
e.views.toLocaleString(),
|
|
314
|
+
" views"
|
|
315
|
+
] })]
|
|
316
|
+
}), /* @__PURE__ */ k(A, {
|
|
317
|
+
variant: "outline",
|
|
318
|
+
size: "sm",
|
|
319
|
+
onClick: () => {
|
|
320
|
+
t(!1), r(e);
|
|
321
|
+
},
|
|
322
|
+
children: [/* @__PURE__ */ O(y, { className: "mr-1.5 h-3.5 w-3.5" }), "Edit"]
|
|
323
|
+
})]
|
|
324
|
+
})] }),
|
|
325
|
+
/* @__PURE__ */ O("div", {
|
|
225
326
|
className: "whitespace-pre-wrap text-sm leading-relaxed text-text-primary",
|
|
226
327
|
children: e.content
|
|
227
328
|
}),
|
|
228
|
-
e.relatedAssetId || e.relatedIncidentId ? /* @__PURE__ */
|
|
329
|
+
e.relatedAssetId || e.relatedIncidentId ? /* @__PURE__ */ k("div", {
|
|
229
330
|
className: "mt-4 space-y-2 border-t border-border-seam pt-4",
|
|
230
|
-
children: [/* @__PURE__ */
|
|
331
|
+
children: [/* @__PURE__ */ k("h4", {
|
|
231
332
|
className: "flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-text-secondary",
|
|
232
|
-
children: [/* @__PURE__ */
|
|
233
|
-
}), /* @__PURE__ */
|
|
333
|
+
children: [/* @__PURE__ */ O(v, { className: "h-3.5 w-3.5" }), "Linked records"]
|
|
334
|
+
}), /* @__PURE__ */ k("div", {
|
|
234
335
|
className: "flex flex-wrap gap-2",
|
|
235
|
-
children: [e.relatedAssetId ? /* @__PURE__ */
|
|
336
|
+
children: [e.relatedAssetId ? /* @__PURE__ */ k(A, {
|
|
236
337
|
variant: "outline",
|
|
237
338
|
size: "sm",
|
|
238
339
|
onClick: () => {
|
|
239
|
-
|
|
340
|
+
t(!1), i(`/assets/${e.relatedAssetId}`);
|
|
240
341
|
},
|
|
241
|
-
children: ["View linked asset", /* @__PURE__ */
|
|
242
|
-
}) : null, e.relatedIncidentId ? /* @__PURE__ */
|
|
342
|
+
children: ["View linked asset", /* @__PURE__ */ O(g, { className: "ml-1.5 h-3.5 w-3.5" })]
|
|
343
|
+
}) : null, e.relatedIncidentId ? /* @__PURE__ */ k(A, {
|
|
243
344
|
variant: "outline",
|
|
244
345
|
size: "sm",
|
|
245
346
|
onClick: () => {
|
|
246
|
-
|
|
347
|
+
t(!1), i(`/itil/incidents/${e.relatedIncidentId}`);
|
|
247
348
|
},
|
|
248
|
-
children: ["View linked incident", /* @__PURE__ */
|
|
349
|
+
children: ["View linked incident", /* @__PURE__ */ O(g, { className: "ml-1.5 h-3.5 w-3.5" })]
|
|
249
350
|
}) : null]
|
|
250
351
|
})]
|
|
251
352
|
}) : null
|
|
@@ -253,90 +354,96 @@ function ie({ article: e, onOpenChange: n }) {
|
|
|
253
354
|
})
|
|
254
355
|
});
|
|
255
356
|
}
|
|
256
|
-
function
|
|
257
|
-
let [
|
|
357
|
+
function ce() {
|
|
358
|
+
let [e, n] = m(!1), [i, d] = m(null), [f, ee] = m(""), [g, v] = m("ALL"), [y, C] = m("ALL"), [w, T] = m("recent"), [D, I] = m(null), { data: L, loading: R, refetch: te } = E(ne, { fetchPolicy: "cache-and-network" }), z = p(() => L?.knowledgeArticles ?? [], [L?.knowledgeArticles]), B = p(() => {
|
|
258
359
|
let e = /* @__PURE__ */ new Map();
|
|
259
|
-
for (let t of
|
|
360
|
+
for (let t of z) for (let n of t.tags) e.set(n, (e.get(n) ?? 0) + 1);
|
|
260
361
|
return [...e.entries()].sort((e, t) => t[1] - e[1]);
|
|
261
|
-
}, [
|
|
262
|
-
let e =
|
|
263
|
-
return [...
|
|
362
|
+
}, [z]), K = p(() => {
|
|
363
|
+
let e = f.trim().toLowerCase();
|
|
364
|
+
return [...z.filter((t) => g !== "ALL" && !t.tags.includes(g) || y !== "ALL" && t.status !== y ? !1 : e ? [
|
|
264
365
|
t.title,
|
|
265
366
|
t.summary ?? "",
|
|
266
367
|
t.content,
|
|
267
368
|
t.tags.join(" ")
|
|
268
|
-
].join(" ").toLowerCase().includes(e) : !0)].sort((e, t) =>
|
|
369
|
+
].join(" ").toLowerCase().includes(e) : !0)].sort((e, t) => w === "views" ? t.views - e.views : new Date(t.updatedAt).getTime() - new Date(e.updatedAt).getTime());
|
|
269
370
|
}, [
|
|
270
|
-
|
|
271
|
-
|
|
371
|
+
z,
|
|
372
|
+
f,
|
|
373
|
+
g,
|
|
272
374
|
y,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
name: G[t]?.label ?? t,
|
|
375
|
+
w
|
|
376
|
+
]), q = p(() => {
|
|
377
|
+
let e = z.reduce((e, t) => (e[t.status] = (e[t.status] ?? 0) + 1, e), {});
|
|
378
|
+
return Object.entries(e).map(([e, n], r) => ({
|
|
379
|
+
name: Y[e]?.label ?? e,
|
|
279
380
|
value: n,
|
|
280
|
-
color:
|
|
381
|
+
color: t(r)
|
|
281
382
|
}));
|
|
282
|
-
}, [
|
|
383
|
+
}, [z]), J = p(() => [...z].filter((e) => e.views > 0).sort((e, t) => t.views - e.views).slice(0, 6).map((e) => ({
|
|
283
384
|
name: e.title,
|
|
284
385
|
views: e.views
|
|
285
|
-
})), [
|
|
386
|
+
})), [z]), X = c(K, {
|
|
286
387
|
pageSize: 9,
|
|
287
|
-
resetKey: `${
|
|
388
|
+
resetKey: `${f}|${g}|${y}|${w}`
|
|
288
389
|
});
|
|
289
|
-
return /* @__PURE__ */
|
|
390
|
+
return /* @__PURE__ */ k("div", {
|
|
290
391
|
className: "space-y-5 p-4 sm:p-6",
|
|
291
392
|
children: [
|
|
292
|
-
/* @__PURE__ */
|
|
393
|
+
/* @__PURE__ */ O(u, {
|
|
293
394
|
hero: !0,
|
|
294
|
-
icon:
|
|
395
|
+
icon: h,
|
|
295
396
|
title: "Knowledge Base",
|
|
296
397
|
description: "Turn one-off fixes and runbooks into repeatable, searchable operational knowledge.",
|
|
297
|
-
actions: /* @__PURE__ */
|
|
398
|
+
actions: /* @__PURE__ */ k(A, {
|
|
298
399
|
className: "w-full sm:w-auto",
|
|
299
|
-
onClick: () =>
|
|
300
|
-
|
|
400
|
+
onClick: () => {
|
|
401
|
+
d(null), n(!0);
|
|
402
|
+
},
|
|
403
|
+
children: [/* @__PURE__ */ O(b, { className: "mr-2 h-4 w-4" }), "New Article"]
|
|
301
404
|
})
|
|
302
405
|
}),
|
|
303
|
-
/* @__PURE__ */
|
|
304
|
-
/* @__PURE__ */
|
|
305
|
-
open:
|
|
306
|
-
onOpenChange:
|
|
307
|
-
|
|
406
|
+
/* @__PURE__ */ O(F, { children: "Capture the fixes and runbooks your team relies on so the next person doesn't start from scratch. Link articles to incidents and assets, then search them when troubleshooting." }),
|
|
407
|
+
/* @__PURE__ */ O(ae, {
|
|
408
|
+
open: e,
|
|
409
|
+
onOpenChange: n,
|
|
410
|
+
article: i,
|
|
411
|
+
onSaved: () => void te()
|
|
308
412
|
}),
|
|
309
|
-
/* @__PURE__ */
|
|
310
|
-
article:
|
|
311
|
-
onOpenChange: (e) => !e &&
|
|
413
|
+
/* @__PURE__ */ O(se, {
|
|
414
|
+
article: D,
|
|
415
|
+
onOpenChange: (e) => !e && I(null),
|
|
416
|
+
onEdit: (e) => {
|
|
417
|
+
I(null), d(e), n(!0);
|
|
418
|
+
}
|
|
312
419
|
}),
|
|
313
|
-
/* @__PURE__ */
|
|
420
|
+
/* @__PURE__ */ k("div", {
|
|
314
421
|
className: "relative",
|
|
315
|
-
children: [/* @__PURE__ */
|
|
422
|
+
children: [/* @__PURE__ */ O(x, { className: "absolute left-3.5 top-1/2 h-4.5 w-4.5 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ O(N, {
|
|
316
423
|
className: "h-12 pl-11 text-base",
|
|
317
424
|
placeholder: "Search runbooks, fixes and procedures…",
|
|
318
|
-
value:
|
|
319
|
-
onChange: (e) =>
|
|
425
|
+
value: f,
|
|
426
|
+
onChange: (e) => ee(e.target.value),
|
|
320
427
|
"aria-label": "Search knowledge articles"
|
|
321
428
|
})]
|
|
322
429
|
}),
|
|
323
|
-
|
|
430
|
+
z.length > 0 && (q.length > 0 || J.length > 0) ? /* @__PURE__ */ k("div", {
|
|
324
431
|
className: "grid gap-4 lg:grid-cols-2",
|
|
325
|
-
children: [/* @__PURE__ */
|
|
432
|
+
children: [/* @__PURE__ */ O(l, {
|
|
326
433
|
title: "Publishing status",
|
|
327
434
|
subtitle: "Articles by status",
|
|
328
|
-
children: /* @__PURE__ */
|
|
329
|
-
data:
|
|
435
|
+
children: /* @__PURE__ */ O(o, {
|
|
436
|
+
data: q,
|
|
330
437
|
height: 200,
|
|
331
438
|
centerLabel: "Articles",
|
|
332
|
-
centerValue:
|
|
439
|
+
centerValue: z.length,
|
|
333
440
|
ariaLabel: "Articles by status"
|
|
334
441
|
})
|
|
335
|
-
}), /* @__PURE__ */
|
|
442
|
+
}), /* @__PURE__ */ O(l, {
|
|
336
443
|
title: "Most consulted",
|
|
337
444
|
subtitle: "Top articles by views",
|
|
338
|
-
children: /* @__PURE__ */
|
|
339
|
-
data:
|
|
445
|
+
children: /* @__PURE__ */ O(a, {
|
|
446
|
+
data: J,
|
|
340
447
|
xKey: "name",
|
|
341
448
|
series: [{
|
|
342
449
|
key: "views",
|
|
@@ -350,89 +457,89 @@ function Y() {
|
|
|
350
457
|
})
|
|
351
458
|
})]
|
|
352
459
|
}) : null,
|
|
353
|
-
/* @__PURE__ */
|
|
460
|
+
/* @__PURE__ */ k("div", {
|
|
354
461
|
className: "grid gap-5 lg:grid-cols-[200px_1fr]",
|
|
355
|
-
children: [/* @__PURE__ */
|
|
462
|
+
children: [/* @__PURE__ */ k("aside", {
|
|
356
463
|
className: "space-y-3",
|
|
357
464
|
children: [
|
|
358
|
-
/* @__PURE__ */
|
|
465
|
+
/* @__PURE__ */ k("div", {
|
|
359
466
|
className: "flex items-center justify-between gap-2",
|
|
360
|
-
children: [/* @__PURE__ */
|
|
467
|
+
children: [/* @__PURE__ */ O("h3", {
|
|
361
468
|
className: "text-sm font-semibold text-text-primary",
|
|
362
469
|
children: "Category"
|
|
363
|
-
}), /* @__PURE__ */
|
|
364
|
-
value:
|
|
365
|
-
onValueChange: (e) =>
|
|
366
|
-
children: [/* @__PURE__ */
|
|
470
|
+
}), /* @__PURE__ */ k(V, {
|
|
471
|
+
value: w,
|
|
472
|
+
onValueChange: (e) => T(e),
|
|
473
|
+
children: [/* @__PURE__ */ O(W, {
|
|
367
474
|
className: "h-8 w-[130px] text-xs lg:hidden",
|
|
368
|
-
children: /* @__PURE__ */
|
|
369
|
-
}), /* @__PURE__ */
|
|
475
|
+
children: /* @__PURE__ */ O(G, {})
|
|
476
|
+
}), /* @__PURE__ */ k(H, { children: [/* @__PURE__ */ O(U, {
|
|
370
477
|
value: "recent",
|
|
371
478
|
children: "Recently updated"
|
|
372
|
-
}), /* @__PURE__ */
|
|
479
|
+
}), /* @__PURE__ */ O(U, {
|
|
373
480
|
value: "views",
|
|
374
481
|
children: "Most viewed"
|
|
375
482
|
})] })]
|
|
376
483
|
})]
|
|
377
484
|
}),
|
|
378
|
-
/* @__PURE__ */
|
|
485
|
+
/* @__PURE__ */ k("div", {
|
|
379
486
|
className: "flex gap-2 overflow-x-auto pb-1 lg:flex-col lg:overflow-visible lg:pb-0",
|
|
380
|
-
children: [/* @__PURE__ */
|
|
487
|
+
children: [/* @__PURE__ */ k("button", {
|
|
381
488
|
type: "button",
|
|
382
|
-
onClick: () =>
|
|
383
|
-
className:
|
|
384
|
-
children: ["All articles", /* @__PURE__ */
|
|
489
|
+
onClick: () => v("ALL"),
|
|
490
|
+
className: r("shrink-0 rounded-md px-3 py-1.5 text-left text-sm font-medium transition-colors lg:w-full", g === "ALL" ? "bg-action-primary-bg/10 text-primary" : "text-text-secondary hover:text-text-primary"),
|
|
491
|
+
children: ["All articles", /* @__PURE__ */ O("span", {
|
|
385
492
|
className: "ml-1.5 text-xs text-text-muted tabular-nums",
|
|
386
|
-
children:
|
|
493
|
+
children: z.length
|
|
387
494
|
})]
|
|
388
|
-
}),
|
|
495
|
+
}), B.map(([e, t]) => /* @__PURE__ */ k("button", {
|
|
389
496
|
type: "button",
|
|
390
|
-
onClick: () =>
|
|
391
|
-
className:
|
|
392
|
-
children: [/* @__PURE__ */
|
|
497
|
+
onClick: () => v(e),
|
|
498
|
+
className: r("flex shrink-0 items-center justify-between gap-2 rounded-md px-3 py-1.5 text-left text-sm font-medium transition-colors lg:w-full", g === e ? "bg-action-primary-bg/10 text-primary" : "text-text-secondary hover:text-text-primary"),
|
|
499
|
+
children: [/* @__PURE__ */ O("span", {
|
|
393
500
|
className: "truncate",
|
|
394
501
|
children: e
|
|
395
|
-
}), /* @__PURE__ */
|
|
502
|
+
}), /* @__PURE__ */ O("span", {
|
|
396
503
|
className: "text-xs text-text-muted tabular-nums",
|
|
397
504
|
children: t
|
|
398
505
|
})]
|
|
399
506
|
}, e))]
|
|
400
507
|
}),
|
|
401
|
-
/* @__PURE__ */
|
|
508
|
+
/* @__PURE__ */ k("div", {
|
|
402
509
|
className: "hidden space-y-1.5 border-t border-border-seam pt-3 lg:block",
|
|
403
510
|
children: [
|
|
404
|
-
/* @__PURE__ */
|
|
511
|
+
/* @__PURE__ */ O(P, {
|
|
405
512
|
className: "text-xs",
|
|
406
513
|
children: "Status"
|
|
407
514
|
}),
|
|
408
|
-
/* @__PURE__ */
|
|
409
|
-
value:
|
|
410
|
-
onValueChange:
|
|
411
|
-
children: [/* @__PURE__ */
|
|
515
|
+
/* @__PURE__ */ k(V, {
|
|
516
|
+
value: y,
|
|
517
|
+
onValueChange: C,
|
|
518
|
+
children: [/* @__PURE__ */ O(W, {
|
|
412
519
|
className: "h-9",
|
|
413
|
-
children: /* @__PURE__ */
|
|
414
|
-
}), /* @__PURE__ */
|
|
520
|
+
children: /* @__PURE__ */ O(G, {})
|
|
521
|
+
}), /* @__PURE__ */ k(H, { children: [/* @__PURE__ */ O(U, {
|
|
415
522
|
value: "ALL",
|
|
416
523
|
children: "All statuses"
|
|
417
|
-
}),
|
|
524
|
+
}), re.map((e) => /* @__PURE__ */ O(U, {
|
|
418
525
|
value: e,
|
|
419
526
|
children: e
|
|
420
527
|
}, e))] })]
|
|
421
528
|
}),
|
|
422
|
-
/* @__PURE__ */
|
|
529
|
+
/* @__PURE__ */ O(P, {
|
|
423
530
|
className: "text-xs",
|
|
424
531
|
children: "Sort"
|
|
425
532
|
}),
|
|
426
|
-
/* @__PURE__ */
|
|
427
|
-
value:
|
|
428
|
-
onValueChange: (e) =>
|
|
429
|
-
children: [/* @__PURE__ */
|
|
533
|
+
/* @__PURE__ */ k(V, {
|
|
534
|
+
value: w,
|
|
535
|
+
onValueChange: (e) => T(e),
|
|
536
|
+
children: [/* @__PURE__ */ O(W, {
|
|
430
537
|
className: "h-9",
|
|
431
|
-
children: /* @__PURE__ */
|
|
432
|
-
}), /* @__PURE__ */
|
|
538
|
+
children: /* @__PURE__ */ O(G, {})
|
|
539
|
+
}), /* @__PURE__ */ k(H, { children: [/* @__PURE__ */ O(U, {
|
|
433
540
|
value: "recent",
|
|
434
541
|
children: "Recently updated"
|
|
435
|
-
}), /* @__PURE__ */
|
|
542
|
+
}), /* @__PURE__ */ O(U, {
|
|
436
543
|
value: "views",
|
|
437
544
|
children: "Most viewed"
|
|
438
545
|
})] })]
|
|
@@ -440,73 +547,73 @@ function Y() {
|
|
|
440
547
|
]
|
|
441
548
|
})
|
|
442
549
|
]
|
|
443
|
-
}), /* @__PURE__ */
|
|
444
|
-
|
|
550
|
+
}), /* @__PURE__ */ k("div", { children: [
|
|
551
|
+
R && z.length === 0 ? /* @__PURE__ */ O("div", {
|
|
445
552
|
className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-3",
|
|
446
|
-
children: Array.from({ length: 6 }).map((e, t) => /* @__PURE__ */
|
|
553
|
+
children: Array.from({ length: 6 }).map((e, t) => /* @__PURE__ */ O("div", { className: "h-40 rounded-xl bg-bg-sunken motion-safe:animate-vc-shimmer" }, t))
|
|
447
554
|
}) : null,
|
|
448
|
-
!
|
|
449
|
-
illustration:
|
|
450
|
-
title:
|
|
451
|
-
description:
|
|
555
|
+
!R && K.length === 0 ? /* @__PURE__ */ O(M, {
|
|
556
|
+
illustration: f || g !== "ALL" || y !== "ALL" ? "empty-search" : "empty-data",
|
|
557
|
+
title: f || g !== "ALL" || y !== "ALL" ? "No articles match your filters" : "No knowledge articles found",
|
|
558
|
+
description: f || g !== "ALL" || y !== "ALL" ? "Try a different term, category or status." : "Publish an article to start building your knowledge base."
|
|
452
559
|
}) : null,
|
|
453
|
-
/* @__PURE__ */
|
|
560
|
+
/* @__PURE__ */ O("div", {
|
|
454
561
|
className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-3",
|
|
455
|
-
children:
|
|
562
|
+
children: X.pageItems.map((e) => /* @__PURE__ */ O(j, {
|
|
456
563
|
treatment: "glass",
|
|
457
564
|
className: "border-border-seam",
|
|
458
|
-
children: /* @__PURE__ */
|
|
565
|
+
children: /* @__PURE__ */ k("button", {
|
|
459
566
|
type: "button",
|
|
460
|
-
onClick: () =>
|
|
567
|
+
onClick: () => I(e),
|
|
461
568
|
className: "flex h-full w-full flex-col gap-3 p-4 text-left",
|
|
462
569
|
children: [
|
|
463
|
-
/* @__PURE__ */
|
|
570
|
+
/* @__PURE__ */ k("div", {
|
|
464
571
|
className: "flex flex-wrap items-center gap-1.5",
|
|
465
|
-
children: [/* @__PURE__ */
|
|
572
|
+
children: [/* @__PURE__ */ O($, { status: e.status }), e.tags.slice(0, 1).map((e) => /* @__PURE__ */ O("span", {
|
|
466
573
|
className: "rounded-full bg-action-primary-bg/10 px-2 py-0.5 text-xs font-medium text-primary",
|
|
467
574
|
children: e
|
|
468
575
|
}, e))]
|
|
469
576
|
}),
|
|
470
|
-
/* @__PURE__ */
|
|
577
|
+
/* @__PURE__ */ O("h2", {
|
|
471
578
|
className: "line-clamp-2 text-base font-semibold text-text-primary",
|
|
472
579
|
children: e.title
|
|
473
580
|
}),
|
|
474
|
-
/* @__PURE__ */
|
|
581
|
+
/* @__PURE__ */ O("p", {
|
|
475
582
|
className: "line-clamp-2 flex-1 text-sm text-text-secondary",
|
|
476
|
-
children:
|
|
583
|
+
children: ie(e)
|
|
477
584
|
}),
|
|
478
|
-
/* @__PURE__ */
|
|
585
|
+
/* @__PURE__ */ k("div", {
|
|
479
586
|
className: "flex items-center justify-between gap-2 border-t border-border-seam/60 pt-3",
|
|
480
|
-
children: [/* @__PURE__ */
|
|
587
|
+
children: [/* @__PURE__ */ k("div", {
|
|
481
588
|
className: "flex min-w-0 items-center gap-2",
|
|
482
|
-
children: [/* @__PURE__ */
|
|
589
|
+
children: [/* @__PURE__ */ O("span", {
|
|
483
590
|
className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-[10px] font-semibold text-text-secondary",
|
|
484
|
-
children: e.updatedBy ?
|
|
485
|
-
}), /* @__PURE__ */
|
|
591
|
+
children: e.updatedBy ? Q(e.updatedBy) : /* @__PURE__ */ O(S, { className: "h-3 w-3" })
|
|
592
|
+
}), /* @__PURE__ */ k("span", {
|
|
486
593
|
className: "truncate text-xs text-text-secondary",
|
|
487
|
-
children: ["Updated ",
|
|
594
|
+
children: ["Updated ", Z(e.updatedAt)]
|
|
488
595
|
})]
|
|
489
|
-
}), /* @__PURE__ */
|
|
596
|
+
}), /* @__PURE__ */ k("span", {
|
|
490
597
|
className: "flex shrink-0 items-center gap-1 text-xs text-text-muted tabular-nums",
|
|
491
|
-
children: [/* @__PURE__ */
|
|
598
|
+
children: [/* @__PURE__ */ O(_, { className: "h-3.5 w-3.5" }), e.views.toLocaleString()]
|
|
492
599
|
})]
|
|
493
600
|
})
|
|
494
601
|
]
|
|
495
602
|
})
|
|
496
603
|
}, e.id))
|
|
497
604
|
}),
|
|
498
|
-
/* @__PURE__ */
|
|
605
|
+
/* @__PURE__ */ O("div", {
|
|
499
606
|
className: "mt-4",
|
|
500
|
-
children: /* @__PURE__ */
|
|
501
|
-
page:
|
|
502
|
-
pageCount:
|
|
503
|
-
rangeStart:
|
|
504
|
-
rangeEnd:
|
|
505
|
-
totalItems:
|
|
506
|
-
canPrev:
|
|
507
|
-
canNext:
|
|
508
|
-
onPrev:
|
|
509
|
-
onNext:
|
|
607
|
+
children: /* @__PURE__ */ O(s, {
|
|
608
|
+
page: X.page,
|
|
609
|
+
pageCount: X.pageCount,
|
|
610
|
+
rangeStart: X.rangeStart,
|
|
611
|
+
rangeEnd: X.rangeEnd,
|
|
612
|
+
totalItems: X.totalItems,
|
|
613
|
+
canPrev: X.canPrev,
|
|
614
|
+
canNext: X.canNext,
|
|
615
|
+
onPrev: X.prevPage,
|
|
616
|
+
onNext: X.nextPage,
|
|
510
617
|
itemLabel: "articles"
|
|
511
618
|
})
|
|
512
619
|
})
|
|
@@ -516,6 +623,6 @@ function Y() {
|
|
|
516
623
|
});
|
|
517
624
|
}
|
|
518
625
|
//#endregion
|
|
519
|
-
export {
|
|
626
|
+
export { ce as KnowledgeBasePage };
|
|
520
627
|
|
|
521
628
|
//# sourceMappingURL=KnowledgeBasePage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KnowledgeBasePage.js","names":[],"sources":["../../../src/assethandler/pages/KnowledgeBasePage.tsx"],"sourcesContent":["import { BookOpen, Plus, Search, Eye, User, Link2, ExternalLink } from 'lucide-react';\nimport {\n Button,\n IllustratedEmptyState,\n GlassCard,\n PagePurpose,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { useMemo, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Pagination,\n usePagination,\n SectionHeader,\n ChartCard,\n DonutChart,\n BarsChart,\n seriesColor,\n type DonutSlice,\n} from '../components';\nimport { useAssetHandlerNavigate } from '../hooks/useAssetHandlerNavigate';\nimport { cn } from '../utils/cn';\nimport { notify } from '../utils/toast';\n\nconst KNOWLEDGE_ARTICLES_QUERY = gql`\n query AssetHandlerKnowledgeArticles {\n knowledgeArticles {\n id\n title\n slug\n summary\n content\n status\n tags\n views\n createdBy\n updatedBy\n publishedAt\n createdAt\n updatedAt\n relatedAssetId\n relatedIncidentId\n }\n }\n`;\n\nconst CREATE_KNOWLEDGE_ARTICLE_MUTATION = gql`\n mutation AssetHandlerCreateKnowledgeArticle($input: CreateKnowledgeArticleInput!) {\n createKnowledgeArticle(input: $input) {\n id\n title\n }\n }\n`;\n\ninterface KnowledgeArticle {\n id: string;\n title: string;\n slug: string;\n summary: string | null;\n content: string;\n status: string;\n tags: string[];\n views: number;\n createdBy: string;\n updatedBy: string;\n publishedAt: string | null;\n createdAt: string;\n updatedAt: string;\n relatedAssetId: string | null;\n relatedIncidentId: string | null;\n}\n\ninterface CreateKnowledgeArticleResult {\n createKnowledgeArticle: { id: string; title: string };\n}\n\nconst ARTICLE_STATUSES = ['DRAFT', 'PUBLISHED', 'ARCHIVED'] as const;\n\nconst STATUS_META: Record<string, { label: string; className: string }> = {\n PUBLISHED: {\n label: 'Published',\n className: 'bg-status-success-bg-subtle text-status-success-text',\n },\n DRAFT: { label: 'Draft', className: 'bg-bg-sunken text-text-secondary' },\n ARCHIVED: {\n label: 'Archived',\n className: 'bg-status-warning-bg-subtle text-status-warning-text',\n },\n};\n\nconst slugify = (value: string): string =>\n value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n\nconst formatDate = (value: string | null): string =>\n value\n ? new Date(value).toLocaleDateString(undefined, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n })\n : '—';\n\n/** A 2-line excerpt: the summary if present, otherwise the opening of the body. */\nfunction excerptOf(article: KnowledgeArticle): string {\n if (article.summary && article.summary.trim()) return article.summary.trim();\n const body = article.content.replace(/\\s+/g, ' ').trim();\n return body.length > 160 ? `${body.slice(0, 160)}…` : body;\n}\n\n/** Short avatar initials from an actor id (best-effort, no fabricated names). */\nfunction initialsOf(actor: string): string {\n const clean = actor.replace(/[^a-zA-Z0-9]/g, '');\n return (clean.slice(0, 2) || '?').toUpperCase();\n}\n\nfunction StatusPill({ status }: { status: string }) {\n const meta = STATUS_META[status] ?? {\n label: status,\n className: 'bg-bg-sunken text-text-secondary',\n };\n return (\n <span\n className={cn(\n 'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',\n meta.className\n )}\n >\n {meta.label}\n </span>\n );\n}\n\nfunction PublishArticleDialog({\n open,\n onOpenChange,\n onPublished,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onPublished: () => void;\n}) {\n const [title, setTitle] = useState('');\n const [slug, setSlug] = useState('');\n const [slugTouched, setSlugTouched] = useState(false);\n const [summary, setSummary] = useState('');\n const [content, setContent] = useState('');\n const [tags, setTags] = useState('');\n const [status, setStatus] = useState<(typeof ARTICLE_STATUSES)[number]>('PUBLISHED');\n\n const reset = () => {\n setTitle('');\n setSlug('');\n setSlugTouched(false);\n setSummary('');\n setContent('');\n setTags('');\n setStatus('PUBLISHED');\n };\n\n const [createArticle, { loading }] = useMutation<CreateKnowledgeArticleResult>(\n CREATE_KNOWLEDGE_ARTICLE_MUTATION,\n {\n onCompleted: (data) => {\n notify.success(\n 'Article published',\n `${data.createKnowledgeArticle.title} is now available.`\n );\n reset();\n onOpenChange(false);\n onPublished();\n },\n onError: (error) => notify.error('Could not publish article', error.message),\n }\n );\n\n const handleTitleChange = (next: string) => {\n setTitle(next);\n if (!slugTouched) setSlug(slugify(next));\n };\n\n const handleSubmit = () => {\n if (!title.trim() || !slug.trim()) {\n notify.warning('Missing details', 'Title and slug are required.');\n return;\n }\n if (!content.trim()) {\n notify.warning('Body required', 'Write the article body before publishing.');\n return;\n }\n const parsedTags = tags\n .split(',')\n .map((t) => t.trim())\n .filter((t) => t.length > 0);\n void createArticle({\n variables: {\n input: {\n title: title.trim(),\n slug: slug.trim(),\n summary: summary.trim() || null,\n content: content.trim(),\n tags: parsedTags,\n status,\n },\n },\n });\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-2xl\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New Article</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Capture a runbook or fix so your team can find it later.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-4\">\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label>Title *</Label>\n <Input value={title} onChange={(e) => handleTitleChange(e.target.value)} />\n </div>\n <div className=\"space-y-2\">\n <Label>Slug *</Label>\n <Input\n value={slug}\n onChange={(e) => {\n setSlugTouched(true);\n setSlug(e.target.value);\n }}\n />\n </div>\n </div>\n <div className=\"space-y-2\">\n <Label>Summary</Label>\n <Input\n placeholder=\"One-line description\"\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Body *</Label>\n <Textarea\n placeholder=\"Write the full article body — steps, context, links…\"\n value={content}\n onChange={(e) => setContent(e.target.value)}\n rows={8}\n />\n </div>\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label>Category tags</Label>\n <Input\n placeholder=\"Runbooks, Fixes, Procedures…\"\n value={tags}\n onChange={(e) => setTags(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Status</Label>\n <Select value={status} onValueChange={(v) => setStatus(v as typeof status)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ARTICLE_STATUSES.map((s) => (\n <SelectItem key={s} value={s}>\n {s}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)} disabled={loading}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={loading}>\n {loading ? 'Publishing…' : 'Publish article'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction ArticleReader({\n article,\n onOpenChange,\n}: {\n article: KnowledgeArticle | null;\n onOpenChange: (open: boolean) => void;\n}) {\n const navigate = useAssetHandlerNavigate();\n return (\n <ResponsiveDialog open={Boolean(article)} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-2xl\">\n {article ? (\n <>\n <ResponsiveDialogHeader>\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusPill status={article.status} />\n {article.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-action-primary-bg/10 px-2 py-0.5 text-xs font-medium text-primary\"\n >\n {tag}\n </span>\n ))}\n </div>\n <ResponsiveDialogTitle className=\"mt-1\">{article.title}</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Updated {formatDate(article.updatedAt)} · {article.views.toLocaleString()} views\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-primary\">\n {article.content}\n </div>\n {article.relatedAssetId || article.relatedIncidentId ? (\n <div className=\"mt-4 space-y-2 border-t border-border-seam pt-4\">\n <h4 className=\"flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n <Link2 className=\"h-3.5 w-3.5\" />\n Linked records\n </h4>\n <div className=\"flex flex-wrap gap-2\">\n {article.relatedAssetId ? (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n onOpenChange(false);\n navigate(`/assets/${article.relatedAssetId}`);\n }}\n >\n View linked asset\n <ExternalLink className=\"ml-1.5 h-3.5 w-3.5\" />\n </Button>\n ) : null}\n {article.relatedIncidentId ? (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n onOpenChange(false);\n navigate(`/itil/incidents/${article.relatedIncidentId}`);\n }}\n >\n View linked incident\n <ExternalLink className=\"ml-1.5 h-3.5 w-3.5\" />\n </Button>\n ) : null}\n </div>\n </div>\n ) : null}\n </>\n ) : null}\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\ntype SortKey = 'recent' | 'views';\n\nexport function KnowledgeBasePage() {\n const [dialogOpen, setDialogOpen] = useState(false);\n const [search, setSearch] = useState('');\n const [category, setCategory] = useState<string>('ALL');\n const [statusFilter, setStatusFilter] = useState<string>('ALL');\n const [sort, setSort] = useState<SortKey>('recent');\n const [reading, setReading] = useState<KnowledgeArticle | null>(null);\n\n const { data, loading, refetch } = useQuery<{ knowledgeArticles: KnowledgeArticle[] }>(\n KNOWLEDGE_ARTICLES_QUERY,\n { fetchPolicy: 'cache-and-network' }\n );\n\n const articles = useMemo(() => data?.knowledgeArticles ?? [], [data?.knowledgeArticles]);\n\n const categories = useMemo(() => {\n const counts = new Map<string, number>();\n for (const a of articles) {\n for (const tag of a.tags) counts.set(tag, (counts.get(tag) ?? 0) + 1);\n }\n return [...counts.entries()].sort((a, b) => b[1] - a[1]);\n }, [articles]);\n\n const filtered = useMemo(() => {\n const term = search.trim().toLowerCase();\n const list = articles.filter((article) => {\n if (category !== 'ALL' && !article.tags.includes(category)) return false;\n if (statusFilter !== 'ALL' && article.status !== statusFilter) return false;\n if (!term) return true;\n const haystack = [\n article.title,\n article.summary ?? '',\n article.content,\n article.tags.join(' '),\n ]\n .join(' ')\n .toLowerCase();\n return haystack.includes(term);\n });\n return [...list].sort((a, b) =>\n sort === 'views'\n ? b.views - a.views\n : new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()\n );\n }, [articles, search, category, statusFilter, sort]);\n\n const statusMix = useMemo<DonutSlice[]>(() => {\n const counts = articles.reduce<Record<string, number>>((acc, a) => {\n acc[a.status] = (acc[a.status] ?? 0) + 1;\n return acc;\n }, {});\n return Object.entries(counts).map(([status, value], i) => ({\n name: STATUS_META[status]?.label ?? status,\n value,\n color: seriesColor(i),\n }));\n }, [articles]);\n\n const topByViews = useMemo(\n () =>\n [...articles]\n .filter((a) => a.views > 0)\n .sort((a, b) => b.views - a.views)\n .slice(0, 6)\n .map((a) => ({ name: a.title, views: a.views })),\n [articles]\n );\n\n const pagination = usePagination(filtered, {\n pageSize: 9,\n resetKey: `${search}|${category}|${statusFilter}|${sort}`,\n });\n\n return (\n <div className=\"space-y-5 p-4 sm:p-6\">\n <SectionHeader\n hero\n icon={BookOpen}\n title=\"Knowledge Base\"\n description=\"Turn one-off fixes and runbooks into repeatable, searchable operational knowledge.\"\n actions={\n <Button className=\"w-full sm:w-auto\" onClick={() => setDialogOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New Article\n </Button>\n }\n />\n <PagePurpose>\n Capture the fixes and runbooks your team relies on so the next person doesn't start\n from scratch. Link articles to incidents and assets, then search them when troubleshooting.\n </PagePurpose>\n\n <PublishArticleDialog\n open={dialogOpen}\n onOpenChange={setDialogOpen}\n onPublished={() => void refetch()}\n />\n <ArticleReader article={reading} onOpenChange={(open) => !open && setReading(null)} />\n\n <div className=\"relative\">\n <Search className=\"absolute left-3.5 top-1/2 h-4.5 w-4.5 -translate-y-1/2 text-text-secondary\" />\n <Input\n className=\"h-12 pl-11 text-base\"\n placeholder=\"Search runbooks, fixes and procedures…\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n aria-label=\"Search knowledge articles\"\n />\n </div>\n\n {articles.length > 0 && (statusMix.length > 0 || topByViews.length > 0) ? (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <ChartCard title=\"Publishing status\" subtitle=\"Articles by status\">\n <DonutChart\n data={statusMix}\n height={200}\n centerLabel=\"Articles\"\n centerValue={articles.length}\n ariaLabel=\"Articles by status\"\n />\n </ChartCard>\n <ChartCard title=\"Most consulted\" subtitle=\"Top articles by views\">\n <BarsChart\n data={topByViews}\n xKey=\"name\"\n series={[{ key: 'views', label: 'Views' }]}\n orientation=\"horizontal\"\n height={200}\n categoryWidth={150}\n emptyLabel=\"No views recorded yet\"\n ariaLabel=\"Top articles by views\"\n />\n </ChartCard>\n </div>\n ) : null}\n\n <div className=\"grid gap-5 lg:grid-cols-[200px_1fr]\">\n {/* Category facet rail */}\n <aside className=\"space-y-3\">\n <div className=\"flex items-center justify-between gap-2\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Category</h3>\n <Select value={sort} onValueChange={(v) => setSort(v as SortKey)}>\n <SelectTrigger className=\"h-8 w-[130px] text-xs lg:hidden\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"recent\">Recently updated</SelectItem>\n <SelectItem value=\"views\">Most viewed</SelectItem>\n </SelectContent>\n </Select>\n </div>\n {/* Mobile: horizontal chips; desktop: vertical list */}\n <div className=\"flex gap-2 overflow-x-auto pb-1 lg:flex-col lg:overflow-visible lg:pb-0\">\n <button\n type=\"button\"\n onClick={() => setCategory('ALL')}\n className={cn(\n 'shrink-0 rounded-md px-3 py-1.5 text-left text-sm font-medium transition-colors lg:w-full',\n category === 'ALL'\n ? 'bg-action-primary-bg/10 text-primary'\n : 'text-text-secondary hover:text-text-primary'\n )}\n >\n All articles\n <span className=\"ml-1.5 text-xs text-text-muted tabular-nums\">{articles.length}</span>\n </button>\n {categories.map(([tag, count]) => (\n <button\n key={tag}\n type=\"button\"\n onClick={() => setCategory(tag)}\n className={cn(\n 'flex shrink-0 items-center justify-between gap-2 rounded-md px-3 py-1.5 text-left text-sm font-medium transition-colors lg:w-full',\n category === tag\n ? 'bg-action-primary-bg/10 text-primary'\n : 'text-text-secondary hover:text-text-primary'\n )}\n >\n <span className=\"truncate\">{tag}</span>\n <span className=\"text-xs text-text-muted tabular-nums\">{count}</span>\n </button>\n ))}\n </div>\n <div className=\"hidden space-y-1.5 border-t border-border-seam pt-3 lg:block\">\n <Label className=\"text-xs\">Status</Label>\n <Select value={statusFilter} onValueChange={setStatusFilter}>\n <SelectTrigger className=\"h-9\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All statuses</SelectItem>\n {ARTICLE_STATUSES.map((s) => (\n <SelectItem key={s} value={s}>\n {s}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Label className=\"text-xs\">Sort</Label>\n <Select value={sort} onValueChange={(v) => setSort(v as SortKey)}>\n <SelectTrigger className=\"h-9\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"recent\">Recently updated</SelectItem>\n <SelectItem value=\"views\">Most viewed</SelectItem>\n </SelectContent>\n </Select>\n </div>\n </aside>\n\n {/* Article grid */}\n <div>\n {loading && articles.length === 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {Array.from({ length: 6 }).map((_, i) => (\n <div\n key={i}\n className=\"h-40 rounded-xl bg-bg-sunken motion-safe:animate-vc-shimmer\"\n />\n ))}\n </div>\n ) : null}\n {!loading && filtered.length === 0 ? (\n <IllustratedEmptyState\n illustration={\n search || category !== 'ALL' || statusFilter !== 'ALL'\n ? 'empty-search'\n : 'empty-data'\n }\n title={\n search || category !== 'ALL' || statusFilter !== 'ALL'\n ? 'No articles match your filters'\n : 'No knowledge articles found'\n }\n description={\n search || category !== 'ALL' || statusFilter !== 'ALL'\n ? 'Try a different term, category or status.'\n : 'Publish an article to start building your knowledge base.'\n }\n />\n ) : null}\n <div className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {pagination.pageItems.map((article) => (\n <GlassCard key={article.id} treatment=\"glass\" className=\"border-border-seam\">\n <button\n type=\"button\"\n onClick={() => setReading(article)}\n className=\"flex h-full w-full flex-col gap-3 p-4 text-left\"\n >\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusPill status={article.status} />\n {article.tags.slice(0, 1).map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-action-primary-bg/10 px-2 py-0.5 text-xs font-medium text-primary\"\n >\n {tag}\n </span>\n ))}\n </div>\n <h2 className=\"line-clamp-2 text-base font-semibold text-text-primary\">\n {article.title}\n </h2>\n <p className=\"line-clamp-2 flex-1 text-sm text-text-secondary\">\n {excerptOf(article)}\n </p>\n <div className=\"flex items-center justify-between gap-2 border-t border-border-seam/60 pt-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-[10px] font-semibold text-text-secondary\">\n {article.updatedBy ? (\n initialsOf(article.updatedBy)\n ) : (\n <User className=\"h-3 w-3\" />\n )}\n </span>\n <span className=\"truncate text-xs text-text-secondary\">\n Updated {formatDate(article.updatedAt)}\n </span>\n </div>\n <span className=\"flex shrink-0 items-center gap-1 text-xs text-text-muted tabular-nums\">\n <Eye className=\"h-3.5 w-3.5\" />\n {article.views.toLocaleString()}\n </span>\n </div>\n </button>\n </GlassCard>\n ))}\n </div>\n <div className=\"mt-4\">\n <Pagination\n page={pagination.page}\n pageCount={pagination.pageCount}\n rangeStart={pagination.rangeStart}\n rangeEnd={pagination.rangeEnd}\n totalItems={pagination.totalItems}\n canPrev={pagination.canPrev}\n canNext={pagination.canNext}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel=\"articles\"\n />\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"KnowledgeBasePage.js","names":[],"sources":["../../../src/assethandler/pages/KnowledgeBasePage.tsx"],"sourcesContent":["import { BookOpen, Plus, Search, Eye, User, Link2, ExternalLink, Pencil, X } from 'lucide-react';\nimport {\n Button,\n IllustratedEmptyState,\n GlassCard,\n PagePurpose,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { useMemo, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Pagination,\n usePagination,\n SectionHeader,\n ChartCard,\n DonutChart,\n BarsChart,\n seriesColor,\n type DonutSlice,\n} from '../components';\nimport { useAssetHandlerNavigate } from '../hooks/useAssetHandlerNavigate';\nimport { useAssetHandlerStore } from '../store/assethandlerStore';\nimport { cn } from '../utils/cn';\nimport { notify } from '../utils/toast';\nimport { formatAssetOption, formatIncidentOption, parseTagsInput } from '../utils/knowledgeBase';\n\nconst KNOWLEDGE_ARTICLES_QUERY = gql`\n query AssetHandlerKnowledgeArticles {\n knowledgeArticles {\n id\n title\n slug\n summary\n content\n status\n tags\n views\n createdBy\n updatedBy\n publishedAt\n createdAt\n updatedAt\n relatedAssetId\n relatedIncidentId\n }\n }\n`;\n\nconst CREATE_KNOWLEDGE_ARTICLE_MUTATION = gql`\n mutation AssetHandlerCreateKnowledgeArticle($input: CreateKnowledgeArticleInput!) {\n createKnowledgeArticle(input: $input) {\n id\n title\n }\n }\n`;\n\nconst UPDATE_KNOWLEDGE_ARTICLE_MUTATION = gql`\n mutation AssetHandlerUpdateKnowledgeArticle($id: ID!, $input: UpdateKnowledgeArticleInput!) {\n updateKnowledgeArticle(id: $id, input: $input) {\n id\n title\n }\n }\n`;\n\ninterface KnowledgeArticle {\n id: string;\n title: string;\n slug: string;\n summary: string | null;\n content: string;\n status: string;\n tags: string[];\n views: number;\n createdBy: string;\n updatedBy: string;\n publishedAt: string | null;\n createdAt: string;\n updatedAt: string;\n relatedAssetId: string | null;\n relatedIncidentId: string | null;\n}\n\ninterface CreateKnowledgeArticleResult {\n createKnowledgeArticle: { id: string; title: string };\n}\n\ninterface UpdateKnowledgeArticleResult {\n updateKnowledgeArticle: { id: string; title: string };\n}\n\nconst ARTICLE_STATUSES = ['DRAFT', 'PUBLISHED', 'ARCHIVED'] as const;\n\nconst STATUS_META: Record<string, { label: string; className: string }> = {\n PUBLISHED: {\n label: 'Published',\n className: 'bg-status-success-bg-subtle text-status-success-text',\n },\n DRAFT: { label: 'Draft', className: 'bg-bg-sunken text-text-secondary' },\n ARCHIVED: {\n label: 'Archived',\n className: 'bg-status-warning-bg-subtle text-status-warning-text',\n },\n};\n\nconst slugify = (value: string): string =>\n value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n\nconst formatDate = (value: string | null): string =>\n value\n ? new Date(value).toLocaleDateString(undefined, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n })\n : '—';\n\n/** A 2-line excerpt: the summary if present, otherwise the opening of the body. */\nfunction excerptOf(article: KnowledgeArticle): string {\n if (article.summary && article.summary.trim()) return article.summary.trim();\n const body = article.content.replace(/\\s+/g, ' ').trim();\n return body.length > 160 ? `${body.slice(0, 160)}…` : body;\n}\n\n/** Short avatar initials from an actor id (best-effort, no fabricated names). */\nfunction initialsOf(actor: string): string {\n const clean = actor.replace(/[^a-zA-Z0-9]/g, '');\n return (clean.slice(0, 2) || '?').toUpperCase();\n}\n\nfunction StatusPill({ status }: { status: string }) {\n const meta = STATUS_META[status] ?? {\n label: status,\n className: 'bg-bg-sunken text-text-secondary',\n };\n return (\n <span\n className={cn(\n 'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',\n meta.className\n )}\n >\n {meta.label}\n </span>\n );\n}\n\nfunction ArticleDialog({\n open,\n onOpenChange,\n article,\n onSaved,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n article: KnowledgeArticle | null;\n onSaved: () => void;\n}) {\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-2xl\">\n {open && (\n <ArticleForm\n key={article?.id ?? 'new'}\n article={article}\n onOpenChange={onOpenChange}\n onSaved={onSaved}\n />\n )}\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction ArticleForm({\n article,\n onOpenChange,\n onSaved,\n}: {\n article: KnowledgeArticle | null;\n onOpenChange: (open: boolean) => void;\n onSaved: () => void;\n}) {\n const isEdit = Boolean(article);\n const { assets, incidents } = useAssetHandlerStore();\n\n const [title, setTitle] = useState(article?.title ?? '');\n const [slug, setSlug] = useState(article?.slug ?? '');\n const [slugTouched, setSlugTouched] = useState(isEdit);\n const [summary, setSummary] = useState(article?.summary ?? '');\n const [content, setContent] = useState(article?.content ?? '');\n const [tags, setTags] = useState(article ? article.tags.join(', ') : '');\n const [status, setStatus] = useState<string>(article?.status ?? 'PUBLISHED');\n const [relatedAssetId, setRelatedAssetId] = useState(article?.relatedAssetId ?? '');\n const [relatedIncidentId, setRelatedIncidentId] = useState(article?.relatedIncidentId ?? '');\n\n const reset = () => {\n setTitle('');\n setSlug('');\n setSlugTouched(false);\n setSummary('');\n setContent('');\n setTags('');\n setStatus('PUBLISHED');\n setRelatedAssetId('');\n setRelatedIncidentId('');\n };\n\n const [createArticle, { loading: creating }] = useMutation<CreateKnowledgeArticleResult>(\n CREATE_KNOWLEDGE_ARTICLE_MUTATION,\n {\n onCompleted: (data) => {\n notify.success(\n 'Article published',\n `${data.createKnowledgeArticle.title} is now available.`\n );\n reset();\n onOpenChange(false);\n onSaved();\n },\n onError: (error) => notify.error('Could not publish article', error.message),\n }\n );\n\n const [updateArticle, { loading: updating }] = useMutation<UpdateKnowledgeArticleResult>(\n UPDATE_KNOWLEDGE_ARTICLE_MUTATION,\n {\n refetchQueries: ['AssetHandlerKnowledgeArticles'],\n onCompleted: (data) => {\n notify.success('Article updated', `${data.updateKnowledgeArticle.title} has been saved.`);\n onOpenChange(false);\n onSaved();\n },\n onError: (error) => notify.error('Could not update article', error.message),\n }\n );\n\n const selectedAsset = useMemo(\n () => assets.find((a) => a.id === relatedAssetId),\n [assets, relatedAssetId]\n );\n const selectedIncident = useMemo(\n () => incidents.find((i) => i.id === relatedIncidentId),\n [incidents, relatedIncidentId]\n );\n\n const handleTitleChange = (next: string) => {\n setTitle(next);\n if (!slugTouched) setSlug(slugify(next));\n };\n\n const handleSubmit = () => {\n if (!title.trim() || !slug.trim()) {\n notify.warning('Missing details', 'Title and slug are required.');\n return;\n }\n if (!content.trim()) {\n notify.warning('Body required', 'Write the article body before publishing.');\n return;\n }\n\n const parsedTags = parseTagsInput(tags);\n const baseInput = {\n title: title.trim(),\n summary: summary.trim() || null,\n content: content.trim(),\n tags: parsedTags,\n status,\n relatedAssetId: relatedAssetId || null,\n relatedIncidentId: relatedIncidentId || null,\n };\n\n if (isEdit && article) {\n void updateArticle({\n variables: { id: article.id, input: baseInput },\n });\n } else {\n void createArticle({\n variables: {\n input: { ...baseInput, slug: slug.trim() },\n },\n });\n }\n };\n\n return (\n <>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{isEdit ? 'Edit article' : 'New Article'}</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {isEdit\n ? 'Update the runbook or fix so the team has the latest guidance.'\n : 'Capture a runbook or fix so your team can find it later.'}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-4\">\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label>Title *</Label>\n <Input value={title} onChange={(e) => handleTitleChange(e.target.value)} />\n </div>\n <div className=\"space-y-2\">\n <Label>Slug *</Label>\n <Input\n value={slug}\n disabled={isEdit}\n onChange={(e) => {\n setSlugTouched(true);\n setSlug(e.target.value);\n }}\n />\n </div>\n </div>\n <div className=\"space-y-2\">\n <Label>Summary</Label>\n <Input\n placeholder=\"One-line description\"\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Body *</Label>\n <Textarea\n placeholder=\"Write the full article body — steps, context, links…\"\n value={content}\n onChange={(e) => setContent(e.target.value)}\n rows={8}\n />\n </div>\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label>Category tags</Label>\n <Input\n placeholder=\"Runbooks, Fixes, Procedures…\"\n value={tags}\n onChange={(e) => setTags(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Status</Label>\n <Select value={status} onValueChange={setStatus}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ARTICLE_STATUSES.map((s) => (\n <SelectItem key={s} value={s}>\n {s}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label>Related asset</Label>\n <Select value={relatedAssetId} onValueChange={setRelatedAssetId}>\n <SelectTrigger>\n <SelectValue placeholder=\"Select an asset\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"\">None</SelectItem>\n {assets.map((asset) => (\n <SelectItem key={asset.id} value={asset.id}>\n {formatAssetOption(asset)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {selectedAsset ? (\n <div className=\"flex flex-wrap gap-2\">\n <span className=\"inline-flex items-center gap-1 rounded-full bg-action-primary-bg/10 px-2 py-0.5 text-xs font-medium text-primary\">\n {formatAssetOption(selectedAsset)}\n <button\n type=\"button\"\n onClick={() => setRelatedAssetId('')}\n className=\"rounded-full p-0.5 hover:bg-action-primary-bg/20\"\n aria-label=\"Clear related asset\"\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n </div>\n ) : null}\n </div>\n <div className=\"space-y-2\">\n <Label>Related incident</Label>\n <Select value={relatedIncidentId} onValueChange={setRelatedIncidentId}>\n <SelectTrigger>\n <SelectValue placeholder=\"Select an incident\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"\">None</SelectItem>\n {incidents.map((incident) => (\n <SelectItem key={incident.id} value={incident.id}>\n {formatIncidentOption(incident)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {selectedIncident ? (\n <div className=\"flex flex-wrap gap-2\">\n <span className=\"inline-flex items-center gap-1 rounded-full bg-action-primary-bg/10 px-2 py-0.5 text-xs font-medium text-primary\">\n {formatIncidentOption(selectedIncident)}\n <button\n type=\"button\"\n onClick={() => setRelatedIncidentId('')}\n className=\"rounded-full p-0.5 hover:bg-action-primary-bg/20\"\n aria-label=\"Clear related incident\"\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n </div>\n ) : null}\n </div>\n </div>\n </div>\n <ResponsiveDialogFooter>\n <Button\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={creating || updating}\n >\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={creating || updating}>\n {isEdit\n ? updating\n ? 'Saving…'\n : 'Save changes'\n : creating\n ? 'Publishing…'\n : 'Publish article'}\n </Button>\n </ResponsiveDialogFooter>\n </>\n );\n}\n\nfunction ArticleReader({\n article,\n onOpenChange,\n onEdit,\n}: {\n article: KnowledgeArticle | null;\n onOpenChange: (open: boolean) => void;\n onEdit: (article: KnowledgeArticle) => void;\n}) {\n const navigate = useAssetHandlerNavigate();\n return (\n <ResponsiveDialog open={Boolean(article)} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-2xl\">\n {article ? (\n <>\n <ResponsiveDialogHeader>\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusPill status={article.status} />\n {article.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-action-primary-bg/10 px-2 py-0.5 text-xs font-medium text-primary\"\n >\n {tag}\n </span>\n ))}\n </div>\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"min-w-0 flex-1\">\n <ResponsiveDialogTitle className=\"mt-1\">{article.title}</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Updated {formatDate(article.updatedAt)} · {article.views.toLocaleString()} views\n </ResponsiveDialogDescription>\n </div>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n onOpenChange(false);\n onEdit(article);\n }}\n >\n <Pencil className=\"mr-1.5 h-3.5 w-3.5\" />\n Edit\n </Button>\n </div>\n </ResponsiveDialogHeader>\n <div className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-primary\">\n {article.content}\n </div>\n {article.relatedAssetId || article.relatedIncidentId ? (\n <div className=\"mt-4 space-y-2 border-t border-border-seam pt-4\">\n <h4 className=\"flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n <Link2 className=\"h-3.5 w-3.5\" />\n Linked records\n </h4>\n <div className=\"flex flex-wrap gap-2\">\n {article.relatedAssetId ? (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n onOpenChange(false);\n navigate(`/assets/${article.relatedAssetId}`);\n }}\n >\n View linked asset\n <ExternalLink className=\"ml-1.5 h-3.5 w-3.5\" />\n </Button>\n ) : null}\n {article.relatedIncidentId ? (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n onOpenChange(false);\n navigate(`/itil/incidents/${article.relatedIncidentId}`);\n }}\n >\n View linked incident\n <ExternalLink className=\"ml-1.5 h-3.5 w-3.5\" />\n </Button>\n ) : null}\n </div>\n </div>\n ) : null}\n </>\n ) : null}\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\ntype SortKey = 'recent' | 'views';\n\nexport function KnowledgeBasePage() {\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [dialogArticle, setDialogArticle] = useState<KnowledgeArticle | null>(null);\n const [search, setSearch] = useState('');\n const [category, setCategory] = useState<string>('ALL');\n const [statusFilter, setStatusFilter] = useState<string>('ALL');\n const [sort, setSort] = useState<SortKey>('recent');\n const [reading, setReading] = useState<KnowledgeArticle | null>(null);\n\n const { data, loading, refetch } = useQuery<{ knowledgeArticles: KnowledgeArticle[] }>(\n KNOWLEDGE_ARTICLES_QUERY,\n { fetchPolicy: 'cache-and-network' }\n );\n\n const articles = useMemo(() => data?.knowledgeArticles ?? [], [data?.knowledgeArticles]);\n\n const categories = useMemo(() => {\n const counts = new Map<string, number>();\n for (const a of articles) {\n for (const tag of a.tags) counts.set(tag, (counts.get(tag) ?? 0) + 1);\n }\n return [...counts.entries()].sort((a, b) => b[1] - a[1]);\n }, [articles]);\n\n const filtered = useMemo(() => {\n const term = search.trim().toLowerCase();\n const list = articles.filter((article) => {\n if (category !== 'ALL' && !article.tags.includes(category)) return false;\n if (statusFilter !== 'ALL' && article.status !== statusFilter) return false;\n if (!term) return true;\n const haystack = [\n article.title,\n article.summary ?? '',\n article.content,\n article.tags.join(' '),\n ]\n .join(' ')\n .toLowerCase();\n return haystack.includes(term);\n });\n return [...list].sort((a, b) =>\n sort === 'views'\n ? b.views - a.views\n : new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()\n );\n }, [articles, search, category, statusFilter, sort]);\n\n const statusMix = useMemo<DonutSlice[]>(() => {\n const counts = articles.reduce<Record<string, number>>((acc, a) => {\n acc[a.status] = (acc[a.status] ?? 0) + 1;\n return acc;\n }, {});\n return Object.entries(counts).map(([status, value], i) => ({\n name: STATUS_META[status]?.label ?? status,\n value,\n color: seriesColor(i),\n }));\n }, [articles]);\n\n const topByViews = useMemo(\n () =>\n [...articles]\n .filter((a) => a.views > 0)\n .sort((a, b) => b.views - a.views)\n .slice(0, 6)\n .map((a) => ({ name: a.title, views: a.views })),\n [articles]\n );\n\n const pagination = usePagination(filtered, {\n pageSize: 9,\n resetKey: `${search}|${category}|${statusFilter}|${sort}`,\n });\n\n const handleNewArticle = () => {\n setDialogArticle(null);\n setIsDialogOpen(true);\n };\n\n const handleEditArticle = (article: KnowledgeArticle) => {\n setReading(null);\n setDialogArticle(article);\n setIsDialogOpen(true);\n };\n\n return (\n <div className=\"space-y-5 p-4 sm:p-6\">\n <SectionHeader\n hero\n icon={BookOpen}\n title=\"Knowledge Base\"\n description=\"Turn one-off fixes and runbooks into repeatable, searchable operational knowledge.\"\n actions={\n <Button className=\"w-full sm:w-auto\" onClick={handleNewArticle}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New Article\n </Button>\n }\n />\n <PagePurpose>\n Capture the fixes and runbooks your team relies on so the next person doesn't start\n from scratch. Link articles to incidents and assets, then search them when troubleshooting.\n </PagePurpose>\n\n <ArticleDialog\n open={isDialogOpen}\n onOpenChange={setIsDialogOpen}\n article={dialogArticle}\n onSaved={() => void refetch()}\n />\n <ArticleReader\n article={reading}\n onOpenChange={(open) => !open && setReading(null)}\n onEdit={handleEditArticle}\n />\n\n <div className=\"relative\">\n <Search className=\"absolute left-3.5 top-1/2 h-4.5 w-4.5 -translate-y-1/2 text-text-secondary\" />\n <Input\n className=\"h-12 pl-11 text-base\"\n placeholder=\"Search runbooks, fixes and procedures…\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n aria-label=\"Search knowledge articles\"\n />\n </div>\n\n {articles.length > 0 && (statusMix.length > 0 || topByViews.length > 0) ? (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <ChartCard title=\"Publishing status\" subtitle=\"Articles by status\">\n <DonutChart\n data={statusMix}\n height={200}\n centerLabel=\"Articles\"\n centerValue={articles.length}\n ariaLabel=\"Articles by status\"\n />\n </ChartCard>\n <ChartCard title=\"Most consulted\" subtitle=\"Top articles by views\">\n <BarsChart\n data={topByViews}\n xKey=\"name\"\n series={[{ key: 'views', label: 'Views' }]}\n orientation=\"horizontal\"\n height={200}\n categoryWidth={150}\n emptyLabel=\"No views recorded yet\"\n ariaLabel=\"Top articles by views\"\n />\n </ChartCard>\n </div>\n ) : null}\n\n <div className=\"grid gap-5 lg:grid-cols-[200px_1fr]\">\n {/* Category facet rail */}\n <aside className=\"space-y-3\">\n <div className=\"flex items-center justify-between gap-2\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Category</h3>\n <Select value={sort} onValueChange={(v) => setSort(v as SortKey)}>\n <SelectTrigger className=\"h-8 w-[130px] text-xs lg:hidden\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"recent\">Recently updated</SelectItem>\n <SelectItem value=\"views\">Most viewed</SelectItem>\n </SelectContent>\n </Select>\n </div>\n {/* Mobile: horizontal chips; desktop: vertical list */}\n <div className=\"flex gap-2 overflow-x-auto pb-1 lg:flex-col lg:overflow-visible lg:pb-0\">\n <button\n type=\"button\"\n onClick={() => setCategory('ALL')}\n className={cn(\n 'shrink-0 rounded-md px-3 py-1.5 text-left text-sm font-medium transition-colors lg:w-full',\n category === 'ALL'\n ? 'bg-action-primary-bg/10 text-primary'\n : 'text-text-secondary hover:text-text-primary'\n )}\n >\n All articles\n <span className=\"ml-1.5 text-xs text-text-muted tabular-nums\">{articles.length}</span>\n </button>\n {categories.map(([tag, count]) => (\n <button\n key={tag}\n type=\"button\"\n onClick={() => setCategory(tag)}\n className={cn(\n 'flex shrink-0 items-center justify-between gap-2 rounded-md px-3 py-1.5 text-left text-sm font-medium transition-colors lg:w-full',\n category === tag\n ? 'bg-action-primary-bg/10 text-primary'\n : 'text-text-secondary hover:text-text-primary'\n )}\n >\n <span className=\"truncate\">{tag}</span>\n <span className=\"text-xs text-text-muted tabular-nums\">{count}</span>\n </button>\n ))}\n </div>\n <div className=\"hidden space-y-1.5 border-t border-border-seam pt-3 lg:block\">\n <Label className=\"text-xs\">Status</Label>\n <Select value={statusFilter} onValueChange={setStatusFilter}>\n <SelectTrigger className=\"h-9\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All statuses</SelectItem>\n {ARTICLE_STATUSES.map((s) => (\n <SelectItem key={s} value={s}>\n {s}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Label className=\"text-xs\">Sort</Label>\n <Select value={sort} onValueChange={(v) => setSort(v as SortKey)}>\n <SelectTrigger className=\"h-9\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"recent\">Recently updated</SelectItem>\n <SelectItem value=\"views\">Most viewed</SelectItem>\n </SelectContent>\n </Select>\n </div>\n </aside>\n\n {/* Article grid */}\n <div>\n {loading && articles.length === 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {Array.from({ length: 6 }).map((_, i) => (\n <div\n key={i}\n className=\"h-40 rounded-xl bg-bg-sunken motion-safe:animate-vc-shimmer\"\n />\n ))}\n </div>\n ) : null}\n {!loading && filtered.length === 0 ? (\n <IllustratedEmptyState\n illustration={\n search || category !== 'ALL' || statusFilter !== 'ALL'\n ? 'empty-search'\n : 'empty-data'\n }\n title={\n search || category !== 'ALL' || statusFilter !== 'ALL'\n ? 'No articles match your filters'\n : 'No knowledge articles found'\n }\n description={\n search || category !== 'ALL' || statusFilter !== 'ALL'\n ? 'Try a different term, category or status.'\n : 'Publish an article to start building your knowledge base.'\n }\n />\n ) : null}\n <div className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {pagination.pageItems.map((article) => (\n <GlassCard key={article.id} treatment=\"glass\" className=\"border-border-seam\">\n <button\n type=\"button\"\n onClick={() => setReading(article)}\n className=\"flex h-full w-full flex-col gap-3 p-4 text-left\"\n >\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusPill status={article.status} />\n {article.tags.slice(0, 1).map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-action-primary-bg/10 px-2 py-0.5 text-xs font-medium text-primary\"\n >\n {tag}\n </span>\n ))}\n </div>\n <h2 className=\"line-clamp-2 text-base font-semibold text-text-primary\">\n {article.title}\n </h2>\n <p className=\"line-clamp-2 flex-1 text-sm text-text-secondary\">\n {excerptOf(article)}\n </p>\n <div className=\"flex items-center justify-between gap-2 border-t border-border-seam/60 pt-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-[10px] font-semibold text-text-secondary\">\n {article.updatedBy ? (\n initialsOf(article.updatedBy)\n ) : (\n <User className=\"h-3 w-3\" />\n )}\n </span>\n <span className=\"truncate text-xs text-text-secondary\">\n Updated {formatDate(article.updatedAt)}\n </span>\n </div>\n <span className=\"flex shrink-0 items-center gap-1 text-xs text-text-muted tabular-nums\">\n <Eye className=\"h-3.5 w-3.5\" />\n {article.views.toLocaleString()}\n </span>\n </div>\n </button>\n </GlassCard>\n ))}\n </div>\n <div className=\"mt-4\">\n <Pagination\n page={pagination.page}\n pageCount={pagination.pageCount}\n rangeStart={pagination.rangeStart}\n rangeEnd={pagination.rangeEnd}\n totalItems={pagination.totalItems}\n canPrev={pagination.canPrev}\n canNext={pagination.canNext}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel=\"articles\"\n />\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Asset, Incident } from '../store/assethandlerStore';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse a comma-separated tag string into a clean list of non-empty tags.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseTagsInput(value: string): string[];
|
|
7
|
+
/**
|
|
8
|
+
* Format an asset as a picker option label.
|
|
9
|
+
* Falls back to the asset tag or id so the option is always identifiable.
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatAssetOption(asset: Asset): string;
|
|
12
|
+
/**
|
|
13
|
+
* Format an incident as a picker option label.
|
|
14
|
+
* Falls back to the incident id so the option is always identifiable.
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatIncidentOption(incident: Incident): string;
|
|
17
|
+
//# sourceMappingURL=knowledgeBase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knowledgeBase.d.ts","sourceRoot":"","sources":["../../../src/assethandler/utils/knowledgeBase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAElE;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAKtD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAKtD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAI/D"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/assethandler/utils/knowledgeBase.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
return e.split(",").map((e) => e.trim()).filter((e) => e.length > 0);
|
|
4
|
+
}
|
|
5
|
+
function t(e) {
|
|
6
|
+
let t = e.name?.trim();
|
|
7
|
+
if (t) return t;
|
|
8
|
+
let n = e.assetTag?.trim();
|
|
9
|
+
return n ? `Tag: ${n}` : e.id;
|
|
10
|
+
}
|
|
11
|
+
function n(e) {
|
|
12
|
+
return e.title?.trim() || e.id;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { t as formatAssetOption, n as formatIncidentOption, e as parseTagsInput };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=knowledgeBase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knowledgeBase.js","names":[],"sources":["../../../src/assethandler/utils/knowledgeBase.ts"],"sourcesContent":["import type { Asset, Incident } from '../store/assethandlerStore';\n\n/**\n * Parse a comma-separated tag string into a clean list of non-empty tags.\n */\nexport function parseTagsInput(value: string): string[] {\n return value\n .split(',')\n .map((t) => t.trim())\n .filter((t) => t.length > 0);\n}\n\n/**\n * Format an asset as a picker option label.\n * Falls back to the asset tag or id so the option is always identifiable.\n */\nexport function formatAssetOption(asset: Asset): string {\n const primary = asset.name?.trim();\n if (primary) return primary;\n const fallback = asset.assetTag?.trim();\n return fallback ? `Tag: ${fallback}` : asset.id;\n}\n\n/**\n * Format an incident as a picker option label.\n * Falls back to the incident id so the option is always identifiable.\n */\nexport function formatIncidentOption(incident: Incident): string {\n const primary = incident.title?.trim();\n if (primary) return primary;\n return incident.id;\n}\n"],"mappings":";AAKA,SAAgB,EAAe,GAAyB;AACtD,QAAO,EACJ,MAAM,IAAI,CACV,KAAK,MAAM,EAAE,MAAM,CAAC,CACpB,QAAQ,MAAM,EAAE,SAAS,EAAE;;AAOhC,SAAgB,EAAkB,GAAsB;CACtD,IAAM,IAAU,EAAM,MAAM,MAAM;AAClC,KAAI,EAAS,QAAO;CACpB,IAAM,IAAW,EAAM,UAAU,MAAM;AACvC,QAAO,IAAW,QAAQ,MAAa,EAAM;;AAO/C,SAAgB,EAAqB,GAA4B;AAG/D,QAFgB,EAAS,OAAO,MAAM,IAE/B,EAAS"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/test/setup.ts"],"names":[],"mappings":"AAAA,OAAO,2BAA2B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-assethandler",
|
|
3
|
-
"version": "2026.820.
|
|
3
|
+
"version": "2026.820.3",
|
|
4
4
|
"description": "Burdenoff AssetHandler Microfrontend",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"lint:sanity": "if [ \"${CI:-}\" = \"true\" ]; then bun run lint; else bun run lint -- --cache --cache-location .eslintcache; fi",
|
|
34
34
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
35
35
|
"type:check": "bash scripts/build-limits.sh tsc --noEmit",
|
|
36
|
-
"test": "
|
|
36
|
+
"test": "bash scripts/build-limits.sh vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"test:coverage": "bash scripts/build-limits.sh vitest run --coverage",
|
|
37
39
|
"sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run lint:sanity && bun run format && bun run type:check && bun run test && bun run build'",
|
|
38
40
|
"codegen:all": "bun run schema:pull && bun run codegen",
|
|
39
41
|
"switch:status": "bun scripts/deps-mode.ts status",
|
|
@@ -81,7 +83,12 @@
|
|
|
81
83
|
"@storybook/test-runner": "0.24.4",
|
|
82
84
|
"@tailwindcss/vite": "^4.3.2",
|
|
83
85
|
"@types/bun": "^1.3.9",
|
|
86
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
87
|
+
"@testing-library/react": "^16.1.0",
|
|
88
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
84
89
|
"@vitejs/plugin-react": "^4.3.4",
|
|
90
|
+
"happy-dom": "^17.0.0",
|
|
91
|
+
"vitest": "^3.2.4",
|
|
85
92
|
"autoprefixer": "^10.4.20",
|
|
86
93
|
"eslint": "^9.18.0",
|
|
87
94
|
"eslint-plugin-react": "^7.37.5",
|