@object-ui/plugin-list 17.2.0 → 17.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +621 -0
- package/README.md +52 -15
- package/dist/ListView.d.ts +89 -7
- package/dist/ListView.d.ts.map +1 -1
- package/dist/ListViewBlock.d.ts +69 -0
- package/dist/ListViewBlock.d.ts.map +1 -0
- package/dist/ObjectGallery.d.ts.map +1 -1
- package/dist/UserFilters.d.ts.map +1 -1
- package/dist/ViewSwitcher.d.ts.map +1 -1
- package/dist/components/TabBar.d.ts.map +1 -1
- package/dist/components/ViewSettingsPopover.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1424 -1236
- package/dist/index.umd.cjs +3 -3
- package/package.json +25 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,621 @@
|
|
|
1
1
|
# @object-ui/plugin-list
|
|
2
2
|
|
|
3
|
+
## 17.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7084f7d: `DashboardRenderer` and `ListView` serve the props they declare — the index signature stops erasing them
|
|
8
|
+
|
|
9
|
+
Both components declared a full props interface and neither was enforced. A `[key: string]: any` on `DashboardRendererProps` and `ListViewProps` puts `string` into `keyof Props`, so `'ref' extends keyof Props` is always true and React's `PropsWithoutRef` takes its `Omit` branch — and `Omit` over a type carrying a string index signature keeps only the index signature. Every declared property was dropped from the resolved type, on both sides: the render function received `{ [x: string]: any }` (so even `schema` was `any` inside the component), and every JSX call site was unchecked. Measured on the pre-fix source, `keyof ComponentProps<typeof DashboardRenderer>` was `string | number` and `ComponentProps<typeof DashboardRenderer>['onWidgetClick']` was `any`, while the interface went on declaring `(widgetId: string | null) => void`. `ListView` measured identically for `onRowClick`. This is objectui#4422 / PR #4438's trap in the two packages that issue left unswept.
|
|
10
|
+
|
|
11
|
+
Graded **minor, not major**: the interfaces have always DECLARED these props; the index signature erased them from the resolved type. Restoring what the interface documents is a FIX to the published contract, not a contract break — no documented capability is removed, and `any`-typed accidental passthrough was never the documented surface. Nothing in either package's README or docs endorses relying on it.
|
|
12
|
+
|
|
13
|
+
The props each component genuinely reads but never declared are now declared by name, at the type each one lands on: `dataSource` on both, plus `onAddRecord` / `onBulkAction` / `onPageSizeChange` / `onEdit` / `onDelete` / `onBulkDelete` on `ListView`. `DashboardRenderer`'s DOM pass-through keys are derived from `toDomProps`' whitelist constant itself, so the declaration and the runtime filter cannot drift — the "declare it and forward it by name" direction `@object-ui/core`'s `dom-props` doctrine asks for, rather than reopening the spread.
|
|
14
|
+
|
|
15
|
+
Type-only: the emitted JS for both packages is byte-identical before and after (verified by sha256 on `dist/index.js` and `dist/index.umd.cjs`), and both packages' runtime suites are untouched and green.
|
|
16
|
+
|
|
17
|
+
Three latent defects the erasure had been hiding are fixed with it, each surfaced by the repo-wide type-check: `DashboardWithConfig` typed its widget-select handler `(widgetId: string)` while `DashboardRenderer` calls `onWidgetClick(null)` to deselect; `InterfaceListPage` built a list schema whose `viewType` was a bare `string`; and `StudioDesignSurface` forwarded a `refreshKey` prop that no component in the chain declares or reads, so it was silently dropped. Per-package structural guards now pin the shape in both packages, covering the public `forwardRef` that takes its props whole — the spelling objectui#4438's `schema`-destructuring scan could not see.
|
|
18
|
+
|
|
19
|
+
- fe52a04: `rowHeightToDensityMode` answers only for the five spec row heights — the coerce-to-`comfortable` fallback is gone
|
|
20
|
+
|
|
21
|
+
Two surfaces narrow a list view's `rowHeight` onto the renderer's three-step
|
|
22
|
+
density vocabulary, and since objectui#4352 they answered differently for the
|
|
23
|
+
same off-spec input: `@object-ui/react`'s spec bridge declined to answer, while
|
|
24
|
+
`@object-ui/core`'s `rowHeightToDensityMode` rehabilitated anything unknown into
|
|
25
|
+
`comfortable`. One metadata-driven system, two answers for one input
|
|
26
|
+
(objectui#4440).
|
|
27
|
+
|
|
28
|
+
The strict answer wins, per AGENTS.md #0.1: a renderer-side rehabilitation of
|
|
29
|
+
off-spec metadata is a second de-facto contract, and one strict contract beats N
|
|
30
|
+
dialects — a bad `rowHeight` gets fixed at the producer, where the schema already
|
|
31
|
+
rejects it. The five mappings themselves are untouched (`compact`/`short` →
|
|
32
|
+
`compact`, `medium` → `comfortable`, `tall`/`extra_tall` → `spacious`), and the
|
|
33
|
+
table keeps its `Record< RowHeight, … >` typing, so a row height added upstream
|
|
34
|
+
still fails the build here.
|
|
35
|
+
|
|
36
|
+
**Breaking semantics, deliberately graded `minor`** (this repo never publishes
|
|
37
|
+
`major` — its major tracks `@objectstack`). Two things change:
|
|
38
|
+
|
|
39
|
+
- **Published type.** `rowHeightToDensityMode` is exported from
|
|
40
|
+
`@object-ui/core`, and its return widens from `DensityMode` to
|
|
41
|
+
`DensityMode | undefined`. A host assigning the result straight into a
|
|
42
|
+
`DensityMode` now has to say what an off-spec row height should mean to it.
|
|
43
|
+
- **Rendered output, for input the spec already rejects.** `ListView` — the one
|
|
44
|
+
in-repo caller — used to render an off-spec `rowHeight` one step looser than an
|
|
45
|
+
ABSENT one (`comfortable`, 40px rows, vs `compact`, 32px). It now renders it
|
|
46
|
+
exactly like an absent one, `compact`, which is also `ObjectGrid`'s own default.
|
|
47
|
+
A sweep of this repo, the `objectstack` example apps and one downstream app
|
|
48
|
+
found zero authored off-spec values, and the legacy `densityMode` alias cannot
|
|
49
|
+
produce one (`DENSITY_MODE_TO_ROW_HEIGHT` is typed
|
|
50
|
+
`Record< DensityMode, RowHeight >`).
|
|
51
|
+
|
|
52
|
+
Also closed while retiring the branch: the lookup guarded membership with `in`,
|
|
53
|
+
which walks the prototype chain, so `rowHeight: 'toString'` returned
|
|
54
|
+
`Object.prototype.toString` — a function — from something typed `DensityMode`. It
|
|
55
|
+
is an own-property check now.
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- ae10a01: Console chrome reaches the bundle — the list switcher, the aggregate footer, the dialog a11y fallbacks and the whole Settings namespace screen stop being English on non-English consoles
|
|
60
|
+
|
|
61
|
+
Six strings on the two screens a user looks at most were hardcoded English literals rather than bundle lookups, so they stayed English on every non-English console with nothing an app could author to change them. They are not object, field, view or action labels — no key in `TranslationData` reaches them — while the console's own bundle already ships zh-CN, ja-JP, es-ES, de, fr, pt, ru, ko and ar and translates hundreds of neighbouring strings. Omissions from an otherwise complete bundle, not a missing capability.
|
|
62
|
+
|
|
63
|
+
**Two of the six needed no new keys at all, which is the more interesting half.** The list-view mode switcher named its nine visualizations from a private `VIEW_LABELS` table while `console.objectView.viewType*` — the same nine words — had been resolved through the bundle by the create-view picker for months; the switcher now reads those keys, so the picker's 「画廊」 and the switcher's 「画廊」 cannot drift apart in nine languages. The create/edit dialog's close button is the remainder of a fix that already landed: objectstack#5505 routed the `sr-only` close label through `common.close` for the two Shadcn-synced primitives, but `MobileDialogContent` is a hand-written wrapper outside that regeneration zone with its own close button, and it is exactly what `ModalForm` renders — so the dialog the report measured was the one place still announcing "Close" in English.
|
|
64
|
+
|
|
65
|
+
The aggregate footer is the one the original report singled out: the **number** was already locale-formatted and the **prefix** was a hardcoded `Avg: ` / `Sum: `. All eleven aggregation kinds now take their prefix from `grid.summary.*`, and the label/value join is its own key rather than a `': '` baked into the renderer — the separator is translatable content, so zh sets a fullwidth colon and fr the French space-before-colon. The numbers are untouched. The form dialog's `sr-only` description fallback joins the packs too; it is clipped, not visible, so the only way an app could displace it was to author a `description` and thereby put a visible subtitle on every dialog.
|
|
66
|
+
|
|
67
|
+
**The Settings namespace screen converts as one unit.** `SettingsView` routed zero framing copy through i18n — save/failure toasts, the env-lock and crypto refusals, the load-error card, the empty-route state, the navigation buttons, the unsaved-changes save bar — while its immediate sibling `SettingsHub`, in the same directory, resolved everything through `t('console.settingsHub.*')`. A zh-CN admin read correctly translated field labels sitting inside an English save bar, because `useSettingsLabel` translates a namespace's authored content but reaches none of the chrome around it. All of it now resolves through a `console.settingsView.*` namespace placed beside the hub's, including the crypto-refusal strings that objectui#4579 deliberately left in English rather than leave one translated string among a dozen literals.
|
|
68
|
+
|
|
69
|
+
The save-bar counter was an English plural rule executing in every locale (`change` plus an `s` when the count exceeds one). It is now a real i18next plural family — base key plus `_one` and `_other` in all ten packs — not the `(s)` spelling translated nine ways. The base key is the load-bearing part: i18next asks `Intl.PluralRules` for the one suffix a language needs and, finding no such slot, falls back to English, so without it Russian would read English at counts 2-20 and Arabic at 2-99. Russian and Arabic take the "noun: {count}" form their packs already use for this exact reason, and the counter is verified rendering in-language at 1, 2 and 5.
|
|
70
|
+
|
|
71
|
+
The Beta badge reuses the hub's existing key rather than minting a twin, and the refusal messages interpolate their subject through the bundle instead of concatenating a translated word onto an English prefix.
|
|
72
|
+
|
|
73
|
+
- bb58d1d: i18n: the two search placeholders become pack values, and four values the packs served in English get translated
|
|
74
|
+
|
|
75
|
+
**objectui#4375** — `ListView` and `LookupField` built their search placeholder as
|
|
76
|
+
`t(key) + '...'`, so the ellipsis was a literal concatenated in code: it stayed ASCII
|
|
77
|
+
in all ten locales on screens where objectui#3878 had converged everything else on
|
|
78
|
+
U+2026, and no pack could opt out of it (sharpest in `ar`, where a left-to-right run
|
|
79
|
+
was appended to right-to-left text). Both now read `table.search`, which is already
|
|
80
|
+
the repo's search-input placeholder key — `data-table`, `RecordPickerDialog` and
|
|
81
|
+
`PeoplePicker` render it too — and is translated with the right ellipsis in all ten
|
|
82
|
+
packs. No new keys.
|
|
83
|
+
|
|
84
|
+
**objectui#4376** — `list.loading` served the English `Loading records…` in eight of
|
|
85
|
+
the nine translation packs (`zh` alone had translated it); `designer.undo` and
|
|
86
|
+
`designer.redo` were English in all nine; `appDesigner.snakeCaseHint` in `ko`, `pt`,
|
|
87
|
+
`ru` and `ar`. All translated, reusing each pack's own established vocabulary. A new
|
|
88
|
+
pin (`untranslated-identity-4376.test.ts`) fails on any value byte-identical to `en`
|
|
89
|
+
inside a non-Latin pack unless the key is on an explicit 22-entry allowlist.
|
|
90
|
+
|
|
91
|
+
- bb68488: An inline per-locale label now renders its locale's string at the thirteen read sites the `@objectstack/spec` 17.0.0-rc.6 bump exposed
|
|
92
|
+
|
|
93
|
+
rc.6 widened `I18nLabel` from `string` to `string | Record<string, string>`, so an author may write `label: { en: 'Owner', 'zh-CN': '负责人' }` anywhere the spec accepts a display label. PR #4169 repaired eight such sites; these thirteen were invisible to it because the five packages involved build through vite/rolldown, so `turbo run build` never type-checks their sources — only `turbo run type-check` does. All thirteen are now resolved through a shared resolver against a real locale, and `turbo run type-check` is 78/78 with zero errors.
|
|
94
|
+
|
|
95
|
+
| package | what an author can now write and see |
|
|
96
|
+
| ----------------------------- | --------------------------------------------------------------------------------------- |
|
|
97
|
+
| `@object-ui/layout` | `NavigationArea.label` — the sidebar area switcher's button and its tooltip |
|
|
98
|
+
| `@object-ui/plugin-list` | `ViewTab.label` — the inline pill row, and the mobile dropdown's trigger and menu items |
|
|
99
|
+
| `@object-ui/plugin-dashboard` | `DashboardWidget.title` — the widget card heading and its `title` attribute |
|
|
100
|
+
| `@object-ui/plugin-designer` | `DashboardWidget.title` — the widget card and the preview tile |
|
|
101
|
+
| `@object-ui/app-shell` | `ActionParam.label` **and** each `ActionParam.options[].label` |
|
|
102
|
+
|
|
103
|
+
**Patch, not minor, in every case: no public surface changes meaning.** Every entry above is a read site that previously could only be reached with a value the type system rejected, so no caller's working code changes behaviour. `@object-ui/app-shell` is the only package with an exported-type change and it is purely additive on the authoring side — `RawActionParam.label` and `RawActionParam.options[].label` widen to `I18nLabel` (they accept strictly more), `ResolveActionParamsContext` gains an optional `locale`, and the new `RawActionParamOption` names the authoring shape that was previously spelled with the resolved one. What `resolveActionParams` **emits** is unchanged: `ActionParamDef.label` and its options' labels are still plain `string`s.
|
|
104
|
+
|
|
105
|
+
Two consequences worth knowing:
|
|
106
|
+
|
|
107
|
+
- **The dashboard designer's title input is deliberately read-only for a map-valued title.** Resolving a per-locale map into a single-line input and writing `e.target.value` back would collapse every other locale on the first keystroke, so the write is guarded and an inline map survives an unrelated edit-and-save round trip untouched — the same conservative branch #4169 took for `DashboardWidgetInspector`. What Studio should actually offer for authoring a per-locale label is objectui#4163 part 2, which is unclaimed and pending design.
|
|
108
|
+
- **`@object-ui/layout` resolves at the spec's `en` default, not the viewer's language.** That package carries no i18n dependency by design (its whole i18n story is injection), and `AppSchemaRendererProps` exposes no locale to thread. The choice and what would change it are documented at the call site.
|
|
109
|
+
|
|
110
|
+
- 297534b: Align 43 inline `defaultValue` strings with the `en` pack, and make the call-site gate enforce it (objectui#3810)
|
|
111
|
+
|
|
112
|
+
`t(key, { defaultValue: 'English text' })` only renders that text when i18next
|
|
113
|
+
**misses** the key. Where the key exists in `packages/i18n/src/locales/en.ts` the
|
|
114
|
+
pack value always wins, so the inline string is dead code — and 43 of those dead
|
|
115
|
+
strings said something different from the sentence users actually read.
|
|
116
|
+
|
|
117
|
+
`scripts/check-i18n-call-site-keys.mjs` (objectui#3530) now compares the two
|
|
118
|
+
whenever a call site carries a literal `defaultValue` for a key `en` defines, and
|
|
119
|
+
fails on any byte of difference. It is a hard rule with **no baseline**: the
|
|
120
|
+
repo-wide census measured 43 sites in 19 files out of 851 literal inline defaults,
|
|
121
|
+
and all 43 are aligned here, so there is no debt for a ratchet to hold. A
|
|
122
|
+
`defaultValue` on a key that is _not_ yet in `en` stays legal — that transition
|
|
123
|
+
runs for months (objectui#3546) and belongs to the existing `missing-key` rule,
|
|
124
|
+
which keeps reporting it alone.
|
|
125
|
+
|
|
126
|
+
Every fix moved the CALL SITE to the pack's wording. `en.ts` is untouched: its
|
|
127
|
+
values are what users read today, and changing one would oblige the same change in
|
|
128
|
+
the nine other packs (`scripts/check-i18n-en-drift.mjs`, objectui#3650). Six of the
|
|
129
|
+
43 differed only in an ellipsis (`...` against U+2026) — invisible in review, which
|
|
130
|
+
is how they survived three i18n gates that are each blind to this class by
|
|
131
|
+
construction.
|
|
132
|
+
|
|
133
|
+
The visible effect is confined to hosts that render these components with **no**
|
|
134
|
+
`I18nProvider` and no initialised i18next instance. There, react-i18next's
|
|
135
|
+
not-ready `t` returns the `defaultValue`, so the inline string was the rendered
|
|
136
|
+
one; it now matches what a provider-backed app has always shown. Inside the
|
|
137
|
+
console — provider mounted — nothing users see changes. The clearest converging
|
|
138
|
+
examples: the workspaces screen was written as "Organizations" at nine call sites
|
|
139
|
+
while every user has been reading "Workspaces"; the forgot-password success line
|
|
140
|
+
was written as "If an account exists, a reset link has been sent." while the pack
|
|
141
|
+
asserts "We've sent a password reset link to {{email}}."
|
|
142
|
+
|
|
143
|
+
- f8595a0: A list emptied by the view's own filter says "no records match", instead of inviting you to create your first record
|
|
144
|
+
|
|
145
|
+
`ListView`'s empty state distinguishes "filtered to empty" from "truly empty (first run)", but the view's own declared `filter` did not count toward that decision — only the search term, the user-filter conditions and the toolbar's live filter group did. A view that returns nothing _because it is filtered_ therefore rendered the first-run copy over an object full of records.
|
|
146
|
+
|
|
147
|
+
That is a small string, and it cost real triage time. In objectui#4155 a stored overlay filter had emptied a list, and the screen said "no data yet / create your first record" — so the report read as data loss or a permission problem, and the investigation went to the data and permission layers rather than to the view layer where the defect was. The same misread is available without any bug at all: a perfectly healthy view declaring `status not_in [archived, deleted]` over an object whose rows are all archived told the user the object was empty.
|
|
148
|
+
|
|
149
|
+
The base `filter` now counts as an active query, in both at-rest shapes (an array of conditions, and the Mongo-style object form). No new copy — this only routes to the `list.noMatches` / `list.noMatchesMessage` strings that already exist in all ten locales, so there are no new keys to translate. An author-supplied `emptyState.title` / `emptyState.message` still wins over both branches, unchanged.
|
|
150
|
+
|
|
151
|
+
- 33c32bf: List sort: the picker stops borrowing the filter whitelist, and a header click is no longer a one-way door out of the view's declared sort
|
|
152
|
+
|
|
153
|
+
`filterableFields` was applied to the single field set both toolbar builders read, so a whitelist authored for _filtering_ silently became the _sort_ whitelist too. A view could declare a two-level default sort — `plan_start_date` then `name` — and get a sort panel that offered neither field and rendered both of its rows blank: the declared sort worked on load and could then be neither reproduced nor modified, and there was no way to express "sortable but not offered as a filter condition" short of widening the filter builder as collateral. The whitelist now narrows the filter builder alone, which is the contract it was written for; the sort picker starts from every field the view can name and applies its own sortability rules.
|
|
154
|
+
|
|
155
|
+
Those rules are about what the sort can honestly reach, so a second one joins the existing relational exclusion: a `formula` field is withheld. It has no materialised column, so ordering by one is refused by the server outright (objectstack `UNMATERIALIZED_SORT_TYPES`) — and it matters here precisely because the base set widened, since a formula field previously reached the picker only if someone had whitelisted it. The exclusion is `formula` alone and deliberately not the spec's `COMPUTED_VALUE_TYPES`: `summary` and `autonumber` are computed too, each gets a real maintained column, and both order correctly. Either rule keeps its existing escape hatch — a field the current sort already uses stays listed, which for a formula field is the only way to remove the offending row.
|
|
156
|
+
|
|
157
|
+
One consequence worth naming: the hint explaining the relational omission used to be gated by the same whitelist. A view whitelisting only `status` showed a near-empty sort picker and no word about why; the withheld relational field now reaches the rule that withholds it, so the explanation appears with it.
|
|
158
|
+
|
|
159
|
+
The second half is the way back. One column-header click replaces the whole sort array, so a view shipping a multi-level default lost it for the rest of the session — the declared `sort` behaved as an initial value only, recoverable just by reloading the page. The sort panel gains a **Reset to view default** control that restores the declared array whole: multi-level, in declared order, not merely cleared. It reads the view's declared sort through the same resolver the initial render already uses, so there is one answer to "what did this view declare". It is disabled while the active sort already matches that default, and absent entirely for a view that declares no sort — there is no default to return to, and clearing the sort under that label would be a second, differently-named way to do what removing the rows already does. The header click's own semantics are unchanged: it still replaces the array, it just no longer does so irreversibly.
|
|
160
|
+
|
|
161
|
+
- 7ffd616: fix(plugin-list): ListView hands the child grid the query behind the window it passes down
|
|
162
|
+
|
|
163
|
+
ListView owns the fetch on the external-pagination path — it holds the filter,
|
|
164
|
+
the search term and the sort, and it is the side that calls `dataSource.find`.
|
|
165
|
+
The grid it hands the window to has a cross-page "select all N matching"
|
|
166
|
+
escalation that RE-ISSUES that query to collect the whole match set, and with
|
|
167
|
+
nothing handed down it replayed its own never-written params ref and so asked the
|
|
168
|
+
server for the entire object, feeding unmatched records to bulk delete.
|
|
169
|
+
|
|
170
|
+
The params object is now hoisted out of the `find` call — one object, one query,
|
|
171
|
+
no reconstruction that could drift from what was actually asked — recorded past
|
|
172
|
+
the stale-request guard so it is always the query that produced the rows on
|
|
173
|
+
screen, and forwarded as `findParams` in the same handoff block as `rowCount`,
|
|
174
|
+
`page` and `onPageChange`. No public API of `ListView` changes.
|
|
175
|
+
|
|
176
|
+
- 37cd8e4: `list-view` now reads its `dataSource` binding through the shared `ElementDataSourceGate` instead of a private copy of the precedence table
|
|
177
|
+
|
|
178
|
+
objectstack#5576 landed the per-element `dataSource` binding on `list-view` by
|
|
179
|
+
writing the precedence table — binding keys override the component's, a `view` is
|
|
180
|
+
only a baseline, `filter` AND-combines rather than replaces, an authored-but-empty
|
|
181
|
+
`columns` counts as unauthored, the row cap lands on `pagination.pageSize`, and
|
|
182
|
+
`viewType` is taken only when the component declared none — inline in
|
|
183
|
+
`ListViewBlock`. objectstack#6953 then needed the same table for the other eight
|
|
184
|
+
object-bound blocks and lifted it into `@object-ui/react`
|
|
185
|
+
(`useElementDataSourceSchema` / `ElementDataSourceGate`), deliberately not
|
|
186
|
+
touching `ListViewBlock`: refactoring already-merged code inside a wiring PR
|
|
187
|
+
would have been an out-of-scope regression surface.
|
|
188
|
+
|
|
189
|
+
That left one table with two implementations — `list-view` on the private copy,
|
|
190
|
+
every other block on the shared one. Nothing was wrong for a user today; the risk
|
|
191
|
+
is the next person to change the rules changing one side, which is how the spec's
|
|
192
|
+
"_additional_ filter criteria" becomes two dialects and a per-element filter
|
|
193
|
+
quietly starts replacing a saved view's instead of narrowing it.
|
|
194
|
+
|
|
195
|
+
`ListViewBlock` now contributes only what is genuinely its own — the names of the
|
|
196
|
+
keys `ListView` reads:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
const LIST_VIEW_DATA_SOURCE: ElementDataSourceMapping = {
|
|
200
|
+
columns: true,
|
|
201
|
+
filter: true,
|
|
202
|
+
sort: true,
|
|
203
|
+
limit: "pagination.pageSize",
|
|
204
|
+
viewType: true,
|
|
205
|
+
};
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
and the ~45-line `useMemo` mapping block is deleted, along with the block's
|
|
209
|
+
hand-rolled error and loading panels (the shared
|
|
210
|
+
`ElementDataSourceErrorPanel` / `ElementDataSourceLoadingPanel` render with the
|
|
211
|
+
`list-view` testId prefix, so `list-view-datasource-error` and
|
|
212
|
+
`list-view-resolving-view` are unchanged down to the byte, and the error heading
|
|
213
|
+
is passed through as `errorTitle`).
|
|
214
|
+
|
|
215
|
+
**No behaviour changes in either direction**, and that is the acceptance
|
|
216
|
+
criterion rather than a hoped-for outcome: objectstack#5576's entire suite passes
|
|
217
|
+
untouched, with no assertion edited — had any single case needed adapting, the
|
|
218
|
+
two implementations would have been proven to disagree, which is a defect to
|
|
219
|
+
re-grade rather than a refactor detail to absorb. New pins cover the mapping
|
|
220
|
+
table key by key at the block/gate seam, plus the shared loading panel, which
|
|
221
|
+
neither implementation had ever asserted.
|
|
222
|
+
|
|
223
|
+
- e076fd5: Inline-edit toggle reads "Edit fields" without an I18nProvider, matching every locale pack
|
|
224
|
+
|
|
225
|
+
`DETAIL_DEFAULT_TRANSLATIONS` said `Edit fields inline` where all ten packs say
|
|
226
|
+
`Edit fields`, so `InlineEditSaveBar`'s toggle announced two different names for one
|
|
227
|
+
control — the map's on provider-less hosts (standalone embeds, the preview gallery),
|
|
228
|
+
the pack's in the console. The pack wins; the map row now mirrors it byte for byte.
|
|
229
|
+
|
|
230
|
+
The three ungated defaults maps (`plugin-detail`, `plugin-list`, `plugin-designer`) are
|
|
231
|
+
now compared key-by-key against the `en` pack by a new gate, generalizing the
|
|
232
|
+
collaboration-only precedent from objectui#3440. `LIST_DEFAULT_TRANSLATIONS` and
|
|
233
|
+
`DESIGNER_DEFAULT_TRANSLATIONS` are exported for it, as `DETAIL_DEFAULT_TRANSLATIONS`
|
|
234
|
+
and `COLLAB_DEFAULT_TRANSLATIONS` already were.
|
|
235
|
+
|
|
236
|
+
- a84385b: `NavigationConfig.mode` is optional — the type now says what the hook does
|
|
237
|
+
|
|
238
|
+
`@object-ui/react` published a `NavigationConfig` that required `mode`, in front of a `useNavigationOverlay` that has always defaulted it. The declaration took the spec's authored config, `Omit`ted `mode`, and re-added it as `NonNullable< … >`; 140 lines below, the hook read `navigation?.mode ?? 'page'`. The type was strictly stricter than the implementation it fronted, and `'page'` is meaningful behaviour rather than a placeholder.
|
|
239
|
+
|
|
240
|
+
The spec never asked for that. `NavigationConfigSchema` declares `mode: NavigationModeSchema.default('page')`, and a `.default()` lands on the authoring side as `| undefined` — so `navigation: { view: 'summary_view' }` is legal authored metadata that lets the mode default. `@object-ui/types` already re-exported the spec's own `NavigationConfig` unchanged, which meant one monorepo shipped two published types of the same name that disagreed about whether `mode` could be omitted.
|
|
241
|
+
|
|
242
|
+
The alias is now the spec's authored config verbatim, with no divergence of its own:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
export type NavigationConfig = SpecAuthoredInput<
|
|
246
|
+
typeof NavigationConfigSchema
|
|
247
|
+
>;
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The cost of the old spelling was paid by callers. `ListView` carried `schema.navigation as NavigationConfig | undefined` for no reason except to get a valid spec-shaped value past the declaration; that assertion is deleted here, not replaced. A type in front of an implementation must not be stricter than the implementation — when it is, every caller pays in casts, and a cast is exactly the renderer-side workaround that belongs back at the producer.
|
|
251
|
+
|
|
252
|
+
**Nothing changes at runtime.** `navigation?.mode ?? 'page'` is untouched, and the default is now pinned as observable behaviour (`useNavigationOverlay.modeDefault.test.tsx`) rather than only as a comment — the explicit modes, the `preventNavigation` and `none` short-circuits, the `onRowClick` priority, and the Cmd/Ctrl/middle-click and `new_window` branches are all pinned alongside it.
|
|
253
|
+
|
|
254
|
+
**Why minor rather than patch**, from the measured `.d.ts`. Optional-izing a property is looser for writers and narrower for readers, so the grade turns on which role the published surface actually plays. In this package `NavigationConfig` occurs only in input positions — `useNavigationOverlay`'s `navigation?:` option and `resolveOverlayWidth`'s parameter — and never in a return type; the package consumes these values and never hands one back. For consumers the change is therefore purely permissive: every call that compiled before still compiles, and spec-shaped configs that previously needed an assertion now compile without one. That gained input shape is a real capability rather than an internal repair, which is more than a patch describes. The reader-side narrowing is real but secondary: code that imports the bare type, annotates its own value with it and reads `.mode` now sees `NavigationMode | undefined`. The in-repo census found exactly one such importer — `ListView` — and it imported the type only to write the assertion this change removes.
|
|
255
|
+
|
|
256
|
+
- dad805d: Six i18n keys no longer render as raw key strings on hosts with no `I18nProvider` (objectui#4396)
|
|
257
|
+
|
|
258
|
+
`detail.saving`, `list.resetSortToDefault`, `appDesigner.widgetProperties`, `appDesigner.addWidget`, `appDesigner.modeEdit` and `common.delete` were read through `createSafeTranslation` without a row in their hook's defaults table and without an inline `defaultValue` at the call site — the only two fallbacks that path has. On a provider-less host (standalone embedding, the preview gallery, host apps that never mount a provider) `fallbackT` therefore returned the key itself, so users saw `detail.saving` in the inline-edit save button, `list.resetSortToDefault` on the sort popover's reset control, `appDesigner.widgetProperties` as the dashboard inspector heading, `appDesigner.addWidget` as its toolbar label, `appDesigner.modeEdit` as a button's accessible name, and `common.delete` on the designer's destructive confirm.
|
|
259
|
+
|
|
260
|
+
Each key now has a row in its consumer hook's defaults table, byte-identical to the `en` pack value. No pack was edited, no key added, no call site changed.
|
|
261
|
+
|
|
262
|
+
- 7f1cb33: List sort: the relational hint stops recommending a formula field, the one type the server refuses to sort by
|
|
263
|
+
|
|
264
|
+
The Sort panel withholds columns that link to another record and explains why, and the last sentence of that explanation named the remedy: _add a formula field holding it_. A formula field is exactly what the platform will not order by. The server keeps `UNMATERIALIZED_SORT_TYPES = new Set(['formula'])` and, since objectstack#6994, a sort naming one is a hard `400 INVALID_SORT` — before that it degraded silently, returning every row with `asc` and `desc` byte-identical. So an author who read the hint, followed it, and built a formula field arrived at a refusal; and since #4243 withheld formula fields from this very picker, at a field the panel does not offer either. Two doors, opposite advice, for one problem.
|
|
265
|
+
|
|
266
|
+
The remedy sentence now names a **stored, denormalised field — written when the source changes** — and rules the formula field out in as many words: it is virtual, no column is stored for it, and the server refuses to sort by one. That is deliberately the server's own vocabulary rather than a third phrasing of the same fact: objectstack#6924 and objectstack#6994 settled on one wording across the refusal doors so an author refused twice is not sent two different ways, and this is the UI door of that same set. The first half of the hint — why relation columns are withheld at all — is unchanged.
|
|
267
|
+
|
|
268
|
+
All ten locale packs move together, as `check:i18n-drift` requires of any `en` edit. The same sentence also lives in `plugin-list`'s provider-less fallback table, which is what renders when the component is used outside an `I18nProvider`; it is updated to match `en` byte for byte, because a pack-only reword would have left the retired advice on exactly the surface this fixes.
|
|
269
|
+
|
|
270
|
+
- 2e3b0c0: fix(list): an `OBJECT_API_DISABLED` list request renders an honest cannot-work state instead of the empty state
|
|
271
|
+
|
|
272
|
+
A list pointed at an object whose `enable` block withholds the API rendered its ordinary
|
|
273
|
+
empty state, so _"this page cannot work, and never could"_ reached the user as _"you have no
|
|
274
|
+
records"_ (objectui#4408). The reported instance — `Setup › Advanced › Signing Keys`, whose
|
|
275
|
+
`sys_jwks` declares `enable.apiEnabled: false` — could not load for any persona and said so
|
|
276
|
+
to nobody. That is also why the upstream defect objectstack#7544 survived review for its
|
|
277
|
+
whole life: a merely unpopulated page invites nobody to click through.
|
|
278
|
+
|
|
279
|
+
The masking had two halves, in two packages, and neither package could see the other:
|
|
280
|
+
|
|
281
|
+
- **`@object-ui/data-objectstack`** (minor — see the grading note below) — `find()` degraded
|
|
282
|
+
**every** 404 into `{ data: [], total: 0 }` and memoised the resource, so the denial arrived
|
|
283
|
+
at the surface as a successful empty result, indistinguishable from a genuinely empty
|
|
284
|
+
object. The two `enable`-block denials are now let through instead: `OBJECT_API_DISABLED`
|
|
285
|
+
(404) and `OBJECT_API_METHOD_NOT_ALLOWED` (405). The memo skips them too — absorbing one
|
|
286
|
+
would have pinned the object to "empty" for the rest of the session.
|
|
287
|
+
- **`@object-ui/plugin-list`** — the load-error panel gained an `api-disabled` kind. The 405
|
|
288
|
+
half was never swallowed, so it already reached this panel, but classified as `network`:
|
|
289
|
+
_"check your connection and try again"_ for a condition no retry can change. It now says
|
|
290
|
+
the object is not exposed through the API, that this is a setting on the object rather than
|
|
291
|
+
a permission, and it offers **no Retry** button, because every retry re-fetches the
|
|
292
|
+
identical refusal.
|
|
293
|
+
|
|
294
|
+
Both denials are pure functions of the object's metadata — no user, no permission, no
|
|
295
|
+
context — so neither is transient or per-user, which is exactly the case where a silent empty
|
|
296
|
+
state is most misleading. Discrimination is on the ADR-0112 `code`, never the status: a
|
|
297
|
+
missing collection, a missing record and a disabled object are all 404.
|
|
298
|
+
|
|
299
|
+
**A genuinely empty object still renders the ordinary empty state**, and a backend without an
|
|
300
|
+
optional collection still degrades to empty — pinned in both directions, at the adapter, at
|
|
301
|
+
the view, and once end-to-end over a real adapter and a real `ListView`.
|
|
302
|
+
|
|
303
|
+
Also closes a code-propagation gap on the same path: `find()`'s raw `$expand`/`$search`
|
|
304
|
+
branch bypasses `@objectstack/client` and hand-rolled its own error, stamping only `status`.
|
|
305
|
+
It now carries the ADR-0112 envelope (`code` + `httpStatus`), so a denial arriving on the
|
|
306
|
+
branch a list takes whenever it expands a lookup or runs a search is no longer anonymous.
|
|
307
|
+
|
|
308
|
+
New strings: `list.loadErrorApiDisabledTitle` / `list.loadErrorApiDisabledMessage`, in the
|
|
309
|
+
`en` pack and mirrored in the list defaults map.
|
|
310
|
+
|
|
311
|
+
## Grading note — why `@object-ui/data-objectstack` is **minor** and not patch
|
|
312
|
+
|
|
313
|
+
Two independent reasons, either of which is sufficient under this repo's precedent
|
|
314
|
+
(objectui#4403 / #4177, and #4485's grading of `@object-ui/core`'s `toDomProps` lift):
|
|
315
|
+
|
|
316
|
+
1. **The emitted `.d.ts` grows two NEW exports.** `isApiAccessDeniedError(error: unknown):
|
|
317
|
+
boolean` and `API_ACCESS_DENIED_CODES` (the readonly tuple
|
|
318
|
+
`['OBJECT_API_DISABLED', 'OBJECT_API_METHOD_NOT_ALLOWED']`) are added to the package's
|
|
319
|
+
public surface. Additive surface growth is minor.
|
|
320
|
+
2. **Observable behaviour on a published API moves.** `ObjectStackDataSource.find()` now
|
|
321
|
+
**REJECTS** for the two `enable`-block denial codes where it previously **RESOLVED** with
|
|
322
|
+
`{ data: [], total: 0 }`. No signature changed and nothing was removed, but a caller that
|
|
323
|
+
relied on those two codes arriving as a successful empty result now receives a rejected
|
|
324
|
+
promise carrying `code` + `httpStatus`, and must handle it.
|
|
325
|
+
|
|
326
|
+
Deliberately unchanged, and still resolving to an empty result exactly as before: a bare 404
|
|
327
|
+
with no code, `OBJECT_NOT_FOUND` (still memoised) and `RECORD_NOT_FOUND`. The behaviour move
|
|
328
|
+
is scoped to the two denial codes named above and to nothing else.
|
|
329
|
+
|
|
330
|
+
Not major: this follows AGENTS.md's version-alignment rule — objectui's major tracks
|
|
331
|
+
`@objectstack`'s, so this repo's own breaking semantics are declared as minor with the change
|
|
332
|
+
described in the body, which is what this note is.
|
|
333
|
+
|
|
334
|
+
- 31ab1ac: fix(print): `window.print()` produces a usable page, and the Print buttons say what they do
|
|
335
|
+
|
|
336
|
+
The list, report and dashboard Print controls were bare `window.print()` calls with no
|
|
337
|
+
print stylesheet, so the browser printed the whole console — sidebar, top bar, chat rail,
|
|
338
|
+
toasts — with the data table clipped to a single viewport. With no label to the contrary
|
|
339
|
+
they were being accepted against "export to PDF" requirements, which they have never been.
|
|
340
|
+
|
|
341
|
+
- `@object-ui/app-shell/styles.css` gains a shared `@media print` block: it hides the shell
|
|
342
|
+
chrome, prints the active content area full-width, releases the viewport-height flex chain
|
|
343
|
+
so long tables paginate instead of clipping, repeats table headers on every sheet, and
|
|
344
|
+
neutralises dark mode (which otherwise prints white-on-white). One sheet serves list,
|
|
345
|
+
report and dashboard.
|
|
346
|
+
- The list and report Print buttons carry a tooltip and accessible name stating that they
|
|
347
|
+
open the browser's own print dialog and are not a PDF export (new `common.printDialogHint`,
|
|
348
|
+
translated in all ten locale packs).
|
|
349
|
+
- The dashboard's `export_dashboard_pdf` action no longer toasts "Preparing PDF export…" —
|
|
350
|
+
it names the print dialog it actually opens (`dashboardActions.pdfPreparing` is replaced by
|
|
351
|
+
`dashboardActions.printDialogOpening`).
|
|
352
|
+
|
|
353
|
+
No control was removed and no headless detection was added. A real print/PDF primitive
|
|
354
|
+
remains out of scope (`objectstack-ai/objectstack#1301`, closed NOT_PLANNED).
|
|
355
|
+
|
|
356
|
+
- ff84b05: Stop the report config panel being titled "Title", and the view-settings colour section "Color"
|
|
357
|
+
|
|
358
|
+
Two call sites asked for a key whose value was written for a different slot, so the rendered copy was wrong (objectui#4118, surfaced by objectui#3810's census).
|
|
359
|
+
|
|
360
|
+
`ReportConfigPanel` used `report.editor.title` for both its heading and the accessible name of its `role="complementary"` landmark. That key is the label of the report's Title _field_ — `report.editor.titlePlaceholder` ('e.g. Pipeline by Quarter') sits directly under it in the pack. So the panel was headed "Title", and a screen reader announced a complementary region named "Title", which says nothing about what the region is. A new `report.editor.panelTitle` ('Edit report' — what the call site's own dead fallback said before objectui#3810 aligned it to the pack) now names the panel, in all ten locale packs.
|
|
361
|
+
|
|
362
|
+
`ViewSettingsPopover`'s colour section used `list.color`. On the wide toolbar `ListView` already uses both keys correctly for the two slots of this one feature: the compact `Paintbrush` button is `list.color` ('Color') and the panel it opens is headed `list.rowColor` ('Row Color'). This popover is that same panel on the collapsed/`compactToolbar` surface, so it now takes `list.rowColor` — an existing key, no pack change.
|
|
363
|
+
|
|
364
|
+
No `en` value of an existing key changed; `scripts/check-i18n-en-drift.mjs` reports 0 en values changed, 1 key added.
|
|
365
|
+
|
|
366
|
+
## 17.4.0
|
|
367
|
+
|
|
368
|
+
### Minor Changes
|
|
369
|
+
|
|
370
|
+
- bd863fe: fix(timeline): the timeline binds to the date axis the view actually declares (#3129)
|
|
371
|
+
|
|
372
|
+
A view whose date axis is bound under `calendar` was **offered** the Timeline
|
|
373
|
+
visualization and then bucketed every record into "No date" — while the calendar
|
|
374
|
+
rendered the very same field correctly. Two read-sites disagreed about what
|
|
375
|
+
counts as a timeline binding:
|
|
376
|
+
|
|
377
|
+
- `ListView`'s capability gate accepted `options.calendar.startDateField` as a
|
|
378
|
+
timeline-resolvable axis; the render branch never read calendar config at all,
|
|
379
|
+
so it fell through to its `created_at` last resort.
|
|
380
|
+
- `app-shell`'s object page emitted `startDateField: 'due_date'` into
|
|
381
|
+
`options.timeline` for **every** object view, declared or not. Downstream that
|
|
382
|
+
is indistinguishable from a real binding, and because it is always present it
|
|
383
|
+
shadowed the fallback entirely.
|
|
384
|
+
|
|
385
|
+
`ListView` now resolves the axis once — `resolveTimelineDateBinding`, consumed by
|
|
386
|
+
the capability gate and the render branch alike, reading spec key before legacy
|
|
387
|
+
alias and `timeline` before `calendar` in both nestings — and the object page
|
|
388
|
+
forwards only what the view declared. A declared `timeline.startDateField` still
|
|
389
|
+
wins wherever both appear, and a view that declares no date axis anywhere keeps
|
|
390
|
+
the historical `created_at` fallback.
|
|
391
|
+
|
|
392
|
+
Observable rendering change (records move out of "No date" into real date
|
|
393
|
+
buckets), hence `minor`.
|
|
394
|
+
|
|
395
|
+
### Patch Changes
|
|
396
|
+
|
|
397
|
+
- e06810e: `PageComponentSchema.dataSource` is now consumed instead of discarded — a
|
|
398
|
+
`list-view` page component can reference a **saved view by name** for the first
|
|
399
|
+
time, and writing the binding no longer breaks the component
|
|
400
|
+
(objectstack#5576).
|
|
401
|
+
|
|
402
|
+
The spec declares a per-element data binding on every page component —
|
|
403
|
+
`dataSource: { object, view?, filter?, sort?, limit? }` — and objectui read none
|
|
404
|
+
of it. `ViewDataProvider.resolveElementDataSource` forwarded
|
|
405
|
+
`filter`/`sort`/`limit` and dropped `view` entirely, and had no caller outside its
|
|
406
|
+
own test; nothing mapped `object` onto the `objectName` a list actually reads. So
|
|
407
|
+
"reference a saved view by name" was published, validated and inert, and every
|
|
408
|
+
page that wanted a saved view's columns/filter/sort had to inline a second copy of
|
|
409
|
+
them — the drift the binding exists to remove.
|
|
410
|
+
|
|
411
|
+
Writing the binding also **broke** the block, for a reason unrelated to `view`:
|
|
412
|
+
`SchemaRenderer` spread the schema's `dataSource` metadata onto the component as a
|
|
413
|
+
React prop, and that is the prop name the host uses to inject the data-source
|
|
414
|
+
ADAPTER. The plain `{ object, view }` object shadowed the adapter, so the first
|
|
415
|
+
`dataSource.find(…)` threw `dataSource.find is not a function` and `list-view`
|
|
416
|
+
rendered "Couldn't load records" — a spec-compliant component failing next to
|
|
417
|
+
identical ones that omitted the binding.
|
|
418
|
+
|
|
419
|
+
- `@object-ui/react` — `SchemaRenderer` no longer spreads `schema.dataSource` as a
|
|
420
|
+
prop (it is metadata, like `visibleWhen`); renderers read it off `schema`. An
|
|
421
|
+
explicit React `dataSource` prop is unaffected. New
|
|
422
|
+
`useElementDataSource(schema, dataSource?)` hook resolves a binding, fetching
|
|
423
|
+
the named saved view from the object definition's `listViews` and the metadata
|
|
424
|
+
overlay's `listViews()`.
|
|
425
|
+
- `@object-ui/core` — new `isElementDataSourceConfig` / `collectSavedViews` /
|
|
426
|
+
`resolveSavedView` / `composeElementDataSource`, and `resolveElementDataSource`
|
|
427
|
+
now honours `view` through an optional `DataFetcher.fetchViews`, reporting an
|
|
428
|
+
unresolvable view as an error instead of silently returning every record.
|
|
429
|
+
`resolveViewId` moved here from `@object-ui/app-shell` (re-exported there) so
|
|
430
|
+
one matcher serves both the object page and a page component.
|
|
431
|
+
- `@object-ui/plugin-list` — `list-view` maps the binding onto the props
|
|
432
|
+
`ListView` reads. `dataSource.*` keys are authoritative, view-supplied values
|
|
433
|
+
are a baseline the component's own keys override, and `filter` AND-combines at
|
|
434
|
+
every level (the spec calls the binding's filter "additional criteria"), so a
|
|
435
|
+
binding can narrow a saved view but never widen it. A `view` name that does not
|
|
436
|
+
resolve renders a configuration error naming the object's actual views and
|
|
437
|
+
issues no query — it never falls back to the object's default view, because that
|
|
438
|
+
turns a typo into a silently wider answer.
|
|
439
|
+
|
|
440
|
+
- aeb8424: List row Edit/Delete, bulk delete and related-list CRUD now run the caller's own permission, not just the object's API exposure (objectui#4096)
|
|
441
|
+
|
|
442
|
+
The row kebab's built-in Edit/Delete rendered for every account, including ones
|
|
443
|
+
the server answers `403 PERMISSION_DENIED` on. Clicking Edit opened a fully
|
|
444
|
+
prefilled dialog that could only fail on save; Delete — a destructive entry —
|
|
445
|
+
sat one click away from users who could never perform it.
|
|
446
|
+
|
|
447
|
+
The gate intersected the object's resolved CRUD affordance with the server's
|
|
448
|
+
effective API operation set (`/me/permissions` `apiOperations`, objectui#3720),
|
|
449
|
+
and nothing else. `apiOperations` is the object's **API exposure surface** —
|
|
450
|
+
"which verbs does this object publish" — and the spec's own describe text says
|
|
451
|
+
so. It is principal-independent: the report measured two accounts with opposite
|
|
452
|
+
`allowEdit`, 30 shared objects, and **30/30 identical** `apiOperations`. A gate
|
|
453
|
+
made only of object-scoped layers therefore fails OPEN for every unprivileged
|
|
454
|
+
caller, which is why the same screen carried three different answers to "may
|
|
455
|
+
this user write this object": the toolbar's New was correctly hidden
|
|
456
|
+
(`affordances.create && can(obj, 'create')`), the record header's Edit/Delete
|
|
457
|
+
were correctly hidden (per-record write probe), and the row kebab was not.
|
|
458
|
+
|
|
459
|
+
Four surfaces now AND the principal's own verdict — `can(obj, 'update' |
|
|
460
|
+
'delete')`, i.e. `/me/permissions` `allowEdit` / `allowDelete`, the toolbar's
|
|
461
|
+
source — on top of the layers they already had:
|
|
462
|
+
|
|
463
|
+
- the grid row kebab's built-in Edit/Delete (`resolveRowCrudAffordances` gained
|
|
464
|
+
`permissionUpdate` / `permissionDelete`, filled at the `ObjectGrid` call site);
|
|
465
|
+
- the grid's bulk-delete bar, which rides the same object-level delete verdict,
|
|
466
|
+
so the row gate and the more destructive bulk entry move together;
|
|
467
|
+
- the non-grid (kanban / calendar / gallery) bulk bar `ListView` renders itself;
|
|
468
|
+
- the related-list Create/Edit/Delete on a child object
|
|
469
|
+
(`RelatedRecordActionsBridge`), which had the same object-only gate.
|
|
470
|
+
|
|
471
|
+
**This is a tightening of the intersection, not a swap.** Every existing layer
|
|
472
|
+
stays: the ADR-0103 lifecycle bucket, `userActions.edit` / `delete`, and
|
|
473
|
+
`apiOperations`. A permission grant cannot re-open what any of them closed, and
|
|
474
|
+
none of them survives a permission denial.
|
|
475
|
+
|
|
476
|
+
Fail-open is preserved where it is the deliberate contract: `usePermissions()`
|
|
477
|
+
with no `PermissionProvider` answers `can: () => true`, so standalone embeds and
|
|
478
|
+
hosts that ship no permission source keep their Edit/Delete exactly as before.
|
|
479
|
+
Under `MePermissionsProvider` the semantics are the toolbar's, unchanged and now
|
|
480
|
+
shared: an authenticated principal whose object is absent from
|
|
481
|
+
`/me/permissions.objects` resolves fail-closed (objectui#2926 ④), an anonymous
|
|
482
|
+
session keeps the permissive default, and children never render while the
|
|
483
|
+
permission set is loading. Per-key absence is still permissive — an object entry
|
|
484
|
+
without `allowEdit` reads as allowed.
|
|
485
|
+
|
|
486
|
+
Server-side enforcement was already hard (403, DB unchanged), so this closes a
|
|
487
|
+
UI-affordance gap rather than an authorization hole.
|
|
488
|
+
|
|
489
|
+
- cb5e32d: `UserFilters` preset tab buttons no longer submit an enclosing form; all six buttons declare `type="button"`
|
|
490
|
+
|
|
491
|
+
An HTML `<button>` defaults to `type="submit"` inside a `<form>`, so a preset
|
|
492
|
+
filter tab (`filter-tab-*`, tabs mode) submitted the enclosing form on every
|
|
493
|
+
click. The three buttons objectstack#6952 named now declare `type="button"`
|
|
494
|
+
explicitly — the dropdown chip trigger (`filter-badge-*`), the overflow trigger
|
|
495
|
+
(`user-filters-more`) and the preset tab — joining the session-tab buttons that
|
|
496
|
+
objectstack#5236 already declared it on.
|
|
497
|
+
|
|
498
|
+
Only one of the three was actually at risk, and the difference is measured
|
|
499
|
+
rather than assumed. The chip and the overflow trigger are
|
|
500
|
+
`PopoverTrigger asChild` children, and Radix's `PopoverTrigger` renders
|
|
501
|
+
`Primitive.button type="button"`; its Slot merges that onto a child declaring no
|
|
502
|
+
`type` of its own, so both already rendered as `button`. Reverting the change
|
|
503
|
+
confirms it: those two keep reading `button`, the plain preset tab button reads
|
|
504
|
+
`null`. For the two triggers this therefore moves a contract out of an upstream
|
|
505
|
+
implementation detail and into local source — the same reasoning objectui#3344
|
|
506
|
+
wrote onto the Combobox trigger — while the preset tab is a real fix.
|
|
507
|
+
|
|
508
|
+
Dormant rather than live: the only mount point today is `ListView`'s toolbar,
|
|
509
|
+
which is not inside a form, so no shipped screen submitted anything. The new
|
|
510
|
+
tests pin every rendered `UserFilters` button, in both modes, so a future button
|
|
511
|
+
cannot land at the submit default and an upstream Radix change surfaces in this
|
|
512
|
+
package's tests instead of in a user's form.
|
|
513
|
+
|
|
514
|
+
The in-file comment claiming "a Radix trigger keeps the HTML default of `submit`"
|
|
515
|
+
is corrected in passing — it is the inaccuracy that propagated into
|
|
516
|
+
objectstack#6952's premise.
|
|
517
|
+
|
|
518
|
+
- cf5be4e: `userFilters` tabs: the `allowAddTab` button now adds a tab instead of doing nothing (objectstack#5236)
|
|
519
|
+
|
|
520
|
+
The affordance `allowAddTab` renders had hover styling and `title="Add filter tab"` but no `onClick`, and `TabFilters` took no add-tab callback at all — a control that looked fully clickable and did nothing, which disguises "not implemented" as "a bug where clicking does nothing". That mattered more once objectstack#5073 promoted `allowAddTab` into the spec's `UserFiltersSchema`: the key became discoverable through JSON Schema, the Studio SchemaForm and the reference docs, so an author writing `allowAddTab: true` gets a declaration the runtime did not honour.
|
|
521
|
+
|
|
522
|
+
Clicking it now opens a small naming popover (the same Popover primitive the filter chips and the "More" overflow already use). Confirming a name adds a tab to the same bar as the presets, carrying a snapshot of the conditions applied at that moment, and selects it. Session tabs also carry a remove affordance; authored presets deliberately do not, since those are metadata. Removing the active session tab re-selects the author's default with the same precedence the initial mount uses, so the bar is never left with no active tab while the removed tab's conditions stay applied.
|
|
523
|
+
|
|
524
|
+
The new tab is **session-scoped, held in component state** — no `sys_metadata` write, no API call, no web storage, per ADR-0047 ("an end user's filter choices are session-scoped and never become metadata"). `sessionStorage` was available and deliberately not used: `UserFilters` receives no object or view identity, so any storage key it could invent would be shared by every list in the browser tab, surfacing one list's ad-hoc tabs on another's bar. Persistence beyond the mount, if ever wanted, belongs to the host that already owns the session channel for filter selections (`onSelectionsChange` mirrored into `uf_*` URL params) and can key it by view. The synthetic tab id is reported through `onSelectionsChange` like any other tab switch, so a host mirroring it into the URL hands it back on the next mount, where the existing id check finds no such tab and falls back to the author's default.
|
|
525
|
+
|
|
526
|
+
No public API change: `UserFiltersProps` is untouched, and `allowAddTab: false` / an omitted `allowAddTab` still render no affordance at all.
|
|
527
|
+
|
|
528
|
+
## 17.3.0
|
|
529
|
+
|
|
530
|
+
### Patch Changes
|
|
531
|
+
|
|
532
|
+
- 978705c: Gallery covers now resolve the `coverField` value through its **file value
|
|
533
|
+
shape** instead of assuming the field value _is_ a URL string, so an
|
|
534
|
+
ADR-0104-conforming `image` value renders a cover again (objectui#3317).
|
|
535
|
+
|
|
536
|
+
Since ADR-0104 D3 wave 2 the stored value of a `file`/`image`/`avatar`/
|
|
537
|
+
`video`/`audio` field is an opaque `sys_file` id, which the read path expands
|
|
538
|
+
in place into `{ id, name, size, mimeType, url }`. `ObjectGallery` read the
|
|
539
|
+
value twice — `hasAnyCover` tested `typeof value === 'string'`, and each card
|
|
540
|
+
did `item[coverField] as string` — so against a spec-correct object value the
|
|
541
|
+
cover area collapsed for the whole gallery, and the card underneath it built
|
|
542
|
+
an `<img src="[object Object]">`. The only values that ever rendered were the
|
|
543
|
+
inline `data:` URIs and external links ADR-0104 retired, which is why this
|
|
544
|
+
stayed invisible.
|
|
545
|
+
|
|
546
|
+
## What changed
|
|
547
|
+
|
|
548
|
+
- Both reads now share one `resolveCoverUrl`, so the "does anything have a
|
|
549
|
+
cover?" predicate and the per-card render can no longer disagree — that
|
|
550
|
+
disagreement is what collapsed the area for records that did have a cover.
|
|
551
|
+
- Shape handling is delegated to `readFileValues` from `@object-ui/fields`,
|
|
552
|
+
the platform's existing single arbiter of file value shapes, rather than
|
|
553
|
+
re-derived in the gallery. It accepts the expanded `{ url }` object, a
|
|
554
|
+
legacy bare URL string (still valid during the dual-mode window), and a
|
|
555
|
+
still-bare `sys_file` id — which resolves to the stable
|
|
556
|
+
`/api/v1/storage/files/:id` endpoint instead of reaching `<img src>` as a
|
|
557
|
+
raw opaque token. A value carrying no resolvable URL yields no cover, which
|
|
558
|
+
collapses the area rather than emitting a broken `src`.
|
|
559
|
+
- A `multiple` file field's first entry is used as the cover.
|
|
560
|
+
|
|
561
|
+
The sibling paths that thread `coverField`/`imageField` around
|
|
562
|
+
(`ListView`, `app-shell/ObjectView`, `plugin-view/ObjectView`) pass the field
|
|
563
|
+
**name**, not the value, and needed no change.
|
|
564
|
+
|
|
565
|
+
- d915c47: Relation fields (`lookup` / `master_detail` / `user` / `tree`) are now usable in action and conditional-formatting predicates: they bind as the stored foreign key on every surface, and the fields a predicate reads are included in the query projection (#3501).
|
|
566
|
+
|
|
567
|
+
Before this, one predicate over one relation field had four different fates, decided by things its author does not control. `$expand` **replaces** the id in place with the whole related record, and a view expands exactly the relations it shows as COLUMNS — so `record.owner == "U1"` was **true** where the column was absent, **false** where it was displayed, and a **fault** where the field was neither displayed nor projected (a list's `$select` was built from its columns alone, and CEL treats an absent key as a fault, not as null). A fault is fail-CLOSED on the row kebab and the selection bar and fail-OPEN on the lenient paths, so the same authoring mistake hid the button from everyone on one surface and showed it to everyone on the next, with nothing on screen to point at either. The server, meanwhile, only ever sees the id — so client and server could not agree, which is the one thing ADR-0036 / ADR-0058 exist to guarantee.
|
|
568
|
+
|
|
569
|
+
Two changes close it. `toPredicateRecord` (new, `@object-ui/core`) collapses expanded relation values back to their ids when a record is bound for evaluation — driven by the object's own field types, not by sniffing for an `id` key, so a `json` field that happens to carry one is untouched. It is threaded through `evalRowPredicate` / `resolveConditionalFormatting` (via a new `fields` option), `useRowPredicate`, `partitionBulkRows`, and both `page:header` evaluators, with the object schema supplied by `ObjectGrid` / `ListView` / `ObjectKanban` / the record context. Kanban card formatting is threaded the same way, so a rule cannot match on the grid view of a list and silently never match on its board. Display is unaffected — a detail-page title still renders the related record's name, and the schema-only `kanban-ui` entry point (which has no object schema to offer) keeps using the payload verbatim. `collectPredicateFieldRefs` / `listViewPredicates` (new) harvest the `record.x` / `data.x` references out of a view's conditional formatting, row-action defs, bulk-action defs, promoted object actions and `userActions` overrides, and add them to `$select` — intersected with the object's declared fields plus the platform columns every object carries (`isProjectableField`), because an unknown key is not ignored by every backend. No `$expand` is added: a predicate wants the foreign key, which is what an unexpanded relation already is.
|
|
570
|
+
|
|
571
|
+
- 6195841: Localize the record-detail overlay heading that `ListView` and `ObjectGrid`
|
|
572
|
+
build themselves (objectui#3426)
|
|
573
|
+
|
|
574
|
+
#3423 gave `NavigationOverlay`'s `resolvedTitle` an i18n default
|
|
575
|
+
(`detail.recordDetail`), but two hosts never let that default run: they
|
|
576
|
+
string-built an English heading in TypeScript and passed it as the `title`
|
|
577
|
+
prop, so a zh/ja/de session got a fully localized drawer with one English
|
|
578
|
+
heading on it.
|
|
579
|
+
|
|
580
|
+
- `packages/plugin-list/src/ListView.tsx` — `` `${schema.label} Detail` ``
|
|
581
|
+
- `packages/plugin-grid/src/ObjectGrid.tsx` — the same template, plus a bare
|
|
582
|
+
`'Record Detail'` literal for the no-label case
|
|
583
|
+
|
|
584
|
+
Both are user-reachable, not dead defaults. `list-view` / `object-grid` are
|
|
585
|
+
public page blocks and `navigation` is an authorable key on their schema, so a
|
|
586
|
+
page that authors `navigation: { mode: 'drawer' }` opens exactly this overlay
|
|
587
|
+
on row click. (`app-shell`'s `ObjectView` does suppress it — it passes its own
|
|
588
|
+
`onRowClick`, which takes priority inside `useNavigationOverlay`, and renders
|
|
589
|
+
its own overlay — but that is one host overriding a public block, not proof the
|
|
590
|
+
branch is unreachable.)
|
|
591
|
+
|
|
592
|
+
## What changed
|
|
593
|
+
|
|
594
|
+
Both call sites now key their heading instead of concatenating it:
|
|
595
|
+
|
|
596
|
+
- a new `detail.recordDetailWithLabel` (`'{{label}} Detail'`) carries the
|
|
597
|
+
object label through interpolation, so a pack whose qualifier trails the noun
|
|
598
|
+
(`de`) or that needs a possessive particle (`ja`/`zh`) can write its own
|
|
599
|
+
arrangement rather than inherit English word order;
|
|
600
|
+
- the no-label branch reuses `detail.recordDetail` — the very key the overlay
|
|
601
|
+
itself defaults to — so one heading on one control cannot drift into two
|
|
602
|
+
translations.
|
|
603
|
+
|
|
604
|
+
The new key is added to all ten locale packs and to each plugin's English
|
|
605
|
+
defaults map (`LIST_DEFAULT_TRANSLATIONS` / `GRID_DEFAULT_TRANSLATIONS`), which
|
|
606
|
+
is what `createSafeTranslation` falls back to with no `I18nProvider` mounted.
|
|
607
|
+
|
|
608
|
+
English output is byte-identical in every branch (`Contacts Detail` /
|
|
609
|
+
`Contacts Detail` / `Record Detail`), with and without a provider — pinned by a
|
|
610
|
+
provider-less test file per plugin, kept separate because `initReactI18next`
|
|
611
|
+
registers its instance as a module global that outlives `cleanup()`.
|
|
612
|
+
|
|
613
|
+
- d7f350a: `UserFilters` no longer carries its own operator table when it lowers a `ViewTab.filter` preset into an ObjectQL AST node. The private `specOperatorToAst` was the second hand-kept operator map in this package and it had drifted: it lowered `not_in` — the spec's OWN canonical spelling — and the legacy `nin` to the spaced `'not in'`, a spelling that appears in no spec vocabulary. `isFilterAST` refuses it, so clicking such a tab produced an empty list plus `400 INVALID_FILTER`. Measured against a real backend (published `@objectstack/*@17.0.0-rc.2` + app-showcase, on `showcase_task`): `$filter=[["status","not in",["done"]]]` returned `400 INVALID_FILTER`, while `[["status","not_in",["done"]]]` returned `200` with the same 8 records as the `["status","!=","done"]` baseline.
|
|
614
|
+
|
|
615
|
+
Lowering is now purely structural — all 19 `VIEW_FILTER_OPERATORS` are already members of the wire's `VALID_AST_OPERATORS`, so nothing needs translating — with the spec's own `normalizeFilterOperator` as the single exit for the legacy spellings stored metadata still carries (`gt`, `eq`, `nin`, `notEquals`, …). That is the same exit the write side (`viewFilterFold`) and the saved-view fold in `@object-ui/core` already use, so the directions cannot drift into two dialects. An operator the spec does not know is passed through verbatim, so a misspelling still earns a loud `400` rather than being coerced into a valid filter.
|
|
616
|
+
|
|
617
|
+
`before` and `after` are now passed through rather than rewritten to `<` and `>`. That was the one judgement call, and it was settled by measurement rather than assumption: on the same live backend the word and the symbol return identical status and identical record ids, on a `date` field and on a `datetime` field, in both directions — so the rewrite was a no-op and dropping it is a pure fix. The remaining 18 canonical operators were measured the same way and are likewise unchanged in what the server answers; `not_in` is the only one whose answer changes, from `400` to the correct rows. Tab presets given in the legacy already-lowered `filters: triplet[]` shape are untouched, as before.
|
|
618
|
+
|
|
3
619
|
## 17.2.0
|
|
4
620
|
|
|
5
621
|
### Minor Changes
|
|
@@ -1293,6 +1909,7 @@ colorField, scale }` arrived with only `titleField` and an axis pinned to the
|
|
|
1293
1909
|
platform primitive for "no records / no data" states. Two new props
|
|
1294
1910
|
keep it flexible enough to absorb the hand-rolled variants that lived
|
|
1295
1911
|
in `plugin-list`, `plugin-kanban`, and `plugin-dashboard`:
|
|
1912
|
+
|
|
1296
1913
|
- `showIcon?: boolean` — drops the icon container entirely. Used by the
|
|
1297
1914
|
kanban board-level empty banner, which is a status banner rather than
|
|
1298
1915
|
a true empty-state.
|
|
@@ -1301,6 +1918,7 @@ colorField, scale }` arrived with only `titleField` and an axis pinned to the
|
|
|
1301
1918
|
empty state, which uses a large standalone glyph).
|
|
1302
1919
|
|
|
1303
1920
|
Adopters:
|
|
1921
|
+
|
|
1304
1922
|
- `plugin-list` (`ListView` grid empty-state) — preserves the existing
|
|
1305
1923
|
large icon, title, message, add-record button and `data-testid`s,
|
|
1306
1924
|
but delegates the structural markup to `DataEmptyState`.
|
|
@@ -1377,6 +1995,7 @@ colorField, scale }` arrived with only `titleField` and an axis pinned to the
|
|
|
1377
1995
|
chrome on phones: title + 1 primary action, plus content. We were
|
|
1378
1996
|
shipping ~5 rows of toolbars + chips + tabs above the data. This commit
|
|
1379
1997
|
hides the desktop-only chrome at the `<sm` breakpoint:
|
|
1998
|
+
|
|
1380
1999
|
- **ListView**: TabBar (view switcher), UserFilters chip row, quick-filters
|
|
1381
2000
|
chip row, Sort button, list-scoped Search popover, and the
|
|
1382
2001
|
(newly-added) mobile-only ViewSettingsPopover gear are all hidden on
|
|
@@ -1398,6 +2017,7 @@ colorField, scale }` arrived with only `titleField` and an axis pinned to the
|
|
|
1398
2017
|
|
|
1399
2018
|
Refactor mobile object-view layout to match the Airtable Interface
|
|
1400
2019
|
pattern:
|
|
2020
|
+
|
|
1401
2021
|
- **AppHeader**: the mobile topbar's static page label is now a
|
|
1402
2022
|
view-switcher dropdown (`<viewName> ▾`). Tapping opens a list of
|
|
1403
2023
|
available views with icons + active-state checkmark. Falls back to
|
|
@@ -1528,6 +2148,7 @@ colorField, scale }` arrived with only `titleField` and an axis pinned to the
|
|
|
1528
2148
|
Library builds (vite lib mode) now externalize every non-relative import instead of bundling third-party CJS dependencies into the published dist. This avoids inlined `require("react")` / `require("react-dom")` calls that cause `Calling \`require\` for "react" in an environment that doesn't expose the \`require\` function` runtime errors when consumer apps re-bundle the published dist.
|
|
1529
2149
|
|
|
1530
2150
|
Specifically fixes:
|
|
2151
|
+
|
|
1531
2152
|
- `@object-ui/plugin-dashboard` no longer inlines `react-grid-layout` (and its transitive `react-draggable` / `react-resizable` CJS bundles). `react-grid-layout` is now declared as a peer dependency so consumers install a single ESM-friendly copy.
|
|
1532
2153
|
- `@object-ui/components`, `@object-ui/plugin-calendar`, `@object-ui/plugin-charts`, `@object-ui/plugin-designer` no longer inline `react-i18next` / `i18next` / `use-sync-external-store` CJS shims.
|
|
1533
2154
|
- All plugin packages now use a unified `external: (id) => !/^[./]/.test(id) && !id.startsWith(__dirname)` rule, ensuring future additions of CJS deps are automatically externalized.
|