@vertebr/react 0.18.0 → 0.19.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/dist/components/admin/AdminContent.d.ts +5 -1
- package/dist/components/admin/AdminContent.d.ts.map +1 -1
- package/dist/components/admin/AdminContent.js +75 -50
- package/dist/components/admin/AuditBoard.d.ts +6 -2
- package/dist/components/admin/AuditBoard.d.ts.map +1 -1
- package/dist/components/admin/AuditBoard.js +145 -3
- package/dist/components/admin/VertebrAdmin.d.ts.map +1 -1
- package/dist/components/admin/VertebrAdmin.js +14 -3
- package/dist/components/config/EnumInput.d.ts.map +1 -1
- package/dist/components/config/EnumInput.js +9 -2
- package/dist/components/config/form/ConfigForm.d.ts.map +1 -1
- package/dist/components/config/form/ConfigForm.js +14 -2
- package/dist/components/emails/EmailsActivityPanel.d.ts.map +1 -1
- package/dist/components/emails/EmailsActivityPanel.js +80 -2
- package/dist/components/metrics/MetricsBoard.d.ts.map +1 -1
- package/dist/components/metrics/MetricsBoard.js +7 -1
- package/dist/components/resources/ColumnVisibilityMenu.d.ts +4 -0
- package/dist/components/resources/ColumnVisibilityMenu.d.ts.map +1 -1
- package/dist/components/resources/ColumnVisibilityMenu.js +14 -7
- package/dist/components/resources/ResourceTable.d.ts +7 -1
- package/dist/components/resources/ResourceTable.d.ts.map +1 -1
- package/dist/components/resources/ResourceTable.js +84 -64
- package/dist/components/resources/SortableHeader.d.ts +9 -0
- package/dist/components/resources/SortableHeader.d.ts.map +1 -0
- package/dist/components/resources/SortableHeader.js +9 -0
- package/dist/components/table/TableFilters.d.ts +20 -0
- package/dist/components/table/TableFilters.d.ts.map +1 -0
- package/dist/components/table/TableFilters.js +55 -0
- package/dist/components/table/TableSearch.d.ts +11 -0
- package/dist/components/table/TableSearch.d.ts.map +1 -0
- package/dist/components/table/TableSearch.js +23 -0
- package/dist/components/table/TableSkeleton.d.ts +16 -0
- package/dist/components/table/TableSkeleton.d.ts.map +1 -0
- package/dist/components/table/TableSkeleton.js +29 -0
- package/dist/components/table/TableWrap.d.ts +23 -0
- package/dist/components/table/TableWrap.d.ts.map +1 -0
- package/dist/components/table/TableWrap.js +35 -0
- package/dist/components/table/filterModel.d.ts +35 -0
- package/dist/components/table/filterModel.d.ts.map +1 -0
- package/dist/components/table/filterModel.js +150 -0
- package/dist/components/table/index.d.ts +13 -0
- package/dist/components/table/index.d.ts.map +1 -0
- package/dist/components/table/index.js +6 -0
- package/dist/components/table/useTableOverflow.d.ts +17 -0
- package/dist/components/table/useTableOverflow.d.ts.map +1 -0
- package/dist/components/table/useTableOverflow.js +54 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/lib/sort.d.ts +10 -0
- package/dist/lib/sort.d.ts.map +1 -0
- package/dist/lib/sort.js +59 -0
- package/dist/styles/audit.css +11 -1
- package/dist/styles/columns.css +1 -84
- package/dist/styles/metrics.css +27 -4
- package/dist/styles/panel.css +143 -0
- package/dist/styles/select.css +16 -0
- package/dist/styles/sync.css +4 -1
- package/dist/styles/table.css +115 -2
- package/package.json +1 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
|
+
import { SortableHeader } from "../resources/SortableHeader";
|
|
4
|
+
import { TableScrollControls, TableSearch, TableWrap, matchesTableQuery, useTableOverflow, } from "../table";
|
|
5
|
+
import { compareFieldValues, nextTableSort, } from "../../lib/sort";
|
|
3
6
|
function formatWhen(iso) {
|
|
4
7
|
const ms = Date.parse(iso);
|
|
5
8
|
if (!Number.isFinite(ms))
|
|
@@ -76,8 +79,47 @@ function statusBadge(status) {
|
|
|
76
79
|
function CellEmpty() {
|
|
77
80
|
return _jsx("span", { className: "vertebr-cell-empty", children: "\u2014" });
|
|
78
81
|
}
|
|
82
|
+
function activitySortValue(row, key) {
|
|
83
|
+
if (row.kind === "scheduled") {
|
|
84
|
+
const campaign = row.campaign;
|
|
85
|
+
switch (key) {
|
|
86
|
+
case "when":
|
|
87
|
+
return campaign.sendAt;
|
|
88
|
+
case "status":
|
|
89
|
+
return campaign.status;
|
|
90
|
+
case "subject":
|
|
91
|
+
return campaign.subject ?? "";
|
|
92
|
+
case "to":
|
|
93
|
+
return String(campaign.recipientCount ?? "");
|
|
94
|
+
case "audience":
|
|
95
|
+
return campaign.resourceLabel ?? campaign.resourceId;
|
|
96
|
+
case "template":
|
|
97
|
+
return campaign.templateLabel ?? campaign.templateId;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const entry = row.entry;
|
|
101
|
+
switch (key) {
|
|
102
|
+
case "when":
|
|
103
|
+
return entry.at;
|
|
104
|
+
case "status":
|
|
105
|
+
return entry.status;
|
|
106
|
+
case "subject":
|
|
107
|
+
return subjectOf(entry);
|
|
108
|
+
case "to":
|
|
109
|
+
return recipientOf(entry);
|
|
110
|
+
case "audience":
|
|
111
|
+
return entry.resourceLabel ?? entry.resourceId;
|
|
112
|
+
case "template":
|
|
113
|
+
return entry.templateLabel ?? entry.templateId;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
79
116
|
export function EmailsActivityPanel({ campaigns, history, canSchedule, canHistory, busy, onCancel, }) {
|
|
80
117
|
const [filter, setFilter] = useState("all");
|
|
118
|
+
const [query, setQuery] = useState("");
|
|
119
|
+
const [sort, setSort] = useState({
|
|
120
|
+
field: "when",
|
|
121
|
+
order: "desc",
|
|
122
|
+
});
|
|
81
123
|
const upcoming = useMemo(() => campaigns
|
|
82
124
|
.filter((campaign) => campaign.status === "scheduled")
|
|
83
125
|
.slice()
|
|
@@ -99,6 +141,39 @@ export function EmailsActivityPanel({ campaigns, history, canSchedule, canHistor
|
|
|
99
141
|
return sent;
|
|
100
142
|
return [...scheduled, ...sent];
|
|
101
143
|
}, [filter, upcoming, history]);
|
|
144
|
+
const visibleRows = useMemo(() => {
|
|
145
|
+
let next = rows;
|
|
146
|
+
if (query.trim()) {
|
|
147
|
+
next = rows.filter((row) => {
|
|
148
|
+
if (row.kind === "scheduled") {
|
|
149
|
+
const campaign = row.campaign;
|
|
150
|
+
return matchesTableQuery({
|
|
151
|
+
subject: campaign.subject,
|
|
152
|
+
to: campaign.resourceLabel ?? campaign.resourceId,
|
|
153
|
+
status: campaign.status,
|
|
154
|
+
template: campaign.templateId,
|
|
155
|
+
}, query);
|
|
156
|
+
}
|
|
157
|
+
const entry = row.entry;
|
|
158
|
+
return matchesTableQuery({
|
|
159
|
+
subject: subjectOf(entry),
|
|
160
|
+
confirmation: confirmationOf(entry),
|
|
161
|
+
to: recipientOf(entry),
|
|
162
|
+
audience: entry.resourceLabel ?? entry.resourceId,
|
|
163
|
+
template: entry.templateLabel ?? entry.templateId,
|
|
164
|
+
status: entry.status,
|
|
165
|
+
via: sourceLabel(entry.source),
|
|
166
|
+
delivery: deliveryOf(entry),
|
|
167
|
+
}, query);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!sort)
|
|
171
|
+
return next;
|
|
172
|
+
const key = sort.field;
|
|
173
|
+
const type = key === "when" ? "datetime" : "string";
|
|
174
|
+
return next.slice().sort((left, right) => compareFieldValues(activitySortValue(left, key), activitySortValue(right, key), type, sort.order));
|
|
175
|
+
}, [rows, query, sort]);
|
|
176
|
+
const tableOverflow = useTableOverflow(visibleRows.length);
|
|
102
177
|
const showActions = Boolean(onCancel) && filter !== "history";
|
|
103
178
|
const cancel = async (campaign) => {
|
|
104
179
|
if (!onCancel)
|
|
@@ -124,7 +199,10 @@ export function EmailsActivityPanel({ campaigns, history, canSchedule, canHistor
|
|
|
124
199
|
}
|
|
125
200
|
return "No activity yet.";
|
|
126
201
|
})();
|
|
127
|
-
|
|
202
|
+
const header = (label, field) => (_jsx("th", { children: _jsx(SortableHeader, { label: label, active: sort?.field === field, order: sort?.field === field ? sort.order : undefined, onSort: () => setSort(nextTableSort(sort, field, field === "when" ? "desc" : "asc")) }) }));
|
|
203
|
+
return (_jsxs("div", { className: "vertebr-emails-activity", children: [_jsxs("div", { className: "vertebr-table-toolbar", children: [_jsx("div", { className: "vertebr-table-toolbar-start", children: _jsxs("div", { className: "vertebr-emails-segment", role: "group", "aria-label": "Activity", children: [_jsx("button", { type: "button", "data-active": filter === "all" ? "true" : undefined, onClick: () => setFilter("all"), children: "All" }), _jsx("button", { type: "button", "data-active": filter === "scheduled" ? "true" : undefined, onClick: () => setFilter("scheduled"), children: "Scheduled" }), _jsx("button", { type: "button", "data-active": filter === "history" ? "true" : undefined, onClick: () => setFilter("history"), children: "History" })] }) }), _jsx(TableSearch, { value: query, onChange: setQuery, "aria-label": "Search activity" }), _jsx(TableScrollControls, { canLeft: tableOverflow.canLeft, canRight: tableOverflow.canRight, onScroll: tableOverflow.scrollBy }), _jsx("span", { className: "vertebr-count", children: query.trim()
|
|
204
|
+
? `${visibleRows.length} of ${rows.length}`
|
|
205
|
+
: rows.length })] }), rows.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: emptyCopy })) : visibleRows.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: "No matching activity." })) : (_jsx(TableWrap, { overflow: tableOverflow, children: _jsxs("table", { className: "vertebr-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [header("When", "when"), header("Status", "status"), header("Subject", "subject"), _jsx("th", { children: "Confirmation" }), header("To", "to"), header("Audience", "audience"), header("Template", "template"), _jsx("th", { children: "Via" }), _jsx("th", { children: "Delivery" }), showActions ? _jsx("th", { className: "vertebr-col-actions", children: "Actions" }) : null] }) }), _jsx("tbody", { children: visibleRows.map((row) => {
|
|
128
206
|
if (row.kind === "scheduled") {
|
|
129
207
|
const campaign = row.campaign;
|
|
130
208
|
const badge = statusBadge("scheduled");
|
|
@@ -134,6 +212,6 @@ export function EmailsActivityPanel({ campaigns, history, canSchedule, canHistor
|
|
|
134
212
|
const badge = statusBadge(entry.status);
|
|
135
213
|
const confirmation = confirmationOf(entry);
|
|
136
214
|
const recipient = recipientOf(entry);
|
|
137
|
-
return (_jsxs("tr", { children: [_jsx("td", { children: _jsx("span", { className: "vertebr-cell-datetime", children: formatWhen(entry.at) }) }), _jsx("td", { children: _jsx("span", { className: "vertebr-badge", "data-tone": badge.tone, children: badge.label }) }), _jsx("td", { children: subjectOf(entry) }), _jsx("td", { children: confirmation || _jsx(CellEmpty, {}) }), _jsx("td", { children: recipient || _jsx(CellEmpty, {}) }), _jsx("td", { children: entry.resourceLabel ?? entry.resourceId }), _jsx("td", { children: entry.templateLabel ?? humanize(entry.templateId) }), _jsx("td", { children: sourceLabel(entry.source) }), _jsx("td", { children: deliveryOf(entry) }), showActions ? (_jsx("td", { className: "vertebr-col-actions" })) : null] }, `history:${row.id}`));
|
|
215
|
+
return (_jsxs("tr", { children: [_jsx("td", { children: _jsx("span", { className: "vertebr-cell-datetime", children: formatWhen(entry.at) }) }), _jsx("td", { children: _jsx("span", { className: "vertebr-badge", "data-tone": badge.tone, children: badge.label }) }), _jsx("td", { children: subjectOf(entry) || _jsx(CellEmpty, {}) }), _jsx("td", { children: confirmation || _jsx(CellEmpty, {}) }), _jsx("td", { children: recipient || _jsx(CellEmpty, {}) }), _jsx("td", { children: (entry.resourceLabel ?? entry.resourceId) || (_jsx(CellEmpty, {})) }), _jsx("td", { children: (entry.templateLabel ?? humanize(entry.templateId)) || (_jsx(CellEmpty, {})) }), _jsx("td", { children: sourceLabel(entry.source) }), _jsx("td", { children: deliveryOf(entry) }), showActions ? (_jsx("td", { className: "vertebr-col-actions", children: _jsx(CellEmpty, {}) })) : null] }, `history:${row.id}`));
|
|
138
216
|
}) })] }) }))] }));
|
|
139
217
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MetricsBoard.d.ts","sourceRoot":"","sources":["../../../src/components/metrics/MetricsBoard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE/D,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"MetricsBoard.d.ts","sourceRoot":"","sources":["../../../src/components/metrics/MetricsBoard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE/D,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,yBAAyB,CAAC;AAQjC,KAAK,iBAAiB,GAAG;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AA6GF,wBAAgB,YAAY,CAAC,EAC3B,KAAkB,EAClB,WAAW,EACX,YAAY,EACZ,OAAO,EACP,MAAW,EACX,KAAK,EACL,aAAa,EACb,SAAS,EACT,OAAO,GACR,EAAE,iBAAiB,+BAmLnB"}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import * as Select from "@radix-ui/react-select";
|
|
3
3
|
import { useMemo, useState } from "react";
|
|
4
4
|
import { METRICS_RANGES, } from "../../lib/metricsRanges";
|
|
5
|
+
import { TableScrollControls, TableWrap, useTableOverflow, } from "../table";
|
|
5
6
|
import { MetricLineChart, MetricSparkline } from "./charts";
|
|
6
7
|
function formatDelta(delta) {
|
|
7
8
|
const abs = Math.abs(delta);
|
|
@@ -40,6 +41,7 @@ export function MetricsBoard({ label = "Overview", windowLabel, compareLabel, me
|
|
|
40
41
|
const kpiIds = new Set(kpis.map((card) => card.id));
|
|
41
42
|
const rest = metrics.filter((card) => !kpiIds.has(card.id));
|
|
42
43
|
const [tableOpen, setTableOpen] = useState(true);
|
|
44
|
+
const tableOverflow = useTableOverflow(rest.length);
|
|
43
45
|
const chartIds = useMemo(() => new Set(charts.map((chart) => chart.id)), [charts]);
|
|
44
46
|
const [chartId, setChartId] = useState(() => charts[0]?.id ?? "");
|
|
45
47
|
const activeChartId = chartIds.has(chartId) ? chartId : (charts[0]?.id ?? "");
|
|
@@ -53,5 +55,9 @@ export function MetricsBoard({ label = "Overview", windowLabel, compareLabel, me
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
: undefined, children: [_jsxs("div", { className: "vertebr-kpi-top", children: [_jsx("p", { className: "vertebr-metric-label", children: card.label }), card.series && card.series.length > 1 ? (_jsx(MetricSparkline, { series: card.series, tone: card.tone })) : null] }), _jsx("p", { className: "vertebr-kpi-value", children: card.value }), _jsxs("div", { className: "vertebr-kpi-foot", children: [card.delta != null ? _jsx(Delta, { value: card.delta }) : null, card.hint ? (_jsx("span", { className: "vertebr-metric-hint", children: card.hint })) : null] })] }, card.id));
|
|
56
|
-
}) }) })) : null, charts.length > 0 && activeChartId ? (_jsx("section", { className: "vertebr-metrics-block", children: _jsx(MetricLineChart, { charts: charts, chartId: activeChartId, onChartIdChange: setChartId, compareLabel: compareLabel }) })) : null, rest.length > 0 ? (_jsx("section", { className: "vertebr-metrics-block", children: _jsxs("div", { className: "vertebr-metrics-table-wrap", "data-collapsed": tableOpen ? undefined : "true", children: [_jsxs("button", { type: "button", className: "vertebr-metrics-table-
|
|
58
|
+
}) }) })) : null, charts.length > 0 && activeChartId ? (_jsx("section", { className: "vertebr-metrics-block", children: _jsx(MetricLineChart, { charts: charts, chartId: activeChartId, onChartIdChange: setChartId, compareLabel: compareLabel }) })) : null, rest.length > 0 ? (_jsx("section", { className: "vertebr-metrics-block", children: _jsxs("div", { className: "vertebr-metrics-table-wrap", "data-collapsed": tableOpen ? undefined : "true", children: [_jsxs("div", { className: "vertebr-metrics-table-head", children: [_jsxs("button", { type: "button", className: "vertebr-metrics-table-toggle", "aria-expanded": tableOpen, onClick: () => setTableOpen((open) => !open), children: [_jsx("p", { className: "vertebr-chart-kicker", children: "Breakdown" }), _jsx("span", { className: "vertebr-metrics-chevron", "aria-hidden": "true" })] }), _jsx(TableScrollControls, { canLeft: tableOverflow.canLeft, canRight: tableOverflow.canRight, onScroll: (direction) => {
|
|
59
|
+
if (!tableOpen)
|
|
60
|
+
setTableOpen(true);
|
|
61
|
+
tableOverflow.scrollBy(direction);
|
|
62
|
+
} })] }), tableOpen ? (_jsx(TableWrap, { overflow: tableOverflow, children: _jsxs("table", { className: "vertebr-table vertebr-metrics-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Metric" }), _jsx("th", { children: "Value" }), _jsx("th", { children: "Change" }), _jsx("th", { children: "Notes" })] }) }), _jsx("tbody", { children: rest.map((card) => (_jsxs("tr", { "data-tone": card.tone ?? "neutral", children: [_jsxs("td", { children: [_jsx("span", { className: "vertebr-metrics-table-label", children: card.label }), card.section ? (_jsx("span", { className: "vertebr-metrics-table-section", children: card.section })) : null] }), _jsx("td", { className: "vertebr-metrics-table-value", children: card.value }), _jsx("td", { children: _jsx(Delta, { value: card.delta }) }), _jsx("td", { className: "vertebr-metrics-table-notes", children: card.hint ?? "—" })] }, card.id))) })] }) })) : null] }) })) : null] }))] }));
|
|
57
63
|
}
|
|
@@ -7,5 +7,9 @@ export type ColumnVisibilityMenuProps = {
|
|
|
7
7
|
visibility: Record<string, boolean>;
|
|
8
8
|
onChange: (next: Record<string, boolean>) => void;
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Column visibility — same filter-chip pattern as table field filters
|
|
12
|
+
* (quiet label + count pill + checklist menu).
|
|
13
|
+
*/
|
|
10
14
|
export declare function ColumnVisibilityMenu({ columns, visibility, onChange, }: ColumnVisibilityMenuProps): import("react").JSX.Element;
|
|
11
15
|
//# sourceMappingURL=ColumnVisibilityMenu.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ColumnVisibilityMenu.d.ts","sourceRoot":"","sources":["../../../src/components/resources/ColumnVisibilityMenu.tsx"],"names":[],"mappings":"AAEA,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACnD,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,EACnC,OAAO,EACP,UAAU,EACV,QAAQ,GACT,EAAE,yBAAyB,+
|
|
1
|
+
{"version":3,"file":"ColumnVisibilityMenu.d.ts","sourceRoot":"","sources":["../../../src/components/resources/ColumnVisibilityMenu.tsx"],"names":[],"mappings":"AAEA,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACnD,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,OAAO,EACP,UAAU,EACV,QAAQ,GACT,EAAE,yBAAyB,+BAmG3B"}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useId, useRef, useState } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Column visibility — same filter-chip pattern as table field filters
|
|
5
|
+
* (quiet label + count pill + checklist menu).
|
|
6
|
+
*/
|
|
3
7
|
export function ColumnVisibilityMenu({ columns, visibility, onChange, }) {
|
|
4
8
|
const [open, setOpen] = useState(false);
|
|
5
9
|
const rootRef = useRef(null);
|
|
6
10
|
const menuId = useId();
|
|
7
|
-
const hiddenCount = columns.filter((col) => visibility[col.id] === false)
|
|
11
|
+
const hiddenCount = columns.filter((col) => visibility[col.id] === false)
|
|
12
|
+
.length;
|
|
13
|
+
const visibleCount = columns.length - hiddenCount;
|
|
14
|
+
const filtering = hiddenCount > 0;
|
|
8
15
|
useEffect(() => {
|
|
9
16
|
if (!open)
|
|
10
17
|
return;
|
|
@@ -24,17 +31,17 @@ export function ColumnVisibilityMenu({ columns, visibility, onChange, }) {
|
|
|
24
31
|
window.removeEventListener("keydown", onKeyDown);
|
|
25
32
|
};
|
|
26
33
|
}, [open]);
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
return (_jsxs("div", { className: "vertebr-table-filter-control", ref: rootRef, "data-open": open ? "true" : undefined, "data-active": filtering ? "true" : undefined, children: [_jsxs("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-table-filter-trigger", "aria-expanded": open, "aria-controls": menuId, "aria-label": filtering
|
|
35
|
+
? `Columns, ${visibleCount} of ${columns.length} visible`
|
|
36
|
+
: "Columns", onClick: () => setOpen((value) => !value), children: ["Columns", filtering ? (_jsx("span", { className: "vertebr-table-filter-count", children: visibleCount })) : null] }), open ? (_jsxs("div", { className: "vertebr-table-filter-menu", id: menuId, role: "listbox", "aria-multiselectable": true, children: [_jsxs("div", { className: "vertebr-table-filter-menu-head", children: [_jsx("span", { children: "Columns" }), filtering ? (_jsx("button", { type: "button", className: "vertebr-link-btn", onClick: () => {
|
|
29
37
|
const next = {};
|
|
30
38
|
for (const col of columns)
|
|
31
39
|
next[col.id] = true;
|
|
32
40
|
onChange(next);
|
|
33
|
-
}, children: "Show all" })] }), _jsx("ul", { className: "vertebr-
|
|
41
|
+
}, children: "Show all" })) : null] }), _jsx("ul", { className: "vertebr-table-filter-list", children: columns.map((col) => {
|
|
34
42
|
const checked = visibility[col.id] !== false;
|
|
35
|
-
return (_jsx("li", { children: _jsxs("label", { className: "vertebr-
|
|
36
|
-
|
|
37
|
-
onChange(next);
|
|
43
|
+
return (_jsx("li", { children: _jsxs("label", { className: "vertebr-table-filter-item", children: [_jsx("input", { type: "checkbox", checked: checked, disabled: checked && visibleCount <= 1, onChange: () => {
|
|
44
|
+
onChange({ ...visibility, [col.id]: !checked });
|
|
38
45
|
} }), _jsx("span", { children: col.label })] }) }, col.id));
|
|
39
46
|
}) })] })) : null] }));
|
|
40
47
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FieldSchema, GalleryItem, MailPreviewResponse, ResourceActionMeta, ResourceUploadMeta } from "@vertebr/client";
|
|
2
|
+
import { type TableSortState } from "../../lib/sort";
|
|
2
3
|
type ResourcePanelProps = {
|
|
3
4
|
schema: Record<string, FieldSchema>;
|
|
4
5
|
items: Record<string, unknown>[];
|
|
@@ -26,8 +27,13 @@ type ResourcePanelProps = {
|
|
|
26
27
|
onBulkAction?: (actionId: string, rowIds: string[], input?: Record<string, unknown>) => Promise<unknown>;
|
|
27
28
|
listQuery?: boolean | Record<string, FieldSchema>;
|
|
28
29
|
total?: number;
|
|
30
|
+
/** When true, show table skeleton instead of rows / empty. */
|
|
31
|
+
loading?: boolean;
|
|
29
32
|
searchQuery?: string;
|
|
30
33
|
onSearchChange?: (q: string) => void;
|
|
34
|
+
/** Active column sort (`schema[field].sortable`). */
|
|
35
|
+
sort?: TableSortState | null;
|
|
36
|
+
onSortChange?: (sort: TableSortState) => void;
|
|
31
37
|
onUpload?: (field: string, files: File[]) => Promise<Array<{
|
|
32
38
|
url: string;
|
|
33
39
|
name?: string;
|
|
@@ -38,7 +44,7 @@ type ResourcePanelProps = {
|
|
|
38
44
|
initialEditRowId?: string;
|
|
39
45
|
onEditRowIdChange?: (rowId: string | null) => void;
|
|
40
46
|
};
|
|
41
|
-
export declare function ResourcePanel({ schema, items, permissions, actions, label, uploads, rowIdKeys, storageKey, onUpdate, onCreate, onDelete, onAction, onActionDone, onPreviewMail, collectionActions, onCollectionAction, bulkActions, onBulkAction, listQuery, total, searchQuery, onSearchChange, onUpload, galleryUpload, onListGallery, onUploadGallery, initialEditRowId, onEditRowIdChange, }: ResourcePanelProps): import("react").JSX.Element;
|
|
47
|
+
export declare function ResourcePanel({ schema, items, permissions, actions, label, uploads, rowIdKeys, storageKey, onUpdate, onCreate, onDelete, onAction, onActionDone, onPreviewMail, collectionActions, onCollectionAction, bulkActions, onBulkAction, listQuery, total, loading, searchQuery, onSearchChange, sort: sortProp, onSortChange, onUpload, galleryUpload, onListGallery, onUploadGallery, initialEditRowId, onEditRowIdChange, }: ResourcePanelProps): import("react").JSX.Element;
|
|
42
48
|
/** @deprecated Prefer ResourcePanel */
|
|
43
49
|
export declare function ResourceTable(props: {
|
|
44
50
|
schema: Record<string, FieldSchema>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResourceTable.d.ts","sourceRoot":"","sources":["../../../src/components/resources/ResourceTable.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"ResourceTable.d.ts","sourceRoot":"","sources":["../../../src/components/resources/ResourceTable.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useMemo,
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table";
|
|
4
4
|
import { ConfigForm } from "../config/ConfigForm";
|
|
5
5
|
import { BulkImageUploadModal } from "./BulkImageUploadModal";
|
|
6
|
-
import { ColumnVisibilityMenu } from "./ColumnVisibilityMenu";
|
|
7
6
|
import { ResourceCell } from "./ResourceCell";
|
|
8
7
|
import { ResourceCreateModal } from "./ResourceCreateModal";
|
|
9
8
|
import { actionInputSchema, ResourceActionModal, } from "./ResourceActionModal";
|
|
10
9
|
import { ResourceMailModal } from "./ResourceMailModal";
|
|
11
10
|
import { ResourceViewModal } from "./ResourceViewModal";
|
|
12
11
|
import { VisibilityEyeToggle } from "../config/VisibilityEye";
|
|
12
|
+
import { SortableHeader } from "./SortableHeader";
|
|
13
|
+
import { TableFilters, TableScrollControls, TableSearch, TableSkeleton, TableWrap, matchesTableFilters, matchesTableQuery, resolveFieldFilter, useTableOverflow, } from "../table";
|
|
14
|
+
import { nextTableSort, sortRows, } from "../../lib/sort";
|
|
13
15
|
function actionHasInput(meta) {
|
|
14
16
|
return Boolean(meta?.input && Object.keys(meta.input).length > 0);
|
|
15
17
|
}
|
|
@@ -130,7 +132,7 @@ function titleFromFilename(name) {
|
|
|
130
132
|
.replace(/\s+/g, " ")
|
|
131
133
|
.trim();
|
|
132
134
|
}
|
|
133
|
-
export function ResourcePanel({ schema, items, permissions, actions = {}, label, uploads, rowIdKeys = ["id", "slug", "uid", "email"], storageKey, onUpdate, onCreate, onDelete, onAction, onActionDone, onPreviewMail, collectionActions = {}, onCollectionAction, bulkActions = {}, onBulkAction, listQuery, total, searchQuery = "", onSearchChange, onUpload, galleryUpload, onListGallery, onUploadGallery, initialEditRowId, onEditRowIdChange, }) {
|
|
135
|
+
export function ResourcePanel({ schema, items, permissions, actions = {}, label, uploads, rowIdKeys = ["id", "slug", "uid", "email"], storageKey, onUpdate, onCreate, onDelete, onAction, onActionDone, onPreviewMail, collectionActions = {}, onCollectionAction, bulkActions = {}, onBulkAction, listQuery, total, loading = false, searchQuery = "", onSearchChange, sort: sortProp = null, onSortChange, onUpload, galleryUpload, onListGallery, onUploadGallery, initialEditRowId, onEditRowIdChange, }) {
|
|
134
136
|
const [editing, setEditing] = useState(null);
|
|
135
137
|
const [creating, setCreating] = useState(false);
|
|
136
138
|
const [viewing, setViewing] = useState(null);
|
|
@@ -138,16 +140,17 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
|
|
|
138
140
|
const [busyAction, setBusyAction] = useState(null);
|
|
139
141
|
const [selected, setSelected] = useState(new Set());
|
|
140
142
|
const [searchDraft, setSearchDraft] = useState(searchQuery);
|
|
143
|
+
const [filters, setFilters] = useState({});
|
|
144
|
+
const [localSort, setLocalSort] = useState(null);
|
|
145
|
+
const sort = onSortChange ? sortProp : localSort;
|
|
146
|
+
const setSort = onSortChange ?? setLocalSort;
|
|
141
147
|
const [pendingAction, setPendingAction] = useState(null);
|
|
142
148
|
const [pendingCollectionAction, setPendingCollectionAction] = useState(null);
|
|
143
149
|
const [pendingBulkAction, setPendingBulkAction] = useState(null);
|
|
144
|
-
const tableWrapRef = useRef(null);
|
|
145
|
-
const [tableOverflow, setTableOverflow] = useState({
|
|
146
|
-
overflow: false,
|
|
147
|
-
canLeft: false,
|
|
148
|
-
canRight: false,
|
|
149
|
-
});
|
|
150
150
|
const [columnVisibility, setColumnVisibility] = useState(() => readVisibility(storageKey, schema));
|
|
151
|
+
const tableOverflow = useTableOverflow(`${items.length}:${Object.keys(columnVisibility)
|
|
152
|
+
.filter((key) => columnVisibility[key] !== false)
|
|
153
|
+
.join(",")}`);
|
|
151
154
|
/** Pending list visibility toggles keyed by row id → field → value. */
|
|
152
155
|
const [visibilityDrafts, setVisibilityDrafts] = useState({});
|
|
153
156
|
const [savingVisibility, setSavingVisibility] = useState(false);
|
|
@@ -163,19 +166,40 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
|
|
|
163
166
|
const visibilityFields = useMemo(() => fieldKeys.filter((key) => isListVisibilityField(key, schema[key])), [fieldKeys, schema]);
|
|
164
167
|
const selectable = Boolean((onAction && actionEntries.length > 0) ||
|
|
165
168
|
(onBulkAction && bulkActionEntries.length > 0));
|
|
166
|
-
|
|
169
|
+
/** Server `q` when listQuery is advertised; otherwise filter loaded rows. */
|
|
170
|
+
const serverSearch = Boolean(listQuery && onSearchChange);
|
|
167
171
|
useEffect(() => {
|
|
168
172
|
setSearchDraft(searchQuery);
|
|
169
173
|
}, [searchQuery]);
|
|
170
174
|
useEffect(() => {
|
|
171
|
-
if (!onSearchChange)
|
|
175
|
+
if (!serverSearch || !onSearchChange)
|
|
172
176
|
return;
|
|
173
177
|
const handle = window.setTimeout(() => {
|
|
174
178
|
if (searchDraft !== searchQuery)
|
|
175
179
|
onSearchChange(searchDraft);
|
|
176
180
|
}, 250);
|
|
177
181
|
return () => window.clearTimeout(handle);
|
|
178
|
-
}, [searchDraft, searchQuery, onSearchChange]);
|
|
182
|
+
}, [searchDraft, searchQuery, onSearchChange, serverSearch]);
|
|
183
|
+
const filterColumns = useMemo(() => {
|
|
184
|
+
return fieldKeys
|
|
185
|
+
.map((key) => {
|
|
186
|
+
const field = schema[key];
|
|
187
|
+
if (!field)
|
|
188
|
+
return null;
|
|
189
|
+
return resolveFieldFilter(key, field.label ?? humanizeKey(key), field);
|
|
190
|
+
})
|
|
191
|
+
.filter((column) => Boolean(column));
|
|
192
|
+
}, [fieldKeys, schema]);
|
|
193
|
+
const visibleItems = useMemo(() => {
|
|
194
|
+
const searched = serverSearch
|
|
195
|
+
? items
|
|
196
|
+
: items.filter((row) => matchesTableQuery(row, searchDraft));
|
|
197
|
+
const filterDefs = Object.fromEntries(filterColumns.map((column) => [
|
|
198
|
+
column.id,
|
|
199
|
+
(row) => row[column.id],
|
|
200
|
+
]));
|
|
201
|
+
return searched.filter((row) => matchesTableFilters(row, filters, filterDefs));
|
|
202
|
+
}, [items, searchDraft, serverSearch, filters, filterColumns]);
|
|
179
203
|
const openEdit = useCallback((row) => {
|
|
180
204
|
setEditing(row);
|
|
181
205
|
onEditRowIdChange?.(resolveRowId(row, rowIdKeys));
|
|
@@ -384,49 +408,12 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
|
|
|
384
408
|
useEffect(() => {
|
|
385
409
|
setColumnVisibility(readVisibility(storageKey, schema));
|
|
386
410
|
}, [storageKey]);
|
|
411
|
+
useEffect(() => {
|
|
412
|
+
setFilters({});
|
|
413
|
+
}, [storageKey]);
|
|
387
414
|
useEffect(() => {
|
|
388
415
|
writeVisibility(storageKey, columnVisibility);
|
|
389
416
|
}, [storageKey, columnVisibility]);
|
|
390
|
-
const updateTableOverflow = useCallback(() => {
|
|
391
|
-
const el = tableWrapRef.current;
|
|
392
|
-
if (!el) {
|
|
393
|
-
setTableOverflow({ overflow: false, canLeft: false, canRight: false });
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
const overflow = el.scrollWidth > el.clientWidth + 2;
|
|
397
|
-
setTableOverflow({
|
|
398
|
-
overflow,
|
|
399
|
-
canLeft: overflow && el.scrollLeft > 2,
|
|
400
|
-
canRight: overflow && el.scrollLeft + el.clientWidth < el.scrollWidth - 2,
|
|
401
|
-
});
|
|
402
|
-
}, []);
|
|
403
|
-
useEffect(() => {
|
|
404
|
-
const el = tableWrapRef.current;
|
|
405
|
-
if (!el)
|
|
406
|
-
return;
|
|
407
|
-
updateTableOverflow();
|
|
408
|
-
const onScroll = () => updateTableOverflow();
|
|
409
|
-
el.addEventListener("scroll", onScroll, { passive: true });
|
|
410
|
-
const observer = new ResizeObserver(updateTableOverflow);
|
|
411
|
-
observer.observe(el);
|
|
412
|
-
const table = el.querySelector("table");
|
|
413
|
-
if (table)
|
|
414
|
-
observer.observe(table);
|
|
415
|
-
return () => {
|
|
416
|
-
el.removeEventListener("scroll", onScroll);
|
|
417
|
-
observer.disconnect();
|
|
418
|
-
};
|
|
419
|
-
}, [updateTableOverflow, items.length, columnVisibility]);
|
|
420
|
-
const scrollTable = (direction) => {
|
|
421
|
-
const el = tableWrapRef.current;
|
|
422
|
-
if (!el)
|
|
423
|
-
return;
|
|
424
|
-
const amount = Math.max(220, Math.round(el.clientWidth * 0.55));
|
|
425
|
-
el.scrollBy({
|
|
426
|
-
left: direction === "left" ? -amount : amount,
|
|
427
|
-
behavior: "smooth",
|
|
428
|
-
});
|
|
429
|
-
};
|
|
430
417
|
const columnOptions = useMemo(() => fieldKeys.map((key) => ({
|
|
431
418
|
id: key,
|
|
432
419
|
label: schema[key]?.label ?? humanizeKey(key),
|
|
@@ -474,7 +461,16 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
|
|
|
474
461
|
return {
|
|
475
462
|
id: key,
|
|
476
463
|
accessorKey: key,
|
|
477
|
-
header:
|
|
464
|
+
header: () => {
|
|
465
|
+
const label = field?.label ?? humanizeKey(key);
|
|
466
|
+
if (!field?.sortable)
|
|
467
|
+
return label;
|
|
468
|
+
const active = sort?.field === key;
|
|
469
|
+
const preferredFirst = field.type === "datetime" || field.type === "number"
|
|
470
|
+
? "desc"
|
|
471
|
+
: "asc";
|
|
472
|
+
return (_jsx(SortableHeader, { label: label, active: Boolean(active), order: active ? sort?.order : undefined, onSort: () => setSort(nextTableSort(sort, key, preferredFirst)) }));
|
|
473
|
+
},
|
|
478
474
|
meta: { fieldKey: key, field },
|
|
479
475
|
cell: ({ row, getValue, }) => {
|
|
480
476
|
if (listVisibility) {
|
|
@@ -525,9 +521,12 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
|
|
|
525
521
|
rowIdKeys,
|
|
526
522
|
visibilityDrafts,
|
|
527
523
|
setVisibilityDraft,
|
|
524
|
+
sort,
|
|
525
|
+
setSort,
|
|
528
526
|
]);
|
|
527
|
+
const sortedItems = useMemo(() => sortRows(visibleItems, sort, schema), [visibleItems, sort, schema]);
|
|
529
528
|
const table = useReactTable({
|
|
530
|
-
data:
|
|
529
|
+
data: sortedItems,
|
|
531
530
|
columns,
|
|
532
531
|
state: { columnVisibility },
|
|
533
532
|
onColumnVisibilityChange: setColumnVisibility,
|
|
@@ -559,7 +558,13 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
|
|
|
559
558
|
closeEdit();
|
|
560
559
|
} })] }));
|
|
561
560
|
}
|
|
562
|
-
const toolbar = (_jsxs("div", { className: "vertebr-table-toolbar", children: [_jsxs("div", { className: "vertebr-table-toolbar-start", children: [
|
|
561
|
+
const toolbar = (_jsxs("div", { className: "vertebr-table-toolbar", children: [_jsxs("div", { className: "vertebr-table-toolbar-start", children: [filterColumns.length > 0 || columnOptions.length > 0 ? (_jsx(TableFilters, { columns: filterColumns, selected: filters, onChange: setFilters, columnVisibility: columnOptions.length > 0
|
|
562
|
+
? {
|
|
563
|
+
columns: columnOptions,
|
|
564
|
+
visibility: columnVisibility,
|
|
565
|
+
onChange: setColumnVisibility,
|
|
566
|
+
}
|
|
567
|
+
: undefined })) : null, canUpload ? (_jsxs("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-icon", onClick: () => setBulkOpen(true), children: [_jsx(ImageUploadIcon, {}), "Bulk upload"] })) : null, visibilityFields.length > 0 && dirtyVisibilityCount > 0 ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "vertebr-count", children: [dirtyVisibilityCount, " visibility change", dirtyVisibilityCount === 1 ? "" : "s"] }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-primary", disabled: savingVisibility, onClick: () => void saveVisibility(), children: savingVisibility ? "Saving…" : "Save visibility" }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: savingVisibility, onClick: () => setVisibilityDrafts({}), children: "Discard" })] })) : null, collectionActionEntries.map(([actionId, meta]) => (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: busyAction != null, onClick: () => queueCollectionAction(actionId), children: meta.label }, actionId))), selectable && selected.size > 0 ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "vertebr-count", children: [selected.size, " selected"] }), actionEntries.map(([actionId, meta]) => (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: busyAction != null, onClick: () => queueAction(actionId, [...selected]), children: meta.label }, actionId))), bulkActionEntries.map(([actionId, meta]) => (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: busyAction != null, onClick: () => queueBulkAction(actionId, [...selected]), children: meta.label }, `bulk:${actionId}`))), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: () => setSelected(new Set()), children: "Clear" })] })) : null] }), _jsx(TableScrollControls, { canLeft: tableOverflow.canLeft, canRight: tableOverflow.canRight, onScroll: tableOverflow.scrollBy })] }));
|
|
563
568
|
const bulkModal = bulkOpen && uploads && onUpload ? (_jsx(BulkImageUploadModal, { uploads: uploads, canCreate: canCreate, fieldLabels: Object.fromEntries(Object.keys(uploads).map((key) => [
|
|
564
569
|
key,
|
|
565
570
|
schema[key]?.label ?? humanizeKey(key),
|
|
@@ -588,15 +593,36 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
|
|
|
588
593
|
const pendingCollectionModal = pendingCollectionAction &&
|
|
589
594
|
collectionActions[pendingCollectionAction]?.input ? (_jsx(ResourceActionModal, { title: collectionActions[pendingCollectionAction]?.label ?? "Run action", eyebrow: "This resource", schema: actionInputSchema(collectionActions[pendingCollectionAction].input), submitLabel: busyAction ? "Working…" : "Run", onClose: () => setPendingCollectionAction(null), onSubmit: (value) => runCollectionAction(pendingCollectionAction, value) })) : null;
|
|
590
595
|
const pendingBulkModal = pendingBulkAction && bulkActions[pendingBulkAction]?.input ? (_jsx(ResourceActionModal, { title: bulkActions[pendingBulkAction]?.label ?? "Bulk action", eyebrow: `${selected.size} selected`, schema: actionInputSchema(bulkActions[pendingBulkAction].input), submitLabel: busyAction ? "Working…" : "Run", onClose: () => setPendingBulkAction(null), onSubmit: (value) => runBulkAction(pendingBulkAction, [...selected], value) })) : null;
|
|
591
|
-
const recordsHead = (_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: "Records" }), _jsxs("div", { className: "vertebr-panel-head-actions", children: [
|
|
596
|
+
const recordsHead = (_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: "Records" }), _jsxs("div", { className: "vertebr-panel-head-actions", children: [_jsx(TableSearch, { value: searchDraft, onChange: setSearchDraft, "aria-label": "Search records" }), canCreate ? (_jsxs("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-icon", onClick: () => setCreating(true), children: [_jsx(PlusIcon, {}), createLabel] })) : null, _jsx("span", { className: "vertebr-count", children: loading
|
|
597
|
+
? "…"
|
|
598
|
+
: total != null
|
|
599
|
+
? `${visibleItems.length} of ${total}`
|
|
600
|
+
: visibleItems.length !== items.length
|
|
601
|
+
? `${visibleItems.length} of ${items.length}`
|
|
602
|
+
: visibleItems.length })] })] }));
|
|
603
|
+
if (loading) {
|
|
604
|
+
return (_jsxs("div", { className: "vertebr-resource-body", children: [recordsHead, _jsx(TableSkeleton, { columns: fieldKeys.map((key) => ({
|
|
605
|
+
id: key,
|
|
606
|
+
label: schema[key]?.label ?? humanizeKey(key),
|
|
607
|
+
})) })] }));
|
|
608
|
+
}
|
|
592
609
|
if (items.length === 0) {
|
|
593
|
-
return (_jsxs("div", { className: "vertebr-resource-body", children: [recordsHead,
|
|
610
|
+
return (_jsxs("div", { className: "vertebr-resource-body", children: [recordsHead, filterColumns.length > 0 ||
|
|
594
611
|
columnOptions.length > 0 ||
|
|
612
|
+
canUpload ||
|
|
595
613
|
collectionActionEntries.length > 0
|
|
596
614
|
? toolbar
|
|
597
615
|
: null, _jsx("div", { className: "vertebr-empty", children: "No rows yet." }), bulkModal, createModal, pendingActionModal, pendingCollectionModal, pendingBulkModal] }));
|
|
598
616
|
}
|
|
599
|
-
|
|
617
|
+
if (visibleItems.length === 0) {
|
|
618
|
+
return (_jsxs("div", { className: "vertebr-resource-body", children: [recordsHead, filterColumns.length > 0 ||
|
|
619
|
+
columnOptions.length > 0 ||
|
|
620
|
+
canUpload ||
|
|
621
|
+
collectionActionEntries.length > 0
|
|
622
|
+
? toolbar
|
|
623
|
+
: null, _jsx("div", { className: "vertebr-empty", children: "No matching rows." }), bulkModal, createModal, pendingActionModal, pendingCollectionModal, pendingBulkModal] }));
|
|
624
|
+
}
|
|
625
|
+
return (_jsxs("div", { className: "vertebr-resource-body", children: [recordsHead, toolbar, _jsx(TableWrap, { overflow: tableOverflow, transitionKey: JSON.stringify(filters), children: _jsxs("table", { className: "vertebr-table", children: [_jsx("thead", { children: table.getHeaderGroups().map((headerGroup) => (_jsx("tr", { children: headerGroup.headers.map((header) => (_jsx("th", { className: header.column.id === "_actions"
|
|
600
626
|
? "vertebr-col-actions"
|
|
601
627
|
: header.column.id === "_select"
|
|
602
628
|
? "vertebr-col-select"
|
|
@@ -627,12 +653,6 @@ export function ResourceTable(props) {
|
|
|
627
653
|
function PlusIcon() {
|
|
628
654
|
return (_jsxs("svg", { className: "vertebr-btn-glyph", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M12 5v14" }), _jsx("path", { d: "M5 12h14" })] }));
|
|
629
655
|
}
|
|
630
|
-
function ChevronLeftIcon() {
|
|
631
|
-
return (_jsx("svg", { className: "vertebr-btn-glyph", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m15 18-6-6 6-6" }) }));
|
|
632
|
-
}
|
|
633
|
-
function ChevronRightIcon() {
|
|
634
|
-
return (_jsx("svg", { className: "vertebr-btn-glyph", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m9 18 6-6-6-6" }) }));
|
|
635
|
-
}
|
|
636
656
|
function ImageUploadIcon() {
|
|
637
657
|
return (_jsxs("svg", { className: "vertebr-btn-glyph", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M16 5h6" }), _jsx("path", { d: "M19 2v6" }), _jsx("rect", { x: "3", y: "7", width: "14", height: "14", rx: "2" }), _jsx("circle", { cx: "8.5", cy: "12.5", r: "1.5" }), _jsx("path", { d: "m17 21-4.5-4.5L7 21" })] }));
|
|
638
658
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SortOrder } from "../../lib/sort";
|
|
2
|
+
export type SortableHeaderProps = {
|
|
3
|
+
label: string;
|
|
4
|
+
active: boolean;
|
|
5
|
+
order?: SortOrder;
|
|
6
|
+
onSort: () => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function SortableHeader({ label, active, order, onSort, }: SortableHeaderProps): import("react").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=SortableHeader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SortableHeader.d.ts","sourceRoot":"","sources":["../../../src/components/resources/SortableHeader.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAEF,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,MAAM,EACN,KAAK,EACL,MAAM,GACP,EAAE,mBAAmB,+BAuBrB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function SortableHeader({ label, active, order, onSort, }) {
|
|
3
|
+
return (_jsxs("button", { type: "button", className: "vertebr-th-sort", "data-active": active ? "true" : undefined, "data-order": active ? order : undefined, "aria-label": active
|
|
4
|
+
? `Sorted by ${label}, ${order === "desc" ? "descending" : "ascending"}. Click to reverse.`
|
|
5
|
+
: `Sort by ${label}`, onClick: (event) => {
|
|
6
|
+
event.stopPropagation();
|
|
7
|
+
onSort();
|
|
8
|
+
}, children: [_jsx("span", { children: label }), _jsx("span", { className: "vertebr-th-sort-icon", "aria-hidden": "true", children: active ? (order === "desc" ? "↓" : "↑") : "↕" })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TableFilterColumn, TableFilterState } from "./filterModel";
|
|
2
|
+
import { type ColumnOption } from "../resources/ColumnVisibilityMenu";
|
|
3
|
+
export type TableColumnVisibilityProps = {
|
|
4
|
+
columns: ColumnOption[];
|
|
5
|
+
visibility: Record<string, boolean>;
|
|
6
|
+
onChange: (next: Record<string, boolean>) => void;
|
|
7
|
+
};
|
|
8
|
+
export type TableFiltersProps = {
|
|
9
|
+
columns: TableFilterColumn[];
|
|
10
|
+
selected: TableFilterState;
|
|
11
|
+
onChange: (next: TableFilterState) => void;
|
|
12
|
+
/** Optional Columns chip — same filter-chip UI as field filters. */
|
|
13
|
+
columnVisibility?: TableColumnVisibilityProps;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Host-declared filter chips (+ optional Columns chip).
|
|
17
|
+
* Selected values stay checked in each dropdown.
|
|
18
|
+
*/
|
|
19
|
+
export declare function TableFilters({ columns, selected, onChange, columnVisibility, }: TableFiltersProps): import("react").JSX.Element | null;
|
|
20
|
+
//# sourceMappingURL=TableFilters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableFilters.d.ts","sourceRoot":"","sources":["../../../src/components/table/TableFilters.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAMzE,OAAO,EAEL,KAAK,YAAY,EAClB,MAAM,mCAAmC,CAAC;AAE3C,MAAM,MAAM,0BAA0B,GAAG;IACvC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACnD,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,QAAQ,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC3C,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,0BAA0B,CAAC;CAC/C,CAAC;AAEF;;;GAGG;AACH,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,gBAAgB,GACjB,EAAE,iBAAiB,sCA4CnB"}
|