From ea240b6082d52163ec7c057b1a719ebc8748a377 Mon Sep 17 00:00:00 2001
From: Claude
Date: Mon, 26 Oct 2026 06:42:07 +0000
Subject: [PATCH 1/5] fix(app-shell): pending changes folds platform-written
system fields and derived keys into one collapsed group
A new object's drill-in listed the injected audit columns and owner_id as
fields the author added, and its own name / label / derived nameField under
"Also changed". System fields (system: true) the publish adds or removes and
a nameField equal to the registry's provisionPrimary derivation now fold into
a collapsed "Generated by the platform (N)" line; a new item's name and
label (shown on its row) are no longer listed as keys.
Claude-Session: https://claude.ai/code/session_01P4aqt32BH9VTnLdDgkBUMx
Co-authored-by: Claude
---
.../src/preview/DraftChangesPanel.tsx | 183 ++++++++++++++++--
ui/packages/i18n/src/locales/ar.ts | 3 +
ui/packages/i18n/src/locales/de.ts | 3 +
ui/packages/i18n/src/locales/en.ts | 3 +
ui/packages/i18n/src/locales/es.ts | 3 +
ui/packages/i18n/src/locales/fr.ts | 3 +
ui/packages/i18n/src/locales/ja.ts | 3 +
ui/packages/i18n/src/locales/ko.ts | 3 +
ui/packages/i18n/src/locales/pt.ts | 3 +
ui/packages/i18n/src/locales/ru.ts | 3 +
ui/packages/i18n/src/locales/zh.ts | 3 +
11 files changed, 193 insertions(+), 20 deletions(-)
diff --git a/ui/packages/app-shell/src/preview/DraftChangesPanel.tsx b/ui/packages/app-shell/src/preview/DraftChangesPanel.tsx
index c8a0f12ced7..868940a6993 100644
--- a/ui/packages/app-shell/src/preview/DraftChangesPanel.tsx
+++ b/ui/packages/app-shell/src/preview/DraftChangesPanel.tsx
@@ -14,7 +14,9 @@
* Each entry expands into the fields (objects) and top-level keys it adds,
* changes and removes, lazily fetched on first expand. An entry whose drill-in
* finds the draft equal to its published version drops the UPDATE mark
- * (objectui#11807): publishing still promotes it, but it changes nothing. This
+ * (objectui#11807): publishing still promotes it, but it changes nothing. What
+ * the platform wrote rather than the author — the system columns it provisions,
+ * the keys it derives — folds into one collapsed line (objectstack#25890). This
* is the review surface that turns Publish from a leap of faith into an
* informed click.
*
@@ -83,7 +85,7 @@ import { MetadataProtectionFields, stripReadDecorations } from '@objectstack/spe
// objectui#12929 — the ONE discriminator of an authored default's shape, the
// same function the batch door's backfill judgement calls. Moves nothing onto
// the eager graph for the reason recorded above (`vendor-objectstack`).
-import { discriminateDefaultValueShape } from '@objectstack/spec/data';
+import { discriminateDefaultValueShape, provisionPrimary } from '@objectstack/spec/data';
import { diffFields, readFields } from '../views/metadata-admin/previews/object-fields-io.js';
// objectui#13125 — the Data pillar's rule for a column the platform injected
// and hides (objectui#11780), reused rather than restated. A leaf module with
@@ -285,6 +287,22 @@ export interface EntryChangeDetail {
changed: string[];
removed: string[];
};
+ /**
+ * What publishing installs that the PLATFORM wrote, not the author: the
+ * system columns it provisions for the object and the keys it derives
+ * (objectstack#25890). Listed once, in a collapsed group of its own, never in
+ * `fields` or `keys` above. Field rows sort before key rows; each sorts by
+ * name. A generated field's label, when it declares one, is in
+ * `fields.labels`.
+ */
+ generated: Array<{ name: string; kind: 'added' | 'changed' | 'removed'; scope: 'field' | 'key' }>;
+ /**
+ * True when there is no published body, so publishing ADDS the item
+ * (objectstack#25890). Its identity keys — `name`, and the `label` the row
+ * already leads with — are then not listed as keys at all: they are the
+ * row, not something the item changes.
+ */
+ isNew: boolean;
}
/** Stable equality for metadata values (small JSON — order-sensitive is fine). */
@@ -292,13 +310,53 @@ function valueEqual(a: unknown, b: unknown): boolean {
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
}
-/** True when the detail lists nothing: no field row and no key row. */
+/** True when the detail lists nothing: no field row, no key row and no generated row. */
function isEmptyChangeDetail(detail: EntryChangeDetail): boolean {
- const { fields, keys } = detail;
+ const { fields, keys, generated } = detail;
const fieldRows = fields ? fields.added.length + fields.changed.length + fields.removed.length : 0;
- return fieldRows + keys.added.length + keys.changed.length + keys.removed.length === 0;
+ return fieldRows + keys.added.length + keys.changed.length + keys.removed.length + generated.length === 0;
}
+/**
+ * The keys a NEW item's row already shows (objectstack#25890): its machine name,
+ * and the label the row leads with (both read off the same draft body). Listed
+ * again under the drill-in they read as two more things the author changed.
+ */
+const NEW_ITEM_ROW_KEYS: ReadonlySet = new Set(['name', 'label']);
+
+/**
+ * Did the platform write this field definition (objectstack#25890)? The spec's own
+ * mark: `system: true`, which `FieldSchema` declares as the auto-injected
+ * system/audit field and tells tools that surface system fields apart from the
+ * author's to branch on. Read as a boolean and nothing else, as the spec
+ * declares it.
+ */
+function isPlatformSystemField(def: Record | undefined): boolean {
+ return def?.system === true;
+}
+
+/**
+ * Is `body.nameField` the pointer the registry DERIVES, rather than one the
+ * author chose (objectstack#25890)? The `/meta` reads replay the registry's
+ * materialization seam, which stamps `nameField` through `provisionPrimary` in
+ * designate-only mode. The answer is that same function over the body without
+ * the pointer — the registry's own write-side test for the same question — so
+ * no derivation rule is restated here. A pointer naming any other field is the
+ * author's choice and stays an author row; an absent pointer is no stamp.
+ */
+function isDerivedNameField(body: Record): boolean {
+ const present = body.nameField;
+ if (typeof present !== 'string' || present === '') return false;
+ const withoutPointer = { ...body };
+ delete withoutPointer.nameField;
+ return provisionPrimary(withoutPointer, { synthesize: false }).nameField === present;
+}
+
+/** Top-level keys whose every present value is platform-derived (objectstack#25890). */
+const DERIVED_KEY_TESTS: Readonly) => boolean>> = {
+ nameField: isDerivedNameField,
+};
+
/**
* The ADR-0010 protection envelope (`_lock`, `_packageId`, `_provenance`, …),
* read off the spec's own declaration so no key list lives here. The review
@@ -351,9 +409,18 @@ function fieldLabelOf(def: Record | undefined): string | undefi
* the Data pillar's own test (`isStudioHiddenSystemField`, objectui#11780).
* The `/meta` reads serve an object with the platform's columns already
* stamped on, so a NEW object's drill-in listed the search companion
- * `__search` as a field the author added. Only that rule decides: a column
- * the platform injects but does not hide (the audit columns, `owner_id`) is
- * still listed.
+ * `__search` as a field the author added. Only that rule drops a row
+ * outright.
+ *
+ * A column the platform injects but does NOT hide — the audit columns,
+ * `owner_id` — is a row, but not the author's (objectstack#25890): a field this
+ * publish adds or removes whose every definition carries `system: true` moves
+ * to `generated`, the collapsed group of what the platform wrote. So does a
+ * top-level key whose value is the platform's derivation (`nameField`, see
+ * {@link isDerivedNameField}). A system field CHANGED between two bodies stays
+ * an author row: the platform injects the same definition on both reads, so a
+ * difference there is an edit someone made. And a NEW item lists neither its
+ * `name` nor its `label` as a key: its row already shows both.
*
* Why not `computeDiffRows` from `LayeredDiff.tsx`, which splits top-level keys
* the same way: that module imports the metadata-admin string table
@@ -368,6 +435,7 @@ export function computeChangeDetail(
const pub = withoutProtectionEnvelope(published ?? {});
const cur = withoutProtectionEnvelope(draft ?? {});
let fields: EntryChangeDetail['fields'] = null;
+ const generatedFields: EntryChangeDetail['generated'] = [];
if (pub.fields != null || cur.fields != null) {
const d = diffFields(pub.fields, cur.fields);
const pubDefs = fieldDefsByName(pub.fields);
@@ -376,7 +444,10 @@ export function computeChangeDetail(
const defs = [pubDefs.get(name), curDefs.get(name)].filter((def) => def !== undefined);
return !(defs.length > 0 && defs.every((def) => isStudioHiddenSystemField(def)));
};
- const added = Object.values(d.byName)
+ // objectstack#25890 — added or removed, and the platform's on every side.
+ const platformWrote = (name: string): boolean =>
+ [pubDefs.get(name), curDefs.get(name)].every((def) => def === undefined || isPlatformSystemField(def));
+ const addedAll = Object.values(d.byName)
.filter((e) => e.status === 'added' && listed(e.name))
.map((e) => e.name)
.sort();
@@ -384,30 +455,45 @@ export function computeChangeDetail(
.filter((e) => e.status === 'changed' && listed(e.name))
.map((e) => ({ name: e.name, keys: e.changedKeys }))
.sort((a, b) => a.name.localeCompare(b.name));
- const removed = d.removed
+ const removedAll = d.removed
.map((e) => e.name)
.filter(listed)
.sort();
+ const added = addedAll.filter((name) => !platformWrote(name));
+ const removed = removedAll.filter((name) => !platformWrote(name));
+ for (const name of addedAll) if (platformWrote(name)) generatedFields.push({ name, kind: 'added', scope: 'field' });
+ for (const name of removedAll) {
+ if (platformWrote(name)) generatedFields.push({ name, kind: 'removed', scope: 'field' });
+ }
+ generatedFields.sort((a, b) => a.name.localeCompare(b.name));
const labels = new Map();
- for (const name of [...added, ...changed.map((f) => f.name), ...removed]) {
+ for (const name of [...addedAll, ...changed.map((f) => f.name), ...removedAll]) {
// The draft's definition where there is one: it is what publishing installs.
const label = fieldLabelOf(curDefs.get(name) ?? pubDefs.get(name));
if (label !== undefined) labels.set(name, label);
}
fields = { added, changed, removed, labels };
}
+ const isNew = published === null;
const names = new Set([...Object.keys(pub), ...Object.keys(cur)]);
names.delete('fields');
+ if (isNew) for (const k of NEW_ITEM_ROW_KEYS) names.delete(k);
const keys: EntryChangeDetail['keys'] = { added: [], changed: [], removed: [] };
+ const generatedKeys: EntryChangeDetail['generated'] = [];
for (const k of [...names].sort()) {
const before = (pub as Record)[k];
const after = (cur as Record)[k];
if (valueEqual(before, after)) continue;
- if (before == null) keys.added.push(k);
- else if (after == null) keys.removed.push(k);
- else keys.changed.push(k);
+ const kind = before == null ? 'added' : after == null ? 'removed' : 'changed';
+ const derived = Object.prototype.hasOwnProperty.call(DERIVED_KEY_TESTS, k) ? DERIVED_KEY_TESTS[k] : undefined;
+ // Derived only when every body that carries the key carries the derivation.
+ if (derived && [before == null ? null : pub, after == null ? null : cur].every((b) => b === null || derived(b))) {
+ generatedKeys.push({ name: k, kind, scope: 'key' });
+ continue;
+ }
+ keys[kind].push(k);
}
- return { fields, keys };
+ return { fields, keys, generated: [...generatedFields, ...generatedKeys], isNew };
}
/**
@@ -678,14 +764,19 @@ function EntryDetail({
);
}
- const { fields, keys } = detail;
+ const { fields, keys, generated, isNew } = detail;
const hasKeyRows = keys.added.length > 0 || keys.changed.length > 0 || keys.removed.length > 0;
if (isEmptyChangeDetail(detail)) {
return (
- {t('preview.changes.detailNone', {
- defaultValue: 'No differences detected — the draft matches the published version.',
- })}
+ {isNew
+ ? // objectstack#25890 — a NEW item with nothing beyond the name and label its row shows.
+ t('preview.changes.detailNewNone', {
+ defaultValue: 'Publishing adds it. It declares nothing beyond its name and label.',
+ })
+ : t('preview.changes.detailNone', {
+ defaultValue: 'No differences detected — the draft matches the published version.',
+ })}
- {t('preview.changes.detailChangedKeys', { defaultValue: 'Also changed:' })}
+ {isNew
+ ? // objectstack#25890 — every key of a NEW item is new: what it includes, not what else changed.
+ t('preview.changes.detailNewKeys', { defaultValue: 'Includes:' })
+ : t('preview.changes.detailChangedKeys', { defaultValue: 'Also changed:' })}
))}
+ {generated.length > 0 && }
+
+ );
+}
+
+/**
+ * objectstack#25890 — what the platform wrote into this publish, folded into
+ * one line: the system columns it provisions and the keys it derives. Collapsed
+ * on every open, the confirm posture's too (that posture expands the item rows,
+ * not this group); the toggle shows the rows, read-only, in the same shape as
+ * the author's.
+ */
+function GeneratedGroup({
+ rows,
+ labels,
+}: {
+ rows: EntryChangeDetail['generated'];
+ labels: ReadonlyMap | undefined;
+}) {
+ const { t } = useObjectTranslation();
+ const [open, setOpen] = useState(false);
+ return (
+
);
}
diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts
index ac057c1c960..3c5997150a6 100644
--- a/ui/packages/i18n/src/locales/ar.ts
+++ b/ui/packages/i18n/src/locales/ar.ts
@@ -4692,6 +4692,9 @@ const ar = {
detailLoadFailed: 'تعذر تحميل تفاصيل التغيير:',
detailNone: 'لم يتم اكتشاف اختلافات — المسودة مطابقة للنسخة المنشورة.',
detailChangedKeys: 'تغييرات أخرى:',
+ detailNewNone: 'سيضيفه النشر. لا يعلن شيئًا سوى اسمه وتسميته.',
+ detailNewKeys: 'يتضمن:',
+ generatedGroup: 'أنشأتها المنصة ({{n}})',
confirmNote: 'النشر يُصدر كل المسودات المعلقة ({{count}}) لهذه الحزمة دفعة واحدة.',
confirmNote_zero: 'النشر يُصدر كل المسودات المعلقة ({{count}}) لهذه الحزمة دفعة واحدة.',
confirmNote_one: 'النشر يُصدر كل المسودات المعلقة ({{count}}) لهذه الحزمة دفعة واحدة.',
diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts
index 2dca8512ae5..5ec2a6ef486 100644
--- a/ui/packages/i18n/src/locales/de.ts
+++ b/ui/packages/i18n/src/locales/de.ts
@@ -4335,6 +4335,9 @@ const de = {
detailLoadFailed: 'Änderungsdetails konnten nicht geladen werden:',
detailNone: 'Keine Unterschiede erkannt — der Entwurf entspricht der veröffentlichten Version.',
detailChangedKeys: 'Ebenfalls geändert:',
+ detailNewNone: 'Die Veröffentlichung fügt es hinzu. Es deklariert nichts außer Namen und Bezeichnung.',
+ detailNewKeys: 'Enthält:',
+ generatedGroup: 'Von der Plattform erzeugt ({{n}})',
confirmNote: 'Beim Veröffentlichen werden alle {{count}} ausstehenden Entwürfe dieses Pakets atomar freigegeben.',
confirmNote_one: 'Beim Veröffentlichen wird der {{count}} ausstehende Entwurf dieses Pakets atomar freigegeben.',
confirmNote_other: 'Beim Veröffentlichen werden alle {{count}} ausstehenden Entwürfe dieses Pakets atomar freigegeben.',
diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts
index ef2ecfa2f7c..948efa45747 100644
--- a/ui/packages/i18n/src/locales/en.ts
+++ b/ui/packages/i18n/src/locales/en.ts
@@ -3749,6 +3749,9 @@ const en = {
detailLoadFailed: 'Could not load change detail:',
detailNone: 'No differences detected — the draft matches the published version.',
detailChangedKeys: 'Also changed:',
+ detailNewNone: 'Publishing adds it. It declares nothing beyond its name and label.',
+ detailNewKeys: 'Includes:',
+ generatedGroup: 'Generated by the platform ({{n}})',
confirmNote: 'Publishing releases all {{count}} pending drafts of this package atomically.',
confirmNote_one: 'Publishing releases the {{count}} pending draft of this package atomically.',
confirmNote_other: 'Publishing releases all {{count}} pending drafts of this package atomically.',
diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts
index 464ece759f5..8098453cda6 100644
--- a/ui/packages/i18n/src/locales/es.ts
+++ b/ui/packages/i18n/src/locales/es.ts
@@ -4425,6 +4425,9 @@ const es = {
detailLoadFailed: 'No se pudieron cargar los detalles del cambio:',
detailNone: 'No se detectaron diferencias — el borrador coincide con la versión publicada.',
detailChangedKeys: 'También cambió:',
+ detailNewNone: 'Publicar lo añade. No declara nada más allá de su nombre y su etiqueta.',
+ detailNewKeys: 'Incluye:',
+ generatedGroup: 'Generado por la plataforma ({{n}})',
confirmNote: 'Publicar libera atómicamente los {{count}} borradores pendientes de este paquete.',
confirmNote_one: 'Publicar libera atómicamente {{count}} borrador pendiente de este paquete.',
confirmNote_many: 'Publicar libera atómicamente los {{count}} de borradores pendientes de este paquete.',
diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts
index 16c2d6bef70..e70a3f6d5cf 100644
--- a/ui/packages/i18n/src/locales/fr.ts
+++ b/ui/packages/i18n/src/locales/fr.ts
@@ -4421,6 +4421,9 @@ const fr = {
detailLoadFailed: 'Impossible de charger les détails de la modification :',
detailNone: 'Aucune différence détectée — le brouillon correspond à la version publiée.',
detailChangedKeys: 'Également modifié :',
+ detailNewNone: 'La publication l’ajoute. Il ne déclare rien d’autre que son nom et son libellé.',
+ detailNewKeys: 'Inclut :',
+ generatedGroup: 'Généré par la plateforme ({{n}})',
confirmNote: 'La publication libère atomiquement les {{count}} brouillons en attente de ce paquet.',
confirmNote_one: 'La publication libère atomiquement {{count}} brouillon en attente de ce paquet.',
confirmNote_many: 'La publication libère atomiquement les {{count}} de brouillons en attente de ce paquet.',
diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts
index ae922186d1a..6ebdb286704 100644
--- a/ui/packages/i18n/src/locales/ja.ts
+++ b/ui/packages/i18n/src/locales/ja.ts
@@ -4337,6 +4337,9 @@ const ja = {
detailLoadFailed: '変更の詳細を読み込めませんでした:',
detailNone: '差分は検出されませんでした — ドラフトは公開版と一致しています。',
detailChangedKeys: 'その他の変更:',
+ detailNewNone: '公開すると追加されます。名前とラベル以外は何も宣言していません。',
+ detailNewKeys: '含まれるもの:',
+ generatedGroup: 'プラットフォームが生成({{n}})',
confirmNote: '公開すると、このパッケージの保留中ドラフト {{count}} 件がまとめて(アトミックに)公開されます。',
confirmNote_one: '公開すると、このパッケージの保留中ドラフト {{count}} 件がまとめて(アトミックに)公開されます。',
confirmNote_other: '公開すると、このパッケージの保留中ドラフト {{count}} 件がまとめて(アトミックに)公開されます。',
diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts
index 4d2e691ec58..c129ca8ff7e 100644
--- a/ui/packages/i18n/src/locales/ko.ts
+++ b/ui/packages/i18n/src/locales/ko.ts
@@ -4334,6 +4334,9 @@ const ko = {
detailLoadFailed: '변경 상세 정보를 불러오지 못했습니다:',
detailNone: '차이가 감지되지 않았습니다 — 초안이 게시된 버전과 일치합니다.',
detailChangedKeys: '기타 변경:',
+ detailNewNone: '게시하면 추가됩니다. 이름과 레이블 외에는 아무것도 선언하지 않습니다.',
+ detailNewKeys: '포함:',
+ generatedGroup: '플랫폼에서 생성됨 ({{n}})',
confirmNote: '게시하면 이 패키지의 대기 중인 초안 {{count}}개가 한 번에(원자적으로) 게시됩니다.',
confirmNote_one: '게시하면 이 패키지의 대기 중인 초안 {{count}}개가 한 번에(원자적으로) 게시됩니다.',
confirmNote_other: '게시하면 이 패키지의 대기 중인 초안 {{count}}개가 한 번에(원자적으로) 게시됩니다.',
diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts
index 6947902e67b..8f1fce16d3c 100644
--- a/ui/packages/i18n/src/locales/pt.ts
+++ b/ui/packages/i18n/src/locales/pt.ts
@@ -4420,6 +4420,9 @@ const pt = {
detailLoadFailed: 'Não foi possível carregar os detalhes da alteração:',
detailNone: 'Nenhuma diferença detectada — o rascunho corresponde à versão publicada.',
detailChangedKeys: 'Também alterado:',
+ detailNewNone: 'Publicar adiciona-o. Não declara nada além do nome e do rótulo.',
+ detailNewKeys: 'Inclui:',
+ generatedGroup: 'Gerado pela plataforma ({{n}})',
confirmNote: 'Publicar libera atomicamente todos os {{count}} rascunhos pendentes deste pacote.',
confirmNote_one: 'Publicar libera atomicamente {{count}} rascunho pendente deste pacote.',
confirmNote_many: 'Publicar libera atomicamente todos os {{count}} de rascunhos pendentes deste pacote.',
diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts
index bd5316c6919..be3b9b73a1e 100644
--- a/ui/packages/i18n/src/locales/ru.ts
+++ b/ui/packages/i18n/src/locales/ru.ts
@@ -4524,6 +4524,9 @@ const ru = {
detailLoadFailed: 'Не удалось загрузить детали изменения:',
detailNone: 'Различий не обнаружено — черновик совпадает с опубликованной версией.',
detailChangedKeys: 'Также изменено:',
+ detailNewNone: 'Публикация добавит его. Кроме имени и метки, в нём ничего не объявлено.',
+ detailNewKeys: 'Включает:',
+ generatedGroup: 'Создано платформой ({{n}})',
confirmNote: 'Публикация атомарно выпускает все {{count}} ожидающих черновиков этого пакета.',
confirmNote_one: 'Публикация атомарно выпускает {{count}} ожидающий черновик этого пакета.',
confirmNote_few: 'Публикация атомарно выпускает все {{count}} ожидающих черновика этого пакета.',
diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts
index e7253201c8d..8ed896be0c3 100644
--- a/ui/packages/i18n/src/locales/zh.ts
+++ b/ui/packages/i18n/src/locales/zh.ts
@@ -3256,6 +3256,9 @@ const zh = {
detailLoadFailed: '无法加载变更详情:',
detailNone: '未检测到差异 —— 草稿与已发布版本一致。',
detailChangedKeys: '其他变更:',
+ detailNewNone: '发布将新增此项;除名称与标签外未声明其他内容。',
+ detailNewKeys: '包含:',
+ generatedGroup: '平台生成({{n}})',
confirmNote: '发布将一次性(原子地)发布此包全部 {{count}} 个待发布草稿。',
confirmNote_one: '发布将一次性(原子地)发布此包全部 {{count}} 个待发布草稿。',
confirmNote_other: '发布将一次性(原子地)发布此包全部 {{count}} 个待发布草稿。',
From 6577bce6370c2d97fa23e776b94bd94ca9cbe098 Mon Sep 17 00:00:00 2001
From: Claude
Date: Mon, 26 Oct 2026 06:54:43 +0000
Subject: [PATCH 2/5] test(app-shell): pin the pending-changes generated group
on the new-object repro; keep name/label rows
Claude-Session: https://claude.ai/code/session_01P4aqt32BH9VTnLdDgkBUMx
Co-authored-by: Claude
---
.../25890-pending-changes-generated-group.md | 17 ++
.../src/preview/DraftChangesPanel.tsx | 27 +--
...DraftChangesPanel.fieldRows-13125.test.tsx | 25 +-
...ChangesPanel.generatedGroup-25890.test.tsx | 221 ++++++++++++++++++
ui/packages/i18n/src/locales/ar.ts | 1 -
ui/packages/i18n/src/locales/de.ts | 1 -
ui/packages/i18n/src/locales/en.ts | 1 -
ui/packages/i18n/src/locales/es.ts | 1 -
ui/packages/i18n/src/locales/fr.ts | 1 -
ui/packages/i18n/src/locales/ja.ts | 1 -
ui/packages/i18n/src/locales/ko.ts | 1 -
ui/packages/i18n/src/locales/pt.ts | 1 -
ui/packages/i18n/src/locales/ru.ts | 1 -
ui/packages/i18n/src/locales/zh.ts | 1 -
14 files changed, 262 insertions(+), 38 deletions(-)
create mode 100644 .changeset/25890-pending-changes-generated-group.md
create mode 100644 ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.generatedGroup-25890.test.tsx
diff --git a/.changeset/25890-pending-changes-generated-group.md b/.changeset/25890-pending-changes-generated-group.md
new file mode 100644
index 00000000000..fd50b54b453
--- /dev/null
+++ b/.changeset/25890-pending-changes-generated-group.md
@@ -0,0 +1,17 @@
+---
+'@object-ui/app-shell': patch
+'@object-ui/i18n': patch
+---
+
+fix(app-shell): pending changes folds what the platform wrote into one collapsed line
+
+Clause-②: no
+Metadata-protocol: none
+
+Publishing a new object from Studio listed the platform's injected system columns (`created_at`, `created_by`, `owner_id`, `updated_at`, `updated_by`) as fields the author added, and the object's derived `nameField` under "Also changed", beside the one field the author typed.
+
+- A field the publish adds or removes whose every definition carries `system: true` (the spec's mark for an auto-injected system/audit column), and a `nameField` equal to the registry's own `provisionPrimary` derivation, now fold into one collapsed "Generated by the platform (N)" line; opening it shows those rows. A system field whose definition differs between the published and draft bodies is an edit and stays an author row, as does a `nameField` the derivation would not pick.
+- A new item's top-level keys are headed "Includes:" rather than "Also changed:": every key of an item that does not exist yet is part of it.
+- The columns the platform injects and hides (`__search`, `organization_id`, `owning_business_unit_id`) are still not listed at all.
+
+New strings `preview.changes.detailNewKeys` and `preview.changes.generatedGroup` ship in all ten locale packs.
diff --git a/ui/packages/app-shell/src/preview/DraftChangesPanel.tsx b/ui/packages/app-shell/src/preview/DraftChangesPanel.tsx
index 868940a6993..6c92a1c507d 100644
--- a/ui/packages/app-shell/src/preview/DraftChangesPanel.tsx
+++ b/ui/packages/app-shell/src/preview/DraftChangesPanel.tsx
@@ -298,9 +298,8 @@ export interface EntryChangeDetail {
generated: Array<{ name: string; kind: 'added' | 'changed' | 'removed'; scope: 'field' | 'key' }>;
/**
* True when there is no published body, so publishing ADDS the item
- * (objectstack#25890). Its identity keys — `name`, and the `label` the row
- * already leads with — are then not listed as keys at all: they are the
- * row, not something the item changes.
+ * (objectstack#25890). Every key it lists is then something the new item
+ * includes, not something else that changed, and the drill-in heads them so.
*/
isNew: boolean;
}
@@ -317,13 +316,6 @@ function isEmptyChangeDetail(detail: EntryChangeDetail): boolean {
return fieldRows + keys.added.length + keys.changed.length + keys.removed.length + generated.length === 0;
}
-/**
- * The keys a NEW item's row already shows (objectstack#25890): its machine name,
- * and the label the row leads with (both read off the same draft body). Listed
- * again under the drill-in they read as two more things the author changed.
- */
-const NEW_ITEM_ROW_KEYS: ReadonlySet = new Set(['name', 'label']);
-
/**
* Did the platform write this field definition (objectstack#25890)? The spec's own
* mark: `system: true`, which `FieldSchema` declares as the auto-injected
@@ -419,8 +411,7 @@ function fieldLabelOf(def: Record | undefined): string | undefi
* top-level key whose value is the platform's derivation (`nameField`, see
* {@link isDerivedNameField}). A system field CHANGED between two bodies stays
* an author row: the platform injects the same definition on both reads, so a
- * difference there is an edit someone made. And a NEW item lists neither its
- * `name` nor its `label` as a key: its row already shows both.
+ * difference there is an edit someone made.
*
* Why not `computeDiffRows` from `LayeredDiff.tsx`, which splits top-level keys
* the same way: that module imports the metadata-admin string table
@@ -477,7 +468,6 @@ export function computeChangeDetail(
const isNew = published === null;
const names = new Set([...Object.keys(pub), ...Object.keys(cur)]);
names.delete('fields');
- if (isNew) for (const k of NEW_ITEM_ROW_KEYS) names.delete(k);
const keys: EntryChangeDetail['keys'] = { added: [], changed: [], removed: [] };
const generatedKeys: EntryChangeDetail['generated'] = [];
for (const k of [...names].sort()) {
@@ -769,14 +759,9 @@ function EntryDetail({
if (isEmptyChangeDetail(detail)) {
return (
- {isNew
- ? // objectstack#25890 — a NEW item with nothing beyond the name and label its row shows.
- t('preview.changes.detailNewNone', {
- defaultValue: 'Publishing adds it. It declares nothing beyond its name and label.',
- })
- : t('preview.changes.detailNone', {
- defaultValue: 'No differences detected — the draft matches the published version.',
- })}
+ {t('preview.changes.detailNone', {
+ defaultValue: 'No differences detected — the draft matches the published version.',
+ })}
);
}
diff --git a/ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.fieldRows-13125.test.tsx b/ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.fieldRows-13125.test.tsx
index 504e106c587..611f4d43053 100644
--- a/ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.fieldRows-13125.test.tsx
+++ b/ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.fieldRows-13125.test.tsx
@@ -30,9 +30,13 @@
*
* - `internal_note` — an author field with `hidden: true` and no `system`:
* listed.
- * - `owner_id` — `system: true` without `hidden`: listed.
- * - `created_at` — an audit column, `system` without `hidden`: listed. The
- * rule does not hide it, and this card does not widen the rule.
+ * - `owner_id` — `system: true` without `hidden`: not dropped.
+ * - `created_at` — an audit column, `system` without `hidden`: not dropped.
+ * The rule does not hide it, and this card does not widen the rule. Both are
+ * rows of the collapsed group of what the platform wrote, not the author's
+ * field rows (objectstack#25890, pinned in the generatedGroup test beside
+ * this one); here they are the controls that the drop rule leaves them in
+ * the detail at all.
*/
import '@testing-library/jest-dom/vitest';
@@ -95,9 +99,9 @@ const VISIT_DRAFT = {
code: { type: 'text', label: 'code' },
// Control: author-hidden, not system — listed.
internal_note: { type: 'text', label: 'Internal Note', hidden: true },
- // Control: system, not hidden — listed.
+ // Control: system, not hidden — not dropped (a generated row, objectstack#25890).
owner_id: { type: 'lookup', reference: 'sys_user', label: 'Owner', required: false, readonly: false, system: true },
- // Control: audit column — listed; the rule does not hide it.
+ // Control: audit column — not dropped; the rule does not hide it (a generated row).
created_at: CREATED_AT_DEF,
owning_business_unit_id: OWNING_BUSINESS_UNIT_DEF,
__search: SEARCH_COMPANION_DEF,
@@ -105,7 +109,9 @@ const VISIT_DRAFT = {
};
const HIDDEN_SYSTEM = ['__search', 'owning_business_unit_id'] as const;
-const VISIT_LISTED = ['bare', 'code', 'created_at', 'internal_note', 'owner_id', 'visit_date'];
+const VISIT_LISTED = ['bare', 'code', 'internal_note', 'visit_date'];
+/** The system columns the drop rule keeps: rows of the generated group (objectstack#25890). */
+const VISIT_GENERATED = ['created_at', 'owner_id'];
afterEach(() => {
cleanup();
@@ -116,6 +122,7 @@ describe('computeChangeDetail — a column the platform injected and hides is no
it("a NEW object's added fields leave out the injected-and-hidden columns, and keep every control", () => {
const d = computeChangeDetail(null, VISIT_DRAFT);
expect(d.fields?.added).toEqual(VISIT_LISTED);
+ expect(d.generated.filter((g) => g.scope === 'field').map((g) => g.name)).toEqual(VISIT_GENERATED);
});
it('an UPDATE lists no changed or removed row for one either, and lists the author rows beside it', () => {
@@ -254,7 +261,11 @@ describe('the drill-in of a NEW object (objectui#13125)', () => {
for (const name of HIDDEN_SYSTEM) expect(text).not.toContain(name);
expect(text).not.toContain(SEARCH_COMPANION_DEF.label);
expect(text).not.toContain(OWNING_BUSINESS_UNIT_DEF.label);
- for (const label of ['Internal Note', 'Owner', 'Created At']) fieldRow(detail, `+ ${label}`);
+ fieldRow(detail, '+ Internal Note');
+ // The two system controls are in the detail, behind the generated group's toggle.
+ fireEvent.click(within(detail).getByTestId('draft-generated-toggle'));
+ for (const label of ['Owner', 'Created At']) fieldRow(detail, `+ ${label}`);
+ for (const name of HIDDEN_SYSTEM) expect(detail.textContent ?? '').not.toContain(name);
});
it('CONTROL: a field with no label reads as its name alone, once, in monospace', async () => {
diff --git a/ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.generatedGroup-25890.test.tsx b/ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.generatedGroup-25890.test.tsx
new file mode 100644
index 00000000000..f35500a0691
--- /dev/null
+++ b/ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.generatedGroup-25890.test.tsx
@@ -0,0 +1,221 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * objectstack#25890 — the pending-changes drill-in lists what the author
+ * changed; what the platform wrote folds into one collapsed line.
+ *
+ * Measured in a console walkthrough: Studio → new object 「报修工单」 (OWD
+ * private) → Publish expanded the one object to six field rows (the five
+ * injected system columns beside the author's `name`) and four "other
+ * changes" (`label`, `name`, `nameField`, `sharingModel`). The `/meta` reads
+ * serve an object with the platform's columns stamped on and its `nameField`
+ * derived, and a NEW object has no published body to cancel them against.
+ *
+ * The fixture is that object as the read serves it. Its system columns are the
+ * spec's own definitions (`injectedSystemColumnDefs`, the table the injection
+ * spreads), relabelled the way the read localises them, so the test proves the
+ * fold reads the `system` mark and not a label or a byte-identity.
+ *
+ * Controls, each a way to get the fold wrong while the headline stays green:
+ *
+ * - a system field CHANGED between two bodies is the author's edit: listed;
+ * - a `nameField` naming a field the derivation would not pick is the author's
+ * choice: listed;
+ * - a system field an UPDATE adds is the platform's: folded;
+ * - an author field that merely looks like an audit column (no `system`): listed.
+ */
+
+import '@testing-library/jest-dom/vitest';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
+import { injectedSystemColumnDefs } from '@objectstack/spec/data';
+
+vi.mock('@object-ui/i18n', async (importOriginal) => ({
+ ...(await importOriginal>()),
+ useObjectTranslation: () => ({
+ t: (_k: string, o?: Record & { defaultValue?: string }) =>
+ (o?.defaultValue ?? _k).replace(/\{\{(\w+)\}\}/g, (_m, key: string) => String(o?.[key] ?? '')),
+ language: 'en',
+ }),
+}));
+
+import { DraftChangesPanel, computeChangeDetail } from '../DraftChangesPanel';
+
+/** The read's zh-CN labels for the injected columns (`SYSTEM_FIELD_LABELS`). */
+const ZH_LABELS: Record = {
+ created_at: '创建时间',
+ created_by: '创建人',
+ owner_id: '所有者',
+ updated_at: '更新时间',
+ updated_by: '更新人',
+};
+
+const AUTHORED = { name: 'com_test_browser_repair_ticket', label: '报修工单', sharingModel: 'private' };
+
+/** The injected columns as the read serves them: the spec's definitions, labels localised. */
+function servedSystemColumns(): Record> {
+ const injected = injectedSystemColumnDefs({ ...AUTHORED, fields: { name: { type: 'text' } } });
+ const out: Record> = {};
+ for (const [name, def] of Object.entries(injected)) {
+ out[name] = { ...def, ...(ZH_LABELS[name] ? { label: ZH_LABELS[name] } : {}) };
+ }
+ return out;
+}
+
+/** The card's repro: one authored field, the platform's columns, the derived pointer. */
+const REPAIR_TICKET_DRAFT = {
+ ...AUTHORED,
+ nameField: 'name',
+ fields: {
+ name: { type: 'text', label: '名称' },
+ ...servedSystemColumns(),
+ // The search companion: injected AND hidden, dropped outright (objectui#13125).
+ __search: { type: 'text', label: 'Search Index', hidden: true, readonly: true, system: true },
+ },
+};
+
+const SYSTEM_COLUMNS = ['created_at', 'created_by', 'owner_id', 'updated_at', 'updated_by'];
+
+afterEach(() => {
+ cleanup();
+ vi.unstubAllGlobals();
+});
+
+describe('computeChangeDetail — what the platform wrote is not an author row (objectstack#25890)', () => {
+ it('the fixture carries the five visible system columns the card measured', () => {
+ // Guards the fixture: if the injection table moves, this test says so here.
+ const visible = Object.entries(REPAIR_TICKET_DRAFT.fields)
+ .filter(([, def]) => (def as { system?: unknown }).system === true && !(def as { hidden?: unknown }).hidden)
+ .map(([name]) => name)
+ .sort();
+ expect(visible).toEqual(SYSTEM_COLUMNS);
+ });
+
+ it("a NEW object's author rows are its authored field and keys; the system columns and nameField are generated", () => {
+ const d = computeChangeDetail(null, REPAIR_TICKET_DRAFT);
+ expect(d.isNew).toBe(true);
+ expect(d.fields?.added).toEqual(['name']);
+ expect(d.fields?.changed).toEqual([]);
+ expect(d.fields?.removed).toEqual([]);
+ expect(d.keys).toEqual({ added: ['label', 'name', 'sharingModel'], changed: [], removed: [] });
+ expect(d.generated).toEqual([
+ ...SYSTEM_COLUMNS.map((name) => ({ name, kind: 'added', scope: 'field' })),
+ { name: 'nameField', kind: 'added', scope: 'key' },
+ ]);
+ // A generated field keeps its label, for the row the group shows when opened.
+ expect(d.fields?.labels.get('owner_id')).toBe('所有者');
+ });
+
+ it('CONTROL: a field shaped like an audit column without the system mark is the author’s', () => {
+ const draft = { name: 'x', fields: { created_on: { type: 'datetime', label: 'Created On', readonly: true } } };
+ const d = computeChangeDetail(null, draft);
+ expect(d.fields?.added).toEqual(['created_on']);
+ expect(d.generated).toEqual([]);
+ });
+
+ it('an UPDATE folds a system column it adds, and lists a system column the author changed', () => {
+ const cols = servedSystemColumns();
+ const published = {
+ name: 'ticket',
+ fields: { subject: { type: 'text' }, created_at: cols.created_at },
+ };
+ const draft = {
+ name: 'ticket',
+ fields: {
+ subject: { type: 'text' },
+ // Relabelled by the author: the same column on both reads, so the difference is an edit.
+ created_at: { ...cols.created_at, label: 'Opened At' },
+ // Newly provisioned (e.g. the OWD now asks for an owner): the platform's.
+ owner_id: cols.owner_id,
+ },
+ };
+ const d = computeChangeDetail(published, draft);
+ expect(d.isNew).toBe(false);
+ expect(d.fields?.added).toEqual([]);
+ expect(d.fields?.changed).toEqual([{ name: 'created_at', keys: ['label'] }]);
+ expect(d.generated).toEqual([{ name: 'owner_id', kind: 'added', scope: 'field' }]);
+ });
+
+ it('CONTROL: a nameField the derivation would not pick is the author’s choice and is listed', () => {
+ const fields = { name: { type: 'text' }, subject: { type: 'text' } };
+ const published = { name: 'ticket', nameField: 'name', fields };
+ const draft = { name: 'ticket', nameField: 'subject', fields };
+ const d = computeChangeDetail(published, draft);
+ expect(d.keys.changed).toEqual(['nameField']);
+ expect(d.generated).toEqual([]);
+ });
+
+ it('a nameField moving between two derived answers is the platform’s', () => {
+ const published = { name: 'ticket', nameField: 'name', fields: { name: { type: 'text' } } };
+ const draft = { name: 'ticket', nameField: 'subject', fields: { subject: { type: 'text' } } };
+ const d = computeChangeDetail(published, draft);
+ expect(d.keys.changed).toEqual([]);
+ expect(d.generated).toEqual([{ name: 'nameField', kind: 'changed', scope: 'key' }]);
+ });
+});
+
+/* ─────────────── the panel ─────────────── */
+
+function stubServer() {
+ const ok = (body: unknown) => ({ ok: true, status: 200, json: async () => body });
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn(async (input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url.includes('/_drafts')) {
+ return ok({ drafts: [{ type: 'object', name: AUTHORED.name, packageId: 'com.x', label: AUTHORED.label }] });
+ }
+ if (/\/meta\/object(\?|$)/.test(url)) return ok([]);
+ if (url.includes(`/meta/object/${AUTHORED.name}`) && url.includes('state=draft')) {
+ return ok({ item: REPAIR_TICKET_DRAFT });
+ }
+ return { ok: false, status: 404, json: async () => ({}) };
+ }) as unknown as typeof fetch,
+ );
+}
+
+async function expandRepairTicket(): Promise {
+ stubServer();
+ render( {}} packageId="com.x" />);
+ const toggle = (await screen.findByText(AUTHORED.label)).closest('[data-testid="draft-entry-toggle"]') as HTMLElement;
+ fireEvent.click(toggle);
+ return screen.findByTestId('draft-entry-detail');
+}
+
+describe("the card's repro in the sheet (objectstack#25890)", () => {
+ it('shows the one authored field and one collapsed line for what the platform wrote', async () => {
+ const detail = await expandRepairTicket();
+ const rows = within(detail).getAllByTestId('draft-field-row');
+ expect(rows.map((r) => r.textContent)).toEqual(['+ 名称 name']);
+ const toggle = within(detail).getByTestId('draft-generated-toggle');
+ expect(toggle).toHaveTextContent('Generated by the platform (6)');
+ expect(toggle).toHaveAttribute('aria-expanded', 'false');
+ const text = detail.textContent ?? '';
+ for (const name of [...SYSTEM_COLUMNS, '__search', 'nameField']) expect(text).not.toContain(name);
+ });
+
+ it('heads a NEW item’s keys as what it includes, not as other changes', async () => {
+ const detail = await expandRepairTicket();
+ expect(within(detail).getByText('Includes:')).toBeInTheDocument();
+ expect(within(detail).queryByText('Also changed:')).toBeNull();
+ expect(within(detail).getByText('+ sharingModel')).toBeInTheDocument();
+ });
+
+ it('opens the group to the five system columns, label first, and the derived nameField', async () => {
+ const detail = await expandRepairTicket();
+ const toggle = within(detail).getByTestId('draft-generated-toggle');
+ fireEvent.click(toggle);
+ expect(toggle).toHaveAttribute('aria-expanded', 'true');
+ const group = within(detail).getByTestId('draft-generated-group');
+ const fieldRows = within(group).getAllByTestId('draft-field-row').map((r) => r.textContent);
+ expect(fieldRows).toEqual(SYSTEM_COLUMNS.map((name) => `+ ${ZH_LABELS[name]} ${name}`));
+ expect(within(group).getByText('+ nameField')).toBeInTheDocument();
+ expect(group.textContent ?? '').not.toContain('__search');
+ });
+});
diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts
index 3c5997150a6..6dfdfcad81b 100644
--- a/ui/packages/i18n/src/locales/ar.ts
+++ b/ui/packages/i18n/src/locales/ar.ts
@@ -4692,7 +4692,6 @@ const ar = {
detailLoadFailed: 'تعذر تحميل تفاصيل التغيير:',
detailNone: 'لم يتم اكتشاف اختلافات — المسودة مطابقة للنسخة المنشورة.',
detailChangedKeys: 'تغييرات أخرى:',
- detailNewNone: 'سيضيفه النشر. لا يعلن شيئًا سوى اسمه وتسميته.',
detailNewKeys: 'يتضمن:',
generatedGroup: 'أنشأتها المنصة ({{n}})',
confirmNote: 'النشر يُصدر كل المسودات المعلقة ({{count}}) لهذه الحزمة دفعة واحدة.',
diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts
index 5ec2a6ef486..e8d8be5cd23 100644
--- a/ui/packages/i18n/src/locales/de.ts
+++ b/ui/packages/i18n/src/locales/de.ts
@@ -4335,7 +4335,6 @@ const de = {
detailLoadFailed: 'Änderungsdetails konnten nicht geladen werden:',
detailNone: 'Keine Unterschiede erkannt — der Entwurf entspricht der veröffentlichten Version.',
detailChangedKeys: 'Ebenfalls geändert:',
- detailNewNone: 'Die Veröffentlichung fügt es hinzu. Es deklariert nichts außer Namen und Bezeichnung.',
detailNewKeys: 'Enthält:',
generatedGroup: 'Von der Plattform erzeugt ({{n}})',
confirmNote: 'Beim Veröffentlichen werden alle {{count}} ausstehenden Entwürfe dieses Pakets atomar freigegeben.',
diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts
index 948efa45747..0f70c5edb2c 100644
--- a/ui/packages/i18n/src/locales/en.ts
+++ b/ui/packages/i18n/src/locales/en.ts
@@ -3749,7 +3749,6 @@ const en = {
detailLoadFailed: 'Could not load change detail:',
detailNone: 'No differences detected — the draft matches the published version.',
detailChangedKeys: 'Also changed:',
- detailNewNone: 'Publishing adds it. It declares nothing beyond its name and label.',
detailNewKeys: 'Includes:',
generatedGroup: 'Generated by the platform ({{n}})',
confirmNote: 'Publishing releases all {{count}} pending drafts of this package atomically.',
diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts
index 8098453cda6..c1100df32f0 100644
--- a/ui/packages/i18n/src/locales/es.ts
+++ b/ui/packages/i18n/src/locales/es.ts
@@ -4425,7 +4425,6 @@ const es = {
detailLoadFailed: 'No se pudieron cargar los detalles del cambio:',
detailNone: 'No se detectaron diferencias — el borrador coincide con la versión publicada.',
detailChangedKeys: 'También cambió:',
- detailNewNone: 'Publicar lo añade. No declara nada más allá de su nombre y su etiqueta.',
detailNewKeys: 'Incluye:',
generatedGroup: 'Generado por la plataforma ({{n}})',
confirmNote: 'Publicar libera atómicamente los {{count}} borradores pendientes de este paquete.',
diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts
index e70a3f6d5cf..01b87c8eeca 100644
--- a/ui/packages/i18n/src/locales/fr.ts
+++ b/ui/packages/i18n/src/locales/fr.ts
@@ -4421,7 +4421,6 @@ const fr = {
detailLoadFailed: 'Impossible de charger les détails de la modification :',
detailNone: 'Aucune différence détectée — le brouillon correspond à la version publiée.',
detailChangedKeys: 'Également modifié :',
- detailNewNone: 'La publication l’ajoute. Il ne déclare rien d’autre que son nom et son libellé.',
detailNewKeys: 'Inclut :',
generatedGroup: 'Généré par la plateforme ({{n}})',
confirmNote: 'La publication libère atomiquement les {{count}} brouillons en attente de ce paquet.',
diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts
index 6ebdb286704..45589513c90 100644
--- a/ui/packages/i18n/src/locales/ja.ts
+++ b/ui/packages/i18n/src/locales/ja.ts
@@ -4337,7 +4337,6 @@ const ja = {
detailLoadFailed: '変更の詳細を読み込めませんでした:',
detailNone: '差分は検出されませんでした — ドラフトは公開版と一致しています。',
detailChangedKeys: 'その他の変更:',
- detailNewNone: '公開すると追加されます。名前とラベル以外は何も宣言していません。',
detailNewKeys: '含まれるもの:',
generatedGroup: 'プラットフォームが生成({{n}})',
confirmNote: '公開すると、このパッケージの保留中ドラフト {{count}} 件がまとめて(アトミックに)公開されます。',
diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts
index c129ca8ff7e..c20d57dbc97 100644
--- a/ui/packages/i18n/src/locales/ko.ts
+++ b/ui/packages/i18n/src/locales/ko.ts
@@ -4334,7 +4334,6 @@ const ko = {
detailLoadFailed: '변경 상세 정보를 불러오지 못했습니다:',
detailNone: '차이가 감지되지 않았습니다 — 초안이 게시된 버전과 일치합니다.',
detailChangedKeys: '기타 변경:',
- detailNewNone: '게시하면 추가됩니다. 이름과 레이블 외에는 아무것도 선언하지 않습니다.',
detailNewKeys: '포함:',
generatedGroup: '플랫폼에서 생성됨 ({{n}})',
confirmNote: '게시하면 이 패키지의 대기 중인 초안 {{count}}개가 한 번에(원자적으로) 게시됩니다.',
diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts
index 8f1fce16d3c..31935d16b2c 100644
--- a/ui/packages/i18n/src/locales/pt.ts
+++ b/ui/packages/i18n/src/locales/pt.ts
@@ -4420,7 +4420,6 @@ const pt = {
detailLoadFailed: 'Não foi possível carregar os detalhes da alteração:',
detailNone: 'Nenhuma diferença detectada — o rascunho corresponde à versão publicada.',
detailChangedKeys: 'Também alterado:',
- detailNewNone: 'Publicar adiciona-o. Não declara nada além do nome e do rótulo.',
detailNewKeys: 'Inclui:',
generatedGroup: 'Gerado pela plataforma ({{n}})',
confirmNote: 'Publicar libera atomicamente todos os {{count}} rascunhos pendentes deste pacote.',
diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts
index be3b9b73a1e..f9cbc3a4621 100644
--- a/ui/packages/i18n/src/locales/ru.ts
+++ b/ui/packages/i18n/src/locales/ru.ts
@@ -4524,7 +4524,6 @@ const ru = {
detailLoadFailed: 'Не удалось загрузить детали изменения:',
detailNone: 'Различий не обнаружено — черновик совпадает с опубликованной версией.',
detailChangedKeys: 'Также изменено:',
- detailNewNone: 'Публикация добавит его. Кроме имени и метки, в нём ничего не объявлено.',
detailNewKeys: 'Включает:',
generatedGroup: 'Создано платформой ({{n}})',
confirmNote: 'Публикация атомарно выпускает все {{count}} ожидающих черновиков этого пакета.',
diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts
index 8ed896be0c3..bfc6a84c42d 100644
--- a/ui/packages/i18n/src/locales/zh.ts
+++ b/ui/packages/i18n/src/locales/zh.ts
@@ -3256,7 +3256,6 @@ const zh = {
detailLoadFailed: '无法加载变更详情:',
detailNone: '未检测到差异 —— 草稿与已发布版本一致。',
detailChangedKeys: '其他变更:',
- detailNewNone: '发布将新增此项;除名称与标签外未声明其他内容。',
detailNewKeys: '包含:',
generatedGroup: '平台生成({{n}})',
confirmNote: '发布将一次性(原子地)发布此包全部 {{count}} 个待发布草稿。',
From f96a85198f53a1b58334d55caae67971a14162ce Mon Sep 17 00:00:00 2001
From: Claude
Date: Mon, 26 Oct 2026 07:25:59 +0000
Subject: [PATCH 3/5] chore(sdui): re-record the registry source hash after the
pending-changes edit (manifest bytes unchanged)
Claude-Session: https://claude.ai/code/session_01P4aqt32BH9VTnLdDgkBUMx
Co-authored-by: Claude
---
scripts/sdui-manifest.record.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json
index 1555759c7e3..af0590beb11 100644
--- a/scripts/sdui-manifest.record.json
+++ b/scripts/sdui-manifest.record.json
@@ -49,6 +49,6 @@
"ui/packages/types"
],
"registrySources": 922,
- "registrySourcesSha256": "ad9e0875b83f339e03c528fc3be774592615dfdc5ee5fe9becc4376ddb5f0d52"
+ "registrySourcesSha256": "11e008133f1746826558dc80f519f0316022884a40b4af7cc2d14450cff9461d"
}
}
From d670dcbbd760b85449aa864b4672f68a8dbc829d Mon Sep 17 00:00:00 2001
From: Claude
Date: Mon, 26 Oct 2026 09:01:40 +0000
Subject: [PATCH 4/5] chore(sdui): re-record the registry source hash on the
merged tree (manifest bytes unchanged)
Claude-Session: https://claude.ai/code/session_01P4aqt32BH9VTnLdDgkBUMx
Co-authored-by: Claude
---
scripts/sdui-manifest.record.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json
index 5d728022f56..7e2867ebeb2 100644
--- a/scripts/sdui-manifest.record.json
+++ b/scripts/sdui-manifest.record.json
@@ -49,6 +49,6 @@
"ui/packages/types"
],
"registrySources": 922,
- "registrySourcesSha256": "7734fe6bc3763fe8318db624b9e0dd4c48b2dc7b15bf2a3fb1f78e46bb8d959e"
+ "registrySourcesSha256": "83e17c720e8aabecb7e00c606bd9552d8133ff393e073e6b1bb50e1b7b8f5058"
}
}
From c206b7951b6e016f9f56d1010a4abcc64090090d Mon Sep 17 00:00:00 2001
From: Claude
Date: Mon, 26 Oct 2026 09:35:26 +0000
Subject: [PATCH 5/5] chore(sdui): re-record the registry source hash on the
merged tree (manifest bytes unchanged)
Claude-Session: https://claude.ai/code/session_01P4aqt32BH9VTnLdDgkBUMx
Co-authored-by: Claude
---
scripts/sdui-manifest.record.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json
index f65f470a705..684a23dffc1 100644
--- a/scripts/sdui-manifest.record.json
+++ b/scripts/sdui-manifest.record.json
@@ -49,6 +49,6 @@
"ui/packages/types"
],
"registrySources": 922,
- "registrySourcesSha256": "3fe916234f1b5346bff70f6f99322077630b851ebef81c9ae6ba8c4ac08e2063"
+ "registrySourcesSha256": "0e127be18a1104d857822dc5c71a6380e4f00b0db729cd8b8a230a11c120fe78"
}
}