diff --git a/.changeset/11930-workspace-timezone-notice.md b/.changeset/11930-workspace-timezone-notice.md new file mode 100644 index 0000000000..f23f475cae --- /dev/null +++ b/.changeset/11930-workspace-timezone-notice.md @@ -0,0 +1,26 @@ +--- +'@object-ui/app-shell': minor +'@object-ui/types': minor +'@object-ui/i18n': minor +'@object-ui/cli': minor +--- + +New SDUI widget `cloud:workspace-timezone-notice`: one line on the Cloud welcome page naming the timezone a workspace was seeded with at creation (objectui#11930, the objectui half of objectstack-ai/cloud#2676). + +**Clause-②: yes** — four published surfaces widen, and nothing that parsed or rendered before changes: + +- the accept set of `AnyComponentSchema`, and so of `safeValidateSchema` and `objectui validate`, widens by one `type` literal, `cloud:workspace-timezone-notice`. `@object-ui/types/zod` exports one new schema, `CloudWorkspaceTimezoneNoticeSchema`, and `@object-ui/types` exports its TypeScript twin of the same name, a member of `AnySchema`; +- `@object-ui/i18n` adds one key, `cloudWorkspaceTimezoneNotice.seeded`, to all ten locale packs; +- `@object-ui/cli`: `objectui check` knows `cloud:workspace-timezone-notice` as a registered type; +- `@object-ui/app-shell` registers the widget, and its `sideEffects` array names the new module in its source and published spellings. + +**Why.** The welcome page is static metadata, and nothing in a page's expression scope carries a per-organization value, so the page could not say which timezone the workspace was created with. The seed is available only from the org-scoped `GET /cloud/environment-entitlements` summary, as the additive `workspaceTimezoneSeed` string. + +**What changed, in observable terms.** + +- A page places the node with no props: `{ "type": "cloud:workspace-timezone-notice" }`. The widget reads the summary through the hook the environment list and `cloud:plan-status` already use, and when the summary carries `workspaceTimezoneSeed` it renders one muted line naming that zone, verbatim. In English: "The workspace timezone was set to Asia/Shanghai from your browser when the workspace was created. You can change it in Settings → Localization." +- It renders nothing when the summary carries no seed (workspaces created before the seed existed, and control planes that do not send it yet), while the summary loads, when the request fails or rejects, and when the body is not the `{ success, data }` envelope. A failed request does not throw. +- The line is text only: no link and no dismissal state. +- The node's `className` and `responsiveStyles` reach the line. +- The widget is registered under one key, `cloud:workspace-timezone-notice`. There is no bare `workspace-timezone-notice` fallback and no `app-shell:`-prefixed twin. +- `CloudWorkspaceTimezoneNoticeSchema` takes no prop: `properties` is optional and may only be `{}`, so any key in the bag, the zone included, is refused at `properties`. `body` and `children` are refused by name, because the widget reads neither. diff --git a/packages/app-shell/package.json b/packages/app-shell/package.json index 3207be147e..8e8cafc1ce 100644 --- a/packages/app-shell/package.json +++ b/packages/app-shell/package.json @@ -10,6 +10,7 @@ "./dist/console/diagnostics/CloudAiModelStatus.js", "./dist/console/home/CloudOnboardingNext.js", "./dist/console/home/CloudPlanStatus.js", + "./dist/console/home/CloudWorkspaceTimezoneNotice.js", "./dist/console/marketplace/InstalledListWidget.js", "./dist/services/builtinComponents.js", "./dist/views/global-notifications-renderer.js", @@ -26,6 +27,7 @@ "./src/console/diagnostics/CloudAiModelStatus.tsx", "./src/console/home/CloudOnboardingNext.tsx", "./src/console/home/CloudPlanStatus.tsx", + "./src/console/home/CloudWorkspaceTimezoneNotice.tsx", "./src/console/marketplace/InstalledListWidget.tsx", "./src/services/builtinComponents.tsx", "./src/views/global-notifications-renderer.tsx", diff --git a/packages/app-shell/src/console/home/CloudWorkspaceTimezoneNotice.tsx b/packages/app-shell/src/console/home/CloudWorkspaceTimezoneNotice.tsx new file mode 100644 index 0000000000..182b3c7c51 --- /dev/null +++ b/packages/app-shell/src/console/home/CloudWorkspaceTimezoneNotice.tsx @@ -0,0 +1,83 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * CloudWorkspaceTimezoneNotice — one line on the Cloud control plane's welcome + * page naming the timezone a workspace was seeded with at creation, registered + * as the SDUI widget `cloud:workspace-timezone-notice` (objectui#11930, the + * objectui half of objectstack-ai/cloud#2676). + * + * The welcome page is static metadata: nothing in its expression scope carries + * a per-organization value, so a page node cannot print the seeded zone. The + * seed is only available from the org-scoped `GET /cloud/environment-entitlements` + * summary (`data.workspaceTimezoneSeed`, an additive string). This widget reads + * that summary and says one thing about it: + * + * • the summary carries a seed → one muted, localized line naming it; + * • the summary carries no seed → nothing. That covers every workspace + * created before the seed existed and every control plane that does not + * send the field yet; + * • loading, failed, or no summary → nothing. The row-derived fallback of + * `useEnvironmentEntitlements` carries no seed, so a failed summary request + * degrades to the empty render, never to a guess and never to a throw. + * + * The zone is printed VERBATIM — it is an IANA id (`Asia/Shanghai`), not a + * value to localize — and the line is text only: no link (a relative link from + * the control plane would open the control plane's own settings, cloud#2676 + * Q2) and no dismissal state. + * + * ## The summary is read through `useEnvironmentEntitlements` + * + * The same hook `cloud:plan-status` and the environment list use: one reading of + * the endpoint and its strict `{ success, data }` envelope, not another copy of + * the fetch. + */ + +import { useState } from 'react'; +import { cn } from '@object-ui/components'; +import { createAuthenticatedFetch } from '@object-ui/auth'; +import { ComponentRegistry } from '@object-ui/core'; +import { useObjectTranslation } from '@object-ui/i18n'; +import { useAdapter } from '../../providers/AdapterProvider.js'; +import { useEnvironmentEntitlements } from '../../environment/useEnvironmentEntitlements.js'; + +export interface CloudWorkspaceTimezoneNoticeProps { + /** Carries the scope class of the node's `responsiveStyles`. */ + className?: string; +} + +export function CloudWorkspaceTimezoneNotice({ className }: CloudWorkspaceTimezoneNoticeProps) { + const { t } = useObjectTranslation(); + const dataSource = useAdapter(); + // `useState`, not `useMemo`: the hook keys its fetch effect on this function, + // so its identity has to be one React guarantees to keep (AGENTS.md #10). + const [authFetch] = useState(() => createAuthenticatedFetch()); + const entitlements = useEnvironmentEntitlements({ + enabled: true, + dataSource, + authFetch, + apiBase: (import.meta.env.VITE_SERVER_URL as string | undefined) || '', + }); + + const zone = entitlements?.workspaceTimezoneSeed; + if (typeof zone !== 'string' || zone === '') return null; + + return ( +

+ {t('cloudWorkspaceTimezoneNotice.seeded', { zone })} +

+ ); +} + +// SDUI registration — the Cloud welcome page places one node. Registered under +// the `cloud` namespace with no bare fallback, so the one key is +// `cloud:workspace-timezone-notice`: the spelling the page authors, and the one +// literal its `@object-ui/types` arm claims (`CloudWorkspaceTimezoneNoticeSchema`). +ComponentRegistry.register('workspace-timezone-notice', (props: CloudWorkspaceTimezoneNoticeProps) => ( + +), { + namespace: 'cloud', + skipFallback: true, + label: 'Cloud Workspace Timezone Notice', + category: 'plugin', + inputs: [], +}); diff --git a/packages/app-shell/src/console/home/__tests__/CloudWorkspaceTimezoneNotice.test.tsx b/packages/app-shell/src/console/home/__tests__/CloudWorkspaceTimezoneNotice.test.tsx new file mode 100644 index 0000000000..d059e1cffe --- /dev/null +++ b/packages/app-shell/src/console/home/__tests__/CloudWorkspaceTimezoneNotice.test.tsx @@ -0,0 +1,199 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * CloudWorkspaceTimezoneNotice (`cloud:workspace-timezone-notice`, + * objectui#11930) prints ONE line on the Cloud welcome page naming the + * timezone the workspace was seeded with, read from the authoritative + * entitlements summary's `workspaceTimezoneSeed` — and nothing in every other + * state: no seed, a failed request, a body outside the envelope, or loading. + */ + +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render, screen, waitFor, cleanup } from '@testing-library/react'; +import React from 'react'; + +let fetchImpl: (url: string, init?: RequestInit) => Promise; +const fetchedUrls: string[] = []; + +vi.mock('@object-ui/auth', async (importOriginal) => ({ + ...(await importOriginal>()), + useAuth: () => ({ activeOrganization: { id: 'org_1' } }), + createAuthenticatedFetch: () => (url: string, init?: RequestInit) => { + fetchedUrls.push(url); + return fetchImpl(url, init); + }, +})); + +import { I18nProvider } from '@object-ui/i18n'; +import { ComponentRegistry } from '@object-ui/core'; +import { AdapterCtx, SchemaRenderer } from '@object-ui/react'; +import { CloudWorkspaceTimezoneNotice } from '../CloudWorkspaceTimezoneNotice'; + +const EN = { defaultLanguage: 'en', detectBrowserLanguage: false } as const; +const ZH = { defaultLanguage: 'zh', detectBrowserLanguage: false } as const; + +/** The two packs' sentences for the seed below — the copy a user reads. */ +const EN_LINE = + 'The workspace timezone was set to Asia/Shanghai from your browser when the workspace was created. ' + + 'You can change it in Settings → Localization.'; +const ZH_LINE = '工作区时区在创建时已按你的浏览器设为 Asia/Shanghai,可在「设置 → 本地化」中修改。'; + +/** + * The line resolves from the locale packs, so a real i18n context is needed — + * without one `t()` returns the raw key and a text assertion would pass + * against nothing. The adapter is the row-derived fallback's data source; its + * rows resolve, so a failed summary falls through to a REAL `derived` state. + */ +const adapter = { find: vi.fn().mockResolvedValue([{ id: 'env_1', environment_type: 'production' }]) }; + +const renderInShell = (ui: React.ReactElement, config: typeof EN | typeof ZH = EN) => + render( + + {ui} + , + ); + +/** The control plane's `{ success, data }` envelope around a summary. */ +function summary(data: Record) { + return { ok: true, status: 200, json: async () => ({ success: true, data }) }; +} + +const SEEDED = { plan: 'free', hasProductionEnv: true, workspaceTimezoneSeed: 'Asia/Shanghai' }; +const UNSEEDED = { plan: 'free', hasProductionEnv: true }; + +const noticeOf = (container: HTMLElement) => container.querySelector('[data-workspace-timezone-notice]'); + +/** Waits until the summary was read and the hook settled, so an empty render is a verdict, not the loading state. */ +async function settledAfterSummary() { + await waitFor(() => expect(fetchedUrls).toHaveLength(1)); + await new Promise((r) => setTimeout(r, 0)); +} + +afterEach(() => { + cleanup(); + fetchedUrls.length = 0; + adapter.find.mockClear(); +}); + +describe('CloudWorkspaceTimezoneNotice — the seed is present (objectui#11930)', () => { + it('names the seeded zone in English, verbatim', async () => { + fetchImpl = async () => summary(SEEDED); + const { container } = renderInShell(); + + expect(await screen.findByText(EN_LINE)).toBeTruthy(); + expect(noticeOf(container)?.textContent).toBe(EN_LINE); + }); + + it('names the seeded zone in Chinese, verbatim', async () => { + fetchImpl = async () => summary(SEEDED); + const { container } = renderInShell(, ZH); + + expect(await screen.findByText(ZH_LINE)).toBeTruthy(); + expect(noticeOf(container)?.textContent).toBe(ZH_LINE); + }); + + it('reads the org-scoped summary endpoint', async () => { + fetchImpl = async () => summary(SEEDED); + renderInShell(); + + await screen.findByText(EN_LINE); + expect(fetchedUrls).toEqual(['/api/v1/cloud/environment-entitlements?organizationId=org_1']); + }); + + it('is one line of text — no link, no button, no dismissal', async () => { + fetchImpl = async () => summary(SEEDED); + const { container } = renderInShell(); + + await screen.findByText(EN_LINE); + expect(container.querySelectorAll('a, button')).toHaveLength(0); + expect(noticeOf(container)?.children).toHaveLength(0); + }); +}); + +describe('CloudWorkspaceTimezoneNotice — nothing renders without a seed (objectui#11930)', () => { + it('renders nothing when the summary carries no seed — the control', async () => { + fetchImpl = async () => summary(UNSEEDED); + const { container } = renderInShell(); + + await settledAfterSummary(); + expect(container.innerHTML).toBe(''); + }); + + it('renders nothing for an empty seed', async () => { + fetchImpl = async () => summary({ ...UNSEEDED, workspaceTimezoneSeed: '' }); + const { container } = renderInShell(); + + await settledAfterSummary(); + expect(container.innerHTML).toBe(''); + }); + + it('renders nothing while the summary is loading', () => { + fetchImpl = () => new Promise(() => {}); + const { container } = renderInShell(); + + expect(container.innerHTML).toBe(''); + }); + + it('renders nothing, and does not throw, when the summary request fails', async () => { + fetchImpl = async () => ({ ok: false, status: 500, json: async () => null }); + const { container } = renderInShell(); + + // The hook fell through to the rows, which resolve (source `derived`) but + // carry no seed — so the line is left out instead of guessed. + await waitFor(() => expect(adapter.find).toHaveBeenCalledTimes(1)); + await new Promise((r) => setTimeout(r, 0)); + expect(container.innerHTML).toBe(''); + }); + + it('renders nothing, and does not throw, when the summary request rejects', async () => { + fetchImpl = async () => { + throw new TypeError('Failed to fetch'); + }; + const { container } = renderInShell(); + + await waitFor(() => expect(adapter.find).toHaveBeenCalledTimes(1)); + await new Promise((r) => setTimeout(r, 0)); + expect(container.innerHTML).toBe(''); + }); + + it('renders nothing for a bare (un-enveloped) body that carries a seed', async () => { + // A bare body is a producer contract violation (cloud#1046): it yields no + // summary, so even a seed in it must not print. + fetchImpl = async () => ({ ok: true, status: 200, json: async () => SEEDED }); + const { container } = renderInShell(); + + await waitFor(() => expect(adapter.find).toHaveBeenCalledTimes(1)); + await new Promise((r) => setTimeout(r, 0)); + expect(container.innerHTML).toBe(''); + }); +}); + +describe('`cloud:workspace-timezone-notice` registration (objectui#11930)', () => { + it('is registered under the one key the page authors, with no second spelling', () => { + expect(ComponentRegistry.has('cloud:workspace-timezone-notice')).toBe(true); + // `skipFallback: true` — no bare `workspace-timezone-notice` fallback … + expect(ComponentRegistry.has('workspace-timezone-notice')).toBe(false); + // … and no `app-shell:`-prefixed twin. + expect(ComponentRegistry.has('app-shell:cloud:workspace-timezone-notice')).toBe(false); + }); + + it('renders from a page node through SchemaRenderer', async () => { + fetchImpl = async () => summary(SEEDED); + const { container } = renderInShell( + , + ); + + expect(await screen.findByText(EN_LINE)).toBeTruthy(); + // The node's `responsiveStyles` scope class reaches the line through + // `className`, so the page can place it. + const scope = container.querySelector('style[data-os-scope]')?.getAttribute('data-os-scope'); + expect(scope).toBeTruthy(); + expect(noticeOf(container)?.classList.contains(scope as string)).toBe(true); + }); +}); diff --git a/packages/app-shell/src/environment/entitlements.ts b/packages/app-shell/src/environment/entitlements.ts index 4e2451a579..657bc3b7c2 100644 --- a/packages/app-shell/src/environment/entitlements.ts +++ b/packages/app-shell/src/environment/entitlements.ts @@ -189,6 +189,13 @@ export interface EnvironmentEntitlementsSummary { seatCount?: number; upgradeUrl?: string; contactSalesUrl?: string; + /** + * The IANA timezone the workspace was seeded with at creation, from the + * creator's browser (objectstack-ai/cloud#2676). Additive: absent when no + * seed was recorded, which includes every workspace created before the seed + * existed and every control plane that does not send it yet. + */ + workspaceTimezoneSeed?: string; } /** Combined client state (authoritative summary, or a row-derived fallback). */ @@ -206,6 +213,13 @@ export interface EnvironmentEntitlementsState { */ upgradeUrl?: string; contactSalesUrl?: string; + /** + * The summary's `workspaceTimezoneSeed`, verbatim. Absent when the control + * plane sent none — including the row-derived and unknown states, which + * never had one — and then the `cloud:workspace-timezone-notice` widget + * renders nothing (objectui#11930). + */ + workspaceTimezoneSeed?: string; /** Where the signal came from — telemetry + degradation note + tests. */ source: 'summary' | 'derived' | 'unknown'; } diff --git a/packages/app-shell/src/environment/useEnvironmentEntitlements.ts b/packages/app-shell/src/environment/useEnvironmentEntitlements.ts index dcfa9d606b..7159e23cfc 100644 --- a/packages/app-shell/src/environment/useEnvironmentEntitlements.ts +++ b/packages/app-shell/src/environment/useEnvironmentEntitlements.ts @@ -90,6 +90,9 @@ export function useEnvironmentEntitlements( // upgrade prompt then renders no CTA (objectui#10437). upgradeUrl: data.upgradeUrl, contactSalesUrl: data.contactSalesUrl, + // Verbatim, like `upgradeUrl`: absent stays absent, and the + // workspace-timezone notice then renders nothing (objectui#11930). + workspaceTimezoneSeed: data.workspaceTimezoneSeed, source: 'summary', }; } catch { diff --git a/packages/app-shell/src/index.ts b/packages/app-shell/src/index.ts index 456fcfbd77..6e6a67b2e4 100644 --- a/packages/app-shell/src/index.ts +++ b/packages/app-shell/src/index.ts @@ -336,6 +336,8 @@ import './console/connect/ConnectAgentWidget.js'; import './console/home/CloudOnboardingNext.js'; // SDUI widget for the Cloud pricing page's "current plan" marker (objectui#10919). import './console/home/CloudPlanStatus.js'; +// SDUI widget for the Cloud welcome page's seeded workspace-timezone line (objectui#11930). +import './console/home/CloudWorkspaceTimezoneNotice.js'; // SDUI widget: read-only admin diagnostic for the env's effective AI model // (cloud#797) — fetches GET /api/v1/ai/effective-model. import './console/diagnostics/CloudAiModelStatus.js'; diff --git a/packages/cli/src/__tests__/registered-types-validate-ratchet-10859.test.ts b/packages/cli/src/__tests__/registered-types-validate-ratchet-10859.test.ts index 324c6a1eba..e597833d5f 100644 --- a/packages/cli/src/__tests__/registered-types-validate-ratchet-10859.test.ts +++ b/packages/cli/src/__tests__/registered-types-validate-ratchet-10859.test.ts @@ -571,6 +571,14 @@ describe('registered NAMESPACED component types refused at `type` — a ratchet expect(BARE_KEYS).not.toContain('plan-status'); expect(refusedAtType('cloud:plan-status')).toBe(false); }); + + it('counts `cloud:workspace-timezone-notice` armed — it registered WITH its arm (objectui#11930)', () => { + // One registry key (`skipFallback: true`, so no bare `workspace-timezone-notice`), + // armed in `@object-ui/types/zod` in the same change, so the pin above did not move. + expect(NAMESPACED_KEYS).toContain('cloud:workspace-timezone-notice'); + expect(BARE_KEYS).not.toContain('workspace-timezone-notice'); + expect(refusedAtType('cloud:workspace-timezone-notice')).toBe(false); + }); }); /* ── End to end: the README document through `objectui validate` ─────────── */ diff --git a/packages/cli/src/utils/known-schema-types.ts b/packages/cli/src/utils/known-schema-types.ts index d01ef11a77..0b0ce0ea14 100644 --- a/packages/cli/src/utils/known-schema-types.ts +++ b/packages/cli/src/utils/known-schema-types.ts @@ -82,6 +82,7 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [ 'cloud:ai-model-status', 'cloud:onboarding-next', 'cloud:plan-status', + 'cloud:workspace-timezone-notice', 'code', 'code-editor', 'collapsible', diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 277bbef78f..246e4dcfd5 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -3371,6 +3371,9 @@ const ar = { cloudPlanStatus: { current: "الخطة الحالية", }, + cloudWorkspaceTimezoneNotice: { + seeded: "تم تعيين المنطقة الزمنية لمساحة العمل إلى {{zone}} وفقًا لمتصفحك عند إنشائها. يمكنك تغييرها من الإعدادات → التوطين.", + }, ai: { nlQuery: { placeholder: "اطرح سؤالًا حول بياناتك…", diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 7a4bde4c60..d75d8c1ba8 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -3124,6 +3124,9 @@ const de = { cloudPlanStatus: { current: "Aktueller Tarif", }, + cloudWorkspaceTimezoneNotice: { + seeded: "Die Zeitzone des Workspace wurde beim Erstellen anhand Ihres Browsers auf {{zone}} festgelegt. Sie können sie unter Einstellungen → Lokalisierung ändern.", + }, ai: { nlQuery: { placeholder: "Stellen Sie eine Frage zu Ihren Daten…", diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 3e73ce3a33..740a55f83b 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -3720,6 +3720,12 @@ const en = { cloudPlanStatus: { current: 'Current plan', }, + // `cloud:workspace-timezone-notice` — the Cloud welcome page's line naming the + // timezone a workspace was seeded with at creation (objectui#11930). `{{zone}}` + // is the seed, an IANA id printed verbatim. + cloudWorkspaceTimezoneNotice: { + seeded: 'The workspace timezone was set to {{zone}} from your browser when the workspace was created. You can change it in Settings → Localization.', + }, // `@object-ui/plugin-ai` — the `nl-query`, `ai-form-assist` and // `ai-recommendations` components (objectui#10232). The two count labels are // i18next count families (objectui#11445): the component passes `count` and diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index 41bfcadcb8..605631d8c5 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -3184,6 +3184,9 @@ const es = { cloudPlanStatus: { current: "Plan actual", }, + cloudWorkspaceTimezoneNotice: { + seeded: "La zona horaria del espacio de trabajo se configuró en {{zone}} a partir de su navegador al crearlo. Puede cambiarla en Configuración → Localización.", + }, ai: { nlQuery: { placeholder: "Haga una pregunta sobre sus datos…", diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index be77a728a7..3a58b3c6b9 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -3181,6 +3181,9 @@ const fr = { cloudPlanStatus: { current: "Offre actuelle", }, + cloudWorkspaceTimezoneNotice: { + seeded: "Le fuseau horaire de l’espace de travail a été défini sur {{zone}} d’après votre navigateur lors de sa création. Vous pouvez le modifier dans Paramètres → Localisation.", + }, ai: { nlQuery: { placeholder: "Posez une question sur vos données…", diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index f7cb5bff61..cf1df2ac03 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -3126,6 +3126,9 @@ const ja = { cloudPlanStatus: { current: "現在のプラン", }, + cloudWorkspaceTimezoneNotice: { + seeded: "ワークスペースのタイムゾーンは、作成時にブラウザーに合わせて {{zone}} に設定されました。「設定 → ローカリゼーション」で変更できます。", + }, ai: { nlQuery: { placeholder: "データについて質問してください…", diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 2119ee5d72..e00f908f6e 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -3123,6 +3123,9 @@ const ko = { cloudPlanStatus: { current: "현재 요금제", }, + cloudWorkspaceTimezoneNotice: { + seeded: "워크스페이스 시간대는 생성 시 브라우저에 맞춰 {{zone}}(으)로 설정되었습니다. 설정 → 현지화에서 변경할 수 있습니다.", + }, ai: { nlQuery: { placeholder: "데이터에 대해 질문하세요…", diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index bb333be7fe..b649e60bbc 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -3179,6 +3179,9 @@ const pt = { cloudPlanStatus: { current: "Plano atual", }, + cloudWorkspaceTimezoneNotice: { + seeded: "O fuso horário do workspace foi definido como {{zone}} com base no seu navegador quando ele foi criado. Você pode alterá-lo em Configurações → Localização.", + }, ai: { nlQuery: { placeholder: "Faça uma pergunta sobre seus dados…", diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index f18746d820..39c9ef2030 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -3260,6 +3260,9 @@ const ru = { cloudPlanStatus: { current: "Текущий тариф", }, + cloudWorkspaceTimezoneNotice: { + seeded: "Часовой пояс рабочего пространства был задан по вашему браузеру при его создании: {{zone}}. Его можно изменить в разделе «Настройки → Локализация».", + }, ai: { nlQuery: { placeholder: "Задайте вопрос о своих данных…", diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 72890117c9..1e43b022e2 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -3271,6 +3271,9 @@ const zh = { cloudPlanStatus: { current: '当前套餐', }, + cloudWorkspaceTimezoneNotice: { + seeded: '工作区时区在创建时已按你的浏览器设为 {{zone}},可在「设置 → 本地化」中修改。', + }, ai: { nlQuery: { placeholder: '就您的数据提问…', diff --git a/packages/types/src/__tests__/any-component-emit-headroom-11573.test.ts b/packages/types/src/__tests__/any-component-emit-headroom-11573.test.ts index e3a99bac77..44934b8fbb 100644 --- a/packages/types/src/__tests__/any-component-emit-headroom-11573.test.ts +++ b/packages/types/src/__tests__/any-component-emit-headroom-11573.test.ts @@ -314,6 +314,7 @@ describe('`AnyComponentSchema` declaration emit headroom (objectui#11573)', () = { member: 'DesignerUnionSchema', kind: 'category union', printsAs: 'DesignerUnionZodType' }, { member: 'PublicBlockComponentSchema', kind: 'category union', printsAs: 'PublicBlockComponentZodType' }, { member: 'CloudPlanStatusSchema', kind: 'arm', printsAs: 'inline' }, + { member: 'CloudWorkspaceTimezoneNoticeSchema', kind: 'arm', printsAs: 'inline' }, ]); }); diff --git a/packages/types/src/__tests__/cloud-workspace-timezone-notice-arm-11930.test.ts b/packages/types/src/__tests__/cloud-workspace-timezone-notice-arm-11930.test.ts new file mode 100644 index 0000000000..7e81d18616 --- /dev/null +++ b/packages/types/src/__tests__/cloud-workspace-timezone-notice-arm-11930.test.ts @@ -0,0 +1,114 @@ +/** + * 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. + */ + +/** + * objectui#11930 — the `cloud:workspace-timezone-notice` arm (`../zod/cloud.zod.ts`). + * + * `@object-ui/app-shell` registers `cloud:workspace-timezone-notice` for the + * Cloud welcome page, and this arm lands with it so `AnyComponentSchema` + * claims the type instead of refusing it at `type`. The arm is declared from + * the widget's read points: the widget reads no prop, so the only `properties` + * bag it takes is `{}`, and both content channels are refused by name. + * + * Refusals are asserted by issue `code` and `path` — the envelope a consumer + * reads — and not by message wording. + */ + +import { describe, expect, it } from 'vitest'; + +import { + AnyComponentSchema, + CloudWorkspaceTimezoneNoticeSchema, + StrictAnyComponentSchema, + safeValidateSchema, +} from '../zod/index.zod.js'; + +const TYPE = 'cloud:workspace-timezone-notice'; + +/** The issues of a refused parse, as `code` + dotted `path`. */ +function issuesOf(document: unknown): { code: string; path: string; message: string }[] { + const result = safeValidateSchema(document); + expect(result.success, 'expected a refusal').toBe(false); + if (result.success) return []; + return result.error.issues.map((issue) => ({ + code: issue.code, + path: issue.path.join('.'), + message: issue.message, + })); +} + +describe('`cloud:workspace-timezone-notice` — the node the welcome page authors (objectui#11930)', () => { + it('accepts the bare node on every face', () => { + const node = { type: TYPE }; + expect(CloudWorkspaceTimezoneNoticeSchema.safeParse(node).success).toBe(true); + expect(safeValidateSchema(node).success).toBe(true); + expect(StrictAnyComponentSchema.safeParse(node).success).toBe(true); + }); + + it('accepts an empty `properties` bag on every face', () => { + const node = { type: TYPE, properties: {} }; + expect(CloudWorkspaceTimezoneNoticeSchema.safeParse(node).success).toBe(true); + expect(safeValidateSchema(node).success).toBe(true); + expect(StrictAnyComponentSchema.safeParse(node).success).toBe(true); + }); + + it('accepts the node inside a page, with the spec page-component keys cloud writes beside it', () => { + const page = { + type: 'page', + children: [{ type: TYPE, id: 'welcome_timezone_notice', className: 'mt-2' }], + }; + expect(safeValidateSchema(page).success).toBe(true); + expect(StrictAnyComponentSchema.safeParse(page).success).toBe(true); + }); + + it('is claimed at `type` — the union no longer refuses the literal', () => { + // The ratchet's reading (`registered-types-validate-ratchet-10859.test.ts`). + expect(safeValidateSchema({ type: TYPE }).success).toBe(true); + // Control: an unclaimed `cloud:` literal IS refused there. + const unclaimed = issuesOf({ type: 'cloud:no-such-widget-11930' }); + expect(unclaimed.some((i) => i.code === 'invalid_union' && i.path === 'type')).toBe(true); + }); +}); + +describe('`cloud:workspace-timezone-notice` — what the arm refuses (objectui#11930)', () => { + it('refuses any key inside the bag — the widget reads none, the zone included', () => { + expect(issuesOf({ type: TYPE, properties: { zone: 'Asia/Shanghai' } })).toEqual([ + expect.objectContaining({ code: 'unrecognized_keys', path: 'properties' }), + ]); + }); + + it('refuses a bag that is not an object', () => { + expect(issuesOf({ type: TYPE, properties: 'Asia/Shanghai' })).toEqual([ + expect.objectContaining({ code: 'invalid_type', path: 'properties' }), + ]); + }); + + it('refuses a flat key the widget does not read, on the strict face', () => { + const strict = StrictAnyComponentSchema.safeParse({ type: TYPE, zone: 'Asia/Shanghai' }); + expect(strict.success).toBe(false); + expect(strict.error?.issues.map((i) => i.code)).toContain('unrecognized_keys'); + }); + + it.each(['children', 'body'])('refuses `%s` by name — the widget reads neither content channel', (key) => { + const issues = issuesOf({ type: TYPE, [key]: [{ type: 'text', content: 'x' }] }); + expect(issues).toEqual([expect.objectContaining({ code: 'invalid_type', path: key })]); + expect(issues[0].message).toContain(TYPE); + }); + + it('reaches the arm through the union, not a second schema', () => { + // The union dispatches on the literal to this arm: the same refusal either way. + const document = { type: TYPE, properties: { zone: 'Asia/Shanghai' } }; + const bare = CloudWorkspaceTimezoneNoticeSchema.safeParse(document); + const viaUnion = AnyComponentSchema.safeParse(document); + expect(bare.success).toBe(false); + expect(viaUnion.success).toBe(false); + expect(viaUnion.error?.issues.map((i) => [i.code, i.path.join('.')])).toEqual( + bare.error?.issues.map((i) => [i.code, i.path.join('.')]), + ); + }); +}); diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index 12a6d205ca..c35215caef 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -944,7 +944,7 @@ import type { z } from 'zod'; import { AIConfigSchema, AIFieldSuggestionSchema, AIFormAssistSchema, AIRecommendationItemSchema, AIRecommendationsSchema, NLQueryResultSchema, NLQuerySchema } from '../zod/ai.zod.js'; import { AppComponentSchema, AppSchemaRendererNodeSchema, MenuItemSchema as AppMenuItemSchema, NavigationAreaSchema, NavigationItemSchema } from '../zod/app.zod.js'; import { BaseSchema, ComponentConfigSchema, ComponentInputSchema, ComponentMetaSchema, KeyedI18nLabelSchema, SchemaNodeSchema } from '../zod/base.zod.js'; -import { CloudPlanStatusSchema } from '../zod/cloud.zod.js'; +import { CloudPlanStatusSchema, CloudWorkspaceTimezoneNoticeSchema } from '../zod/cloud.zod.js'; import { CalendarEventSchema, CalendarViewSchema, CarouselItemSchema, CarouselSchema, ChatbotSchema, ChatbotEnhancedSchema, ChatbotFloatingSchema, ChatMessageSchema, ChatMessageSourceSchema, ChatToolInvocationSchema, DashboardComponentSchema, DashboardConfigSchema, DashboardWidgetConfigSchema, DashboardWidgetLayoutSchema, DashboardWidgetSchema, FilterBuilderSchema, FilterFieldSchema, KanbanCardSchema, KanbanColumnSchema, CardTemplateSchema, FilterGroupSchema } from '../zod/complex.zod.js'; import { ActionSchema, CRUDDialogSchema, DetailSchema } from '../zod/crud.zod.js'; import { AlertSchema, AvatarSchema, BadgeSchema, BarChartSchema, ChartDataSeriesSchema, ChartSchema, DataTableSchema, DrillDownConfigSchema, HtmlSchema, KbdSchema, ListItemSchema, ListSchema, MarkdownSchema, PivotTableSchema, StaticTableColumnSchema, StatisticSchema, TableColumnSchema, TableSchema, TimelineEventSchema, TimelineFeedItemSchema, TimelineGanttItemBarSchema, TimelineGanttItemSchema, TimelineSchema, TreeNodeSchema, TreeViewSchema } from '../zod/data-display.zod.js'; @@ -962,7 +962,7 @@ import { DetailSectionNodeSchema, DetailViewFieldSchema, DetailViewSchema, Detai import type { AIConfig as Ts_AIConfig, AIFieldSuggestion as Ts_AIFieldSuggestion, AIFormAssistSchema as Ts_AIFormAssistSchema, AIRecommendationItem as Ts_AIRecommendationItem, AIRecommendationsSchema as Ts_AIRecommendationsSchema, NLQueryResult as Ts_NLQueryResult, NLQuerySchema as Ts_NLQuerySchema } from '../ai'; import type { AppComponentSchema as Ts_AppComponentSchema, AppSchemaRendererNodeSchema as Ts_AppSchemaRendererNodeSchema, NavigationArea as Ts_NavigationArea } from '../app'; import type { BaseSchema as Ts_BaseSchema, ComponentConfig as Ts_ComponentConfig, ComponentInput as Ts_ComponentInput, ComponentMeta as Ts_ComponentMeta, KeyedI18nLabel as Ts_KeyedI18nLabel } from '../base'; -import type { CloudPlanStatusSchema as Ts_CloudPlanStatusSchema } from '../cloud'; +import type { CloudPlanStatusSchema as Ts_CloudPlanStatusSchema, CloudWorkspaceTimezoneNoticeSchema as Ts_CloudWorkspaceTimezoneNoticeSchema } from '../cloud'; import type { CalendarEvent as Ts_CalendarEvent, CalendarViewSchema as Ts_CalendarViewSchema, CarouselItem as Ts_CarouselItem, CarouselSchema as Ts_CarouselSchema, ChatbotSchema as Ts_ChatbotSchema, ChatbotEnhancedSchema as Ts_ChatbotEnhancedSchema, ChatbotFloatingSchema as Ts_ChatbotFloatingSchema, ChatMessage as Ts_ChatMessage, ChatMessageSource as Ts_ChatMessageSource, ChatToolInvocation as Ts_ChatToolInvocation, DashboardComponentSchema as Ts_DashboardComponentSchema, DashboardWidgetLayout as Ts_DashboardWidgetLayout, DashboardWidgetSchema as Ts_DashboardWidgetSchema, FilterBuilderSchema as Ts_FilterBuilderSchema, FilterField as Ts_FilterField, KanbanCard as Ts_KanbanCard, KanbanColumn as Ts_KanbanColumn, CardTemplate as Ts_CardTemplate } from '../complex'; import type { BPMNEdge as Ts_BPMNEdge, BPMNLane as Ts_BPMNLane, BPMNNode as Ts_BPMNNode, DashboardConfig as Ts_DashboardConfig, DashboardWidgetConfig as Ts_DashboardWidgetConfig, DataModelDesignerSchema as Ts_DataModelDesignerSchema, DataModelEntity as Ts_DataModelEntity, DataModelField as Ts_DataModelField, DataModelRelationship as Ts_DataModelRelationship, DesignerCanvasConfig as Ts_DesignerCanvasConfig, DesignerFieldDefinition as Ts_DesignerFieldDefinition, DesignerFieldOption as Ts_DesignerFieldOption, DesignerPaletteCategory as Ts_DesignerPaletteCategory, DesignerPaletteItem as Ts_DesignerPaletteItem, DesignerPosition as Ts_DesignerPosition, FieldDesignerSchema as Ts_FieldDesignerSchema, ObjectDefinition as Ts_ObjectDefinition, ObjectManagerSchema as Ts_ObjectManagerSchema, PageDesignerSchema as Ts_PageDesignerSchema, ProcessDesignerSchema as Ts_ProcessDesignerSchema, ReportDesignerElement as Ts_ReportDesignerElement, ReportDesignerSchema as Ts_ReportDesignerSchema, ReportDesignerSection as Ts_ReportDesignerSection } from '../designer'; import type { CRUDDialogSchema as Ts_CRUDDialogSchema, DetailSchema as Ts_DetailSchema } from '../crud'; @@ -1800,6 +1800,8 @@ const MIRRORS = { 'base.zod.ts#KeyedI18nLabelSchema': KeyedI18nLabelSchema, // objectui#11515 — the zod-only arm's TS twin. 'cloud.zod.ts#CloudPlanStatusSchema': CloudPlanStatusSchema, + // objectui#11930 — the arm and its TS twin landed together. + 'cloud.zod.ts#CloudWorkspaceTimezoneNoticeSchema': CloudWorkspaceTimezoneNoticeSchema, 'complex.zod.ts#CalendarEventSchema': CalendarEventSchema, 'complex.zod.ts#CalendarViewSchema': CalendarViewSchema, 'complex.zod.ts#CarouselItemSchema': CarouselItemSchema, @@ -2000,6 +2002,7 @@ interface Declared { 'base.zod.ts#ComponentMetaSchema': Ts_ComponentMeta; 'base.zod.ts#KeyedI18nLabelSchema': Ts_KeyedI18nLabel; 'cloud.zod.ts#CloudPlanStatusSchema': Ts_CloudPlanStatusSchema; + 'cloud.zod.ts#CloudWorkspaceTimezoneNoticeSchema': Ts_CloudWorkspaceTimezoneNoticeSchema; 'complex.zod.ts#CalendarEventSchema': Ts_CalendarEvent; 'complex.zod.ts#CalendarViewSchema': Ts_CalendarViewSchema; 'complex.zod.ts#CarouselItemSchema': Ts_CarouselItem; @@ -5229,7 +5232,7 @@ const ZOD_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'zod'); * MINUEND under it had moved. Nothing failed on any of those days, because nothing * compared the registry to a number. objectui#7433 is that absence, not the digits. */ -const EXPECTED_MIRROR_PAIRS = 194; +const EXPECTED_MIRROR_PAIRS = 195; /** * A ledger this file can size from its own AST. `WiderThanDeclared` joined at diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index 6ee13640d9..356457f5b2 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -9,7 +9,7 @@ /** * @object-ui/types - Cloud Widget Schemas * - * The TypeScript twin of `zod/cloud.zod.ts` (objectui#11515). + * The TypeScript twin of `zod/cloud.zod.ts` (objectui#11515, objectui#11930). * * @module cloud * @packageDocumentation @@ -68,3 +68,48 @@ export interface CloudPlanStatusSchema extends BaseSchema { */ children?: never; } + +/** + * `cloud:workspace-timezone-notice` — the Cloud welcome page's line naming the + * timezone a workspace was seeded with at creation, the SDUI widget + * `@object-ui/app-shell` registers for that page (objectui#11930): the + * TypeScript twin of `zod/cloud.zod.ts`'s `CloudWorkspaceTimezoneNoticeSchema`, + * landed with the arm. + * + * The members are read from the widget's read points, as the arm's module + * header gives them: + * + * - `properties` — optional and empty: the widget reads no key of the bag. + * The zone it prints comes from the organization's entitlements summary, + * never from the page. + * - Neither content channel is read, so both are refused by name, the twin + * of the arm's two `retirementTombstone` members. + * + * The parity pair is `cloud.zod.ts#CloudWorkspaceTimezoneNoticeSchema` in + * `__tests__/zod-mirror-parity.test.ts`. The widget's own props type stays in + * `@object-ui/app-shell`. + */ +export interface CloudWorkspaceTimezoneNoticeSchema extends BaseSchema { + type: 'cloud:workspace-timezone-notice'; + /** + * The props bag — the widget reads no prop, so the only bag it accepts is + * `{}`. The zone it names comes from the organization's entitlements + * summary. + */ + properties?: Record; + /** + * REFUSED BY NAME (objectui#11930) — `cloud:workspace-timezone-notice` reads + * neither content channel: `CloudWorkspaceTimezoneNotice` reads only + * `className`, and `SchemaRenderer` strips both channels out of the props it + * spreads. + * + * @deprecated Not a channel `cloud:workspace-timezone-notice` reads — nothing renders it. + */ + body?: never; + /** + * REFUSED BY NAME (objectui#11930), for the reason `body` gives. + * + * @deprecated Not a channel `cloud:workspace-timezone-notice` reads — nothing renders it. + */ + children?: never; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 6fcb41f349..697e532b3d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -847,7 +847,7 @@ import type { import type { CRUDComponentSchema } from './crud.js'; import type { ObjectQLComponentSchema, ListViewSchema } from './objectql.js'; import type { AppComponentSchema, AppSchemaRendererNodeSchema } from './app.js'; -import type { CloudPlanStatusSchema } from './cloud.js'; +import type { CloudPlanStatusSchema, CloudWorkspaceTimezoneNoticeSchema } from './cloud.js'; import type { PageDesignerSchema, DataModelDesignerSchema, @@ -971,8 +971,10 @@ export type { } from './ai.js'; export type { - // Cloud widgets — the `cloud:plan-status` node's TS twin (objectui#11515). + // Cloud widgets — the `cloud:plan-status` node's TS twin (objectui#11515) + // and the `cloud:workspace-timezone-notice` node's (objectui#11930). CloudPlanStatusSchema, + CloudWorkspaceTimezoneNoticeSchema, } from './cloud.js'; // The Block System re-export block is GONE, not emptied: `BlockSchema`, @@ -1071,7 +1073,9 @@ export type AnySchema = // objectui#11515 — the TS twins of two zod-only arms; the third, // `DetailSectionNodeSchema`, is a member through `ViewComponentSchema`. | AppSchemaRendererNodeSchema - | CloudPlanStatusSchema; + | CloudPlanStatusSchema + // objectui#11930 — landed with its zod arm. + | CloudWorkspaceTimezoneNoticeSchema; /** * Utility type to extract the schema type from a type string. diff --git a/packages/types/src/zod/README.md b/packages/types/src/zod/README.md index bfee9cdc28..e185e88881 100644 --- a/packages/types/src/zod/README.md +++ b/packages/types/src/zod/README.md @@ -280,9 +280,10 @@ The last six arrived with `@objectstack/spec` 17.5.0, whose rows for them were m - `AppSchemaRendererNodeSchema` — `app-schema-renderer`, `@object-ui/layout`'s whole-shell node (objectui#11440). It declares its registration's three inputs: `schema`, the app document the shell draws, which is the `app` document's own arm in `app.zod.ts`, the same schema object, by reference (objectui#11494); `mobileNavMode` (`'drawer'` | `'bottom_nav'`); and `basePath`. The document is nested, `{ "type": "app-schema-renderer", "schema": { "type": "app", "navigation": [...] } }`, and keeps its own refusals there (`mobileNavMode` inside `schema` is refused, objectui#11363). Its keys written flat on the node are refused by the strict face as unrecognized keys, and the node does not draw them: one spelling. Both content channels are refused: a node draws neither. - `DetailSectionNodeSchema` — `detail-section`, one field section of `@object-ui/plugin-detail` as a node (objectui#11440): the ten `DetailViewSectionSchema` members its registration publishes, flat on the node, `fields` required. It is not `record:details`'s section shape (that one names its heading `label` and takes field names), so it is a node of its own. -### Cloud Widgets (1) +### Cloud Widgets (2) `@object-ui/app-shell`'s `cloud:` SDUI widgets. `@objectstack/spec` has no `ComponentPropsMap` row for them, so each arm is declared here from the keys its widget reads. - `CloudPlanStatusSchema` — `cloud:plan-status`, the pricing page's "current plan" marker: `properties` must be exactly `{ plan }`, a non-empty plan code. +- `CloudWorkspaceTimezoneNoticeSchema` — `cloud:workspace-timezone-notice`, the welcome page's line naming the workspace's seeded timezone: the widget reads no prop, so `properties` may only be `{}`. The zone comes from the organization's entitlements summary. ## Schema Structure diff --git a/packages/types/src/zod/cloud.zod.ts b/packages/types/src/zod/cloud.zod.ts index 2b14c511b8..4fe384ae3e 100644 --- a/packages/types/src/zod/cloud.zod.ts +++ b/packages/types/src/zod/cloud.zod.ts @@ -9,9 +9,11 @@ /** * @object-ui/types/zod - Cloud Widget Zod Validators * - * The arm for `cloud:plan-status`, the SDUI widget `@object-ui/app-shell` - * registers for the Cloud pricing page (`console/home/CloudPlanStatus.tsx`, - * objectui#10919). + * The arms for `@object-ui/app-shell`'s armed `cloud:` SDUI widgets: + * `cloud:plan-status`, for the Cloud pricing page + * (`console/home/CloudPlanStatus.tsx`, objectui#10919), and + * `cloud:workspace-timezone-notice`, for the Cloud welcome page + * (`console/home/CloudWorkspaceTimezoneNotice.tsx`, objectui#11930). * * ## Why the arm lands with the registration * @@ -28,6 +30,8 @@ * nothing is read by reference: the members below are the keys the widget * reads, and no other. * + * `cloud:plan-status`: + * * - `properties.plan` — the plan code of the card the node sits on. The page * authors it in the `properties` bag, the one spelling a page component is * written in, and the widget reads it there alone. Required: a node that @@ -38,6 +42,16 @@ * - `body` / `children` — refused by name. The widget renders a badge and * reads neither content channel (objectui#9256's rule for such nodes). * + * `cloud:workspace-timezone-notice`: + * + * - `properties` — optional and EMPTY: the widget reads no key of the bag. + * The zone it prints comes from the organization's entitlements summary, + * never from the page, so there is nothing for the node to author. The + * closed, empty bag is `element:divider`'s spelling for a node with no + * prop (`public-blocks.zod.ts`). + * - `body` / `children` — refused by name. The widget renders one line of + * its own copy and reads neither content channel. + * * ⛔ No `.default()` anywhere in this module — see the "authors no default" * note in `index.zod.ts`. * @@ -80,3 +94,31 @@ export const CloudPlanStatusSchema = BaseSchema.extend({ body: retirementTombstone(CLOUD_PLAN_STATUS_NEITHER_CHANNEL), children: retirementTombstone(CLOUD_PLAN_STATUS_NEITHER_CHANNEL), }); + +/** One refusal string for both content channels of `CloudWorkspaceTimezoneNoticeSchema`. */ +const CLOUD_WORKSPACE_TIMEZONE_NOTICE_NEITHER_CHANNEL = + 'REFUSED (objectui#11930) — `cloud:workspace-timezone-notice` reads NEITHER content channel: its ' + + 'registration hands the node to `CloudWorkspaceTimezoneNotice`, which reads only `className`, and ' + + '`SchemaRenderer` strips both channels out of the props bag it spreads. An authored value would ' + + 'render NOTHING — no render-time error or warning and no element; only the parser tier\'s ' + + '`not-a-container` warning (objectui#9910) noticed it, because the registration declares no ' + + '`children` input. What it renders instead: one line naming the timezone the workspace was seeded ' + + 'with, read from the organization\'s entitlements summary, and nothing when the summary carries no seed.'; + +/** + * `cloud:workspace-timezone-notice` — the welcome page's line naming the + * workspace's seeded timezone (objectui#11930). Pinned by + * `../__tests__/cloud-workspace-timezone-notice-arm-11930.test.ts`. + */ +export const CloudWorkspaceTimezoneNoticeSchema = BaseSchema.extend({ + type: z.literal('cloud:workspace-timezone-notice'), + properties: z + .strictObject({}) + .optional() + .describe( + 'The `cloud:workspace-timezone-notice` props bag — the widget reads no prop, so the only bag it ' + + 'accepts is `{}`. The zone it names comes from the organization\'s entitlements summary.', + ), + body: retirementTombstone(CLOUD_WORKSPACE_TIMEZONE_NOTICE_NEITHER_CHANNEL), + children: retirementTombstone(CLOUD_WORKSPACE_TIMEZONE_NOTICE_NEITHER_CHANNEL), +}); diff --git a/packages/types/src/zod/index.zod.ts b/packages/types/src/zod/index.zod.ts index 5f97f19888..885227bf07 100644 --- a/packages/types/src/zod/index.zod.ts +++ b/packages/types/src/zod/index.zod.ts @@ -513,7 +513,7 @@ export { // ============================================================================ // Cloud Widgets - `@object-ui/app-shell`'s `cloud:` SDUI widgets (objectui#10919) // ============================================================================ -export { CloudPlanStatusSchema } from './cloud.zod.js'; +export { CloudPlanStatusSchema, CloudWorkspaceTimezoneNoticeSchema } from './cloud.zod.js'; // ============================================================================ // Union Types - All Component Schemas @@ -537,7 +537,7 @@ import { ViewComponentSchema } from './views.zod.js'; import { AIComponentSchema } from './ai.zod.js'; import { DesignerUnionSchema } from './designer.zod.js'; import { PublicBlockComponentSchema } from './public-blocks.zod.js'; -import { CloudPlanStatusSchema } from './cloud.zod.js'; +import { CloudPlanStatusSchema, CloudWorkspaceTimezoneNoticeSchema } from './cloud.zod.js'; import { nestedComponentJudgment } from './nested-component-walk.js'; /** @@ -642,6 +642,7 @@ export const AnyComponentSchema = defineNodeComponentUnion(z.discriminatedUnion( DesignerUnionSchema, PublicBlockComponentSchema, CloudPlanStatusSchema, + CloudWorkspaceTimezoneNoticeSchema, ], { // Zod's default message for a missed discriminator spells out EVERY accepted // literal — measured, 1,462 chars naming all 107. That is the "print every