Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/user-actions-three-keys.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@objectstack/spec': minor
---

`UserActionsConfigSchema` adopts `group`, `hideFields` and `rowColor` (ruled Option A on objectui#5435): the three toolbar affordances ListView already honours become authorable in a spec-valid document, so the runtime fold of legacy `showGroup`/`showHideFields`/`showColor` flags now passes the save gate instead of being rejected by name. All three are booleans; the defaults copy the renderer's reads — `group` defaults on, `hideFields`/`rowColor` default off. Accept-set widening only: no existing document changes meaning, and the object-level `userActions` block (create/import/edit/delete/exportCsv) still rejects all view-vocabulary keys by name.
3 changes: 3 additions & 0 deletions content/docs/references/ui/view.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -736,8 +736,11 @@ User action toggles for the view toolbar
| **filter** | `boolean` | optional (default: `true`) | Allow users to filter records |
| **refresh** | `boolean` | optional (default: `true`) | Allow users to reload the view data from the backend without a full page reload |
| **rowHeight** | `boolean` | optional (default: `true`) | Allow users to toggle row height/density |
| **group** | `boolean` | optional (default: `true`) | Allow users to change record grouping from the toolbar. Toggle only — the grouping itself is configured in the view-level `grouping` block. |
| **addRecordForm** | `boolean` | optional (default: `false`) | Add records through a form instead of inline |
| **editInline** | `boolean` | optional (default: `false`) | Allow users to edit records inline — click a cell to edit it with the field's type-aware widget (the same control the form uses). Off by default: the list is read-only unless the author opts in. |
| **hideFields** | `boolean` | optional (default: `false`) | Allow users to hide/show fields from the toolbar (the affordance behind the view-level `hiddenFields` list). Boolean toggle — distinct from the record-details component's `hideFields`, which is an array of field names to omit. Off by default: column hiding is opt-in. |
| **rowColor** | `boolean` | optional (default: `false`) | Allow users to configure row colouring from the toolbar. Boolean toggle — the colour rules themselves live in the view-level `rowColor` block. Off by default: row colouring is opt-in. |
| **buttons** | `string[]` | optional | Custom action button IDs to show in the toolbar |


Expand Down
3 changes: 3 additions & 0 deletions packages/spec/authorable-defaults/ui.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,7 +109,10 @@
"ui/UserActionsConfig:addRecordForm = false",
"ui/UserActionsConfig:editInline = false",
"ui/UserActionsConfig:filter = true",
"ui/UserActionsConfig:group = true",
"ui/UserActionsConfig:hideFields = false",
"ui/UserActionsConfig:refresh = true",
"ui/UserActionsConfig:rowColor = false",
"ui/UserActionsConfig:rowHeight = true",
"ui/UserActionsConfig:search = true",
"ui/UserActionsConfig:sort = true",
Expand Down
3 changes: 3 additions & 0 deletions packages/spec/authorable-surface/ui.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -1126,7 +1126,10 @@
"ui/UserActionsConfig:buttons",
"ui/UserActionsConfig:editInline",
"ui/UserActionsConfig:filter",
"ui/UserActionsConfig:group",
"ui/UserActionsConfig:hideFields",
"ui/UserActionsConfig:refresh",
"ui/UserActionsConfig:rowColor",
"ui/UserActionsConfig:rowHeight",
"ui/UserActionsConfig:search",
"ui/UserActionsConfig:sort",
Expand Down
26 changes: 23 additions & 3 deletions packages/spec/src/data/object-strictness-batch20.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,6 +55,7 @@ import {
} from './object.zod';
import { resolveInjectedSystemColumns } from './injected-system-columns';
import { getMetadataTypeSchema } from '../kernel/metadata-type-schemas';
import { UserActionsConfigSchema } from '../ui/view.zod';

/** Reject `value` through `schema` and return its issues as a searchable string. */
function reject(
Expand DownExpand Up@@ -409,13 +410,32 @@ describe('#4001 批 20 — curation is anchored to the sibling contract that mak

it('`userActions.sort` names the VIEW block it belongs to — same key NAME, disjoint vocabulary', () => {
// `ui/view.zod.ts`'s UserActionsConfigSchema declares sort/search/filter/
// refresh/rowHeight/addRecordForm/editInline/buttons; the object block
// declares create/import/edit/delete/exportCsv. Nothing overlaps, which
// is exactly why an author who learned one writes it on the other.
// refresh/rowHeight/group/addRecordForm/editInline/hideFields/rowColor/
// buttons (group/hideFields/rowColor adopted at #11195, ruled A on
// objectui#5435); the object block declares create/import/edit/delete/
// exportCsv. Nothing overlaps, which is exactly why an author who
// learned one writes it on the other.
const msg = rejectOnObject({ userActions: { sort: false } });
expect(msg).toContain('VIEW');
});

it('the two `userActions` vocabularies stay disjoint — re-checked mechanically, not assumed (#11195)', () => {
// The enumeration above is prose and can drift; this reads the VIEW
// block's real shape. The object block's five verbs are transcribed —
// they are pinned individually across this file — and the overlap is
// asserted empty so the NEXT adoption re-runs this check for free.
const viewKeys = Object.keys(UserActionsConfigSchema.shape);
const objectVerbs = ['create', 'import', 'edit', 'delete', 'exportCsv'];
for (const k of ['group', 'hideFields', 'rowColor']) expect(viewKeys).toContain(k);
expect(viewKeys.filter((k) => objectVerbs.includes(k))).toEqual([]);
// …and the object block still REFUSES the three adopted view keys by
// name — the operative half of "disjoint": the wrong-layer write stays
// a loud rejection, never a silent second meaning.
for (const k of ['group', 'hideFields', 'rowColor']) {
expect(rejectOnObject({ userActions: { [k]: true } })).toContain(k);
}
});

it('`userActions.clone` points at the `enable` capability block, which really does declare it', () => {
const msg = rejectOnObject({ userActions: { clone: false } });
expect(msg).toContain('enable');
Expand Down
7 changes: 4 additions & 3 deletions packages/spec/src/data/object.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1724,9 +1724,10 @@ const ObjectSchemaBase = strictObject(
guidance: {
// Wrong-LAYER, and the trap is that the key name is right somewhere else.
// `ui/view.zod.ts` declares its own `userActions` with a completely
// disjoint vocabulary (sort/search/filter/refresh/rowHeight/
// addRecordForm/editInline/buttons), so an author who learned that block
// writes these here and gets a shape that has never heard of them.
// disjoint vocabulary (sort/search/filter/refresh/rowHeight/group/
// addRecordForm/editInline/hideFields/rowColor/buttons — the last three
// adopted at #11195), so an author who learned that block writes these
// here and gets a shape that has never heard of them.
sort:
'`sort` is a VIEW `userActions` key, not an object one — the two blocks share a ' +
'name and nothing else. The object block governs CRUD affordances ' +
Expand Down
35 changes: 35 additions & 0 deletions packages/spec/src/ui/view.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2496,25 +2496,60 @@ describe('UserActionsConfigSchema', () => {
expect(config.filter).toBe(true);
expect(config.refresh).toBe(true);
expect(config.rowHeight).toBe(true);
expect(config.group).toBe(true);
expect(config.addRecordForm).toBe(false);
expect(config.editInline).toBe(false);
expect(config.hideFields).toBe(false);
expect(config.rowColor).toBe(false);
expect(config.buttons).toBeUndefined();
});

// The split is copied from the renderer's reads, not chosen by the spec
// (ruled A on objectui#5435): ListView.tsx computes `showGroup` as
// `ua?.group !== false` (absent ⇒ shown) but `showHideFields` / `showColor`
// as `ua?.hideFields === true` / `ua?.rowColor === true` (absent ⇒ hidden).
// Normalising the three to one default would silently flip a toolbar
// affordance on every view that never wrote the key.
it('pins the group-ON / hideFields-OFF / rowColor-OFF defaults asymmetry (objectui#5435)', () => {
const config = UserActionsConfigSchema.parse({});
expect(config.group).toBe(true);
expect(config.hideFields).toBe(false);
expect(config.rowColor).toBe(false);
});

// objectui's normalize-list-view fold maps legacy `showGroup` /
// `showHideFields` / `showColor` onto exactly these keys, so this document is
// the fold's own OUTPUT — before adoption the save gate rejected it by name.
it('accepts the legacy-fold output through ListViewSchema (acceptance criterion, objectstack#11195)', () => {
const result = ListViewSchema.safeParse({
name: 'my_view',
label: 'My View',
columns: [{ field: 'name' }],
userActions: { group: false, hideFields: true, rowColor: true },
});
expect(result.success).toBe(true);
});

it('should accept full configuration', () => {
const config = UserActionsConfigSchema.parse({
sort: false,
search: true,
filter: false,
rowHeight: true,
group: false,
addRecordForm: true,
editInline: true,
hideFields: true,
rowColor: true,
buttons: ['btn_export', 'btn_archive'],
});
expect(config.sort).toBe(false);
expect(config.filter).toBe(false);
expect(config.group).toBe(false);
expect(config.addRecordForm).toBe(true);
expect(config.editInline).toBe(true);
expect(config.hideFields).toBe(true);
expect(config.rowColor).toBe(true);
expect(config.buttons).toEqual(['btn_export', 'btn_archive']);
});

Expand Down
19 changes: 19 additions & 0 deletions packages/spec/src/ui/view.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -848,6 +848,22 @@ export const VisualizationTypeSchema = lazySchema(() => z.enum([
* Controls which interactive actions are available to users in the view toolbar.
* Each boolean toggles the corresponding toolbar element on/off.
*
* `group` / `hideFields` / `rowColor` adopted from objectui's legacy `show*`
* fold (`SHOW_FLAG_TO_USER_ACTION` in normalize-list-view.ts; ruled Option A on
* objectui#5435): the ListView toolbar already honoured all three, but no
* spec-valid document could declare them, so the fold's own output failed the
* save gate. The defaults asymmetry is LOAD-BEARING, copied from the renderer's
* reads, not chosen here: `group` defaults ON (`ua?.group !== false`) like the
* other core toolbar controls, while `hideFields` / `rowColor` default OFF
* (`=== true`) — column hiding and row colouring are opt-in affordances.
*
* Name-collision note (same key NAME, different shape, on OTHER surfaces —
* deliberate, each toggle is named after the config it gates): these three are
* booleans HERE, while `rowColor` on the list view itself is a
* {@link RowColorConfigSchema} (the colour rules this toggle exposes to users)
* and `hideFields` on the record-details component is a `string[]` of field
* names to omit. The describes below say which side of each pair this is.
*
* @see Airtable Interface → "User actions" panel
*/
export const UserActionsConfigSchema = lazySchema(() => strictObject({
Expand All@@ -859,8 +875,11 @@ export const UserActionsConfigSchema = lazySchema(() => strictObject({
filter: z.boolean().default(true).describe('Allow users to filter records'),
refresh: z.boolean().default(true).describe('Allow users to reload the view data from the backend without a full page reload'),
rowHeight: z.boolean().default(true).describe('Allow users to toggle row height/density'),
group: z.boolean().default(true).describe('Allow users to change record grouping from the toolbar. Toggle only — the grouping itself is configured in the view-level `grouping` block.'),
addRecordForm: z.boolean().default(false).describe('Add records through a form instead of inline'),
editInline: z.boolean().default(false).describe('Allow users to edit records inline — click a cell to edit it with the field\'s type-aware widget (the same control the form uses). Off by default: the list is read-only unless the author opts in.'),
hideFields: z.boolean().default(false).describe('Allow users to hide/show fields from the toolbar (the affordance behind the view-level `hiddenFields` list). Boolean toggle — distinct from the record-details component\'s `hideFields`, which is an array of field names to omit. Off by default: column hiding is opt-in.'),
rowColor: z.boolean().default(false).describe('Allow users to configure row colouring from the toolbar. Boolean toggle — the colour rules themselves live in the view-level `rowColor` block. Off by default: row colouring is opt-in.'),
buttons: z.array(z.string()).optional().describe('Custom action button IDs to show in the toolbar'),
}).describe('User action toggles for the view toolbar'));

Expand Down
Loading