From 56f889e00ed59231c43d70059b93dae6c6866544 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 10:55:10 +0000 Subject: [PATCH 1/2] fix(plugin-list): a list view locked by lockedBy alone shows its lock indicator The toolbar's sharing indicator rendered only under a declared sharing.type, so a view authored with only lockedBy (the spec defaults type to collaborative, and the served view carries the authored body) showed no lock. The indicator now keys on type or lockedBy, and an absent type reads the spec's ViewSharingSchema default instead of restating it. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01QsR2jSSoRjvEqsGsZZLPC9 --- ui/packages/plugin-list/src/ListView.tsx | 32 +++++++++++++---- .../src/__tests__/ListView.test.tsx | 35 +++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/ui/packages/plugin-list/src/ListView.tsx b/ui/packages/plugin-list/src/ListView.tsx index 469d4e3fa77..ccc95a10c76 100644 --- a/ui/packages/plugin-list/src/ListView.tsx +++ b/ui/packages/plugin-list/src/ListView.tsx @@ -42,7 +42,7 @@ import { useObjectLabel, useSafeFieldLabel, createSafeTranslation, useDisplayLoc // objectui's keyed `{ key, defaultValue, params }` ref — that vocabulary lives // on the FLAT `schema.ariaLabel` and is resolved by `SchemaRenderer` instead // (objectui#5134). -import { resolveI18nLabel as resolveInlineI18nLabel, normalizeFilterOperator, PaginationConfigSchema } from '@objectstack/spec/ui'; +import { resolveI18nLabel as resolveInlineI18nLabel, normalizeFilterOperator, PaginationConfigSchema, ViewSharingSchema } from '@objectstack/spec/ui'; import type { GroupingConfig } from '@objectstack/spec/ui'; import { resolveInjectedSystemColumns, resolveSearchFieldResolution, type SearchFieldMeta } from '@objectstack/spec/data'; import { SystemFieldName } from '@objectstack/spec/system'; @@ -1311,6 +1311,23 @@ function readSpecDisplayPageSize(): number { } const DEFAULT_LIST_DISPLAY_PAGE_SIZE = readSpecDisplayPageSize(); +/** + * The ownership type a `sharing` block means when it declares none. READ from + * `@objectstack/spec`, not restated, the same read as the page size above: + * `ViewSharingSchema` declares `type` with a default, nothing on the render + * path parses the served view, so an authored `{ lockedBy }` arrives with + * `type` absent, and parsing an empty block is the spec's own answer. + */ +const SPEC_DEFAULT_SHARING_TYPE = ViewSharingSchema.parse({}).type; + +/** + * Whether the toolbar draws the sharing indicator: a declared `type` or a + * `lockedBy`. The lock keys on `lockedBy` alone and never waits on `type`. + */ +function hasSharingIndicator(sharing: { type?: string; lockedBy?: string } | undefined): boolean { + return !!(sharing?.type || sharing?.lockedBy); +} + /** * The `$top` of the ONE unpaged fetch a list view makes when no usable page * size is declared: every view this component does not page on the server @@ -6454,12 +6471,15 @@ export const ListView = React.forwardRef(({ (`type` + optional `lockedBy`), never a button. No view-sharing control or endpoint exists to open, so a clickable "Share" here was a dead button. `lockedBy` is the spec's free string ("User - who locked the view configuration"), rendered as written. */} - {schema.sharing?.type && (() => { - const sharingLabel = schema.sharing.type === 'personal' + who locked the view configuration"), rendered as written. A + block with `lockedBy` and no `type` shows the lock beside the + spec's default type. */} + {hasSharingIndicator(schema.sharing) && (() => { + const sharingType = schema.sharing?.type ?? SPEC_DEFAULT_SHARING_TYPE; + const sharingLabel = sharingType === 'personal' ? t('list.sharingPersonal') : t('list.sharingCollaborative'); - const lockedBy = schema.sharing.lockedBy; + const lockedBy = schema.sharing?.lockedBy; const lockedLabel = lockedBy ? t('list.sharingLockedBy', { user: lockedBy }) : undefined; const hint = lockedLabel ? `${sharingLabel} · ${lockedLabel}` : sharingLabel; return ( @@ -6508,7 +6528,7 @@ export const ListView = React.forwardRef(({ {/* --- Separator: Print/Share/Export | Search --- */} {(() => { - const hasLeftSideItems = schema.allowPrinting || !!schema.sharing?.type + const hasLeftSideItems = schema.allowPrinting || hasSharingIndicator(schema.sharing) || (!toolbarFlags.compactToolbar && resolvedExportOptions && exportPermitted && exportableFormats.length > 0); return toolbarFlags.showSearch && hasLeftSideItems ? (
diff --git a/ui/packages/plugin-list/src/__tests__/ListView.test.tsx b/ui/packages/plugin-list/src/__tests__/ListView.test.tsx index 1992351172e..517e5113354 100644 --- a/ui/packages/plugin-list/src/__tests__/ListView.test.tsx +++ b/ui/packages/plugin-list/src/__tests__/ListView.test.tsx @@ -2644,6 +2644,41 @@ describe('ListView', () => { expect(indicator).not.toHaveAttribute('role'); expect(indicator).not.toHaveAttribute('tabindex'); }); + + // The lock keys on `lockedBy`, never on `type`: the spec defaults `type` + // to `collaborative` and the served view carries the authored body, so a + // view that writes only `lockedBy` arrives with `type` absent. + it.each([ + ['no `type` (the spec default)', { lockedBy: 'admin@objectos.ai' }, 'Shared view'], + ['`type: collaborative`', { type: 'collaborative' as const, lockedBy: 'admin@objectos.ai' }, 'Shared view'], + ['`type: personal`', { type: 'personal' as const, lockedBy: 'admin@objectos.ai' }, 'Personal view'], + ])('shows the lock from `sharing.lockedBy` with %s', (_case, sharing, typeLabel) => { + const schema: ListViewSchema = { + type: 'list-view', + objectName: 'contacts', + viewType: 'grid', + fields: ['name', 'email'], + sharing, + }; + + renderWithProvider(); + expect(screen.getByTestId('sharing-locked-by')).toHaveTextContent('Locked by admin@objectos.ai'); + expect(screen.getByTestId('share-button')).toHaveAttribute('title', `${typeLabel} · Locked by admin@objectos.ai`); + }); + + it('shows no sharing indicator for a `sharing` block with neither `type` nor `lockedBy`', () => { + const schema: ListViewSchema = { + type: 'list-view', + objectName: 'contacts', + viewType: 'grid', + fields: ['name', 'email'], + sharing: {}, + }; + + renderWithProvider(); + expect(screen.queryByTestId('share-button')).not.toBeInTheDocument(); + expect(screen.queryByTestId('sharing-locked-by')).not.toBeInTheDocument(); + }); }); // ============================ From b6269b70a7c8f2703cf101926c113cad54a493d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 10:55:22 +0000 Subject: [PATCH 2/2] chore(changeset): plugin-list lock indicator without a sharing type Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01QsR2jSSoRjvEqsGsZZLPC9 --- .changeset/29009-list-lock-without-type.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/29009-list-lock-without-type.md diff --git a/.changeset/29009-list-lock-without-type.md b/.changeset/29009-list-lock-without-type.md new file mode 100644 index 00000000000..5f51c55c523 --- /dev/null +++ b/.changeset/29009-list-lock-without-type.md @@ -0,0 +1,13 @@ +--- +'@object-ui/plugin-list': patch +--- + +fix(plugin-list): a list view whose `sharing` sets `lockedBy` and no `type` shows its lock indicator + +Clause-②: no +Metadata-protocol: none + +The list toolbar drew its read-only sharing indicator only when the view declared `sharing.type`. `ViewSharingSchema` defaults `type` to `collaborative`, and the served view carries the authored body without that default, so a view authored as `sharing: { lockedBy: 'admin@objectos.ai' }` showed no lock: other readers did not learn the view was locked. + +- **Now.** The indicator draws when `sharing` declares a `type` or a `lockedBy`; the lock keys on `lockedBy` alone. An absent `type` shows the spec's default type label ("Shared view"), read from `ViewSharingSchema` rather than restated in the renderer. +- **Unchanged.** A view with `type` set renders as before, a `sharing` block with neither key shows nothing, and no key, value or copy changes.