From b0f4f82973e81d3c55bcf1672b0bd9df904654ed Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:54:41 +0000 Subject: [PATCH 1/6] feat(app-shell): record save toasts name the saved record The console's create and update success toasts (the main crud_success path and the child-form task path) now carry the saved record's ADR-0079 display name, e.g. `Task "Quarterly review" created`. A record with nothing nameable keeps the object-label toast. Two new form.* keys, createdNamed and updatedNamed, in all ten locale packs. Claude-Session: https://claude.ai/code/session_01DiQzqGc2iCV6ZSmHBw4mwe Co-authored-by: Claude --- .../app-shell/src/console/AppContent.tsx | 44 +++- ....recordSaveToastNamesRecord-26172.test.tsx | 238 ++++++++++++++++++ ui/packages/i18n/src/locales/ar.ts | 2 + ui/packages/i18n/src/locales/de.ts | 2 + ui/packages/i18n/src/locales/en.ts | 6 + ui/packages/i18n/src/locales/es.ts | 2 + ui/packages/i18n/src/locales/fr.ts | 2 + ui/packages/i18n/src/locales/ja.ts | 2 + ui/packages/i18n/src/locales/ko.ts | 2 + ui/packages/i18n/src/locales/pt.ts | 2 + ui/packages/i18n/src/locales/ru.ts | 2 + ui/packages/i18n/src/locales/zh.ts | 2 + 12 files changed, 293 insertions(+), 13 deletions(-) create mode 100644 ui/packages/app-shell/src/console/__tests__/AppContent.recordSaveToastNamesRecord-26172.test.tsx diff --git a/ui/packages/app-shell/src/console/AppContent.tsx b/ui/packages/app-shell/src/console/AppContent.tsx index f4e6da25105..66183e3229a 100644 --- a/ui/packages/app-shell/src/console/AppContent.tsx +++ b/ui/packages/app-shell/src/console/AppContent.tsx @@ -15,6 +15,7 @@ import { ModalForm } from '@object-ui/plugin-form'; import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components'; import { toast } from 'sonner'; import { useActionRunner, useGlobalUndo, useMutationInvalidationBridge, notifyDataChanged, FilterScopeProvider } from '@object-ui/react'; +import { getRecordDisplayName } from '@object-ui/core'; import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n'; import type { AppAccessVerdict, ConnectionState } from '@object-ui/data-objectstack'; import { useAuth, useWorkspaceAdminStatus } from '@object-ui/auth'; @@ -807,17 +808,37 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps = : currentObjectDef; const isChildFormTask = !!formObjectParam && formObjectParam !== currentObjectDef?.name; - const handleCrudSuccess = useCallback(() => { + // objectstack-ai/objectstack#26172 — a record save names the record it wrote + // (`ux-conventions.write-feedback`): `Task "Quarterly review" created`. The + // name is the saved record's display name from the ONE ADR-0079 resolver, + // read through the object being saved. The resolver's floor (`Record #`, + // or the empty fallback for an id-less row) is not a name, so a record with + // nothing nameable keeps the object-label toast it always had. + const recordSaveMessage = useCallback((saved: any): string => { const label = formObjectDef ? objectLabel(formObjectDef as any) : t('common.record', { defaultValue: 'Record' }); + const resolved = saved && typeof saved === 'object' + ? getRecordDisplayName(formObjectDef, saved, { fallback: '' }) + : ''; + const id = saved?.id ?? saved?._id; + const title = resolved && !(id !== null && id !== undefined && resolved === `Record #${id}`) + ? resolved + : undefined; + if (editingRecord) { + return title + ? t('form.updatedNamed', { object: label, title, defaultValue: `${label} "${title}" updated` }) + : t('form.updateSuccess', { object: label, defaultValue: `${label} updated successfully` }); + } + return title + ? t('form.createdNamed', { object: label, title, defaultValue: `${label} "${title}" created` }) + : t('form.createSuccess', { object: label, defaultValue: `${label} created successfully` }); + }, [editingRecord, formObjectDef, objectLabel, t]); + + const handleCrudSuccess = useCallback((saved?: any) => { executeAction({ type: 'crud_success', - params: { - message: editingRecord - ? t('form.updateSuccess', { object: label, defaultValue: `${label} updated successfully` }) - : t('form.createSuccess', { object: label, defaultValue: `${label} created successfully` }), - }, + params: { message: recordSaveMessage(saved) }, }); - }, [executeAction, editingRecord, formObjectDef, objectLabel, t]); + }, [executeAction, recordSaveMessage]); const handleDialogCancel = useCallback(() => { executeAction({ type: 'dialog_cancel' }); @@ -837,14 +858,11 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps = // invalidation bus (#2269): the dataSource write already emitted a // MutationEvent that the bridge fans out — no manual notify needed. if (isChildFormTask) { - const label = formObjectDef ? objectLabel(formObjectDef as any) : t('common.record', { defaultValue: 'Record' }); - toast.success(editingRecord - ? t('form.updateSuccess', { object: label, defaultValue: `${label} updated successfully` }) - : t('form.createSuccess', { object: label, defaultValue: `${label} created successfully` })); + toast.success(recordSaveMessage(saved)); closeRecordForm(); return; } - handleCrudSuccess(); + handleCrudSuccess(saved); if (editingRecord || !currentObjectDef) return; const target = resolvePostCreateTarget({ objectName: currentObjectDef.name, @@ -878,7 +896,7 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps = return; } navigate(target.url, { replace: true }); - }, [handleCrudSuccess, isChildFormTask, formObjectDef, editingRecord, currentObjectDef, appSegment, location.pathname, navigate, closeRecordForm, objectLabel, objectPluralLabel, t]); + }, [handleCrudSuccess, recordSaveMessage, isChildFormTask, editingRecord, currentObjectDef, appSegment, location.pathname, navigate, closeRecordForm, objectPluralLabel]); // Track recent items on route change. The hook matches `appName` against the // pathname's app segment, so it takes the ROUTE SEGMENT: handed diff --git a/ui/packages/app-shell/src/console/__tests__/AppContent.recordSaveToastNamesRecord-26172.test.tsx b/ui/packages/app-shell/src/console/__tests__/AppContent.recordSaveToastNamesRecord-26172.test.tsx new file mode 100644 index 00000000000..8a9b6fad44f --- /dev/null +++ b/ui/packages/app-shell/src/console/__tests__/AppContent.recordSaveToastNamesRecord-26172.test.tsx @@ -0,0 +1,238 @@ +/** + * 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-ai/objectstack#26172 — the record create and update success + * toasts name the saved record (`ux-conventions.write-feedback`), e.g. + * `Task "Quarterly review" created`, by its ADR-0079 display name. A record + * with nothing nameable keeps the object-label toast (`Task created + * successfully`). + * + * Measured from a real `AppContent` mount inside a real i18next instance (the + * `AppContent.createLandingOriginPlural-11733` harness): the global record-form + * modal is a stand-in whose `onSuccess` is called with the saved record. Two + * paths raise the toast. The main path hands it to the action runner as a + * `crud_success` message, read here off the runner's `execute`; the child-form + * task (a subtable child opened over its parent's record page) toasts it + * directly, read here off sonner's `toast.success`. + * + * Direction, written before the run: with the toast put back to the object + * label alone, every "names the record" cell RED (`Task created + * successfully`); every "no name" cell GREEN on both sides. + */ + +import '@testing-library/jest-dom/vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import React from 'react'; +import { render, cleanup, waitFor } from '@testing-library/react'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; +import { toast } from 'sonner'; +import { createI18n, I18nProvider } from '@object-ui/i18n'; + +/** The part of the modal's schema this file reads. */ +interface ModalSchemaProbe { + objectName?: string; + onSuccess: (saved: Record) => Promise | void; +} +const { modalSchemas, execute } = vi.hoisted(() => ({ + modalSchemas: [] as ModalSchemaProbe[], + execute: vi.fn(), +})); + +/** The global record-form modal: its schema carries the `onSuccess` under test. */ +vi.mock('@object-ui/plugin-form', async (importOriginal) => ({ + ...(await importOriginal>()), + ModalForm: ({ schema }: { schema: ModalSchemaProbe }) => { + modalSchemas.push(schema); + return
; + }, +})); + +vi.mock('@object-ui/plugin-designer', async (importOriginal) => ({ + ...(await importOriginal()), + CreateAppPage: () =>
, + EditAppPage: () =>
, + DashboardDesignPage: () =>
, +})); + +vi.mock('../../layout/ConsoleLayout', () => ({ + ConsoleLayout: ({ children }: { children?: React.ReactNode }) => ( +
{children}
+ ), +})); +vi.mock('../../chrome/CommandPalette', () => ({ CommandPalette: () => null })); +vi.mock('../../chrome/KeyboardShortcutsDialog', () => ({ KeyboardShortcutsDialog: () => null })); +vi.mock('../../chrome/OnboardingWalkthrough', () => ({ OnboardingWalkthrough: () => null })); +vi.mock('../../views/ObjectView', () => ({ ObjectView: () =>
})); +vi.mock('../../views/RecordDetailView', () => ({ RecordDetailView: () =>
})); + +vi.mock('@object-ui/auth', async (importOriginal) => ({ + ...(await importOriginal>()), + useAuth: () => ({ + user: { id: 'u1', name: 'Ada', email: 'ada@example.com', role: 'user' }, + getAuthConfig: async () => ({ features: {} }), + activeOrganization: null, + }), + useWorkspaceAdminStatus: () => ({ isAdmin: true, isResolved: true }), +})); + +const dataSourceStub = { + onConnectionStateChange: () => () => {}, + getConnectionState: () => 'connected', +}; +vi.mock('../../providers/AdapterProvider', async (importOriginal) => ({ + ...(await importOriginal>()), + useAdapter: () => dataSourceStub, +})); + +const OBJECTS = [ + // Declares its name field: a saved task is named by `subject`. + { + name: 'showcase_task', + label: 'Task', + nameField: 'subject', + fields: { subject: { type: 'text', label: 'Subject' }, done: { type: 'boolean', label: 'Done' } }, + }, + // The parent the child-form task opens over. + { name: 'showcase_project', label: 'Project', fields: { name: { type: 'text', label: 'Name' } } }, +]; + +const APPS = [{ name: 'crm', label: 'CRM', isDefault: true, navigation: [] }]; + +// One value for the whole run: the route's effects key on these members. +const METADATA = { + apps: APPS, + objects: OBJECTS, + loading: false, + ensureType: undefined, + error: null, + refresh: async () => {}, +}; +vi.mock('../../providers/MetadataProvider', async (importOriginal) => ({ + ...(await importOriginal>()), + useMetadata: () => METADATA, +})); + +const ACTION_RUNNER = { execute, runner: { registerHandler: () => {}, getContext: () => ({}) } }; +vi.mock('@object-ui/react', async (importOriginal) => ({ + ...(await importOriginal>()), + useActionRunner: () => ACTION_RUNNER, + useGlobalUndo: () => {}, + useMutationInvalidationBridge: () => {}, +})); + +import { AppContent } from '../AppContent'; + +type Lang = 'en' | 'zh-CN'; + +/** The catalog as the console loads it: `zh-CN` translates the task's label. */ +const BUNDLE: Record> = { + en: {}, + 'zh-CN': { showcase: { objects: { showcase_task: { label: '任务' } } } }, +}; + +const MAIN_CREATE = '/apps/crm/showcase_task?form=new'; +const MAIN_EDIT = '/apps/crm/showcase_task?form=t1'; +const CHILD_CREATE = '/apps/crm/showcase_project/record/p1?form=new&formObject=showcase_task'; +const CHILD_EDIT = '/apps/crm/showcase_project/record/p1?form=t1&formObject=showcase_task'; + +let toastSuccess: ReturnType; +beforeEach(() => { + modalSchemas.length = 0; + execute.mockClear(); + toastSuccess = vi.spyOn(toast, 'success'); +}); +afterEach(() => { + toastSuccess.mockRestore(); + cleanup(); +}); + +/** Mount at `url`, save `saved` through the record-form modal, and wait for its toast. */ +async function saveFrom(url: string, saved: Record, lang: Lang = 'en'): Promise { + const i18n = createI18n({ defaultLanguage: lang, detectBrowserLanguage: false }); + expect(i18n.language).toBe(lang); + i18n.addResourceBundle(lang, 'translation', BUNDLE[lang], true, true); + render( + + + + } /> + + + , + ); + await waitFor(() => expect(modalSchemas.length).toBeGreaterThan(0)); + const schema = modalSchemas[modalSchemas.length - 1]; + expect(schema.objectName).toBe('showcase_task'); + // Not inside `act`, as in the harness this file copies: the save settles at once. + await schema.onSuccess(saved); +} + +/** The message the main path handed the runner as its `crud_success`. */ +function crudSuccessMessage(): string | undefined { + const call = execute.mock.calls.find(([action]) => action?.type === 'crud_success'); + return call?.[0]?.params?.message; +} + +/** The message the child-form task toasted directly. */ +function childToastMessage(): string | undefined { + return toastSuccess.mock.calls[0]?.[0] as string | undefined; +} + +describe('AppContent — the record save toast names the saved record (objectstack-ai/objectstack#26172)', () => { + describe('main path (crud_success)', () => { + it('create: names the record by its declared name field', async () => { + await saveFrom(MAIN_CREATE, { id: 't1', subject: 'Quarterly review' }); + expect(crudSuccessMessage()).toBe('Task "Quarterly review" created'); + }); + + it('update: names the record by its declared name field', async () => { + await saveFrom(MAIN_EDIT, { id: 't1', subject: 'Quarterly review' }); + expect(crudSuccessMessage()).toBe('Task "Quarterly review" updated'); + }); + + it('create, no name: the object-label toast', async () => { + await saveFrom(MAIN_CREATE, { id: 't2', done: false }); + expect(crudSuccessMessage()).toBe('Task created successfully'); + }); + + it('update, no name: the object-label toast', async () => { + await saveFrom(MAIN_EDIT, { id: 't1', done: true }); + expect(crudSuccessMessage()).toBe('Task updated successfully'); + }); + + it('zh-CN: the pack string, with the translated object label', async () => { + await saveFrom(MAIN_CREATE, { id: 't1', subject: '季度复盘' }, 'zh-CN'); + expect(crudSuccessMessage()).toBe('任务“季度复盘”已创建'); + }); + }); + + describe('child-form task (direct toast)', () => { + it('create: names the record', async () => { + await saveFrom(CHILD_CREATE, { id: 't1', subject: 'Quarterly review' }); + expect(childToastMessage()).toBe('Task "Quarterly review" created'); + // The child task keeps the parent page as it is: no crud_success. + expect(crudSuccessMessage()).toBeUndefined(); + }); + + it('update: names the record', async () => { + await saveFrom(CHILD_EDIT, { id: 't1', subject: 'Quarterly review' }); + expect(childToastMessage()).toBe('Task "Quarterly review" updated'); + }); + + it('create, no name: the object-label toast', async () => { + await saveFrom(CHILD_CREATE, { id: 't2', done: false }); + expect(childToastMessage()).toBe('Task created successfully'); + }); + + it('update, no name: the object-label toast', async () => { + await saveFrom(CHILD_EDIT, { id: 't1', done: true }); + expect(childToastMessage()).toBe('Task updated successfully'); + }); + }); +}); diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts index eac8180d458..f1f5948459f 100644 --- a/ui/packages/i18n/src/locales/ar.ts +++ b/ui/packages/i18n/src/locales/ar.ts @@ -236,6 +236,8 @@ const ar = { update: "تحديث", createSuccess: "تم إنشاء {{object}}", updateSuccess: "تم تحديث {{object}}", + createdNamed: "تم إنشاء {{object}} \"{{title}}\"", + updatedNamed: "تم تحديث {{object}} \"{{title}}\"", deleteSuccess: "تم حذف {{object}}", created: "تم الإنشاء", saved: "تم الحفظ", diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts index a8695048e2f..6532b7ca10b 100644 --- a/ui/packages/i18n/src/locales/de.ts +++ b/ui/packages/i18n/src/locales/de.ts @@ -222,6 +222,8 @@ const de = { update: "Aktualisieren", createSuccess: "{{object}} erfolgreich erstellt", updateSuccess: "{{object}} erfolgreich aktualisiert", + createdNamed: "{{object}} „{{title}}“ erstellt", + updatedNamed: "{{object}} „{{title}}“ aktualisiert", deleteSuccess: "{{object}} erfolgreich gelöscht", created: "Erstellt", saved: "Gespeichert", diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts index a33346faa3b..61980bda316 100644 --- a/ui/packages/i18n/src/locales/en.ts +++ b/ui/packages/i18n/src/locales/en.ts @@ -324,6 +324,12 @@ const en = { update: 'Update', createSuccess: '{{object}} created successfully', updateSuccess: '{{object}} updated successfully', + // The console's record-save toasts when the saved record has a display + // name (ADR-0079): `{{object}}` is the object label, `{{title}}` the + // record's name. A record with no name keeps `createSuccess` / + // `updateSuccess` above. + createdNamed: '{{object}} "{{title}}" created', + updatedNamed: '{{object}} "{{title}}" updated', deleteSuccess: '{{object}} deleted successfully', // plugin-form's own feedback chrome (objectui#11039): the success toast // a form raises when the author declared no `successMessage` (`created` diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts index e4447cd7f52..26d7f625b3b 100644 --- a/ui/packages/i18n/src/locales/es.ts +++ b/ui/packages/i18n/src/locales/es.ts @@ -229,6 +229,8 @@ const es = { update: "Actualizar", createSuccess: "{{object}} creado", updateSuccess: "{{object}} actualizado", + createdNamed: "{{object}} \"{{title}}\" creado", + updatedNamed: "{{object}} \"{{title}}\" actualizado", deleteSuccess: "{{object}} eliminado", created: "Creado", saved: "Guardado", diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts index e676ea2c1b9..74cf7299951 100644 --- a/ui/packages/i18n/src/locales/fr.ts +++ b/ui/packages/i18n/src/locales/fr.ts @@ -224,6 +224,8 @@ const fr = { update: "Mettre à jour", createSuccess: "{{object}} créé", updateSuccess: "{{object}} mis à jour", + createdNamed: "{{object}} \"{{title}}\" créé", + updatedNamed: "{{object}} \"{{title}}\" mis à jour", deleteSuccess: "{{object}} supprimé", created: "Créé", saved: "Enregistré", diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts index fdfd3525642..e130bffe896 100644 --- a/ui/packages/i18n/src/locales/ja.ts +++ b/ui/packages/i18n/src/locales/ja.ts @@ -222,6 +222,8 @@ const ja = { update: "更新", createSuccess: "{{object}}が作成されました", updateSuccess: "{{object}}が更新されました", + createdNamed: "{{object}}「{{title}}」を作成しました", + updatedNamed: "{{object}}「{{title}}」を更新しました", deleteSuccess: "{{object}}が削除されました", created: "作成しました", saved: "保存しました", diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts index 0f23549e44e..b7ea86efd26 100644 --- a/ui/packages/i18n/src/locales/ko.ts +++ b/ui/packages/i18n/src/locales/ko.ts @@ -222,6 +222,8 @@ const ko = { update: "업데이트", createSuccess: "{{object}} 생성됨", updateSuccess: "{{object}} 업데이트됨", + createdNamed: "{{object}} \"{{title}}\" 생성됨", + updatedNamed: "{{object}} \"{{title}}\" 업데이트됨", deleteSuccess: "{{object}} 삭제됨", created: "생성됨", saved: "저장됨", diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts index da93c21309a..c9f3a779982 100644 --- a/ui/packages/i18n/src/locales/pt.ts +++ b/ui/packages/i18n/src/locales/pt.ts @@ -224,6 +224,8 @@ const pt = { update: "Atualizar", createSuccess: "{{object}} criado", updateSuccess: "{{object}} atualizado", + createdNamed: "{{object}} \"{{title}}\" criado", + updatedNamed: "{{object}} \"{{title}}\" atualizado", deleteSuccess: "{{object}} excluído", created: "Criado", saved: "Salvo", diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts index 3e26915e619..456830706b6 100644 --- a/ui/packages/i18n/src/locales/ru.ts +++ b/ui/packages/i18n/src/locales/ru.ts @@ -229,6 +229,8 @@ const ru = { update: "Обновить", createSuccess: "{{object}} создан", updateSuccess: "{{object}} обновлён", + createdNamed: "{{object}} \"{{title}}\" создан", + updatedNamed: "{{object}} \"{{title}}\" обновлён", deleteSuccess: "{{object}} удалён", created: "Создано", saved: "Сохранено", diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts index dd7b5235fbb..c7508a723eb 100644 --- a/ui/packages/i18n/src/locales/zh.ts +++ b/ui/packages/i18n/src/locales/zh.ts @@ -227,6 +227,8 @@ const zh = { update: '更新', createSuccess: '{{object}}创建成功', updateSuccess: '{{object}}更新成功', + createdNamed: '{{object}}“{{title}}”已创建', + updatedNamed: '{{object}}“{{title}}”已更新', deleteSuccess: '{{object}}删除成功', // objectui#11039 — plugin-form feedback chrome; see the `en` pack. created: '已创建', From 60850009fbf14508fe605788117ca8766b9a9496 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:57:06 +0000 Subject: [PATCH 2/6] chore(changeset): record save toasts name the saved record Claude-Session: https://claude.ai/code/session_01DiQzqGc2iCV6ZSmHBw4mwe Co-authored-by: Claude --- .../26172-record-save-toast-names-record.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .changeset/26172-record-save-toast-names-record.md diff --git a/.changeset/26172-record-save-toast-names-record.md b/.changeset/26172-record-save-toast-names-record.md new file mode 100644 index 00000000000..de0e1737ac7 --- /dev/null +++ b/.changeset/26172-record-save-toast-names-record.md @@ -0,0 +1,16 @@ +--- +'@object-ui/app-shell': patch +'@object-ui/i18n': patch +--- + +fix(app-shell): the record create and update toasts name the saved record + +Clause-②: no (adds or rewords message keys; no key is removed, so `TranslationKeys` only widens; no exported runtime contract changes) +Metadata-protocol: none + +A user entering several records in a row saw "Task created successfully" after each save and could not tell which record was just written. The Console's record-form save toasts now carry the saved record's display name (ADR-0079), for example `Task "Quarterly review" created` and `Task "Quarterly review" updated`. This covers the record-form overlay on a list and the child-record form opened over a parent record's page. + +- **`@object-ui/app-shell`:** the name comes from the same display-name resolver the record page title and lookups use, read through the saved object's declared name field. A record with nothing nameable (the resolver would only answer `Record #ID`) keeps the object-label toast it showed before. +- **`@object-ui/i18n`:** two new keys under `form.` in all ten packs, `createdNamed` and `updatedNamed`, each with `{{object}}` (the object label) and `{{title}}` (the record's name). The existing `form.createSuccess` / `form.updateSuccess` strings are unchanged and remain the toast for a record with no name. + +Settings saves and flow-completion toasts are unchanged. From 3c5713b5e85bbbc9b6475667e27e5dbc66d92ff6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 15:01:48 +0000 Subject: [PATCH 3/6] test(i18n): re-pin the de quote-pair and AppContent residue counts Two new de values add one matched pair each, and AppContent's two record-save toast sites now share one fallback label. Claude-Session: https://claude.ai/code/session_01DiQzqGc2iCV6ZSmHBw4mwe Co-authored-by: Claude --- .../i18n/src/__tests__/de-quote-pairing-3876.test.ts | 11 ++++++++--- .../src/__tests__/residue-namespaces-3546.test.tsx | 6 ++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/ui/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts b/ui/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts index de3091f7259..a7375a20bae 100644 --- a/ui/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts +++ b/ui/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts @@ -351,7 +351,10 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight // 94 once objectui#13355 added `connectAgent.apiKey.confirmTitle`, the // API-key confirm that names the key („{{name}}“) — one interpolated span, // runtime data. - expect(okSpans, 'correctly paired spans').toBe(94); + // 96 once objectstack-ai/objectstack#26172 added `form.createdNamed` and + // `.updatedNamed`, the record-save toasts that name the record + // („{{title}}“) — one interpolated span each, runtime data. + expect(okSpans, 'correctly paired spans').toBe(96); }); it('keeps the count identity that replaces the card’s count(„) === count(“)', () => { @@ -413,8 +416,10 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight // `detail.deleteRowConfirmation`, one more matched pair. 93 / 93 / 0 after // objectui#13330 added `console.objectView.managedUsers.inviteLine`, one // more matched pair. 94 / 94 / 0 after objectui#13355 added - // `connectAgent.apiKey.confirmTitle`, one more matched pair. - expect({ open, close, rdq }).toEqual({ open: 94, close: 94, rdq: 0 }); + // `connectAgent.apiKey.confirmTitle`, one more matched pair. 96 / 96 / 0 + // after objectstack-ai/objectstack#26172 added `form.createdNamed` and + // `.updatedNamed`, one matched pair each. + expect({ open, close, rdq }).toEqual({ open: 96, close: 96, rdq: 0 }); // The durable shape: every „ closed by a “, every surplus “ an English // opener answered by a ”. Survived translating the two English values. expect(close).toBe(open + rdq); diff --git a/ui/packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx b/ui/packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx index 3467f8d9979..04a2ecf4b76 100644 --- a/ui/packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx +++ b/ui/packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx @@ -383,10 +383,12 @@ describe('objectui#3546 slice seven — the ratchet residue', () => { } // PageView uses it twice on the same button (title + aria-label) and // RelatedList twice in two different affordances — the counts are pinned so a - // silent drop of one of them is visible. + // silent drop of one of them is visible. AppContent's two record-save toast + // sites share one message builder since objectstack-ai/objectstack#26172, + // so its fallback label is written once. expect(sourceOf(PAGE_VIEW).split("t('common.editInStudio', { defaultValue: 'Edit in studio' })").length - 1).toBe(2); expect(sourceOf(RELATED_LIST).split("t('detail.add', { defaultValue: 'Add' })").length - 1).toBe(2); - expect(sourceOf(APP_CONTENT).split("t('common.record', { defaultValue: 'Record' })").length - 1).toBe(2); + expect(sourceOf(APP_CONTENT).split("t('common.record', { defaultValue: 'Record' })").length - 1).toBe(1); }); it('dashboard.loading matches useSafeTranslate positional fallback, not a defaultValue', () => { From e9bd0d06c5376f5f01d249b51fdcf965ac995b58 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 15:15:09 +0000 Subject: [PATCH 4/6] chore(sdui): re-record the i18n registry source digest Claude-Session: https://claude.ai/code/session_01DiQzqGc2iCV6ZSmHBw4mwe Co-authored-by: Claude --- scripts/sdui-manifest.sources/i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdui-manifest.sources/i18n.json b/scripts/sdui-manifest.sources/i18n.json index 01a3863646a..c3b4cb942bc 100644 --- a/scripts/sdui-manifest.sources/i18n.json +++ b/scripts/sdui-manifest.sources/i18n.json @@ -1 +1 @@ -{"package":"ui/packages/i18n","sources":31,"sha256":"57909816bcbfa5661852ecf652392b19d6a1e971a608b07360bfa2399bca11e4","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/i18n","sources":31,"sha256":"95ff4e1411b82ad683bfd5827c30f3b7ee722b27353de7a31ff7bc92d866ab57","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} From 08ff39d0dd8467a6e2030b1e017bcaa0ee82ede5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 16:57:32 +0000 Subject: [PATCH 5/6] chore(sdui): re-record the i18n registry source digest after merging main Claude-Session: https://claude.ai/code/session_01DiQzqGc2iCV6ZSmHBw4mwe Co-authored-by: Claude --- scripts/sdui-manifest.sources/i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdui-manifest.sources/i18n.json b/scripts/sdui-manifest.sources/i18n.json index 6fe92078785..e000776c8a8 100644 --- a/scripts/sdui-manifest.sources/i18n.json +++ b/scripts/sdui-manifest.sources/i18n.json @@ -1 +1 @@ -{"package":"ui/packages/i18n","sources":31,"sha256":"ae5a36bc2327b985cb909eed35e099d1b2d953b7c4cb7dda463e4d8d3ef5cc86","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/i18n","sources":31,"sha256":"7732bad8f5d02dd8f72dbfe6885b94445ad38ff966591263854a675172bff500","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} From 19ef03844b49223c8b4640e1c827c5a437bc819b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 18:46:53 +0000 Subject: [PATCH 6/6] chore(sdui): re-record the i18n registry source digest after merging main Claude-Session: https://claude.ai/code/session_01DiQzqGc2iCV6ZSmHBw4mwe Co-authored-by: Claude --- scripts/sdui-manifest.sources/i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdui-manifest.sources/i18n.json b/scripts/sdui-manifest.sources/i18n.json index 9acdc472ef8..0c252a718c8 100644 --- a/scripts/sdui-manifest.sources/i18n.json +++ b/scripts/sdui-manifest.sources/i18n.json @@ -1 +1 @@ -{"package":"ui/packages/i18n","sources":31,"sha256":"7e7907af7823796538ad5a70e56fb172dc533ec3988a2318c8aeddf7069af14a","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/i18n","sources":31,"sha256":"83894d7c91938618b320bda4f6664c1b39a0d5d6e7c3238e8e5eac5c51e1ec39","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}