@viliha/vui-ui 1.8.0 → 1.9.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/AGENT.md +2 -0
- package/CHANGELOG.md +19 -0
- package/package.json +1 -1
- package/src/record-view.tsx +45 -21
- package/template/app/(app)/system/regions/regions-table.tsx +3 -0
package/AGENT.md
CHANGED
|
@@ -334,6 +334,8 @@ Build forms with VUI and shadcn/ui together, reaching for shadcn Form, React Hoo
|
|
|
334
334
|
|
|
335
335
|
Never hand-build an HTML table; always use `RecordView`. Configure columns through its field props (`editable`, `required`, `copyable`, `options`, `render`), and let `RecordView` own the rest: sorting, filtering, pagination, bulk actions, and import/export.
|
|
336
336
|
|
|
337
|
+
Sorting follows column visibility by default; use `sortable` to decouple them — `sortable: true` sorts a field with no column (e.g. a `hideInTable` name shown via `getPrimary`), `sortable: false` locks a visible column. Set the flag; don't rewire the Sort dropdown or headers.
|
|
338
|
+
|
|
337
339
|
## Filtering
|
|
338
340
|
|
|
339
341
|
The Filter panel is a single keyword box by default (built in, matches all fields). For a labeled control per field, set `filterable` on the relevant fields and never hand-roll a filter form: `filterable: true` (text) or `filterable: { control, label, placeholder, options }` where `control` is `"text" | "number" | "date" | "select" | "combobox" | "checkbox"`. When any field is filterable the panel shows a control per field plus Search / Clear. It only **collects** values — handle `onFilter(values)` on `RecordView` to run a query or client filter (Search and Clear both call it). Types: `FilterControl`, `FieldFilter`, `FilterValues<T>`. Need a control kind that isn't listed? Extend the `FilterControl` union in the component.
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ backward-compatible features, **major** for breaking changes.
|
|
|
7
7
|
|
|
8
8
|
To upgrade, see [Upgrading](./AGENT.md#upgrading) in the agent guide.
|
|
9
9
|
|
|
10
|
+
## 1.9.0 — 2026-07-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`sortable` field flag in `RecordView`** — decouples sorting from column
|
|
15
|
+
visibility. By default a field is sortable iff it's a visible column
|
|
16
|
+
(`!hideInTable`), unchanged. Now:
|
|
17
|
+
- `sortable: true` sorts a field with **no column** (e.g. a `hideInTable` name
|
|
18
|
+
shown via `getPrimary`) — it appears in the Sort dropdown.
|
|
19
|
+
- `sortable: false` keeps a **visible column unsortable** (its header stops
|
|
20
|
+
being a sort toggle).
|
|
21
|
+
|
|
22
|
+
Affects both the Sort dropdown and the column-header click. Fully backward
|
|
23
|
+
compatible — omit the flag for today's behavior.
|
|
24
|
+
|
|
25
|
+
**For agents:** to make a non-column field sortable (or lock a column), set
|
|
26
|
+
`sortable` on the `RecordField`; don't rewire the Sort dropdown or headers.
|
|
27
|
+
Demo: `system/regions` sorts by Name (which has no column).
|
|
28
|
+
|
|
10
29
|
## 1.8.0 — 2026-07-26
|
|
11
30
|
|
|
12
31
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viliha/vui-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Vui UI — a clean, token-driven React admin/CRM component library built on Tailwind CSS v4, shadcn-style patterns, and Radix Icons. Ships as TypeScript source (Just-in-Time), compiled by the consuming app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Suman Bonakurthi",
|
package/src/record-view.tsx
CHANGED
|
@@ -226,6 +226,12 @@ export interface RecordField<T> {
|
|
|
226
226
|
copyable?: boolean;
|
|
227
227
|
/** Show in the detail panel only, not as a table column (e.g. first/last name). */
|
|
228
228
|
hideInTable?: boolean;
|
|
229
|
+
/** Whether this field can be sorted — decoupled from column visibility.
|
|
230
|
+
* Default: sortable iff it's a visible column (`!hideInTable`), the historic
|
|
231
|
+
* behavior. Set `true` to sort a field with no column (e.g. a `hideInTable`
|
|
232
|
+
* name shown via `getPrimary`); set `false` to keep a visible column
|
|
233
|
+
* unsortable. Controls both the Sort dropdown and the column-header toggle. */
|
|
234
|
+
sortable?: boolean;
|
|
229
235
|
/** Custom, non-editable cell/value renderer. */
|
|
230
236
|
render?: (row: T) => React.ReactNode;
|
|
231
237
|
/** If set, the field becomes a choice field: the Add/Edit form renders a
|
|
@@ -460,6 +466,12 @@ export function RecordView<T extends { id: RowId }>({
|
|
|
460
466
|
|
|
461
467
|
const tableFields = fields.filter((f) => !f.hideInTable);
|
|
462
468
|
const visibleFields = tableFields.filter((f) => !hidden.has(f.key));
|
|
469
|
+
// Sorting is decoupled from column visibility: a field is sortable when its
|
|
470
|
+
// `sortable` flag says so, else it falls back to "is a visible column".
|
|
471
|
+
const canSort = (f: RecordField<T>) => f.sortable ?? !f.hideInTable;
|
|
472
|
+
// Fields offered in the Sort dropdown (may include hidden-but-sortable fields
|
|
473
|
+
// and exclude visible-but-unsortable ones).
|
|
474
|
+
const sortFields = fields.filter(canSort);
|
|
463
475
|
// Fields opted into per-field filtering. Non-empty → the Filter panel renders
|
|
464
476
|
// a control per field instead of the single keyword box.
|
|
465
477
|
const filterFields = fields.filter((f) => f.filterable);
|
|
@@ -1182,7 +1194,7 @@ export function RecordView<T extends { id: RowId }>({
|
|
|
1182
1194
|
|
|
1183
1195
|
<Dropdown label="Sort" icon={<ArrowUpDown className="size-3.5 text-blue-500" />}>
|
|
1184
1196
|
<DropdownLabel>Sort by</DropdownLabel>
|
|
1185
|
-
{
|
|
1197
|
+
{sortFields.map((f) => (
|
|
1186
1198
|
<DropdownItem
|
|
1187
1199
|
key={f.key}
|
|
1188
1200
|
onSelect={() => toggleSort(f.key)}
|
|
@@ -1300,32 +1312,44 @@ export function RecordView<T extends { id: RowId }>({
|
|
|
1300
1312
|
</TableHead>
|
|
1301
1313
|
{visibleFields.map((f) => {
|
|
1302
1314
|
const HeadIcon = f.icon ?? DEFAULT_FIELD_ICON;
|
|
1315
|
+
const sortable = canSort(f);
|
|
1316
|
+
const headInner = (
|
|
1317
|
+
<>
|
|
1318
|
+
<HeadIcon className="size-3.5 shrink-0 text-foreground" />
|
|
1319
|
+
<span className="flex items-center gap-1 whitespace-nowrap">
|
|
1320
|
+
{f.label}
|
|
1321
|
+
{f.required && <RequiredMark />}
|
|
1322
|
+
</span>
|
|
1323
|
+
{sort?.key === f.key &&
|
|
1324
|
+
(sort.dir === "asc" ? (
|
|
1325
|
+
<ArrowUp className="size-3 shrink-0" />
|
|
1326
|
+
) : (
|
|
1327
|
+
<ArrowDown className="size-3 shrink-0" />
|
|
1328
|
+
))}
|
|
1329
|
+
</>
|
|
1330
|
+
);
|
|
1331
|
+
const headClass = cn(
|
|
1332
|
+
"flex h-8 w-full items-center gap-1.5 whitespace-nowrap",
|
|
1333
|
+
ALIGN_BOX[alignOf(f.key)],
|
|
1334
|
+
);
|
|
1303
1335
|
return (
|
|
1304
1336
|
<TableHead
|
|
1305
1337
|
key={f.key}
|
|
1306
1338
|
className="relative w-max"
|
|
1307
1339
|
style={{ width: colWidths[f.key] }}
|
|
1308
1340
|
>
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
"
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
</span>
|
|
1322
|
-
{sort?.key === f.key &&
|
|
1323
|
-
(sort.dir === "asc" ? (
|
|
1324
|
-
<ArrowUp className="size-3 shrink-0" />
|
|
1325
|
-
) : (
|
|
1326
|
-
<ArrowDown className="size-3 shrink-0" />
|
|
1327
|
-
))}
|
|
1328
|
-
</button>
|
|
1341
|
+
{sortable ? (
|
|
1342
|
+
<button
|
|
1343
|
+
type="button"
|
|
1344
|
+
onClick={() => toggleSort(f.key)}
|
|
1345
|
+
className={cn(headClass, "hover:text-foreground")}
|
|
1346
|
+
>
|
|
1347
|
+
{headInner}
|
|
1348
|
+
</button>
|
|
1349
|
+
) : (
|
|
1350
|
+
// Not sortable: a static label, no toggle / hover affordance.
|
|
1351
|
+
<span className={headClass}>{headInner}</span>
|
|
1352
|
+
)}
|
|
1329
1353
|
{resizeHandle(f.key, f.label)}
|
|
1330
1354
|
</TableHead>
|
|
1331
1355
|
);
|
|
@@ -21,6 +21,9 @@ const fields: RecordField<Region>[] = [
|
|
|
21
21
|
required: true,
|
|
22
22
|
group: "General",
|
|
23
23
|
hideInTable: true,
|
|
24
|
+
// No Name column (shown in the identity cell via getPrimary), but still
|
|
25
|
+
// sortable from the Sort dropdown — `sortable` decouples it from visibility.
|
|
26
|
+
sortable: true,
|
|
24
27
|
filterable: true,
|
|
25
28
|
},
|
|
26
29
|
{
|