@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.
- package/.agents/rules/build-and-deploy.md +4 -0
- package/.agents/rules/components.md +3 -1
- package/README.MD +2 -2
- package/client/components/data-table/data-table.tsx +2 -0
- package/client/components/data-table/pagination.tsx +6 -6
- package/client/components/flow-diagram.tsx +645 -83
- package/client/components/flow-layout.ts +117 -7
- package/client/components/lifecycle-diagram.tsx +175 -37
- package/client/components/record-link.tsx +20 -0
- package/client/components/required-role-setup.tsx +93 -0
- package/client/components/role-list.tsx +21 -5
- package/client/components/setup-readiness-state.ts +135 -0
- package/client/components/setup-readiness.tsx +91 -0
- package/client/components/ui/combobox.tsx +300 -0
- package/client/extensions/nocobase-workflow-management/README.md +3 -1
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +19 -13
- package/client/extensions/nocobase-workflow-management/layout.tsx +1 -1
- package/client/hooks/use-table-url-state.ts +93 -0
- package/client/lib/required-role-setup.ts +98 -0
- package/client/lib/return-to.ts +19 -0
- package/client/locales/system/en-US.ts +10 -0
- package/client/locales/system/settings-en-US.ts +37 -1
- package/client/locales/system/settings-zh-CN.ts +33 -2
- package/client/locales/system/zh-CN.ts +10 -0
- package/client/pages/settings/default-access-form.tsx +4 -1
- package/client/pages/settings/default-access.tsx +40 -8
- package/client/pages/settings/role-form.tsx +1 -1
- package/client/pages/settings/roles.tsx +44 -6
- package/client/pages/settings/shared.tsx +15 -13
- package/client/pages/settings/user-detail.tsx +15 -259
- package/client/pages/settings/user-form.tsx +299 -147
- package/client/pages/settings/users.tsx +211 -123
- package/config.example.yml +6 -5
- package/package.json +11 -11
- package/tests/components/default-access.test.tsx +141 -2
- package/tests/components/settings-continuity.test.tsx +426 -0
- package/tests/components/settings-feedback.test.tsx +304 -0
- package/tests/components/settings.test.tsx +1 -1
- package/tests/components/setup-readiness.test.tsx +370 -0
- package/tests/components/table-url-state.test.tsx +95 -0
- package/tests/components/user-drawer.test.tsx +526 -0
- package/tests/logic/return-to.test.ts +19 -0
- package/tsconfig.json +1 -0
|
@@ -1,268 +1,24 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
1
2
|
import {
|
|
2
|
-
|
|
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
|
-
|
|
7
|
+
useResolvedPath,
|
|
22
8
|
} from 'react-router';
|
|
23
|
-
import {
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
178
|
-
const
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
}
|