@donotdev/cli 0.0.8 → 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/dependencies-matrix.json +177 -76
- package/dist/bin/commands/build.js +2 -2
- package/dist/bin/commands/bump.js +578 -94
- package/dist/bin/commands/cacheout.js +2 -2
- package/dist/bin/commands/create-app.js +46 -9
- package/dist/bin/commands/create-project.js +63 -10
- package/dist/bin/commands/deploy.js +114 -25
- package/dist/bin/commands/dev.js +2 -2
- package/dist/bin/commands/emu.js +2 -2
- package/dist/bin/commands/format.js +2 -2
- package/dist/bin/commands/lint.js +2 -2
- package/dist/bin/commands/preview.js +2 -2
- package/dist/bin/commands/sync-secrets.js +2 -2
- package/dist/bin/dndev.js +7 -4
- package/dist/bin/donotdev.js +7 -4
- package/dist/index.js +177 -33
- package/package.json +5 -4
- 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/.claude/agents/architect.md.example +313 -0
- package/templates/root-consumer/.claude/agents/builder.md.example +329 -0
- package/templates/root-consumer/.claude/agents/coder.md.example +87 -0
- package/templates/root-consumer/.claude/agents/extractor.md.example +235 -0
- package/templates/root-consumer/.claude/agents/polisher.md.example +359 -0
- package/templates/root-consumer/.claude/agents/prompt-engineer.md.example +85 -0
- package/templates/root-consumer/.claude/commands/brainstorm.md.example +133 -0
- package/templates/root-consumer/.claude/commands/build.md.example +109 -0
- package/templates/root-consumer/.claude/commands/design.md.example +136 -0
- package/templates/root-consumer/.claude/commands/polish.md.example +145 -0
- package/templates/root-consumer/.cursor/mcp.json.example +8 -0
- package/templates/root-consumer/.firebaserc.example +5 -0
- package/templates/root-consumer/.mcp.json.example +8 -0
- package/templates/root-consumer/CLAUDE.md.example +146 -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/AGENT_START_HERE.md.example +15 -12
- package/templates/root-consumer/guides/dndev/COMPONENTS_CRUD.md.example +9 -6
- package/templates/root-consumer/guides/dndev/COMPONENT_API.md.example +195 -0
- package/templates/root-consumer/guides/dndev/INDEX.md.example +3 -1
- package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +485 -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`, `InquiryFormTemplate`, `InquiryAdminTemplate`) | `@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 |
|
|
@@ -59,13 +147,54 @@ export const productEntity = defineEntity({
|
|
|
59
147
|
| `'user'` | Authenticated |
|
|
60
148
|
| `'admin'` | Admins |
|
|
61
149
|
| `'super'` | Super admins |
|
|
150
|
+
| `'technical'` | Admins only (read-only in forms; e.g. scope field) |
|
|
151
|
+
| `'owner'` | Only when the current user is a stakeholder (see **Stakeholder Access** below) |
|
|
62
152
|
| `'hidden'` | Never |
|
|
63
153
|
|
|
154
|
+
### Stakeholder Access (ownership)
|
|
155
|
+
|
|
156
|
+
For marketplace-style entities: documents **public when available**, **private to stakeholders** (e.g. partner, customer) when booked.
|
|
157
|
+
|
|
158
|
+
**1. Add `ownership` to the entity:**
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
ownership: {
|
|
162
|
+
ownerFields: ['providerId', 'customerId'], // Document fields whose value is a user id
|
|
163
|
+
publicCondition: [ // When can anyone read? (AND together)
|
|
164
|
+
{ field: 'status', op: '==', value: 'available' },
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**2. Use `visibility: 'owner'`** for fields only stakeholders should see (e.g. internal notes, customer contact after booking):
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
internalNotes: {
|
|
173
|
+
name: 'internalNotes',
|
|
174
|
+
label: 'Internal notes',
|
|
175
|
+
type: 'textarea',
|
|
176
|
+
visibility: 'owner', // Returned only when request.auth.uid matches one of ownerFields
|
|
177
|
+
editable: true,
|
|
178
|
+
validation: { required: false },
|
|
179
|
+
},
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**3. Firestore rules (read and update):** Use the framework helper, then paste the condition into your hand-written `firestore.rules`:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { generateFirestoreRuleCondition } from '@donotdev/core';
|
|
186
|
+
|
|
187
|
+
const condition = generateFirestoreRuleCondition(scheduleEntity.ownership);
|
|
188
|
+
// Paste into firestore.rules: allow read, update: if <condition>;
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Behavior:** `listCard_` returns only documents matching `publicCondition` (e.g. public slots). `list_` returns only documents where the current user is in one of `ownerFields` ("mine"). Same condition is used for **allow read** and **allow update** so owners can update (e.g. status to cancelled, notes). For multiple owner fields, prefer an `ownerIds` array and `array-contains` in rules and queries.
|
|
192
|
+
|
|
64
193
|
### Entity Access (who CAN DO)
|
|
65
194
|
```typescript
|
|
66
195
|
access: {
|
|
67
196
|
create: 'admin', // default
|
|
68
|
-
read: 'guest', // default
|
|
197
|
+
read: 'guest', // default
|
|
69
198
|
update: 'admin', // default
|
|
70
199
|
delete: 'admin', // default
|
|
71
200
|
}
|
|
@@ -97,10 +226,13 @@ export const crud = createCrudFunctions(entities);
|
|
|
97
226
|
|
|
98
227
|
## 4. Frontend Usage
|
|
99
228
|
|
|
100
|
-
|
|
229
|
+
Use components directly from `@donotdev/ui`. They handle routing automatically - no navigate handlers needed.
|
|
230
|
+
|
|
231
|
+
#### Create/Edit Page
|
|
101
232
|
|
|
102
233
|
```tsx
|
|
103
|
-
import {
|
|
234
|
+
import { useCrud } from '@donotdev/crud';
|
|
235
|
+
import { EntityFormRenderer, useParams } from '@donotdev/ui';
|
|
104
236
|
import { productEntity } from 'entities';
|
|
105
237
|
|
|
106
238
|
export default function ProductPage() {
|
|
@@ -115,10 +247,9 @@ export default function ProductPage() {
|
|
|
115
247
|
|
|
116
248
|
const handleSubmit = async (formData: any) => {
|
|
117
249
|
isNew ? await add(formData) : await update(id!, formData);
|
|
118
|
-
|
|
250
|
+
// Navigation happens automatically via cancelPath (defaults to /products)
|
|
119
251
|
};
|
|
120
252
|
|
|
121
|
-
// DON'T mount form until data ready (edit mode)
|
|
122
253
|
if (!isNew && !data) return <Spinner />;
|
|
123
254
|
|
|
124
255
|
return (
|
|
@@ -127,82 +258,270 @@ export default function ProductPage() {
|
|
|
127
258
|
operation={isNew ? 'create' : 'edit'}
|
|
128
259
|
defaultValues={data}
|
|
129
260
|
onSubmit={handleSubmit}
|
|
261
|
+
// Cancel automatically navigates to /products (or cancelPath if provided)
|
|
262
|
+
cancelPath="/products"
|
|
130
263
|
/>
|
|
131
264
|
);
|
|
132
265
|
}
|
|
133
266
|
```
|
|
134
267
|
|
|
135
|
-
|
|
268
|
+
#### List Page
|
|
136
269
|
|
|
137
270
|
```tsx
|
|
138
|
-
import { EntityList } from '@donotdev/
|
|
271
|
+
import { EntityList, PageContainer } from '@donotdev/ui';
|
|
139
272
|
import { useAuth } from '@donotdev/auth';
|
|
140
|
-
import { PageContainer, useNavigate } from '@donotdev/ui';
|
|
141
273
|
import { productEntity } from 'entities';
|
|
142
274
|
|
|
143
275
|
export default function ProductsPage() {
|
|
144
276
|
const user = useAuth('user');
|
|
145
|
-
const navigate = useNavigate();
|
|
146
277
|
|
|
147
278
|
return (
|
|
148
279
|
<PageContainer>
|
|
149
280
|
<EntityList
|
|
150
281
|
entity={productEntity}
|
|
151
282
|
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}`)}
|
|
283
|
+
// Routing: basePath defaults to /products. View/Edit = basePath/:id, Create = basePath/new
|
|
284
|
+
// Optional: basePath="/admin/products" or onClick={(id) => openSheet(id)}
|
|
158
285
|
/>
|
|
159
286
|
</PageContainer>
|
|
160
287
|
);
|
|
161
288
|
}
|
|
162
289
|
```
|
|
163
290
|
|
|
164
|
-
|
|
165
|
-
- `onEdit` **(required)** - Function `(id: string) => void` called when edit button is clicked. Typically navigates to `/${collection}/${id}` (e.g., `/products/${id}`).
|
|
166
|
-
- `onCreate` **(optional)** - Function `() => void` called when "Add New" button is clicked. Typically navigates to `/${collection}/new`.
|
|
167
|
-
- `onView` **(optional)** - Function `(id: string) => void` called when a table row is clicked. Useful for view-only pages or detail navigation.
|
|
168
|
-
- `userRole` **(optional)** - Current user role for field visibility filtering (backend still enforces security).
|
|
291
|
+
#### Card Grid
|
|
169
292
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
293
|
+
```tsx
|
|
294
|
+
import { EntityCardList, PageContainer } from '@donotdev/ui';
|
|
295
|
+
import { productEntity } from 'entities';
|
|
173
296
|
|
|
174
|
-
|
|
297
|
+
export default function ShopPage() {
|
|
298
|
+
return (
|
|
299
|
+
<PageContainer>
|
|
300
|
+
<EntityCardList
|
|
301
|
+
entity={productEntity}
|
|
302
|
+
// Routing: basePath defaults to /products. View = basePath/:id
|
|
303
|
+
// Optional: basePath="/shop/products" or onClick={(id) => openSheet(id)}
|
|
304
|
+
/>
|
|
305
|
+
</PageContainer>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
#### Detail/View Page
|
|
175
311
|
|
|
176
312
|
```tsx
|
|
177
|
-
import {
|
|
178
|
-
import { PageContainer, useNavigate } from '@donotdev/ui';
|
|
313
|
+
import { EntityDisplayRenderer, useParams } from '@donotdev/ui';
|
|
179
314
|
import { productEntity } from 'entities';
|
|
180
315
|
|
|
181
|
-
export default function
|
|
316
|
+
export default function ProductDetailPage() {
|
|
317
|
+
const { id } = useParams<{ id: string }>();
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<EntityDisplayRenderer
|
|
321
|
+
entity={productEntity}
|
|
322
|
+
id={id}
|
|
323
|
+
// Auto-fetches data, shows loading state
|
|
324
|
+
// Read-only display of all visible fields
|
|
325
|
+
/>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### Custom Detail Page with Favorites
|
|
331
|
+
|
|
332
|
+
For custom detail pages, add favorites support using `useEntityFavorites`:
|
|
333
|
+
|
|
334
|
+
```tsx
|
|
335
|
+
import { useState, useEffect } from 'react';
|
|
336
|
+
import { Heart } from 'lucide-react';
|
|
337
|
+
import { PageContainer, Section, Button, Stack } from '@donotdev/components';
|
|
338
|
+
import { useParams, useNavigate } from '@donotdev/ui';
|
|
339
|
+
import { useCrud, useEntityFavorites } from '@donotdev/crud';
|
|
340
|
+
import { useTranslation } from '@donotdev/core';
|
|
341
|
+
import { productEntity } from 'entities';
|
|
342
|
+
|
|
343
|
+
export default function ProductDetailPage() {
|
|
344
|
+
const { id } = useParams<{ id: string }>();
|
|
182
345
|
const navigate = useNavigate();
|
|
346
|
+
const { get } = useCrud(productEntity);
|
|
347
|
+
const { isFavorite, toggleFavorite } = useEntityFavorites({
|
|
348
|
+
collection: productEntity.collection,
|
|
349
|
+
});
|
|
350
|
+
const { t } = useTranslation('crud');
|
|
351
|
+
const [data, setData] = useState<any>(null);
|
|
352
|
+
const [loading, setLoading] = useState(true);
|
|
353
|
+
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
if (id) {
|
|
356
|
+
get(id).then((result) => {
|
|
357
|
+
setData(result);
|
|
358
|
+
setLoading(false);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}, [id, get]);
|
|
362
|
+
|
|
363
|
+
if (loading) return <div>Loading...</div>;
|
|
364
|
+
if (!data) return <div>Not found</div>;
|
|
183
365
|
|
|
184
366
|
return (
|
|
185
367
|
<PageContainer>
|
|
186
|
-
<
|
|
368
|
+
<Section>
|
|
369
|
+
<Stack direction="row" gap="medium" align="center">
|
|
370
|
+
<h1>{data.name}</h1>
|
|
371
|
+
<Button
|
|
372
|
+
variant={isFavorite(id) ? 'primary' : 'outline'}
|
|
373
|
+
icon={<Heart size={18} fill={isFavorite(id) ? 'currentColor' : 'none'} />}
|
|
374
|
+
onClick={() => toggleFavorite(id)}
|
|
375
|
+
>
|
|
376
|
+
{isFavorite(id) ? t('favorites.saved') : t('favorites.save')}
|
|
377
|
+
</Button>
|
|
378
|
+
</Stack>
|
|
379
|
+
{/* Your custom detail page content */}
|
|
380
|
+
</Section>
|
|
381
|
+
</PageContainer>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Note:** `EntityCardList` automatically includes favorites support - heart icons on cards and a favorites filter toggle. No configuration needed.
|
|
387
|
+
|
|
388
|
+
#### Product Shop (Specialized Template)
|
|
389
|
+
|
|
390
|
+
For e-commerce/shop pages with price, image, and category support, use the specialized template:
|
|
391
|
+
|
|
392
|
+
```tsx
|
|
393
|
+
import { ProductCardListTemplate } from '@donotdev/templates';
|
|
394
|
+
import { productEntity } from 'entities';
|
|
395
|
+
|
|
396
|
+
export default function ShopPage() {
|
|
397
|
+
return (
|
|
398
|
+
<PageContainer>
|
|
399
|
+
<ProductCardListTemplate
|
|
187
400
|
entity={productEntity}
|
|
188
|
-
//
|
|
189
|
-
|
|
401
|
+
// Auto-detects price, image, category fields
|
|
402
|
+
// Routing: basePath defaults to /products. Optional: basePath or onClick
|
|
403
|
+
/>
|
|
404
|
+
</PageContainer>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
#### Contact/Inquiry Form (Specialized Template)
|
|
410
|
+
|
|
411
|
+
For contact forms that create both a Customer and an Inquiry record:
|
|
412
|
+
|
|
413
|
+
```tsx
|
|
414
|
+
import { InquiryFormTemplate } from '@donotdev/templates';
|
|
415
|
+
import { customerEntity, inquiryEntity } from 'entities';
|
|
416
|
+
|
|
417
|
+
export default function ContactPage() {
|
|
418
|
+
return (
|
|
419
|
+
<PageContainer>
|
|
420
|
+
<InquiryFormTemplate
|
|
421
|
+
customerEntity={customerEntity}
|
|
422
|
+
inquiryEntity={inquiryEntity}
|
|
423
|
+
// Optional: contextId="car123" - links inquiry to a car/product
|
|
424
|
+
// Optional: contextName="BMW X5 2024" - shown in message placeholder
|
|
425
|
+
// Optional: contextDetails="BMW X5 2024 • 50,000 km • €45,000" - detailed placeholder
|
|
426
|
+
// Optional: messageField="message" - defaults to 'message'
|
|
427
|
+
// Optional: contextField="carId" - defaults to 'carId'
|
|
428
|
+
// Optional: customerFields={['firstName', 'lastName', 'email', 'phone']} - fields to show
|
|
429
|
+
// Optional: onSuccess={() => navigate('/thank-you')}
|
|
430
|
+
/>
|
|
431
|
+
</PageContainer>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**What it does:**
|
|
437
|
+
- Creates a Customer record (with `findOrCreate` logic via `uniqueKeys` - prevents duplicates by email/phone)
|
|
438
|
+
- Creates an Inquiry record linked to the customer
|
|
439
|
+
- Handles GDPR consent tracking
|
|
440
|
+
- Auto-fills message with context details if provided
|
|
441
|
+
- Shows success state after submission
|
|
442
|
+
|
|
443
|
+
**Requirements:**
|
|
444
|
+
- Customer entity must have `uniqueKeys` configured (e.g., `{ fields: ['email'], findOrCreate: true }`)
|
|
445
|
+
- Inquiry entity should have `customerId` field (type: `reference`)
|
|
446
|
+
- Both entities should have `status` field (defaults to `'draft'`)
|
|
447
|
+
|
|
448
|
+
#### Inquiry Admin Dashboard (Specialized Template)
|
|
449
|
+
|
|
450
|
+
For admin pages to review and respond to inquiries with one-click actions:
|
|
451
|
+
|
|
452
|
+
```tsx
|
|
453
|
+
import { InquiryAdminTemplate } from '@donotdev/templates';
|
|
454
|
+
import { customerEntity, inquiryEntity } from 'entities';
|
|
455
|
+
|
|
456
|
+
export default function InquiriesAdminPage() {
|
|
457
|
+
return (
|
|
458
|
+
<PageContainer>
|
|
459
|
+
<InquiryAdminTemplate
|
|
460
|
+
customerEntity={customerEntity}
|
|
461
|
+
inquiryEntity={inquiryEntity}
|
|
462
|
+
// Optional: customerBasePath="/customers" - defaults to '/customers'
|
|
190
463
|
/>
|
|
191
464
|
</PageContainer>
|
|
192
465
|
);
|
|
193
466
|
}
|
|
194
467
|
```
|
|
195
468
|
|
|
196
|
-
**
|
|
197
|
-
-
|
|
198
|
-
-
|
|
199
|
-
-
|
|
200
|
-
-
|
|
201
|
-
-
|
|
469
|
+
**What it does:**
|
|
470
|
+
- Shows all inquiries in a responsive card grid (1 column mobile, 2 columns desktop)
|
|
471
|
+
- Displays inquiry message preview (first 150 chars)
|
|
472
|
+
- Shows customer info inline (name, email, phone)
|
|
473
|
+
- One-click actions:
|
|
474
|
+
- **Email** - Opens `mailto:` link (includes subject if `carId` present)
|
|
475
|
+
- **Call** - Opens `tel:` link
|
|
476
|
+
- **View Customer** - Navigates to customer detail page
|
|
477
|
+
- **Mark Responded** - Updates inquiry status to `'responded'` (only shown if not already responded)
|
|
478
|
+
- Status badges with icons (✓ for responded, ⏰ for pending)
|
|
479
|
+
- Auto-sorted by priority: available → draft → responded → deleted (then by date, newest first)
|
|
480
|
+
|
|
481
|
+
**Features:**
|
|
482
|
+
- Fetches all inquiries and customers in parallel
|
|
483
|
+
- Efficient customer lookup via Map (no N+1 queries)
|
|
484
|
+
- Loading state with spinner
|
|
485
|
+
- Empty state message
|
|
486
|
+
- Responsive grid layout
|
|
202
487
|
|
|
203
488
|
---
|
|
204
489
|
|
|
205
|
-
## 5.
|
|
490
|
+
## 5. Component Props
|
|
491
|
+
|
|
492
|
+
### EntityList
|
|
493
|
+
- `basePath` **(optional)** - Base path for view/edit/create. Defaults to `/${collection}`. View/Edit = basePath/:id, Create = basePath/new
|
|
494
|
+
- `onClick` **(optional)** - Called when user clicks a row. If provided, overrides navigation (e.g. open sheet)
|
|
495
|
+
- `userRole` **(optional)** - Current user role for field visibility
|
|
496
|
+
|
|
497
|
+
### EntityCardList
|
|
498
|
+
- `basePath` **(optional)** - Base path for view. Defaults to `/${collection}`. View = basePath/:id
|
|
499
|
+
- `onClick` **(optional)** - Called when user clicks a card. If provided, overrides navigation (e.g. open sheet)
|
|
500
|
+
- `cols` **(optional)** - `[mobile, tablet, desktop, wide]` (default: `[1, 2, 3, 4]`)
|
|
501
|
+
- `staleTime` **(optional)** - Cache stale time in ms (default: 30 min)
|
|
502
|
+
- `filter` **(optional)** - `(item: any) => boolean` - Client-side filter
|
|
503
|
+
- `hideFilters` **(optional)** - Hide filters section
|
|
504
|
+
|
|
505
|
+
**Favorites:** Automatically enabled - heart icons on cards, favorites filter toggle in filters section. Uses `useEntityFavorites` hook internally with `entity.collection` as storage key.
|
|
506
|
+
|
|
507
|
+
### EntityFormRenderer
|
|
508
|
+
- `entity` **(required)** - Entity definition
|
|
509
|
+
- `onSubmit` **(required)** - `(data: any) => Promise<void>` - Submit handler
|
|
510
|
+
- `defaultValues` **(optional)** - Initial form values (for edit mode)
|
|
511
|
+
- `operation` **(optional)** - `'create' | 'edit'` (auto-detected from defaultValues)
|
|
512
|
+
- `viewerRole` **(optional)** - Role for field visibility/editability
|
|
513
|
+
- `cancelPath` **(optional)** - Path to navigate on cancel (defaults to `/${collection}`)
|
|
514
|
+
- `onCancel` **(optional)** - Cancel button handler (takes precedence over cancelPath)
|
|
515
|
+
- `cancelText` **(optional)** - Cancel button text (null to hide)
|
|
516
|
+
|
|
517
|
+
### EntityDisplayRenderer
|
|
518
|
+
- `entity` **(required)** - Entity definition
|
|
519
|
+
- `id` **(required)** - Entity ID to fetch and display
|
|
520
|
+
- `viewerRole` **(optional)** - Role for field visibility
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## 6. Field Types
|
|
206
525
|
|
|
207
526
|
### Text Inputs
|
|
208
527
|
- `text` - Single-line text input
|
|
@@ -216,6 +535,8 @@ export default function ShopPage() {
|
|
|
216
535
|
|
|
217
536
|
### Numbers
|
|
218
537
|
- `number` - Numeric input
|
|
538
|
+
- `currency` - Single amount with currency symbol (value: number; options: `fieldSpecific.currency`)
|
|
539
|
+
- `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
540
|
- `range` - Slider input
|
|
220
541
|
- `year` - Year input (combobox: type or select from dropdown)
|
|
221
542
|
|
|
@@ -261,6 +582,42 @@ export default function ShopPage() {
|
|
|
261
582
|
- `submit` - Submit button (uncontrolled)
|
|
262
583
|
- `reset` - Reset button (uncontrolled)
|
|
263
584
|
|
|
585
|
+
### Price field type (structured)
|
|
586
|
+
|
|
587
|
+
Use `type: 'price'` when you need amount, currency, VAT label, and optional discount in one field. Replaces separate "price" + "specialPrice" number fields.
|
|
588
|
+
|
|
589
|
+
**Definition:**
|
|
590
|
+
```typescript
|
|
591
|
+
price: {
|
|
592
|
+
name: 'price',
|
|
593
|
+
label: 'price',
|
|
594
|
+
type: 'price',
|
|
595
|
+
visibility: 'guest',
|
|
596
|
+
validation: { required: true },
|
|
597
|
+
options: {
|
|
598
|
+
fieldSpecific: {
|
|
599
|
+
defaultCurrency: 'EUR', // Omit → 'EUR'. Used when value.currency is unset
|
|
600
|
+
currencies: ['EUR', 'USD'], // Omit → list = [EUR, USD, GBP, CHF, JPY, KRW]. One item e.g. ['EUR'] → no dropdown, single currency
|
|
601
|
+
optionsTitle: 'Price options', // Omit → 'Price options'. Aria-label for the expand button
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
**Value shape (stored in DB):**
|
|
608
|
+
```ts
|
|
609
|
+
{ amount: number; currency?: string; vatIncluded?: boolean; discountPercent?: number }
|
|
610
|
+
```
|
|
611
|
+
- Only `discountPercent` is stored for discount; effective price is computed as `amount * (1 - discountPercent/100)` for display.
|
|
612
|
+
|
|
613
|
+
**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.
|
|
614
|
+
|
|
615
|
+
**Display:** Formatted amount + "VAT Incl." when `vatIncluded`. When `discountPercent > 0`: original amount crossed out + effective price.
|
|
616
|
+
|
|
617
|
+
**Templates:** `CarCardListTemplate`, `CarDetailTemplate`, `ProductCardListTemplate` use the price field when present and show a "Sale" (or `price.badge.sale` i18n) badge when discounted.
|
|
618
|
+
|
|
619
|
+
**Filtering:** Range filter on `amount`; optional "deals" filter (items with `discountPercent > 0`).
|
|
620
|
+
|
|
264
621
|
### Select Options
|
|
265
622
|
|
|
266
623
|
```typescript
|
|
@@ -282,7 +639,7 @@ Use `type: 'year'` for year inputs (combobox with type-or-select UX):
|
|
|
282
639
|
```typescript
|
|
283
640
|
year: {
|
|
284
641
|
type: 'year',
|
|
285
|
-
validation: {
|
|
642
|
+
validation: {
|
|
286
643
|
required: true,
|
|
287
644
|
min: 1900, // Optional: defaults to 1900
|
|
288
645
|
max: 2100 // Optional: defaults to current year + 10
|
|
@@ -306,20 +663,17 @@ transmission: {
|
|
|
306
663
|
|
|
307
664
|
---
|
|
308
665
|
|
|
309
|
-
##
|
|
666
|
+
## 7. Custom Fields & Schemas
|
|
310
667
|
|
|
311
668
|
### Custom Field Types with UI Components
|
|
312
669
|
|
|
313
670
|
For custom field types that need custom UI components:
|
|
314
671
|
|
|
315
672
|
```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';
|
|
673
|
+
import { registerFieldType, useController } from '@donotdev/crud';
|
|
322
674
|
import type { ControlledFieldProps } from '@donotdev/crud';
|
|
675
|
+
import { defineEntity } from '@donotdev/core';
|
|
676
|
+
import * as v from 'valibot';
|
|
323
677
|
|
|
324
678
|
function RepairOperationsField({
|
|
325
679
|
fieldConfig,
|
|
@@ -335,7 +689,7 @@ function RepairOperationsField({
|
|
|
335
689
|
|
|
336
690
|
// Use field.value and field.onChange for form state
|
|
337
691
|
const value = (field.value as any) || [];
|
|
338
|
-
|
|
692
|
+
|
|
339
693
|
return (
|
|
340
694
|
<div>
|
|
341
695
|
<label>{t(fieldConfig.label)}</label>
|
|
@@ -376,7 +730,7 @@ export const carEntity = defineEntity({
|
|
|
376
730
|
});
|
|
377
731
|
```
|
|
378
732
|
|
|
379
|
-
**Important:**
|
|
733
|
+
**Important:**
|
|
380
734
|
- Custom controlled components receive `control` prop, NOT `field` prop
|
|
381
735
|
- You MUST use `useController` hook to get `field` and `fieldState`
|
|
382
736
|
- Define schema in `validation.schema` - it's the single source of truth
|
|
@@ -386,6 +740,7 @@ export const carEntity = defineEntity({
|
|
|
386
740
|
For custom validation without custom UI, just define the schema:
|
|
387
741
|
|
|
388
742
|
```typescript
|
|
743
|
+
import { defineEntity } from '@donotdev/core';
|
|
389
744
|
import * as v from 'valibot';
|
|
390
745
|
|
|
391
746
|
export const carEntity = defineEntity({
|
|
@@ -436,28 +791,30 @@ export const carEntity = defineEntity({
|
|
|
436
791
|
|
|
437
792
|
---
|
|
438
793
|
|
|
439
|
-
##
|
|
794
|
+
## 8. Hooks API
|
|
440
795
|
|
|
441
796
|
```typescript
|
|
797
|
+
import { useCrud, useCrudList, useCrudCardList } from '@donotdev/crud';
|
|
798
|
+
|
|
442
799
|
// Actions
|
|
443
800
|
const { add, update, delete: remove, get } = useCrud(productEntity);
|
|
444
801
|
|
|
445
802
|
// List (auto-fetch for tables)
|
|
446
|
-
const { items, loading } = useCrudList(productEntity);
|
|
803
|
+
const { items, loading, refresh } = useCrudList(productEntity);
|
|
447
804
|
|
|
448
805
|
// Cards (optimized for public)
|
|
449
|
-
const { items, loading,
|
|
806
|
+
const { items, loading, refresh } = useCrudCardList(productEntity);
|
|
450
807
|
```
|
|
451
808
|
|
|
452
809
|
---
|
|
453
810
|
|
|
454
|
-
##
|
|
811
|
+
## 9. Custom Form Layouts
|
|
455
812
|
|
|
456
813
|
Need a custom layout instead of `EntityFormRenderer`? Use `useEntityForm` directly.
|
|
457
814
|
|
|
458
815
|
```tsx
|
|
459
816
|
import { useId, useEffect } from 'react';
|
|
460
|
-
import { useEntityForm, useCrud, UploadProvider } from '@donotdev/crud';
|
|
817
|
+
import { useEntityForm, useCrud, UploadProvider, useController } from '@donotdev/crud';
|
|
461
818
|
import { productEntity } from 'entities';
|
|
462
819
|
|
|
463
820
|
export function CustomProductForm() {
|
|
@@ -498,7 +855,7 @@ export function CustomProductForm() {
|
|
|
498
855
|
|
|
499
856
|
---
|
|
500
857
|
|
|
501
|
-
##
|
|
858
|
+
## 10. i18n
|
|
502
859
|
|
|
503
860
|
**Namespace:** Automatically set to `entity-${entity.name.toLowerCase()}` (e.g., `entity-product`)
|
|
504
861
|
|
|
@@ -528,9 +885,80 @@ export const productEntity = defineEntity({
|
|
|
528
885
|
|
|
529
886
|
Field `label` → translation key in `fields.*`
|
|
530
887
|
|
|
888
|
+
### Status Field Translations
|
|
889
|
+
|
|
890
|
+
The `status` field uses CRUD namespace translations with entity-specific overrides:
|
|
891
|
+
|
|
892
|
+
**Translation Fallback Order:**
|
|
893
|
+
1. **Entity namespace** (`entity-{name}`) - User overrides take priority
|
|
894
|
+
2. **CRUD namespace** (`crud`) - Framework defaults
|
|
895
|
+
|
|
896
|
+
**Framework Defaults** (in `crud` namespace):
|
|
897
|
+
- `crud:status.draft` → "Draft"
|
|
898
|
+
- `crud:status.available` → "Available"
|
|
899
|
+
- `crud:status.deleted` → "Deleted"
|
|
900
|
+
|
|
901
|
+
**Override in Entity Translation File:**
|
|
902
|
+
|
|
903
|
+
```json
|
|
904
|
+
// locales/entity-inquiry_en.json
|
|
905
|
+
{
|
|
906
|
+
"status": {
|
|
907
|
+
"draft": "New",
|
|
908
|
+
"available": "Read",
|
|
909
|
+
"deleted": "Closed"
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
**How it works:**
|
|
915
|
+
- Framework components (`EntityList`, `EntityFormRenderer`, etc.) automatically use `[entity.namespace, 'crud']` translation namespaces
|
|
916
|
+
- Status labels are translated via `translateLabel()` which tries entity namespace first, then falls back to `crud`
|
|
917
|
+
- No `dndev` namespace fallback - CRUD is optional and doesn't pollute core framework translations
|
|
918
|
+
|
|
919
|
+
**Example:** For an `inquiry` entity:
|
|
920
|
+
- If `entity-inquiry_en.json` has `"status.draft": "New"` → displays "New"
|
|
921
|
+
- If not found → falls back to `crud:status.draft` → displays "Draft"
|
|
922
|
+
- Never falls back to `dndev` namespace
|
|
923
|
+
|
|
924
|
+
---
|
|
925
|
+
|
|
926
|
+
## 11. Display Utilities
|
|
927
|
+
|
|
928
|
+
For custom displays, use `formatValue` from `@donotdev/crud`:
|
|
929
|
+
|
|
930
|
+
```tsx
|
|
931
|
+
import { formatValue, translateFieldLabel } from '@donotdev/crud';
|
|
932
|
+
import { useTranslation } from '@donotdev/core';
|
|
933
|
+
import { productEntity } from 'entities';
|
|
934
|
+
|
|
935
|
+
function ProductCard({ item }) {
|
|
936
|
+
const { t } = useTranslation(productEntity.namespace);
|
|
937
|
+
|
|
938
|
+
// Format any field value using entity config
|
|
939
|
+
const priceDisplay = formatValue(
|
|
940
|
+
item.price,
|
|
941
|
+
productEntity.fields.price,
|
|
942
|
+
t,
|
|
943
|
+
{ compact: true } // compact mode for lists/cards
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
// Get translated field label
|
|
947
|
+
const priceLabel = translateFieldLabel('price', productEntity.fields.price, t);
|
|
948
|
+
|
|
949
|
+
return (
|
|
950
|
+
<div>
|
|
951
|
+
<span>{priceLabel}: {priceDisplay}</span>
|
|
952
|
+
</div>
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
**`formatValue` handles all field types:** dates, numbers, selects (translates option labels), images, files, etc.
|
|
958
|
+
|
|
531
959
|
---
|
|
532
960
|
|
|
533
|
-
##
|
|
961
|
+
## 12. Routing Convention
|
|
534
962
|
|
|
535
963
|
| Route | Purpose |
|
|
536
964
|
|-------|---------|
|