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
36 changes: 36 additions & 0 deletions .changeset/11583-refused-writes-said.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
'@object-ui/app-shell': patch
'@object-ui/i18n': patch
---

A refused save, pin, reorder, view setting, report save, publish or discard in the console is now said to the user, and the view-config panel no longer reports a refused save as saved (objectui#11583).

objectui#11578 made the two Create View doors say a refused save. The console's other metadata
writes on the object page, the report page and the draft bar still caught a refusal with a
console line and nothing else, so a permission refusal or a spec refusal looked like a saved
change:

- the view-config panel's Save on an existing view;
- pinning or unpinning a view, and reordering views in "Manage views";
- a toolbar setting on a list view (density, sort, columns, hidden fields), when the server
refuses it (the console's own permission check already said its refusal, and still does);
- the report editor's Save;
- Publish and Discard draft on the draft bar of those two editors.

Each now raises the refusal through the console's error toast, with the save door's own
message: the field-anchored issues of a validation refusal, one per line, or the refusal's text.
The draft bar's toasts lead with "Publish failed" or "Discard failed", two new strings in all
ten language packs; the others lead with "Failed to save". Set as default, which already raised
an untranslated "Failed to set default view" with no reason, now does the same.

The report editor waits for its save. It closes once the report is saved; a refused save leaves
it open with the edit in place, so Save can be pressed again (it used to close at once, and
reopening it showed the stored report). Save is disabled, and the editor read-only, while the
save is in flight.

The view-config panel waits for the save before it reports the edit as saved. A refused save
leaves the panel dirty, so Save stays enabled for a retry, and the "unpublished changes"
indicator is not raised for a draft that was never written. Save is disabled while the save is
in flight. `ViewConfigPanel`'s `onSave` accepts any return, as it did when it was typed `void`:
the panel awaits it, and `false` (returned, or resolved by a promise) or a rejection means the
save was refused; anything else, nothing included, is read as saved.
59 changes: 49 additions & 10 deletions packages/app-shell/src/views/ObjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,14 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
return;
}
console.error('[ObjectView] Failed to persist view config:', err);
// objectui#11583: every other refusal is said too, with the
// door's message. The client gate above answers only for a
// session whose capabilities were reported; an unreported
// one passes it, and the server's 403 lands here.
toast.error(t('form.saveError'), {
description: formatMetadataError(err),
classNames: { description: 'whitespace-pre-line' },
});
});
}, 300);
},
Expand Down Expand Up @@ -1512,27 +1520,42 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
* folds through it. What is gone is the automatic write, not the fold.
*/

const handleViewConfigSave = useCallback((draft: Record<string, any>) => {
/**
* The view-config panel's edit Save. Resolves whether the draft was staged,
* so the panel stays dirty, and announces no draft, on anything but `true`
* (objectui#11583).
*/
const handleViewConfigSave = useCallback(async (draft: Record<string, any>): Promise<boolean> => {
setViewDraft(draft);
setRefreshKey(k => k + 1);

// ADR-0034: stage a per-item draft via the metadata seam; an explicit
// Publish (RuntimeDraftBar) promotes it + records a version.
const vid = draft.id;
if (metadataClient && vid) {
if (!metadataClient || !vid) {
console.warn('[ViewConfigPanel] Cannot persist view config: missing metadataClient or viewId.');
return false;
}
try {
// `dataSource` + `objectName` let the seam drop this object's view
// cache keys (#4373) — the adapter owns which keys those are.
persistRuntimeMetadata('view', vid, buildViewConfigSaveBody(objectName, draft), {
await persistRuntimeMetadata('view', vid, buildViewConfigSaveBody(objectName, draft), {
metadataClient,
dataSource,
objectName,
}).catch((err: any) => {
console.error('[ViewConfigPanel] Failed to persist view config:', err);
});
} else {
console.warn('[ViewConfigPanel] Cannot persist view config: missing metadataClient or viewId.');
return true;
} catch (err) {
console.error('[ViewConfigPanel] Failed to persist view config:', err);
// objectui#11583: a refused save is SAID, with the door's own
// message, as the Create View doors do (objectui#11578).
toast.error(t('form.saveError'), {
description: formatMetadataError(err),
classNames: { description: 'whitespace-pre-line' },
});
return false;
}
}, [metadataClient, dataSource, objectName]);
}, [metadataClient, dataSource, objectName, t]);

/**
* Create a new view: the Create View dialog's door and the view-config
Expand Down Expand Up @@ -2278,6 +2301,11 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
setRefreshKey(k => k + 1);
} catch (err) {
console.error('[ViewTabBar] Failed to pin view:', err);
// objectui#11583: a refused pin is said, with the door's message.
toast.error(t('form.saveError'), {
description: formatMetadataError(err),
classNames: { description: 'whitespace-pre-line' },
});
}
}, [dataSource, objectName, isSavedView, t]);

Expand All @@ -2304,7 +2332,11 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
setRefreshKey(k => k + 1);
} catch (err) {
console.error('[ViewTabBar] Failed to set default view:', err);
toast.error('Failed to set default view');
// objectui#11583: said like its siblings, with the door's message.
toast.error(t('form.saveError'), {
description: formatMetadataError(err),
classNames: { description: 'whitespace-pre-line' },
});
}
}, [dataSource, objectName, savedViews, isSavedView, t]);

Expand All @@ -2331,10 +2363,17 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
await Promise.all(updates);
} catch (err) {
console.error('[ViewTabBar] Failed to reorder views:', err);
// objectui#11583: the new order still shows from this
// browser's copy, but other sessions read the server's, so a
// refused write is said, with the door's message.
toast.error(t('form.saveError'), {
description: formatMetadataError(err),
classNames: { description: 'whitespace-pre-line' },
});
}
}
setRefreshKey(k => k + 1);
}, [dataSource, savedViews, objectName]);
}, [dataSource, savedViews, objectName, t]);

const handleConfigView = useCallback((vid: string) => {
// System (metadata-defined) views are read-only — opening the
Expand Down
Loading
Loading