@nocobase/app-template-default 1.0.0-beta.83 → 1.0.0-beta.85
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 +1 -1
- package/.agents/rules/shell.md +4 -0
- package/AGENTS.md +8 -2
- package/README.MD +10 -2
- package/client/components/back-button.tsx +3 -5
- package/client/components/data-table/data-table.tsx +86 -32
- package/client/components/default-access-editor.tsx +13 -2
- package/client/components/feedback-form.tsx +474 -0
- package/client/components/lifecycle-diagram.tsx +10 -10
- package/client/components/list-feedback.tsx +174 -0
- package/client/components/permission-editor.tsx +419 -226
- package/client/extensions/nocobase-auth-forms/password-login-form.tsx +17 -6
- package/client/extensions/nocobase-auth-forms/password-registration-form.tsx +24 -7
- package/client/extensions/nocobase-auth-forms/password-reset-form.tsx +19 -7
- package/client/extensions/nocobase-auth-forms/password-reset-request-form.tsx +15 -7
- package/client/extensions/nocobase-device-approval/device-approval.tsx +7 -2
- package/client/extensions/nocobase-device-approval/locales/en-US.ts +16 -0
- package/client/extensions/nocobase-device-approval/locales/zh-CN.ts +7 -0
- package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +20 -17
- package/client/extensions/nocobase-workflow-management/README.md +3 -1
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +19 -7
- package/client/extensions/nocobase-workflow-management/execution-list.tsx +61 -48
- package/client/hooks/use-list-data.ts +76 -0
- package/client/hooks/use-list-location.ts +23 -0
- package/client/hooks/use-list-tabs.ts +42 -0
- package/client/hooks/use-table-url-state.ts +143 -30
- package/client/locales/system/en-US.ts +24 -1
- package/client/locales/system/settings-en-US.ts +8 -10
- package/client/locales/system/settings-zh-CN.ts +8 -8
- package/client/locales/system/zh-CN.ts +22 -1
- package/client/pages/settings/default-access-form.tsx +44 -26
- package/client/pages/settings/default-access.tsx +100 -93
- package/client/pages/settings/hooks.tsx +4 -59
- package/client/pages/settings/page-permissions.ts +17 -0
- package/client/pages/settings/permissions-model.ts +54 -59
- package/client/pages/settings/role-form.tsx +98 -56
- package/client/pages/settings/roles.tsx +69 -95
- package/client/pages/settings/shared.tsx +43 -33
- package/client/pages/settings/user-form.tsx +51 -30
- package/client/pages/settings/users.tsx +105 -91
- package/client/routing/session-preparation.tsx +10 -3
- package/client/runtime.ts +0 -4
- package/config.example.yml +24 -0
- package/database/main/migrations/202611020002_create_mutation_receipts.ts +19 -0
- package/package.json +24 -21
- package/server/app.ts +0 -2
- package/server/config/auth.ts +2 -0
- package/server/config/concurrency.ts +3 -0
- package/server/config/index.ts +3 -0
- package/server/config/jobs.ts +1 -13
- package/server/config/notification.ts +1 -1
- package/server/config/queue.ts +1 -7
- package/server/plugins.ts +2 -0
- package/server/providers/index.ts +4 -1
- package/server/providers/submission.ts +61 -0
- package/tests/client/auth-i18n.test.tsx +9 -0
- package/tests/client/back-button.test.tsx +3 -3
- package/tests/client/data-table.test.tsx +9 -4
- package/tests/client/default-access.test.tsx +99 -5
- package/tests/client/form-feedback.test.tsx +589 -0
- package/tests/client/list-feedback.test.tsx +301 -0
- package/tests/client/list-recovery.test.tsx +373 -0
- package/tests/client/list-tabs.test.tsx +121 -0
- package/tests/client/manual-list-retry.test.tsx +190 -0
- package/tests/client/page-permission-navigation.test.ts +8 -0
- package/tests/client/permission-editor.test.tsx +191 -21
- package/tests/client/permission-filter-binding.test.tsx +1 -1
- package/tests/client/permission-workspace.test.ts +13 -0
- package/tests/client/role-return.test.tsx +2 -1
- package/tests/client/session-preparation.test.tsx +24 -1
- package/tests/client/settings-continuity.test.tsx +85 -5
- package/tests/client/settings-feedback.test.tsx +32 -15
- package/tests/client/settings-permissions.test.ts +35 -0
- package/tests/client/settings.test.tsx +2 -1
- package/tests/client/table-url-state.test.tsx +117 -3
- package/tests/client/user-drawer.test.tsx +2 -1
- package/tests/client/user-permissions.test.tsx +30 -5
- package/tests/database/mutation-receipts.test.ts +45 -0
- package/tests/fixtures/form-feedback/README.md +33 -0
- package/tests/fixtures/form-feedback/equipment-detail.tsx +11 -0
- package/tests/fixtures/form-feedback/equipment-layout.tsx +15 -0
- package/tests/fixtures/form-feedback/equipment-migration.ts +27 -0
- package/tests/fixtures/form-feedback/equipment-server.ts +197 -0
- package/tests/fixtures/form-feedback/equipment-validation.tsx +94 -0
- package/tests/fixtures/form-feedback/equipment.tsx +369 -0
- package/tests/fixtures/form-feedback/verify-inline.mjs +174 -0
- package/tests/fixtures/form-feedback/verify.mjs +373 -0
- package/tests/fixtures/permission-alignment.html +16 -0
- package/tests/fixtures/permission-alignment.tsx +124 -0
- package/tests/fixtures/submission/README.md +35 -0
- package/tests/fixtures/submission/browser.test.ts +478 -0
- package/tests/fixtures/submission/migration.ts +61 -0
- package/tests/fixtures/submission/page.tsx +138 -0
- package/tests/fixtures/submission/playwright.config.ts +11 -0
- package/tests/fixtures/submission/routes.ts +208 -0
- package/tests/helpers/settings-client.tsx +31 -0
- package/tests/playwright/permission-alignment.test.ts +109 -0
- package/tests/server/auth-email-config.test.ts +79 -0
- package/tests/server/concurrency.test.ts +61 -0
- package/tests/server/config.test.ts +34 -14
- package/tests/server/member-role.test.ts +7 -1
- package/tests/server/submission-settings.test.ts +241 -0
- package/client/route-overrides.ts +0 -9
- package/client/source-extensions.ts +0 -19
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FeedbackForm } from '#components/feedback-form';
|
|
1
2
|
import {
|
|
2
3
|
useId,
|
|
3
4
|
useState,
|
|
@@ -11,7 +12,7 @@ import { FieldGroup } from '#components/ui/field';
|
|
|
11
12
|
import { Input } from '#components/ui/input';
|
|
12
13
|
|
|
13
14
|
import { invalidProps } from './field-props.js';
|
|
14
|
-
import { AuthField,
|
|
15
|
+
import { AuthField, AuthFormSubmit } from './form-parts.js';
|
|
15
16
|
import { PasswordInput } from './password-input.js';
|
|
16
17
|
|
|
17
18
|
export interface PasswordLoginValues {
|
|
@@ -71,13 +72,21 @@ export function PasswordLoginForm({
|
|
|
71
72
|
const [identifier, setIdentifier] = useState('');
|
|
72
73
|
const [password, setPassword] = useState('');
|
|
73
74
|
|
|
74
|
-
const handleSubmit = (
|
|
75
|
+
const handleSubmit = async (
|
|
76
|
+
event: FormEvent<HTMLFormElement>,
|
|
77
|
+
): Promise<void> => {
|
|
75
78
|
event.preventDefault();
|
|
76
|
-
|
|
79
|
+
await onSubmit({ identifier: identifier.trim(), password });
|
|
77
80
|
};
|
|
78
81
|
|
|
79
82
|
return (
|
|
80
|
-
<
|
|
83
|
+
<FeedbackForm
|
|
84
|
+
submitting={submitting}
|
|
85
|
+
error={error}
|
|
86
|
+
errorContent={error}
|
|
87
|
+
className={className}
|
|
88
|
+
onSubmit={handleSubmit}
|
|
89
|
+
>
|
|
81
90
|
<FieldGroup>
|
|
82
91
|
<AuthField
|
|
83
92
|
error={fieldErrors?.identifier}
|
|
@@ -88,6 +97,7 @@ export function PasswordLoginForm({
|
|
|
88
97
|
autoComplete='username'
|
|
89
98
|
autoFocus
|
|
90
99
|
id={identifierId}
|
|
100
|
+
name='identifier'
|
|
91
101
|
onChange={(event) => setIdentifier(event.target.value)}
|
|
92
102
|
required
|
|
93
103
|
value={identifier}
|
|
@@ -104,6 +114,7 @@ export function PasswordLoginForm({
|
|
|
104
114
|
autoComplete='current-password'
|
|
105
115
|
hideLabel={labels.hidePassword}
|
|
106
116
|
id={passwordId}
|
|
117
|
+
name='password'
|
|
107
118
|
onChange={(event) => setPassword(event.target.value)}
|
|
108
119
|
required
|
|
109
120
|
showLabel={labels.showPassword}
|
|
@@ -111,13 +122,13 @@ export function PasswordLoginForm({
|
|
|
111
122
|
{...invalidProps(passwordId, fieldErrors?.password)}
|
|
112
123
|
/>
|
|
113
124
|
</AuthField>
|
|
114
|
-
|
|
125
|
+
|
|
115
126
|
<AuthFormSubmit footer={footer}>
|
|
116
127
|
<Button className='w-full' disabled={submitting} type='submit'>
|
|
117
128
|
{submitting ? labels.submitting : labels.submit}
|
|
118
129
|
</Button>
|
|
119
130
|
</AuthFormSubmit>
|
|
120
131
|
</FieldGroup>
|
|
121
|
-
</
|
|
132
|
+
</FeedbackForm>
|
|
122
133
|
);
|
|
123
134
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FeedbackForm } from '#components/feedback-form';
|
|
1
2
|
import {
|
|
2
3
|
useId,
|
|
3
4
|
useState,
|
|
@@ -11,7 +12,7 @@ import { FieldGroup } from '#components/ui/field';
|
|
|
11
12
|
import { Input } from '#components/ui/input';
|
|
12
13
|
|
|
13
14
|
import { invalidProps } from './field-props.js';
|
|
14
|
-
import { AuthField,
|
|
15
|
+
import { AuthField, AuthFormSubmit } from './form-parts.js';
|
|
15
16
|
|
|
16
17
|
export interface PasswordRegistrationValues {
|
|
17
18
|
readonly name: string;
|
|
@@ -91,14 +92,16 @@ export function PasswordRegistrationForm({
|
|
|
91
92
|
(event: { target: { value: string } }): void =>
|
|
92
93
|
setValues((current) => ({ ...current, [field]: event.target.value }));
|
|
93
94
|
|
|
94
|
-
const handleSubmit = (
|
|
95
|
+
const handleSubmit = async (
|
|
96
|
+
event: FormEvent<HTMLFormElement>,
|
|
97
|
+
): Promise<void> => {
|
|
95
98
|
event.preventDefault();
|
|
96
99
|
if (values.password !== values.confirmation) {
|
|
97
100
|
setMismatch(true);
|
|
98
101
|
return;
|
|
99
102
|
}
|
|
100
103
|
setMismatch(false);
|
|
101
|
-
|
|
104
|
+
await onSubmit({
|
|
102
105
|
name: values.name.trim(),
|
|
103
106
|
username: values.username.trim(),
|
|
104
107
|
email: values.email.trim(),
|
|
@@ -106,16 +109,26 @@ export function PasswordRegistrationForm({
|
|
|
106
109
|
});
|
|
107
110
|
};
|
|
108
111
|
|
|
109
|
-
const confirmationError =
|
|
112
|
+
const confirmationError =
|
|
113
|
+
mismatch && values.password !== values.confirmation
|
|
114
|
+
? labels.passwordMismatch
|
|
115
|
+
: undefined;
|
|
110
116
|
|
|
111
117
|
return (
|
|
112
|
-
<
|
|
118
|
+
<FeedbackForm
|
|
119
|
+
submitting={submitting}
|
|
120
|
+
error={error}
|
|
121
|
+
errorContent={error}
|
|
122
|
+
className={className}
|
|
123
|
+
onSubmit={handleSubmit}
|
|
124
|
+
>
|
|
113
125
|
<FieldGroup>
|
|
114
126
|
<AuthField error={fieldErrors?.name} id={ids.name} label={labels.name}>
|
|
115
127
|
<Input
|
|
116
128
|
autoComplete='name'
|
|
117
129
|
autoFocus
|
|
118
130
|
id={ids.name}
|
|
131
|
+
name='name'
|
|
119
132
|
onChange={set('name')}
|
|
120
133
|
required
|
|
121
134
|
value={values.name}
|
|
@@ -130,6 +143,7 @@ export function PasswordRegistrationForm({
|
|
|
130
143
|
<Input
|
|
131
144
|
autoComplete='username'
|
|
132
145
|
id={ids.username}
|
|
146
|
+
name='username'
|
|
133
147
|
onChange={set('username')}
|
|
134
148
|
required
|
|
135
149
|
value={values.username}
|
|
@@ -144,6 +158,7 @@ export function PasswordRegistrationForm({
|
|
|
144
158
|
<Input
|
|
145
159
|
autoComplete='email'
|
|
146
160
|
id={ids.email}
|
|
161
|
+
name='email'
|
|
147
162
|
onChange={set('email')}
|
|
148
163
|
required
|
|
149
164
|
type='email'
|
|
@@ -159,6 +174,7 @@ export function PasswordRegistrationForm({
|
|
|
159
174
|
<Input
|
|
160
175
|
autoComplete='new-password'
|
|
161
176
|
id={ids.password}
|
|
177
|
+
name='password'
|
|
162
178
|
onChange={set('password')}
|
|
163
179
|
required
|
|
164
180
|
type='password'
|
|
@@ -174,6 +190,7 @@ export function PasswordRegistrationForm({
|
|
|
174
190
|
<Input
|
|
175
191
|
autoComplete='new-password'
|
|
176
192
|
id={ids.confirmation}
|
|
193
|
+
name='confirmation'
|
|
177
194
|
onChange={set('confirmation')}
|
|
178
195
|
required
|
|
179
196
|
type='password'
|
|
@@ -181,13 +198,13 @@ export function PasswordRegistrationForm({
|
|
|
181
198
|
{...invalidProps(ids.confirmation, confirmationError)}
|
|
182
199
|
/>
|
|
183
200
|
</AuthField>
|
|
184
|
-
|
|
201
|
+
|
|
185
202
|
<AuthFormSubmit footer={footer}>
|
|
186
203
|
<Button className='w-full' disabled={submitting} type='submit'>
|
|
187
204
|
{submitting ? labels.submitting : labels.submit}
|
|
188
205
|
</Button>
|
|
189
206
|
</AuthFormSubmit>
|
|
190
207
|
</FieldGroup>
|
|
191
|
-
</
|
|
208
|
+
</FeedbackForm>
|
|
192
209
|
);
|
|
193
210
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FeedbackForm } from '#components/feedback-form';
|
|
1
2
|
import {
|
|
2
3
|
useId,
|
|
3
4
|
useState,
|
|
@@ -11,7 +12,7 @@ import { FieldGroup } from '#components/ui/field';
|
|
|
11
12
|
import { Input } from '#components/ui/input';
|
|
12
13
|
|
|
13
14
|
import { invalidProps } from './field-props.js';
|
|
14
|
-
import { AuthField,
|
|
15
|
+
import { AuthField, AuthFormSubmit } from './form-parts.js';
|
|
15
16
|
|
|
16
17
|
export interface PasswordResetValues {
|
|
17
18
|
readonly password: string;
|
|
@@ -66,7 +67,9 @@ export function PasswordResetForm({
|
|
|
66
67
|
const [confirmation, setConfirmation] = useState('');
|
|
67
68
|
const [mismatch, setMismatch] = useState(false);
|
|
68
69
|
|
|
69
|
-
const handleSubmit = (
|
|
70
|
+
const handleSubmit = async (
|
|
71
|
+
event: FormEvent<HTMLFormElement>,
|
|
72
|
+
): Promise<void> => {
|
|
70
73
|
event.preventDefault();
|
|
71
74
|
if (disabled) return;
|
|
72
75
|
if (password !== confirmation) {
|
|
@@ -74,13 +77,20 @@ export function PasswordResetForm({
|
|
|
74
77
|
return;
|
|
75
78
|
}
|
|
76
79
|
setMismatch(false);
|
|
77
|
-
|
|
80
|
+
await onSubmit({ password });
|
|
78
81
|
};
|
|
79
82
|
|
|
80
|
-
const confirmationError =
|
|
83
|
+
const confirmationError =
|
|
84
|
+
mismatch && password !== confirmation ? labels.passwordMismatch : undefined;
|
|
81
85
|
|
|
82
86
|
return (
|
|
83
|
-
<
|
|
87
|
+
<FeedbackForm
|
|
88
|
+
submitting={submitting}
|
|
89
|
+
error={error}
|
|
90
|
+
errorContent={error}
|
|
91
|
+
className={className}
|
|
92
|
+
onSubmit={handleSubmit}
|
|
93
|
+
>
|
|
84
94
|
<FieldGroup>
|
|
85
95
|
<AuthField
|
|
86
96
|
error={fieldErrors?.password}
|
|
@@ -92,6 +102,7 @@ export function PasswordResetForm({
|
|
|
92
102
|
autoFocus
|
|
93
103
|
disabled={disabled}
|
|
94
104
|
id={passwordId}
|
|
105
|
+
name='password'
|
|
95
106
|
onChange={(event) => setPassword(event.target.value)}
|
|
96
107
|
required
|
|
97
108
|
type='password'
|
|
@@ -108,6 +119,7 @@ export function PasswordResetForm({
|
|
|
108
119
|
autoComplete='new-password'
|
|
109
120
|
disabled={disabled}
|
|
110
121
|
id={confirmationId}
|
|
122
|
+
name='confirmation'
|
|
111
123
|
onChange={(event) => setConfirmation(event.target.value)}
|
|
112
124
|
required
|
|
113
125
|
type='password'
|
|
@@ -115,7 +127,7 @@ export function PasswordResetForm({
|
|
|
115
127
|
{...invalidProps(confirmationId, confirmationError)}
|
|
116
128
|
/>
|
|
117
129
|
</AuthField>
|
|
118
|
-
|
|
130
|
+
|
|
119
131
|
<AuthFormSubmit footer={footer}>
|
|
120
132
|
<Button
|
|
121
133
|
className='w-full'
|
|
@@ -127,6 +139,6 @@ export function PasswordResetForm({
|
|
|
127
139
|
</Button>
|
|
128
140
|
</AuthFormSubmit>
|
|
129
141
|
</FieldGroup>
|
|
130
|
-
</
|
|
142
|
+
</FeedbackForm>
|
|
131
143
|
);
|
|
132
144
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FeedbackForm } from '#components/feedback-form';
|
|
1
2
|
import {
|
|
2
3
|
useId,
|
|
3
4
|
useState,
|
|
@@ -63,19 +64,28 @@ export function PasswordResetRequestForm({
|
|
|
63
64
|
const emailId = `${useId()}-email`;
|
|
64
65
|
const [email, setEmail] = useState('');
|
|
65
66
|
|
|
66
|
-
const handleSubmit = (
|
|
67
|
+
const handleSubmit = async (
|
|
68
|
+
event: FormEvent<HTMLFormElement>,
|
|
69
|
+
): Promise<void> => {
|
|
67
70
|
event.preventDefault();
|
|
68
|
-
|
|
71
|
+
await onSubmit({ email: email.trim() });
|
|
69
72
|
};
|
|
70
73
|
|
|
71
74
|
return (
|
|
72
|
-
<
|
|
75
|
+
<FeedbackForm
|
|
76
|
+
submitting={submitting}
|
|
77
|
+
error={error}
|
|
78
|
+
errorContent={error}
|
|
79
|
+
className={className}
|
|
80
|
+
onSubmit={handleSubmit}
|
|
81
|
+
>
|
|
73
82
|
<FieldGroup>
|
|
74
83
|
<AuthField error={fieldErrors?.email} id={emailId} label={labels.email}>
|
|
75
84
|
<Input
|
|
76
85
|
autoComplete='email'
|
|
77
86
|
autoFocus
|
|
78
87
|
id={emailId}
|
|
88
|
+
name='email'
|
|
79
89
|
onChange={(event) => setEmail(event.target.value)}
|
|
80
90
|
required
|
|
81
91
|
type='email'
|
|
@@ -83,9 +93,7 @@ export function PasswordResetRequestForm({
|
|
|
83
93
|
{...invalidProps(emailId, fieldErrors?.email)}
|
|
84
94
|
/>
|
|
85
95
|
</AuthField>
|
|
86
|
-
{error ? (
|
|
87
|
-
<AuthFormStatus type='error'>{error}</AuthFormStatus>
|
|
88
|
-
) : success ? (
|
|
96
|
+
{!error && success ? (
|
|
89
97
|
<AuthFormStatus type='success'>{labels.success}</AuthFormStatus>
|
|
90
98
|
) : null}
|
|
91
99
|
<AuthFormSubmit footer={footer}>
|
|
@@ -94,6 +102,6 @@ export function PasswordResetRequestForm({
|
|
|
94
102
|
</Button>
|
|
95
103
|
</AuthFormSubmit>
|
|
96
104
|
</FieldGroup>
|
|
97
|
-
</
|
|
105
|
+
</FeedbackForm>
|
|
98
106
|
);
|
|
99
107
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FeedbackForm } from '#components/feedback-form';
|
|
1
2
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
2
3
|
import { CircleAlert, CircleCheck, CircleX, Clock } from 'lucide-react';
|
|
3
4
|
import {
|
|
@@ -125,7 +126,10 @@ function CodeEntry({
|
|
|
125
126
|
if (code) onSubmit(formatUserCode(code));
|
|
126
127
|
};
|
|
127
128
|
return (
|
|
128
|
-
<
|
|
129
|
+
<FeedbackForm
|
|
130
|
+
className={cn('flex flex-col gap-6', className)}
|
|
131
|
+
onSubmit={submit}
|
|
132
|
+
>
|
|
129
133
|
<FieldGroup>
|
|
130
134
|
<Field>
|
|
131
135
|
<FieldLabel htmlFor={id}>
|
|
@@ -137,6 +141,7 @@ function CodeEntry({
|
|
|
137
141
|
autoFocus
|
|
138
142
|
className='font-mono tracking-widest uppercase'
|
|
139
143
|
id={id}
|
|
144
|
+
name='userCode'
|
|
140
145
|
onChange={(event) => setTyped(event.target.value)}
|
|
141
146
|
placeholder='ABCD-EFGH'
|
|
142
147
|
spellCheck={false}
|
|
@@ -152,7 +157,7 @@ function CodeEntry({
|
|
|
152
157
|
<Button disabled={typed.trim() === ''} type='submit'>
|
|
153
158
|
{t('deviceApproval.continue', { defaultValue: 'Continue' })}
|
|
154
159
|
</Button>
|
|
155
|
-
</
|
|
160
|
+
</FeedbackForm>
|
|
156
161
|
);
|
|
157
162
|
}
|
|
158
163
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/** Every key the device approval looks up, so each locale is checked against the same list. */
|
|
2
2
|
export interface DeviceApprovalLocale {
|
|
3
|
+
readonly formFeedback: {
|
|
4
|
+
readonly field: string;
|
|
5
|
+
readonly invalid: string;
|
|
6
|
+
readonly failed: string;
|
|
7
|
+
readonly network: string;
|
|
8
|
+
readonly denied: string;
|
|
9
|
+
};
|
|
3
10
|
readonly 'deviceApproval.title': string;
|
|
4
11
|
readonly 'deviceApproval.description': string;
|
|
5
12
|
readonly 'deviceApproval.enterCode': string;
|
|
@@ -23,6 +30,15 @@ export interface DeviceApprovalLocale {
|
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
const enUS: DeviceApprovalLocale = {
|
|
33
|
+
formFeedback: {
|
|
34
|
+
field: 'This value was rejected. Check it and try again.',
|
|
35
|
+
invalid: 'Check the highlighted fields and try again.',
|
|
36
|
+
failed: 'Unable to save. Your input has been kept. Try again.',
|
|
37
|
+
network:
|
|
38
|
+
'Unable to connect. Check your connection and try again. Your input has been kept.',
|
|
39
|
+
denied:
|
|
40
|
+
'You cannot submit this form with your current access. Your input has been kept.',
|
|
41
|
+
},
|
|
26
42
|
'deviceApproval.title': 'Authorize a device',
|
|
27
43
|
'deviceApproval.description':
|
|
28
44
|
'A command line or another device asked to sign in as you. Approve it only if you started the sign-in yourself.',
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { DeviceApprovalLocale } from './en-US.js';
|
|
2
2
|
|
|
3
3
|
const zhCN: DeviceApprovalLocale = {
|
|
4
|
+
formFeedback: {
|
|
5
|
+
field: '此字段的值未通过校验,请检查并修改后重试。',
|
|
6
|
+
invalid: '请检查标记的字段后重试。',
|
|
7
|
+
failed: '保存失败,已保留您的输入,请重试。',
|
|
8
|
+
network: '无法连接,请检查网络后重试。已保留您的输入。',
|
|
9
|
+
denied: '当前权限无法提交此表单。已保留您的输入。',
|
|
10
|
+
},
|
|
4
11
|
'deviceApproval.title': '授权设备',
|
|
5
12
|
'deviceApproval.description':
|
|
6
13
|
'一个命令行或其他设备请求以你的身份登录。只有在登录是你自己发起的时候才批准。',
|
|
@@ -86,10 +86,28 @@ function OpenFilePreviewDialog(
|
|
|
86
86
|
>
|
|
87
87
|
<div className='flex items-center justify-between gap-3 pr-10'>
|
|
88
88
|
<div className='min-w-0'>
|
|
89
|
-
<
|
|
89
|
+
<div className='flex min-w-0 items-center gap-2'>
|
|
90
|
+
<DialogTitle className='truncate'>{file.filename}</DialogTitle>
|
|
91
|
+
{allowDownload ? (
|
|
92
|
+
<Button
|
|
93
|
+
type='button'
|
|
94
|
+
size='icon'
|
|
95
|
+
className='shrink-0'
|
|
96
|
+
variant='ghost'
|
|
97
|
+
aria-label={`${downloadLabel}: ${file.filename}`}
|
|
98
|
+
onClick={() =>
|
|
99
|
+
void downloadFile(file, onDownload).catch(
|
|
100
|
+
(error: unknown) => reportDownloadError(onError, error),
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
>
|
|
104
|
+
<Download aria-hidden='true' />
|
|
105
|
+
</Button>
|
|
106
|
+
) : null}
|
|
107
|
+
</div>
|
|
90
108
|
<p className='text-sm text-muted-foreground'>{file.mimeType}</p>
|
|
91
109
|
</div>
|
|
92
|
-
<div className='flex gap-1'>
|
|
110
|
+
<div className='flex shrink-0 gap-1'>
|
|
93
111
|
{files.length > 1 ? (
|
|
94
112
|
<>
|
|
95
113
|
<Button
|
|
@@ -122,21 +140,6 @@ function OpenFilePreviewDialog(
|
|
|
122
140
|
</Button>
|
|
123
141
|
</>
|
|
124
142
|
) : null}
|
|
125
|
-
{allowDownload ? (
|
|
126
|
-
<Button
|
|
127
|
-
type='button'
|
|
128
|
-
size='icon'
|
|
129
|
-
variant='ghost'
|
|
130
|
-
aria-label={`${downloadLabel}: ${file.filename}`}
|
|
131
|
-
onClick={() =>
|
|
132
|
-
void downloadFile(file, onDownload).catch((error: unknown) =>
|
|
133
|
-
reportDownloadError(onError, error),
|
|
134
|
-
)
|
|
135
|
-
}
|
|
136
|
-
>
|
|
137
|
-
<Download aria-hidden='true' />
|
|
138
|
-
</Button>
|
|
139
|
-
) : null}
|
|
140
143
|
</div>
|
|
141
144
|
</div>
|
|
142
145
|
{/* Only the preview scrolls; the title and its actions stay in view. */}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Workflow management
|
|
2
2
|
|
|
3
|
-
Install with `yes n | pnpm exec shadcn add @nocobase/workflow-management`. This block provides the workflow example's administration layout and interactions as presentational parts in `client/extensions/nocobase-workflow-management/`. Read `@nocobase/workflow-management-demo` for a standalone composition.
|
|
3
|
+
Install with `yes n | pnpm exec shadcn add @nocobase/workflow-management`. This block provides the workflow example's administration layout and interactions as presentational parts in `client/extensions/nocobase-workflow-management/`. Read `@nocobase/workflow-management-demo` for a standalone composition. Business labels and formatted values are supplied by the caller; the block imports no example plugin, API client, authorization provider or router.
|
|
4
4
|
|
|
5
5
|
| File | Exports and responsibility |
|
|
6
6
|
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -70,3 +70,5 @@ Connect `LifecycleDiagram.configuration` to the application's existing authorize
|
|
|
70
70
|
Omit `WorkflowDefinitionDetail.parameters` when the editor opens contextually so the diagram spans the available width; keep the slot when the same editor is visible beside the diagram. No editor, API, sample business registration or menu is installed by this block. Definition and execution lists use the same TanStack DataTable as the templates, with no second local pagination over a server page.
|
|
71
71
|
|
|
72
72
|
Ordering is optional: omit `onSort` when activity ordering is not supported so the header stays plain text. Omit a row's optional `name` when its linked `title` already supplies the complete display name. Hosts own filtering, pagination and their URL state; keep that state when opening and returning from a definition or execution.
|
|
73
|
+
|
|
74
|
+
List hosts pass `listState={resolveListState(request, rows.length, hasEffectiveFilters)}` and `onClearFilters`, using the current request error and data. The DataTable dependency supplies shared loading, denial, session and failure copy; merge the `listFeedback` English/Chinese keys from the components README into application translations. `onRefresh` is reused for retry and must preserve the current query. Keep `loaded`, `loading`, custom `empty` content and existing permission checks when reconciling installed copies. A successful empty filter is `no-results`; an empty unfiltered read is `empty`. Hosts that retain the legacy status slot without a listState still own their error rendering.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ListState } from '@nocobase/app-client';
|
|
1
2
|
import type { ReactElement, ReactNode } from 'react';
|
|
2
3
|
import type {
|
|
3
4
|
CellContext,
|
|
@@ -16,13 +17,15 @@ export interface WorkflowDefinitionRow {
|
|
|
16
17
|
activity: ReactNode;
|
|
17
18
|
}
|
|
18
19
|
export interface WorkflowDefinitionListProps {
|
|
20
|
+
listState?: ListState;
|
|
21
|
+
onClearFilters?: () => void;
|
|
19
22
|
rows: readonly WorkflowDefinitionRow[];
|
|
20
23
|
categories?: ReactNode;
|
|
21
24
|
search: ReactNode;
|
|
22
25
|
status?: ReactNode;
|
|
23
26
|
loaded: boolean;
|
|
24
27
|
loading: boolean;
|
|
25
|
-
empty
|
|
28
|
+
empty?: ReactNode;
|
|
26
29
|
labels: {
|
|
27
30
|
refresh: string;
|
|
28
31
|
workflow: string;
|
|
@@ -39,6 +42,8 @@ type DefinitionActivityMeta = ColumnMeta<WorkflowDefinitionRow, unknown> &
|
|
|
39
42
|
|
|
40
43
|
export function WorkflowDefinitionList({
|
|
41
44
|
rows,
|
|
45
|
+
listState,
|
|
46
|
+
onClearFilters,
|
|
42
47
|
categories,
|
|
43
48
|
search,
|
|
44
49
|
status,
|
|
@@ -71,10 +76,22 @@ export function WorkflowDefinitionList({
|
|
|
71
76
|
</Button>
|
|
72
77
|
</div>
|
|
73
78
|
{status}
|
|
74
|
-
{
|
|
79
|
+
{(listState !== undefined || loaded || loading) && (
|
|
75
80
|
<div className='bg-card'>
|
|
76
81
|
<DataTable
|
|
77
82
|
data={rows}
|
|
83
|
+
loading={loading}
|
|
84
|
+
listState={
|
|
85
|
+
listState ??
|
|
86
|
+
(!loaded ? 'loading' : rows.length ? 'ready' : 'empty')
|
|
87
|
+
}
|
|
88
|
+
onRetry={onRefresh}
|
|
89
|
+
onClearFilters={onClearFilters}
|
|
90
|
+
emptyMessage={
|
|
91
|
+
<Empty>
|
|
92
|
+
<EmptyDescription>{empty}</EmptyDescription>
|
|
93
|
+
</Empty>
|
|
94
|
+
}
|
|
78
95
|
getRowId={(row) => row.id}
|
|
79
96
|
pagination={false}
|
|
80
97
|
manualSorting
|
|
@@ -100,11 +117,6 @@ export function WorkflowDefinitionList({
|
|
|
100
117
|
/>
|
|
101
118
|
</div>
|
|
102
119
|
)}
|
|
103
|
-
{loaded && rows.length === 0 && (
|
|
104
|
-
<Empty>
|
|
105
|
-
<EmptyDescription>{empty}</EmptyDescription>
|
|
106
|
-
</Empty>
|
|
107
|
-
)}
|
|
108
120
|
</div>
|
|
109
121
|
</section>
|
|
110
122
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ListState } from '@nocobase/app-client';
|
|
1
2
|
import type { ReactElement, ReactNode } from 'react';
|
|
2
3
|
import type {
|
|
3
4
|
CellContext,
|
|
@@ -17,12 +18,14 @@ export interface WorkflowExecutionRow {
|
|
|
17
18
|
createdAt: ReactNode;
|
|
18
19
|
}
|
|
19
20
|
export interface WorkflowExecutionListProps {
|
|
21
|
+
listState?: ListState;
|
|
22
|
+
onClearFilters?: () => void;
|
|
20
23
|
rows: readonly WorkflowExecutionRow[];
|
|
21
24
|
loaded: boolean;
|
|
22
25
|
loading: boolean;
|
|
23
26
|
filters: ReactNode;
|
|
24
27
|
status?: ReactNode;
|
|
25
|
-
empty
|
|
28
|
+
empty?: ReactNode;
|
|
26
29
|
labels: {
|
|
27
30
|
refresh: string;
|
|
28
31
|
record: string;
|
|
@@ -47,6 +50,8 @@ type ExecutionDateMeta = ColumnMeta<WorkflowExecutionRow, unknown> &
|
|
|
47
50
|
|
|
48
51
|
export function WorkflowExecutionList({
|
|
49
52
|
rows,
|
|
53
|
+
listState,
|
|
54
|
+
onClearFilters,
|
|
50
55
|
loaded,
|
|
51
56
|
loading,
|
|
52
57
|
filters,
|
|
@@ -72,53 +77,61 @@ export function WorkflowExecutionList({
|
|
|
72
77
|
</Button>
|
|
73
78
|
</div>
|
|
74
79
|
{status}
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
80
|
+
{(listState !== undefined || loaded || loading) && (
|
|
81
|
+
<div className='bg-card'>
|
|
82
|
+
<DataTable
|
|
83
|
+
data={rows}
|
|
84
|
+
listState={
|
|
85
|
+
listState ??
|
|
86
|
+
(!loaded ? 'loading' : rows.length ? 'ready' : 'empty')
|
|
87
|
+
}
|
|
88
|
+
onRetry={onRefresh}
|
|
89
|
+
onClearFilters={onClearFilters}
|
|
90
|
+
getRowId={(row) => row.id}
|
|
91
|
+
pagination={false}
|
|
92
|
+
manualSorting
|
|
93
|
+
sortingState={[{ id: 'createdAt', desc: order === 'newest' }]}
|
|
94
|
+
loading={loading}
|
|
95
|
+
emptyMessage={
|
|
96
|
+
loaded ? (
|
|
97
|
+
<Empty>
|
|
98
|
+
<EmptyDescription>{empty}</EmptyDescription>
|
|
99
|
+
</Empty>
|
|
100
|
+
) : (
|
|
101
|
+
''
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
columns={[
|
|
105
|
+
{
|
|
106
|
+
id: 'record',
|
|
107
|
+
header: labels.record,
|
|
108
|
+
enableSorting: false,
|
|
109
|
+
cell: ExecutionRecordCell,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'workflow',
|
|
113
|
+
header: labels.workflow,
|
|
114
|
+
enableSorting: false,
|
|
115
|
+
cell: ExecutionWorkflowCell,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'state',
|
|
119
|
+
header: labels.state,
|
|
120
|
+
enableSorting: false,
|
|
121
|
+
cell: ExecutionStateCell,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'createdAt',
|
|
125
|
+
accessorFn: (row) => row.createdAt,
|
|
126
|
+
enableSorting: true,
|
|
127
|
+
meta: activityMeta,
|
|
128
|
+
header: ExecutionDateHeader,
|
|
129
|
+
cell: ExecutionCreatedAtCell,
|
|
130
|
+
},
|
|
131
|
+
]}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
122
135
|
<div className='flex flex-wrap items-center justify-between gap-3 text-sm'>
|
|
123
136
|
<p>{pagination}</p>
|
|
124
137
|
<div className='flex gap-2'>
|