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
13 changes: 13 additions & 0 deletions .changeset/29009-list-lock-without-type.md
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 26 additions & 6 deletions ui/packages/plugin-list/src/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -6454,12 +6471,15 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
(`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 (
Expand Down Expand Up @@ -6508,7 +6528,7 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({

{/* --- 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 ? (
<div className="h-5 w-px bg-border/50 mx-1 shrink-0" />
Expand Down
35 changes: 35 additions & 0 deletions ui/packages/plugin-list/src/__tests__/ListView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<ListView schema={schema} />);
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(<ListView schema={schema} />);
expect(screen.queryByTestId('share-button')).not.toBeInTheDocument();
expect(screen.queryByTestId('sharing-locked-by')).not.toBeInTheDocument();
});
});

// ============================
Expand Down
Loading