@donotdev/cli 0.0.8 → 0.0.9
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/dependencies-matrix.json +158 -74
- package/dist/bin/commands/create-app.js +43 -6
- package/dist/bin/commands/create-project.js +60 -7
- package/dist/bin/commands/deploy.js +111 -22
- package/dist/bin/dndev.js +7 -4
- package/dist/bin/donotdev.js +7 -4
- package/dist/index.js +174 -30
- package/package.json +4 -3
- package/templates/app-next/src/config/app.ts.example +1 -1
- package/templates/app-vite/index.html.example +24 -2
- package/templates/app-vite/src/config/app.ts.example +1 -1
- package/templates/app-vite/src/pages/FormPageExample.tsx.example +8 -5
- package/templates/app-vite/src/pages/ListPageExample.tsx.example +4 -7
- package/templates/root-consumer/.firebaserc.example +5 -0
- package/templates/root-consumer/entities/ExampleEntity.ts.example +2 -1
- package/templates/root-consumer/entities/demo.ts.example +1 -1
- package/templates/root-consumer/firestore.indexes.json.example +4 -0
- package/templates/root-consumer/firestore.rules.example +11 -0
- package/templates/root-consumer/guides/dndev/COMPONENTS_CRUD.md.example +9 -6
- package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +329 -57
- package/templates/root-consumer/guides/wai-way/entity_patterns.md.example +1 -1
- package/templates/root-consumer/storage.rules.example +8 -0
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## Quick Import Reference
|
|
8
|
+
|
|
9
|
+
| What | Package |
|
|
10
|
+
|------|---------|
|
|
11
|
+
| **Hooks** (`useCrud`, `useCrudList`, `useCrudCardList`, `useEntityForm`, `useEntityFavorites`) | `@donotdev/crud` |
|
|
12
|
+
| **Display Utilities** (`formatValue`, `DisplayFieldRenderer`, `EntityFilters`, `translateFieldLabel`) | `@donotdev/crud` |
|
|
13
|
+
| **Form Components** (`FormFieldRenderer`, `Controlled*Field`, `UploadProvider`) | `@donotdev/crud` |
|
|
14
|
+
| **Routing-Aware Components** (`EntityFormRenderer`, `EntityList`, `EntityCardList`, `EntityDisplayRenderer`) | `@donotdev/ui` |
|
|
15
|
+
| **Specialized Templates** (`ProductCardListTemplate`, `CarCardListTemplate`) | `@donotdev/templates` |
|
|
16
|
+
| **Routing** (`useNavigate`, `Link`, `useParams`) | `@donotdev/ui` |
|
|
17
|
+
| **Entity Definition** (`defineEntity`) | `@donotdev/core` |
|
|
18
|
+
|
|
19
|
+
**Architecture:** Hooks and display utilities live in `@donotdev/crud`. Routing-aware page components that compose CRUD with navigation live in `@donotdev/ui`.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
7
23
|
## 1. Define Entity
|
|
8
24
|
|
|
9
25
|
```typescript
|
|
@@ -25,9 +41,16 @@ export const productEntity = defineEntity({
|
|
|
25
41
|
price: {
|
|
26
42
|
name: 'price',
|
|
27
43
|
label: 'price',
|
|
28
|
-
type: '
|
|
44
|
+
type: 'price', // Structured: { amount, currency?, vatIncluded?, discountPercent? }
|
|
29
45
|
visibility: 'guest',
|
|
30
|
-
validation: { required: true }
|
|
46
|
+
validation: { required: true },
|
|
47
|
+
// Optional: omit options entirely → defaultCurrency 'EUR', list = [EUR, USD, GBP, CHF, JPY, KRW]
|
|
48
|
+
options: {
|
|
49
|
+
fieldSpecific: {
|
|
50
|
+
defaultCurrency: 'EUR', // Omit → 'EUR'. Used when value.currency is unset
|
|
51
|
+
currencies: ['EUR', 'USD'], // Omit → framework default (6 currencies). One item e.g. ['EUR'] → no dropdown, EUR only
|
|
52
|
+
},
|
|
53
|
+
},
|
|
31
54
|
},
|
|
32
55
|
image: {
|
|
33
56
|
name: 'image',
|
|
@@ -50,7 +73,72 @@ export const productEntity = defineEntity({
|
|
|
50
73
|
|
|
51
74
|
---
|
|
52
75
|
|
|
53
|
-
## 2.
|
|
76
|
+
## 2. Multi-Tenancy (Company/Tenant Scoping)
|
|
77
|
+
|
|
78
|
+
For multi-tenant apps where data belongs to a company/tenant/workspace:
|
|
79
|
+
|
|
80
|
+
### Step 1: Register Scope Provider (once at app startup)
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// src/main.tsx or src/App.tsx
|
|
84
|
+
import { registerScopeProvider } from '@donotdev/core';
|
|
85
|
+
import { useCurrentCompanyStore } from './stores/currentCompanyStore';
|
|
86
|
+
|
|
87
|
+
// Register before app renders
|
|
88
|
+
registerScopeProvider('company', () =>
|
|
89
|
+
useCurrentCompanyStore.getState().currentCompanyId
|
|
90
|
+
);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Step 2: Add `scope` to Entity Definition
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// entities/client.ts
|
|
97
|
+
import { defineEntity } from '@donotdev/core';
|
|
98
|
+
|
|
99
|
+
export const clientEntity = defineEntity({
|
|
100
|
+
name: 'Client',
|
|
101
|
+
collection: 'clients',
|
|
102
|
+
scope: { field: 'companyId', provider: 'company' }, // <-- Add this
|
|
103
|
+
fields: {
|
|
104
|
+
// NO need to manually define companyId - framework adds it automatically
|
|
105
|
+
name: { name: 'name', label: 'name', type: 'text', visibility: 'user', validation: { required: true } },
|
|
106
|
+
// ...
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### What Happens Automatically
|
|
112
|
+
|
|
113
|
+
| Operation | Behavior |
|
|
114
|
+
|-----------|----------|
|
|
115
|
+
| `add()` / `set()` | Auto-injects `companyId` from scope provider |
|
|
116
|
+
| `useCrudList()` | Auto-filters by `companyId == currentCompanyId` |
|
|
117
|
+
| `useCrudCardList()` | Auto-filters by `companyId == currentCompanyId` |
|
|
118
|
+
| `query()` | Auto-adds `where companyId == currentCompanyId` |
|
|
119
|
+
|
|
120
|
+
**Result:** Zero boilerplate. Users only see data from their current company.
|
|
121
|
+
|
|
122
|
+
### Scope Field Properties
|
|
123
|
+
|
|
124
|
+
The auto-added scope field has:
|
|
125
|
+
- `type: 'reference'` (references the scoped collection, e.g., `companies`)
|
|
126
|
+
- `visibility: 'technical'` (hidden from regular users)
|
|
127
|
+
- `editable: 'create-only'` (cannot change scope after creation)
|
|
128
|
+
|
|
129
|
+
### Multiple Scope Types
|
|
130
|
+
|
|
131
|
+
You can register multiple scope providers for different use cases:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
registerScopeProvider('company', () => companyStore.getState().currentCompanyId);
|
|
135
|
+
registerScopeProvider('workspace', () => workspaceStore.getState().currentWorkspaceId);
|
|
136
|
+
registerScopeProvider('tenant', () => tenantStore.getState().currentTenantId);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 3. Visibility & Access
|
|
54
142
|
|
|
55
143
|
### Field Visibility (who SEES)
|
|
56
144
|
| Level | Who |
|
|
@@ -65,7 +153,7 @@ export const productEntity = defineEntity({
|
|
|
65
153
|
```typescript
|
|
66
154
|
access: {
|
|
67
155
|
create: 'admin', // default
|
|
68
|
-
read: 'guest', // default
|
|
156
|
+
read: 'guest', // default
|
|
69
157
|
update: 'admin', // default
|
|
70
158
|
delete: 'admin', // default
|
|
71
159
|
}
|
|
@@ -97,10 +185,13 @@ export const crud = createCrudFunctions(entities);
|
|
|
97
185
|
|
|
98
186
|
## 4. Frontend Usage
|
|
99
187
|
|
|
100
|
-
|
|
188
|
+
Use components directly from `@donotdev/ui`. They handle routing automatically - no navigate handlers needed.
|
|
189
|
+
|
|
190
|
+
#### Create/Edit Page
|
|
101
191
|
|
|
102
192
|
```tsx
|
|
103
|
-
import {
|
|
193
|
+
import { useCrud } from '@donotdev/crud';
|
|
194
|
+
import { EntityFormRenderer, useParams } from '@donotdev/ui';
|
|
104
195
|
import { productEntity } from 'entities';
|
|
105
196
|
|
|
106
197
|
export default function ProductPage() {
|
|
@@ -115,10 +206,9 @@ export default function ProductPage() {
|
|
|
115
206
|
|
|
116
207
|
const handleSubmit = async (formData: any) => {
|
|
117
208
|
isNew ? await add(formData) : await update(id!, formData);
|
|
118
|
-
|
|
209
|
+
// Navigation happens automatically via cancelPath (defaults to /products)
|
|
119
210
|
};
|
|
120
211
|
|
|
121
|
-
// DON'T mount form until data ready (edit mode)
|
|
122
212
|
if (!isNew && !data) return <Spinner />;
|
|
123
213
|
|
|
124
214
|
return (
|
|
@@ -127,82 +217,191 @@ export default function ProductPage() {
|
|
|
127
217
|
operation={isNew ? 'create' : 'edit'}
|
|
128
218
|
defaultValues={data}
|
|
129
219
|
onSubmit={handleSubmit}
|
|
220
|
+
// Cancel automatically navigates to /products (or cancelPath if provided)
|
|
221
|
+
cancelPath="/products"
|
|
130
222
|
/>
|
|
131
223
|
);
|
|
132
224
|
}
|
|
133
225
|
```
|
|
134
226
|
|
|
135
|
-
|
|
227
|
+
#### List Page
|
|
136
228
|
|
|
137
229
|
```tsx
|
|
138
|
-
import { EntityList } from '@donotdev/
|
|
230
|
+
import { EntityList, PageContainer } from '@donotdev/ui';
|
|
139
231
|
import { useAuth } from '@donotdev/auth';
|
|
140
|
-
import { PageContainer, useNavigate } from '@donotdev/ui';
|
|
141
232
|
import { productEntity } from 'entities';
|
|
142
233
|
|
|
143
234
|
export default function ProductsPage() {
|
|
144
235
|
const user = useAuth('user');
|
|
145
|
-
const navigate = useNavigate();
|
|
146
236
|
|
|
147
237
|
return (
|
|
148
238
|
<PageContainer>
|
|
149
239
|
<EntityList
|
|
150
240
|
entity={productEntity}
|
|
151
241
|
userRole={user?.role}
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
// Optional: Create handler - navigates to create page when "Add New" clicked
|
|
155
|
-
onCreate={() => navigate('/products/new')}
|
|
156
|
-
// Optional: View handler - called when row is clicked
|
|
157
|
-
onView={(id) => navigate(`/products/${id}`)}
|
|
242
|
+
// Routing: basePath defaults to /products. View/Edit = basePath/:id, Create = basePath/new
|
|
243
|
+
// Optional: basePath="/admin/products" or onClick={(id) => openSheet(id)}
|
|
158
244
|
/>
|
|
159
245
|
</PageContainer>
|
|
160
246
|
);
|
|
161
247
|
}
|
|
162
248
|
```
|
|
163
249
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
250
|
+
#### Card Grid
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
import { EntityCardList, PageContainer } from '@donotdev/ui';
|
|
254
|
+
import { productEntity } from 'entities';
|
|
169
255
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
256
|
+
export default function ShopPage() {
|
|
257
|
+
return (
|
|
258
|
+
<PageContainer>
|
|
259
|
+
<EntityCardList
|
|
260
|
+
entity={productEntity}
|
|
261
|
+
// Routing: basePath defaults to /products. View = basePath/:id
|
|
262
|
+
// Optional: basePath="/shop/products" or onClick={(id) => openSheet(id)}
|
|
263
|
+
/>
|
|
264
|
+
</PageContainer>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
```
|
|
173
268
|
|
|
174
|
-
|
|
269
|
+
#### Detail/View Page
|
|
175
270
|
|
|
176
271
|
```tsx
|
|
177
|
-
import {
|
|
178
|
-
import { PageContainer, useNavigate } from '@donotdev/ui';
|
|
272
|
+
import { EntityDisplayRenderer, useParams } from '@donotdev/ui';
|
|
179
273
|
import { productEntity } from 'entities';
|
|
180
274
|
|
|
181
|
-
export default function
|
|
275
|
+
export default function ProductDetailPage() {
|
|
276
|
+
const { id } = useParams<{ id: string }>();
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<EntityDisplayRenderer
|
|
280
|
+
entity={productEntity}
|
|
281
|
+
id={id}
|
|
282
|
+
// Auto-fetches data, shows loading state
|
|
283
|
+
// Read-only display of all visible fields
|
|
284
|
+
/>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
#### Custom Detail Page with Favorites
|
|
290
|
+
|
|
291
|
+
For custom detail pages, add favorites support using `useEntityFavorites`:
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
import { useState, useEffect } from 'react';
|
|
295
|
+
import { Heart } from 'lucide-react';
|
|
296
|
+
import { PageContainer, Section, Button, Stack } from '@donotdev/components';
|
|
297
|
+
import { useParams, useNavigate } from '@donotdev/ui';
|
|
298
|
+
import { useCrud, useEntityFavorites } from '@donotdev/crud';
|
|
299
|
+
import { useTranslation } from '@donotdev/core';
|
|
300
|
+
import { productEntity } from 'entities';
|
|
301
|
+
|
|
302
|
+
export default function ProductDetailPage() {
|
|
303
|
+
const { id } = useParams<{ id: string }>();
|
|
182
304
|
const navigate = useNavigate();
|
|
305
|
+
const { get } = useCrud(productEntity);
|
|
306
|
+
const { isFavorite, toggleFavorite } = useEntityFavorites({
|
|
307
|
+
collection: productEntity.collection,
|
|
308
|
+
});
|
|
309
|
+
const { t } = useTranslation('crud');
|
|
310
|
+
const [data, setData] = useState<any>(null);
|
|
311
|
+
const [loading, setLoading] = useState(true);
|
|
312
|
+
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
if (id) {
|
|
315
|
+
get(id).then((result) => {
|
|
316
|
+
setData(result);
|
|
317
|
+
setLoading(false);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}, [id, get]);
|
|
321
|
+
|
|
322
|
+
if (loading) return <div>Loading...</div>;
|
|
323
|
+
if (!data) return <div>Not found</div>;
|
|
183
324
|
|
|
184
325
|
return (
|
|
185
326
|
<PageContainer>
|
|
186
|
-
<
|
|
327
|
+
<Section>
|
|
328
|
+
<Stack direction="row" gap="medium" align="center">
|
|
329
|
+
<h1>{data.name}</h1>
|
|
330
|
+
<Button
|
|
331
|
+
variant={isFavorite(id) ? 'primary' : 'outline'}
|
|
332
|
+
icon={<Heart size={18} fill={isFavorite(id) ? 'currentColor' : 'none'} />}
|
|
333
|
+
onClick={() => toggleFavorite(id)}
|
|
334
|
+
>
|
|
335
|
+
{isFavorite(id) ? t('favorites.saved') : t('favorites.save')}
|
|
336
|
+
</Button>
|
|
337
|
+
</Stack>
|
|
338
|
+
{/* Your custom detail page content */}
|
|
339
|
+
</Section>
|
|
340
|
+
</PageContainer>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Note:** `EntityCardList` automatically includes favorites support - heart icons on cards and a favorites filter toggle. No configuration needed.
|
|
346
|
+
|
|
347
|
+
#### Product Shop (Specialized Template)
|
|
348
|
+
|
|
349
|
+
For e-commerce/shop pages with price, image, and category support, use the specialized template:
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
import { ProductCardListTemplate } from '@donotdev/templates';
|
|
353
|
+
import { productEntity } from 'entities';
|
|
354
|
+
|
|
355
|
+
export default function ShopPage() {
|
|
356
|
+
return (
|
|
357
|
+
<PageContainer>
|
|
358
|
+
<ProductCardListTemplate
|
|
187
359
|
entity={productEntity}
|
|
188
|
-
//
|
|
189
|
-
|
|
360
|
+
// Auto-detects price, image, category fields
|
|
361
|
+
// Routing: basePath defaults to /products. Optional: basePath or onClick
|
|
190
362
|
/>
|
|
191
363
|
</PageContainer>
|
|
192
364
|
);
|
|
193
365
|
}
|
|
194
366
|
```
|
|
195
367
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
- `
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## 5. Component Props
|
|
371
|
+
|
|
372
|
+
### EntityList
|
|
373
|
+
- `basePath` **(optional)** - Base path for view/edit/create. Defaults to `/${collection}`. View/Edit = basePath/:id, Create = basePath/new
|
|
374
|
+
- `onClick` **(optional)** - Called when user clicks a row. If provided, overrides navigation (e.g. open sheet)
|
|
375
|
+
- `userRole` **(optional)** - Current user role for field visibility
|
|
376
|
+
|
|
377
|
+
### EntityCardList
|
|
378
|
+
- `basePath` **(optional)** - Base path for view. Defaults to `/${collection}`. View = basePath/:id
|
|
379
|
+
- `onClick` **(optional)** - Called when user clicks a card. If provided, overrides navigation (e.g. open sheet)
|
|
380
|
+
- `cols` **(optional)** - `[mobile, tablet, desktop, wide]` (default: `[1, 2, 3, 4]`)
|
|
381
|
+
- `staleTime` **(optional)** - Cache stale time in ms (default: 30 min)
|
|
382
|
+
- `filter` **(optional)** - `(item: any) => boolean` - Client-side filter
|
|
383
|
+
- `hideFilters` **(optional)** - Hide filters section
|
|
384
|
+
|
|
385
|
+
**Favorites:** Automatically enabled - heart icons on cards, favorites filter toggle in filters section. Uses `useEntityFavorites` hook internally with `entity.collection` as storage key.
|
|
386
|
+
|
|
387
|
+
### EntityFormRenderer
|
|
388
|
+
- `entity` **(required)** - Entity definition
|
|
389
|
+
- `onSubmit` **(required)** - `(data: any) => Promise<void>` - Submit handler
|
|
390
|
+
- `defaultValues` **(optional)** - Initial form values (for edit mode)
|
|
391
|
+
- `operation` **(optional)** - `'create' | 'edit'` (auto-detected from defaultValues)
|
|
392
|
+
- `viewerRole` **(optional)** - Role for field visibility/editability
|
|
393
|
+
- `cancelPath` **(optional)** - Path to navigate on cancel (defaults to `/${collection}`)
|
|
394
|
+
- `onCancel` **(optional)** - Cancel button handler (takes precedence over cancelPath)
|
|
395
|
+
- `cancelText` **(optional)** - Cancel button text (null to hide)
|
|
396
|
+
|
|
397
|
+
### EntityDisplayRenderer
|
|
398
|
+
- `entity` **(required)** - Entity definition
|
|
399
|
+
- `id` **(required)** - Entity ID to fetch and display
|
|
400
|
+
- `viewerRole` **(optional)** - Role for field visibility
|
|
202
401
|
|
|
203
402
|
---
|
|
204
403
|
|
|
205
|
-
##
|
|
404
|
+
## 6. Field Types
|
|
206
405
|
|
|
207
406
|
### Text Inputs
|
|
208
407
|
- `text` - Single-line text input
|
|
@@ -216,6 +415,8 @@ export default function ShopPage() {
|
|
|
216
415
|
|
|
217
416
|
### Numbers
|
|
218
417
|
- `number` - Numeric input
|
|
418
|
+
- `currency` - Single amount with currency symbol (value: number; options: `fieldSpecific.currency`)
|
|
419
|
+
- `price` - **Structured price:** amount + currency, VAT label, discount %. Value: `{ amount, currency?, vatIncluded?, discountPercent? }`. Options: `fieldSpecific.defaultCurrency`, `fieldSpecific.currencies` (e.g. `['EUR', 'USD']`; one currency = no dropdown). Form: amount + collapsible options (currency if >1, VAT Incl., discount %). Display: formatted amount; when discounted shows original crossed out + effective price. Filterable by amount (range); "deals" = items with discount.
|
|
219
420
|
- `range` - Slider input
|
|
220
421
|
- `year` - Year input (combobox: type or select from dropdown)
|
|
221
422
|
|
|
@@ -261,6 +462,42 @@ export default function ShopPage() {
|
|
|
261
462
|
- `submit` - Submit button (uncontrolled)
|
|
262
463
|
- `reset` - Reset button (uncontrolled)
|
|
263
464
|
|
|
465
|
+
### Price field type (structured)
|
|
466
|
+
|
|
467
|
+
Use `type: 'price'` when you need amount, currency, VAT label, and optional discount in one field. Replaces separate "price" + "specialPrice" number fields.
|
|
468
|
+
|
|
469
|
+
**Definition:**
|
|
470
|
+
```typescript
|
|
471
|
+
price: {
|
|
472
|
+
name: 'price',
|
|
473
|
+
label: 'price',
|
|
474
|
+
type: 'price',
|
|
475
|
+
visibility: 'guest',
|
|
476
|
+
validation: { required: true },
|
|
477
|
+
options: {
|
|
478
|
+
fieldSpecific: {
|
|
479
|
+
defaultCurrency: 'EUR', // Omit → 'EUR'. Used when value.currency is unset
|
|
480
|
+
currencies: ['EUR', 'USD'], // Omit → list = [EUR, USD, GBP, CHF, JPY, KRW]. One item e.g. ['EUR'] → no dropdown, single currency
|
|
481
|
+
optionsTitle: 'Price options', // Omit → 'Price options'. Aria-label for the expand button
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Value shape (stored in DB):**
|
|
488
|
+
```ts
|
|
489
|
+
{ amount: number; currency?: string; vatIncluded?: boolean; discountPercent?: number }
|
|
490
|
+
```
|
|
491
|
+
- Only `discountPercent` is stored for discount; effective price is computed as `amount * (1 - discountPercent/100)` for display.
|
|
492
|
+
|
|
493
|
+
**Form:** Main input = amount (number) with label "Price (symbol)". Collapsible "+" opens: currency select (if more than one in `currencies`), VAT included (checkbox), discount (%). Single currency → no dropdown, value still stored.
|
|
494
|
+
|
|
495
|
+
**Display:** Formatted amount + "VAT Incl." when `vatIncluded`. When `discountPercent > 0`: original amount crossed out + effective price.
|
|
496
|
+
|
|
497
|
+
**Templates:** `CarCardListTemplate`, `CarDetailTemplate`, `ProductCardListTemplate` use the price field when present and show a "Sale" (or `price.badge.sale` i18n) badge when discounted.
|
|
498
|
+
|
|
499
|
+
**Filtering:** Range filter on `amount`; optional "deals" filter (items with `discountPercent > 0`).
|
|
500
|
+
|
|
264
501
|
### Select Options
|
|
265
502
|
|
|
266
503
|
```typescript
|
|
@@ -282,7 +519,7 @@ Use `type: 'year'` for year inputs (combobox with type-or-select UX):
|
|
|
282
519
|
```typescript
|
|
283
520
|
year: {
|
|
284
521
|
type: 'year',
|
|
285
|
-
validation: {
|
|
522
|
+
validation: {
|
|
286
523
|
required: true,
|
|
287
524
|
min: 1900, // Optional: defaults to 1900
|
|
288
525
|
max: 2100 // Optional: defaults to current year + 10
|
|
@@ -306,20 +543,17 @@ transmission: {
|
|
|
306
543
|
|
|
307
544
|
---
|
|
308
545
|
|
|
309
|
-
##
|
|
546
|
+
## 7. Custom Fields & Schemas
|
|
310
547
|
|
|
311
548
|
### Custom Field Types with UI Components
|
|
312
549
|
|
|
313
550
|
For custom field types that need custom UI components:
|
|
314
551
|
|
|
315
552
|
```typescript
|
|
316
|
-
import { registerFieldType,
|
|
317
|
-
import { useController } from 'react-hook-form';
|
|
318
|
-
import * as v from 'valibot';
|
|
319
|
-
|
|
320
|
-
// Custom controlled component MUST use framework's useController
|
|
321
|
-
import { useController, registerFieldType } from '@donotdev/crud';
|
|
553
|
+
import { registerFieldType, useController } from '@donotdev/crud';
|
|
322
554
|
import type { ControlledFieldProps } from '@donotdev/crud';
|
|
555
|
+
import { defineEntity } from '@donotdev/core';
|
|
556
|
+
import * as v from 'valibot';
|
|
323
557
|
|
|
324
558
|
function RepairOperationsField({
|
|
325
559
|
fieldConfig,
|
|
@@ -335,7 +569,7 @@ function RepairOperationsField({
|
|
|
335
569
|
|
|
336
570
|
// Use field.value and field.onChange for form state
|
|
337
571
|
const value = (field.value as any) || [];
|
|
338
|
-
|
|
572
|
+
|
|
339
573
|
return (
|
|
340
574
|
<div>
|
|
341
575
|
<label>{t(fieldConfig.label)}</label>
|
|
@@ -376,7 +610,7 @@ export const carEntity = defineEntity({
|
|
|
376
610
|
});
|
|
377
611
|
```
|
|
378
612
|
|
|
379
|
-
**Important:**
|
|
613
|
+
**Important:**
|
|
380
614
|
- Custom controlled components receive `control` prop, NOT `field` prop
|
|
381
615
|
- You MUST use `useController` hook to get `field` and `fieldState`
|
|
382
616
|
- Define schema in `validation.schema` - it's the single source of truth
|
|
@@ -386,6 +620,7 @@ export const carEntity = defineEntity({
|
|
|
386
620
|
For custom validation without custom UI, just define the schema:
|
|
387
621
|
|
|
388
622
|
```typescript
|
|
623
|
+
import { defineEntity } from '@donotdev/core';
|
|
389
624
|
import * as v from 'valibot';
|
|
390
625
|
|
|
391
626
|
export const carEntity = defineEntity({
|
|
@@ -436,28 +671,30 @@ export const carEntity = defineEntity({
|
|
|
436
671
|
|
|
437
672
|
---
|
|
438
673
|
|
|
439
|
-
##
|
|
674
|
+
## 8. Hooks API
|
|
440
675
|
|
|
441
676
|
```typescript
|
|
677
|
+
import { useCrud, useCrudList, useCrudCardList } from '@donotdev/crud';
|
|
678
|
+
|
|
442
679
|
// Actions
|
|
443
680
|
const { add, update, delete: remove, get } = useCrud(productEntity);
|
|
444
681
|
|
|
445
682
|
// List (auto-fetch for tables)
|
|
446
|
-
const { items, loading } = useCrudList(productEntity);
|
|
683
|
+
const { items, loading, refresh } = useCrudList(productEntity);
|
|
447
684
|
|
|
448
685
|
// Cards (optimized for public)
|
|
449
|
-
const { items, loading,
|
|
686
|
+
const { items, loading, refresh } = useCrudCardList(productEntity);
|
|
450
687
|
```
|
|
451
688
|
|
|
452
689
|
---
|
|
453
690
|
|
|
454
|
-
##
|
|
691
|
+
## 9. Custom Form Layouts
|
|
455
692
|
|
|
456
693
|
Need a custom layout instead of `EntityFormRenderer`? Use `useEntityForm` directly.
|
|
457
694
|
|
|
458
695
|
```tsx
|
|
459
696
|
import { useId, useEffect } from 'react';
|
|
460
|
-
import { useEntityForm, useCrud, UploadProvider } from '@donotdev/crud';
|
|
697
|
+
import { useEntityForm, useCrud, UploadProvider, useController } from '@donotdev/crud';
|
|
461
698
|
import { productEntity } from 'entities';
|
|
462
699
|
|
|
463
700
|
export function CustomProductForm() {
|
|
@@ -498,7 +735,7 @@ export function CustomProductForm() {
|
|
|
498
735
|
|
|
499
736
|
---
|
|
500
737
|
|
|
501
|
-
##
|
|
738
|
+
## 10. i18n
|
|
502
739
|
|
|
503
740
|
**Namespace:** Automatically set to `entity-${entity.name.toLowerCase()}` (e.g., `entity-product`)
|
|
504
741
|
|
|
@@ -530,7 +767,42 @@ Field `label` → translation key in `fields.*`
|
|
|
530
767
|
|
|
531
768
|
---
|
|
532
769
|
|
|
533
|
-
##
|
|
770
|
+
## 11. Display Utilities
|
|
771
|
+
|
|
772
|
+
For custom displays, use `formatValue` from `@donotdev/crud`:
|
|
773
|
+
|
|
774
|
+
```tsx
|
|
775
|
+
import { formatValue, translateFieldLabel } from '@donotdev/crud';
|
|
776
|
+
import { useTranslation } from '@donotdev/core';
|
|
777
|
+
import { productEntity } from 'entities';
|
|
778
|
+
|
|
779
|
+
function ProductCard({ item }) {
|
|
780
|
+
const { t } = useTranslation(productEntity.namespace);
|
|
781
|
+
|
|
782
|
+
// Format any field value using entity config
|
|
783
|
+
const priceDisplay = formatValue(
|
|
784
|
+
item.price,
|
|
785
|
+
productEntity.fields.price,
|
|
786
|
+
t,
|
|
787
|
+
{ compact: true } // compact mode for lists/cards
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
// Get translated field label
|
|
791
|
+
const priceLabel = translateFieldLabel('price', productEntity.fields.price, t);
|
|
792
|
+
|
|
793
|
+
return (
|
|
794
|
+
<div>
|
|
795
|
+
<span>{priceLabel}: {priceDisplay}</span>
|
|
796
|
+
</div>
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
**`formatValue` handles all field types:** dates, numbers, selects (translates option labels), images, files, etc.
|
|
802
|
+
|
|
803
|
+
---
|
|
804
|
+
|
|
805
|
+
## 12. Routing Convention
|
|
534
806
|
|
|
535
807
|
| Route | Purpose |
|
|
536
808
|
|-------|---------|
|