@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.
- package/dist/crud/components/EntityCardList.d.ts +16 -0
- package/dist/crud/components/EntityCardList.d.ts.map +1 -0
- package/dist/crud/components/EntityCardList.js +175 -0
- package/dist/crud/components/EntityDisplayRenderer.d.ts +13 -21
- package/dist/crud/components/EntityDisplayRenderer.d.ts.map +1 -1
- package/dist/crud/components/EntityDisplayRenderer.js +138 -23
- package/dist/crud/components/EntityFormRenderer.d.ts +18 -0
- package/dist/crud/components/EntityFormRenderer.d.ts.map +1 -0
- package/dist/crud/components/EntityFormRenderer.js +275 -0
- package/dist/crud/components/EntityList.d.ts +14 -0
- package/dist/crud/components/EntityList.d.ts.map +1 -0
- package/dist/crud/components/EntityList.js +201 -0
- package/dist/crud/components/index.d.ts +7 -5
- package/dist/crud/components/index.d.ts.map +1 -1
- package/dist/crud/components/index.js +6 -5
- package/dist/dndev.css +179 -0
- package/dist/index.js +4 -64
- package/dist/internal/layout/components/AutoMetaTags.d.ts.map +1 -1
- package/dist/internal/layout/components/AutoMetaTags.js +36 -6
- package/dist/internal/layout/components/NextJsAutoMetaTags.d.ts.map +1 -1
- package/dist/internal/layout/components/NextJsAutoMetaTags.js +38 -10
- package/dist/internal/layout/components/footer/FooterBranding.js +2 -2
- package/dist/styles/index.css +179 -0
- package/package.json +12 -12
- package/dist/crud/components/DisplayFieldRenderer.d.ts +0 -26
- package/dist/crud/components/DisplayFieldRenderer.d.ts.map +0 -1
- package/dist/crud/components/DisplayFieldRenderer.js +0 -107
- package/dist/crud/components/fields/display/AvatarFieldDisplay.d.ts +0 -23
- package/dist/crud/components/fields/display/AvatarFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/AvatarFieldDisplay.js +0 -38
- package/dist/crud/components/fields/display/BadgeFieldDisplay.d.ts +0 -21
- package/dist/crud/components/fields/display/BadgeFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/BadgeFieldDisplay.js +0 -31
- package/dist/crud/components/fields/display/ButtonFieldDisplay.d.ts +0 -29
- package/dist/crud/components/fields/display/ButtonFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/ButtonFieldDisplay.js +0 -12
- package/dist/crud/components/fields/display/CheckboxFieldDisplay.d.ts +0 -21
- package/dist/crud/components/fields/display/CheckboxFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/CheckboxFieldDisplay.js +0 -27
- package/dist/crud/components/fields/display/DateFieldDisplay.d.ts +0 -24
- package/dist/crud/components/fields/display/DateFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/DateFieldDisplay.js +0 -41
- package/dist/crud/components/fields/display/DropdownDisplay.d.ts +0 -21
- package/dist/crud/components/fields/display/DropdownDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/DropdownDisplay.js +0 -25
- package/dist/crud/components/fields/display/FileFieldDisplay.d.ts +0 -21
- package/dist/crud/components/fields/display/FileFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/FileFieldDisplay.js +0 -25
- package/dist/crud/components/fields/display/GeoPointFieldDisplay.d.ts +0 -25
- package/dist/crud/components/fields/display/GeoPointFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/GeoPointFieldDisplay.js +0 -25
- package/dist/crud/components/fields/display/HiddenFieldDisplay.d.ts +0 -30
- package/dist/crud/components/fields/display/HiddenFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/HiddenFieldDisplay.js +0 -12
- package/dist/crud/components/fields/display/ImageFieldDisplay.d.ts +0 -24
- package/dist/crud/components/fields/display/ImageFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/ImageFieldDisplay.js +0 -38
- package/dist/crud/components/fields/display/LinkFieldDisplay.d.ts +0 -22
- package/dist/crud/components/fields/display/LinkFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/LinkFieldDisplay.js +0 -48
- package/dist/crud/components/fields/display/MapFieldDisplay.d.ts +0 -25
- package/dist/crud/components/fields/display/MapFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/MapFieldDisplay.js +0 -25
- package/dist/crud/components/fields/display/MultiDropdownDisplay.d.ts +0 -22
- package/dist/crud/components/fields/display/MultiDropdownDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/MultiDropdownDisplay.js +0 -25
- package/dist/crud/components/fields/display/MultiInputTextFieldDisplay.d.ts +0 -22
- package/dist/crud/components/fields/display/MultiInputTextFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/MultiInputTextFieldDisplay.js +0 -25
- package/dist/crud/components/fields/display/NumberFieldDisplay.d.ts +0 -24
- package/dist/crud/components/fields/display/NumberFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/NumberFieldDisplay.js +0 -28
- package/dist/crud/components/fields/display/PasswordFieldDisplay.d.ts +0 -24
- package/dist/crud/components/fields/display/PasswordFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/PasswordFieldDisplay.js +0 -31
- package/dist/crud/components/fields/display/PhoneNumberDisplay.d.ts +0 -22
- package/dist/crud/components/fields/display/PhoneNumberDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/PhoneNumberDisplay.js +0 -25
- package/dist/crud/components/fields/display/RadioFieldDisplay.d.ts +0 -22
- package/dist/crud/components/fields/display/RadioFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/RadioFieldDisplay.js +0 -25
- package/dist/crud/components/fields/display/RangeFieldDisplay.d.ts +0 -22
- package/dist/crud/components/fields/display/RangeFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/RangeFieldDisplay.js +0 -25
- package/dist/crud/components/fields/display/ReferenceFieldDisplay.d.ts +0 -22
- package/dist/crud/components/fields/display/ReferenceFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/ReferenceFieldDisplay.js +0 -26
- package/dist/crud/components/fields/display/RichTextDisplay.d.ts +0 -25
- package/dist/crud/components/fields/display/RichTextDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/RichTextDisplay.js +0 -104
- package/dist/crud/components/fields/display/TextAreaDisplay.d.ts +0 -22
- package/dist/crud/components/fields/display/TextAreaDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/TextAreaDisplay.js +0 -25
- package/dist/crud/components/fields/display/TextFieldDisplay.d.ts +0 -42
- package/dist/crud/components/fields/display/TextFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/TextFieldDisplay.js +0 -97
- package/dist/crud/components/fields/display/TimestampFieldDisplay.d.ts +0 -22
- package/dist/crud/components/fields/display/TimestampFieldDisplay.d.ts.map +0 -1
- package/dist/crud/components/fields/display/TimestampFieldDisplay.js +0 -33
- package/dist/crud/components/fields/display/index.d.ts +0 -32
- package/dist/crud/components/fields/display/index.d.ts.map +0 -1
- package/dist/crud/components/fields/display/index.js +0 -32
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EntityCardListProps } from '@donotdev/core';
|
|
2
|
+
export type { EntityCardListProps };
|
|
3
|
+
/**
|
|
4
|
+
* Entity Card List Component - Card grid view for public/user-facing browsing
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Responsive card grid layout
|
|
8
|
+
* - Image + key fields display
|
|
9
|
+
* - Click card to navigate to detail
|
|
10
|
+
* - Simple formatted text display (labels + values)
|
|
11
|
+
* - Empty state handling
|
|
12
|
+
* - Auto-routing when handler not provided
|
|
13
|
+
*/
|
|
14
|
+
export declare function EntityCardList({ entity, basePath, onClick, cols, staleTime, // 30 minutes default cache
|
|
15
|
+
filter, hideFilters, }: EntityCardListProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=EntityCardList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EntityCardList.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityCardList.tsx"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAmB,EACnB,SAA0B,EAAE,2BAA2B;AACvD,MAAM,EACN,WAAmB,GACpB,EAAE,mBAAmB,2CA0RrB"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
// packages/ui/src/crud/components/EntityCardList.tsx
|
|
4
|
+
/**
|
|
5
|
+
* @fileoverview Entity Card List Component
|
|
6
|
+
* @description Card grid view for public/user-facing entity browsing.
|
|
7
|
+
* Features: Responsive grid of cards with images, key fields, and navigation to detail pages.
|
|
8
|
+
*
|
|
9
|
+
* **Routing:** Convention basePath = `/${collection}`. View = basePath/:id.
|
|
10
|
+
* Override basePath for nested routes; use onClick(id) to open sheet instead of navigating.
|
|
11
|
+
*
|
|
12
|
+
* @version 0.2.0
|
|
13
|
+
* @since 0.0.1
|
|
14
|
+
* @author AMBROISE PARK Consulting
|
|
15
|
+
*/
|
|
16
|
+
import { useMemo, useCallback, useState } from 'react';
|
|
17
|
+
import { Heart } from 'lucide-react';
|
|
18
|
+
import { Grid, Card, Stack, Text, Spinner, Section, Button, } from '@donotdev/components';
|
|
19
|
+
import { useTranslation } from '@donotdev/core';
|
|
20
|
+
import { useNavigate } from '../../routing';
|
|
21
|
+
import { translateFieldLabel, useCrudCardList, EntityFilters, useEntityFavorites, matchesFilter, formatValue, } from '@donotdev/crud';
|
|
22
|
+
/**
|
|
23
|
+
* Entity Card List Component - Card grid view for public/user-facing browsing
|
|
24
|
+
*
|
|
25
|
+
* Features:
|
|
26
|
+
* - Responsive card grid layout
|
|
27
|
+
* - Image + key fields display
|
|
28
|
+
* - Click card to navigate to detail
|
|
29
|
+
* - Simple formatted text display (labels + values)
|
|
30
|
+
* - Empty state handling
|
|
31
|
+
* - Auto-routing when handler not provided
|
|
32
|
+
*/
|
|
33
|
+
export function EntityCardList({ entity, basePath, onClick, cols = [1, 2, 3, 4], staleTime = 1000 * 60 * 30, // 30 minutes default cache
|
|
34
|
+
filter, hideFilters = false, }) {
|
|
35
|
+
const navigate = useNavigate();
|
|
36
|
+
const base = basePath ?? `/${entity.collection}`;
|
|
37
|
+
// useCrudCardList -> handles fetching optimized for cards automatically
|
|
38
|
+
const { data: listData, loading } = useCrudCardList(entity, { staleTime });
|
|
39
|
+
const rawData = listData?.items || [];
|
|
40
|
+
// Favorites - always enabled, no props needed
|
|
41
|
+
const { isFavorite, toggleFavorite, favoritesFilter } = useEntityFavorites({
|
|
42
|
+
collection: entity.collection,
|
|
43
|
+
});
|
|
44
|
+
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false);
|
|
45
|
+
// Filter state
|
|
46
|
+
const [filters, setFilters] = useState({});
|
|
47
|
+
// Apply filters from EntityFilters component
|
|
48
|
+
const applyFilters = useCallback((item) => {
|
|
49
|
+
if (Object.keys(filters).length === 0)
|
|
50
|
+
return true;
|
|
51
|
+
return Object.entries(filters).every(([fieldName, filterValue]) => {
|
|
52
|
+
const itemValue = item[fieldName];
|
|
53
|
+
const fieldConfig = entity.fields[fieldName];
|
|
54
|
+
const fieldType = fieldConfig?.type || 'text';
|
|
55
|
+
return matchesFilter(itemValue, filterValue, fieldType);
|
|
56
|
+
});
|
|
57
|
+
}, [filters, entity.fields]);
|
|
58
|
+
// Apply client-side filtering (including favorites)
|
|
59
|
+
const data = useMemo(() => {
|
|
60
|
+
let result = rawData;
|
|
61
|
+
result = result.filter(applyFilters);
|
|
62
|
+
// Apply favorites filter if enabled
|
|
63
|
+
if (showFavoritesOnly) {
|
|
64
|
+
result = result.filter(favoritesFilter);
|
|
65
|
+
}
|
|
66
|
+
if (filter) {
|
|
67
|
+
result = result.filter(filter);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}, [rawData, applyFilters, showFavoritesOnly, favoritesFilter, filter]);
|
|
71
|
+
// Entity + crud namespaces so formatValue can resolve crud:price.* etc.
|
|
72
|
+
const { t } = useTranslation([entity.namespace, 'crud']);
|
|
73
|
+
const { t: tCrud } = useTranslation('crud');
|
|
74
|
+
// Card click: onClick(id) if provided, else navigate to basePath/:id
|
|
75
|
+
const handleView = useCallback((id) => {
|
|
76
|
+
if (onClick) {
|
|
77
|
+
onClick(id);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
navigate(`${base}/${id}`);
|
|
81
|
+
}
|
|
82
|
+
}, [base, navigate, onClick]);
|
|
83
|
+
// Determine which fields to show in cards
|
|
84
|
+
const fieldsToShow = useMemo(() => {
|
|
85
|
+
const cardFields = entity.listCardFields ?? entity.listFields;
|
|
86
|
+
if (cardFields && cardFields.length > 0)
|
|
87
|
+
return cardFields;
|
|
88
|
+
return Object.keys(entity.fields).slice(0, 4);
|
|
89
|
+
}, [entity.listCardFields, entity.listFields, entity.fields]);
|
|
90
|
+
// Find image field
|
|
91
|
+
const imageField = useMemo(() => {
|
|
92
|
+
const imageFieldsInList = fieldsToShow.filter((fieldName) => {
|
|
93
|
+
const fieldConfig = entity.fields[fieldName];
|
|
94
|
+
return fieldConfig?.type === 'image' || fieldConfig?.type === 'images';
|
|
95
|
+
});
|
|
96
|
+
if (imageFieldsInList.length > 0)
|
|
97
|
+
return imageFieldsInList[0];
|
|
98
|
+
// Fallback: search all entity fields
|
|
99
|
+
const allImageFields = Object.keys(entity.fields).filter((fieldName) => {
|
|
100
|
+
const fieldConfig = entity.fields[fieldName];
|
|
101
|
+
return fieldConfig?.type === 'image' || fieldConfig?.type === 'images';
|
|
102
|
+
});
|
|
103
|
+
return allImageFields[0] || null;
|
|
104
|
+
}, [fieldsToShow, entity.fields]);
|
|
105
|
+
// Get other fields (non-image)
|
|
106
|
+
const otherFields = useMemo(() => {
|
|
107
|
+
return fieldsToShow.filter((fieldName) => fieldName !== imageField);
|
|
108
|
+
}, [fieldsToShow, imageField]);
|
|
109
|
+
const entityName = t('name', { defaultValue: entity.name });
|
|
110
|
+
return (_jsxs(_Fragment, { children: [!hideFilters && (_jsx(Section, { title: tCrud('filters.title', {
|
|
111
|
+
entity: entityName,
|
|
112
|
+
defaultValue: `Browse ${entityName} - Filters`,
|
|
113
|
+
}), collapsible: true, defaultOpen: true, children: _jsxs(Stack, { direction: "column", gap: "medium", children: [_jsx(Button, { variant: showFavoritesOnly ? 'primary' : 'outline', icon: _jsx(Heart, { size: 18 }), onClick: () => setShowFavoritesOnly(!showFavoritesOnly), children: showFavoritesOnly
|
|
114
|
+
? tCrud('favorites.showAll', { defaultValue: 'Show All' })
|
|
115
|
+
: tCrud('favorites.showFavorites', {
|
|
116
|
+
defaultValue: 'Show Favorites',
|
|
117
|
+
}) }), _jsx(EntityFilters, { entity: entity, data: rawData, filters: filters, onFiltersChange: setFilters, fieldsToFilter: entity.listCardFields })] }) })), _jsx(Section, { title: loading
|
|
118
|
+
? tCrud('results.title.fetching', { defaultValue: 'Fetching...' })
|
|
119
|
+
: tCrud('results.title.count', {
|
|
120
|
+
count: data.length,
|
|
121
|
+
defaultValue: data.length === 1
|
|
122
|
+
? 'Found 1 occurrence'
|
|
123
|
+
: `Found ${data.length} occurrences`,
|
|
124
|
+
}), collapsible: true, defaultOpen: true, children: loading ? (_jsx(Stack, { align: "center", justify: "center", gap: "medium", style: { padding: 'var(--gap-3xl)' }, children: _jsx(Spinner, {}) })) : data.length === 0 ? (_jsxs(Stack, { align: "center", justify: "center", gap: "medium", style: { padding: 'var(--gap-3xl)', textAlign: 'center' }, children: [_jsx(Text, { level: "h3", style: { color: 'var(--muted-foreground)' }, children: tCrud('emptyState.title', {
|
|
125
|
+
defaultValue: `No ${entity.name.toLowerCase()} found`,
|
|
126
|
+
}) }), _jsx(Text, { style: { color: 'var(--muted-foreground)' }, children: tCrud('emptyState.description', {
|
|
127
|
+
defaultValue: `No ${entity.name.toLowerCase()} available at this time.`,
|
|
128
|
+
}) })] })) : (_jsx(Grid, { cols: cols, gap: "medium", children: data.map((item) => {
|
|
129
|
+
const imageValue = imageField ? item[imageField] : null;
|
|
130
|
+
// Backend optimizes picture fields for listCard: returns thumbUrl string directly
|
|
131
|
+
// (or fullUrl if thumbUrl missing, or null if no picture)
|
|
132
|
+
const imageUrl = typeof imageValue === 'string' ? imageValue : null;
|
|
133
|
+
// Title from first non-image field
|
|
134
|
+
const titleField = otherFields[0];
|
|
135
|
+
const titleValue = titleField ? item[titleField] : item.id;
|
|
136
|
+
const itemIsFavorite = isFavorite(item.id);
|
|
137
|
+
return (_jsxs(Card, { title: String(titleValue || ''), clickable: true, onClick: () => handleView(item.id), elevated: true, children: [_jsx(Heart, { fill: itemIsFavorite ? '#ef4444' : '#ffffff', stroke: itemIsFavorite ? '#ef4444' : 'var(--muted-foreground)', onClick: (e) => {
|
|
138
|
+
e.stopPropagation();
|
|
139
|
+
toggleFavorite(item.id);
|
|
140
|
+
}, style: {
|
|
141
|
+
position: 'absolute',
|
|
142
|
+
top: 'var(--gap-sm)',
|
|
143
|
+
right: 'var(--gap-sm)',
|
|
144
|
+
zIndex: 10,
|
|
145
|
+
cursor: 'pointer',
|
|
146
|
+
width: 'var(--icon-md)',
|
|
147
|
+
height: 'var(--icon-md)',
|
|
148
|
+
transition: 'fill 0.2s, stroke 0.2s',
|
|
149
|
+
}, "aria-label": itemIsFavorite
|
|
150
|
+
? tCrud('favorites.remove', {
|
|
151
|
+
defaultValue: 'Remove from favorites',
|
|
152
|
+
})
|
|
153
|
+
: tCrud('favorites.add', {
|
|
154
|
+
defaultValue: 'Add to favorites',
|
|
155
|
+
}) }), _jsxs(Stack, { direction: "column", gap: "medium", children: [imageUrl && (_jsx("div", { style: {
|
|
156
|
+
width: '100%',
|
|
157
|
+
aspectRatio: '16/9',
|
|
158
|
+
borderRadius: 'var(--radius-md)',
|
|
159
|
+
overflow: 'hidden',
|
|
160
|
+
backgroundColor: 'var(--muted)',
|
|
161
|
+
position: 'relative',
|
|
162
|
+
}, children: _jsx("img", { src: imageUrl, alt: String(titleValue || ''), style: {
|
|
163
|
+
width: '100%',
|
|
164
|
+
height: '100%',
|
|
165
|
+
objectFit: 'cover',
|
|
166
|
+
} }) })), _jsx(Stack, { direction: "column", gap: "tight", children: otherFields.slice(1, 4).map((fieldName) => {
|
|
167
|
+
const fieldConfig = entity.fields[fieldName];
|
|
168
|
+
if (!fieldConfig)
|
|
169
|
+
return null;
|
|
170
|
+
return (_jsxs("div", { children: [_jsx(Text, { level: "small", variant: "muted", children: translateFieldLabel(fieldName, fieldConfig, t) }), _jsx(Text, { children: formatValue(item[fieldName], fieldConfig, t, {
|
|
171
|
+
compact: true,
|
|
172
|
+
}) })] }, fieldName));
|
|
173
|
+
}) })] })] }, item.id));
|
|
174
|
+
}) })) })] }));
|
|
175
|
+
}
|
|
@@ -1,29 +1,21 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
/** Entity definition with field configs */
|
|
4
|
-
entity: Record<string, EntityField>;
|
|
5
|
-
/** Entity data to display */
|
|
6
|
-
data: T;
|
|
7
|
-
/** Translation function */
|
|
8
|
-
t?: (key: string, options?: Record<string, any>) => string;
|
|
9
|
-
/** Additional CSS classes */
|
|
10
|
-
className?: string;
|
|
11
|
-
/** Whether data is loading */
|
|
12
|
-
loading?: boolean;
|
|
13
|
-
}
|
|
1
|
+
import type { EntityDisplayRendererProps, EntityRecord } from '@donotdev/core';
|
|
2
|
+
export type { EntityDisplayRendererProps };
|
|
14
3
|
/**
|
|
15
|
-
* EntityDisplayRenderer -
|
|
4
|
+
* EntityDisplayRenderer - Automatically fetches and displays entity data
|
|
16
5
|
*
|
|
17
6
|
* Features:
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
7
|
+
* - Automatic data fetching using useCrud
|
|
8
|
+
* - Loading state handling
|
|
9
|
+
* - Error handling
|
|
10
|
+
* - Automatic field rendering (respects visibility rules)
|
|
20
11
|
* - Full i18n support
|
|
21
|
-
* -
|
|
12
|
+
* - No form/submit buttons - pure read-only display
|
|
22
13
|
*
|
|
23
|
-
* @
|
|
24
|
-
*
|
|
25
|
-
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <EntityDisplayRenderer entity={carEntity} id={carId} />
|
|
17
|
+
* ```
|
|
26
18
|
*/
|
|
27
|
-
export declare function EntityDisplayRenderer<T extends
|
|
19
|
+
export declare function EntityDisplayRenderer<T extends EntityRecord = EntityRecord>({ entity, id, t, className, backend, loadingMessage, notFoundMessage, viewerRole, }: EntityDisplayRendererProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
28
20
|
export default EntityDisplayRenderer;
|
|
29
21
|
//# sourceMappingURL=EntityDisplayRenderer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EntityDisplayRenderer.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityDisplayRenderer.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"EntityDisplayRenderer.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityDisplayRenderer.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,0BAA0B,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG/E,YAAY,EAAE,0BAA0B,EAAE,CAAC;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAAE,EAC3E,MAAM,EACN,EAAE,EACF,CAAC,EACD,SAAc,EACd,OAAqB,EACrB,cAAc,EACd,eAAe,EACf,UAAoB,GACrB,EAAE,0BAA0B,CAAC,CAAC,CAAC,2CAgM/B;AAED,eAAe,qBAAqB,CAAC"}
|
|
@@ -3,39 +3,154 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// packages/ui/src/crud/components/EntityDisplayRenderer.tsx
|
|
4
4
|
/**
|
|
5
5
|
* @fileoverview EntityDisplayRenderer component
|
|
6
|
-
* @description High-level component that
|
|
6
|
+
* @description High-level component that automatically fetches and displays entity data in read-only mode.
|
|
7
|
+
* Perfect for detail/view pages - just pass the entity and it handles everything.
|
|
7
8
|
*
|
|
8
|
-
* @version 0.0.
|
|
9
|
+
* @version 0.0.2
|
|
9
10
|
* @since 0.0.1
|
|
10
11
|
* @author AMBROISE PARK Consulting
|
|
11
12
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { useEffect, useState, useMemo } from 'react';
|
|
14
|
+
import { Stack, Spinner } from '@donotdev/components';
|
|
15
|
+
import { useTranslation, isFieldVisible } from '@donotdev/core';
|
|
16
|
+
import { useCrud, DisplayFieldRenderer } from '@donotdev/crud';
|
|
15
17
|
/**
|
|
16
|
-
* EntityDisplayRenderer -
|
|
18
|
+
* EntityDisplayRenderer - Automatically fetches and displays entity data
|
|
17
19
|
*
|
|
18
20
|
* Features:
|
|
19
|
-
* -
|
|
20
|
-
* -
|
|
21
|
+
* - Automatic data fetching using useCrud
|
|
22
|
+
* - Loading state handling
|
|
23
|
+
* - Error handling
|
|
24
|
+
* - Automatic field rendering (respects visibility rules)
|
|
21
25
|
* - Full i18n support
|
|
22
|
-
* -
|
|
26
|
+
* - No form/submit buttons - pure read-only display
|
|
23
27
|
*
|
|
24
|
-
* @
|
|
25
|
-
*
|
|
26
|
-
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <EntityDisplayRenderer entity={carEntity} id={carId} />
|
|
31
|
+
* ```
|
|
27
32
|
*/
|
|
28
|
-
export function EntityDisplayRenderer({ entity,
|
|
29
|
-
const {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
export function EntityDisplayRenderer({ entity, id, t, className = '', backend = 'functions', loadingMessage, notFoundMessage, viewerRole = 'guest', }) {
|
|
34
|
+
const { get, loading, data: storeData, error: storeError, isAvailable, } = useCrud(entity, { backend });
|
|
35
|
+
const [isFetching, setIsFetching] = useState(false);
|
|
36
|
+
const [fetchError, setFetchError] = useState(null);
|
|
37
|
+
const [data, setData] = useState(null);
|
|
38
|
+
// Entity + crud namespaces so formatValue can resolve crud:price.* etc.
|
|
39
|
+
const { t: tDual } = useTranslation([entity.namespace, 'crud']);
|
|
40
|
+
const { t: tCrud } = useTranslation('crud');
|
|
41
|
+
const translate = t || tDual;
|
|
42
|
+
// Fetch data when id is available and service is ready
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
// Early return if no ID
|
|
45
|
+
if (!id) {
|
|
46
|
+
setData(null);
|
|
47
|
+
setFetchError(null);
|
|
48
|
+
setIsFetching(false);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Early return if service not ready
|
|
52
|
+
if (!isAvailable || !get) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let cancelled = false;
|
|
56
|
+
setIsFetching(true);
|
|
57
|
+
setFetchError(null);
|
|
58
|
+
// Call get function - it should trigger the fetch
|
|
59
|
+
get(id)
|
|
60
|
+
.then((fetchedData) => {
|
|
61
|
+
if (cancelled)
|
|
62
|
+
return;
|
|
63
|
+
setIsFetching(false);
|
|
64
|
+
if (fetchedData) {
|
|
65
|
+
setData(fetchedData);
|
|
66
|
+
setFetchError(null);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
setData(null);
|
|
70
|
+
setFetchError(new Error('Entity not found'));
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
.catch((err) => {
|
|
74
|
+
if (cancelled)
|
|
75
|
+
return;
|
|
76
|
+
setIsFetching(false);
|
|
77
|
+
setFetchError(err instanceof Error ? err : new Error(String(err)));
|
|
78
|
+
setData(null);
|
|
79
|
+
});
|
|
80
|
+
return () => {
|
|
81
|
+
cancelled = true;
|
|
82
|
+
};
|
|
83
|
+
}, [id, get, isAvailable]);
|
|
84
|
+
// Use store data if available, otherwise use local state
|
|
85
|
+
const displayData = storeData || data;
|
|
86
|
+
const displayError = storeError || fetchError;
|
|
87
|
+
const isLoading = loading || isFetching || !id;
|
|
88
|
+
// Filter fields: respect visibility AND skip empty values
|
|
89
|
+
// MUST be called before any early returns (Rules of Hooks)
|
|
90
|
+
const visibleFields = useMemo(() => {
|
|
91
|
+
if (!displayData)
|
|
92
|
+
return [];
|
|
93
|
+
return Object.entries(entity.fields).filter(([fieldName, fieldConfig]) => {
|
|
94
|
+
// Check visibility first
|
|
95
|
+
if (!isFieldVisible(fieldConfig.visibility, viewerRole)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
// Skip hidden fields
|
|
99
|
+
if (fieldConfig.visibility === 'hidden') {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
// Get field value
|
|
103
|
+
const value = displayData[fieldName];
|
|
104
|
+
// Hide if value is empty/null/undefined
|
|
105
|
+
if (value === null || value === undefined) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
// Hide empty strings
|
|
109
|
+
if (typeof value === 'string' && value.trim() === '') {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
// Hide empty arrays
|
|
113
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
// Hide empty objects (but allow objects with properties)
|
|
117
|
+
if (typeof value === 'object' &&
|
|
118
|
+
!Array.isArray(value) &&
|
|
119
|
+
value !== null &&
|
|
120
|
+
Object.keys(value).length === 0) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
}, [entity.fields, viewerRole, displayData]);
|
|
126
|
+
// Loading state
|
|
127
|
+
if (isLoading) {
|
|
128
|
+
return (_jsx("div", { style: {
|
|
129
|
+
position: 'relative',
|
|
130
|
+
width: '100%',
|
|
131
|
+
gridColumn: '1 / -1',
|
|
132
|
+
display: 'contents',
|
|
133
|
+
}, className: className, children: _jsx(Spinner, { overlay: true, "aria-label": loadingMessage ||
|
|
134
|
+
tCrud('form.loading', { defaultValue: 'Loading...' }) }) }));
|
|
135
|
+
}
|
|
136
|
+
// Error or not found state
|
|
137
|
+
if (displayError || !displayData) {
|
|
138
|
+
return (_jsx(Stack, { align: "center", justify: "center", gap: "medium", style: {
|
|
139
|
+
padding: 'var(--gap-3xl)',
|
|
140
|
+
textAlign: 'center',
|
|
141
|
+
}, className: className, children: _jsxs(Stack, { direction: "column", gap: "tight", children: [_jsx("h3", { style: { color: 'var(--muted-foreground)' }, children: notFoundMessage ||
|
|
142
|
+
tCrud('errors.notFound', {
|
|
143
|
+
defaultValue: `${entity.name} not found`,
|
|
144
|
+
}) }), displayError && (_jsx("p", { style: {
|
|
145
|
+
color: 'var(--destructive)',
|
|
146
|
+
fontSize: 'var(--font-size-sm)',
|
|
147
|
+
}, children: displayError instanceof Error
|
|
148
|
+
? displayError.message
|
|
149
|
+
: String(displayError) }))] }) }));
|
|
38
150
|
}
|
|
39
|
-
|
|
151
|
+
// Render all visible fields with values
|
|
152
|
+
return (_jsx(Stack, { direction: "column", gap: "medium", className: className, children: visibleFields.map(([fieldName, fieldConfig]) => {
|
|
153
|
+
return (_jsx(DisplayFieldRenderer, { name: fieldName, config: fieldConfig, value: displayData[fieldName], t: translate }, fieldName));
|
|
154
|
+
}) }));
|
|
40
155
|
}
|
|
41
156
|
export default EntityDisplayRenderer;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EntityFormRendererProps, EntityRecord } from '@donotdev/core';
|
|
2
|
+
export type { EntityFormRendererProps };
|
|
3
|
+
/**
|
|
4
|
+
* EntityFormRenderer - Dumb component that renders a form from entity definition.
|
|
5
|
+
*
|
|
6
|
+
* All orchestration (uploads, validation, status tracking) is handled by useEntityForm.
|
|
7
|
+
* This component just:
|
|
8
|
+
* - Generates formId
|
|
9
|
+
* - Renders fields
|
|
10
|
+
* - Renders submit button with status from useEntityForm
|
|
11
|
+
*
|
|
12
|
+
* @version 0.0.7
|
|
13
|
+
* @since 0.0.1
|
|
14
|
+
* @author AMBROISE PARK Consulting
|
|
15
|
+
*/
|
|
16
|
+
export declare function EntityFormRenderer<T extends EntityRecord = EntityRecord>({ entity, onSubmit, t, className, submitText, loading, defaultValues, submitVariant, secondaryButtonText, secondaryButtonVariant, onSecondarySubmit, viewerRole, operation, autoSave, formId: externalFormId, cancelText, cancelPath, successPath, onCancel, warnOnUnsavedChanges, unsavedChangesMessage, hideVisibilityInfo, }: EntityFormRendererProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export default EntityFormRenderer;
|
|
18
|
+
//# sourceMappingURL=EntityFormRenderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EntityFormRenderer.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityFormRenderer.tsx"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EACV,uBAAuB,EACvB,YAAY,EAEb,MAAM,gBAAgB,CAAC;AAExB,YAAY,EAAE,uBAAuB,EAAE,CAAC;AAExC;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAAE,EACxE,MAAM,EACN,QAAQ,EACR,CAAC,EACD,SAAc,EACd,UAAU,EACV,OAAe,EACf,aAAa,EACb,aAAyB,EACzB,mBAAmB,EACnB,sBAAkC,EAClC,iBAAiB,EACjB,UAAU,EACV,SAA6C,EAC7C,QAAiC,EACjC,MAAM,EAAE,cAAc,EACtB,UAAU,EACV,UAAU,EACV,WAAW,EACX,QAAQ,EACR,oBAA2B,EAC3B,qBAAqB,EACrB,kBAA0B,GAC3B,EAAE,uBAAuB,CAAC,CAAC,CAAC,2CA+a5B;AAED,eAAe,kBAAkB,CAAC"}
|