@donotdev/ui 0.0.10 → 0.0.11

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 (102) hide show
  1. package/dist/crud/components/EntityCardList.d.ts +16 -0
  2. package/dist/crud/components/EntityCardList.d.ts.map +1 -0
  3. package/dist/crud/components/EntityCardList.js +175 -0
  4. package/dist/crud/components/EntityDisplayRenderer.d.ts +13 -21
  5. package/dist/crud/components/EntityDisplayRenderer.d.ts.map +1 -1
  6. package/dist/crud/components/EntityDisplayRenderer.js +138 -23
  7. package/dist/crud/components/EntityFormRenderer.d.ts +18 -0
  8. package/dist/crud/components/EntityFormRenderer.d.ts.map +1 -0
  9. package/dist/crud/components/EntityFormRenderer.js +275 -0
  10. package/dist/crud/components/EntityList.d.ts +14 -0
  11. package/dist/crud/components/EntityList.d.ts.map +1 -0
  12. package/dist/crud/components/EntityList.js +201 -0
  13. package/dist/crud/components/index.d.ts +7 -5
  14. package/dist/crud/components/index.d.ts.map +1 -1
  15. package/dist/crud/components/index.js +6 -5
  16. package/dist/dndev.css +179 -0
  17. package/dist/index.js +4 -64
  18. package/dist/internal/layout/components/AutoMetaTags.d.ts.map +1 -1
  19. package/dist/internal/layout/components/AutoMetaTags.js +36 -6
  20. package/dist/internal/layout/components/NextJsAutoMetaTags.d.ts.map +1 -1
  21. package/dist/internal/layout/components/NextJsAutoMetaTags.js +38 -10
  22. package/dist/internal/layout/components/footer/FooterBranding.js +2 -2
  23. package/dist/styles/index.css +179 -0
  24. package/package.json +12 -12
  25. package/dist/crud/components/DisplayFieldRenderer.d.ts +0 -26
  26. package/dist/crud/components/DisplayFieldRenderer.d.ts.map +0 -1
  27. package/dist/crud/components/DisplayFieldRenderer.js +0 -107
  28. package/dist/crud/components/fields/display/AvatarFieldDisplay.d.ts +0 -23
  29. package/dist/crud/components/fields/display/AvatarFieldDisplay.d.ts.map +0 -1
  30. package/dist/crud/components/fields/display/AvatarFieldDisplay.js +0 -38
  31. package/dist/crud/components/fields/display/BadgeFieldDisplay.d.ts +0 -21
  32. package/dist/crud/components/fields/display/BadgeFieldDisplay.d.ts.map +0 -1
  33. package/dist/crud/components/fields/display/BadgeFieldDisplay.js +0 -31
  34. package/dist/crud/components/fields/display/ButtonFieldDisplay.d.ts +0 -29
  35. package/dist/crud/components/fields/display/ButtonFieldDisplay.d.ts.map +0 -1
  36. package/dist/crud/components/fields/display/ButtonFieldDisplay.js +0 -12
  37. package/dist/crud/components/fields/display/CheckboxFieldDisplay.d.ts +0 -21
  38. package/dist/crud/components/fields/display/CheckboxFieldDisplay.d.ts.map +0 -1
  39. package/dist/crud/components/fields/display/CheckboxFieldDisplay.js +0 -27
  40. package/dist/crud/components/fields/display/DateFieldDisplay.d.ts +0 -24
  41. package/dist/crud/components/fields/display/DateFieldDisplay.d.ts.map +0 -1
  42. package/dist/crud/components/fields/display/DateFieldDisplay.js +0 -41
  43. package/dist/crud/components/fields/display/DropdownDisplay.d.ts +0 -21
  44. package/dist/crud/components/fields/display/DropdownDisplay.d.ts.map +0 -1
  45. package/dist/crud/components/fields/display/DropdownDisplay.js +0 -25
  46. package/dist/crud/components/fields/display/FileFieldDisplay.d.ts +0 -21
  47. package/dist/crud/components/fields/display/FileFieldDisplay.d.ts.map +0 -1
  48. package/dist/crud/components/fields/display/FileFieldDisplay.js +0 -25
  49. package/dist/crud/components/fields/display/GeoPointFieldDisplay.d.ts +0 -25
  50. package/dist/crud/components/fields/display/GeoPointFieldDisplay.d.ts.map +0 -1
  51. package/dist/crud/components/fields/display/GeoPointFieldDisplay.js +0 -25
  52. package/dist/crud/components/fields/display/HiddenFieldDisplay.d.ts +0 -30
  53. package/dist/crud/components/fields/display/HiddenFieldDisplay.d.ts.map +0 -1
  54. package/dist/crud/components/fields/display/HiddenFieldDisplay.js +0 -12
  55. package/dist/crud/components/fields/display/ImageFieldDisplay.d.ts +0 -24
  56. package/dist/crud/components/fields/display/ImageFieldDisplay.d.ts.map +0 -1
  57. package/dist/crud/components/fields/display/ImageFieldDisplay.js +0 -38
  58. package/dist/crud/components/fields/display/LinkFieldDisplay.d.ts +0 -22
  59. package/dist/crud/components/fields/display/LinkFieldDisplay.d.ts.map +0 -1
  60. package/dist/crud/components/fields/display/LinkFieldDisplay.js +0 -48
  61. package/dist/crud/components/fields/display/MapFieldDisplay.d.ts +0 -25
  62. package/dist/crud/components/fields/display/MapFieldDisplay.d.ts.map +0 -1
  63. package/dist/crud/components/fields/display/MapFieldDisplay.js +0 -25
  64. package/dist/crud/components/fields/display/MultiDropdownDisplay.d.ts +0 -22
  65. package/dist/crud/components/fields/display/MultiDropdownDisplay.d.ts.map +0 -1
  66. package/dist/crud/components/fields/display/MultiDropdownDisplay.js +0 -25
  67. package/dist/crud/components/fields/display/MultiInputTextFieldDisplay.d.ts +0 -22
  68. package/dist/crud/components/fields/display/MultiInputTextFieldDisplay.d.ts.map +0 -1
  69. package/dist/crud/components/fields/display/MultiInputTextFieldDisplay.js +0 -25
  70. package/dist/crud/components/fields/display/NumberFieldDisplay.d.ts +0 -24
  71. package/dist/crud/components/fields/display/NumberFieldDisplay.d.ts.map +0 -1
  72. package/dist/crud/components/fields/display/NumberFieldDisplay.js +0 -28
  73. package/dist/crud/components/fields/display/PasswordFieldDisplay.d.ts +0 -24
  74. package/dist/crud/components/fields/display/PasswordFieldDisplay.d.ts.map +0 -1
  75. package/dist/crud/components/fields/display/PasswordFieldDisplay.js +0 -31
  76. package/dist/crud/components/fields/display/PhoneNumberDisplay.d.ts +0 -22
  77. package/dist/crud/components/fields/display/PhoneNumberDisplay.d.ts.map +0 -1
  78. package/dist/crud/components/fields/display/PhoneNumberDisplay.js +0 -25
  79. package/dist/crud/components/fields/display/RadioFieldDisplay.d.ts +0 -22
  80. package/dist/crud/components/fields/display/RadioFieldDisplay.d.ts.map +0 -1
  81. package/dist/crud/components/fields/display/RadioFieldDisplay.js +0 -25
  82. package/dist/crud/components/fields/display/RangeFieldDisplay.d.ts +0 -22
  83. package/dist/crud/components/fields/display/RangeFieldDisplay.d.ts.map +0 -1
  84. package/dist/crud/components/fields/display/RangeFieldDisplay.js +0 -25
  85. package/dist/crud/components/fields/display/ReferenceFieldDisplay.d.ts +0 -22
  86. package/dist/crud/components/fields/display/ReferenceFieldDisplay.d.ts.map +0 -1
  87. package/dist/crud/components/fields/display/ReferenceFieldDisplay.js +0 -26
  88. package/dist/crud/components/fields/display/RichTextDisplay.d.ts +0 -25
  89. package/dist/crud/components/fields/display/RichTextDisplay.d.ts.map +0 -1
  90. package/dist/crud/components/fields/display/RichTextDisplay.js +0 -104
  91. package/dist/crud/components/fields/display/TextAreaDisplay.d.ts +0 -22
  92. package/dist/crud/components/fields/display/TextAreaDisplay.d.ts.map +0 -1
  93. package/dist/crud/components/fields/display/TextAreaDisplay.js +0 -25
  94. package/dist/crud/components/fields/display/TextFieldDisplay.d.ts +0 -42
  95. package/dist/crud/components/fields/display/TextFieldDisplay.d.ts.map +0 -1
  96. package/dist/crud/components/fields/display/TextFieldDisplay.js +0 -97
  97. package/dist/crud/components/fields/display/TimestampFieldDisplay.d.ts +0 -22
  98. package/dist/crud/components/fields/display/TimestampFieldDisplay.d.ts.map +0 -1
  99. package/dist/crud/components/fields/display/TimestampFieldDisplay.js +0 -33
  100. package/dist/crud/components/fields/display/index.d.ts +0 -32
  101. package/dist/crud/components/fields/display/index.d.ts.map +0 -1
  102. package/dist/crud/components/fields/display/index.js +0 -32
@@ -0,0 +1,275 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // packages/ui/src/crud/components/EntityFormRenderer.tsx
4
+ /**
5
+ * @fileoverview EntityFormRenderer component
6
+ * @description Dumb renderer that composes form from entity definition.
7
+ * All orchestration logic lives in useEntityForm.
8
+ *
9
+ * @version 0.0.7
10
+ * @since 0.0.1
11
+ * @author AMBROISE PARK Consulting
12
+ */
13
+ import { useEffect, useId, useMemo, useState } from 'react';
14
+ import { FormProvider } from 'react-hook-form';
15
+ import { Badge, Button, DropdownMenu, Grid, Stack, Spinner, } from '@donotdev/components';
16
+ import { useTranslation } from '@donotdev/core';
17
+ import { useNavigate } from '../../routing';
18
+ import { DisplayFieldRenderer, FormFieldRenderer, UploadProvider, useEntityForm, useUnsavedChangesWarning, useConfirmNavigation, useFormStore, } from '@donotdev/crud';
19
+ /**
20
+ * EntityFormRenderer - Dumb component that renders a form from entity definition.
21
+ *
22
+ * All orchestration (uploads, validation, status tracking) is handled by useEntityForm.
23
+ * This component just:
24
+ * - Generates formId
25
+ * - Renders fields
26
+ * - Renders submit button with status from useEntityForm
27
+ *
28
+ * @version 0.0.7
29
+ * @since 0.0.1
30
+ * @author AMBROISE PARK Consulting
31
+ */
32
+ export function EntityFormRenderer({ entity, onSubmit, t, className = '', submitText, loading = false, defaultValues, submitVariant = 'primary', secondaryButtonText, secondaryButtonVariant = 'outline', onSecondarySubmit, viewerRole, operation = defaultValues ? 'edit' : 'create', autoSave = operation === 'create', formId: externalFormId, cancelText, cancelPath, successPath, onCancel, warnOnUnsavedChanges = true, unsavedChangesMessage, hideVisibilityInfo = false, }) {
33
+ const navigate = useNavigate();
34
+ // Generate stable form ID
35
+ const generatedFormId = useId();
36
+ const formId = externalFormId ?? `entity-form-${entity.name}-${generatedFormId}`;
37
+ // Use entity name as i18n namespace
38
+ const { t: translationFn } = useTranslation(entity.namespace);
39
+ const { t: tCrud } = useTranslation('crud');
40
+ const translate = t || translationFn;
41
+ // Preview role state for View As toggle (only used when visibility info is shown)
42
+ const [previewRole, setPreviewRole] = useState('super');
43
+ // Visibility → Badge variant mapping (inline, no separate file needed)
44
+ const visibilityBadgeVariant = {
45
+ guest: 'muted',
46
+ user: 'secondary',
47
+ admin: 'warning',
48
+ super: 'destructive',
49
+ technical: 'accent',
50
+ };
51
+ // Role hierarchy for filtering (higher index = more access)
52
+ const roleHierarchy = ['guest', 'user', 'admin', 'super'];
53
+ // Role display labels (hardcoded English - universal technical terms)
54
+ const roleLabels = {
55
+ guest: 'Guest',
56
+ user: 'User',
57
+ admin: 'Admin',
58
+ super: 'Super',
59
+ technical: 'System',
60
+ };
61
+ // Preview dropdown menu items
62
+ const previewMenuItems = useMemo(() => roleHierarchy.map((role) => ({
63
+ label: roleLabels[role],
64
+ checked: previewRole === role,
65
+ onClick: () => setPreviewRole(role),
66
+ })), [previewRole]);
67
+ // useEntityForm handles all orchestration
68
+ const form = useEntityForm(entity, {
69
+ formId,
70
+ operation,
71
+ defaultValues,
72
+ viewerRole,
73
+ t: translate,
74
+ autoSave,
75
+ });
76
+ const { control, handleSubmit, formState: { errors }, fields: rawFields, formStatus, uploadProgress, cleanup, isDirty, resetForm, } = form;
77
+ // Filter fields based on previewRole when visibility info is shown
78
+ const renderableFields = useMemo(() => {
79
+ if (hideVisibilityInfo)
80
+ return rawFields;
81
+ const previewRoleIndex = roleHierarchy.indexOf(previewRole);
82
+ return rawFields.filter(({ config }) => {
83
+ const fieldVisibility = config.visibility;
84
+ // 'hidden' fields are never shown, 'technical' always shown as read-only
85
+ if (fieldVisibility === 'hidden')
86
+ return false;
87
+ if (fieldVisibility === 'technical')
88
+ return true;
89
+ const fieldRoleIndex = roleHierarchy.indexOf(fieldVisibility);
90
+ return fieldRoleIndex <= previewRoleIndex;
91
+ });
92
+ }, [rawFields, hideVisibilityInfo, previewRole]);
93
+ // Sync isDirty to FormStore (single source of truth)
94
+ useEffect(() => {
95
+ if (formId) {
96
+ useFormStore.getState().setIsDirty(formId, isDirty);
97
+ }
98
+ }, [formId, isDirty]);
99
+ // Cleanup on unmount
100
+ useEffect(() => {
101
+ return cleanup;
102
+ }, [cleanup]);
103
+ // Compute i18n messages once (DRY, performance)
104
+ const unsavedChangesLeaveMessage = useMemo(() => unsavedChangesMessage ||
105
+ tCrud('messages.unsavedChangesLeave', {
106
+ defaultValue: 'You have unsaved changes. Are you sure you want to leave?',
107
+ }), [unsavedChangesMessage, tCrud]);
108
+ const unsavedChangesDiscardMessage = useMemo(() => unsavedChangesMessage ||
109
+ tCrud('messages.unsavedChangesDiscard', {
110
+ defaultValue: 'You have unsaved changes. Discard them?',
111
+ }), [unsavedChangesMessage, tCrud]);
112
+ // Warn about unsaved changes when navigating away (browser refresh/close)
113
+ // SPA navigation blocking is handled by router hooks via FormStore
114
+ useUnsavedChangesWarning({
115
+ isDirty,
116
+ enabled: warnOnUnsavedChanges,
117
+ message: unsavedChangesLeaveMessage,
118
+ });
119
+ // Use framework helper for cancel confirmation (DRY, SSR-safe)
120
+ const confirmNavigation = useConfirmNavigation(isDirty, unsavedChangesDiscardMessage);
121
+ // Handle cancel with confirmation
122
+ const handleCancel = async () => {
123
+ if (isDirty) {
124
+ const confirmed = await confirmNavigation();
125
+ if (!confirmed)
126
+ return;
127
+ }
128
+ resetForm();
129
+ // If onCancel callback provided, use it (takes precedence)
130
+ if (onCancel) {
131
+ onCancel();
132
+ }
133
+ else {
134
+ // Otherwise, navigate back or to cancelPath if provided
135
+ if (cancelPath) {
136
+ navigate(cancelPath);
137
+ }
138
+ else {
139
+ navigate('back');
140
+ }
141
+ }
142
+ };
143
+ // Wrap onSubmit to navigate optimistically (fire and forget)
144
+ // handleSubmit from react-hook-form expects: (data: EntityData) => void | Promise<void>
145
+ // Our onSubmit prop is (data: T) => void | Promise<void>, where T extends EntityRecord
146
+ // EntityData should be compatible with T, so we can safely pass it
147
+ const handleFormSubmit = (data) => {
148
+ // Call onSubmit (don't await - optimistic update pattern)
149
+ // Type assertion is safe: EntityData is the validated form data, T is EntityRecord
150
+ onSubmit(data);
151
+ // Navigate immediately (optimistic navigation - feels fast)
152
+ // If onSubmit is async, navigation happens while it's processing in background
153
+ if (successPath) {
154
+ navigate(successPath);
155
+ }
156
+ else {
157
+ // Default: navigate back (optimistic - user came from list, go back there)
158
+ navigate('back');
159
+ }
160
+ };
161
+ // Determine if cancel button should show
162
+ // Show by default, hide only if cancelText is explicitly null
163
+ const showCancelButton = cancelText !== null;
164
+ const cancelButtonText = cancelText ?? tCrud('form.cancel', { defaultValue: 'Cancel' });
165
+ // Common button container style (sticky footer)
166
+ const buttonContainerStyle = {
167
+ gridColumn: '1 / -1',
168
+ position: 'sticky',
169
+ bottom: 0,
170
+ backgroundColor: 'var(--background)',
171
+ borderTop: '1px solid var(--border)',
172
+ paddingTop: 'var(--gap-md)',
173
+ paddingBottom: 'var(--gap-md)',
174
+ zIndex: 10,
175
+ marginTop: 'var(--gap-lg)',
176
+ };
177
+ // Auto-detect loading for edit forms waiting for data
178
+ const isLoading = loading || (operation === 'edit' && !defaultValues);
179
+ // Compute button state from form status
180
+ const buttonState = useMemo(() => {
181
+ if (formStatus === 'uploading') {
182
+ return {
183
+ loading: true,
184
+ loadingText: uploadProgress < 100
185
+ ? tCrud('form.uploading', {
186
+ progress: Math.round(uploadProgress),
187
+ defaultValue: `Uploading ${Math.round(uploadProgress)}%...`,
188
+ })
189
+ : tCrud('form.processing', { defaultValue: 'Processing...' }),
190
+ progress: uploadProgress,
191
+ };
192
+ }
193
+ if (formStatus === 'validating') {
194
+ return {
195
+ loading: true,
196
+ loadingText: tCrud('form.validating', {
197
+ defaultValue: 'Validating...',
198
+ }),
199
+ };
200
+ }
201
+ if (formStatus === 'submitting') {
202
+ return {
203
+ loading: true,
204
+ loadingText: tCrud('form.saving', { defaultValue: 'Saving...' }),
205
+ };
206
+ }
207
+ return { loading: false };
208
+ }, [formStatus, uploadProgress, tCrud]);
209
+ return (_jsx(FormProvider, { ...form, children: _jsx(UploadProvider, { formId: formId, children: _jsxs("div", { style: {
210
+ position: 'relative',
211
+ width: '100%',
212
+ gridColumn: '1 / -1',
213
+ display: 'contents',
214
+ }, children: [isLoading && _jsx(Spinner, { overlay: true }), _jsxs("form", { onSubmit: handleSubmit(handleFormSubmit), noValidate: true, className: className, style: { width: '100%', gridColumn: '1 / -1', display: 'contents' }, children: [renderableFields.map(({ name, config, editable }) => {
215
+ // Show visibility badge for non-guest fields when visibility info is enabled
216
+ const showBadge = !hideVisibilityInfo && config.visibility !== 'guest';
217
+ const badgeVariant = config.visibility !== 'hidden'
218
+ ? visibilityBadgeVariant[config.visibility]
219
+ : undefined;
220
+ const badgeLabel = roleLabels[config.visibility] ??
221
+ config.visibility;
222
+ // Wrap field with visibility badge when enabled
223
+ const fieldElement = !editable ? (_jsx(DisplayFieldRenderer, { name: name, config: config, value: defaultValues?.[name], t: translate }, name)) : (_jsx(FormFieldRenderer, { name: name, config: config, control: control, errors: errors, t: translate }, name));
224
+ // If no badge needed, return field directly
225
+ if (!showBadge)
226
+ return fieldElement;
227
+ // Wrap field with badge indicator
228
+ return (_jsxs("div", { style: { position: 'relative' }, children: [_jsx(Badge, { as: "span", variant: badgeVariant, style: {
229
+ position: 'absolute',
230
+ top: 0,
231
+ insetInlineEnd: 0,
232
+ fontSize: 'var(--font-size-xs)',
233
+ zIndex: 1,
234
+ }, children: badgeLabel }), fieldElement] }, name));
235
+ }), (() => {
236
+ // Define all possible buttons (conditionally included)
237
+ const buttons = [];
238
+ // Cancel button
239
+ if (showCancelButton && cancelButtonText) {
240
+ buttons.push(_jsx(Button, { type: "button", onClick: handleCancel, disabled: buttonState.loading, variant: "outline", className: "dndev-w-full", children: cancelButtonText }, "cancel"));
241
+ }
242
+ // Preview dropdown
243
+ if (!hideVisibilityInfo) {
244
+ const previewVariant = visibilityBadgeVariant[previewRole] || 'muted';
245
+ buttons.push(_jsx(DropdownMenu, { trigger: _jsxs(Button, { type: "button", variant: previewVariant, disabled: buttonState.loading, className: "dndev-w-full", children: [tCrud('visibility.preview', {
246
+ defaultValue: 'Preview',
247
+ }), ": ", roleLabels[previewRole]] }), items: previewMenuItems }, "preview"));
248
+ }
249
+ // Secondary submit button
250
+ if (secondaryButtonText) {
251
+ buttons.push(_jsx(Button, { type: "button", onClick: () => {
252
+ if (onSecondarySubmit) {
253
+ const currentValues = form.getValues();
254
+ onSecondarySubmit(currentValues);
255
+ }
256
+ }, loading: buttonState.loading, loadingText: buttonState.loadingText, progress: buttonState.progress, variant: secondaryButtonVariant, className: "dndev-w-full", children: secondaryButtonText }, "secondary"));
257
+ }
258
+ // Primary submit button (always shown)
259
+ buttons.push(_jsx(Button, { type: "submit", loading: buttonState.loading, loadingText: buttonState.loadingText, progress: buttonState.progress, variant: submitVariant, className: "dndev-w-full", children: submitText ||
260
+ tCrud('form.submit', { defaultValue: 'Submit' }) }, "submit"));
261
+ const buttonCount = buttons.length;
262
+ // Single button: use Stack
263
+ if (buttonCount === 1) {
264
+ return (_jsx(Stack, { direction: "column", gap: "tight", style: buttonContainerStyle, children: buttons[0] }));
265
+ }
266
+ // Multiple buttons: use Grid with proportional columns (Submit gets 2fr)
267
+ const templateColumns = buttonCount === 2
268
+ ? '1fr 2fr' // Cancel/Preview + Submit
269
+ : buttonCount === 3
270
+ ? '1fr 1fr 2fr' // Cancel + Preview + Submit
271
+ : '1fr 1fr 1fr 2fr'; // Cancel + Preview + Secondary + Submit
272
+ return (_jsx(Grid, { cols: [1, buttonCount, buttonCount, buttonCount], templateColumns: templateColumns, gap: "tight", style: buttonContainerStyle, children: buttons }));
273
+ })()] })] }) }) }));
274
+ }
275
+ export default EntityFormRenderer;
@@ -0,0 +1,14 @@
1
+ import type { EntityListProps } from '@donotdev/core';
2
+ export type { EntityListProps };
3
+ /**
4
+ * Entity List Component - Table view for admin/internal operations
5
+ *
6
+ * Features:
7
+ * - Filters section (collapsible) with actions and filter inputs
8
+ * - Results section (collapsible) with DataTable
9
+ * - Excel-like table display with formatted values
10
+ * - Edit and Delete actions (admin only)
11
+ * - Auto-routing when handlers not provided
12
+ */
13
+ export declare function EntityList({ entity, userRole, basePath, onClick, hideFilters, pagination, pageSize: pageSizeProp, queryOptions, }: EntityListProps): import("react/jsx-runtime").JSX.Element;
14
+ //# sourceMappingURL=EntityList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EntityList.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityList.tsx"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,QAAkB,EAClB,QAAQ,EACR,OAAO,EACP,WAAmB,EACnB,UAAqB,EACrB,QAAQ,EAAE,YAAY,EACtB,YAAY,GACb,EAAE,eAAe,2CAsTjB"}
@@ -0,0 +1,201 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ // packages/ui/src/crud/components/EntityList.tsx
4
+ /**
5
+ * @fileoverview Entity List Component
6
+ * @description Table view for admin/internal CRUD operations.
7
+ * Features: Filters section + Results section with DataTable
8
+ *
9
+ * **Routing:** Convention basePath = `/${collection}`. View/Edit = basePath/:id, Create = basePath/new.
10
+ * Override basePath for nested routes; use onClick(id) to open sheet instead of navigating.
11
+ *
12
+ * @version 0.3.2
13
+ * @since 0.0.1
14
+ * @author AMBROISE PARK Consulting
15
+ */
16
+ import { RefreshCw, Plus, Trash2, Edit, Search } from 'lucide-react';
17
+ import { useMemo, useCallback, useState } from 'react';
18
+ import { DataTable, Button, Stack, ActionButton, Section, Input, } from '@donotdev/components';
19
+ import { useTranslation } from '@donotdev/core';
20
+ import { useNavigate } from '../../routing';
21
+ import { translateFieldLabel, useCrud, useCrudList, EntityFilters, matchesFilter, formatValue, } from '@donotdev/crud';
22
+ /**
23
+ * Entity List Component - Table view for admin/internal operations
24
+ *
25
+ * Features:
26
+ * - Filters section (collapsible) with actions and filter inputs
27
+ * - Results section (collapsible) with DataTable
28
+ * - Excel-like table display with formatted values
29
+ * - Edit and Delete actions (admin only)
30
+ * - Auto-routing when handlers not provided
31
+ */
32
+ export function EntityList({ entity, userRole = 'guest', basePath, onClick, hideFilters = false, pagination = 'client', pageSize: pageSizeProp, queryOptions, }) {
33
+ const navigate = useNavigate();
34
+ const base = basePath ?? `/${entity.collection}`;
35
+ // Server-side pagination state (only used when pagination='server')
36
+ const [currentPage, setCurrentPage] = useState(1);
37
+ // For server-side, we need to track pageSize state (DataTable will call onPageSizeChange)
38
+ // For client-side, we just pass pageSizeProp to DataTable and it handles its own state
39
+ const [serverPageSize, setServerPageSize] = useState(pageSizeProp);
40
+ // Separation of Concerns:
41
+ // Client mode: fetches ALL items, DataTable handles pagination
42
+ // Server mode: fetches per page, controlled pagination
43
+ const { data: listData, loading, mutate: refreshList, } = useCrudList(entity, {
44
+ pagination,
45
+ ...(queryOptions && { queryOptions }),
46
+ ...(pagination === 'server' && {
47
+ page: currentPage,
48
+ pageSize: serverPageSize,
49
+ }),
50
+ });
51
+ // useCrud -> handles actions (delete)
52
+ const { delete: deleteItem } = useCrud(entity);
53
+ const { t: tCrud } = useTranslation('crud');
54
+ const data = listData?.items || [];
55
+ // Entity + crud namespaces so formatValue can resolve crud:price.* etc.
56
+ const { t } = useTranslation([entity.namespace, 'crud']);
57
+ // Filter state - supports string, number range {min, max}, date range {min, max}
58
+ const [filters, setFilters] = useState({});
59
+ const [searchQuery, setSearchQuery] = useState('');
60
+ // Refresh handler - triggers manual refetch in useList
61
+ const handleRefresh = useCallback(async () => {
62
+ await refreshList();
63
+ }, [refreshList]);
64
+ // Edit button: always navigate to basePath/:id
65
+ const handleEdit = useCallback((id) => {
66
+ navigate(`${base}/${id}`);
67
+ }, [base, navigate]);
68
+ // Row click: onClick(id) if provided, else navigate to basePath/:id
69
+ const handleView = useCallback((id) => {
70
+ if (onClick) {
71
+ onClick(id);
72
+ }
73
+ else {
74
+ navigate(`${base}/${id}`);
75
+ }
76
+ }, [base, navigate, onClick]);
77
+ // Add New: navigate to basePath/new
78
+ const handleCreate = useCallback(() => {
79
+ navigate(`${base}/new`);
80
+ }, [base, navigate]);
81
+ // Delete handler - store handles optimistic removal automatically
82
+ const handleDelete = useCallback(async (itemId) => {
83
+ await deleteItem(itemId);
84
+ }, [deleteItem]);
85
+ // Update filters (for EntityFilters component)
86
+ const handleFiltersChange = useCallback((newFilters) => {
87
+ setFilters(newFilters);
88
+ }, []);
89
+ // Apply search and filters to data
90
+ // @todo Server-side filtering: Currently only handles client-side filtering.
91
+ // When pagination='server', filters should be sent to the server via useCrudList options.
92
+ // Until then, filters are applied client-side after fetching.
93
+ const filteredData = useMemo(() => {
94
+ let result = data;
95
+ // Apply search query (searches all fields)
96
+ if (searchQuery) {
97
+ const searchLower = searchQuery.toLowerCase();
98
+ result = result.filter((item) => {
99
+ return Object.values(item).some((value) => {
100
+ if (value === null || value === undefined)
101
+ return false;
102
+ return String(value).toLowerCase().includes(searchLower);
103
+ });
104
+ });
105
+ }
106
+ // Apply column filters
107
+ if (Object.keys(filters).length > 0) {
108
+ result = result.filter((item) => {
109
+ return Object.entries(filters).every(([fieldName, filterValue]) => {
110
+ const itemValue = item[fieldName];
111
+ const fieldConfig = entity.fields[fieldName];
112
+ const fieldType = fieldConfig?.type || 'text';
113
+ return matchesFilter(itemValue, filterValue, fieldType);
114
+ });
115
+ });
116
+ }
117
+ return result;
118
+ }, [data, searchQuery, filters, entity.fields]);
119
+ // Generate columns from entity.listFields or entity.fields
120
+ const columns = useMemo(() => {
121
+ const fieldsToShow = entity.listFields || Object.keys(entity.fields);
122
+ const baseColumns = fieldsToShow
123
+ .map((fieldName) => {
124
+ const fieldConfig = entity.fields[fieldName];
125
+ if (!fieldConfig)
126
+ return null;
127
+ const label = translateFieldLabel(fieldName, fieldConfig, t);
128
+ const fieldType = fieldConfig.type || 'text';
129
+ const isNumeric = fieldType === 'number' || fieldType === 'range';
130
+ const align = isNumeric ? 'end' : 'start';
131
+ return {
132
+ key: fieldName,
133
+ title: label,
134
+ dataIndex: fieldName,
135
+ sortable: true,
136
+ filterable: true,
137
+ align,
138
+ render: (value, record) => formatValue(value, fieldConfig, t, { compact: true }),
139
+ };
140
+ })
141
+ .filter(Boolean);
142
+ // Add actions column at the front (for mobile accessibility)
143
+ baseColumns.unshift({
144
+ key: '_actions',
145
+ title: tCrud('actions.label', { defaultValue: 'Actions' }),
146
+ dataIndex: undefined,
147
+ sortable: false,
148
+ width: 120,
149
+ align: 'center',
150
+ render: (_, record) => (_jsxs(Stack, { direction: "row", gap: "tight", align: "center", justify: "center", children: [_jsx(Button, { variant: "outline", icon: Edit, onClick: (e) => {
151
+ e.stopPropagation();
152
+ handleEdit(record.id);
153
+ }, "aria-label": tCrud('edit', { defaultValue: 'Edit' }) }), _jsx(ActionButton, { action: async () => {
154
+ await handleDelete(record.id);
155
+ }, confirmText: tCrud('delete.confirm', {
156
+ defaultValue: 'Are you sure you want to delete this item?',
157
+ }), confirmTitle: tCrud('delete.title', {
158
+ defaultValue: 'Delete Item',
159
+ }), loadingText: tCrud('delete.loading', {
160
+ defaultValue: 'Deleting...',
161
+ }), variant: "destructive", icon: Trash2, "aria-label": tCrud('delete', { defaultValue: 'Delete' }), children: tCrud('delete', { defaultValue: 'Delete' }) })] })),
162
+ });
163
+ return baseColumns;
164
+ }, [entity, t, tCrud, handleEdit, handleDelete]);
165
+ // Entity name for section title
166
+ const entityName = t('name', { defaultValue: entity.name });
167
+ // Result count
168
+ const resultCount = filteredData.length;
169
+ // Always show table structure - DataTable will show skeleton rows when loading
170
+ return (_jsxs(_Fragment, { children: [_jsx(Section, { title: tCrud('filters.title', {
171
+ entity: entityName,
172
+ defaultValue: `Browse ${entityName} - Filters`,
173
+ }), collapsible: true, defaultOpen: true, children: _jsxs(Stack, { gap: "medium", children: [_jsxs(Stack, { direction: "row", gap: "tight", align: "center", className: "dndev-w-full", style: { display: 'grid', gridTemplateColumns: '1fr auto auto' }, children: [_jsx(Input, { placeholder: tCrud('search.placeholder', {
174
+ defaultValue: 'Search...',
175
+ }), value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), icon: Search, className: "dndev-w-full" }), _jsx(Button, { icon: RefreshCw, variant: "outline", onClick: handleRefresh, disabled: loading, display: "compact", "aria-label": tCrud('refresh', { defaultValue: 'Refresh' }) }), _jsx(Button, { icon: Plus, onClick: handleCreate, display: "compact", children: tCrud('addNew', { defaultValue: 'Add New' }) })] }), !hideFilters && (_jsx(EntityFilters, { entity: entity, data: data, filters: filters, onFiltersChange: handleFiltersChange, fieldsToFilter: entity.listFields }))] }) }), _jsx(Section, { title: loading
176
+ ? tCrud('results.title.fetching', {
177
+ defaultValue: 'Fetching...',
178
+ })
179
+ : tCrud('results.title.count', {
180
+ count: resultCount,
181
+ defaultValue: resultCount === 1
182
+ ? 'Found 1 occurrence'
183
+ : `Found ${resultCount} occurrences`,
184
+ }), collapsible: true, defaultOpen: true, children: _jsx(DataTable, { data: filteredData, columns: columns, sortable: true, searchable: false, pagination: true, loading: loading, onRowClick: (item) => handleView(item.id),
185
+ // Pagination labels (translated)
186
+ showingLabel: tCrud('pagination.showing', {
187
+ defaultValue: 'Showing {{from}} to {{to}} of {{total}} entries',
188
+ }), paginationPreviousLabel: tCrud('pagination.previous', {
189
+ defaultValue: 'Previous',
190
+ }), paginationNextLabel: tCrud('pagination.next', {
191
+ defaultValue: 'Next',
192
+ }), paginationItemsPerPagePlaceholder: tCrud('pagination.itemsPerPagePlaceholder', {
193
+ defaultValue: 'Items per page',
194
+ }), ...(pageSizeProp && { pageSize: pageSizeProp }), ...(pagination === 'server' && {
195
+ currentPage,
196
+ pageSize: serverPageSize,
197
+ total: listData?.total,
198
+ onPageChange: setCurrentPage,
199
+ onPageSizeChange: setServerPageSize,
200
+ }) }) })] }));
201
+ }
@@ -1,13 +1,15 @@
1
1
  /**
2
- * @fileoverview CRUD display components
3
- * @description Display components for CRUD operations (form components moved to @donotdev/crud)
2
+ * @fileoverview CRUD components
3
+ * @description CRUD components with routing support (moved from @donotdev/crud)
4
4
  *
5
- * @version 0.0.1
5
+ * @version 0.0.3
6
6
  * @since 0.0.1
7
7
  * @author AMBROISE PARK Consulting
8
8
  */
9
- export { DisplayFieldRenderer } from './DisplayFieldRenderer';
10
9
  export { EntityDisplayRenderer } from './EntityDisplayRenderer';
10
+ export { EntityList } from './EntityList';
11
+ export { EntityCardList } from './EntityCardList';
12
+ export { EntityFormRenderer } from './EntityFormRenderer';
11
13
  export * from './Form';
12
- export * from './fields/display';
14
+ export type { EntityListProps, EntityCardListProps, EntityFormRendererProps, EntityDisplayRendererProps, } from '@donotdev/core';
13
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/crud/components/index.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,cAAc,QAAQ,CAAC;AACvB,cAAc,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/crud/components/index.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,cAAc,QAAQ,CAAC;AAEvB,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,uBAAuB,EACvB,0BAA0B,GAC3B,MAAM,gBAAgB,CAAC"}
@@ -1,13 +1,14 @@
1
1
  // packages/ui/src/crud/components/index.ts
2
2
  /**
3
- * @fileoverview CRUD display components
4
- * @description Display components for CRUD operations (form components moved to @donotdev/crud)
3
+ * @fileoverview CRUD components
4
+ * @description CRUD components with routing support (moved from @donotdev/crud)
5
5
  *
6
- * @version 0.0.1
6
+ * @version 0.0.3
7
7
  * @since 0.0.1
8
8
  * @author AMBROISE PARK Consulting
9
9
  */
10
- export { DisplayFieldRenderer } from './DisplayFieldRenderer';
11
10
  export { EntityDisplayRenderer } from './EntityDisplayRenderer';
11
+ export { EntityList } from './EntityList';
12
+ export { EntityCardList } from './EntityCardList';
13
+ export { EntityFormRenderer } from './EntityFormRenderer';
12
14
  export * from './Form';
13
- export * from './fields/display';