@donotdev/cli 0.0.7 → 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/README.md +3 -18
- package/dependencies-matrix.json +54 -45
- package/dist/bin/commands/build.js +19 -5
- package/dist/bin/commands/bump.js +30 -5
- package/dist/bin/commands/cacheout.js +36 -19
- package/dist/bin/commands/create-app.js +73 -7
- package/dist/bin/commands/create-project.js +90 -8
- package/dist/bin/commands/deploy.js +130 -27
- package/dist/bin/commands/dev.js +23 -7
- package/dist/bin/commands/emu.js +28 -12
- package/dist/bin/commands/format.js +39 -22
- package/dist/bin/commands/lint.js +36 -19
- package/dist/bin/commands/preview.js +24 -8
- package/dist/bin/commands/sync-secrets.js +19 -5
- package/dist/bin/dndev.js +7 -20
- package/dist/bin/donotdev.js +7 -20
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +209 -100
- package/dist/index.js.map +1 -1
- 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 +15 -1
- package/templates/root-consumer/eslint.config.js.example +2 -80
- 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 +376 -38
- package/templates/root-consumer/guides/dndev/SETUP_I18N.md.example +46 -0
- 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
|
|
@@ -13,6 +29,7 @@ import { defineEntity } from '@donotdev/core';
|
|
|
13
29
|
export const productEntity = defineEntity({
|
|
14
30
|
name: 'Product',
|
|
15
31
|
collection: 'products',
|
|
32
|
+
// namespace: 'entity-product', // Optional: defaults to entity-${name.toLowerCase()}
|
|
16
33
|
fields: {
|
|
17
34
|
name: {
|
|
18
35
|
name: 'name',
|
|
@@ -24,9 +41,16 @@ export const productEntity = defineEntity({
|
|
|
24
41
|
price: {
|
|
25
42
|
name: 'price',
|
|
26
43
|
label: 'price',
|
|
27
|
-
type: '
|
|
44
|
+
type: 'price', // Structured: { amount, currency?, vatIncluded?, discountPercent? }
|
|
28
45
|
visibility: 'guest',
|
|
29
|
-
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
|
+
},
|
|
30
54
|
},
|
|
31
55
|
image: {
|
|
32
56
|
name: 'image',
|
|
@@ -49,7 +73,72 @@ export const productEntity = defineEntity({
|
|
|
49
73
|
|
|
50
74
|
---
|
|
51
75
|
|
|
52
|
-
## 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
|
|
53
142
|
|
|
54
143
|
### Field Visibility (who SEES)
|
|
55
144
|
| Level | Who |
|
|
@@ -64,7 +153,7 @@ export const productEntity = defineEntity({
|
|
|
64
153
|
```typescript
|
|
65
154
|
access: {
|
|
66
155
|
create: 'admin', // default
|
|
67
|
-
read: 'guest', // default
|
|
156
|
+
read: 'guest', // default
|
|
68
157
|
update: 'admin', // default
|
|
69
158
|
delete: 'admin', // default
|
|
70
159
|
}
|
|
@@ -96,10 +185,13 @@ export const crud = createCrudFunctions(entities);
|
|
|
96
185
|
|
|
97
186
|
## 4. Frontend Usage
|
|
98
187
|
|
|
99
|
-
|
|
188
|
+
Use components directly from `@donotdev/ui`. They handle routing automatically - no navigate handlers needed.
|
|
189
|
+
|
|
190
|
+
#### Create/Edit Page
|
|
100
191
|
|
|
101
192
|
```tsx
|
|
102
|
-
import {
|
|
193
|
+
import { useCrud } from '@donotdev/crud';
|
|
194
|
+
import { EntityFormRenderer, useParams } from '@donotdev/ui';
|
|
103
195
|
import { productEntity } from 'entities';
|
|
104
196
|
|
|
105
197
|
export default function ProductPage() {
|
|
@@ -114,10 +206,9 @@ export default function ProductPage() {
|
|
|
114
206
|
|
|
115
207
|
const handleSubmit = async (formData: any) => {
|
|
116
208
|
isNew ? await add(formData) : await update(id!, formData);
|
|
117
|
-
|
|
209
|
+
// Navigation happens automatically via cancelPath (defaults to /products)
|
|
118
210
|
};
|
|
119
211
|
|
|
120
|
-
// DON'T mount form until data ready (edit mode)
|
|
121
212
|
if (!isNew && !data) return <Spinner />;
|
|
122
213
|
|
|
123
214
|
return (
|
|
@@ -126,35 +217,191 @@ export default function ProductPage() {
|
|
|
126
217
|
operation={isNew ? 'create' : 'edit'}
|
|
127
218
|
defaultValues={data}
|
|
128
219
|
onSubmit={handleSubmit}
|
|
220
|
+
// Cancel automatically navigates to /products (or cancelPath if provided)
|
|
221
|
+
cancelPath="/products"
|
|
129
222
|
/>
|
|
130
223
|
);
|
|
131
224
|
}
|
|
132
225
|
```
|
|
133
226
|
|
|
134
|
-
|
|
227
|
+
#### List Page
|
|
135
228
|
|
|
136
229
|
```tsx
|
|
137
|
-
import { EntityList } from '@donotdev/
|
|
230
|
+
import { EntityList, PageContainer } from '@donotdev/ui';
|
|
231
|
+
import { useAuth } from '@donotdev/auth';
|
|
138
232
|
import { productEntity } from 'entities';
|
|
139
233
|
|
|
140
234
|
export default function ProductsPage() {
|
|
141
|
-
|
|
235
|
+
const user = useAuth('user');
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<PageContainer>
|
|
239
|
+
<EntityList
|
|
240
|
+
entity={productEntity}
|
|
241
|
+
userRole={user?.role}
|
|
242
|
+
// Routing: basePath defaults to /products. View/Edit = basePath/:id, Create = basePath/new
|
|
243
|
+
// Optional: basePath="/admin/products" or onClick={(id) => openSheet(id)}
|
|
244
|
+
/>
|
|
245
|
+
</PageContainer>
|
|
246
|
+
);
|
|
142
247
|
}
|
|
143
248
|
```
|
|
144
249
|
|
|
145
|
-
|
|
250
|
+
#### Card Grid
|
|
146
251
|
|
|
147
252
|
```tsx
|
|
148
|
-
import { EntityCardList } from '@donotdev/
|
|
253
|
+
import { EntityCardList, PageContainer } from '@donotdev/ui';
|
|
254
|
+
import { productEntity } from 'entities';
|
|
149
255
|
|
|
150
256
|
export default function ShopPage() {
|
|
151
|
-
return
|
|
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
|
+
```
|
|
268
|
+
|
|
269
|
+
#### Detail/View Page
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
import { EntityDisplayRenderer, useParams } from '@donotdev/ui';
|
|
273
|
+
import { productEntity } from 'entities';
|
|
274
|
+
|
|
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 }>();
|
|
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>;
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<PageContainer>
|
|
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
|
|
359
|
+
entity={productEntity}
|
|
360
|
+
// Auto-detects price, image, category fields
|
|
361
|
+
// Routing: basePath defaults to /products. Optional: basePath or onClick
|
|
362
|
+
/>
|
|
363
|
+
</PageContainer>
|
|
364
|
+
);
|
|
152
365
|
}
|
|
153
366
|
```
|
|
154
367
|
|
|
155
368
|
---
|
|
156
369
|
|
|
157
|
-
## 5.
|
|
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
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## 6. Field Types
|
|
158
405
|
|
|
159
406
|
### Text Inputs
|
|
160
407
|
- `text` - Single-line text input
|
|
@@ -168,7 +415,10 @@ export default function ShopPage() {
|
|
|
168
415
|
|
|
169
416
|
### Numbers
|
|
170
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.
|
|
171
420
|
- `range` - Slider input
|
|
421
|
+
- `year` - Year input (combobox: type or select from dropdown)
|
|
172
422
|
|
|
173
423
|
### Boolean
|
|
174
424
|
- `checkbox` - Checkbox input
|
|
@@ -212,6 +462,42 @@ export default function ShopPage() {
|
|
|
212
462
|
- `submit` - Submit button (uncontrolled)
|
|
213
463
|
- `reset` - Reset button (uncontrolled)
|
|
214
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
|
+
|
|
215
501
|
### Select Options
|
|
216
502
|
|
|
217
503
|
```typescript
|
|
@@ -226,14 +512,18 @@ category: {
|
|
|
226
512
|
}
|
|
227
513
|
```
|
|
228
514
|
|
|
229
|
-
### Year
|
|
515
|
+
### Year Field
|
|
230
516
|
|
|
231
|
-
|
|
232
|
-
import { yearOptions } from '@donotdev/crud';
|
|
517
|
+
Use `type: 'year'` for year inputs (combobox with type-or-select UX):
|
|
233
518
|
|
|
519
|
+
```typescript
|
|
234
520
|
year: {
|
|
235
|
-
type: '
|
|
236
|
-
validation: {
|
|
521
|
+
type: 'year',
|
|
522
|
+
validation: {
|
|
523
|
+
required: true,
|
|
524
|
+
min: 1900, // Optional: defaults to 1900
|
|
525
|
+
max: 2100 // Optional: defaults to current year + 10
|
|
526
|
+
}
|
|
237
527
|
}
|
|
238
528
|
```
|
|
239
529
|
|
|
@@ -253,20 +543,17 @@ transmission: {
|
|
|
253
543
|
|
|
254
544
|
---
|
|
255
545
|
|
|
256
|
-
##
|
|
546
|
+
## 7. Custom Fields & Schemas
|
|
257
547
|
|
|
258
548
|
### Custom Field Types with UI Components
|
|
259
549
|
|
|
260
550
|
For custom field types that need custom UI components:
|
|
261
551
|
|
|
262
552
|
```typescript
|
|
263
|
-
import { registerFieldType,
|
|
264
|
-
import { useController } from 'react-hook-form';
|
|
265
|
-
import * as v from 'valibot';
|
|
266
|
-
|
|
267
|
-
// Custom controlled component MUST use framework's useController
|
|
268
|
-
import { useController, registerFieldType } from '@donotdev/crud';
|
|
553
|
+
import { registerFieldType, useController } from '@donotdev/crud';
|
|
269
554
|
import type { ControlledFieldProps } from '@donotdev/crud';
|
|
555
|
+
import { defineEntity } from '@donotdev/core';
|
|
556
|
+
import * as v from 'valibot';
|
|
270
557
|
|
|
271
558
|
function RepairOperationsField({
|
|
272
559
|
fieldConfig,
|
|
@@ -282,7 +569,7 @@ function RepairOperationsField({
|
|
|
282
569
|
|
|
283
570
|
// Use field.value and field.onChange for form state
|
|
284
571
|
const value = (field.value as any) || [];
|
|
285
|
-
|
|
572
|
+
|
|
286
573
|
return (
|
|
287
574
|
<div>
|
|
288
575
|
<label>{t(fieldConfig.label)}</label>
|
|
@@ -323,7 +610,7 @@ export const carEntity = defineEntity({
|
|
|
323
610
|
});
|
|
324
611
|
```
|
|
325
612
|
|
|
326
|
-
**Important:**
|
|
613
|
+
**Important:**
|
|
327
614
|
- Custom controlled components receive `control` prop, NOT `field` prop
|
|
328
615
|
- You MUST use `useController` hook to get `field` and `fieldState`
|
|
329
616
|
- Define schema in `validation.schema` - it's the single source of truth
|
|
@@ -333,6 +620,7 @@ export const carEntity = defineEntity({
|
|
|
333
620
|
For custom validation without custom UI, just define the schema:
|
|
334
621
|
|
|
335
622
|
```typescript
|
|
623
|
+
import { defineEntity } from '@donotdev/core';
|
|
336
624
|
import * as v from 'valibot';
|
|
337
625
|
|
|
338
626
|
export const carEntity = defineEntity({
|
|
@@ -383,28 +671,30 @@ export const carEntity = defineEntity({
|
|
|
383
671
|
|
|
384
672
|
---
|
|
385
673
|
|
|
386
|
-
##
|
|
674
|
+
## 8. Hooks API
|
|
387
675
|
|
|
388
676
|
```typescript
|
|
677
|
+
import { useCrud, useCrudList, useCrudCardList } from '@donotdev/crud';
|
|
678
|
+
|
|
389
679
|
// Actions
|
|
390
680
|
const { add, update, delete: remove, get } = useCrud(productEntity);
|
|
391
681
|
|
|
392
682
|
// List (auto-fetch for tables)
|
|
393
|
-
const { items, loading } = useCrudList(productEntity);
|
|
683
|
+
const { items, loading, refresh } = useCrudList(productEntity);
|
|
394
684
|
|
|
395
685
|
// Cards (optimized for public)
|
|
396
|
-
const { items, loading,
|
|
686
|
+
const { items, loading, refresh } = useCrudCardList(productEntity);
|
|
397
687
|
```
|
|
398
688
|
|
|
399
689
|
---
|
|
400
690
|
|
|
401
|
-
##
|
|
691
|
+
## 9. Custom Form Layouts
|
|
402
692
|
|
|
403
693
|
Need a custom layout instead of `EntityFormRenderer`? Use `useEntityForm` directly.
|
|
404
694
|
|
|
405
695
|
```tsx
|
|
406
696
|
import { useId, useEffect } from 'react';
|
|
407
|
-
import { useEntityForm, useCrud, UploadProvider } from '@donotdev/crud';
|
|
697
|
+
import { useEntityForm, useCrud, UploadProvider, useController } from '@donotdev/crud';
|
|
408
698
|
import { productEntity } from 'entities';
|
|
409
699
|
|
|
410
700
|
export function CustomProductForm() {
|
|
@@ -445,11 +735,24 @@ export function CustomProductForm() {
|
|
|
445
735
|
|
|
446
736
|
---
|
|
447
737
|
|
|
448
|
-
##
|
|
738
|
+
## 10. i18n
|
|
739
|
+
|
|
740
|
+
**Namespace:** Automatically set to `entity-${entity.name.toLowerCase()}` (e.g., `entity-product`)
|
|
741
|
+
|
|
742
|
+
You can customize it by setting `namespace` in your entity definition:
|
|
449
743
|
|
|
450
|
-
|
|
744
|
+
```typescript
|
|
745
|
+
export const productEntity = defineEntity({
|
|
746
|
+
name: 'Product',
|
|
747
|
+
collection: 'products',
|
|
748
|
+
namespace: 'custom-namespace', // Optional: override default
|
|
749
|
+
fields: { ... }
|
|
750
|
+
});
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
**Default behavior:** If not specified, the namespace is automatically set to `entity-${entity.name.toLowerCase()}`.
|
|
451
754
|
|
|
452
|
-
**
|
|
755
|
+
**Translation file:** `locales/entity-product_en.json` (or your custom namespace)
|
|
453
756
|
```json
|
|
454
757
|
{
|
|
455
758
|
"fields": {
|
|
@@ -464,7 +767,42 @@ Field `label` → translation key in `fields.*`
|
|
|
464
767
|
|
|
465
768
|
---
|
|
466
769
|
|
|
467
|
-
##
|
|
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
|
|
468
806
|
|
|
469
807
|
| Route | Purpose |
|
|
470
808
|
|-------|---------|
|
|
@@ -184,4 +184,50 @@ const { languages, currentLanguage } = useLanguageSelector();
|
|
|
184
184
|
|
|
185
185
|
---
|
|
186
186
|
|
|
187
|
+
## Advanced: Shared Entity Translations (Monorepos)
|
|
188
|
+
|
|
189
|
+
**Auto-detected:** The framework automatically scans `../../entities/locales/*_*.json` for shared entity translations. If the path exists, translations are loaded; if not, nothing happens.
|
|
190
|
+
|
|
191
|
+
**File naming:** `entity-car_en.json`, `entity-car_fr.json`
|
|
192
|
+
|
|
193
|
+
**Merge behavior:** Base translations from shared package, app can override any key. Deep merge, app wins.
|
|
194
|
+
|
|
195
|
+
**Example structure:**
|
|
196
|
+
```
|
|
197
|
+
monorepo/
|
|
198
|
+
entities/
|
|
199
|
+
locales/
|
|
200
|
+
entity-car_en.json # ✅ Auto-detected - no config needed
|
|
201
|
+
entity-car_fr.json
|
|
202
|
+
apps/
|
|
203
|
+
admin/ # ✅ Just works - framework auto-detects
|
|
204
|
+
public/ # ✅ Just works - framework auto-detects
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Custom paths:** If your shared translations are in a different location, use `additionalPaths`:
|
|
208
|
+
|
|
209
|
+
**Vite:**
|
|
210
|
+
```ts
|
|
211
|
+
// vite.config.ts
|
|
212
|
+
export default defineViteConfig({
|
|
213
|
+
appConfig,
|
|
214
|
+
i18n: {
|
|
215
|
+
additionalPaths: ['../../packages/shared/locales'] // Custom path
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Next.js:**
|
|
221
|
+
```ts
|
|
222
|
+
// next.config.ts
|
|
223
|
+
export default defineNextConfig({
|
|
224
|
+
appConfig,
|
|
225
|
+
i18n: {
|
|
226
|
+
additionalPaths: ['../../packages/shared/locales'] // Custom path
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
187
233
|
**Drop files, get languages. Framework handles the rest.**
|