From a551c491fa94d313dddd88755805c59c6e140f4b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 15:46:21 +0000 Subject: [PATCH 1/2] refactor(app-shell): Explain access picks its object and operation with the shared Select (objectui#11865) The Explain access panel's two native selects, Object (the package's objects) and Operation, become one local ExplainPicker on the shared Select. Items carry their option's index, so the "select an object" option (value '') needs no empty SelectItem, and an object the package does not list is shown in the trigger instead of the first option. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude --- .../metadata-admin/AccessExplainPanel.tsx | 118 ++++++++++++++---- 1 file changed, 97 insertions(+), 21 deletions(-) diff --git a/packages/app-shell/src/views/metadata-admin/AccessExplainPanel.tsx b/packages/app-shell/src/views/metadata-admin/AccessExplainPanel.tsx index 5aade6a7da..7476be2417 100644 --- a/packages/app-shell/src/views/metadata-admin/AccessExplainPanel.tsx +++ b/packages/app-shell/src/views/metadata-admin/AccessExplainPanel.tsx @@ -30,6 +30,11 @@ import { Badge, Button, Input, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, Sheet, SheetContent, SheetDescription, @@ -91,6 +96,77 @@ import type { type ExplainOperation = ExplainDecision['operation']; const OPERATIONS = ['read', 'create', 'update', 'delete', 'transfer', 'restore', 'purge'] as const satisfies readonly ExplainOperation[]; +/** The item a value none of a picker's options carries is shown by. */ +const OUTSIDE_OPTIONS = 'outside'; + +/** + * objectui#11865 — the request form's two pickers (object, operation), drawn + * with the shared `Select`, the control the validation rule editor's pickers + * (`RulePicker`) pick with. Both used to be browser-native ` { + // `undefined` for the outside item: it is the current value, so there is nothing to set. + const picked = options[Number(token)]; + if (picked) onPick(picked.value); + }} + > + + + + + {at === -1 && {value}} + {options.map((o, i) => ( + + {o.label} + + ))} + + + ); +} + +/** The classes a picker takes, to sit at the height of the form's inputs. */ +const PICKER_CLASS = 'h-8 px-2 text-xs'; + /** Pipeline layer ids, derived from the spec's `ExplainLayer` (C2 adds `tenant_isolation`). */ export type ExplainLayerId = ExplainLayer['layer']; @@ -368,19 +444,21 @@ export function AccessExplainPanel({ open, onOpenChange, defaultObject, packageI // objectui#2600 B2 — package-scoped dropdown: the object must be // one of this package's ~20 objects, so free-text (with an // example that isn't even in the package) only invites typos. - + onPick={setObjectName} + options={[ + { value: '', label: t('engine.studio.access.explain.objectSelect', locale) }, + ...objectOptions.map((o) => ({ + value: o.name, + label: o.label && o.label !== o.name ? `${o.label} (${o.name})` : o.name, + })), + ]} + /> ) : ( {t('engine.studio.access.explain.operation', locale)} - + onPick={(picked) => { + const op = OPERATIONS.find((o) => o === picked); + if (op) setOperation(op); + }} + options={OPERATIONS.map((op) => ({ value: op, label: opLabel(op) }))} + /> From 0c5300d7c1e7eac6c9fe90f60cc8f79532005795 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 15:49:36 +0000 Subject: [PATCH 2/2] test(app-shell): pin Explain access's shared-Select pickers on the request they send (objectui#11865) A new suite pins both pickers as the Radix trigger named by their labels, every option against the explain request the native control's change produced, re-pick setting nothing, an object outside the package shown in the trigger, and Enter opening and selecting. The package-scoped dropdown test in AccessExplainPanel.test.tsx picks through the trigger instead of a native select. Patch changeset for @object-ui/app-shell. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude --- .../11865-explain-access-shared-select.md | 15 ++ ...ssExplainPanel.sharedSelect-11865.test.tsx | 213 ++++++++++++++++++ .../AccessExplainPanel.test.tsx | 14 +- 3 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 .changeset/11865-explain-access-shared-select.md create mode 100644 packages/app-shell/src/views/metadata-admin/AccessExplainPanel.sharedSelect-11865.test.tsx diff --git a/.changeset/11865-explain-access-shared-select.md b/.changeset/11865-explain-access-shared-select.md new file mode 100644 index 0000000000..bbc499119f --- /dev/null +++ b/.changeset/11865-explain-access-shared-select.md @@ -0,0 +1,15 @@ +--- +'@object-ui/app-shell': patch +--- + +Studio's *Explain access* panel picks the object and the operation with the shared `Select`, the control Studio's validation rule editor already picks with (objectui#11865, the *Explain access* part of that card). + +The two pickers were browser-native selects, so they looked and behaved differently from Studio's other dropdowns. They now use the shared Radix `Select`: the same trigger, dropdown and keyboard behaviour. In the object list, typing an object's leading letters jumps to it. + +What they set is unchanged. Each option makes Explain send the same request as before: an object sends its name, an operation sends its name, and "Select an object…" leaves Explain disabled until an object is picked. Re-picking the current option changes nothing. Each picker keeps the accessible name its label gave the native select. + +One display change: when the object the panel holds is not one of the package's objects, the object picker now shows that object. The native select showed "Select an object…" instead, while Explain still asked about the held object. + +Unchanged: without a package's object list the object is still a free-text input, and the user picker, the record selector and the explain request itself are untouched. + +**Clause-②: no.** No published face moves: `AccessExplainPanel` takes the same props, the package entry exports the same names, and no i18n key is added. What moves is the panel's own markup, described above. diff --git a/packages/app-shell/src/views/metadata-admin/AccessExplainPanel.sharedSelect-11865.test.tsx b/packages/app-shell/src/views/metadata-admin/AccessExplainPanel.sharedSelect-11865.test.tsx new file mode 100644 index 0000000000..aded611b64 --- /dev/null +++ b/packages/app-shell/src/views/metadata-admin/AccessExplainPanel.sharedSelect-11865.test.tsx @@ -0,0 +1,213 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * *Explain access* picks its object and its operation with the shared `Select` + * (objectui#11865). + * + * The request form picked the object (a package's objects, plus "select an + * object") and the operation with browser-native selects, beside the shared + * Radix `Select` the rest of Studio picks with. The card asks for one control + * for one kind of choice, surface by surface; this suite covers this panel's + * two pickers. + * + * What is pinned: + * - each picker IS the primitive (a Radix combobox trigger), keeps the name + * its `