@docyrus/docyrus 0.0.66 → 0.0.68
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/main.js +136 -136
- package/main.js.map +3 -3
- package/package.json +3 -3
- package/resources/pi-agent/extensions/context.ts +22 -16
- package/resources/pi-agent/extensions/control.ts +43 -27
- package/resources/pi-agent/extensions/knowledge.ts +42 -16
- package/resources/pi-agent/extensions/loop.ts +44 -27
- package/resources/pi-agent/extensions/pi-bash-live-view/widget.ts +2 -17
- package/resources/pi-agent/extensions/plan.ts +79 -40
- package/resources/pi-agent/extensions/prompt-url-widget.ts +39 -18
- package/resources/pi-agent/extensions/review.ts +13 -2
- package/resources/pi-agent/shared/extensionLifecycle.ts +11 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/component-selection-guide.md +42 -5
- package/resources/pi-agent/skills/docyrus-data-grid-page-design/SKILL.md +58 -0
- package/resources/pi-agent/skills/docyrus-data-grid-page-design/references/advanced-saved-view-query-patterns.md +158 -0
- package/resources/pi-agent/skills/docyrus-data-grid-page-design/references/hook-pages.md +183 -0
- package/resources/pi-agent/skills/docyrus-data-grid-page-design/references/manual-pages.md +145 -0
- package/resources/pi-agent/skills/docyrus-data-grid-page-design/references/tenant-and-users-providers.md +290 -0
- package/resources/pi-agent/skills/docyrus-record-detail-form-design/SKILL.md +54 -0
- package/resources/pi-agent/skills/docyrus-record-detail-form-design/references/advanced-inline-edit-and-renderers.md +140 -0
- package/resources/pi-agent/skills/docyrus-record-detail-form-design/references/field-type-mapping-and-fallbacks.md +150 -0
- package/resources/pi-agent/skills/docyrus-record-detail-form-design/references/hook-form-view.md +127 -0
- package/resources/pi-agent/skills/docyrus-record-detail-form-design/references/manual-form-detail-patterns.md +125 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Manual DataGrid Page Patterns
|
|
2
|
+
|
|
3
|
+
## Use this path when
|
|
4
|
+
|
|
5
|
+
- The page uses local/demo data.
|
|
6
|
+
- You need a custom toolbar layout.
|
|
7
|
+
- You want backend-saved views but your own row-query lifecycle.
|
|
8
|
+
- You only need local views and not Docyrus view persistence.
|
|
9
|
+
|
|
10
|
+
## Local views with full manual composition
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
'use client';
|
|
14
|
+
|
|
15
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
DataGrid,
|
|
19
|
+
DataGridFilterMenu,
|
|
20
|
+
DataGridGroupMenu,
|
|
21
|
+
DataGridRowHeightMenu,
|
|
22
|
+
DataGridSortMenu,
|
|
23
|
+
applyViewToTable,
|
|
24
|
+
getDataGridActionsColumn,
|
|
25
|
+
getDataGridSelectColumn,
|
|
26
|
+
useDataGrid,
|
|
27
|
+
type ColumnDef,
|
|
28
|
+
type SavedDataGridView
|
|
29
|
+
} from '@docyrus/ui/components/data-grid';
|
|
30
|
+
import { DataGridViewSelect } from '@docyrus/ui/components/data-grid-view-select';
|
|
31
|
+
|
|
32
|
+
const columns: ColumnDef<Row>[] = [
|
|
33
|
+
getDataGridSelectColumn<Row>(),
|
|
34
|
+
getDataGridActionsColumn<Row>({ cell: ({ row }) => <OpenButton row={row.original} /> }),
|
|
35
|
+
...dataColumns
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export function TasksPage() {
|
|
39
|
+
const [views, setViews] = useState<SavedDataGridView[]>(INITIAL_VIEWS);
|
|
40
|
+
const [activeViewId, setActiveViewId] = useState('all');
|
|
41
|
+
const activeView = useMemo(() => views.find(view => view.id === activeViewId), [views, activeViewId]);
|
|
42
|
+
|
|
43
|
+
const { table, ...gridProps } = useDataGrid<Row>({
|
|
44
|
+
data: rows,
|
|
45
|
+
columns,
|
|
46
|
+
enableSearch: true,
|
|
47
|
+
enableGrouping: true
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!activeView) return;
|
|
52
|
+
applyViewToTable(table, activeView);
|
|
53
|
+
}, [table, activeView]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="flex h-full flex-col gap-4 overflow-hidden">
|
|
57
|
+
<div className="shrink-0 flex items-center gap-2">
|
|
58
|
+
<DataGridViewSelect
|
|
59
|
+
table={table}
|
|
60
|
+
variant="horizontal-tabs"
|
|
61
|
+
editable
|
|
62
|
+
views={views}
|
|
63
|
+
activeViewId={activeViewId}
|
|
64
|
+
onViewChange={(view) => setActiveViewId(view.id)}
|
|
65
|
+
onViewSave={(view) => setViews(prev => prev.map(item => item.id === view.id ? view : item))}
|
|
66
|
+
onViewCreate={(view) => setViews(prev => [...prev, view])}
|
|
67
|
+
onViewDelete={(viewId) => setViews(prev => prev.filter(item => item.id !== viewId))} />
|
|
68
|
+
<DataGridFilterMenu table={table} />
|
|
69
|
+
<DataGridGroupMenu table={table} />
|
|
70
|
+
<DataGridSortMenu table={table} />
|
|
71
|
+
<DataGridRowHeightMenu table={table} />
|
|
72
|
+
</div>
|
|
73
|
+
<div className="min-h-0 flex-1">
|
|
74
|
+
<DataGrid table={table} {...gridProps} height="auto" />
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`DataGridViewSelect` applies views automatically when the user clicks a different tab. The `useEffect` above is still needed for the initial/default view.
|
|
82
|
+
|
|
83
|
+
## Backend saved views with custom row fetching
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
const viewSelect = useDocyrusDataViewSelect({
|
|
87
|
+
client,
|
|
88
|
+
appSlug: 'crm',
|
|
89
|
+
dataSourceSlug: 'contacts',
|
|
90
|
+
defaultRowGroupingColumn: 'status',
|
|
91
|
+
systemViews: [ALL_CONTACTS_VIEW]
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const activeView = useMemo(
|
|
95
|
+
() => viewSelect.views.find(view => view.id === viewSelect.activeViewId),
|
|
96
|
+
[viewSelect.views, viewSelect.activeViewId]
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const { table, ...gridProps } = useDataGrid<Row>({ data: rows, columns, enableGrouping: true });
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!activeView) return;
|
|
103
|
+
applyViewToTable(table, activeView);
|
|
104
|
+
}, [table, activeView]);
|
|
105
|
+
|
|
106
|
+
<DataGridViewSelect table={table} editable {...viewSelect.gridViewSelectProps} />
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Important: `useDocyrusDataViewSelect` gives you `views`, `fields`, `activeViewId`, CRUD callbacks, hidden-view state, and persistence. It does **not** fetch rows. If the active view should also shape the backend query, translate `activeView.columnVisibility`, `columnOrder`, `sorting`, and `filterQuery` into your request yourself or switch to `useDocyrusDataGrid`.
|
|
110
|
+
|
|
111
|
+
## Manual toolbar building blocks
|
|
112
|
+
|
|
113
|
+
Use these when you do not want the prebuilt hook toolbar:
|
|
114
|
+
|
|
115
|
+
- `DataGridViewSelect`
|
|
116
|
+
- `DataGridFilterMenu`
|
|
117
|
+
- `DataGridSortMenu`
|
|
118
|
+
- `DataGridGroupMenu`
|
|
119
|
+
- `DataGridRowHeightMenu`
|
|
120
|
+
- `DataGridDisplayMenu`
|
|
121
|
+
- `DataGridViewMenu`
|
|
122
|
+
- your own search input wired to `table.setGlobalFilter(...)` or to backend query state
|
|
123
|
+
|
|
124
|
+
For persistence, system views, paging ownership, or manual translation of the active view into Docyrus query params, also read `advanced-saved-view-query-patterns.md`.
|
|
125
|
+
|
|
126
|
+
## What you lose by going manual
|
|
127
|
+
|
|
128
|
+
`useDocyrusDataGrid` does several things automatically that manual mode does **not**:
|
|
129
|
+
|
|
130
|
+
- **Auto-`expand`** for reference fields. In manual mode you must add reference field slugs to your request's `expand` array yourself, or rich cells (user / relation / select / status) will only see bare IDs.
|
|
131
|
+
- **Reserved-column pinning.** `select` and `actions` are not auto-pinned to the left in manual mode — call `table.setColumnPinning({ left: ['select', 'actions'], right: [] })` after applying the active view.
|
|
132
|
+
- **Filter menu refetch wiring.** `DataGridFilterMenu` writes through to `table.setColumnFilters`, but a manual page's row query doesn't watch that state. You need to read `table.getState().columnFilters` (or lift the state into the host) and translate the filter values into your request's `filters` payload. Reset `pageIndex` to 0 on changes.
|
|
133
|
+
- **Async option search for relation/user filter columns.** The filter menu will show empty option lists unless you pass a `getAsyncOptions` resolver (signature: `(column) => AsyncOptionsConfig | undefined`) that calls `/v1/users` for user fields and `/v1/apps/{appSlug}/data-sources/{slug}/items` for relations.
|
|
134
|
+
- **Tenant-aware formatters.** Pass `formatDate`, `formatDateTime`, `formatNumber` through `useDataGrid({ meta: { ... } })`. Cells read them from `tableMeta`.
|
|
135
|
+
- **Shared users list.** Pass `users: ReadonlyArray<CellUserOption>` to `buildTanstackColumnDef({ users })` (or set the `cell.options` directly via `mapColumn`) so user cells get avatar + label from the global list.
|
|
136
|
+
|
|
137
|
+
If any of these matter to the page, prefer `useDocyrusDataGrid` and use `mapColumn` / `extraColumns` / toolbar enable flags to customize.
|
|
138
|
+
|
|
139
|
+
## Gotchas
|
|
140
|
+
|
|
141
|
+
- Local/manual `SavedDataGridView` objects can include reserved columns like `select` and `actions` in `columnOrder`.
|
|
142
|
+
- Backend Docyrus-saved views store real field slugs only. If you want the standard reserved-column behavior, `useDocyrusDataGrid` is the safest path.
|
|
143
|
+
- Pass `fields` into `DataGridViewSelect` when you want the filter builder inside the editor.
|
|
144
|
+
- Pass `isSaving` and `isLoading` when the host owns async view CRUD.
|
|
145
|
+
- For manual Docyrus item requests, always send `columns`. Add reference-type field slugs (`field-userSelect`, `field-userMultiSelect`, `field-relation`, `field-relatedField`, `field-select`, `field-radioGroup`, `field-enum`, `field-systemEnum`, `field-multiSelect`, `field-tagSelect`, `field-status`, `field-approvalStatus`) to `expand` so the API returns `{ id, name, ... }` payloads.
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# Tenant Preferences and Shared Users Providers
|
|
2
|
+
|
|
3
|
+
`useDocyrusDataGrid` accepts two tenant-scoped inputs that should be sourced from app-level providers, not refetched per page:
|
|
4
|
+
|
|
5
|
+
- **Tenant preferences** → drives `formatDate`, `formatDateTime`, `formatNumber` so date and money cells respect regional settings.
|
|
6
|
+
- **Shared users list** → seeds `field-userSelect` / `field-userMultiSelect` cell options so avatar + name render instantly without per-row fetches.
|
|
7
|
+
|
|
8
|
+
Both should be fetched **once** at app boot and re-used everywhere.
|
|
9
|
+
|
|
10
|
+
## Tenant preferences provider
|
|
11
|
+
|
|
12
|
+
Source the utilities from `@docyrus/app-utils`:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import {
|
|
16
|
+
getTenantPreferences,
|
|
17
|
+
createDateUtils,
|
|
18
|
+
createNumberUtils,
|
|
19
|
+
type DateUtils,
|
|
20
|
+
type NumberUtils,
|
|
21
|
+
type TenantPreferences
|
|
22
|
+
} from '@docyrus/app-utils';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Provider shape
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
'use client';
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
createContext, useContext, useMemo, type ReactNode
|
|
32
|
+
} from 'react';
|
|
33
|
+
|
|
34
|
+
import { useDocyrusAuth } from '@docyrus/signin';
|
|
35
|
+
import { useQuery } from '@tanstack/react-query';
|
|
36
|
+
|
|
37
|
+
interface TenantContextValue {
|
|
38
|
+
preferences: TenantPreferences | null;
|
|
39
|
+
dateUtils: DateUtils | null;
|
|
40
|
+
numberUtils: NumberUtils | null;
|
|
41
|
+
isLoading: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const TenantContext = createContext<TenantContextValue>({
|
|
45
|
+
preferences: null,
|
|
46
|
+
dateUtils: null,
|
|
47
|
+
numberUtils: null,
|
|
48
|
+
isLoading: false
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export function TenantProvider({ children }: { children: ReactNode }) {
|
|
52
|
+
const { client, status, user } = useDocyrusAuth();
|
|
53
|
+
const userTimezone = (user as { timeZone?: { id?: string } } | null)?.timeZone?.id;
|
|
54
|
+
|
|
55
|
+
const query = useQuery<TenantPreferences>({
|
|
56
|
+
queryKey: ['tenant', 'preferences'],
|
|
57
|
+
queryFn: () => getTenantPreferences(client!),
|
|
58
|
+
enabled: status === 'authenticated' && Boolean(client),
|
|
59
|
+
staleTime: 30 * 60_000
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const value = useMemo<TenantContextValue>(() => {
|
|
63
|
+
const preferences = query.data ?? null;
|
|
64
|
+
|
|
65
|
+
if (!preferences) {
|
|
66
|
+
return {
|
|
67
|
+
preferences: null, dateUtils: null, numberUtils: null, isLoading: query.isLoading
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
preferences,
|
|
73
|
+
dateUtils: createDateUtils({ preferences, userTimezone }),
|
|
74
|
+
numberUtils: createNumberUtils({ preferences }),
|
|
75
|
+
isLoading: query.isLoading
|
|
76
|
+
};
|
|
77
|
+
}, [query.data, query.isLoading, userTimezone]);
|
|
78
|
+
|
|
79
|
+
return <TenantContext.Provider value={value}>{children}</TenantContext.Provider>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function useTenant() {
|
|
83
|
+
return useContext(TenantContext);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Wrapping for the data-grid hook
|
|
88
|
+
|
|
89
|
+
The grid hook expects three callable formatters. Wrap the utils once and expose them via a small hook:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
export function useGridFormatters() {
|
|
93
|
+
const { dateUtils, numberUtils } = useTenant();
|
|
94
|
+
|
|
95
|
+
return useMemo(() => {
|
|
96
|
+
const formatDate = (value: unknown): string => {
|
|
97
|
+
if (value == null) return '';
|
|
98
|
+
if (!dateUtils) return String(value);
|
|
99
|
+
try { return dateUtils.formatDate(value as string | Date) ?? ''; } catch { return String(value); }
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const formatDateTime = (value: unknown): string => {
|
|
103
|
+
if (value == null) return '';
|
|
104
|
+
if (!dateUtils) return String(value);
|
|
105
|
+
try { return dateUtils.formatDateTime(value as string | Date) ?? ''; } catch { return String(value); }
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const formatNumber = (
|
|
109
|
+
value: unknown,
|
|
110
|
+
opts?: { variant?: 'number' | 'currency' | 'percent'; currency?: string }
|
|
111
|
+
): string => {
|
|
112
|
+
if (value == null || value === '') return '';
|
|
113
|
+
|
|
114
|
+
const numeric = typeof value === 'number' ? value : Number(value);
|
|
115
|
+
|
|
116
|
+
if (Number.isNaN(numeric)) return String(value);
|
|
117
|
+
if (!numberUtils) return String(value);
|
|
118
|
+
|
|
119
|
+
const variant = opts?.variant ?? 'number';
|
|
120
|
+
const base = numberUtils.formatNumber(variant === 'percent' ? numeric * 100 : numeric) ?? String(numeric);
|
|
121
|
+
|
|
122
|
+
if (variant === 'currency' && opts?.currency) return `${base} ${opts.currency}`;
|
|
123
|
+
if (variant === 'percent') return `${base}%`;
|
|
124
|
+
|
|
125
|
+
return base;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return { formatDate, formatDateTime, formatNumber };
|
|
129
|
+
}, [dateUtils, numberUtils]);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Note the `formatNumber` wrapper handles the variant semantics:
|
|
134
|
+
|
|
135
|
+
- `'percent'` multiplies by 100 and appends `%`.
|
|
136
|
+
- `'currency'` appends the currency code provided in the cell meta.
|
|
137
|
+
- Plain `'number'` goes straight through `numberUtils.formatNumber`.
|
|
138
|
+
|
|
139
|
+
If the consuming app uses different conventions (e.g. always show a currency symbol), customize the wrapper accordingly.
|
|
140
|
+
|
|
141
|
+
## Shared users provider
|
|
142
|
+
|
|
143
|
+
`useDocyrusDataGrid` accepts `users: ReadonlyArray<CellUserOption>` (`{ value, label, avatarUrl?, initials? }`). Populate it from `/v1/users`:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
'use client';
|
|
147
|
+
|
|
148
|
+
import {
|
|
149
|
+
createContext, useContext, useMemo, type ReactNode
|
|
150
|
+
} from 'react';
|
|
151
|
+
|
|
152
|
+
import { useDocyrusAuth } from '@docyrus/signin';
|
|
153
|
+
import { useQuery } from '@tanstack/react-query';
|
|
154
|
+
|
|
155
|
+
import { type CellUserOption } from '@docyrus/ui/components/data-grid';
|
|
156
|
+
|
|
157
|
+
interface UserRecord {
|
|
158
|
+
id: string;
|
|
159
|
+
firstname?: string | null;
|
|
160
|
+
lastname?: string | null;
|
|
161
|
+
email?: string | null;
|
|
162
|
+
photo?: string | null;
|
|
163
|
+
[key: string]: unknown;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const UsersContext = createContext<{ users: ReadonlyArray<CellUserOption>; records: ReadonlyArray<UserRecord>; isLoading: boolean }>({
|
|
167
|
+
users: [], records: [], isLoading: false
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
function pickString(record: Record<string, unknown>, ...keys: Array<string>): string | null {
|
|
171
|
+
for (const key of keys) {
|
|
172
|
+
const value = record[key];
|
|
173
|
+
|
|
174
|
+
if (typeof value === 'string' && value.length > 0) return value;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function toUserOption(record: UserRecord): CellUserOption | null {
|
|
181
|
+
if (!record.id) return null;
|
|
182
|
+
|
|
183
|
+
const firstName = pickString(record, 'firstname', 'firstName', 'first_name');
|
|
184
|
+
const lastName = pickString(record, 'lastname', 'lastName', 'last_name');
|
|
185
|
+
const fullName = firstName ? lastName ? `${firstName} ${lastName}` : firstName : lastName;
|
|
186
|
+
const email = pickString(record, 'email');
|
|
187
|
+
const label = fullName ?? email ?? record.id;
|
|
188
|
+
const avatarUrl = pickString(record, 'photo', 'avatar_url', 'avatarUrl', 'profile_image_url', 'profileImageUrl') ?? undefined;
|
|
189
|
+
|
|
190
|
+
return { value: record.id, label, ...(avatarUrl ? { avatarUrl } : {}) };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function UsersProvider({ children }: { children: ReactNode }) {
|
|
194
|
+
const { client, status } = useDocyrusAuth();
|
|
195
|
+
|
|
196
|
+
const query = useQuery<Array<UserRecord>>({
|
|
197
|
+
queryKey: ['users', 'all'],
|
|
198
|
+
queryFn: async () => {
|
|
199
|
+
const response = await client!.get<Array<UserRecord> | { data?: Array<UserRecord> }>(
|
|
200
|
+
'/v1/users',
|
|
201
|
+
{ limit: 1000 }
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return Array.isArray(response) ? response : (response.data ?? []);
|
|
205
|
+
},
|
|
206
|
+
enabled: status === 'authenticated' && Boolean(client),
|
|
207
|
+
staleTime: 5 * 60_000
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const value = useMemo(() => {
|
|
211
|
+
const records = query.data ?? [];
|
|
212
|
+
const users = records
|
|
213
|
+
.map(toUserOption)
|
|
214
|
+
.filter((option): option is CellUserOption => option !== null);
|
|
215
|
+
|
|
216
|
+
return { users, records, isLoading: query.isLoading };
|
|
217
|
+
}, [query.data, query.isLoading]);
|
|
218
|
+
|
|
219
|
+
return <UsersContext.Provider value={value}>{children}</UsersContext.Provider>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function useUsers() {
|
|
223
|
+
return useContext(UsersContext);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### User record field name fallbacks
|
|
228
|
+
|
|
229
|
+
The Docyrus `/v1/users` API returns `firstname` / `lastname` (lowercase, no separator) plus `email` and `photo`. The provider also accepts the common camelCase / snake_case aliases as fallbacks so the same code works against tenants that have customized the user schema.
|
|
230
|
+
|
|
231
|
+
## Wiring providers into the app shell
|
|
232
|
+
|
|
233
|
+
Place the providers inside the auth + query providers and outside the routes:
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
<AuthProvider>
|
|
237
|
+
<QueryProvider>
|
|
238
|
+
<DevtoolsProvider>
|
|
239
|
+
<TenantProvider>
|
|
240
|
+
<UsersProvider>
|
|
241
|
+
<Routes>{/* ... */}</Routes>
|
|
242
|
+
</UsersProvider>
|
|
243
|
+
</TenantProvider>
|
|
244
|
+
</DevtoolsProvider>
|
|
245
|
+
</QueryProvider>
|
|
246
|
+
</AuthProvider>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Order matters:
|
|
250
|
+
|
|
251
|
+
- `AuthProvider` must wrap both because the providers need an authenticated `client` to fetch.
|
|
252
|
+
- `QueryProvider` must wrap both because they use `useQuery`.
|
|
253
|
+
- `TenantProvider` and `UsersProvider` are siblings; either order is fine.
|
|
254
|
+
|
|
255
|
+
## Wiring providers into a grid page
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
const { users } = useUsers();
|
|
259
|
+
const { formatDate, formatDateTime, formatNumber } = useGridFormatters();
|
|
260
|
+
|
|
261
|
+
const { table, gridProps, toolbar } = useDocyrusDataGrid<Row>({
|
|
262
|
+
client,
|
|
263
|
+
appSlug,
|
|
264
|
+
dataSourceSlug,
|
|
265
|
+
users,
|
|
266
|
+
formatDate,
|
|
267
|
+
formatDateTime,
|
|
268
|
+
formatNumber,
|
|
269
|
+
// ... other options
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The grid hook routes:
|
|
274
|
+
|
|
275
|
+
- `formatDate` / `formatDateTime` / `formatNumber` into TanStack `tableMeta`. `DateCell`, `DateTimeCell`, `NumberCell` (and its currency / percent variants) read these via `tableMeta?.formatDate` etc. and prefer them over their built-in fallbacks.
|
|
276
|
+
- `users` into the cell-options builder for `field-userSelect` and `field-userMultiSelect` columns. The cells render avatar + label from this list immediately. Rows pointing at users not in the list still render correctly via the expanded-payload fallback.
|
|
277
|
+
|
|
278
|
+
## Why centralize these
|
|
279
|
+
|
|
280
|
+
- Single network round-trip per session for tenant prefs and the user roster.
|
|
281
|
+
- Consistent formatting and avatar display across every grid, form, and dialog in the app.
|
|
282
|
+
- Pages don't need to know about formatters or user fetching — they just consume context.
|
|
283
|
+
- Tenant settings change rarely (long staleTime); user roster also changes slowly (5 min staleTime is a safe default).
|
|
284
|
+
|
|
285
|
+
## Debug checklist
|
|
286
|
+
|
|
287
|
+
- **Dates / numbers showing as raw values?** Tenant query likely failed or hasn't resolved yet. Check `useTenant().isLoading` and the network tab for `getTenantPreferences`.
|
|
288
|
+
- **User cells show "Unassigned" instead of names?** Either `users` wasn't passed in, or the row's user payload didn't expand. Verify `useDocyrusDataGrid` sees both the global `users` list (via context) and that the user column slug appears in `expand` (auto-added; check `resolvedListParams.expand`).
|
|
289
|
+
- **Currency shows "1.234,50 USD" but you wanted "$1,234.50"?** Customize the `formatNumber` wrapper for that variant. The default is locale-formatted number + currency code suffix.
|
|
290
|
+
- **Avatars missing?** The provider reads `photo` first, then `avatar_url` / `avatarUrl` / `profile_image_url` / `profileImageUrl`. Confirm the API returns one of those fields.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: docyrus-record-detail-form-design
|
|
3
|
+
description: Build Docyrus React record forms, detail sheets, inspector panels, and inline-edit record UIs with `useDocyrusFormView`, `DynamicFormField`, `DynamicValue`, `EditableRecordDetail`, `EditableValue`, and value renderers. Use when asked to create or refactor create/edit/view pages, modal or sheet record forms, click-to-edit detail panels, metadata-driven field layouts, manual read-only record summaries, or field-type mapping logic that must choose correctly between form inputs, inline editors, and value-renderer fallbacks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Docyrus Record Detail Form Design
|
|
7
|
+
|
|
8
|
+
Build full Docyrus record create, edit, and detail experiences around shared field metadata.
|
|
9
|
+
|
|
10
|
+
## Choose the build mode
|
|
11
|
+
|
|
12
|
+
1. **Standard create/edit/view record screen** → use `useDocyrusFormView`.
|
|
13
|
+
- Best when the page is a normal Docyrus form or detail surface.
|
|
14
|
+
- Handles metadata loading, item loading, option hydration, field rendering, and submit behavior.
|
|
15
|
+
- Read `references/hook-form-view.md`.
|
|
16
|
+
|
|
17
|
+
2. **Custom manual form or detail layout** → use `DynamicFormField`, `DynamicValue`, or `useDocyrusFieldComponent`.
|
|
18
|
+
- Best when the page already owns form state, layout, or data fetching.
|
|
19
|
+
- Read `references/manual-form-detail-patterns.md`.
|
|
20
|
+
|
|
21
|
+
3. **Inline editing, click-to-edit detail rows, or field-by-field editing** → use `EditableRecordDetail` and `EditableValue`.
|
|
22
|
+
- Best when users should edit a record in place without switching to a full form.
|
|
23
|
+
- Read `references/advanced-inline-edit-and-renderers.md`.
|
|
24
|
+
|
|
25
|
+
4. **Complex query/schema/API work** → also load `docyrus-api-dev` or `docyrus-app-dev-react`.
|
|
26
|
+
5. **Field-type mapping or unsupported-field decisions** → read `references/field-type-mapping-and-fallbacks.md`.
|
|
27
|
+
|
|
28
|
+
## Default workflow
|
|
29
|
+
|
|
30
|
+
1. Confirm `appSlug`, `dataSourceSlug`, `mode`, and `itemId` if the record already exists.
|
|
31
|
+
2. Decide whether the page should be hook-first or fully manual.
|
|
32
|
+
3. Keep editable fields and read-only renderers on the same field-type registry.
|
|
33
|
+
4. Hydrate enum, user, and relation options before declaring the page done.
|
|
34
|
+
5. Verify create, edit, view, reset, and save behavior.
|
|
35
|
+
6. If the page supports inline editing, verify companion-field saves for composite types such as money, phone, and status.
|
|
36
|
+
|
|
37
|
+
## Non-negotiables
|
|
38
|
+
|
|
39
|
+
- Prefer `useDocyrusFormView` for standard create/edit/view pages.
|
|
40
|
+
- Manual editable layouts should use `DynamicFormField`, specific form-field components, or `useDocyrusFieldComponent(..., 'form-field')`.
|
|
41
|
+
- Manual read-only layouts should use `DynamicValue`, specific value renderers, or `useDocyrusFieldComponent(..., 'value-renderer')`.
|
|
42
|
+
- Do not invent parallel per-field switch statements when the shared registry already solves the dispatch.
|
|
43
|
+
- `useDocyrusFormView` already keeps form fields, value renderers, and inline detail mode aligned through `useDocyrusFieldComponent`.
|
|
44
|
+
- `clickToEdit` on `useDocyrusFormView` view layouts routes through `EditableRecordDetail`; use it when you want view-first UX with inline saves.
|
|
45
|
+
- Manual Docyrus item fetching must always send `columns`. `useDocyrusFormView` derives them automatically, including companion columns.
|
|
46
|
+
- Composite fields can require companion keys on read and write: money, phone, status, avatar, and similar types must stay intact in manual save flows.
|
|
47
|
+
- If a field has no editable component, create-mode UX should usually skip it; edit/view UX should usually fall back to a value renderer unless you have a better explicit design.
|
|
48
|
+
|
|
49
|
+
## References
|
|
50
|
+
|
|
51
|
+
- `references/hook-form-view.md` — hook-first create, edit, view, layout, submit, and click-to-edit patterns.
|
|
52
|
+
- `references/manual-form-detail-patterns.md` — manual form and detail composition with field components, renderers, and shared field maps.
|
|
53
|
+
- `references/advanced-inline-edit-and-renderers.md` — `EditableRecordDetail`, `EditableValue`, renderer selection rules, and companion-field save patterns.
|
|
54
|
+
- `references/field-type-mapping-and-fallbacks.md` — shared field maps, fallback rules, and unsupported-field strategy by render mode.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Advanced Inline Editing and Renderer Patterns
|
|
2
|
+
|
|
3
|
+
## Read this when
|
|
4
|
+
|
|
5
|
+
- The page should edit fields inline instead of switching to a dedicated form.
|
|
6
|
+
- You need change tracking or save/discard actions.
|
|
7
|
+
- You need to decide between `EditableRecordDetail`, `EditableValue`, `DynamicValue`, and raw form fields.
|
|
8
|
+
- A field type has companion values that must survive save flows.
|
|
9
|
+
|
|
10
|
+
## `EditableRecordDetail` for record-level inline editing
|
|
11
|
+
|
|
12
|
+
Use `EditableRecordDetail` when the whole record should be displayed as a detail surface, but each row can become editable.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import {
|
|
16
|
+
EditableRecordDetail,
|
|
17
|
+
EditableRecordDetailField
|
|
18
|
+
} from '@docyrus/ui/components/editable-record-detail';
|
|
19
|
+
|
|
20
|
+
<EditableRecordDetail
|
|
21
|
+
fields={fields}
|
|
22
|
+
record={record}
|
|
23
|
+
onSave={async (changes, values) => {
|
|
24
|
+
void changes;
|
|
25
|
+
void values;
|
|
26
|
+
}}
|
|
27
|
+
trackChanges>
|
|
28
|
+
<EditableRecordDetailField slug="full_name" />
|
|
29
|
+
<EditableRecordDetailField slug="email" />
|
|
30
|
+
<EditableRecordDetailField slug="status" />
|
|
31
|
+
</EditableRecordDetail>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use it when:
|
|
35
|
+
|
|
36
|
+
- the page is a detail panel or sheet
|
|
37
|
+
- users should edit a few fields in place
|
|
38
|
+
- you want a floating save/discard bar with changed-field summaries
|
|
39
|
+
|
|
40
|
+
## `EditableValue` for single-field inline editing
|
|
41
|
+
|
|
42
|
+
Use `EditableValue` when only one field should toggle between display and edit modes.
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
<EditableValue
|
|
46
|
+
field={field}
|
|
47
|
+
value={record[field.slug]}
|
|
48
|
+
record={record}
|
|
49
|
+
enumOptions={enumOptions}
|
|
50
|
+
onValueChange={(next) => saveOneField(field.slug, next)}
|
|
51
|
+
onCompanionChange={(companion) => saveCompanionFields(companion)}
|
|
52
|
+
changed={isChanged}
|
|
53
|
+
trackChanges
|
|
54
|
+
/>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This is the right primitive for compact inline edits inside cards, summaries, or custom detail rows.
|
|
58
|
+
|
|
59
|
+
## Field behavior categories inside `EditableValue`
|
|
60
|
+
|
|
61
|
+
`EditableValue` is not a simple text editor. It dispatches behavior by field type:
|
|
62
|
+
|
|
63
|
+
- inline types save on blur or Enter
|
|
64
|
+
- instant-save types commit immediately on change
|
|
65
|
+
- explicit-save types show confirm/cancel actions
|
|
66
|
+
- popover types keep edit mode stable while focus moves into portaled content
|
|
67
|
+
- read-only types stay display-only
|
|
68
|
+
|
|
69
|
+
That is why `useDocyrusFieldComponent(..., 'editable-value')` always returns `EditableValue` instead of a field-type-specific component.
|
|
70
|
+
|
|
71
|
+
## Companion fields matter
|
|
72
|
+
|
|
73
|
+
Manual inline save flows must preserve companion keys for composite field types.
|
|
74
|
+
|
|
75
|
+
Common examples:
|
|
76
|
+
|
|
77
|
+
- money → value + `__slug_currency`
|
|
78
|
+
- phone → value + `__slug_country`
|
|
79
|
+
- status → value + `__slug_secondary`, `__slug_description`, `__slug_followup_date`
|
|
80
|
+
- avatar → main value plus mapped companion fields
|
|
81
|
+
|
|
82
|
+
If you save only the visible main field, you can silently corrupt or flatten the record state.
|
|
83
|
+
|
|
84
|
+
## Renderer selection rules
|
|
85
|
+
|
|
86
|
+
Use these defaults:
|
|
87
|
+
|
|
88
|
+
- full editable form → form-field component
|
|
89
|
+
- full read-only detail → value renderer
|
|
90
|
+
- one-off inline field → `EditableValue`
|
|
91
|
+
- view-first detail screen with record-level inline save → `EditableRecordDetail`
|
|
92
|
+
|
|
93
|
+
If a field has no editable form component, prefer a value-render fallback instead of inventing a broken partial editor.
|
|
94
|
+
|
|
95
|
+
## Value-renderer guidance
|
|
96
|
+
|
|
97
|
+
Value renderers are not just pretty labels. They understand field semantics:
|
|
98
|
+
|
|
99
|
+
- `StatusValue` reads status companion data like description and follow-up date
|
|
100
|
+
- `MoneyValue` formats amounts with currency context
|
|
101
|
+
- `UserValue` and `UserMultiValue` present users properly
|
|
102
|
+
- `RelationValue` renders relationship display values
|
|
103
|
+
- `RichTextValue` and `DocEditorValue` display stored rich content safely
|
|
104
|
+
|
|
105
|
+
Prefer the value renderer over custom text formatting whenever you are showing a Docyrus field type in read-only mode.
|
|
106
|
+
|
|
107
|
+
For the pure field-type dispatch matrix and unsupported-field defaults, also read `field-type-mapping-and-fallbacks.md`.
|
|
108
|
+
|
|
109
|
+
## Shared maps are the source of truth
|
|
110
|
+
|
|
111
|
+
The safest advanced pattern is to stay on the shared registries exposed by `useDocyrusFieldComponent`:
|
|
112
|
+
|
|
113
|
+
- `FORM_FIELD_MAP`
|
|
114
|
+
- `VALUE_RENDERER_MAP`
|
|
115
|
+
- `CELL_COMPONENT_MAP`
|
|
116
|
+
|
|
117
|
+
That keeps forms, detail views, data-grid cells, and inline editing behavior aligned.
|
|
118
|
+
|
|
119
|
+
## Good combinations
|
|
120
|
+
|
|
121
|
+
### Detail page with manual sections + inline fields
|
|
122
|
+
|
|
123
|
+
- layout shell: your own markup
|
|
124
|
+
- read-only rows: `DynamicValue`
|
|
125
|
+
- editable rows: `EditableValue`
|
|
126
|
+
- bulk record save experience: `EditableRecordDetail`
|
|
127
|
+
|
|
128
|
+
### Hook-first detail page with selective inline editing
|
|
129
|
+
|
|
130
|
+
- outer data workflow: `useDocyrusFormView`
|
|
131
|
+
- default layout: `renderLayout()` in `view` mode
|
|
132
|
+
- inline edit UX: `clickToEdit: true`
|
|
133
|
+
|
|
134
|
+
## Debug checklist
|
|
135
|
+
|
|
136
|
+
- **Field never becomes editable?** It may be a read-only field type or lack a registered form-field component.
|
|
137
|
+
- **Inline save loses currency/country/status metadata?** You forgot companion-field handling.
|
|
138
|
+
- **Renderer shows weak output?** Check whether `record` or `enumOptions` is incomplete.
|
|
139
|
+
- **Detail page edits but does not show change state?** Enable `trackChanges` and wire changed-state inputs correctly.
|
|
140
|
+
- **Custom manual switch statement is drifting from Docyrus UI behavior?** Replace it with `useDocyrusFieldComponent`, `DynamicFormField`, or `DynamicValue`.
|