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
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import { MemoryRouter } from 'react-router-dom';
interface Server {
set: Record<string, unknown>;
packageObjects: Array<{ name: string; label?: string }>;
// Keyed by object name → the merged object definition returned by
// get('object', name). `fields` may be an array or a `{ [name]: def }` map.
objectFields: Record<string, { fields?: unknown } | undefined>;
saved: Array<Record<string, unknown>>;
savedOpts: Array<Record<string, unknown> | undefined>;
}
Expand All @@ -54,8 +57,17 @@ function makeClient(server: Server) {
void opts;
return server.packageObjects.map((o) => ({ item: o }));
}
// The field-level editor no longer lists the `field` type; it reads the
// merged object definition via get('object', name). A bare list('field')
// returning [] would reproduce the "no fields" bug this suite guards.
return [];
},
get: async (type: string, name: string) => {
// Mirror `GET /api/v1/meta/object/<name>` — the merged definition whose
// `fields` reflect the published object (inline + standalone fields).
if (type === 'object') return server.objectFields[name] ?? null;
return null;
},
save: async (_type: string, _name: string, payload: Record<string, unknown>, opts?: Record<string, unknown>) => {
server.saved.push(payload);
server.savedOpts.push(opts);
Expand Down Expand Up @@ -86,6 +98,22 @@ afterEach(cleanup);
function freshServer(): Server {
return {
packageObjects: [{ name: 'a_account' }, { name: 'a_contact' }],
// a_account carries its fields inline on the published object definition
// (array form); a_contact uses the keyed-map form. Both must surface.
objectFields: {
a_account: {
fields: [
{ name: 'name', label: 'Name' },
{ name: 'industry', label: 'Industry' },
],
},
a_contact: {
fields: {
email: { label: 'Email' },
phone: { label: 'Phone' },
},
},
},
saved: [],
savedOpts: [],
set: {
Expand Down Expand Up @@ -162,4 +190,30 @@ describe('PermissionMatrixEditPage — package scope + slice merge (ADR-0086 P0)
viewAllRecords: true,
});
});

// Regression: expanding an object row for field-level read/write must list
// the object's published fields (read via get('object', name)) — not fall
// back to "no fields" because list('field') was empty.
it('lists an object\'s published fields when its row is expanded', async () => {
const server = freshServer();
clientImpl = makeClient(server);
renderMatrix();

// Expand a_account (fields provided as an array on the object def).
const accountToggle = await screen.findByRole('button', { name: /a_account/ });
fireEvent.click(accountToggle);

// Both inline fields surface with their labels — never "no fields".
await screen.findByLabelText('a_account.name readable');
expect(screen.getByLabelText('a_account.name editable')).toBeInTheDocument();
expect(screen.getByLabelText('a_account.industry readable')).toBeInTheDocument();
expect(
screen.queryByText('No fields registered for this object.'),
).not.toBeInTheDocument();

// Expand a_contact (fields provided as a keyed map) — same outcome.
fireEvent.click(screen.getByRole('button', { name: /a_contact/ }));
await screen.findByLabelText('a_contact.email readable');
expect(screen.getByLabelText('a_contact.phone editable')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@
* Wiring: registered from `builtinComponents.tsx` as
* registerMetadataResource({ type: 'permission', EditPage: PermissionMatrixEditPage })
*
* The component reads `/api/v1/meta/object` + `/api/v1/meta/field` to
* enumerate available objects and their fields. Saves through the
* The component reads `/api/v1/meta/object` to enumerate available
* objects, and reads each object's merged definition (`GET
* /api/v1/meta/object/<name>`, whose `fields` reflect the published
* object — inline + standalone) to enumerate that object's fields for
* field-level permission editing. Saves through the
* standard metadata save flow (overlay-aware, OCC, destructive-change
* dialog already provided by the generic engine — we go through
* client.save() directly).
Expand Down Expand Up @@ -219,14 +222,26 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved,
async function ensureFields(objectName: string) {
if (fieldsByObject[objectName]) return;
try {
// Fields are stored as `${object}__${field}` keys under the
// `field` metadata type. We resolve by listing then filtering
// on the `object` attribute of the item.
const items = await client.list<any>('field');
const list: FieldSummary[] = ((items as any[]) ?? [])
.map((row) => row?.item ?? row)
.filter((f) => String(f?.object ?? '') === objectName)
.map((f) => ({ name: String(f?.name ?? ''), label: f?.label }))
// Read the authoritative, merged object definition (same source the
// object settings tab uses for its nameField dropdown). The `field`
// LIST endpoint only surfaces standalone/code-package field metadata
// and misses fields carried inline on a published object, which left
// this editor showing "no fields" for objects that clearly have them.
// `fields` may come back as an array or as a `{ [name]: def }` map.
const obj = (await client.get<any>('object', objectName)) as
| { fields?: Record<string, any> | Array<any> }
| null;
const raw = obj?.fields;
const list: FieldSummary[] = (
Array.isArray(raw)
? raw.map((f: any) => ({ name: String(f?.name ?? ''), label: f?.label }))
: raw && typeof raw === 'object'
? Object.entries(raw).map(([name, f]: [string, any]) => ({
name: String(f?.name ?? name),
label: f?.label,
}))
: []
)
.filter((f) => !!f.name)
.sort((a, b) => a.name.localeCompare(b.name));
setFieldsByObject((prev) => ({ ...prev, [objectName]: list }));
Expand Down
Loading