@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
@@ -0,0 +1,135 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import type { ReactNode } from 'react';
3
+
4
+ export type SetupUnknownReason =
5
+ 'unavailable' | 'signIn' | 'forbidden' | 'invalid';
6
+ export interface SetupInspection {
7
+ readonly status: 'ready' | 'missing' | 'unknown';
8
+ readonly reason?: SetupUnknownReason;
9
+ readonly canConfigure?: boolean;
10
+ }
11
+ export interface SetupCheck {
12
+ readonly id: string;
13
+ readonly title: ReactNode;
14
+ readonly description: ReactNode;
15
+ readonly action?: ReactNode;
16
+ }
17
+ export type SetupInspections = Readonly<Record<string, SetupInspection>>;
18
+ export interface SetupReadinessState {
19
+ readonly active: boolean;
20
+ readonly items: readonly (SetupCheck & SetupInspection)[];
21
+ readonly loading: boolean;
22
+ readonly refresh: () => void;
23
+ }
24
+
25
+ /** Keep transport errors out of UI copy and never call a failed read missing. */
26
+ export function setupInspectionFailure(error: unknown): SetupInspection {
27
+ const status =
28
+ typeof error === 'object' && error !== null && 'status' in error
29
+ ? Number(error.status)
30
+ : undefined;
31
+ return {
32
+ status: 'unknown',
33
+ reason:
34
+ status === 401
35
+ ? 'signIn'
36
+ : status === 403
37
+ ? 'forbidden'
38
+ : status === 400 || status === 404
39
+ ? 'invalid'
40
+ : 'unavailable',
41
+ };
42
+ }
43
+
44
+ /** Memoize checks and inspect. The owning feature supplies a real authorized read. */
45
+ export function useSetupReadiness(
46
+ checks: readonly SetupCheck[],
47
+ inspect: () => Promise<SetupInspections>,
48
+ {
49
+ enabled = true,
50
+ refreshKey = '',
51
+ }: { enabled?: boolean; refreshKey?: string } = {},
52
+ ): SetupReadinessState {
53
+ const [revision, setRevision] = useState(0);
54
+ const active = enabled && checks.length > 0;
55
+ const request = useMemo(
56
+ () => ({ checks, inspect, refreshKey, revision, active }),
57
+ [checks, inspect, refreshKey, revision, active],
58
+ );
59
+ const [snapshot, setSnapshot] = useState<{
60
+ request: typeof request;
61
+ values: SetupInspections;
62
+ }>();
63
+ const busyRef = useRef(false);
64
+ const refresh = useCallback(() => setRevision((value) => value + 1), []);
65
+ useEffect(() => {
66
+ const { active, checks, inspect } = request;
67
+ if (!active) return;
68
+ let current = true;
69
+ busyRef.current = true;
70
+ const finish = (values: SetupInspections) => {
71
+ if (!current) return;
72
+ busyRef.current = false;
73
+ setSnapshot({ request, values });
74
+ };
75
+ void Promise.resolve()
76
+ .then(() => (current ? inspect() : {}))
77
+ .then(finish, (error: unknown) =>
78
+ finish(
79
+ Object.fromEntries(
80
+ checks.map(({ id }) => [id, setupInspectionFailure(error)]),
81
+ ),
82
+ ),
83
+ );
84
+ return () => {
85
+ current = false;
86
+ busyRef.current = false;
87
+ };
88
+ }, [request]);
89
+ useEffect(() => {
90
+ if (!active) return;
91
+ const onFocus = () => {
92
+ if (!busyRef.current) refresh();
93
+ };
94
+ window.addEventListener('focus', onFocus);
95
+ return () => window.removeEventListener('focus', onFocus);
96
+ }, [active, refresh]);
97
+ const loading = active && snapshot?.request !== request;
98
+ return {
99
+ active,
100
+ items:
101
+ !active || loading
102
+ ? []
103
+ : checks.map((check) => {
104
+ const values = snapshot?.values;
105
+ const value =
106
+ values && Object.hasOwn(values, check.id)
107
+ ? values[check.id]
108
+ : undefined;
109
+ if (
110
+ !value ||
111
+ !['ready', 'missing', 'unknown'].includes(value.status)
112
+ )
113
+ return {
114
+ ...check,
115
+ status: 'unknown' as const,
116
+ reason: 'invalid' as const,
117
+ };
118
+ const reason =
119
+ value.reason &&
120
+ ['unavailable', 'signIn', 'forbidden', 'invalid'].includes(
121
+ value.reason,
122
+ )
123
+ ? value.reason
124
+ : 'invalid';
125
+ return {
126
+ ...check,
127
+ status: value.status,
128
+ reason,
129
+ canConfigure: value.canConfigure !== false,
130
+ };
131
+ }),
132
+ loading,
133
+ refresh,
134
+ };
135
+ }
@@ -0,0 +1,91 @@
1
+ import { useId, useState, type ReactElement } from 'react';
2
+ import { Button } from '#components/ui/button';
3
+ import type {
4
+ SetupReadinessState,
5
+ SetupUnknownReason,
6
+ } from './setup-readiness-state.js';
7
+
8
+ export interface SetupReadinessLabels {
9
+ readonly title: string;
10
+ readonly checking: string;
11
+ readonly checkAgain: string;
12
+ readonly ready: string;
13
+ readonly missing: string;
14
+ readonly unknown: Readonly<Record<SetupUnknownReason, string>>;
15
+ }
16
+
17
+ /** A feature mounts this only for its actual prerequisites, never every plugin. */
18
+ export function SetupReadiness({
19
+ state,
20
+ labels,
21
+ }: {
22
+ readonly state: SetupReadinessState;
23
+ readonly labels: SetupReadinessLabels;
24
+ }): ReactElement | null {
25
+ const id = useId();
26
+ const [keepConfirmation, setKeepConfirmation] = useState(false);
27
+ const pending = state.items.filter(({ status }) => status !== 'ready');
28
+ if (!state.active) return null;
29
+ if (!state.loading && pending.length === 0 && !keepConfirmation) return null;
30
+ const canRefresh =
31
+ state.loading ||
32
+ state.items.some(
33
+ ({ status, reason }) => status !== 'unknown' || reason === 'unavailable',
34
+ );
35
+ return (
36
+ <section
37
+ aria-labelledby={id}
38
+ aria-busy={state.loading}
39
+ className='flex flex-col gap-4 rounded-lg border bg-card p-4 text-card-foreground'
40
+ >
41
+ <div className='flex flex-wrap items-center justify-between gap-2'>
42
+ <h2 id={id} className='text-base font-medium'>
43
+ {labels.title}
44
+ </h2>
45
+ {(canRefresh || keepConfirmation) && (
46
+ <Button
47
+ type='button'
48
+ variant='outline'
49
+ size='sm'
50
+ disabled={state.loading || !canRefresh}
51
+ onClick={() => {
52
+ setKeepConfirmation(true);
53
+ state.refresh();
54
+ }}
55
+ >
56
+ {labels.checkAgain}
57
+ </Button>
58
+ )}
59
+ </div>
60
+ <p role='status' className='text-sm text-muted-foreground'>
61
+ {state.loading
62
+ ? labels.checking
63
+ : pending.length
64
+ ? labels.missing
65
+ : labels.ready}
66
+ </p>
67
+ {!state.loading && pending.length > 0 && (
68
+ <ul className='divide-y'>
69
+ {pending.map((item) => (
70
+ <li
71
+ key={item.id}
72
+ className='flex flex-wrap items-center justify-between gap-3 py-2 first:pt-0 last:pb-0'
73
+ >
74
+ <div className='min-w-0 flex-1 basis-48 break-words'>
75
+ <p className='text-sm font-medium'>{item.title}</p>
76
+ <p className='text-sm text-muted-foreground'>
77
+ {item.status === 'missing'
78
+ ? item.description
79
+ : labels.unknown[item.reason ?? 'invalid']}
80
+ </p>
81
+ </div>
82
+ {item.status === 'missing' &&
83
+ item.canConfigure !== false &&
84
+ item.action}
85
+ </li>
86
+ ))}
87
+ </ul>
88
+ )}
89
+ </section>
90
+ );
91
+ }
@@ -0,0 +1,300 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Combobox as ComboboxPrimitive } from '@base-ui/react';
5
+ import { cn } from 'cn';
6
+
7
+ import { Button } from '#components/ui/button';
8
+ import {
9
+ InputGroup,
10
+ InputGroupAddon,
11
+ InputGroupButton,
12
+ InputGroupInput,
13
+ } from '#components/ui/input-group';
14
+ import { ChevronDownIcon, XIcon, CheckIcon } from 'lucide-react';
15
+
16
+ const Combobox = ComboboxPrimitive.Root;
17
+
18
+ function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
19
+ return <ComboboxPrimitive.Value data-slot='combobox-value' {...props} />;
20
+ }
21
+
22
+ function ComboboxTrigger({
23
+ className,
24
+ children,
25
+ ...props
26
+ }: ComboboxPrimitive.Trigger.Props) {
27
+ return (
28
+ <ComboboxPrimitive.Trigger
29
+ data-slot='combobox-trigger'
30
+ className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
31
+ {...props}
32
+ >
33
+ {children}
34
+ <ChevronDownIcon className='pointer-events-none size-4 text-muted-foreground' />
35
+ </ComboboxPrimitive.Trigger>
36
+ );
37
+ }
38
+
39
+ function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
40
+ return (
41
+ <ComboboxPrimitive.Clear
42
+ data-slot='combobox-clear'
43
+ render={<InputGroupButton variant='ghost' size='icon-xs' />}
44
+ className={cn(className)}
45
+ {...props}
46
+ >
47
+ <XIcon className='pointer-events-none' />
48
+ </ComboboxPrimitive.Clear>
49
+ );
50
+ }
51
+
52
+ function ComboboxInput({
53
+ className,
54
+ children,
55
+ disabled = false,
56
+ showTrigger = true,
57
+ showClear = false,
58
+ ...props
59
+ }: ComboboxPrimitive.Input.Props & {
60
+ showTrigger?: boolean;
61
+ showClear?: boolean;
62
+ }) {
63
+ return (
64
+ <InputGroup className={cn('w-auto', className)}>
65
+ <ComboboxPrimitive.Input
66
+ render={<InputGroupInput disabled={disabled} />}
67
+ {...props}
68
+ />
69
+ <InputGroupAddon align='inline-end'>
70
+ {showTrigger && (
71
+ <InputGroupButton
72
+ size='icon-xs'
73
+ variant='ghost'
74
+ render={<ComboboxTrigger />}
75
+ data-slot='input-group-button'
76
+ className='group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent'
77
+ disabled={disabled}
78
+ />
79
+ )}
80
+ {showClear && <ComboboxClear disabled={disabled} />}
81
+ </InputGroupAddon>
82
+ {children}
83
+ </InputGroup>
84
+ );
85
+ }
86
+
87
+ function ComboboxContent({
88
+ className,
89
+ side = 'bottom',
90
+ sideOffset = 6,
91
+ align = 'start',
92
+ alignOffset = 0,
93
+ anchor,
94
+ ...props
95
+ }: ComboboxPrimitive.Popup.Props &
96
+ Pick<
97
+ ComboboxPrimitive.Positioner.Props,
98
+ 'side' | 'align' | 'sideOffset' | 'alignOffset' | 'anchor'
99
+ >) {
100
+ return (
101
+ <ComboboxPrimitive.Portal>
102
+ <ComboboxPrimitive.Positioner
103
+ side={side}
104
+ sideOffset={sideOffset}
105
+ align={align}
106
+ alignOffset={alignOffset}
107
+ anchor={anchor}
108
+ className='isolate z-50'
109
+ >
110
+ <ComboboxPrimitive.Popup
111
+ data-slot='combobox-content'
112
+ data-chips={!!anchor}
113
+ className={cn(
114
+ 'group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
115
+ className,
116
+ )}
117
+ {...props}
118
+ />
119
+ </ComboboxPrimitive.Positioner>
120
+ </ComboboxPrimitive.Portal>
121
+ );
122
+ }
123
+
124
+ function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
125
+ return (
126
+ <ComboboxPrimitive.List
127
+ data-slot='combobox-list'
128
+ className={cn(
129
+ 'no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0',
130
+ className,
131
+ )}
132
+ {...props}
133
+ />
134
+ );
135
+ }
136
+
137
+ function ComboboxItem({
138
+ className,
139
+ children,
140
+ ...props
141
+ }: ComboboxPrimitive.Item.Props) {
142
+ return (
143
+ <ComboboxPrimitive.Item
144
+ data-slot='combobox-item'
145
+ className={cn(
146
+ "relative flex w-full cursor-default items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
147
+ className,
148
+ )}
149
+ {...props}
150
+ >
151
+ {children}
152
+ <ComboboxPrimitive.ItemIndicator
153
+ render={
154
+ <span className='pointer-events-none absolute right-2 flex size-4 items-center justify-center' />
155
+ }
156
+ >
157
+ <CheckIcon className='pointer-events-none' />
158
+ </ComboboxPrimitive.ItemIndicator>
159
+ </ComboboxPrimitive.Item>
160
+ );
161
+ }
162
+
163
+ function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
164
+ return (
165
+ <ComboboxPrimitive.Group
166
+ data-slot='combobox-group'
167
+ className={cn(className)}
168
+ {...props}
169
+ />
170
+ );
171
+ }
172
+
173
+ function ComboboxLabel({
174
+ className,
175
+ ...props
176
+ }: ComboboxPrimitive.GroupLabel.Props) {
177
+ return (
178
+ <ComboboxPrimitive.GroupLabel
179
+ data-slot='combobox-label'
180
+ className={cn('px-2 py-1.5 text-xs text-muted-foreground', className)}
181
+ {...props}
182
+ />
183
+ );
184
+ }
185
+
186
+ function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
187
+ return (
188
+ <ComboboxPrimitive.Collection data-slot='combobox-collection' {...props} />
189
+ );
190
+ }
191
+
192
+ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
193
+ return (
194
+ <ComboboxPrimitive.Empty
195
+ data-slot='combobox-empty'
196
+ className={cn(
197
+ 'hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex',
198
+ className,
199
+ )}
200
+ {...props}
201
+ />
202
+ );
203
+ }
204
+
205
+ function ComboboxSeparator({
206
+ className,
207
+ ...props
208
+ }: ComboboxPrimitive.Separator.Props) {
209
+ return (
210
+ <ComboboxPrimitive.Separator
211
+ data-slot='combobox-separator'
212
+ className={cn('-mx-1 my-1 h-px bg-border', className)}
213
+ {...props}
214
+ />
215
+ );
216
+ }
217
+
218
+ function ComboboxChips({
219
+ className,
220
+ ...props
221
+ }: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
222
+ ComboboxPrimitive.Chips.Props) {
223
+ return (
224
+ <ComboboxPrimitive.Chips
225
+ data-slot='combobox-chips'
226
+ className={cn(
227
+ 'flex min-h-8 flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40',
228
+ className,
229
+ )}
230
+ {...props}
231
+ />
232
+ );
233
+ }
234
+
235
+ function ComboboxChip({
236
+ className,
237
+ children,
238
+ showRemove = true,
239
+ ...props
240
+ }: ComboboxPrimitive.Chip.Props & {
241
+ showRemove?: boolean;
242
+ }) {
243
+ return (
244
+ <ComboboxPrimitive.Chip
245
+ data-slot='combobox-chip'
246
+ className={cn(
247
+ 'flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0',
248
+ className,
249
+ )}
250
+ {...props}
251
+ >
252
+ {children}
253
+ {showRemove && (
254
+ <ComboboxPrimitive.ChipRemove
255
+ render={<Button variant='ghost' size='icon-xs' />}
256
+ className='-ml-1 opacity-50 hover:opacity-100'
257
+ data-slot='combobox-chip-remove'
258
+ >
259
+ <XIcon className='pointer-events-none' />
260
+ </ComboboxPrimitive.ChipRemove>
261
+ )}
262
+ </ComboboxPrimitive.Chip>
263
+ );
264
+ }
265
+
266
+ function ComboboxChipsInput({
267
+ className,
268
+ ...props
269
+ }: ComboboxPrimitive.Input.Props) {
270
+ return (
271
+ <ComboboxPrimitive.Input
272
+ data-slot='combobox-chip-input'
273
+ className={cn('min-w-16 flex-1 outline-none', className)}
274
+ {...props}
275
+ />
276
+ );
277
+ }
278
+
279
+ function useComboboxAnchor() {
280
+ return React.useRef<HTMLDivElement | null>(null);
281
+ }
282
+
283
+ export {
284
+ Combobox,
285
+ ComboboxInput,
286
+ ComboboxContent,
287
+ ComboboxList,
288
+ ComboboxItem,
289
+ ComboboxGroup,
290
+ ComboboxLabel,
291
+ ComboboxCollection,
292
+ ComboboxEmpty,
293
+ ComboboxSeparator,
294
+ ComboboxChips,
295
+ ComboboxChip,
296
+ ComboboxChipsInput,
297
+ ComboboxTrigger,
298
+ ComboboxValue,
299
+ useComboboxAnchor,
300
+ };
@@ -63,8 +63,10 @@ The application owns authentication, initial administrator grants, navigation an
63
63
 
64
64
  ## Standard diagram and contextual configuration
65
65
 
66
- Current application templates already contain these parts and the standard React Flow/ELK components. Reuse the installed `LifecycleDiagram` and preserve its default visuals unless the user explicitly asks to customize them. UI Library entries are shadcn registry source, not npm packages; use `shadcn view/add`, never an npm 404 as a fallback decision.
66
+ Current application templates already contain these parts and the standard React Flow/ELK components. Reuse the installed `LifecycleDiagram`; the installable demo opts into `presentation="process"` for compact boundary pills and work cards in both definitions and executions. Supply application-owned `stateKind` semantics and keep configuration and history callbacks intact. Omit `presentation` to retain the classic visuals. UI Library entries are shadcn registry source, not npm packages; use `shadcn view/add`, never an npm 404 as a fallback decision.
67
67
 
68
68
  Connect `LifecycleDiagram.configuration` to the application's existing authorized parameter editor. Native `parameterConfiguration` bindings supply the exact state/transition fields; the component automatically renders bound state controls and sends bound transition selections to `onConfigure`. `canConfigure` is mandatory and controls the affordance only, never server security. The application editor reads current values/revision, preserves other group fields, validates and saves the complete authorized group with CAS, and refreshes the diagram summary from persisted data. Avoid replacing a group with the callback's filtered field subset. Keep historical snapshots read-only.
69
69
 
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
+
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.
@@ -11,7 +11,7 @@ import { DataTable } from '#components/data-table/data-table';
11
11
 
12
12
  export interface WorkflowDefinitionRow {
13
13
  id: string;
14
- name: string;
14
+ name?: string;
15
15
  title: ReactNode;
16
16
  activity: ReactNode;
17
17
  }
@@ -30,7 +30,7 @@ export interface WorkflowDefinitionListProps {
30
30
  executionActivity: string;
31
31
  };
32
32
  sort?: { ascending: boolean };
33
- onSort: () => void;
33
+ onSort?: () => void;
34
34
  onRefresh: () => void;
35
35
  }
36
36
 
@@ -91,7 +91,7 @@ export function WorkflowDefinitionList({
91
91
  {
92
92
  id: 'activity',
93
93
  accessorFn: (row) => row.activity,
94
- enableSorting: true,
94
+ enableSorting: Boolean(onSort),
95
95
  meta: activityMeta,
96
96
  header: DefinitionActivityHeader,
97
97
  cell: DefinitionActivityCell,
@@ -118,7 +118,9 @@ function DefinitionTitleCell({
118
118
  return (
119
119
  <>
120
120
  {row.original.title}
121
- <p className='text-xs text-muted-foreground'>{row.original.name}</p>
121
+ {row.original.name && row.original.name !== row.original.title && (
122
+ <p className='text-xs text-muted-foreground'>{row.original.name}</p>
123
+ )}
122
124
  </>
123
125
  );
124
126
  }
@@ -134,15 +136,19 @@ function DefinitionActivityHeader({
134
136
  .meta as DefinitionActivityMeta;
135
137
  return (
136
138
  <div className='text-right'>
137
- <Button
138
- variant='ghost'
139
- size='sm'
140
- title={labels.executionCount}
141
- onClick={onSort}
142
- >
143
- {labels.executionActivity}
144
- {sort && (sort.ascending ? <ArrowUp /> : <ArrowDown />)}
145
- </Button>
139
+ {onSort ? (
140
+ <Button
141
+ variant='ghost'
142
+ size='sm'
143
+ title={labels.executionCount}
144
+ onClick={onSort}
145
+ >
146
+ {labels.executionActivity}
147
+ {sort && (sort.ascending ? <ArrowUp /> : <ArrowDown />)}
148
+ </Button>
149
+ ) : (
150
+ <span>{labels.executionActivity}</span>
151
+ )}
146
152
  </div>
147
153
  );
148
154
  }
@@ -68,7 +68,7 @@ export function WorkflowDefinitionDetail({
68
68
  className={
69
69
  parameters == null
70
70
  ? 'min-w-0'
71
- : 'grid min-w-0 grid-cols-1 items-start gap-6 lg:grid-cols-2'
71
+ : 'grid min-w-0 grid-cols-1 items-start gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]'
72
72
  }
73
73
  >
74
74
  <Card className='min-w-0' role='region' aria-label={diagramTitle}>