@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.
Files changed (76) hide show
  1. package/.agents/rules/components.md +5 -1
  2. package/.agents/rules/shell.md +3 -1
  3. package/AGENTS.md +7 -3
  4. package/Dockerfile.dockerignore +0 -1
  5. package/README.MD +3 -1
  6. package/client/app.ts +15 -0
  7. package/client/components/data-table/column-header.tsx +91 -0
  8. package/client/components/data-table/data-table.tsx +216 -0
  9. package/client/components/data-table/index.ts +13 -0
  10. package/client/components/data-table/pagination.tsx +165 -0
  11. package/client/components/data-table/view-options.tsx +77 -0
  12. package/client/components/flow-diagram-css.d.ts +2 -0
  13. package/client/components/flow-diagram.tsx +674 -0
  14. package/client/components/flow-layout.ts +250 -0
  15. package/client/components/lifecycle-diagram-model.ts +217 -0
  16. package/client/components/lifecycle-diagram.tsx +310 -0
  17. package/client/components/permission-editor.tsx +589 -0
  18. package/client/components/permission-filter.tsx +455 -0
  19. package/client/components/route-overlay.tsx +26 -13
  20. package/client/components/ui/checkbox.tsx +25 -0
  21. package/client/components/ui/sidebar.tsx +1 -4
  22. package/client/components/ui/switch.tsx +30 -0
  23. package/client/components/ui/table.tsx +113 -0
  24. package/client/extensions/nocobase-workflow-management/README.md +70 -0
  25. package/client/extensions/nocobase-workflow-management/definition-list.tsx +148 -0
  26. package/client/extensions/nocobase-workflow-management/execution-detail.tsx +230 -0
  27. package/client/extensions/nocobase-workflow-management/execution-list.tsx +175 -0
  28. package/client/extensions/nocobase-workflow-management/layout.tsx +95 -0
  29. package/client/layouts/components/app-sidebar.tsx +1 -1
  30. package/client/layouts/use-sidebar-preference.ts +62 -42
  31. package/client/locales/system/en-US.ts +29 -0
  32. package/client/locales/system/settings-en-US.ts +142 -0
  33. package/client/locales/system/settings-zh-CN.ts +136 -0
  34. package/client/locales/system/zh-CN.ts +29 -0
  35. package/client/pages/auth/auth-page.css +15 -0
  36. package/client/pages/auth/login.tsx +1 -1
  37. package/client/pages/auth/shared.tsx +3 -1
  38. package/client/pages/settings/hooks.tsx +137 -0
  39. package/client/pages/settings/page-permission-titles.ts +7 -0
  40. package/client/pages/settings/page-permissions.ts +42 -0
  41. package/client/pages/settings/permission-filters.tsx +119 -0
  42. package/client/pages/settings/permissions-model.ts +260 -0
  43. package/client/pages/settings/role-form.tsx +405 -0
  44. package/client/pages/settings/roles.tsx +163 -0
  45. package/client/pages/settings/routes.ts +79 -0
  46. package/client/pages/settings/shared.tsx +119 -0
  47. package/client/pages/settings/user-detail.tsx +268 -0
  48. package/client/pages/settings/user-form.tsx +427 -0
  49. package/client/pages/settings/users.tsx +451 -0
  50. package/client/routing/app-router.tsx +4 -1
  51. package/client/routing/session-preparation.tsx +65 -0
  52. package/client/styles.css +4 -7
  53. package/package.json +27 -24
  54. package/tests/components/app-layout-routes.test.tsx +14 -7
  55. package/tests/components/data-table.test.tsx +127 -0
  56. package/tests/components/layout-i18n.test.tsx +8 -1
  57. package/tests/components/navigation-menu.test.tsx +24 -14
  58. package/tests/components/permission-editor.test.tsx +477 -0
  59. package/tests/components/permission-filter-binding.test.tsx +150 -0
  60. package/tests/components/permission-filter.test.tsx +202 -0
  61. package/tests/components/settings.test.tsx +294 -0
  62. package/tests/components/sidebar-permissions.test.tsx +12 -4
  63. package/tests/components/sidebar-preference.test.tsx +209 -14
  64. package/tests/components/user-permissions.test.tsx +142 -0
  65. package/tests/fixtures/theme-tokens.tsx +9 -1
  66. package/tests/logic/asset-url-build.test.ts +0 -1
  67. package/tests/logic/client-routes.test.ts +40 -0
  68. package/tests/logic/client-shell.test.tsx +10 -2
  69. package/tests/logic/config-example.test.ts +30 -0
  70. package/tests/logic/dev-client-watch.test.ts +8 -1
  71. package/tests/logic/page-permission-titles.test.ts +223 -0
  72. package/tests/logic/route-overlay.test.tsx +42 -0
  73. package/tests/logic/session-preparation.test.tsx +223 -0
  74. package/tests/logic/settings-permissions.test.ts +168 -0
  75. package/vite.config.ts +0 -22
  76. 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
+ }