@nocobase/app-template-default 1.0.0-beta.88 → 1.0.0-beta.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.agents/rules/components.md +3 -1
  2. package/.agents/rules/shell.md +8 -2
  3. package/AGENTS.md +1 -1
  4. package/client/components/back-button.tsx +37 -12
  5. package/client/components/brand-loading-mark.tsx +5 -2
  6. package/client/components/brand-mark-geometry.ts +3 -3
  7. package/client/components/breadcrumbs.tsx +47 -3
  8. package/client/components/data-table/data-table.tsx +90 -19
  9. package/client/components/data-table/pagination.tsx +17 -2
  10. package/client/components/list-feedback.tsx +31 -22
  11. package/client/components/loading.tsx +127 -16
  12. package/client/components/pending-button.tsx +111 -0
  13. package/client/components/role-list.tsx +7 -0
  14. package/client/components/route-child-page.tsx +72 -1
  15. package/client/components/route-overlay.tsx +149 -15
  16. package/client/components/task-progress.tsx +155 -0
  17. package/client/components/use-route-overlay.ts +8 -11
  18. package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +62 -32
  19. package/client/extensions/nocobase-file-component-ui/locales/de-DE.ts +2 -0
  20. package/client/extensions/nocobase-file-component-ui/locales/en-US.ts +1 -0
  21. package/client/extensions/nocobase-file-component-ui/locales/es-ES.ts +1 -0
  22. package/client/extensions/nocobase-file-component-ui/locales/fr-FR.ts +1 -0
  23. package/client/extensions/nocobase-file-component-ui/locales/id-ID.ts +1 -0
  24. package/client/extensions/nocobase-file-component-ui/locales/ja-JP.ts +2 -0
  25. package/client/extensions/nocobase-file-component-ui/locales/pt-BR.ts +1 -0
  26. package/client/extensions/nocobase-file-component-ui/locales/vi-VN.ts +1 -0
  27. package/client/extensions/nocobase-file-component-ui/locales/zh-CN.ts +1 -0
  28. package/client/extensions/nocobase-file-component-ui/locales/zh-TW.ts +1 -0
  29. package/client/extensions/nocobase-workflow-management/README.md +5 -5
  30. package/client/extensions/nocobase-workflow-management/definition-list.tsx +2 -9
  31. package/client/extensions/nocobase-workflow-management/execution-list.tsx +2 -4
  32. package/client/layouts/app-layout.tsx +0 -4
  33. package/client/locales/system/en-US.ts +19 -0
  34. package/client/locales/system/zh-CN.ts +19 -0
  35. package/client/pages/settings/default-access.tsx +1 -1
  36. package/client/pages/settings/roles.tsx +27 -27
  37. package/client/pages/settings/routes.ts +15 -7
  38. package/client/pages/settings/user-detail.tsx +3 -17
  39. package/client/pages/settings/user-form.tsx +9 -15
  40. package/client/pages/settings/users.tsx +11 -27
  41. package/client/routing/client-route.tsx +199 -37
  42. package/client/routing/navigation-destination.ts +42 -0
  43. package/client/routing/route-leave-boundary.tsx +13 -1
  44. package/client/styles.css +18 -1
  45. package/package.json +8 -8
  46. package/tests/client/auth-i18n.test.tsx +1 -1
  47. package/tests/client/authenticated-client-fixture.test.tsx +37 -0
  48. package/tests/client/back-button.test.tsx +34 -0
  49. package/tests/client/client-route-recovery.test.tsx +93 -1
  50. package/tests/client/list-feedback.test.tsx +5 -5
  51. package/tests/client/list-loading.test.tsx +397 -0
  52. package/tests/client/loading.test.tsx +20 -11
  53. package/tests/client/manual-list-retry.test.tsx +2 -2
  54. package/tests/client/navigation-parent.test.tsx +172 -0
  55. package/tests/client/pending-button.test.tsx +106 -0
  56. package/tests/client/permission-management-tabs.test.tsx +48 -12
  57. package/tests/client/primitive-labels.test.tsx +2 -2
  58. package/tests/client/role-return.test.tsx +21 -3
  59. package/tests/client/roles.test.tsx +84 -3
  60. package/tests/client/route-leave-recheck.test.tsx +74 -0
  61. package/tests/client/route-presentation.test.tsx +557 -0
  62. package/tests/client/session-preparation.test.tsx +20 -17
  63. package/tests/client/settings-continuity.test.tsx +35 -11
  64. package/tests/client/settings-home.test.tsx +4 -2
  65. package/tests/client/settings-idle-list.test.tsx +124 -0
  66. package/tests/client/user-drawer.test.tsx +3 -4
  67. package/tests/fixtures/dialog-footer.html +12 -0
  68. package/tests/fixtures/dialog-footer.tsx +215 -0
  69. package/tests/helpers/authenticated-client.tsx +70 -22
  70. package/tests/playwright/dialog-footer.test.ts +227 -0
@@ -0,0 +1,227 @@
1
+ /// <reference lib="dom" />
2
+ import { expect, test, type Locator, type Page } from '@playwright/test';
3
+ import path from 'node:path';
4
+
5
+ const applicationUrl = process.env.APP_URL;
6
+ test.skip(!applicationUrl, 'Set APP_URL to the running application entry.');
7
+ const fixture = `${applicationUrl?.replace(/\/$/, '')}/tests/fixtures/dialog-footer.html`;
8
+
9
+ async function geometry(page: Page) {
10
+ return page.getByTestId('cancel').evaluate((cancel) => {
11
+ const submit = document.querySelector('[data-testid=submit]')!;
12
+ const footer = cancel.parentElement!;
13
+ const panel = cancel.closest('[role=dialog], [role=alertdialog]')!;
14
+ const measure = (element: Element) => {
15
+ const rect = element.getBoundingClientRect();
16
+ const css = getComputedStyle(element);
17
+ return {
18
+ x: rect.x,
19
+ y: rect.y,
20
+ right: rect.right,
21
+ bottom: rect.bottom,
22
+ height: rect.height,
23
+ width: rect.width,
24
+ paddingLeft: css.paddingLeft,
25
+ paddingRight: css.paddingRight,
26
+ paddingTop: css.paddingTop,
27
+ paddingBottom: css.paddingBottom,
28
+ minHeight: parseFloat(css.minHeight),
29
+ overflow: element.scrollWidth > element.clientWidth + 1,
30
+ };
31
+ };
32
+ return {
33
+ cancel: measure(cancel),
34
+ submit: measure(submit),
35
+ footer: measure(footer),
36
+ panel: measure(panel),
37
+ horizontal:
38
+ getComputedStyle(footer).flexDirection === 'row' ||
39
+ getComputedStyle(footer).display === 'grid',
40
+ };
41
+ });
42
+ }
43
+
44
+ async function checkGeometry(page: Page) {
45
+ await expect(page.getByTestId('cancel')).toBeVisible();
46
+ let previous = '';
47
+ await expect
48
+ .poll(async () => {
49
+ const current = JSON.stringify(await geometry(page));
50
+ const stable = current === previous;
51
+ previous = current;
52
+ return stable;
53
+ })
54
+ .toBe(true);
55
+ const result = await geometry(page);
56
+ const { cancel, submit, footer, panel, horizontal } = result;
57
+ expect(cancel.height).toBeGreaterThanOrEqual(cancel.minHeight - 1);
58
+ expect(submit.height).toBeGreaterThanOrEqual(submit.minHeight - 1);
59
+ expect(cancel.paddingLeft).toBe(submit.paddingLeft);
60
+ expect(cancel.paddingRight).toBe(submit.paddingRight);
61
+ expect(cancel.paddingTop).toBe(submit.paddingTop);
62
+ expect(cancel.paddingBottom).toBe(submit.paddingBottom);
63
+ if (horizontal) {
64
+ expect(Math.abs(cancel.y - submit.y)).toBeLessThan(1);
65
+ expect(Math.abs(cancel.bottom - submit.bottom)).toBeLessThan(1);
66
+ expect(cancel.right).toBeLessThanOrEqual(submit.x + 1);
67
+ } else {
68
+ expect(submit.bottom).toBeLessThanOrEqual(cancel.y + 1);
69
+ }
70
+ for (const button of [cancel, submit]) {
71
+ expect(button.overflow).toBe(false);
72
+ expect(button.x).toBeGreaterThanOrEqual(panel.x);
73
+ expect(button.right).toBeLessThanOrEqual(panel.right + 1);
74
+ expect(button.y).toBeGreaterThanOrEqual(footer.y);
75
+ expect(button.bottom).toBeLessThanOrEqual(panel.bottom + 1);
76
+ expect(button.bottom).toBeLessThanOrEqual(page.viewportSize()!.height);
77
+ }
78
+ expect(panel.x).toBeGreaterThanOrEqual(0);
79
+ expect(panel.right).toBeLessThanOrEqual(page.viewportSize()!.width);
80
+ expect(panel.overflow).toBe(false);
81
+ return result;
82
+ }
83
+
84
+ async function screenshot(page: Page, name: string) {
85
+ if (process.env.DIALOG_EVIDENCE_DIR) {
86
+ await page.screenshot({
87
+ path: path.join(process.env.DIALOG_EVIDENCE_DIR, `${name}.png`),
88
+ });
89
+ }
90
+ }
91
+
92
+ for (const kind of [
93
+ 'dialog',
94
+ 'dialog-close',
95
+ 'alert',
96
+ 'alert-small',
97
+ 'route',
98
+ 'drawer',
99
+ ]) {
100
+ for (const preset of ['compact', 'default']) {
101
+ for (const width of [1440, 375]) {
102
+ test(`${kind}, ${preset}, ${width}: semantic actions align and keep their padding`, async ({
103
+ page,
104
+ }) => {
105
+ await page.setViewportSize({ width, height: 900 });
106
+ await page.goto(`${fixture}?kind=${kind}&preset=${preset}`);
107
+ const result = await checkGeometry(page);
108
+ expect(result.cancel.height).toBeCloseTo(result.submit.height, 0);
109
+ expect(result.cancel.minHeight).toBe(preset === 'compact' ? 36 : 40);
110
+ expect(result.cancel.paddingLeft).toBe(
111
+ preset === 'compact' ? '14.4px' : '16px',
112
+ );
113
+ if (width > 640 && kind !== 'alert-small')
114
+ expect(result.cancel.width).toBeLessThan(result.submit.width);
115
+ if (preset === 'default') await screenshot(page, `${kind}-${width}-en`);
116
+ });
117
+ }
118
+ }
119
+
120
+ for (const language of ['english', 'chinese']) {
121
+ test(`${kind}: long ${language} labels remain visible on a short phone viewport`, async ({
122
+ page,
123
+ }) => {
124
+ await page.setViewportSize({ width: 375, height: 667 });
125
+ await page.goto(
126
+ `${fixture}?kind=${kind}&long&${language === 'chinese' ? 'chinese&' : ''}dark`,
127
+ );
128
+ await checkGeometry(page);
129
+ await screenshot(page, `${kind}-375-long-${language}`);
130
+ });
131
+ }
132
+
133
+ test(`${kind}: disabled and submitting actions retain geometry`, async ({
134
+ page,
135
+ }) => {
136
+ await page.setViewportSize({ width: 375, height: 900 });
137
+ await page.goto(`${fixture}?kind=${kind}&pending`);
138
+ await checkGeometry(page);
139
+ await expect(page.getByTestId('cancel')).toBeDisabled();
140
+ await expect(page.getByTestId('submit')).toBeDisabled();
141
+ await expect(page.getByTestId('submit')).toHaveAttribute(
142
+ 'aria-busy',
143
+ 'true',
144
+ );
145
+ await page.keyboard.press('Escape');
146
+ await expect(page.getByTestId('submit')).toBeVisible();
147
+ });
148
+ }
149
+
150
+ for (const kind of ['dialog', 'alert']) {
151
+ test(`${kind}: a phone row wraps long labels without overflowing`, async ({
152
+ page,
153
+ }) => {
154
+ await page.setViewportSize({ width: 375, height: 667 });
155
+ await page.goto(`${fixture}?kind=${kind}&horizontal&chinese`);
156
+ await checkGeometry(page);
157
+ });
158
+ }
159
+
160
+ async function expectKeyboardFocus(action: Locator) {
161
+ await action.focus();
162
+ await expect(action).toBeFocused();
163
+ expect(
164
+ await action.evaluate(
165
+ (element) =>
166
+ getComputedStyle(element).outlineStyle !== 'none' ||
167
+ getComputedStyle(element).boxShadow !== 'none',
168
+ ),
169
+ ).toBe(true);
170
+ }
171
+
172
+ for (const kind of ['dialog', 'route', 'drawer']) {
173
+ test(`${kind}: link cancel and native form submit remain independent`, async ({
174
+ page,
175
+ }) => {
176
+ await page.goto(`${fixture}?kind=${kind}`);
177
+ await checkGeometry(page);
178
+ const cancel = page.getByTestId('cancel');
179
+ await expect(cancel).toHaveAttribute('href', '/instances?filter=recent');
180
+ expect(await cancel.evaluate((element) => element.tagName)).toBe('A');
181
+ await expectKeyboardFocus(cancel);
182
+ await page.keyboard.press('Enter');
183
+ await expect(page.getByTestId('location')).toHaveText(
184
+ '/instances?filter=recent',
185
+ );
186
+ await expect(page.getByTestId('submit')).toHaveCount(0);
187
+ await page.goto(`${fixture}?kind=${kind}`);
188
+ await checkGeometry(page);
189
+ await page.getByLabel('Instance name').fill('New customer portal');
190
+ await expectKeyboardFocus(page.getByTestId('submit'));
191
+ await page.keyboard.press('Enter');
192
+ await expect(page.getByTestId('location')).toHaveText(
193
+ '/instances?created=1',
194
+ );
195
+ });
196
+ }
197
+
198
+ test('route footer stays visible when the body scrolls', async ({ page }) => {
199
+ await page.setViewportSize({ width: 375, height: 667 });
200
+ await page.goto(`${fixture}?kind=route&long`);
201
+ const before = await checkGeometry(page);
202
+ await page.locator('[data-slot=route-overlay-body]').evaluate((element) => {
203
+ element.scrollTop = element.scrollHeight;
204
+ });
205
+ await expect
206
+ .poll(() =>
207
+ page
208
+ .locator('[data-slot=route-overlay-body]')
209
+ .evaluate((element) => element.scrollTop),
210
+ )
211
+ .toBeGreaterThan(0);
212
+ const after = await checkGeometry(page);
213
+ expect(after.footer.y).toBe(before.footer.y);
214
+ await page.getByTestId('cancel').click();
215
+ await expect(page.getByTestId('location')).toHaveText(
216
+ '/instances?filter=recent',
217
+ );
218
+ });
219
+
220
+ // DialogClose replaces Button's semantic slot just as AlertDialog's actions do.
221
+ test('DialogClose cancels without submitting the form', async ({ page }) => {
222
+ await page.goto(`${fixture}?kind=dialog-close`);
223
+ await checkGeometry(page);
224
+ await page.getByTestId('cancel').click();
225
+ await expect(page.getByTestId('submit')).toHaveCount(0);
226
+ await expect(page.getByTestId('location')).toHaveText('Instance list');
227
+ });