@nocobase/app-template-default 1.0.0-beta.68 → 1.0.0-beta.70
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/components.md +5 -1
- package/.agents/rules/shell.md +3 -1
- package/AGENTS.md +7 -3
- package/Dockerfile.dockerignore +0 -1
- package/README.MD +3 -1
- package/client/app.ts +15 -0
- package/client/components/data-table/column-header.tsx +91 -0
- package/client/components/data-table/data-table.tsx +216 -0
- package/client/components/data-table/index.ts +13 -0
- package/client/components/data-table/pagination.tsx +165 -0
- package/client/components/data-table/view-options.tsx +77 -0
- package/client/components/flow-diagram-css.d.ts +2 -0
- package/client/components/flow-diagram.tsx +674 -0
- package/client/components/flow-layout.ts +250 -0
- package/client/components/lifecycle-diagram-model.ts +217 -0
- package/client/components/lifecycle-diagram.tsx +310 -0
- package/client/components/permission-editor.tsx +589 -0
- package/client/components/permission-filter.tsx +455 -0
- package/client/components/route-overlay.tsx +26 -13
- package/client/components/ui/checkbox.tsx +25 -0
- package/client/components/ui/sidebar.tsx +1 -4
- package/client/components/ui/switch.tsx +30 -0
- package/client/components/ui/table.tsx +113 -0
- package/client/extensions/nocobase-workflow-management/README.md +70 -0
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +148 -0
- package/client/extensions/nocobase-workflow-management/execution-detail.tsx +230 -0
- package/client/extensions/nocobase-workflow-management/execution-list.tsx +175 -0
- package/client/extensions/nocobase-workflow-management/layout.tsx +95 -0
- package/client/layouts/components/app-sidebar.tsx +1 -1
- package/client/layouts/use-sidebar-preference.ts +62 -42
- package/client/locales/system/en-US.ts +29 -0
- package/client/locales/system/settings-en-US.ts +142 -0
- package/client/locales/system/settings-zh-CN.ts +136 -0
- package/client/locales/system/zh-CN.ts +29 -0
- package/client/pages/auth/auth-page.css +15 -0
- package/client/pages/auth/login.tsx +1 -1
- package/client/pages/auth/shared.tsx +3 -1
- package/client/pages/settings/hooks.tsx +137 -0
- package/client/pages/settings/page-permission-titles.ts +7 -0
- package/client/pages/settings/page-permissions.ts +42 -0
- package/client/pages/settings/permission-filters.tsx +119 -0
- package/client/pages/settings/permissions-model.ts +260 -0
- package/client/pages/settings/role-form.tsx +405 -0
- package/client/pages/settings/roles.tsx +163 -0
- package/client/pages/settings/routes.ts +79 -0
- package/client/pages/settings/shared.tsx +119 -0
- package/client/pages/settings/user-detail.tsx +268 -0
- package/client/pages/settings/user-form.tsx +427 -0
- package/client/pages/settings/users.tsx +451 -0
- package/client/routing/app-router.tsx +4 -1
- package/client/routing/session-preparation.tsx +65 -0
- package/client/styles.css +4 -7
- package/package.json +27 -24
- package/tests/components/app-layout-routes.test.tsx +14 -7
- package/tests/components/data-table.test.tsx +127 -0
- package/tests/components/layout-i18n.test.tsx +8 -1
- package/tests/components/navigation-menu.test.tsx +24 -14
- package/tests/components/permission-editor.test.tsx +477 -0
- package/tests/components/permission-filter-binding.test.tsx +150 -0
- package/tests/components/permission-filter.test.tsx +202 -0
- package/tests/components/settings.test.tsx +294 -0
- package/tests/components/sidebar-permissions.test.tsx +12 -4
- package/tests/components/sidebar-preference.test.tsx +209 -14
- package/tests/components/user-permissions.test.tsx +142 -0
- package/tests/fixtures/theme-tokens.tsx +9 -1
- package/tests/logic/asset-url-build.test.ts +0 -1
- package/tests/logic/client-routes.test.ts +40 -0
- package/tests/logic/client-shell.test.tsx +10 -2
- package/tests/logic/config-example.test.ts +30 -0
- package/tests/logic/dev-client-watch.test.ts +8 -1
- package/tests/logic/page-permission-titles.test.ts +223 -0
- package/tests/logic/route-overlay.test.tsx +42 -0
- package/tests/logic/session-preparation.test.tsx +223 -0
- package/tests/logic/settings-permissions.test.ts +168 -0
- package/vite.config.ts +0 -22
- package/client/agent-annotations-host.ts +0 -25
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMemo,
|
|
3
|
+
useRef,
|
|
4
|
+
useState,
|
|
5
|
+
type ReactElement,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { ChevronDownIcon, Settings2Icon } from 'lucide-react';
|
|
9
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
10
|
+
import { Button } from '#components/ui/button';
|
|
11
|
+
import {
|
|
12
|
+
Popover,
|
|
13
|
+
PopoverContent,
|
|
14
|
+
PopoverTitle,
|
|
15
|
+
PopoverTrigger,
|
|
16
|
+
} from '#components/ui/popover';
|
|
17
|
+
import type { LifecycleDescription } from '@nocobase/lifecycle';
|
|
18
|
+
import { FlowDiagram, type FlowDiagramProps } from '#components/flow-diagram';
|
|
19
|
+
import type { FlowDiagramNode } from '#components/flow-layout';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
lifecycleDiagramGraph,
|
|
23
|
+
lifecycleConfigurationSelection,
|
|
24
|
+
lifecycleNodeConfigurationSelections,
|
|
25
|
+
type LifecycleConfigurationSelection,
|
|
26
|
+
type LifecycleConfigurationTarget,
|
|
27
|
+
type LifecycleDiagramConfiguration,
|
|
28
|
+
type LifecycleDiagramOptions,
|
|
29
|
+
} from './lifecycle-diagram-model.js';
|
|
30
|
+
|
|
31
|
+
export interface LifecycleDiagramProps
|
|
32
|
+
extends
|
|
33
|
+
LifecycleDiagramOptions,
|
|
34
|
+
Omit<
|
|
35
|
+
FlowDiagramProps,
|
|
36
|
+
| 'nodes'
|
|
37
|
+
| 'edges'
|
|
38
|
+
| 'renderNode'
|
|
39
|
+
| 'nodeActions'
|
|
40
|
+
| 'onNodeClick'
|
|
41
|
+
| 'onEdgeClick'
|
|
42
|
+
> {
|
|
43
|
+
readonly description: LifecycleDescription;
|
|
44
|
+
/** Opens the application-owned, authorized parameter editor for declared bindings. */
|
|
45
|
+
readonly configuration?: LifecycleDiagramConfiguration;
|
|
46
|
+
readonly stateActions?: (
|
|
47
|
+
state: LifecycleDescription['stateInfo'][number],
|
|
48
|
+
) => ReactNode;
|
|
49
|
+
readonly renderState?: (
|
|
50
|
+
state: LifecycleDescription['stateInfo'][number],
|
|
51
|
+
node: FlowDiagramNode,
|
|
52
|
+
) => ReactNode;
|
|
53
|
+
readonly onStateClick?: (
|
|
54
|
+
state: LifecycleDescription['stateInfo'][number],
|
|
55
|
+
) => void;
|
|
56
|
+
readonly onTransitionClick?: (
|
|
57
|
+
transition: LifecycleDescription['transitions'][number],
|
|
58
|
+
) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Definitions are source-owned; clicking opens consumer-owned details, never fires a transition. */
|
|
62
|
+
export function LifecycleDiagram({
|
|
63
|
+
description,
|
|
64
|
+
currentState,
|
|
65
|
+
endLabel,
|
|
66
|
+
history,
|
|
67
|
+
visitedStates,
|
|
68
|
+
availableTransitions,
|
|
69
|
+
stateLabel,
|
|
70
|
+
transitionLabel,
|
|
71
|
+
renderState,
|
|
72
|
+
stateActions,
|
|
73
|
+
configuration,
|
|
74
|
+
onStateClick,
|
|
75
|
+
onTransitionClick,
|
|
76
|
+
...props
|
|
77
|
+
}: LifecycleDiagramProps): ReactElement {
|
|
78
|
+
const sourceGraph = useMemo(
|
|
79
|
+
() =>
|
|
80
|
+
lifecycleDiagramGraph(description, {
|
|
81
|
+
currentState,
|
|
82
|
+
endLabel,
|
|
83
|
+
history,
|
|
84
|
+
visitedStates,
|
|
85
|
+
availableTransitions,
|
|
86
|
+
stateLabel,
|
|
87
|
+
transitionLabel,
|
|
88
|
+
}),
|
|
89
|
+
[
|
|
90
|
+
description,
|
|
91
|
+
currentState,
|
|
92
|
+
endLabel,
|
|
93
|
+
history,
|
|
94
|
+
visitedStates,
|
|
95
|
+
availableTransitions,
|
|
96
|
+
stateLabel,
|
|
97
|
+
transitionLabel,
|
|
98
|
+
],
|
|
99
|
+
);
|
|
100
|
+
const graph = configuration
|
|
101
|
+
? {
|
|
102
|
+
nodes: sourceGraph.nodes.map((node) => ({
|
|
103
|
+
...node,
|
|
104
|
+
configurable: Boolean(
|
|
105
|
+
lifecycleNodeConfigurationSelections(
|
|
106
|
+
description,
|
|
107
|
+
node.id,
|
|
108
|
+
configuration.canConfigure,
|
|
109
|
+
).length,
|
|
110
|
+
),
|
|
111
|
+
})),
|
|
112
|
+
edges: sourceGraph.edges.map((edge) => ({
|
|
113
|
+
...edge,
|
|
114
|
+
configurable:
|
|
115
|
+
!edge.decorative &&
|
|
116
|
+
Boolean(
|
|
117
|
+
lifecycleConfigurationSelection(
|
|
118
|
+
description,
|
|
119
|
+
{
|
|
120
|
+
kind: 'transition',
|
|
121
|
+
name: (JSON.parse(edge.id) as string[])[0],
|
|
122
|
+
},
|
|
123
|
+
configuration.canConfigure,
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
})),
|
|
127
|
+
}
|
|
128
|
+
: sourceGraph;
|
|
129
|
+
return (
|
|
130
|
+
<FlowDiagram
|
|
131
|
+
direction='DOWN'
|
|
132
|
+
initialMinZoom={0.7}
|
|
133
|
+
autoHeight
|
|
134
|
+
{...props}
|
|
135
|
+
{...graph}
|
|
136
|
+
nodeActions={(node) => {
|
|
137
|
+
const state = description.stateInfo.find(
|
|
138
|
+
(state) => state.name === node.id,
|
|
139
|
+
);
|
|
140
|
+
if (!state) return undefined;
|
|
141
|
+
const actions = stateActions?.(state);
|
|
142
|
+
const selections =
|
|
143
|
+
configuration &&
|
|
144
|
+
lifecycleNodeConfigurationSelections(
|
|
145
|
+
description,
|
|
146
|
+
state.name,
|
|
147
|
+
configuration.canConfigure,
|
|
148
|
+
);
|
|
149
|
+
if (!selections?.length || !configuration) return actions;
|
|
150
|
+
return (
|
|
151
|
+
<div className='nodrag nopan flex items-center gap-1'>
|
|
152
|
+
{actions}
|
|
153
|
+
<NodeConfigurationActions
|
|
154
|
+
configuration={configuration}
|
|
155
|
+
selections={selections}
|
|
156
|
+
state={{ kind: 'state', name: state.name }}
|
|
157
|
+
title={(target) => {
|
|
158
|
+
if (target.kind === 'state')
|
|
159
|
+
return stateLabel?.(state) ?? state.title;
|
|
160
|
+
const transition = description.transitions.find(
|
|
161
|
+
(item) => item.name === target.name,
|
|
162
|
+
)!;
|
|
163
|
+
return transitionLabel?.(transition) ?? transition.title;
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}}
|
|
169
|
+
renderNode={(node) => {
|
|
170
|
+
const state = description.stateInfo.find(
|
|
171
|
+
(state) => state.name === node.id,
|
|
172
|
+
);
|
|
173
|
+
return state && renderState ? renderState(state, node) : node.label;
|
|
174
|
+
}}
|
|
175
|
+
onNodeClick={(node) => {
|
|
176
|
+
const state = description.stateInfo.find(
|
|
177
|
+
(state) => state.name === node.id,
|
|
178
|
+
);
|
|
179
|
+
if (state) onStateClick?.(state);
|
|
180
|
+
}}
|
|
181
|
+
onEdgeClick={(edge) => {
|
|
182
|
+
if (edge.decorative) return;
|
|
183
|
+
const [name] = JSON.parse(edge.id) as string[];
|
|
184
|
+
const transition = description.transitions.find(
|
|
185
|
+
(item) => item.name === name,
|
|
186
|
+
);
|
|
187
|
+
if (!transition) return;
|
|
188
|
+
const selection =
|
|
189
|
+
configuration &&
|
|
190
|
+
lifecycleConfigurationSelection(
|
|
191
|
+
description,
|
|
192
|
+
{ kind: 'transition', name: transition.name },
|
|
193
|
+
configuration.canConfigure,
|
|
194
|
+
);
|
|
195
|
+
if (selection && configuration) configuration.onConfigure(selection);
|
|
196
|
+
else onTransitionClick?.(transition);
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Reuse the authoritative editor; this chooser never invents or saves values. */
|
|
203
|
+
function NodeConfigurationActions({
|
|
204
|
+
configuration,
|
|
205
|
+
selections,
|
|
206
|
+
state,
|
|
207
|
+
title,
|
|
208
|
+
}: {
|
|
209
|
+
readonly configuration: LifecycleDiagramConfiguration;
|
|
210
|
+
readonly selections: readonly LifecycleConfigurationSelection[];
|
|
211
|
+
readonly state: LifecycleConfigurationTarget;
|
|
212
|
+
readonly title: (target: LifecycleConfigurationTarget) => string;
|
|
213
|
+
}): ReactElement {
|
|
214
|
+
const { t } = useTranslation();
|
|
215
|
+
const [open, setOpen] = useState(false);
|
|
216
|
+
const pendingRef = useRef<LifecycleConfigurationSelection | undefined>(
|
|
217
|
+
undefined,
|
|
218
|
+
);
|
|
219
|
+
const first = selections[0];
|
|
220
|
+
const direct =
|
|
221
|
+
first.target.kind === 'state' || selections.length === 1
|
|
222
|
+
? first
|
|
223
|
+
: undefined;
|
|
224
|
+
const choices = direct ? selections.slice(1) : selections;
|
|
225
|
+
const label = t('flowDiagram.configure', { defaultValue: 'Configure' });
|
|
226
|
+
const className =
|
|
227
|
+
'h-6 border border-primary/15 bg-primary/5 px-1.5 text-xs text-primary hover:bg-primary/10';
|
|
228
|
+
return (
|
|
229
|
+
<>
|
|
230
|
+
{direct && (
|
|
231
|
+
<Button
|
|
232
|
+
type='button'
|
|
233
|
+
variant='ghost'
|
|
234
|
+
size='sm'
|
|
235
|
+
className={className}
|
|
236
|
+
onClick={(event) => {
|
|
237
|
+
event.stopPropagation();
|
|
238
|
+
configuration.onConfigure(direct);
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
<Settings2Icon aria-hidden />
|
|
242
|
+
{configuration.label(direct.target)}
|
|
243
|
+
</Button>
|
|
244
|
+
)}
|
|
245
|
+
{choices.length > 0 && (
|
|
246
|
+
<Popover
|
|
247
|
+
open={open}
|
|
248
|
+
onOpenChange={(nextOpen) => {
|
|
249
|
+
if (nextOpen) pendingRef.current = undefined;
|
|
250
|
+
setOpen(nextOpen);
|
|
251
|
+
}}
|
|
252
|
+
onOpenChangeComplete={(nextOpen) => {
|
|
253
|
+
// Open the editor only after the chooser has closed and restored focus.
|
|
254
|
+
// Otherwise its focus restoration can steal focus from the editor.
|
|
255
|
+
if (!nextOpen && pendingRef.current) {
|
|
256
|
+
const selection = pendingRef.current;
|
|
257
|
+
pendingRef.current = undefined;
|
|
258
|
+
configuration.onConfigure(selection);
|
|
259
|
+
}
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
<PopoverTrigger
|
|
263
|
+
render={
|
|
264
|
+
<Button
|
|
265
|
+
type='button'
|
|
266
|
+
variant='ghost'
|
|
267
|
+
size='sm'
|
|
268
|
+
className={className}
|
|
269
|
+
aria-label={`${label}: ${title(state)}`}
|
|
270
|
+
onClick={(event) => event.stopPropagation()}
|
|
271
|
+
/>
|
|
272
|
+
}
|
|
273
|
+
>
|
|
274
|
+
{!direct && (
|
|
275
|
+
<>
|
|
276
|
+
<Settings2Icon aria-hidden />
|
|
277
|
+
{label}
|
|
278
|
+
</>
|
|
279
|
+
)}
|
|
280
|
+
<ChevronDownIcon aria-hidden />
|
|
281
|
+
</PopoverTrigger>
|
|
282
|
+
<PopoverContent
|
|
283
|
+
align='end'
|
|
284
|
+
className='w-auto max-w-[min(var(--container-sm),var(--available-width))]'
|
|
285
|
+
onClick={(event) => event.stopPropagation()}
|
|
286
|
+
>
|
|
287
|
+
<PopoverTitle className='sr-only'>
|
|
288
|
+
{label}: {title(state)}
|
|
289
|
+
</PopoverTitle>
|
|
290
|
+
{choices.map((selection) => (
|
|
291
|
+
<Button
|
|
292
|
+
key={`${selection.target.kind}:${selection.target.name}`}
|
|
293
|
+
type='button'
|
|
294
|
+
variant='ghost'
|
|
295
|
+
className='h-auto justify-start whitespace-normal text-left'
|
|
296
|
+
onClick={() => {
|
|
297
|
+
pendingRef.current = selection;
|
|
298
|
+
setOpen(false);
|
|
299
|
+
}}
|
|
300
|
+
>
|
|
301
|
+
{configuration.label(selection.target)} ·{' '}
|
|
302
|
+
{title(selection.target)}
|
|
303
|
+
</Button>
|
|
304
|
+
))}
|
|
305
|
+
</PopoverContent>
|
|
306
|
+
</Popover>
|
|
307
|
+
)}
|
|
308
|
+
</>
|
|
309
|
+
);
|
|
310
|
+
}
|