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

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 (43) 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/flow-diagram.tsx +645 -83
  7. package/client/components/flow-layout.ts +117 -7
  8. package/client/components/lifecycle-diagram.tsx +175 -37
  9. package/client/components/record-link.tsx +20 -0
  10. package/client/components/required-role-setup.tsx +93 -0
  11. package/client/components/role-list.tsx +21 -5
  12. package/client/components/setup-readiness-state.ts +135 -0
  13. package/client/components/setup-readiness.tsx +91 -0
  14. package/client/components/ui/combobox.tsx +300 -0
  15. package/client/extensions/nocobase-workflow-management/README.md +3 -1
  16. package/client/extensions/nocobase-workflow-management/definition-list.tsx +19 -13
  17. package/client/extensions/nocobase-workflow-management/layout.tsx +1 -1
  18. package/client/hooks/use-table-url-state.ts +93 -0
  19. package/client/lib/required-role-setup.ts +98 -0
  20. package/client/lib/return-to.ts +19 -0
  21. package/client/locales/system/en-US.ts +10 -0
  22. package/client/locales/system/settings-en-US.ts +37 -1
  23. package/client/locales/system/settings-zh-CN.ts +33 -2
  24. package/client/locales/system/zh-CN.ts +10 -0
  25. package/client/pages/settings/default-access-form.tsx +4 -1
  26. package/client/pages/settings/default-access.tsx +40 -8
  27. package/client/pages/settings/role-form.tsx +1 -1
  28. package/client/pages/settings/roles.tsx +44 -6
  29. package/client/pages/settings/shared.tsx +15 -13
  30. package/client/pages/settings/user-detail.tsx +15 -259
  31. package/client/pages/settings/user-form.tsx +299 -147
  32. package/client/pages/settings/users.tsx +211 -123
  33. package/config.example.yml +6 -5
  34. package/package.json +11 -11
  35. package/tests/components/default-access.test.tsx +141 -2
  36. package/tests/components/settings-continuity.test.tsx +426 -0
  37. package/tests/components/settings-feedback.test.tsx +304 -0
  38. package/tests/components/settings.test.tsx +1 -1
  39. package/tests/components/setup-readiness.test.tsx +370 -0
  40. package/tests/components/table-url-state.test.tsx +95 -0
  41. package/tests/components/user-drawer.test.tsx +526 -0
  42. package/tests/logic/return-to.test.ts +19 -0
  43. package/tsconfig.json +1 -0
@@ -1,268 +1,24 @@
1
+ import type { ReactElement } from 'react';
1
2
  import {
2
- useSettingsData,
3
- useUserPermission,
4
- useSettingsLeave,
5
- } from './hooks.js';
6
- import { ApiClientError, useApiClient, useToaster } from '@nocobase/app-client';
7
- import {
8
- UsersClient,
9
- type ManagedUser,
10
- type UserRoleScopeOption,
11
- type UserRoleValue,
12
- type UsersOptions,
13
- } from '@nocobase/app-plugin-users/client/user-client';
14
- import { useTranslation } from '@nocobase/i18n/client';
15
- import { useEffect, useMemo, useRef, useState, type ReactElement } from 'react';
16
- import {
17
- Link,
18
- Outlet,
3
+ Navigate,
19
4
  useLocation,
5
+ useOutlet,
20
6
  useOutletContext,
21
- useParams,
7
+ useResolvedPath,
22
8
  } from 'react-router';
23
- import { RouteDrawer } from '#components/route-drawer';
24
- import { Button } from '#components/ui/button';
25
- import { Badge } from '#components/ui/badge';
26
- import {
27
- Card,
28
- CardHeader,
29
- CardTitle,
30
- CardDescription,
31
- CardContent,
32
- CardFooter,
33
- } from '#components/ui/card';
34
- import { RoleSelection } from './user-form.js';
35
- import { ConfirmAction, SettingsError, SettingsLoading } from './shared.js';
9
+ import type { ManagedUser } from '@nocobase/app-plugin-users/client/user-client';
36
10
 
37
- function ScopeCard({
38
- scope,
39
- user,
40
- reload,
41
- onDirtyChange,
42
- }: {
43
- readonly scope: UserRoleScopeOption;
44
- readonly user: ManagedUser;
45
- readonly reload: (updated?: ManagedUser) => void;
46
- readonly onDirtyChange: (dirty: boolean, busy: boolean) => void;
47
- }): ReactElement {
48
- const { t } = useTranslation();
49
- const api = useApiClient();
50
- const toaster = useToaster();
51
- const client = useMemo(() => new UsersClient(api), [api]);
52
- const allowed = useUserPermission('assign-role', user.id);
53
- const current =
54
- user.roleScopes[scope.key] ?? (scope.selection === 'single' ? '' : []);
55
- const key = JSON.stringify(current);
56
- const [draftValue, setDraftValue] = useState<UserRoleValue>();
57
- const value = draftValue ?? current;
58
- const [busy, setBusy] = useState(false);
59
- const [error, setError] = useState<unknown>();
60
- const [confirm, setConfirm] = useState(false);
61
- const changed = JSON.stringify(value) !== key;
62
- const removed = (typeof current === 'string' ? [current] : current).filter(
63
- (item) => !(typeof value === 'string' ? [value] : value).includes(item),
64
- );
65
- const save = async () => {
66
- setBusy(true);
67
- onDirtyChange(changed, true);
68
- setError(undefined);
69
- let saved = false;
70
- try {
71
- const updated = await client.replaceRoleScope(user.id, scope.key, value);
72
- saved = true;
73
- onDirtyChange(false, false);
74
- reload(updated);
75
- toaster.show({
76
- type: 'success',
77
- title: t('settings.rolesSaved', { name: user.name }),
78
- });
79
- setConfirm(false);
80
- } catch (next) {
81
- setError(next);
82
- } finally {
83
- setBusy(false);
84
- onDirtyChange(!saved && changed, false);
85
- }
86
- };
87
- return (
88
- <Card>
89
- <CardHeader>
90
- <CardTitle>
91
- {scope.labelI18nKey
92
- ? t(scope.labelI18nKey, {
93
- ns: scope.labelI18nNs,
94
- defaultValue: scope.label,
95
- })
96
- : scope.label}
97
- </CardTitle>
98
- <CardDescription>{t('settings.rolesDescription')}</CardDescription>
99
- </CardHeader>
100
- <CardContent>
101
- <RoleSelection
102
- scope={scope}
103
- value={value}
104
- onChange={(next) => {
105
- setDraftValue(next);
106
- onDirtyChange(JSON.stringify(next) !== key, false);
107
- }}
108
- disabled={!allowed || busy}
109
- />
110
- <SettingsError error={error} />
111
- </CardContent>
112
- {allowed && (
113
- <CardFooter className='justify-end'>
114
- <Button
115
- disabled={!changed || busy}
116
- onClick={() => (removed.length ? setConfirm(true) : void save())}
117
- >
118
- {t(busy ? 'settings.saving' : 'settings.save')}
119
- </Button>
120
- </CardFooter>
121
- )}
122
- <ConfirmAction
123
- open={confirm}
124
- confirmLabel={t('settings.revokeRoles')}
125
- title={t('settings.revokeRoleTitle', { name: user.name })}
126
- description={
127
- <>
128
- <p>{t('settings.revokeRoleHint')}</p>
129
- <p>
130
- {scope.labelI18nKey
131
- ? t(scope.labelI18nKey, {
132
- ns: scope.labelI18nNs,
133
- defaultValue: scope.label,
134
- })
135
- : scope.label}
136
- :{' '}
137
- {removed
138
- .map((key) => {
139
- const option = scope.options.find(
140
- (item) => item.value === key,
141
- );
142
- return option?.labelI18nKey
143
- ? t(option.labelI18nKey, {
144
- ns: option.labelI18nNs,
145
- defaultValue: option.label,
146
- })
147
- : (option?.label ?? key);
148
- })
149
- .join(', ')}
150
- </p>
151
- <SettingsError error={error} />
152
- </>
153
- }
154
- busy={busy}
155
- onCancel={() => setConfirm(false)}
156
- onConfirm={() => void save()}
157
- />
158
- </Card>
159
- );
160
- }
161
- function UserDetailView(): ReactElement {
162
- const { userId = '' } = useParams();
163
- const [draftRevision, setDraftRevision] = useState(0);
164
- const [scopeState, setScopeState] = useState<
165
- Record<string, { dirty: boolean; busy: boolean }>
166
- >({});
167
- const leave = useSettingsLeave(
168
- Object.values(scopeState).some((state) => state.dirty),
169
- Object.values(scopeState).some((state) => state.busy),
170
- () => {
171
- setScopeState({});
172
- setDraftRevision((value) => value + 1);
173
- },
174
- );
175
- const { t } = useTranslation();
11
+ /** Keep stored route identifiers and old detail links while using one editor. */
12
+ export default function UserDetail(): ReactElement {
13
+ const parent = useOutletContext<{
14
+ reload: (updated?: ManagedUser) => void;
15
+ }>();
176
16
  const location = useLocation();
177
- const parent = useOutletContext<{ reload: () => void }>();
178
- const request = useSettingsData<{ data: ManagedUser }>(
179
- `users/${encodeURIComponent(userId)}`,
180
- );
181
- const missingRef = useRef<unknown>(undefined);
182
- useEffect(() => {
183
- if (
184
- request.error instanceof ApiClientError &&
185
- request.error.status === 404 &&
186
- missingRef.current !== request.error
187
- ) {
188
- missingRef.current = request.error;
189
- parent.reload();
190
- }
191
- }, [request.error, parent]);
192
- const options = useSettingsData<{ data: UsersOptions }>('users/options');
193
- const canUpdate = useUserPermission('update', userId);
194
- const user = request.data?.data;
195
- const reload = (updated?: ManagedUser) => {
196
- if (updated) request.replaceData({ data: updated });
197
- request.reload();
198
- parent.reload();
199
- };
17
+ const listPath = useResolvedPath('..').pathname;
18
+ const outlet = useOutlet({ ...parent, listPath });
200
19
  return (
201
- <RouteDrawer
202
- beforeClose={leave.guard}
203
- title={user?.name ?? t('settings.users')}
204
- closeTo={{ pathname: '/settings/users', search: location.search }}
205
- footer={
206
- canUpdate && user ? (
207
- <Button
208
- variant='outline'
209
- nativeButton={false}
210
- render={<Link to={{ pathname: 'edit', search: location.search }} />}
211
- >
212
- {t('settings.edit')}
213
- </Button>
214
- ) : undefined
215
- }
216
- >
217
- <SettingsError error={request.error} retry={reload} />
218
- {request.loading && !user ? (
219
- <SettingsLoading />
220
- ) : (
221
- user && (
222
- <div className='flex flex-col gap-6'>
223
- <dl className='grid grid-cols-[8rem_minmax(0,1fr)] gap-3 text-sm'>
224
- {(['name', 'username', 'email'] as const).map((field) => (
225
- <div key={field} className='contents'>
226
- <dt className='text-muted-foreground'>
227
- {t(`settings.${field}`)}
228
- </dt>
229
- <dd className='min-w-0 break-words'>{user[field] || '—'}</dd>
230
- </div>
231
- ))}
232
- <dt className='text-muted-foreground'>{t('settings.status')}</dt>
233
- <dd>
234
- <Badge variant='secondary'>
235
- {t(
236
- user.disabledAt ? 'settings.disabled' : 'settings.enabled',
237
- )}
238
- </Badge>
239
- </dd>
240
- </dl>
241
- <SettingsError error={options.error} retry={options.reload} />
242
- {options.data?.data.roleScopes.map((scope) => (
243
- <ScopeCard
244
- key={`${draftRevision}:${scope.key}:${JSON.stringify(user.roleScopes[scope.key])}`}
245
- scope={scope}
246
- user={user}
247
- reload={reload}
248
- onDirtyChange={(dirty, busy) =>
249
- setScopeState((old) => ({
250
- ...old,
251
- [scope.key]: { dirty, busy },
252
- }))
253
- }
254
- />
255
- ))}
256
- </div>
257
- )
258
- )}
259
- {leave.dialog}
260
- <Outlet context={{ reload }} />
261
- </RouteDrawer>
20
+ outlet ?? (
21
+ <Navigate to={{ pathname: 'edit', search: location.search }} replace />
22
+ )
262
23
  );
263
24
  }
264
-
265
- export default function UserDetail(): ReactElement {
266
- const { userId } = useParams();
267
- return <UserDetailView key={userId} />;
268
- }