@simmalugnt-se/payload-content-health 0.5.2 → 0.6.0
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/CHANGELOG.md +13 -0
- package/README.md +17 -9
- package/dist/categories.d.ts +21 -0
- package/dist/categories.js +43 -0
- package/dist/copy.d.ts +12 -17
- package/dist/copy.js +19 -65
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/links.d.ts +6 -0
- package/dist/links.js +8 -2
- package/dist/styles/tokens.css +26 -0
- package/dist/styles/view.css +157 -74
- package/dist/styles/widget.css +71 -85
- package/dist/view/ContentHealthView.d.ts +4 -3
- package/dist/view/ContentHealthView.js +32 -9
- package/dist/view/WorkList.d.ts +12 -5
- package/dist/view/WorkList.js +47 -11
- package/dist/widget/ContentHealthWidget.d.ts +3 -2
- package/dist/widget/ContentHealthWidget.js +16 -29
- package/package.json +1 -1
package/dist/view/WorkList.js
CHANGED
|
@@ -2,28 +2,64 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Button, Link, ReactSelect } from "@payloadcms/ui";
|
|
4
4
|
import { useId, useMemo, useState } from "react";
|
|
5
|
+
import { categoryOf } from "../categories.js";
|
|
5
6
|
import { LIST_COPY } from "../copy.js";
|
|
6
7
|
import { BATCH_CHECKS, batchURL, filterQuery, MAX_BATCH_TARGETS, matchesFilters, } from "../links.js";
|
|
8
|
+
import { CHECK_IDS } from "../types.js";
|
|
7
9
|
/**
|
|
8
|
-
* The findings
|
|
9
|
-
* kept in the address, so the view can be linked to
|
|
10
|
+
* The findings, one category at a time and one section per check. The category and the collection
|
|
11
|
+
* and locale filters apply in the browser and are kept in the address, so the view can be linked to
|
|
12
|
+
* and reloaded with them.
|
|
10
13
|
*/
|
|
11
|
-
export function WorkList({ adminRoute, batchAsk,
|
|
14
|
+
export function WorkList({ adminRoute, batchAsk, categories, collections, initialFilters, language, locales, rows, }) {
|
|
12
15
|
const copy = LIST_COPY[language];
|
|
16
|
+
const panelId = useId();
|
|
13
17
|
const [filters, setFilters] = useState(initialFilters);
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const category = filters.category ?? categories[0]?.id;
|
|
19
|
+
// Tab counts follow the collection and locale filters, so they match what the tab then shows.
|
|
20
|
+
const narrowed = useMemo(() => rows.filter((row) => matchesFilters(row, { collection: filters.collection, locale: filters.locale })), [rows, filters.collection, filters.locale]);
|
|
21
|
+
const counts = useMemo(() => {
|
|
22
|
+
const result = new Map();
|
|
23
|
+
for (const row of narrowed) {
|
|
24
|
+
const id = categoryOf(row.check);
|
|
25
|
+
result.set(id, (result.get(id) ?? 0) + 1);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}, [narrowed]);
|
|
29
|
+
const shown = narrowed.filter((row) => categoryOf(row.check) === category);
|
|
30
|
+
const sections = CHECK_IDS.flatMap((check) => {
|
|
31
|
+
const own = shown.filter((row) => row.check === check);
|
|
32
|
+
return own.length ? [{ check, rows: own }] : [];
|
|
33
|
+
});
|
|
34
|
+
const everywhere = rows.some((row) => categoryOf(row.check) === category);
|
|
21
35
|
const update = (next) => {
|
|
22
36
|
setFilters(next);
|
|
23
37
|
const query = filterQuery(next);
|
|
24
38
|
window.history.replaceState(window.history.state, "", `${window.location.pathname}${query ? `?${query}` : ""}`);
|
|
25
39
|
};
|
|
26
|
-
|
|
40
|
+
const moveTab = (event, index) => {
|
|
41
|
+
const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
|
|
42
|
+
const target = categories[(index + step + categories.length) % categories.length];
|
|
43
|
+
if (!step || !target)
|
|
44
|
+
return;
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
update({ ...filters, category: target.id });
|
|
47
|
+
document.getElementById(`${panelId}-tab-${target.id}`)?.focus();
|
|
48
|
+
};
|
|
49
|
+
return (_jsxs("div", { className: "content-health-list__body", children: [_jsx("div", { "aria-label": copy.categories, className: "content-health-list__tabs", role: "tablist", children: categories.map((entry, index) => {
|
|
50
|
+
const selected = entry.id === category;
|
|
51
|
+
return (_jsxs("button", { "aria-controls": `${panelId}-panel`, "aria-selected": selected, className: `content-health-list__tab${selected ? " content-health-list__tab--selected" : ""}`, id: `${panelId}-tab-${entry.id}`, onClick: () => update({ ...filters, category: entry.id }), onKeyDown: (event) => moveTab(event, index), role: "tab", tabIndex: selected ? 0 : -1, type: "button", children: [_jsxs("span", { className: "content-health-list__tab-name", children: [_jsx("span", { "aria-hidden": "true", className: `content-health-list__dot content-health-list__dot--${entry.tone}` }), entry.label] }), _jsx("span", { className: "content-health-list__tab-count", children: counts.get(entry.id) ?? 0 })] }, entry.id));
|
|
52
|
+
}) }), collections.length > 1 || locales.length > 1 ? (_jsxs("div", { className: "content-health-list__filters", children: [collections.length > 1 ? (_jsx(Filter, { label: copy.collection, all: copy.all, onChange: (collection) => update({ ...filters, category, collection }), options: collections, value: filters.collection })) : null, locales.length > 1 ? (_jsx(Filter, { label: copy.locale, all: copy.all, onChange: (locale) => update({ ...filters, category, locale }), options: locales, value: filters.locale })) : null] })) : null, _jsx("div", { "aria-labelledby": category ? `${panelId}-tab-${category}` : undefined, className: "content-health-list__panel", id: `${panelId}-panel`, role: "tabpanel", children: sections.length ? (sections.map((section) => (_jsx(Section, { adminRoute: adminRoute, ask: batchAsk[section.check], copy: copy, filters: { ...filters, category }, rows: section.rows }, section.check)))) : (_jsx("p", { className: "content-health-list__empty", children: everywhere ? copy.noneFiltered : copy.categoryOk })) })] }));
|
|
53
|
+
}
|
|
54
|
+
function Section({ adminRoute, ask, copy, filters, rows, }) {
|
|
55
|
+
const check = rows[0]?.check;
|
|
56
|
+
const documents = new Set(rows.map((row) => row.href.split("?")[0])).size;
|
|
57
|
+
const targets = ask && BATCH_CHECKS[check] ? rows.flatMap((row) => row.batchTarget ?? []) : [];
|
|
58
|
+
// The check says how far each fix may reach, so the batch link carries it.
|
|
59
|
+
const batchHref = ask && targets.length > 1
|
|
60
|
+
? batchURL(adminRoute, { ...filters, check }, targets, ask)
|
|
61
|
+
: undefined;
|
|
62
|
+
return (_jsxs("section", { className: "content-health-list__section", children: [_jsxs("header", { className: "content-health-list__section-head", children: [_jsx("h2", { children: rows[0]?.checkLabel }), _jsx("span", { className: "content-health-list__pill", children: copy.documents(documents) }), batchHref ? (_jsx(Button, { buttonStyle: "secondary", className: "content-health-list__batch", el: "link", margin: false, size: "small", url: batchHref, children: copy.fixMany(Math.min(targets.length, MAX_BATCH_TARGETS), targets.length) })) : null] }), _jsx("ul", { className: "content-health-list__items", children: rows.map((row) => (_jsxs("li", { className: "content-health-list__item", children: [_jsx(Link, { href: row.href, prefetch: false, children: row.title }), row.locale ? _jsx("span", { className: "content-health-list__pill", children: row.locale }) : null, row.draft ? (_jsx("span", { className: "content-health-list__pill content-health-list__pill--draft", children: copy.draft })) : null, row.detail ? _jsx("span", { className: "content-health-list__detail", children: row.detail }) : null, row.askHref ? (_jsx(Link, { className: "content-health-list__fix", href: row.askHref, prefetch: false, children: copy.fix })) : null] }, row.key))) })] }));
|
|
27
63
|
}
|
|
28
64
|
function Filter({ all, label, onChange, options, value, }) {
|
|
29
65
|
const inputId = useId();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { WidgetServerProps } from "payload";
|
|
2
|
+
import "../styles/tokens.css";
|
|
2
3
|
import "../styles/widget.css";
|
|
3
4
|
/**
|
|
4
|
-
* Dashboard widget: the share of checked fields in order, overall and per
|
|
5
|
-
*
|
|
5
|
+
* Dashboard widget: the share of checked fields in order, overall and per category. Each category
|
|
6
|
+
* links to its part of the work list, where the documents are.
|
|
6
7
|
*/
|
|
7
8
|
export declare function ContentHealthWidget({ req, widgetData }: WidgetServerProps): Promise<import("react").JSX.Element>;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { Button } from "@payloadcms/ui";
|
|
3
|
+
import { categoryStats, categoryTone } from "../categories.js";
|
|
4
|
+
import { CATEGORY_LABELS, COPY, uiLanguage } from "../copy.js";
|
|
3
5
|
import { checkStats, countDocuments } from "../group.js";
|
|
4
|
-
import {
|
|
6
|
+
import { listURL } from "../links.js";
|
|
5
7
|
import { scanContentHealth } from "../scan.js";
|
|
6
8
|
import { CHECK_IDS } from "../types.js";
|
|
7
9
|
import { Ring } from "./Ring.js";
|
|
10
|
+
import "../styles/tokens.css";
|
|
8
11
|
import "../styles/widget.css";
|
|
9
|
-
const ROWS_PER_CHECK = 4;
|
|
10
12
|
/**
|
|
11
|
-
* Dashboard widget: the share of checked fields in order, overall and per
|
|
12
|
-
*
|
|
13
|
+
* Dashboard widget: the share of checked fields in order, overall and per category. Each category
|
|
14
|
+
* links to its part of the work list, where the documents are.
|
|
13
15
|
*/
|
|
14
16
|
export async function ContentHealthWidget({ req, widgetData }) {
|
|
15
17
|
const language = uiLanguage(req.i18n?.language);
|
|
@@ -23,34 +25,19 @@ export async function ContentHealthWidget({ req, widgetData }) {
|
|
|
23
25
|
checks: selected,
|
|
24
26
|
});
|
|
25
27
|
const stats = checkStats(result.checks, result.checked, result.findings);
|
|
28
|
+
const categories = categoryStats(stats);
|
|
26
29
|
const total = stats.reduce((sum, stat) => sum + stat.checked, 0);
|
|
27
30
|
const failing = stats.reduce((sum, stat) => sum + stat.failing, 0);
|
|
28
|
-
const passing = stats.filter((stat) => stat.checked > 0 && stat.failing === 0).length;
|
|
29
31
|
const documents = Object.values(result.documents).reduce((sum, count) => sum + count, 0);
|
|
30
32
|
const adminRoute = req.payload.config.routes.admin;
|
|
31
|
-
return (_jsxs("section", { className: "content-health", children: [_jsx("h2", { className: "content-health__heading", children: copy.title }), _jsxs("div", { className: "content-health__panel", children: [_jsxs("div", { className: "content-health__overview", children: [_jsx(Ring, { className: "content-health__ring
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
return (_jsxs("section", { className: "content-health", children: [_jsx("h2", { className: "content-health__heading", children: copy.title }), _jsxs("div", { className: "content-health__panel", children: [_jsxs("div", { className: "content-health__overview", children: [_jsx(Ring, { className: "content-health__ring", failing: failing, label: copy.ok(total - failing, total), total: total }), _jsxs("div", { className: "content-health__overview-text", children: [_jsx("p", { className: "content-health__overall", children: copy.overall }), _jsx("p", { className: "content-health__summary", children: result.findings.length
|
|
34
|
+
? copy.summary(countDocuments(result.findings), documents)
|
|
35
|
+
: copy.allGood })] }), _jsx(Button, { buttonStyle: "secondary", el: "link", margin: false, size: "small", url: listURL(adminRoute), children: copy.openDetails })] }), _jsx("ul", { className: "content-health__categories", children: categories.map((stat) => (_jsx(CategoryRow, { adminRoute: adminRoute, language: language, stat: stat }, stat.category))) }), result.problems.length ? (_jsxs("div", { className: "content-health__problems", children: [_jsx("p", { children: copy.problems }), _jsx("ul", { children: result.problems.map((problem) => (_jsx("li", { children: problem }, problem))) })] })) : null] })] }));
|
|
34
36
|
}
|
|
35
|
-
function
|
|
37
|
+
function CategoryRow({ adminRoute, language, stat, }) {
|
|
36
38
|
const copy = COPY[language];
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
: copy.checkPasses;
|
|
42
|
-
return (_jsxs("li", { className: "content-health__tile", children: [_jsxs("div", { className: "content-health__tile-head", children: [_jsx(Ring, { className: "content-health__ring", failing: stat.failing, label: status, total: stat.checked }), _jsxs("div", { children: [_jsx("h3", { className: "content-health__tile-title", children: TILE_LABELS[language][stat.check] }), _jsx("p", { className: "content-health__tile-status", children: status })] })] }), stat.documents.length ? (_jsxs("ul", { className: "content-health__rows", children: [stat.documents.slice(0, ROWS_PER_CHECK).map((row) => (_jsx(FindingRow, { adminRoute: adminRoute, language: language, row: row }, `${row.collection}:${row.id}`))), stat.documents.length > ROWS_PER_CHECK ? (_jsx("li", { className: "content-health__more", children: _jsx("a", { href: listURL(adminRoute, { check: stat.check }), children: copy.more(stat.documents.length - ROWS_PER_CHECK) }) })) : null] })) : null] }));
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* A document on its own line, then its locales: one per line when they carry details (lengths, word
|
|
46
|
-
* counts), otherwise side by side.
|
|
47
|
-
*/
|
|
48
|
-
function FindingRow({ adminRoute, language, row, }) {
|
|
49
|
-
const locales = row.findings.map((finding) => ({
|
|
50
|
-
finding,
|
|
51
|
-
detail: findingDetail(language, finding),
|
|
52
|
-
}));
|
|
53
|
-
const stacked = locales.some(({ detail }) => detail);
|
|
54
|
-
const shown = locales.filter(({ finding, detail }) => finding.locale || detail);
|
|
55
|
-
return (_jsxs("li", { className: "content-health__row", children: [_jsx("a", { href: editURL(adminRoute, row.findings[0]), children: row.title }), shown.length ? (_jsx("ul", { className: `content-health__locales${stacked ? " content-health__locales--stacked" : ""}`, children: shown.map(({ finding, detail }) => (_jsxs("li", { className: "content-health__locale", children: [finding.locale ? (_jsx("a", { className: "content-health__pill", href: editURL(adminRoute, finding), children: finding.locale })) : null, detail ? _jsx("span", { className: "content-health__detail", children: detail }) : null] }, finding.locale ?? ""))) })) : null] }));
|
|
39
|
+
const tone = categoryTone(stat);
|
|
40
|
+
const share = stat.checked ? (stat.checked - stat.failing) / stat.checked : 0;
|
|
41
|
+
const status = tone === "none" ? copy.nothingYet : stat.failing ? copy.toFix(stat.failing) : copy.checkPasses;
|
|
42
|
+
return (_jsx("li", { children: _jsxs("a", { className: "content-health__category", href: listURL(adminRoute, { category: stat.category }), children: [_jsx("span", { className: "content-health__category-name", children: CATEGORY_LABELS[language][stat.category] }), _jsx("span", { "aria-hidden": "true", className: "content-health__bar", children: _jsx("span", { className: `content-health__bar-fill content-health__bar-fill--${tone}`, style: { width: `${Math.round(share * 100)}%` } }) }), _jsx("span", { className: `content-health__category-status content-health__tone--${tone}`, children: status })] }) }));
|
|
56
43
|
}
|
package/package.json
CHANGED