@pilotiq/pilotiq 0.6.2 → 0.7.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/.turbo/turbo-build.log +6 -2
- package/CHANGELOG.md +608 -0
- package/CLAUDE.md +6 -5
- package/dist/Column.d.ts +35 -0
- package/dist/Column.d.ts.map +1 -1
- package/dist/Column.js +41 -0
- package/dist/Column.js.map +1 -1
- package/dist/Page.d.ts +13 -4
- package/dist/Page.d.ts.map +1 -1
- package/dist/Page.js +9 -2
- package/dist/Page.js.map +1 -1
- package/dist/Pilotiq.d.ts +84 -0
- package/dist/Pilotiq.d.ts.map +1 -1
- package/dist/Pilotiq.js +66 -0
- package/dist/Pilotiq.js.map +1 -1
- package/dist/Resource.d.ts +26 -0
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +9 -0
- package/dist/Resource.js.map +1 -1
- package/dist/actions/exportFactory.js +1 -1
- package/dist/actions/exportFactory.js.map +1 -1
- package/dist/columns/SelectColumn.d.ts +32 -5
- package/dist/columns/SelectColumn.d.ts.map +1 -1
- package/dist/columns/SelectColumn.js +37 -7
- package/dist/columns/SelectColumn.js.map +1 -1
- package/dist/defaultPages.d.ts.map +1 -1
- package/dist/defaultPages.js +3 -0
- package/dist/defaultPages.js.map +1 -1
- package/dist/elements/Form.d.ts +17 -0
- package/dist/elements/Form.d.ts.map +1 -1
- package/dist/elements/Form.js +17 -0
- package/dist/elements/Form.js.map +1 -1
- package/dist/elements/Table.d.ts +26 -0
- package/dist/elements/Table.d.ts.map +1 -1
- package/dist/elements/Table.js +15 -1
- package/dist/elements/Table.js.map +1 -1
- package/dist/elements/TableGroup.d.ts +84 -0
- package/dist/elements/TableGroup.d.ts.map +1 -1
- package/dist/elements/TableGroup.js +103 -0
- package/dist/elements/TableGroup.js.map +1 -1
- package/dist/elements/dispatchForm.d.ts.map +1 -1
- package/dist/elements/dispatchForm.js +36 -6
- package/dist/elements/dispatchForm.js.map +1 -1
- package/dist/elements/dispatchTable.d.ts +12 -0
- package/dist/elements/dispatchTable.d.ts.map +1 -1
- package/dist/elements/dispatchTable.js +103 -28
- package/dist/elements/dispatchTable.js.map +1 -1
- package/dist/fields/Field.d.ts +7 -2
- package/dist/fields/Field.d.ts.map +1 -1
- package/dist/fields/Field.js +8 -3
- package/dist/fields/Field.js.map +1 -1
- package/dist/fields/RepeaterField.d.ts +65 -0
- package/dist/fields/RepeaterField.d.ts.map +1 -1
- package/dist/fields/RepeaterField.js +48 -0
- package/dist/fields/RepeaterField.js.map +1 -1
- package/dist/orm/modelDefaults.d.ts.map +1 -1
- package/dist/orm/modelDefaults.js +19 -0
- package/dist/orm/modelDefaults.js.map +1 -1
- package/dist/pageData.d.ts +20 -0
- package/dist/pageData.d.ts.map +1 -1
- package/dist/pageData.js +242 -34
- package/dist/pageData.js.map +1 -1
- package/dist/react/AppShell.d.ts +17 -1
- package/dist/react/AppShell.d.ts.map +1 -1
- package/dist/react/AppShell.js +34 -3
- package/dist/react/AppShell.js.map +1 -1
- package/dist/react/PendingSuggestionApplierRegistry.d.ts +34 -0
- package/dist/react/PendingSuggestionApplierRegistry.d.ts.map +1 -0
- package/dist/react/PendingSuggestionApplierRegistry.js +51 -0
- package/dist/react/PendingSuggestionApplierRegistry.js.map +1 -0
- package/dist/react/PendingSuggestionOverlayRegistry.d.ts +46 -0
- package/dist/react/PendingSuggestionOverlayRegistry.d.ts.map +1 -0
- package/dist/react/PendingSuggestionOverlayRegistry.js +16 -0
- package/dist/react/PendingSuggestionOverlayRegistry.js.map +1 -0
- package/dist/react/PendingSuggestionsContext.d.ts +153 -0
- package/dist/react/PendingSuggestionsContext.d.ts.map +1 -0
- package/dist/react/PendingSuggestionsContext.js +46 -0
- package/dist/react/PendingSuggestionsContext.js.map +1 -0
- package/dist/react/SchemaRenderer.d.ts.map +1 -1
- package/dist/react/SchemaRenderer.js +312 -39
- package/dist/react/SchemaRenderer.js.map +1 -1
- package/dist/react/cells/EditableCell.d.ts +8 -0
- package/dist/react/cells/EditableCell.d.ts.map +1 -1
- package/dist/react/cells/EditableCell.js +6 -2
- package/dist/react/cells/EditableCell.js.map +1 -1
- package/dist/react/fields/CheckboxListInput.d.ts.map +1 -1
- package/dist/react/fields/CheckboxListInput.js +29 -2
- package/dist/react/fields/CheckboxListInput.js.map +1 -1
- package/dist/react/fields/ColorInput.d.ts.map +1 -1
- package/dist/react/fields/ColorInput.js +28 -2
- package/dist/react/fields/ColorInput.js.map +1 -1
- package/dist/react/fields/DateTimeInput.d.ts.map +1 -1
- package/dist/react/fields/DateTimeInput.js +28 -2
- package/dist/react/fields/DateTimeInput.js.map +1 -1
- package/dist/react/fields/FieldShell.d.ts.map +1 -1
- package/dist/react/fields/FieldShell.js +161 -3
- package/dist/react/fields/FieldShell.js.map +1 -1
- package/dist/react/fields/FileUploadInput.d.ts.map +1 -1
- package/dist/react/fields/FileUploadInput.js +27 -2
- package/dist/react/fields/FileUploadInput.js.map +1 -1
- package/dist/react/fields/KeyValueInput.d.ts.map +1 -1
- package/dist/react/fields/KeyValueInput.js +33 -2
- package/dist/react/fields/KeyValueInput.js.map +1 -1
- package/dist/react/fields/RadioInput.d.ts.map +1 -1
- package/dist/react/fields/RadioInput.js +28 -2
- package/dist/react/fields/RadioInput.js.map +1 -1
- package/dist/react/fields/SelectFieldInput.d.ts.map +1 -1
- package/dist/react/fields/SelectFieldInput.js +31 -2
- package/dist/react/fields/SelectFieldInput.js.map +1 -1
- package/dist/react/fields/SliderInput.d.ts.map +1 -1
- package/dist/react/fields/SliderInput.js +26 -2
- package/dist/react/fields/SliderInput.js.map +1 -1
- package/dist/react/fields/TagsInput.d.ts.map +1 -1
- package/dist/react/fields/TagsInput.js +26 -2
- package/dist/react/fields/TagsInput.js.map +1 -1
- package/dist/react/fields/ToggleFieldInput.d.ts.map +1 -1
- package/dist/react/fields/ToggleFieldInput.js +29 -2
- package/dist/react/fields/ToggleFieldInput.js.map +1 -1
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +55 -2
- package/dist/routes.js.map +1 -1
- package/dist/schema/Section.d.ts +16 -0
- package/dist/schema/Section.d.ts.map +1 -1
- package/dist/schema/Section.js +16 -0
- package/dist/schema/Section.js.map +1 -1
- package/dist/schema/Wizard.d.ts +45 -0
- package/dist/schema/Wizard.d.ts.map +1 -1
- package/dist/schema/Wizard.js +50 -0
- package/dist/schema/Wizard.js.map +1 -1
- package/dist/schema/resolveSchema.d.ts +8 -0
- package/dist/schema/resolveSchema.d.ts.map +1 -1
- package/dist/schema/resolveSchema.js +70 -1
- package/dist/schema/resolveSchema.js.map +1 -1
- package/dist/sessionFilters.d.ts.map +1 -1
- package/dist/sessionFilters.js +12 -1
- package/dist/sessionFilters.js.map +1 -1
- package/dist/styles/file-upload.css +13 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +9 -2
- package/dist/vite.js.map +1 -1
- package/package.json +6 -4
- package/src/Column.test.ts +36 -0
- package/src/Column.ts +54 -0
- package/src/Page.ts +13 -4
- package/src/Pilotiq.ts +109 -0
- package/src/Resource.ts +29 -0
- package/src/actions/exportFactory.ts +1 -1
- package/src/columns/SelectColumn.ts +46 -8
- package/src/columns/editableColumns.test.ts +45 -0
- package/src/defaultPages.ts +3 -0
- package/src/elements/Form.ts +19 -0
- package/src/elements/Table.ts +35 -1
- package/src/elements/TableGroup.test.ts +111 -0
- package/src/elements/TableGroup.ts +135 -0
- package/src/elements/dispatchForm.ts +34 -7
- package/src/elements/dispatchTable.test.ts +267 -0
- package/src/elements/dispatchTable.ts +111 -32
- package/src/fields/Field.test.ts +15 -0
- package/src/fields/Field.ts +8 -3
- package/src/fields/RepeaterField.ts +104 -0
- package/src/fields/RepeaterRelationship.test.ts +173 -0
- package/src/nestedRelationManagerData.test.ts +21 -0
- package/src/orm/modelDefaults.ts +21 -0
- package/src/pageData.ts +267 -47
- package/src/react/AppShell.tsx +55 -4
- package/src/react/PendingSuggestionApplierRegistry.ts +80 -0
- package/src/react/PendingSuggestionOverlayRegistry.ts +54 -0
- package/src/react/PendingSuggestionsContext.tsx +172 -0
- package/src/react/SchemaRenderer.tsx +504 -95
- package/src/react/cells/EditableCell.tsx +11 -2
- package/src/react/fields/CheckboxListInput.tsx +23 -2
- package/src/react/fields/ColorInput.tsx +22 -2
- package/src/react/fields/DateTimeInput.tsx +22 -2
- package/src/react/fields/FieldShell.tsx +167 -3
- package/src/react/fields/FileUploadInput.tsx +21 -2
- package/src/react/fields/KeyValueInput.tsx +32 -2
- package/src/react/fields/RadioInput.tsx +23 -2
- package/src/react/fields/SelectFieldInput.tsx +25 -2
- package/src/react/fields/SliderInput.tsx +20 -2
- package/src/react/fields/TagsInput.tsx +20 -2
- package/src/react/fields/ToggleFieldInput.tsx +23 -2
- package/src/react/index.ts +18 -0
- package/src/relationManagerData.test.ts +451 -2
- package/src/routes.ts +58 -2
- package/src/schema/Section.ts +17 -0
- package/src/schema/Wizard.ts +67 -0
- package/src/schema/containers.test.ts +90 -0
- package/src/schema/resolveSchema.test.ts +50 -0
- package/src/schema/resolveSchema.ts +79 -1
- package/src/sessionFilters.test.ts +23 -0
- package/src/sessionFilters.ts +11 -1
- package/src/styles/file-upload.css +13 -0
- package/src/vite.ts +9 -2
|
@@ -32,7 +32,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '
|
|
|
32
32
|
import { Table as DataTable, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow, } from './ui/table.js';
|
|
33
33
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
|
|
34
34
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from './ui/tooltip.js';
|
|
35
|
-
import { FilterIcon, CircleIcon, InboxIcon, GripVerticalIcon, ChevronDownIcon, CopyIcon, CheckIcon, XIcon, InfoIcon, TriangleAlertIcon, CircleCheckIcon, CircleAlertIcon, } from 'lucide-react';
|
|
35
|
+
import { FilterIcon, CircleIcon, InboxIcon, GripVerticalIcon, ChevronDownIcon, CopyIcon, CheckIcon, XIcon, InfoIcon, TriangleAlertIcon, CircleCheckIcon, CircleAlertIcon, Columns3Icon, } from 'lucide-react';
|
|
36
36
|
import { useNavigate } from './navigate.js';
|
|
37
37
|
import { parseDateRangeValue, encodeDateRangeValue, } from '../filters/DateRangeFilter.js';
|
|
38
38
|
import { parseMultiSelectValue, encodeMultiSelectValue, } from '../filters/MultiSelectFilter.js';
|
|
@@ -1094,16 +1094,59 @@ function SectionRenderer({ el, index }) {
|
|
|
1094
1094
|
return (_jsxs("section", { className: `flex flex-col ${compact ? 'gap-2' : 'gap-3'} rounded-lg border ${surfaceClass} ${padding} ${layoutClasses(el)}`.trim(), children: [(title || description || collapsible || badge || afterHeader.length > 0) && (_jsxs("header", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "flex items-start gap-2", children: [Icon && _jsx(Icon, { className: "size-4 mt-0.5 text-muted-foreground", "aria-hidden": "true" }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [title && _jsx("h3", { className: `${titleSize} font-semibold`, children: title }), badge && (_jsx("span", { className: "rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: badge }))] }), description && _jsx("p", { className: "text-xs text-muted-foreground mt-0.5", children: description })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [afterHeader.length > 0 && (_jsx("div", { className: "flex items-center gap-1", children: afterHeader.map((a, i) => renderElement(a, i)) })), collapsible && (_jsx("button", { type: "button", onClick: () => setCollapsed(c => !c), className: "text-xs text-muted-foreground hover:text-foreground", children: collapsed ? 'Expand' : 'Collapse' }))] })] })), !collapsed && el.children && el.children.length > 0 && (_jsx("div", { className: gridClass, children: el.children.map((c, i) => renderElement(c, i)) }))] }, index));
|
|
1095
1095
|
}
|
|
1096
1096
|
// ─── Wizard (Plan #8) ───────────────────────────────────────
|
|
1097
|
+
/**
|
|
1098
|
+
* Resolve the initial active step for `WizardRenderer`. Priority:
|
|
1099
|
+
* 1. URL `?<queryKey>=N` (1-based — wizards expose human-friendly indexes
|
|
1100
|
+
* when `Wizard.persistStepInQueryString()` is enabled).
|
|
1101
|
+
* 2. `localStorage[<storageKey>]` (0-based, set by the persist effect).
|
|
1102
|
+
* 3. `startOnStep` configured on the Wizard.
|
|
1103
|
+
*
|
|
1104
|
+
* SSR-safe: returns `startOnStep` when `window` is undefined.
|
|
1105
|
+
*/
|
|
1106
|
+
function readInitialWizardStep(total, startOnStep, storageKey, queryKey) {
|
|
1107
|
+
if (typeof window === 'undefined')
|
|
1108
|
+
return startOnStep;
|
|
1109
|
+
if (queryKey) {
|
|
1110
|
+
try {
|
|
1111
|
+
const raw = new URL(window.location.href).searchParams.get(queryKey);
|
|
1112
|
+
if (raw !== null && raw !== '') {
|
|
1113
|
+
const n = Number(raw) - 1;
|
|
1114
|
+
if (Number.isFinite(n) && n >= 0 && n < total)
|
|
1115
|
+
return n;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
catch { /* ignore */ }
|
|
1119
|
+
}
|
|
1120
|
+
if (storageKey) {
|
|
1121
|
+
try {
|
|
1122
|
+
const stored = window.localStorage.getItem(storageKey);
|
|
1123
|
+
if (stored !== null) {
|
|
1124
|
+
const n = Number(stored);
|
|
1125
|
+
if (Number.isFinite(n) && n >= 0 && n < total)
|
|
1126
|
+
return n;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
catch { /* ignore */ }
|
|
1130
|
+
}
|
|
1131
|
+
return startOnStep;
|
|
1132
|
+
}
|
|
1097
1133
|
/**
|
|
1098
1134
|
* Multi-step form layout. Tracks active step in `useState`, optionally
|
|
1099
|
-
* persisted to localStorage. On Next click,
|
|
1100
|
-
* the form's `wizardUrl` (stamped by the
|
|
1101
|
-
* has a Wizard descendant). 200 → advance;
|
|
1102
|
-
* absent `wizardUrl` → advance immediately
|
|
1135
|
+
* persisted to localStorage and/or the URL query string. On Next click,
|
|
1136
|
+
* POSTs `{ step, values }` to the form's `wizardUrl` (stamped by the
|
|
1137
|
+
* route handler when the form has a Wizard descendant). 200 → advance;
|
|
1138
|
+
* 422 → stamp inline errors; absent `wizardUrl` → advance immediately
|
|
1139
|
+
* (no validation).
|
|
1103
1140
|
*
|
|
1104
1141
|
* Inactive steps render hidden (display:none) rather than unmounted so
|
|
1105
1142
|
* controlled inputs preserve their values across step transitions and
|
|
1106
1143
|
* cross-step `$get` works on the resolved meta.
|
|
1144
|
+
*
|
|
1145
|
+
* Nav buttons honor `Wizard.submitAction() / nextAction() / previousAction()`
|
|
1146
|
+
* — chrome (label / icon / color / size / outlined / iconOnly / tooltip /
|
|
1147
|
+
* disabled rules) carries through to the rendered button while the click
|
|
1148
|
+
* behavior stays hardwired (advance / recede / submit-form). Bare wizards
|
|
1149
|
+
* keep the built-in defaults.
|
|
1107
1150
|
*/
|
|
1108
1151
|
function WizardRenderer({ el, index }) {
|
|
1109
1152
|
const formState = useFormState();
|
|
@@ -1114,37 +1157,47 @@ function WizardRenderer({ el, index }) {
|
|
|
1114
1157
|
const startOnStep = Math.max(0, Math.min(Math.max(0, steps.length - 1), Number(el['startOnStep'] ?? 0)));
|
|
1115
1158
|
const persist = el['persist'] !== false;
|
|
1116
1159
|
const storageKey = persist && formId ? `pilotiq.wizard.${formId}.step` : undefined;
|
|
1117
|
-
const [
|
|
1160
|
+
const queryKey = typeof el['persistStepInQueryString'] === 'string'
|
|
1161
|
+
? String(el['persistStepInQueryString'])
|
|
1162
|
+
: undefined;
|
|
1163
|
+
const submitActionMeta = el['submitAction'];
|
|
1164
|
+
const nextActionMeta = el['nextAction'];
|
|
1165
|
+
const previousActionMeta = el['previousAction'];
|
|
1166
|
+
// Initial-step resolution priority: URL (?<key>=N, 1-based) > localStorage >
|
|
1167
|
+
// startOnStep. URL wins on first paint so deep links land on the right step
|
|
1168
|
+
// before localStorage can override. Lazy initializer — resolution runs once.
|
|
1169
|
+
const [active, setActive] = useState(() => readInitialWizardStep(steps.length, startOnStep, storageKey, queryKey));
|
|
1118
1170
|
const [advancing, setAdvancing] = useState(false);
|
|
1119
1171
|
const [advanceError, setAdvanceError] = useState(null);
|
|
1120
|
-
//
|
|
1172
|
+
// Persist active step changes to localStorage (when enabled).
|
|
1121
1173
|
useEffect(() => {
|
|
1122
1174
|
if (!storageKey)
|
|
1123
1175
|
return;
|
|
1124
1176
|
if (typeof window === 'undefined')
|
|
1125
1177
|
return;
|
|
1126
1178
|
try {
|
|
1127
|
-
|
|
1128
|
-
if (stored !== null) {
|
|
1129
|
-
const n = Number(stored);
|
|
1130
|
-
if (Number.isFinite(n) && n >= 0 && n < steps.length)
|
|
1131
|
-
setActive(n);
|
|
1132
|
-
}
|
|
1179
|
+
window.localStorage.setItem(storageKey, String(active));
|
|
1133
1180
|
}
|
|
1134
1181
|
catch { /* ignore */ }
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
//
|
|
1182
|
+
}, [storageKey, active]);
|
|
1183
|
+
// Mirror active step to the URL via replaceState — purely client-side state
|
|
1184
|
+
// sync, no SPA re-fetch. 1-based externally; cleared when on the first step
|
|
1185
|
+
// so bare URLs don't grow ?step=1 noise.
|
|
1138
1186
|
useEffect(() => {
|
|
1139
|
-
if (!
|
|
1187
|
+
if (!queryKey)
|
|
1140
1188
|
return;
|
|
1141
1189
|
if (typeof window === 'undefined')
|
|
1142
1190
|
return;
|
|
1143
1191
|
try {
|
|
1144
|
-
window.
|
|
1192
|
+
const url = new URL(window.location.href);
|
|
1193
|
+
if (active === 0)
|
|
1194
|
+
url.searchParams.delete(queryKey);
|
|
1195
|
+
else
|
|
1196
|
+
url.searchParams.set(queryKey, String(active + 1));
|
|
1197
|
+
window.history.replaceState(window.history.state, '', url.toString());
|
|
1145
1198
|
}
|
|
1146
1199
|
catch { /* ignore */ }
|
|
1147
|
-
}, [
|
|
1200
|
+
}, [queryKey, active]);
|
|
1148
1201
|
if (steps.length === 0) {
|
|
1149
1202
|
return (_jsx("div", { className: "rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground", children: "No steps configured." }, index));
|
|
1150
1203
|
}
|
|
@@ -1211,9 +1264,43 @@ function WizardRenderer({ el, index }) {
|
|
|
1211
1264
|
: isDone ? 'bg-muted-foreground/20 text-foreground'
|
|
1212
1265
|
: 'bg-muted text-muted-foreground',
|
|
1213
1266
|
].join(' '), children: Icon ? _jsx(Icon, { className: "size-3", "aria-hidden": "true" }) : i + 1 }), _jsx("span", { className: "font-medium", children: String(s['label'] ?? `Step ${i + 1}`) })] }), i < steps.length - 1 && _jsx("span", { className: "h-px w-6 bg-border", "aria-hidden": "true" })] }, i));
|
|
1214
|
-
}) }), Boolean(steps[active]?.['description']) && (_jsx("p", { className: "text-sm text-muted-foreground", children: String(steps[active]['description']) })), steps.map((s, i) => (_jsx("div", { className: i === active ? 'flex flex-col gap-4' : 'hidden', "aria-hidden": i === active ? undefined : true, children: (s.children ?? []).map((c, ci) => renderElement(c, ci)) }, i))), advanceError && (_jsx("p", { className: "text-sm text-destructive", role: "alert", children: advanceError })), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(
|
|
1215
|
-
?
|
|
1216
|
-
|
|
1267
|
+
}) }), Boolean(steps[active]?.['description']) && (_jsx("p", { className: "text-sm text-muted-foreground", children: String(steps[active]['description']) })), steps.map((s, i) => (_jsx("div", { className: i === active ? 'flex flex-col gap-4' : 'hidden', "aria-hidden": i === active ? undefined : true, children: (s.children ?? []).map((c, ci) => renderElement(c, ci)) }, i))), advanceError && (_jsx("p", { className: "text-sm text-destructive", role: "alert", children: advanceError })), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(WizardNavButton, { actionMeta: previousActionMeta, fallbackLabel: "Back", disabled: isFirst || advancing, onClick: () => advance(active - 1) }), isLast
|
|
1268
|
+
? (submitActionMeta
|
|
1269
|
+
? _jsx(WizardNavButton, { actionMeta: submitActionMeta, fallbackLabel: "Submit", type: "submit", disabled: advancing })
|
|
1270
|
+
: _jsx("span", { className: "text-xs text-muted-foreground", children: "Submit the form to finish." }))
|
|
1271
|
+
: _jsx(WizardNavButton, { actionMeta: nextActionMeta, fallbackLabel: advancing ? 'Validating…' : 'Next', disabled: advancing, onClick: () => advance(active + 1) })] })] }, index));
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Renders one wizard nav slot (Back / Next / Submit). Falls back to plain
|
|
1275
|
+
* built-in chrome (border button for Back, primary button for Next/Submit)
|
|
1276
|
+
* when no `actionMeta` is supplied; otherwise reads the resolved Action's
|
|
1277
|
+
* chrome (`label / icon / color / size / outlined / iconOnly / tooltip /
|
|
1278
|
+
* disabled`) and applies it to a button whose click is hardwired by the
|
|
1279
|
+
* surrounding wizard. `type="submit"` lets the Submit slot trigger the
|
|
1280
|
+
* surrounding form's onSubmit dispatcher (no `onClick` needed).
|
|
1281
|
+
*
|
|
1282
|
+
* Hidden actions (`.visible(false)` resolved-away) drop the slot entirely
|
|
1283
|
+
* — the resolver returns `undefined` for hidden Action elements, which
|
|
1284
|
+
* arrives here as `actionMeta == null` so we fall through to the default
|
|
1285
|
+
* chrome. Use `Wizard.skippable()` semantics to hide nav buttons when
|
|
1286
|
+
* appropriate; for permanent removal subclass the wizard.
|
|
1287
|
+
*/
|
|
1288
|
+
function WizardNavButton({ actionMeta, fallbackLabel, type = 'button', disabled, onClick, }) {
|
|
1289
|
+
// Bare default — keep historical chrome for back-compat (un-customized
|
|
1290
|
+
// wizards look identical to before this change).
|
|
1291
|
+
if (!actionMeta) {
|
|
1292
|
+
const isPrimary = type === 'submit' || fallbackLabel !== 'Back';
|
|
1293
|
+
return (_jsx("button", { type: type, disabled: disabled, onClick: onClick, className: isPrimary
|
|
1294
|
+
? 'rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed'
|
|
1295
|
+
: 'rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed', children: fallbackLabel }));
|
|
1296
|
+
}
|
|
1297
|
+
const ownDisabled = Boolean(actionMeta['disabled']);
|
|
1298
|
+
const label = String(actionMeta['label'] ?? fallbackLabel);
|
|
1299
|
+
const tooltip = actionMeta['tooltip'] ? String(actionMeta['tooltip']) : undefined;
|
|
1300
|
+
const iconOnly = Boolean(actionMeta['iconOnly']);
|
|
1301
|
+
const className = actionButtonClass(actionMeta, {});
|
|
1302
|
+
const node = (_jsxs("button", { type: type, disabled: disabled || ownDisabled, onClick: onClick, className: `${className} disabled:opacity-50 disabled:cursor-not-allowed`, "aria-label": iconOnly ? label : undefined, children: [renderActionIcon(actionMeta), !iconOnly && _jsx("span", { children: label }), renderActionBadge(actionMeta)] }));
|
|
1303
|
+
return _jsx(_Fragment, { children: withTooltip(node, tooltip) });
|
|
1217
1304
|
}
|
|
1218
1305
|
// ─── Top-level dispatch ─────────────────────────────────────
|
|
1219
1306
|
const TEXT_COLOR_CLASSES = {
|
|
@@ -1965,6 +2052,7 @@ function buildTableQuery(state, override, pathname, filterValues = {}, prefix) {
|
|
|
1965
2052
|
prefixK(prefix, 'page'),
|
|
1966
2053
|
prefixK(prefix, 'perPage'),
|
|
1967
2054
|
prefixK(prefix, 'group'),
|
|
2055
|
+
prefixK(prefix, 'groupKey'),
|
|
1968
2056
|
...Object.keys(filterValues).map(n => prefixK(prefix, n)),
|
|
1969
2057
|
]);
|
|
1970
2058
|
for (const [k, v] of currentParams) {
|
|
@@ -1988,6 +2076,12 @@ function buildTableQuery(state, override, pathname, filterValues = {}, prefix) {
|
|
|
1988
2076
|
params.set(prefixK(prefix, 'page'), String(merged.page));
|
|
1989
2077
|
if (merged.group !== undefined)
|
|
1990
2078
|
params.set(prefixK(prefix, 'group'), merged.group);
|
|
2079
|
+
// groupKey is sparse — only writes when the override sets a non-empty
|
|
2080
|
+
// value. Drill-out (chip ×) passes `''` to clear; the foreign-param
|
|
2081
|
+
// dedupe set above already filtered the stale value out, so an empty
|
|
2082
|
+
// override produces a URL without the key.
|
|
2083
|
+
if (merged.groupKey)
|
|
2084
|
+
params.set(prefixK(prefix, 'groupKey'), merged.groupKey);
|
|
1991
2085
|
const qs = params.toString();
|
|
1992
2086
|
// Always anchor to a real pathname — Vike's client-side router treats
|
|
1993
2087
|
// a bare `?qs` href as a fresh URL with empty pathname, which routes
|
|
@@ -2687,6 +2781,43 @@ function RecordCellLink({ href, navigate, children, }) {
|
|
|
2687
2781
|
};
|
|
2688
2782
|
return (_jsx("a", { href: href, onClick: onClick, className: "block px-2 py-2 text-inherit no-underline hover:text-inherit focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded", children: children }));
|
|
2689
2783
|
}
|
|
2784
|
+
/**
|
|
2785
|
+
* "Drilled into <Label>: <Value>" chip above the table when a group
|
|
2786
|
+
* heading has been clicked. The × clears `?<prefix>groupKey=`, returning
|
|
2787
|
+
* the table to its banded view. Real `<a href>` with `useNavigate()`
|
|
2788
|
+
* intercept on plain left-click so cmd-click / middle-click open a
|
|
2789
|
+
* fresh tab (rare but valid for sharing the banded view URL).
|
|
2790
|
+
*/
|
|
2791
|
+
function ActiveGroupKeyChip({ label, value, displayValue, clearHref, navigate, }) {
|
|
2792
|
+
const onClick = (e) => {
|
|
2793
|
+
if (e.button !== 0)
|
|
2794
|
+
return;
|
|
2795
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
|
|
2796
|
+
return;
|
|
2797
|
+
e.preventDefault();
|
|
2798
|
+
void navigate(clearHref);
|
|
2799
|
+
};
|
|
2800
|
+
return (_jsxs("div", { className: "flex items-center gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Drilled into" }), _jsxs("span", { className: "font-medium text-foreground", children: [label ? `${label}: ` : '', displayValue || value] }), _jsx("a", { href: clearHref, onClick: onClick, "aria-label": "Clear drill-in", className: "ms-auto text-muted-foreground hover:text-foreground", children: "\u00D7" })] }));
|
|
2801
|
+
}
|
|
2802
|
+
/**
|
|
2803
|
+
* Group-heading text wrapped in a real `<a href>` that SPA-navs into the
|
|
2804
|
+
* drilled-in URL. Plain left-click intercepts for `useNavigate()`;
|
|
2805
|
+
* cmd/ctrl/shift-click + middle-click fall through to the browser so
|
|
2806
|
+
* "open in new tab" semantics work. Visually inherits the heading
|
|
2807
|
+
* styling — the link adds underline-on-hover affordance without
|
|
2808
|
+
* disturbing the surrounding text-transform / size.
|
|
2809
|
+
*/
|
|
2810
|
+
function GroupHeadingLink({ href, navigate, children, }) {
|
|
2811
|
+
const onClick = (e) => {
|
|
2812
|
+
if (e.button !== 0)
|
|
2813
|
+
return;
|
|
2814
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
|
|
2815
|
+
return;
|
|
2816
|
+
e.preventDefault();
|
|
2817
|
+
void navigate(href);
|
|
2818
|
+
};
|
|
2819
|
+
return (_jsx("a", { href: href, onClick: onClick, className: "inline-flex items-center gap-1 text-inherit no-underline hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded", children: children }));
|
|
2820
|
+
}
|
|
2690
2821
|
/**
|
|
2691
2822
|
* List-page tab strip — Filament-style query shortcuts above the table
|
|
2692
2823
|
* ("All / Drafts / Published / Archived"). Each trigger is a real `<a>`
|
|
@@ -2818,6 +2949,26 @@ function SortByPicker({ columns, active, onChange, }) {
|
|
|
2818
2949
|
return (_jsxs(React.Fragment, { children: [_jsxs(SelectItem, { value: `${name}:asc`, children: [label, " (A\u2192Z)"] }), _jsxs(SelectItem, { value: `${name}:desc`, children: [label, " (Z\u2192A)"] })] }, name));
|
|
2819
2950
|
}) })] }));
|
|
2820
2951
|
}
|
|
2952
|
+
/**
|
|
2953
|
+
* Toolbar dropdown for `Column.toggleable()` columns. Lists every
|
|
2954
|
+
* toggleable column with a checkbox; toggling writes through to a
|
|
2955
|
+
* caller-supplied `onToggle` (the `TableRendererBody` owns the state
|
|
2956
|
+
* + the localStorage round-trip). Mounted only when at least one
|
|
2957
|
+
* column is toggleable.
|
|
2958
|
+
*/
|
|
2959
|
+
function ColumnsToggleDropdown({ columns, hidden, onToggle, }) {
|
|
2960
|
+
if (columns.length === 0)
|
|
2961
|
+
return null;
|
|
2962
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: (props) => (_jsxs("button", { ...props, type: "button", className: "inline-flex h-9 items-center gap-1.5 rounded-md border border-input bg-background px-3 text-sm font-medium text-foreground hover:bg-accent", "aria-label": "Show or hide columns", children: [_jsx(Columns3Icon, { className: "h-4 w-4", "aria-hidden": "true" }), _jsx("span", { children: "Columns" })] })) }), _jsx(DropdownMenuContent, { align: "end", className: "min-w-[12rem]", children: columns.map((col, i) => {
|
|
2963
|
+
const name = String(col['name'] ?? '');
|
|
2964
|
+
const label = String(col['label'] ?? name);
|
|
2965
|
+
const isHidden = hidden.has(name);
|
|
2966
|
+
return (_jsxs(DropdownMenuItem, {
|
|
2967
|
+
// Suppress menu-close so users can toggle multiple columns
|
|
2968
|
+
// without re-opening the dropdown.
|
|
2969
|
+
closeOnClick: false, onClick: () => onToggle(name, !isHidden), children: [_jsx("span", { className: "inline-flex w-4 items-center justify-center", children: !isHidden && _jsx(CheckIcon, { className: "h-4 w-4", "aria-hidden": "true" }) }), _jsx("span", { children: label })] }, i));
|
|
2970
|
+
}) })] }));
|
|
2971
|
+
}
|
|
2821
2972
|
/**
|
|
2822
2973
|
* Lookup tables for responsive grid column-counts in `contentLayout:
|
|
2823
2974
|
* 'cards'`. Tailwind's JIT scanner needs **literal** class strings; we
|
|
@@ -3003,6 +3154,11 @@ function TableRendererBody({ el }) {
|
|
|
3003
3154
|
const navigate = useNavigate();
|
|
3004
3155
|
const children = el.children ?? [];
|
|
3005
3156
|
const columns = children.filter(c => c.type === 'column');
|
|
3157
|
+
// `Column.toggleable()` columns — sourced from the resolved meta. The
|
|
3158
|
+
// user's per-table visibility map is owned + persisted below; the full
|
|
3159
|
+
// `columns` list stays available for the toolbar dropdown so hidden
|
|
3160
|
+
// columns can be re-shown without a roundtrip.
|
|
3161
|
+
const toggleableColumns = columns.filter(c => c['toggleable'] !== undefined);
|
|
3006
3162
|
// Actions and ActionGroups share placement — both show up in the
|
|
3007
3163
|
// header/bulk/row toolbars depending on their `placement` field.
|
|
3008
3164
|
const actionLike = children.filter(c => c.type === 'action' || c.type === 'actionGroup' || c.type === 'slotComponent');
|
|
@@ -3011,6 +3167,7 @@ function TableRendererBody({ el }) {
|
|
|
3011
3167
|
const hasRecordClasses = Boolean(el['recordClasses']);
|
|
3012
3168
|
const pollInterval = typeof el['pollInterval'] === 'number' ? el['pollInterval'] : undefined;
|
|
3013
3169
|
const defaultGroup = typeof el['defaultGroup'] === 'string' ? el['defaultGroup'] : undefined;
|
|
3170
|
+
const activeGroupKey = typeof el['activeGroupKey'] === 'string' ? el['activeGroupKey'] : undefined;
|
|
3014
3171
|
const summaries = el['summaries'];
|
|
3015
3172
|
const groupSummaries = el['groupSummaries'];
|
|
3016
3173
|
const groupOptions = el['groups'] ?? [];
|
|
@@ -3026,6 +3183,11 @@ function TableRendererBody({ el }) {
|
|
|
3026
3183
|
})
|
|
3027
3184
|
: undefined;
|
|
3028
3185
|
const groupColumnLabel = activeGroupMeta?.label;
|
|
3186
|
+
// Heading text becomes a real `<a href>` when the active group opts in
|
|
3187
|
+
// via `.scopable()`. Synthesized bare-column groups can't be scopable
|
|
3188
|
+
// (no builder call ran).
|
|
3189
|
+
const groupHeadingScopable = activeGroupMeta !== undefined
|
|
3190
|
+
&& activeGroupMeta.scopable === true;
|
|
3029
3191
|
// Auto-refresh: re-visit current URL on a timer so sort/filter/pagination
|
|
3030
3192
|
// state survives. Pause while the document is hidden — background tabs
|
|
3031
3193
|
// shouldn't keep hammering the server.
|
|
@@ -3074,6 +3236,67 @@ function TableRendererBody({ el }) {
|
|
|
3074
3236
|
const perPage = el['perPage'];
|
|
3075
3237
|
const searchable = Boolean(el['searchable']);
|
|
3076
3238
|
const currentPath = el['currentPath'] ?? '';
|
|
3239
|
+
// `Column.toggleable()` user-visibility map. Persisted per-table at
|
|
3240
|
+
// `pilotiq.table.<currentPath>.columns.<name>` ('1' = hidden,
|
|
3241
|
+
// '0' = visible). On first paint, fall back to `meta.toggleable.initiallyHidden`.
|
|
3242
|
+
// SSR returns the meta default — the localStorage hydrate happens
|
|
3243
|
+
// inside the effect so server + first client render match.
|
|
3244
|
+
const columnsVisibilityKey = (name) => `pilotiq.table.${currentPath}.columns.${name}`;
|
|
3245
|
+
const initialHidden = () => {
|
|
3246
|
+
const out = new Set();
|
|
3247
|
+
for (const col of toggleableColumns) {
|
|
3248
|
+
const cfg = col['toggleable'];
|
|
3249
|
+
if (cfg?.initiallyHidden)
|
|
3250
|
+
out.add(String(col['name']));
|
|
3251
|
+
}
|
|
3252
|
+
return out;
|
|
3253
|
+
};
|
|
3254
|
+
const [hiddenColumns, setHiddenColumns] = useState(initialHidden);
|
|
3255
|
+
useEffect(() => {
|
|
3256
|
+
if (typeof window === 'undefined')
|
|
3257
|
+
return;
|
|
3258
|
+
if (toggleableColumns.length === 0)
|
|
3259
|
+
return;
|
|
3260
|
+
const next = new Set();
|
|
3261
|
+
for (const col of toggleableColumns) {
|
|
3262
|
+
const name = String(col['name']);
|
|
3263
|
+
const cfg = col['toggleable'];
|
|
3264
|
+
try {
|
|
3265
|
+
const stored = window.localStorage.getItem(columnsVisibilityKey(name));
|
|
3266
|
+
if (stored === '1')
|
|
3267
|
+
next.add(name);
|
|
3268
|
+
else if (stored === '0') { /* visible */ }
|
|
3269
|
+
else if (cfg?.initiallyHidden)
|
|
3270
|
+
next.add(name);
|
|
3271
|
+
}
|
|
3272
|
+
catch {
|
|
3273
|
+
if (cfg?.initiallyHidden)
|
|
3274
|
+
next.add(name);
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
setHiddenColumns(next);
|
|
3278
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3279
|
+
}, [currentPath, toggleableColumns.length]);
|
|
3280
|
+
const toggleColumnHidden = (name, nextHidden) => {
|
|
3281
|
+
setHiddenColumns(prev => {
|
|
3282
|
+
const next = new Set(prev);
|
|
3283
|
+
if (nextHidden)
|
|
3284
|
+
next.add(name);
|
|
3285
|
+
else
|
|
3286
|
+
next.delete(name);
|
|
3287
|
+
if (typeof window !== 'undefined') {
|
|
3288
|
+
try {
|
|
3289
|
+
window.localStorage.setItem(columnsVisibilityKey(name), nextHidden ? '1' : '0');
|
|
3290
|
+
}
|
|
3291
|
+
catch { /* private mode / quota — silent */ }
|
|
3292
|
+
}
|
|
3293
|
+
return next;
|
|
3294
|
+
});
|
|
3295
|
+
};
|
|
3296
|
+
// Filtered column list used by every render path (header, body cells,
|
|
3297
|
+
// group + footer summaries, empty-state colSpan). Non-toggleable
|
|
3298
|
+
// columns always survive.
|
|
3299
|
+
const visibleColumns = columns.filter(c => !hiddenColumns.has(String(c['name'])));
|
|
3077
3300
|
// Tier-3 — when the table opts into `Table.queryStringIdentifier(...)`,
|
|
3078
3301
|
// every URL key (search / sort / page / perPage / group / filter names)
|
|
3079
3302
|
// gets prefixed with `${id}_` so multiple tables on one page don't
|
|
@@ -3156,6 +3379,7 @@ function TableRendererBody({ el }) {
|
|
|
3156
3379
|
...(urlGroup !== undefined ? { group: urlGroup }
|
|
3157
3380
|
: defaultGroup !== undefined ? { group: defaultGroup }
|
|
3158
3381
|
: {}),
|
|
3382
|
+
...(activeGroupKey !== undefined ? { groupKey: activeGroupKey } : {}),
|
|
3159
3383
|
};
|
|
3160
3384
|
// Snapshot active filter values for sort/pagination href construction.
|
|
3161
3385
|
// Filter form submits already carry these (selects are inside the
|
|
@@ -3166,6 +3390,12 @@ function TableRendererBody({ el }) {
|
|
|
3166
3390
|
if (typeof v === 'string' && v !== '')
|
|
3167
3391
|
activeFilters[String(f['name'])] = v;
|
|
3168
3392
|
}
|
|
3393
|
+
// Drill-in / drill-out URL builders for the group heading link and the
|
|
3394
|
+
// active-key chip's clear button. Drill-in sets `?<prefix>groupKey=v`
|
|
3395
|
+
// and resets `page`; drill-out clears it. Both round-trip foreign
|
|
3396
|
+
// params (other tables' state) through `buildTableQuery`.
|
|
3397
|
+
const buildGroupKeyHref = (value) => buildTableQuery(state, { groupKey: value, page: 1 }, currentPath, activeFilters, queryPrefix);
|
|
3398
|
+
const drillOutHref = () => buildTableQuery(state, { groupKey: '', page: 1 }, currentPath, activeFilters, queryPrefix);
|
|
3169
3399
|
// Track which row ids are currently checked. Keyed by id (string), not
|
|
3170
3400
|
// by index, so pagination and re-renders don't drop selection state.
|
|
3171
3401
|
const [selected, setSelected] = useState(() => new Set());
|
|
@@ -3328,7 +3558,8 @@ function TableRendererBody({ el }) {
|
|
|
3328
3558
|
// Only modal + collapsible mount a toolbar widget; the always-visible
|
|
3329
3559
|
// strip modes don't add anything to the header bar.
|
|
3330
3560
|
const showFiltersInToolbar = hasFilters && (filtersInModal || filtersCollapsible);
|
|
3331
|
-
const
|
|
3561
|
+
const hasColumnsToggle = toggleableColumns.length > 0;
|
|
3562
|
+
const showHeaderBar = searchable || headerActions.length > 0 || showFiltersInToolbar || hasGroupPicker || hasSortPicker || hasColumnsToggle;
|
|
3332
3563
|
const hasBulkActions = bulkActions.length > 0;
|
|
3333
3564
|
const hasRowActions = rowActions.length > 0;
|
|
3334
3565
|
// Drag-to-reorder is enabled only when the visible rows ARE the
|
|
@@ -3348,7 +3579,7 @@ function TableRendererBody({ el }) {
|
|
|
3348
3579
|
!searchActive &&
|
|
3349
3580
|
currentPage === 1;
|
|
3350
3581
|
const reorderColumnVisible = reorderableColumn !== undefined;
|
|
3351
|
-
const totalCols =
|
|
3582
|
+
const totalCols = visibleColumns.length
|
|
3352
3583
|
+ (hasBulkActions ? 1 : 0)
|
|
3353
3584
|
+ (hasRowActions ? 1 : 0)
|
|
3354
3585
|
+ (reorderColumnVisible ? 1 : 0);
|
|
@@ -3365,7 +3596,7 @@ function TableRendererBody({ el }) {
|
|
|
3365
3596
|
// pre-2026-05-04 behavior for tables that haven't opted in.
|
|
3366
3597
|
const activeEmpty = (hasFilterOrSearch && filteredEmptyState) ? filteredEmptyState : emptyState;
|
|
3367
3598
|
const EmptyIcon = activeEmpty?.icon ? (resolveIcon(activeEmpty.icon) ?? InboxIcon) : InboxIcon;
|
|
3368
|
-
return (_jsxs("div", { className: "flex flex-col gap-3", children: [(tableHeading || tableDescription) && (_jsxs("div", { className: "flex flex-col gap-1", children: [tableHeading && _jsx("h2", { className: "text-lg font-semibold", children: tableHeading }), tableDescription && _jsx("p", { className: "text-sm text-muted-foreground", children: tableDescription })] })), showHeaderBar && (_jsxs("div", { className: "flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-between", children: [(searchable || showFiltersInToolbar || hasGroupPicker || hasSortPicker) ? (_jsxs("div", { className: "flex items-center gap-2", children: [searchable && (_jsxs("form", { method: "get", action: currentPath || undefined, className: "flex items-end gap-2", children: [_jsx(SearchFormHiddenInputs, { prefix: queryPrefix }), _jsx(Input, { type: "search", name: prefixK(queryPrefix, 'search'), defaultValue: search ?? '', placeholder: "Search\u2026", className: "h-9 w-64" }), _jsx("button", { type: "submit", className: "sr-only", tabIndex: -1, "aria-hidden": "true", children: "Apply" })] })), hasFilters && filtersInModal && (_jsx(FilterPopover, { filters: filters, prefix: queryPrefix })), hasFilters && filtersCollapsible && (_jsx(FilterStripToggle, { filters: filters, open: filtersOpen, onToggle: toggleFiltersOpen })), hasGroupPicker && (_jsx(TableGroupPicker, { options: groupOptions, active: defaultGroup, onChange: (value) => {
|
|
3599
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [(tableHeading || tableDescription) && (_jsxs("div", { className: "flex flex-col gap-1", children: [tableHeading && _jsx("h2", { className: "text-lg font-semibold", children: tableHeading }), tableDescription && _jsx("p", { className: "text-sm text-muted-foreground", children: tableDescription })] })), showHeaderBar && (_jsxs("div", { className: "flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-between", children: [(searchable || showFiltersInToolbar || hasGroupPicker || hasSortPicker || hasColumnsToggle) ? (_jsxs("div", { className: "flex items-center gap-2", children: [searchable && (_jsxs("form", { method: "get", action: currentPath || undefined, className: "flex items-end gap-2", children: [_jsx(SearchFormHiddenInputs, { prefix: queryPrefix }), _jsx(Input, { type: "search", name: prefixK(queryPrefix, 'search'), defaultValue: search ?? '', placeholder: "Search\u2026", className: "h-9 w-64" }), _jsx("button", { type: "submit", className: "sr-only", tabIndex: -1, "aria-hidden": "true", children: "Apply" })] })), hasFilters && filtersInModal && (_jsx(FilterPopover, { filters: filters, prefix: queryPrefix })), hasFilters && filtersCollapsible && (_jsx(FilterStripToggle, { filters: filters, open: filtersOpen, onToggle: toggleFiltersOpen })), hasGroupPicker && (_jsx(TableGroupPicker, { options: groupOptions, active: defaultGroup, onChange: (value) => {
|
|
3369
3600
|
// value === '' → explicit "None" (clears defaultGroup);
|
|
3370
3601
|
// value !== '' → switch to that column.
|
|
3371
3602
|
const href = buildTableQuery(state, { page: 1, group: value }, currentPath, activeFilters, queryPrefix);
|
|
@@ -3373,7 +3604,23 @@ function TableRendererBody({ el }) {
|
|
|
3373
3604
|
} })), hasSortPicker && (_jsx(SortByPicker, { columns: sortableColumns, active: currentSort, onChange: (column, direction) => {
|
|
3374
3605
|
const href = buildTableQuery(state, { sort: { column, direction }, page: 1 }, currentPath, activeFilters, queryPrefix);
|
|
3375
3606
|
navigate(href);
|
|
3376
|
-
} }))] })) : _jsx("span", {}), headerActions.length > 0 && (_jsx("div", { className: "flex items-center gap-2", children: headerActions.map((a, i) => renderActionLike(a, i)) }))] })), hasFilters && filtersInModal && _jsx(ActiveFiltersBar, { filters: filters, prefix: queryPrefix }), hasFilters && filtersAbove && filtersOpen && (_jsx(FilterStrip, { filters: filters, prefix: queryPrefix })),
|
|
3607
|
+
} })), toggleableColumns.length > 0 && (_jsx(ColumnsToggleDropdown, { columns: toggleableColumns, hidden: hiddenColumns, onToggle: toggleColumnHidden }))] })) : _jsx("span", {}), headerActions.length > 0 && (_jsx("div", { className: "flex items-center gap-2", children: headerActions.map((a, i) => renderActionLike(a, i)) }))] })), hasFilters && filtersInModal && _jsx(ActiveFiltersBar, { filters: filters, prefix: queryPrefix }), hasFilters && filtersAbove && filtersOpen && (_jsx(FilterStrip, { filters: filters, prefix: queryPrefix })), activeGroupKey !== undefined && (_jsx(ActiveGroupKeyChip, { label: groupColumnLabel ?? defaultGroup ?? '', value: activeGroupKey, displayValue: (() => {
|
|
3608
|
+
// Prefer a row-resolved `_groupTitle` (server stamped via
|
|
3609
|
+
// `getTitleFromRecordUsing`) so the chip reads the same as
|
|
3610
|
+
// a banded heading. Falls back to the raw bucket key when
|
|
3611
|
+
// no row matched — empty drilled-in pages still show what
|
|
3612
|
+
// they're drilled into.
|
|
3613
|
+
for (const r of rows) {
|
|
3614
|
+
const obj = r;
|
|
3615
|
+
if (String(obj['_groupValue'] ?? '') !== activeGroupKey)
|
|
3616
|
+
continue;
|
|
3617
|
+
const t = obj['_groupTitle'];
|
|
3618
|
+
if (typeof t === 'string' && t !== '')
|
|
3619
|
+
return t;
|
|
3620
|
+
break;
|
|
3621
|
+
}
|
|
3622
|
+
return activeGroupKey;
|
|
3623
|
+
})(), clearHref: drillOutHref(), navigate: navigate })), hasBulkActions && someChecked && (_jsxs("div", { className: "flex items-center justify-between gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm", children: [_jsxs("span", { className: "text-muted-foreground", children: [selected.size, " selected"] }), _jsxs("div", { className: "flex items-center gap-2", children: [bulkActions.map((a, i) => renderActionLike(a, i, { ids: Array.from(selected) })), _jsx("button", { type: "button", onClick: () => setSelected(new Set()), className: "text-xs text-muted-foreground hover:text-foreground", children: "Clear" })] })] })), isCardsLayout ? (_jsx(CardsLayoutBody, { el: el, columns: columns, rows: rows, visibleIds: visibleIds, selected: selected, toggleRow: toggleRow, hasBulkActions: hasBulkActions, hasRowActions: hasRowActions, rowActions: rowActions, hasRecordUrl: hasRecordUrl, hasRecordClasses: hasRecordClasses, striped: striped, activeEmpty: activeEmpty, EmptyIcon: EmptyIcon, hasFilterOrSearch: hasFilterOrSearch, defaultGroup: defaultGroup, groupColumnLabel: groupColumnLabel, groupCollapsible: groupCollapsible, collapsedGroups: collapsedGroups, toggleGroupCollapsed: toggleGroupCollapsed, cardsPerRow: cardsPerRow, navigate: navigate, groupHeadingScopable: groupHeadingScopable, buildGroupKeyHref: buildGroupKeyHref })) : (_jsx("div", { className: "rounded-xl border bg-card overflow-hidden", children: _jsxs(DataTable, { children: [_jsx(TableHeader, { className: "bg-muted", children: _jsxs(TableRow, { children: [reorderColumnVisible && (_jsx(TableHead, { className: "w-9 px-2", "aria-label": "Reorder" })), hasBulkActions && (_jsx(TableHead, { className: "w-9 px-3", children: _jsx(Checkbox, { "aria-label": "Select all rows", checked: allChecked, onCheckedChange: () => toggleAll() }) })), visibleColumns.map((col, i) => {
|
|
3377
3624
|
const name = String(col['name'] ?? '');
|
|
3378
3625
|
const label = String(col['label'] ?? name);
|
|
3379
3626
|
const sortable = Boolean(col['sortable']);
|
|
@@ -3428,16 +3675,28 @@ function TableRendererBody({ el }) {
|
|
|
3428
3675
|
.filter(Boolean)
|
|
3429
3676
|
.join(' ')
|
|
3430
3677
|
.trim();
|
|
3431
|
-
return (_jsxs(React.Fragment, { children: [showGroupHeader && (_jsx(TableRow, { className: "bg-muted/40 hover:bg-muted/40", children: _jsx(TableCell, { colSpan: totalCols, className: "px-3 py-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground", children:
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3678
|
+
return (_jsxs(React.Fragment, { children: [showGroupHeader && (_jsx(TableRow, { className: "bg-muted/40 hover:bg-muted/40", children: _jsx(TableCell, { colSpan: totalCols, className: "px-3 py-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: (() => {
|
|
3679
|
+
const drillable = groupHeadingScopable
|
|
3680
|
+
&& groupValue !== undefined
|
|
3681
|
+
&& groupValue !== '';
|
|
3682
|
+
const headingText = (_jsx(GroupHeaderText, { label: groupColumnLabel, value: groupValue, title: groupTitle, description: groupDescription }));
|
|
3683
|
+
const headingNode = drillable
|
|
3684
|
+
? _jsx(GroupHeadingLink, { href: buildGroupKeyHref(groupValue), navigate: navigate, children: headingText })
|
|
3685
|
+
: headingText;
|
|
3686
|
+
if (groupCollapsible) {
|
|
3687
|
+
return (_jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx("button", { type: "button", className: "inline-flex items-center", onClick: () => toggleGroupCollapsed(groupValue), "aria-expanded": !isInCollapsedGroup, "aria-label": isInCollapsedGroup ? 'Expand group' : 'Collapse group', children: _jsx(ChevronDownIcon, { className: [
|
|
3688
|
+
'size-4 transition-transform',
|
|
3689
|
+
isInCollapsedGroup ? '-rotate-90' : '',
|
|
3690
|
+
].filter(Boolean).join(' ') }) }), headingNode] }));
|
|
3691
|
+
}
|
|
3692
|
+
return headingNode;
|
|
3693
|
+
})() }) }, `group-${id}`)), isInCollapsedGroup ? null : (_jsxs(TableRow, { "data-state": isSelected ? 'selected' : undefined, className: [
|
|
3435
3694
|
rowClassName,
|
|
3436
3695
|
dragId === id ? 'opacity-50' : '',
|
|
3437
3696
|
dropAt === ri && dragId !== null ? 'border-t-2 border-t-primary' : '',
|
|
3438
3697
|
].filter(Boolean).join(' ') || undefined, draggable: reorderEnabled || undefined, onDragStart: reorderEnabled ? onRowDragStart(id) : undefined, onDragOver: reorderEnabled ? onRowDragOver(ri) : undefined, onDrop: reorderEnabled ? onRowDrop : undefined, onDragEnd: reorderEnabled ? onRowDragEnd : undefined, children: [reorderColumnVisible && (_jsx(TableCell, { className: "w-9 px-2", children: _jsx("span", { "aria-label": reorderEnabled ? 'Drag to reorder' : 'Reorder paused — clear filters and sort to enable', className: reorderEnabled
|
|
3439
3698
|
? 'inline-flex cursor-grab text-muted-foreground hover:text-foreground active:cursor-grabbing'
|
|
3440
|
-
: 'inline-flex cursor-not-allowed text-muted-foreground/40', children: _jsx(GripVerticalIcon, { className: "size-4" }) }) })), hasBulkActions && (_jsx(TableCell, { className: "w-9 px-3", children: _jsx(Checkbox, { "aria-label": `Select row ${id}`, checked: isSelected, onCheckedChange: () => toggleRow(id) }) })),
|
|
3699
|
+
: 'inline-flex cursor-not-allowed text-muted-foreground/40', children: _jsx(GripVerticalIcon, { className: "size-4" }) }) })), hasBulkActions && (_jsx(TableCell, { className: "w-9 px-3", children: _jsx(Checkbox, { "aria-label": `Select row ${id}`, checked: isSelected, onCheckedChange: () => toggleRow(id) }) })), visibleColumns.map((col, ci) => {
|
|
3441
3700
|
const name = String(col['name'] ?? '');
|
|
3442
3701
|
const value = recordObj[name];
|
|
3443
3702
|
const align = col['alignment'] === 'center' ? 'text-center'
|
|
@@ -3459,7 +3718,9 @@ function TableRendererBody({ el }) {
|
|
|
3459
3718
|
: null;
|
|
3460
3719
|
if (EditableComp && editUrl !== undefined) {
|
|
3461
3720
|
const cellDisabled = col['disabled'] === true || cellDisabledMap?.[name] === true;
|
|
3462
|
-
|
|
3721
|
+
const cellSelectOptionsMap = recordObj['_cellSelectOptions'];
|
|
3722
|
+
const rowOptions = cellSelectOptionsMap?.[name];
|
|
3723
|
+
return (_jsx(TableCell, { className: `text-sm text-foreground ${align} p-0`, style: widthStyle, children: _jsx(EditableComp, { url: editUrl, col: col, value: value, disabled: cellDisabled, ...(rowOptions ? { rowOptions } : {}) }) }, ci));
|
|
3463
3724
|
}
|
|
3464
3725
|
const cellContent = formatCell(value, col, recordObj);
|
|
3465
3726
|
const colUrl = resolveColumnUrl(col, tableUrl, colUrls);
|
|
@@ -3480,7 +3741,7 @@ function TableRendererBody({ el }) {
|
|
|
3480
3741
|
const perCol = groupSummaries[groupValue];
|
|
3481
3742
|
if (!perCol || Object.keys(perCol).length === 0)
|
|
3482
3743
|
return null;
|
|
3483
|
-
return (_jsxs(TableRow, { className: "bg-muted/20 hover:bg-muted/20", children: [reorderColumnVisible && _jsx(TableCell, {}), hasBulkActions && _jsx(TableCell, {}),
|
|
3744
|
+
return (_jsxs(TableRow, { className: "bg-muted/20 hover:bg-muted/20", children: [reorderColumnVisible && _jsx(TableCell, {}), hasBulkActions && _jsx(TableCell, {}), visibleColumns.map((col, ci) => {
|
|
3484
3745
|
const name = String(col['name'] ?? '');
|
|
3485
3746
|
const align = col['alignment'] === 'center' ? 'text-center'
|
|
3486
3747
|
: col['alignment'] === 'end' ? 'text-right'
|
|
@@ -3489,7 +3750,7 @@ function TableRendererBody({ el }) {
|
|
|
3489
3750
|
return (_jsx(TableCell, { className: `text-xs font-medium ${align} px-2 py-1.5`, children: items?.map((s, i) => (_jsxs("div", { className: "leading-tight", children: [s.label && _jsxs("span", { className: "text-muted-foreground", children: [s.label, ": "] }), _jsx("span", { children: s.value })] }, i))) }, ci));
|
|
3490
3751
|
}), hasRowActions && _jsx(TableCell, {})] }, `group-summary-${id}`));
|
|
3491
3752
|
})()] }, id));
|
|
3492
|
-
}) }), summaries && Object.keys(summaries).length > 0 && (_jsx(TableFooter, { children: _jsxs(TableRow, { children: [reorderColumnVisible && _jsx(TableCell, {}), hasBulkActions && _jsx(TableCell, {}),
|
|
3753
|
+
}) }), summaries && Object.keys(summaries).length > 0 && (_jsx(TableFooter, { children: _jsxs(TableRow, { children: [reorderColumnVisible && _jsx(TableCell, {}), hasBulkActions && _jsx(TableCell, {}), visibleColumns.map((col, ci) => {
|
|
3493
3754
|
const name = String(col['name'] ?? '');
|
|
3494
3755
|
const align = col['alignment'] === 'center' ? 'text-center'
|
|
3495
3756
|
: col['alignment'] === 'end' ? 'text-right'
|
|
@@ -3516,7 +3777,7 @@ function TableRendererBody({ el }) {
|
|
|
3516
3777
|
* configured per-card grid (`cardsPerRow`) re-applies inside every
|
|
3517
3778
|
* section so the column count stays consistent.
|
|
3518
3779
|
*/
|
|
3519
|
-
function CardsLayoutBody({ el, columns, rows, visibleIds, selected, toggleRow, hasBulkActions, hasRowActions, rowActions, hasRecordUrl, hasRecordClasses, striped, activeEmpty, EmptyIcon, hasFilterOrSearch, defaultGroup, groupColumnLabel, groupCollapsible, collapsedGroups, toggleGroupCollapsed, cardsPerRow, navigate, }) {
|
|
3780
|
+
function CardsLayoutBody({ el, columns, rows, visibleIds, selected, toggleRow, hasBulkActions, hasRowActions, rowActions, hasRecordUrl, hasRecordClasses, striped, activeEmpty, EmptyIcon, hasFilterOrSearch, defaultGroup, groupColumnLabel, groupCollapsible, collapsedGroups, toggleGroupCollapsed, cardsPerRow, navigate, groupHeadingScopable, buildGroupKeyHref, }) {
|
|
3520
3781
|
void el; // keep prop for future telemetry; silences unused-prop lint
|
|
3521
3782
|
void columns;
|
|
3522
3783
|
void striped; // visual stripes don't apply to cards (each card has its own surface)
|
|
@@ -3549,10 +3810,22 @@ function CardsLayoutBody({ el, columns, rows, visibleIds, selected, toggleRow, h
|
|
|
3549
3810
|
const collapsed = groupCollapsible
|
|
3550
3811
|
&& section.groupValue !== undefined
|
|
3551
3812
|
&& collapsedGroups[section.groupValue] === true;
|
|
3552
|
-
return (_jsxs("div", { className: "flex flex-col gap-3", children: [section.groupValue !== undefined && (
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3813
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [section.groupValue !== undefined && (() => {
|
|
3814
|
+
const drillable = groupHeadingScopable === true
|
|
3815
|
+
&& buildGroupKeyHref !== undefined
|
|
3816
|
+
&& section.groupValue !== '';
|
|
3817
|
+
const headingText = (_jsx(GroupHeaderText, { label: groupColumnLabel, value: section.groupValue, title: section.title, description: section.description }));
|
|
3818
|
+
const headingNode = drillable
|
|
3819
|
+
? _jsx(GroupHeadingLink, { href: buildGroupKeyHref(section.groupValue), navigate: navigate, children: headingText })
|
|
3820
|
+
: headingText;
|
|
3821
|
+
if (groupCollapsible) {
|
|
3822
|
+
return (_jsxs("div", { className: "flex w-full items-center gap-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: [_jsx("button", { type: "button", className: "inline-flex items-center", onClick: () => toggleGroupCollapsed(section.groupValue), "aria-expanded": !collapsed, "aria-label": collapsed ? 'Expand group' : 'Collapse group', children: _jsx(ChevronDownIcon, { className: [
|
|
3823
|
+
'size-4 transition-transform',
|
|
3824
|
+
collapsed ? '-rotate-90' : '',
|
|
3825
|
+
].filter(Boolean).join(' ') }) }), headingNode] }));
|
|
3826
|
+
}
|
|
3827
|
+
return (_jsx("div", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: headingNode }));
|
|
3828
|
+
})(), !collapsed && (_jsx("div", { className: gridClass, children: section.indices.map((ri) => {
|
|
3556
3829
|
const id = visibleIds[ri];
|
|
3557
3830
|
const recordObj = rows[ri];
|
|
3558
3831
|
const isSelected = selected.has(id);
|