@nocobase/app-template-default 1.0.0-beta.75 → 1.0.0-beta.77

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 (61) hide show
  1. package/.agents/rules/build-and-deploy.md +4 -0
  2. package/.agents/rules/components.md +3 -1
  3. package/README.MD +2 -2
  4. package/client/components/data-table/data-table.tsx +2 -0
  5. package/client/components/data-table/pagination.tsx +6 -6
  6. package/client/components/default-access-editor.tsx +79 -52
  7. package/client/components/flow-diagram.tsx +645 -83
  8. package/client/components/flow-layout.ts +117 -7
  9. package/client/components/lifecycle-diagram.tsx +175 -37
  10. package/client/components/long-text.tsx +200 -0
  11. package/client/components/permission-editor.tsx +534 -341
  12. package/client/components/record-link.tsx +20 -0
  13. package/client/components/required-role-setup.tsx +93 -0
  14. package/client/components/role-list.tsx +21 -5
  15. package/client/components/setup-readiness-state.ts +135 -0
  16. package/client/components/setup-readiness.tsx +91 -0
  17. package/client/components/ui/combobox.tsx +300 -0
  18. package/client/extensions/nocobase-workflow-management/README.md +3 -1
  19. package/client/extensions/nocobase-workflow-management/definition-list.tsx +19 -13
  20. package/client/extensions/nocobase-workflow-management/layout.tsx +1 -1
  21. package/client/hooks/use-table-url-state.ts +93 -0
  22. package/client/lib/required-role-setup.ts +98 -0
  23. package/client/lib/return-to.ts +19 -0
  24. package/client/locales/system/en-US.ts +10 -0
  25. package/client/locales/system/settings-en-US.ts +51 -4
  26. package/client/locales/system/settings-zh-CN.ts +46 -5
  27. package/client/locales/system/zh-CN.ts +10 -0
  28. package/client/pages/settings/default-access-form.tsx +14 -2
  29. package/client/pages/settings/default-access-model.ts +6 -2
  30. package/client/pages/settings/default-access.tsx +40 -8
  31. package/client/pages/settings/page-permissions.ts +166 -1
  32. package/client/pages/settings/permission-filters.tsx +9 -2
  33. package/client/pages/settings/permission-workspace.ts +330 -0
  34. package/client/pages/settings/permissions-model.ts +62 -53
  35. package/client/pages/settings/role-form.tsx +31 -95
  36. package/client/pages/settings/roles.tsx +44 -6
  37. package/client/pages/settings/shared.tsx +15 -13
  38. package/client/pages/settings/user-detail.tsx +15 -259
  39. package/client/pages/settings/user-form.tsx +299 -147
  40. package/client/pages/settings/users.tsx +211 -123
  41. package/config.example.yml +6 -5
  42. package/package.json +17 -17
  43. package/server/config/app.ts +1 -1
  44. package/server/config/session.ts +7 -5
  45. package/tests/components/default-access-editor.test.tsx +130 -0
  46. package/tests/components/default-access.test.tsx +141 -2
  47. package/tests/components/permission-editor.test.tsx +613 -572
  48. package/tests/components/permission-filter-binding.test.tsx +20 -0
  49. package/tests/components/settings-continuity.test.tsx +426 -0
  50. package/tests/components/settings-feedback.test.tsx +304 -0
  51. package/tests/components/settings.test.tsx +34 -9
  52. package/tests/components/setup-readiness.test.tsx +370 -0
  53. package/tests/components/table-url-state.test.tsx +95 -0
  54. package/tests/components/user-drawer.test.tsx +526 -0
  55. package/tests/components/user-permissions.test.tsx +47 -13
  56. package/tests/logic/config.test.ts +3 -1
  57. package/tests/logic/page-permission-navigation.test.ts +394 -0
  58. package/tests/logic/permission-workspace.test.ts +570 -0
  59. package/tests/logic/return-to.test.ts +19 -0
  60. package/tests/logic/session-isolation.test.ts +250 -0
  61. package/tsconfig.json +1 -0
@@ -2,6 +2,10 @@
2
2
 
3
3
  Read this before changing the build, packing a deployment archive, building or editing the Docker image, setting `CDN_BASE_URL`, or building for another platform. The deployment process itself is in the [deployment Skill](../skills/nocobase-deployment/SKILL.md), and dependency placement and the build failure table are in [where to declare a dependency](../skills/nocobase-app-development/references/dependencies.md).
4
4
 
5
+ ## Client typechecking
6
+
7
+ Keep `types: ["node"]` in the client `tsconfig.json`. Shared contracts from database and lifecycle packages resolve to their TypeScript sources in a workspace and require Node.js declarations during typechecking. Declare that requirement explicitly instead of relying on unrelated plugin imports to load the declarations; type-only imports still disappear from the browser bundle.
8
+
5
9
  ## CDN assets at build time
6
10
 
7
11
  `CDN_BASE_URL` sets Vite's `base` only during a production build: `CDN_BASE_URL=https://cdn.example.com/my-app/v1/ pnpm build`. Use a full HTTPS URL ending in `/`; unset, empty or whitespace-only values keep the default `./`. `pnpm build` reads it from the process environment, then `.env.local`, then `.env`; development ignores it and continues using `APP_BASE_PATH`. It is a build parameter, absent from `config variables` and `dist/variables.json`, with no `config.yml` setting. Changing it requires rebuilding.
@@ -35,4 +35,6 @@ The default brand palette and density live in `client/theme/themes/*.css`; share
35
35
 
36
36
  ## Standard process management
37
37
 
38
- The template includes `FlowDiagram`, `LifecycleDiagram` and `client/extensions/nocobase-workflow-management/` source. Reuse them when business workflows are introduced; preinstallation does not register a settings route or sample process. `LifecycleDiagram.configuration` automatically offers configuration for authorized native parameter bindings, using the application’s real parameter editor and API. Read [Lifecycle UI](../skills/nocobase-app-development/references/lifecycle-ui.md) before wiring the workflow directory, definition and execution pages. Preserve the standard visuals unless the user explicitly asks to customize them. UI Library names are shadcn registry items, not npm package names; an npm 404 is not evidence that a component is unavailable.
38
+ The template includes `FlowDiagram`, `LifecycleDiagram` and `client/extensions/nocobase-workflow-management/` source. Reuse them when business workflows are introduced; preinstallation does not register a settings route or sample process. `LifecycleDiagram.configuration` automatically offers configuration for authorized native parameter bindings, using the application’s real parameter editor and API. Read [Lifecycle UI](../skills/nocobase-app-development/references/lifecycle-ui.md) before wiring the workflow directory, definition and execution pages. The existing `LifecycleDiagram` supports `presentation="process"` for compact approval/work cards and start/outcome pills, with `stateKind` supplied by the application; omitting it keeps the classic presentation. Preserve configuration and recorded-history behavior when selecting a presentation. UI Library names are shadcn registry items, not npm package names; an npm 404 is not evidence that a component is unavailable.
39
+
40
+ Use `RecordLink` from `#components/record-link` for clickable record names and identifiers in lists and tables. It uses foreground text with medium weight, an underline on hover, and an underline plus focus outline for keyboard navigation. Preserve the native link semantics and route query context. Do not give record fields a permanent underline, primary-blue text, or a button-shaped surface. Keep action buttons and menu items on their own components; do not globally restyle every link in the application.
package/README.MD CHANGED
@@ -403,9 +403,9 @@ Tests live in `tests/`, never beside the source they cover. Put logic and integr
403
403
 
404
404
  Application tests use the `/server`, `/client` and `/cli` entries of `@nocobase/app-testing`. Start page tests from `tests/components/page-harness.test.tsx`: it uses `renderWithApp()` with real application hooks, strict translations, permission checks and a test toaster. Answer requests through `answerApi()` and replace external services through `services`, without also registering their owning plugins. The template's Vitest configuration includes client plugins in custom package scopes so their contexts and service tokens remain shared. See `.agents/skills/nocobase-app-development/references/testing.md` for fixture ownership and complete server, migration and CLI examples.
405
405
 
406
- ### Notification testing
406
+ ### Notification verification
407
407
 
408
- With the `notification:test` `send` permission, send a real message through the durable Notification Manager with `POST <APP_BASE_PATH>/api/notifications/testSends`. `GET <APP_BASE_PATH>/api/notifications/testTargets` lists each configured channel and the fields its test takes: Email tests require an address and IM tests use the configured logical target. The server enforces the permission when the message is submitted.
408
+ Exercise the application's business notification flow, or use a server-side command resolving `notificationServiceToken` and calling `send()` with complete messages and explicit recipients. Inspect the returned notification ID through the protected `/api/notifications/logs` API or the logs page installed by the application. Log access requires `page:notification.logs` `access`.
409
409
 
410
410
  ## API documentation
411
411
 
@@ -107,6 +107,8 @@ export function DataTable<TData, TValue = unknown>({
107
107
  data: tableData,
108
108
  columns,
109
109
  manualPagination,
110
+ // Controlled hosts own URL page resets and correction after writes.
111
+ autoResetPageIndex: paginationState === undefined,
110
112
  rowCount,
111
113
  manualSorting,
112
114
  ...(onPaginationChange ? { onPaginationChange } : {}),
@@ -69,9 +69,9 @@ export function DataTablePagination<TData>({
69
69
  })
70
70
  : null}
71
71
  </div>
72
- <div className='flex flex-wrap items-center gap-6 lg:gap-8'>
73
- <div className='flex items-center gap-2'>
74
- <p className='text-sm font-medium'>
72
+ <div className='flex min-w-0 flex-wrap items-center justify-end gap-x-6 gap-y-3 lg:gap-x-8'>
73
+ <div className='flex shrink-0 items-center gap-2'>
74
+ <p className='text-sm font-medium whitespace-nowrap'>
75
75
  {t('dataTable.rowsPerPage', { defaultValue: 'Rows per page' })}
76
76
  </p>
77
77
  <Select
@@ -83,7 +83,7 @@ export function DataTablePagination<TData>({
83
83
  >
84
84
  <SelectTrigger
85
85
  size='sm'
86
- className='w-[70px]'
86
+ className='w-[70px] shrink-0'
87
87
  aria-label={t('dataTable.rowsPerPage', {
88
88
  defaultValue: 'Rows per page',
89
89
  })}
@@ -102,14 +102,14 @@ export function DataTablePagination<TData>({
102
102
  </SelectContent>
103
103
  </Select>
104
104
  </div>
105
- <div className='flex w-[100px] items-center justify-center text-sm font-medium'>
105
+ <div className='shrink-0 text-sm font-medium whitespace-nowrap tabular-nums'>
106
106
  {t('dataTable.pageOf', {
107
107
  defaultValue: 'Page {{page}} of {{pageCount}}',
108
108
  page: pageIndex + 1,
109
109
  pageCount,
110
110
  })}
111
111
  </div>
112
- <div className='flex items-center gap-2'>
112
+ <div className='flex shrink-0 items-center gap-2'>
113
113
  <Button
114
114
  variant='outline'
115
115
  size='icon-sm'
@@ -17,6 +17,8 @@ export interface DefaultAccessChoice {
17
17
  export interface DefaultAccessScopeControl {
18
18
  readonly id: string;
19
19
  readonly action: string;
20
+ /** Stable operation identity; titles alone may be shared by different actions. */
21
+ readonly actionId?: string;
20
22
  readonly scope: string;
21
23
  readonly value: string;
22
24
  readonly options: readonly DefaultAccessChoice[];
@@ -37,6 +39,8 @@ export interface DefaultAccessEditorProps {
37
39
  readonly resource: string;
38
40
  readonly chooseResource: string;
39
41
  readonly action: string;
42
+ /** Stable operation identity; titles alone may be shared by different actions. */
43
+ readonly actionId?: string;
40
44
  readonly scope: string;
41
45
  readonly noScopes: string;
42
46
  };
@@ -61,6 +65,16 @@ export function DefaultAccessEditor({
61
65
  onScopeChange,
62
66
  }: DefaultAccessEditorProps): ReactElement {
63
67
  const id = useId();
68
+ const groups = new Map<
69
+ string,
70
+ { action: string; scopes: DefaultAccessScopeControl[] }
71
+ >();
72
+ for (const scope of scopes) {
73
+ const key = scope.actionId ?? scope.action;
74
+ const group = groups.get(key);
75
+ if (group) group.scopes.push(scope);
76
+ else groups.set(key, { action: scope.action, scopes: [scope] });
77
+ }
64
78
  return (
65
79
  <div className='flex min-w-0 flex-col gap-6'>
66
80
  <div className='grid gap-4 sm:grid-cols-2'>
@@ -119,63 +133,76 @@ export function DefaultAccessEditor({
119
133
  )}
120
134
  </div>
121
135
  </div>
122
- <div className='overflow-hidden rounded-lg border'>
136
+ <div className='grid min-w-0 gap-3'>
123
137
  {scopes.length ? (
124
- scopes.map((scope) => (
125
- <div
126
- key={scope.id}
127
- className='grid min-w-0 gap-3 border-b p-4 last:border-b-0 sm:grid-cols-3'
138
+ [...groups].map(([key, group]) => (
139
+ <section
140
+ key={key}
141
+ aria-labelledby={`${id}-action-${encodeURIComponent(key)}`}
142
+ className='grid min-w-0 gap-3 rounded-lg border bg-card p-3 sm:p-4'
128
143
  >
129
- <div className='min-w-0 break-words text-sm font-medium'>
130
- {scope.action}
131
- </div>
132
- <div className='flex min-w-0 flex-col gap-2 sm:col-span-2'>
133
- <Label htmlFor={`${id}-${scope.id}`}>
134
- {scope.scope || labels.scope}
135
- </Label>
136
- <Select
137
- value={scope.value}
138
- items={scope.options}
139
- disabled={readOnly}
140
- onValueChange={(value) => {
141
- if (value !== null) onScopeChange(scope.id, value);
142
- }}
143
- >
144
- <SelectTrigger
145
- id={`${id}-${scope.id}`}
146
- className='w-full'
147
- aria-label={`${scope.action} · ${scope.scope || labels.scope}`}
148
- aria-invalid={Boolean(scope.error)}
149
- aria-describedby={
150
- scope.error ? `${id}-${scope.id}-error` : undefined
151
- }
152
- >
153
- <SelectValue />
154
- </SelectTrigger>
155
- <SelectContent className='w-auto max-w-[min(var(--container-sm),var(--available-width))] min-w-(--anchor-width) [&_[data-slot=select-item]>:first-child]:whitespace-normal'>
156
- <SelectGroup>
157
- {scope.options.map((option) => (
158
- <SelectItem key={option.value} value={option.value}>
159
- {option.label}
160
- </SelectItem>
161
- ))}
162
- </SelectGroup>
163
- </SelectContent>
164
- </Select>
165
- {scope.error && (
166
- <p
167
- id={`${id}-${scope.id}-error`}
168
- className='text-sm text-destructive'
169
- >
170
- {scope.error}
171
- </p>
172
- )}
173
- {scope.configuration}
144
+ <h3
145
+ id={`${id}-action-${encodeURIComponent(key)}`}
146
+ className='text-sm font-medium break-words'
147
+ >
148
+ {group.action}
149
+ </h3>
150
+ <div className='grid min-w-0 grid-cols-[repeat(auto-fit,minmax(min(100%,16rem),1fr))] items-start gap-3'>
151
+ {group.scopes.map((scope) => (
152
+ <div key={scope.id} className='grid min-w-0 gap-2'>
153
+ <Label
154
+ htmlFor={`${id}-${scope.id}`}
155
+ className='text-xs text-muted-foreground'
156
+ >
157
+ {scope.scope || labels.scope}
158
+ </Label>
159
+ <Select
160
+ value={scope.value}
161
+ items={scope.options}
162
+ disabled={readOnly}
163
+ onValueChange={(value) => {
164
+ if (value !== null) onScopeChange(scope.id, value);
165
+ }}
166
+ >
167
+ <SelectTrigger
168
+ id={`${id}-${scope.id}`}
169
+ className='w-full'
170
+ aria-label={`${scope.action} · ${scope.scope || labels.scope}`}
171
+ aria-invalid={Boolean(scope.error)}
172
+ aria-describedby={
173
+ scope.error ? `${id}-${scope.id}-error` : undefined
174
+ }
175
+ >
176
+ <SelectValue />
177
+ </SelectTrigger>
178
+ <SelectContent className='w-auto max-w-[min(var(--container-sm),var(--available-width))] min-w-(--anchor-width) [&_[data-slot=select-item]>:first-child]:whitespace-normal'>
179
+ <SelectGroup>
180
+ {scope.options.map((option) => (
181
+ <SelectItem key={option.value} value={option.value}>
182
+ {option.label}
183
+ </SelectItem>
184
+ ))}
185
+ </SelectGroup>
186
+ </SelectContent>
187
+ </Select>
188
+ {scope.error && (
189
+ <p
190
+ id={`${id}-${scope.id}-error`}
191
+ className='text-sm text-destructive'
192
+ >
193
+ {scope.error}
194
+ </p>
195
+ )}
196
+ {scope.configuration}
197
+ </div>
198
+ ))}
174
199
  </div>
175
- </div>
200
+ </section>
176
201
  ))
177
202
  ) : (
178
- <p className='p-4 text-sm text-muted-foreground'>{labels.noScopes}</p>
203
+ <p className='rounded-lg border p-4 text-sm text-muted-foreground'>
204
+ {labels.noScopes}
205
+ </p>
179
206
  )}
180
207
  </div>
181
208
  </div>