Skip to content

Commit 88d6020

Browse files
feat(studio): New Package asks only for a name; the package ID, namespace, version and type sit behind Advanced (#27883)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg4i9JiKjEjwtuVzd4gKy9hB)_ ## 维护者速读 - 改了什么:Studio「新建软件包」对话框第一步只剩「显示名称」和可选的「描述」;软件包 ID、对象命名空间、版本、类型收进默认折叠的「高级」区,名称下方一行写明自动生成的 ID 和命名空间。 - 为什么改:原来一打开就是反向域名 ID、命名空间、SemVer 版本和类型,面向开发者;其实 ID/命名空间/版本早就能从名称自动生成,缺的是一个普通人能直接回答的第一步。 - 风险与回滚:只改 ui 的这个对话框和 studio 中英文案;「高级」里的字段任何一个挡住「创建」时会自动展开(例如纯中文名称生成不了 ID),不会出现按钮灰着却看不到原因。编辑已有软件包不变。回滚 = revert 本 PR。 - 先看:`PackageFormDialog.tsx` 的 `advancedNeeded` / `showAdvanced` 两行和 `Collapsible` 一段;`PackageFormDialog.nameOnlyFirstStep-27833.test.tsx`。 - 两分钟验证:Studio → 首页「新建」→「新建软件包」,只输入 `Field Service` 点「创建软件包」,应直接建成,名称下方先显示自动生成的 ID(有当前组织时形如 `com.ORG.field-service`)和命名空间 `field_service`;改输 `工单中心`,「高级」应自动展开并提示需要手填 ID。 - 需要你做的:无,席位落地。 Fixes #27833 Clause-②: no Metadata-protocol: none ## What changes **Create mode of `PackageFormDialog`** (Home → New → New Package, Packages → New Package, the Studio switcher's New package): - The **Basics** card holds only the display name and the optional description. Under the name, once the id and namespace it derived are valid, one line says what they are: "Its ID will be ID, and its object names will start with NAMESPACE_. You can change both under Advanced." (`engine.packages.create.derivedSummary`). - The package ID (`PackageIdInput`), object namespace, version and type move into one **Advanced** disclosure, collapsed by default, with a hint line ("Package ID, namespace, version and type, filled in from the name."). The spec form's own Advanced rows (`defaultDatasource`) and the dependencies editor join the same disclosure, so the dialog no longer shows two sections both called Advanced. - The disclosure's content stays mounted while closed (`forceMount` plus `hidden`), so the id input keeps its own state across a collapse and the rows leave the accessibility tree while hidden. - The disclosure opens by itself whenever, with a name typed, the id, namespace or version is what keeps **Create package** disabled (`advancedNeeded`). A name with no Latin letters (no id derived), an id the spec refuses, or an emptied namespace never leaves the author at a disabled button with the reason folded away; while that holds, the disclosure cannot be closed. - The dialog description drops the reverse-domain explanation and says what a package is, in plain words (en + zh changed together, as `check:i18n-drift` requires for the studio catalogue). Unchanged: the derivation rules (objectui#11792 id from the name, framework#2694 namespace from the id, objectui#12512 marks and select-on-focus), the spec validation, the POST body, and edit / view mode. ## Tests - New `PackageFormDialog.nameOnlyFirstStep-27833.test.tsx` (7 cases): the first step's textboxes are exactly name + description and the technical rows are out of the accessibility tree; a name alone creates the package with the derived id / namespace / version / type in the POST; Advanced shows the derived id and namespace and both are editable; an edited id does not move on relabel; a hand-typed id the spec refuses is refused in words (`idRule.formatHint`), Create waits, and the disclosure cannot be folded; a CJK-only name opens Advanced by itself at the `idRule.notDerived` line; edit mode has no disclosure and keeps the locked id and the version row. - Pins moved with the layout, each with its reason in a comment: `derivedFields-12512` (id / namespace / version now ordered under Advanced, the name still first in Basics), `createCopy-13126` (positive control is the description's own sentence, since the example id left the copy), `dependenciesEditor-13278` (e) (opens Advanced first), and the live spec `ui/e2e/live/studio-object-designer.spec.ts` F1 (clicks Advanced before filling the id; Playwright's `fill` waits for a visible input). ## Acceptance notes - **A name alone creates the package**: case 2 posts `{ id: 'com.field-service', namespace: 'field_service', name: 'Field Service', version: '0.1.0', type: 'app' }` after typing only the name. With an active organization the id is `com.ORG.field-service`, as before (the card's `com.default.field-service` is that rule with the org slug `default`). - **Derived id / namespace shown and editable under Advanced**: case 3. - **An edited id never moves on relabel**: case 4. - **An invalid hand-typed id is refused in words**: case 5 (the spec-rule sentence under the input, Create disabled). - **Editing an existing package keeps today's fields**: case 7, plus the unchanged edit / view suites (`idDerive-11792`, `editDependencies-13121`, `createScope-12869`, `EditPackageDialog`). - **Not changed, on purpose**: the landing copy "writable base" / "environment overlay" pinned by `BuilderLanding.environmentOverlayCopy-12171`; this card's step does not need it. The English help lines under the id and version rows are still `ManifestSchema`'s `.describe()` text ("Unique package identifier — must match reverse-domain notation …", "Package version (SemVer 2.0.0 …)"); they now sit under Advanced, where that developer wording fits, and `helpTextI18n` keeps the spec as their one English producer. Rewording them at the source is a `packages/spec` change, named as a seam in the report, not taken here. - **Not measured in a browser**: the layout was checked through jsdom (roles, `hidden`, `aria-expanded`); the live spec F1 (`ui/e2e/live/studio-object-designer.spec.ts`) drives the dialog in Chromium wherever the live e2e suite runs. ## Verification <details><summary>Tests (head 05d9ba6)</summary> - `pnpm --dir ui exec vitest run` over 37 named files (every test that renders `PackageFormDialog`, `CreatePackageDialog`, `EditPackageDialog`, `PackagesPage` or `BuilderLanding`) plus `packages/i18n/src/__tests__/`, under the verify lock: first run `Test Files 1 failed | 126 passed (127) · Tests 1 failed | 1666 passed | 13 skipped`; the one red was `dependenciesEditor-13278` (e), which now opens Advanced first, then `Tests 5 passed (5)` on that file. - The new file plus the two moved pins: `Test Files 3 passed (3) · Tests 23 passed (23)`. - Not run locally: the whole `app-shell` suite and the live e2e; CI runs both. </details> <details><summary>Ablations (scripts/ablation-replace.mjs, mutation proven on disk, restored to the HEAD blob)</summary> app-shell tests import `src`, so no dist build is involved. - A: `const advancedNeeded = createMode` → `const advancedNeeded = false && !'ABLATED_27833_A' && createMode` (the disclosure no longer opens by itself): `Tests 2 failed | 5 passed (7)`, red on "a hand-typed id the spec refuses … Advanced cannot fold it away" and "a name that derives no id opens Advanced by itself". Restored: blob `1cc49767f808` == HEAD, `git diff HEAD` empty. - B: `hidden={!showAdvanced}` → `hidden={false && !'ABLATED_27833_B'}` (the closed disclosure stays visible): `Tests 1 failed | 6 passed (7)`, red on "opens with the name and the description in reach, the technical rows folded away". Restored the same way. </details> <details><summary>Gates (head 05d9ba6)</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 34 commands; each run and its exit code recorded before any pipe; `--ran` reconciliation: `34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN`. Four more run because the diff changes studio copy, app-shell / i18n sources and an e2e spec (outside the derivation): ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-sdui-manifest.mjs :: exit 0 node scripts/check-sdui-manifest.mjs --self-test :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --dir ui exec vitest run scripts/ :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 pnpm --dir ui run check:i18n-drift :: exit 0 pnpm --filter @object-ui/app-shell run type-check :: exit 0 pnpm --filter @object-ui/i18n run type-check :: exit 0 pnpm --dir ui run type-check:e2e :: exit 0 ``` Everything CI runs beyond this subset: the PR's check-runs. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CZHUokxLoPS381KzP6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a68430a commit 88d6020

9 files changed

Lines changed: 371 additions & 83 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
feat(studio): New Package asks only for a name; the package ID, namespace, version and type sit behind Advanced
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
The New Package dialog (Home → New → New Package, and Packages → New Package) used to open on a reverse-domain package ID, an object namespace, a SemVer version and a package type, all up front. Its first step now holds the display name and the optional description. The ID and namespace are still filled in from the name, and a line under the name says what they will be. The four technical fields sit in a collapsed **Advanced** section, where they stay editable; it opens by itself when one of them is what keeps **Create package** disabled, for example a name with no Latin letters to make an ID from. The dialog's description no longer explains reverse-domain IDs. Editing an existing package is unchanged.

‎ui/e2e/live/studio-object-designer.spec.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ test('F1: a newly created package appears in the switcher without a reload', asy
126126
await chip.click();
127127
await page.getByRole('button', { name: /New package/i }).click();
128128
await page.getByRole('dialog').getByLabel(/^Display name/).first().fill(pkgName);
129+
// The id sits in the collapsed Advanced section (objectstack#27833).
130+
await page.getByTestId('package-form-advanced-toggle').click();
129131
await page.getByTestId('package-form-id-input').fill(pkgId);
130132
await page.getByTestId('package-form-submit').click();
131133

‎ui/packages/app-shell/src/views/metadata-admin/PackageFormDialog.createCopy-13126.test.tsx‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,25 @@
99
* description carries no scope word at all.
1010
*
1111
* The description is read off the rendered dialog through its
12-
* `aria-describedby` link, and the interpolated `{example}` is the positive
13-
* control that the node read is the description and not an empty one.
12+
* `aria-describedby` link, and its catalogue sentence is the positive control
13+
* that the node read is the description and not an empty one. (It used to be
14+
* the interpolated example id; objectstack#27833 took the id out of the first
15+
* step's copy.)
1416
*/
1517

1618
import '@testing-library/jest-dom/vitest';
1719
import * as React from 'react';
1820
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1921
import { render, screen } from '@testing-library/react';
2022
import { PackageFormDialog } from './PackageFormDialog';
23+
import { t } from './i18n';
24+
// The zh rows live in `@object-ui/i18n`'s `studio` namespace, fetched on
25+
// demand (objectstack#25922). Resident before any case, so the dialog and the
26+
// positive control below read the same catalogue: rendering zh otherwise
27+
// starts the fetch, and the control could read zh while the dialog had
28+
// rendered the en fallback.
29+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
30+
await loadStudioCatalogue('zh');
2131

2232
let localeFixture: 'en-US' | 'zh-CN' = 'en-US';
2333
vi.mock('./i18n', async (importOriginal) => ({
@@ -39,8 +49,8 @@ async function createDescription(): Promise<string> {
3949
const dialog = screen.getByRole('dialog');
4050
const ids = (dialog.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean);
4151
const text = ids.map((id) => document.getElementById(id)?.textContent ?? '').join(' ');
42-
// Positive control: the example id is interpolated into the description.
43-
expect(text).toContain('com.acme.crm');
52+
// Positive control: the node read is the description's own sentence.
53+
expect(text).toContain(t('engine.packages.create.description', localeFixture));
4454
return text;
4555
}
4656

‎ui/packages/app-shell/src/views/metadata-admin/PackageFormDialog.dependenciesEditor-13278.test.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
* (b) adding `@acme/core` → `^1.2.0` and saving in edit mode sends that map;
1515
* (c) removing every row sends `{}`, which clears the row (objectui#13121);
1616
* (d) a duplicate package name is flagged inline and nothing is sent;
17-
* (e) CONTROL — create mode shows the same editor.
17+
* (e) CONTROL — create mode shows the same editor (under Advanced,
18+
* objectstack#27833).
1819
*/
1920

2021
import * as React from 'react';
@@ -146,6 +147,9 @@ describe('PackageFormDialog — the Dependencies row is a key/value editor (obje
146147

147148
it('(e) CONTROL — create mode shows the same editor', async () => {
148149
openDialog('create');
150+
// objectstack#27833 — on create the editor sits in the collapsed
151+
// Advanced disclosure with the other technical rows.
152+
fireEvent.click(screen.getByTestId('package-form-advanced-toggle'));
149153
const group = await dependencies();
150154
expect(within(group).getByTestId('package-dependencies-empty').textContent).toBe('No dependencies.');
151155
addRow(group, '@acme/core', '^1.2.0');

‎ui/packages/app-shell/src/views/metadata-admin/PackageFormDialog.derivedFields-12512.test.tsx‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,17 @@ function openCreate() {
4949
}
5050

5151
describe('New package form: display name first (objectui#12512)', () => {
52-
it('opens on the display name, with the id, namespace and version after it in the same card', () => {
52+
// objectstack#27833 moved the id, namespace and version out of the Basics
53+
// card into the Advanced disclosure; the name still opens the form, and
54+
// the three keep their order behind it.
55+
it('opens on the display name, with the id, namespace and version after it under Advanced', () => {
5356
const f = openCreate();
54-
const card = screen.getByTestId('package-form-basics');
55-
const inputs = within(card).getAllByRole('textbox');
56-
const order = [f.name(), f.id(), f.ns(), f.version()].map((el) => inputs.indexOf(el));
57-
expect(order).toEqual([0, 1, 2, 3]);
57+
const basics = within(screen.getByTestId('package-form-basics')).getAllByRole('textbox');
58+
expect(basics.indexOf(f.name())).toBe(0);
59+
const advanced = screen.getByTestId('package-form-advanced');
60+
const inputs = Array.from(advanced.querySelectorAll('input'));
61+
const order = [f.id(), f.ns(), f.version()].map((el) => inputs.indexOf(el));
62+
expect(order).toEqual([0, 1, 2]);
5863
});
5964

6065
it('typing a display name fills the id, namespace and version, each marked until edited', () => {
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* New package: the first step asks only for a name (objectstack#27833).
5+
*
6+
* Measured on the card: Home → New → New Package opened on a reverse-domain
7+
* id, an object namespace, a SemVer version and a package type, all up front,
8+
* under developer help. The id, namespace and version were already filled in
9+
* from the name (objectui#11792, objectui#12512); what was missing was a first
10+
* step a non-developer can answer. The technical rows now sit behind one
11+
* collapsed Advanced disclosure, and a line under the name says what was
12+
* filled in.
13+
*
14+
* "Closed" is read the way a browser hides it: the disclosure's content
15+
* carries `hidden`, so its rows leave the accessibility tree (`getByRole`
16+
* cannot reach them) while staying mounted.
17+
*/
18+
19+
import '@testing-library/jest-dom/vitest';
20+
import * as React from 'react';
21+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
22+
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
23+
import userEvent from '@testing-library/user-event';
24+
import { PackageFormDialog } from './PackageFormDialog';
25+
import { t, tFormat } from './i18n';
26+
27+
vi.mock('./i18n', async (importOriginal) => ({
28+
...(await importOriginal<Record<string, unknown>>()),
29+
useMetadataLocale: () => 'en-US',
30+
}));
31+
32+
let fetchMock: ReturnType<typeof vi.fn>;
33+
beforeEach(() => {
34+
fetchMock = vi.fn(
35+
async () => ({ ok: true, status: 201, text: async () => '{"success":true,"data":{}}' }) as Response,
36+
);
37+
vi.stubGlobal('fetch', fetchMock);
38+
});
39+
afterEach(() => {
40+
cleanup();
41+
vi.unstubAllGlobals();
42+
});
43+
44+
const label = (key: string) => t(key, 'en-US');
45+
46+
function openCreate() {
47+
render(<PackageFormDialog mode="create" open onOpenChange={vi.fn()} />);
48+
const byLabel = (key: string) => screen.getByLabelText(label(key), { exact: false }) as HTMLInputElement;
49+
return {
50+
name: () => byLabel('engine.packages.create.name'),
51+
id: () => byLabel('engine.packages.create.id'),
52+
ns: () => byLabel('engine.packages.create.namespace'),
53+
toggle: () => screen.getByTestId('package-form-advanced-toggle'),
54+
content: () => screen.getByTestId('package-form-id').parentElement as HTMLElement,
55+
submit: () => screen.getByTestId('package-form-submit'),
56+
};
57+
}
58+
59+
describe('the first step asks only for a name (objectstack#27833)', () => {
60+
it('opens with the name and the description in reach, the technical rows folded away', () => {
61+
const f = openCreate();
62+
const basics = screen.getByTestId('package-form-basics');
63+
expect(within(basics).getAllByRole('textbox')).toEqual([
64+
f.name(),
65+
screen.getByLabelText(label('engine.packages.detail.description'), { exact: false }),
66+
]);
67+
68+
expect(f.toggle()).toHaveAttribute('aria-expanded', 'false');
69+
expect(f.content()).toHaveAttribute('hidden');
70+
// Mounted, but out of the accessibility tree while closed.
71+
expect(screen.queryByRole('textbox', { name: new RegExp(label('engine.packages.create.id')) })).toBeNull();
72+
expect(screen.queryByRole('textbox', { name: new RegExp(label('engine.packages.create.version')) })).toBeNull();
73+
});
74+
75+
it('a name alone creates the package, with the id and namespace it derived', async () => {
76+
const user = userEvent.setup();
77+
const f = openCreate();
78+
await user.type(f.name(), 'Field Service');
79+
80+
expect(screen.getByTestId('package-form-derived-summary')).toHaveTextContent(
81+
tFormat('engine.packages.create.derivedSummary', 'en-US', {
82+
id: 'com.field-service',
83+
namespace: 'field_service',
84+
}),
85+
);
86+
expect(f.toggle()).toHaveAttribute('aria-expanded', 'false');
87+
88+
await user.click(f.submit());
89+
expect(fetchMock).toHaveBeenCalledTimes(1);
90+
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
91+
expect(url).toBe('/api/v1/packages');
92+
expect(JSON.parse(String(init.body)).manifest).toMatchObject({
93+
id: 'com.field-service',
94+
namespace: 'field_service',
95+
name: 'Field Service',
96+
version: '0.1.0',
97+
type: 'app',
98+
});
99+
});
100+
101+
it('Advanced shows the derived id and namespace, and both are editable there', async () => {
102+
const user = userEvent.setup();
103+
const f = openCreate();
104+
await user.type(f.name(), 'Field Service');
105+
await user.click(f.toggle());
106+
107+
expect(f.content()).not.toHaveAttribute('hidden');
108+
const id = screen.getByRole('textbox', { name: new RegExp(label('engine.packages.create.id')) });
109+
const ns = screen.getByRole('textbox', { name: new RegExp(label('engine.packages.create.namespace')) });
110+
expect(id).toHaveValue('com.field-service');
111+
expect(ns).toHaveValue('field_service');
112+
113+
await user.clear(ns);
114+
await user.type(ns, 'fs');
115+
expect(ns).toHaveValue('fs');
116+
expect(screen.getByTestId('package-form-derived-summary')).toHaveTextContent(
117+
tFormat('engine.packages.create.derivedSummary', 'en-US', { id: 'com.field-service', namespace: 'fs' }),
118+
);
119+
});
120+
121+
it('an edited id never moves when the name changes again', async () => {
122+
const user = userEvent.setup();
123+
const f = openCreate();
124+
await user.type(f.name(), 'Field Service');
125+
await user.click(f.toggle());
126+
await user.clear(f.id());
127+
await user.type(f.id(), 'com.acme.repairs');
128+
129+
await user.clear(f.name());
130+
await user.type(f.name(), 'Repairs Center');
131+
expect(f.id()).toHaveValue('com.acme.repairs');
132+
expect(f.ns()).toHaveValue('repairs');
133+
});
134+
135+
it('a hand-typed id the spec refuses is refused in words, Create waits, and Advanced cannot fold it away', async () => {
136+
const user = userEvent.setup();
137+
const f = openCreate();
138+
await user.type(f.name(), 'Field Service');
139+
await user.click(f.toggle());
140+
await user.clear(f.id());
141+
await user.type(f.id(), 'acme');
142+
143+
expect(screen.getByTestId('pkg-id-format-hint')).toHaveTextContent(
144+
tFormat('engine.packages.idRule.formatHint', 'en-US', { example: 'com.acme.crm' }),
145+
);
146+
expect(f.submit()).toBeDisabled();
147+
expect(screen.queryByTestId('package-form-derived-summary')).toBeNull();
148+
149+
await user.click(f.toggle());
150+
expect(f.content()).not.toHaveAttribute('hidden');
151+
expect(screen.getByTestId('pkg-id-format-hint')).toBeVisible();
152+
});
153+
154+
it('a name that derives no id opens Advanced by itself, at the line that says why', () => {
155+
const f = openCreate();
156+
fireEvent.change(f.name(), { target: { value: '工单中心' } });
157+
158+
expect(f.submit()).toBeDisabled();
159+
expect(f.toggle()).toHaveAttribute('aria-expanded', 'true');
160+
expect(f.content()).not.toHaveAttribute('hidden');
161+
expect(screen.getByTestId('package-form-id-missing')).toHaveTextContent(
162+
tFormat('engine.packages.idRule.notDerived', 'en-US', { example: 'com.acme.crm' }),
163+
);
164+
});
165+
});
166+
167+
describe('editing an existing package keeps its fields (objectstack#27833)', () => {
168+
it('has no Advanced disclosure of its own and shows the locked id with the other rows', () => {
169+
const manifest = { id: 'com.acme.crm', name: 'Acme CRM', version: '1.0.0', type: 'app', namespace: 'crm' };
170+
render(<PackageFormDialog mode="edit" open onOpenChange={vi.fn()} manifest={manifest} />);
171+
172+
expect(screen.queryByTestId('package-form-advanced')).toBeNull();
173+
expect(screen.queryByTestId('package-form-derived-summary')).toBeNull();
174+
const id = screen.getByLabelText(label('engine.packages.create.id'), { exact: false }) as HTMLInputElement;
175+
expect(id).toHaveValue('com.acme.crm');
176+
expect(id).toHaveAttribute('readonly');
177+
expect(screen.getByRole('textbox', { name: new RegExp(label('engine.packages.create.version')) })).toHaveValue(
178+
'1.0.0',
179+
);
180+
});
181+
});

0 commit comments

Comments
 (0)