@donotdev/cli 0.0.15 → 0.0.17
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 +67 -147
- package/dist/bin/commands/build.js +69 -52
- package/dist/bin/commands/bump.js +15 -14
- package/dist/bin/commands/create-app.js +258 -55
- package/dist/bin/commands/create-project.js +290 -161
- package/dist/bin/commands/deploy.js +146 -63
- package/dist/bin/commands/dev.js +43 -24
- package/dist/bin/commands/doctor.d.ts +6 -0
- package/dist/bin/commands/doctor.d.ts.map +1 -0
- package/dist/bin/commands/{lint.js → doctor.js} +1370 -146
- package/dist/bin/commands/doctor.js.map +1 -0
- package/dist/bin/commands/emu.js +295 -107
- package/dist/bin/commands/make-admin.js +77519 -11
- package/dist/bin/commands/preview.js +44 -25
- package/dist/bin/commands/setup.d.ts +6 -0
- package/dist/bin/commands/setup.d.ts.map +1 -0
- package/dist/bin/commands/setup.js +12123 -0
- package/dist/bin/commands/setup.js.map +1 -0
- package/dist/bin/commands/type-check.d.ts.map +1 -1
- package/dist/bin/commands/type-check.js +2022 -283
- package/dist/bin/commands/type-check.js.map +1 -1
- package/dist/bin/dndev.js +54 -58
- package/dist/bin/donotdev.js +54 -58
- package/dist/index.js +860 -459
- package/package.json +2 -2
- package/templates/app-expo/.env.example +2 -22
- package/templates/app-expo/README.md.example +1 -1
- package/templates/app-expo/assets/adaptive-icon.png +0 -0
- package/templates/app-expo/assets/favicon.png +0 -0
- package/templates/app-expo/assets/icon.png +0 -0
- package/templates/app-expo/assets/splash.png +0 -0
- package/templates/app-expo/src/config/app.ts.example +46 -0
- package/templates/app-expo/src/config/providers.ts.example +7 -0
- package/templates/app-next/src/config/providers.ts.example +7 -0
- package/templates/app-vite/src/config/providers.ts.example +7 -0
- package/templates/app-vite/src/pages/HomePage.tsx.example +1 -1
- package/templates/functions-firebase/README.md.example +1 -1
- package/templates/functions-firebase/functions-firebase/.env.example.example +1 -1
- package/templates/functions-firebase/functions-firebase/README.md.example +1 -1
- package/templates/functions-firebase/functions-firebase/tsconfig.json.example +1 -1
- package/templates/functions-firebase/functions.config.js.example +1 -1
- package/templates/functions-supabase/supabase/config.toml.example +59 -0
- package/templates/functions-supabase/supabase/functions/.env.example +13 -0
- package/templates/functions-supabase/supabase/functions/deno.json.example +8 -0
- package/templates/overlay-firebase/env.fragment.example +1 -1
- package/templates/overlay-firebase/env.fragment.expo.example +1 -1
- package/templates/overlay-firebase/env.fragment.nextjs.example +1 -1
- package/templates/overlay-supabase/env.fragment.example +8 -3
- package/templates/overlay-supabase/env.fragment.expo.example +8 -3
- package/templates/overlay-supabase/env.fragment.nextjs.example +8 -3
- package/templates/overlay-vercel/env.fragment.example +1 -1
- package/templates/overlay-vercel/env.fragment.nextjs.example +1 -1
- package/templates/root-consumer/AI.md.example +15 -0
- package/templates/root-consumer/guides/dndev/AGENT_START_HERE.md.example +2 -2
- package/templates/root-consumer/guides/dndev/ENV_SETUP.md.example +12 -12
- package/templates/root-consumer/guides/dndev/INDEX.md.example +3 -3
- package/templates/root-consumer/guides/dndev/SETUP_APP_CONFIG.md.example +3 -3
- package/templates/root-consumer/guides/dndev/SETUP_AUTH.md.example +13 -6
- package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +149 -988
- package/templates/root-consumer/guides/dndev/SETUP_FIREBASE.md.example +72 -20
- package/templates/root-consumer/guides/dndev/SETUP_FUNCTIONS.md.example +6 -111
- package/templates/root-consumer/guides/dndev/SETUP_OAUTH_PROVIDERS.md.example +60 -0
- package/templates/root-consumer/guides/dndev/SETUP_STRIPE.md.example +62 -0
- package/templates/root-consumer/guides/dndev/SETUP_SUPABASE.md.example +124 -33
- package/templates/root-consumer/guides/dndev/SETUP_VERCEL.md.example +108 -91
- package/templates/root-consumer/guides/dndev/advanced/EMULATORS.md.example +2 -2
- package/templates/root-consumer/guides/wai-way/WAI_WAY_CLI.md.example +7 -8
- package/templates/root-consumer/guides/wai-way/blueprints/1_scaffold.md.example +9 -5
- package/dist/bin/commands/firebase-setup.d.ts +0 -6
- package/dist/bin/commands/firebase-setup.d.ts.map +0 -1
- package/dist/bin/commands/firebase-setup.js +0 -7
- package/dist/bin/commands/firebase-setup.js.map +0 -1
- package/dist/bin/commands/lint.d.ts +0 -11
- package/dist/bin/commands/lint.d.ts.map +0 -1
- package/dist/bin/commands/lint.js.map +0 -1
- package/dist/bin/commands/staging.d.ts +0 -11
- package/dist/bin/commands/staging.d.ts.map +0 -1
- package/dist/bin/commands/staging.js +0 -12
- package/dist/bin/commands/staging.js.map +0 -1
- package/dist/bin/commands/supabase-setup.d.ts +0 -6
- package/dist/bin/commands/supabase-setup.d.ts.map +0 -1
- package/dist/bin/commands/supabase-setup.js +0 -7
- package/dist/bin/commands/supabase-setup.js.map +0 -1
|
@@ -1,243 +1,52 @@
|
|
|
1
1
|
# Setup: CRUD Operations
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
> **API details:** `lookup_symbol("symbolName")` via MCP for full props, return types, and examples from JSDoc.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 1. Define Entity (SSOT)
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
23
|
-
## 0. Provider configuration (required)
|
|
24
|
-
|
|
25
|
-
CRUD operations use the **CRUD provider** registered at app startup. You must call `configureProviders()` before any component uses `useCrud`.
|
|
26
|
-
|
|
27
|
-
- **Where:** In a dedicated module, e.g. `src/config/providers.ts`.
|
|
28
|
-
- **When:** Import that module from your root component (e.g. `App.tsx`) so it runs before any CRUD usage.
|
|
9
|
+
The entity definition drives everything — forms, lists, validation, access, search, sort, backend.
|
|
29
10
|
|
|
30
11
|
```typescript
|
|
31
|
-
// App.tsx
|
|
32
|
-
import './config/providers'; // Before any useCrud
|
|
33
|
-
// ...
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
// config/providers.ts
|
|
38
|
-
import { configureProviders } from '@donotdev/core';
|
|
39
|
-
import { FirestoreAdapter } from '@donotdev/firebase'; // or SupabaseCrudAdapter from '@donotdev/supabase'
|
|
40
|
-
|
|
41
|
-
configureProviders({
|
|
42
|
-
crud: new FirestoreAdapter(),
|
|
43
|
-
// auth, storage optional
|
|
44
|
-
});
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
If you skip this step, `useCrud` will throw at runtime: "Provider \"crud\" not available. Call configureProviders() at app startup."
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## 1. Define Entity
|
|
52
|
-
|
|
53
|
-
```typescript
|
|
54
|
-
// entities/product.ts
|
|
55
12
|
import { defineEntity } from '@donotdev/core';
|
|
56
13
|
|
|
57
14
|
export const productEntity = defineEntity({
|
|
58
15
|
name: 'Product',
|
|
59
16
|
collection: 'products',
|
|
60
|
-
|
|
17
|
+
search: { fields: ['name', 'description'] },
|
|
18
|
+
defaultSort: { field: 'createdAt', direction: 'desc' },
|
|
61
19
|
fields: {
|
|
62
|
-
name: {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
type: 'text',
|
|
66
|
-
visibility: 'guest',
|
|
67
|
-
validation: { required: true }
|
|
68
|
-
},
|
|
69
|
-
price: {
|
|
70
|
-
name: 'price',
|
|
71
|
-
label: 'price',
|
|
72
|
-
type: 'price', // Structured: { amount, currency?, vatIncluded?, discountPercent? }
|
|
73
|
-
visibility: 'guest',
|
|
74
|
-
validation: { required: true },
|
|
75
|
-
// Optional: omit options entirely → defaultCurrency 'EUR', list = [EUR, USD, GBP, CHF, JPY, KRW]
|
|
76
|
-
options: {
|
|
77
|
-
fieldSpecific: {
|
|
78
|
-
defaultCurrency: 'EUR', // Omit → 'EUR'. Used when value.currency is unset
|
|
79
|
-
currencies: ['EUR', 'USD'], // Omit → framework default (6 currencies). One item e.g. ['EUR'] → no dropdown, EUR only
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
image: {
|
|
84
|
-
name: 'image',
|
|
85
|
-
label: 'image',
|
|
86
|
-
type: 'image',
|
|
87
|
-
visibility: 'guest'
|
|
88
|
-
},
|
|
89
|
-
customerContact: {
|
|
90
|
-
name: 'customerContact',
|
|
91
|
-
label: 'customerContact',
|
|
92
|
-
type: 'email',
|
|
93
|
-
visibility: 'admin',
|
|
94
|
-
editable: 'admin'
|
|
95
|
-
}
|
|
20
|
+
name: { name: 'name', label: 'name', type: 'text', visibility: 'guest', validation: { required: true } },
|
|
21
|
+
price: { name: 'price', label: 'price', type: 'price', visibility: 'guest', validation: { required: true } },
|
|
22
|
+
image: { name: 'image', label: 'image', type: 'image', visibility: 'guest' },
|
|
96
23
|
}
|
|
97
24
|
});
|
|
98
25
|
```
|
|
99
26
|
|
|
100
|
-
**Auto-added fields:** `id`, `createdAt`, `updatedAt`, `createdById`, `updatedById`, `status
|
|
101
|
-
|
|
102
|
-
---
|
|
103
|
-
|
|
104
|
-
## 2. Multi-Tenancy (Company/Tenant Scoping)
|
|
105
|
-
|
|
106
|
-
For multi-tenant apps where data belongs to a company/tenant/workspace:
|
|
107
|
-
|
|
108
|
-
### Step 1: Register Scope Provider (once at app startup)
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
// src/App.tsx
|
|
112
|
-
import { registerScopeProvider } from '@donotdev/core';
|
|
113
|
-
import { useCurrentCompanyStore } from './stores/currentCompanyStore';
|
|
114
|
-
|
|
115
|
-
// Register before app renders
|
|
116
|
-
registerScopeProvider('company', () =>
|
|
117
|
-
useCurrentCompanyStore.getState().currentCompanyId
|
|
118
|
-
);
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### Step 2: Add `scope` to Entity Definition
|
|
27
|
+
**Auto-added fields:** `id`, `createdAt`, `updatedAt`, `createdById`, `updatedById`, `status`.
|
|
122
28
|
|
|
123
|
-
|
|
124
|
-
// entities/client.ts
|
|
125
|
-
import { defineEntity } from '@donotdev/core';
|
|
126
|
-
|
|
127
|
-
export const clientEntity = defineEntity({
|
|
128
|
-
name: 'Client',
|
|
129
|
-
collection: 'clients',
|
|
130
|
-
scope: { field: 'companyId', provider: 'company' }, // <-- Add this
|
|
131
|
-
fields: {
|
|
132
|
-
// NO need to manually define companyId - framework adds it automatically
|
|
133
|
-
name: { name: 'name', label: 'name', type: 'text', visibility: 'user', validation: { required: true } },
|
|
134
|
-
// ...
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### What Happens Automatically
|
|
140
|
-
|
|
141
|
-
| Operation | Behavior |
|
|
142
|
-
|-----------|----------|
|
|
143
|
-
| `add()` / `set()` | Auto-injects `companyId` from scope provider |
|
|
144
|
-
| `useCrudList()` | Auto-filters by `companyId == currentCompanyId` |
|
|
145
|
-
| `useCrudCardList()` | Auto-filters by `companyId == currentCompanyId` |
|
|
146
|
-
| `query()` | Auto-adds `where companyId == currentCompanyId` |
|
|
147
|
-
|
|
148
|
-
**Result:** Zero boilerplate. Users only see data from their current company.
|
|
149
|
-
|
|
150
|
-
### Scope Field Properties
|
|
151
|
-
|
|
152
|
-
The auto-added scope field has:
|
|
153
|
-
- `type: 'reference'` (references the scoped collection, e.g., `companies`)
|
|
154
|
-
- `visibility: 'technical'` (hidden from regular users)
|
|
155
|
-
- `editable: 'create-only'` (cannot change scope after creation)
|
|
156
|
-
|
|
157
|
-
### Multiple Scope Types
|
|
158
|
-
|
|
159
|
-
You can register multiple scope providers for different use cases:
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
registerScopeProvider('company', () => companyStore.getState().currentCompanyId);
|
|
163
|
-
registerScopeProvider('workspace', () => workspaceStore.getState().currentWorkspaceId);
|
|
164
|
-
registerScopeProvider('tenant', () => tenantStore.getState().currentTenantId);
|
|
165
|
-
```
|
|
29
|
+
See `lookup_symbol("defineEntity")` for all options (scope, access, ownership, uniqueKeys, validation).
|
|
166
30
|
|
|
167
31
|
---
|
|
168
32
|
|
|
169
|
-
##
|
|
170
|
-
|
|
171
|
-
### Field Visibility (who SEES)
|
|
172
|
-
| Level | Who |
|
|
173
|
-
|-------|-----|
|
|
174
|
-
| `'guest'` | Everyone |
|
|
175
|
-
| `'user'` | Authenticated |
|
|
176
|
-
| `'admin'` | Admins |
|
|
177
|
-
| `'super'` | Super admins |
|
|
178
|
-
| `'technical'` | Admins only (read-only in forms; e.g. scope field) |
|
|
179
|
-
| `'owner'` | Only when the current user is a stakeholder (see **Stakeholder Access** below) |
|
|
180
|
-
| `'hidden'` | Never |
|
|
181
|
-
|
|
182
|
-
### Stakeholder Access (ownership)
|
|
183
|
-
|
|
184
|
-
For marketplace-style entities: documents **public when available**, **private to stakeholders** (e.g. partner, customer) when booked.
|
|
185
|
-
|
|
186
|
-
**1. Add `ownership` to the entity:**
|
|
187
|
-
|
|
188
|
-
```typescript
|
|
189
|
-
ownership: {
|
|
190
|
-
ownerFields: ['providerId', 'customerId'], // Document fields whose value is a user id
|
|
191
|
-
publicCondition: [ // When can anyone read? (AND together)
|
|
192
|
-
{ field: 'status', op: '==', value: 'available' },
|
|
193
|
-
],
|
|
194
|
-
},
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
**2. Use `visibility: 'owner'`** for fields only stakeholders should see (e.g. internal notes, customer contact after booking):
|
|
198
|
-
|
|
199
|
-
```typescript
|
|
200
|
-
internalNotes: {
|
|
201
|
-
name: 'internalNotes',
|
|
202
|
-
label: 'Internal notes',
|
|
203
|
-
type: 'textarea',
|
|
204
|
-
visibility: 'owner', // Returned only when request.auth.uid matches one of ownerFields
|
|
205
|
-
editable: true,
|
|
206
|
-
validation: { required: false },
|
|
207
|
-
},
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
**3. Firestore rules (read and update):** Use the framework helper, then paste the condition into your hand-written `firestore.rules`:
|
|
33
|
+
## 2. Provider Setup (once)
|
|
211
34
|
|
|
212
35
|
```typescript
|
|
213
|
-
|
|
36
|
+
// config/providers.ts
|
|
37
|
+
import { configureProviders } from '@donotdev/core';
|
|
38
|
+
import { FirestoreAdapter } from '@donotdev/firebase'; // or SupabaseCrudAdapter
|
|
214
39
|
|
|
215
|
-
|
|
216
|
-
// Paste into firestore.rules: allow read, update: if <condition>;
|
|
40
|
+
configureProviders({ crud: new FirestoreAdapter() });
|
|
217
41
|
```
|
|
218
42
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
### Entity Access (who CAN DO)
|
|
222
|
-
```typescript
|
|
223
|
-
access: {
|
|
224
|
-
create: 'admin', // default
|
|
225
|
-
read: 'guest', // default
|
|
226
|
-
update: 'admin', // default
|
|
227
|
-
delete: 'admin', // default
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
**Override:**
|
|
232
|
-
```typescript
|
|
233
|
-
// Contact form - guests submit, admins read
|
|
234
|
-
access: { create: 'guest', read: 'admin' }
|
|
235
|
-
```
|
|
43
|
+
Import in `App.tsx`: `import './config/providers';`
|
|
236
44
|
|
|
237
45
|
---
|
|
238
46
|
|
|
239
|
-
## 3.
|
|
47
|
+
## 3. Backend Functions
|
|
240
48
|
|
|
49
|
+
**Firebase:**
|
|
241
50
|
```typescript
|
|
242
51
|
// functions/src/index.ts
|
|
243
52
|
import { initializeApp } from 'firebase-admin/app';
|
|
@@ -248,851 +57,203 @@ initializeApp();
|
|
|
248
57
|
export const crud = createCrudFunctions(entities);
|
|
249
58
|
```
|
|
250
59
|
|
|
251
|
-
**
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
Use components directly from `@donotdev/ui`. They handle routing automatically - no navigate handlers needed.
|
|
258
|
-
|
|
259
|
-
#### Create/Edit Page
|
|
260
|
-
|
|
261
|
-
```tsx
|
|
262
|
-
import { useCrud } from '@donotdev/crud';
|
|
263
|
-
import { EntityFormRenderer, useParams } from '@donotdev/ui';
|
|
264
|
-
import { productEntity } from 'entities';
|
|
265
|
-
|
|
266
|
-
export default function ProductPage() {
|
|
267
|
-
const { id } = useParams<{ id: string }>();
|
|
268
|
-
const { add, update, get } = useCrud(productEntity);
|
|
269
|
-
const isNew = id === 'new';
|
|
270
|
-
const [data, setData] = useState<any>(null);
|
|
271
|
-
|
|
272
|
-
useEffect(() => {
|
|
273
|
-
if (!isNew && id) get(id).then(setData);
|
|
274
|
-
}, [id]);
|
|
275
|
-
|
|
276
|
-
const handleSubmit = async (formData: any) => {
|
|
277
|
-
isNew ? await add(formData) : await update(id!, formData);
|
|
278
|
-
// Navigation happens automatically via cancelPath (defaults to /products)
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
if (!isNew && !data) return <Spinner />;
|
|
282
|
-
|
|
283
|
-
return (
|
|
284
|
-
<EntityFormRenderer
|
|
285
|
-
entity={productEntity}
|
|
286
|
-
operation={isNew ? 'create' : 'edit'}
|
|
287
|
-
defaultValues={data}
|
|
288
|
-
onSubmit={handleSubmit}
|
|
289
|
-
// Cancel automatically navigates to /products (or cancelPath if provided)
|
|
290
|
-
cancelPath="/products"
|
|
291
|
-
/>
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
#### List Page
|
|
297
|
-
|
|
298
|
-
```tsx
|
|
299
|
-
import { EntityList, PageContainer } from '@donotdev/ui';
|
|
300
|
-
import { useAuth } from '@donotdev/auth';
|
|
301
|
-
import { productEntity } from 'entities';
|
|
302
|
-
|
|
303
|
-
export default function ProductsPage() {
|
|
304
|
-
const user = useAuth('user');
|
|
305
|
-
|
|
306
|
-
return (
|
|
307
|
-
<PageContainer>
|
|
308
|
-
<EntityList
|
|
309
|
-
entity={productEntity}
|
|
310
|
-
userRole={user?.role}
|
|
311
|
-
// Routing: basePath defaults to /products. View/Edit = basePath/:id, Create = basePath/new
|
|
312
|
-
// Optional: basePath="/admin/products" or onClick={(id) => openSheet(id)}
|
|
313
|
-
/>
|
|
314
|
-
</PageContainer>
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
#### Card Grid
|
|
320
|
-
|
|
321
|
-
```tsx
|
|
322
|
-
import { EntityCardList, PageContainer } from '@donotdev/ui';
|
|
323
|
-
import { productEntity } from 'entities';
|
|
324
|
-
|
|
325
|
-
export default function ShopPage() {
|
|
326
|
-
return (
|
|
327
|
-
<PageContainer>
|
|
328
|
-
<EntityCardList
|
|
329
|
-
entity={productEntity}
|
|
330
|
-
// Routing: basePath defaults to /products. View = basePath/:id
|
|
331
|
-
// Optional: basePath="/shop/products" or onClick={(id) => openSheet(id)}
|
|
332
|
-
/>
|
|
333
|
-
</PageContainer>
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
#### Detail/View Page
|
|
339
|
-
|
|
340
|
-
```tsx
|
|
341
|
-
import { EntityDisplayRenderer, useParams } from '@donotdev/ui';
|
|
342
|
-
import { productEntity } from 'entities';
|
|
343
|
-
|
|
344
|
-
export default function ProductDetailPage() {
|
|
345
|
-
const { id } = useParams<{ id: string }>();
|
|
346
|
-
|
|
347
|
-
return (
|
|
348
|
-
<EntityDisplayRenderer
|
|
349
|
-
entity={productEntity}
|
|
350
|
-
id={id}
|
|
351
|
-
// Auto-fetches data, shows loading state
|
|
352
|
-
// Read-only display of all visible fields
|
|
353
|
-
/>
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
#### Custom Detail Page with Favorites
|
|
359
|
-
|
|
360
|
-
For custom detail pages, add favorites support using `useEntityFavorites`:
|
|
361
|
-
|
|
362
|
-
```tsx
|
|
363
|
-
import { useState, useEffect } from 'react';
|
|
364
|
-
import { Heart } from 'lucide-react';
|
|
365
|
-
import { PageContainer, Section, Button, Stack } from '@donotdev/components';
|
|
366
|
-
import { useParams, useNavigate } from '@donotdev/ui';
|
|
367
|
-
import { useCrud, useEntityFavorites } from '@donotdev/crud';
|
|
368
|
-
import { useTranslation } from '@donotdev/core';
|
|
369
|
-
import { productEntity } from 'entities';
|
|
370
|
-
|
|
371
|
-
export default function ProductDetailPage() {
|
|
372
|
-
const { id } = useParams<{ id: string }>();
|
|
373
|
-
const navigate = useNavigate();
|
|
374
|
-
const { get } = useCrud(productEntity);
|
|
375
|
-
const { isFavorite, toggleFavorite } = useEntityFavorites({
|
|
376
|
-
collection: productEntity.collection,
|
|
377
|
-
});
|
|
378
|
-
const { t } = useTranslation('crud');
|
|
379
|
-
const [data, setData] = useState<any>(null);
|
|
380
|
-
const [loading, setLoading] = useState(true);
|
|
381
|
-
|
|
382
|
-
useEffect(() => {
|
|
383
|
-
if (id) {
|
|
384
|
-
get(id).then((result) => {
|
|
385
|
-
setData(result);
|
|
386
|
-
setLoading(false);
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
}, [id, get]);
|
|
390
|
-
|
|
391
|
-
if (loading) return <div>Loading...</div>;
|
|
392
|
-
if (!data) return <div>Not found</div>;
|
|
393
|
-
|
|
394
|
-
return (
|
|
395
|
-
<PageContainer>
|
|
396
|
-
<Section>
|
|
397
|
-
<Stack direction="row" align="center">
|
|
398
|
-
<h1>{data.name}</h1>
|
|
399
|
-
<Button
|
|
400
|
-
variant={isFavorite(id) ? 'primary' : 'outline'}
|
|
401
|
-
icon={<Heart size={18} fill={isFavorite(id) ? 'currentColor' : 'none'} />}
|
|
402
|
-
onClick={() => toggleFavorite(id)}
|
|
403
|
-
>
|
|
404
|
-
{isFavorite(id) ? t('favorites.saved') : t('favorites.save')}
|
|
405
|
-
</Button>
|
|
406
|
-
</Stack>
|
|
407
|
-
{/* Your custom detail page content */}
|
|
408
|
-
</Section>
|
|
409
|
-
</PageContainer>
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
**Note:** `EntityCardList` automatically includes favorites support - heart icons on cards and a favorites filter toggle. No configuration needed.
|
|
415
|
-
|
|
416
|
-
#### Product Shop (Specialized Template)
|
|
417
|
-
|
|
418
|
-
For e-commerce/shop pages with price, image, and category support, use the specialized template:
|
|
419
|
-
|
|
420
|
-
```tsx
|
|
421
|
-
import { ProductCardListTemplate } from '@donotdev/templates';
|
|
422
|
-
import { productEntity } from 'entities';
|
|
423
|
-
|
|
424
|
-
export default function ShopPage() {
|
|
425
|
-
return (
|
|
426
|
-
<PageContainer>
|
|
427
|
-
<ProductCardListTemplate
|
|
428
|
-
entity={productEntity}
|
|
429
|
-
// Auto-detects price, image, category fields
|
|
430
|
-
// Routing: basePath defaults to /products. Optional: basePath or onClick
|
|
431
|
-
/>
|
|
432
|
-
</PageContainer>
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
#### Contact/Inquiry Form (Specialized Template)
|
|
438
|
-
|
|
439
|
-
For contact forms that create both a Customer and an Inquiry record:
|
|
440
|
-
|
|
441
|
-
```tsx
|
|
442
|
-
import { InquiryFormTemplate } from '@donotdev/templates';
|
|
443
|
-
import { customerEntity, inquiryEntity } from 'entities';
|
|
60
|
+
**Supabase:**
|
|
61
|
+
```typescript
|
|
62
|
+
// supabase/functions/crud/index.ts
|
|
63
|
+
import * as entities from '../_shared/entities.ts';
|
|
64
|
+
import { createSupabaseCrudFunctions } from '@donotdev/functions/supabase';
|
|
444
65
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
<PageContainer>
|
|
448
|
-
<InquiryFormTemplate
|
|
449
|
-
customerEntity={customerEntity}
|
|
450
|
-
inquiryEntity={inquiryEntity}
|
|
451
|
-
// Optional: contextId="car123" - links inquiry to a car/product
|
|
452
|
-
// Optional: contextName="BMW X5 2024" - shown in message placeholder
|
|
453
|
-
// Optional: contextDetails="BMW X5 2024 • 50,000 km • €45,000" - detailed placeholder
|
|
454
|
-
// Optional: messageField="message" - defaults to 'message'
|
|
455
|
-
// Optional: contextField="carId" - defaults to 'carId'
|
|
456
|
-
// Optional: customerFields={['firstName', 'lastName', 'email', 'phone']} - fields to show
|
|
457
|
-
// Optional: onSuccess={() => navigate('/thank-you')}
|
|
458
|
-
/>
|
|
459
|
-
</PageContainer>
|
|
460
|
-
);
|
|
461
|
-
}
|
|
66
|
+
const { serve } = createSupabaseCrudFunctions(entities);
|
|
67
|
+
Deno.serve(serve);
|
|
462
68
|
```
|
|
463
69
|
|
|
464
|
-
|
|
465
|
-
- Creates a Customer record (with `findOrCreate` logic via `uniqueKeys` - prevents duplicates by email/phone)
|
|
466
|
-
- Creates an Inquiry record linked to the customer
|
|
467
|
-
- Handles GDPR consent tracking
|
|
468
|
-
- Auto-fills message with context details if provided
|
|
469
|
-
- Shows success state after submission
|
|
70
|
+
See [SETUP_FIREBASE.md](./SETUP_FIREBASE.md) or [SETUP_SUPABASE.md](./SETUP_SUPABASE.md) for full function setup.
|
|
470
71
|
|
|
471
|
-
|
|
472
|
-
- Customer entity must have `uniqueKeys` configured (e.g., `{ fields: ['email'], findOrCreate: true }`)
|
|
473
|
-
- Inquiry entity should have `customerId` field (type: `reference`)
|
|
474
|
-
- Both entities should have `status` field (defaults to `'draft'`)
|
|
72
|
+
---
|
|
475
73
|
|
|
476
|
-
|
|
74
|
+
## 4. Use Components
|
|
477
75
|
|
|
478
|
-
|
|
76
|
+
Drop components on a page. Entity drives the UI — no configuration needed.
|
|
479
77
|
|
|
480
78
|
```tsx
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return (
|
|
486
|
-
<PageContainer>
|
|
487
|
-
<InquiryAdminTemplate
|
|
488
|
-
customerEntity={customerEntity}
|
|
489
|
-
inquiryEntity={inquiryEntity}
|
|
490
|
-
// Optional: customerBasePath="/customers" - defaults to '/customers'
|
|
491
|
-
/>
|
|
492
|
-
</PageContainer>
|
|
493
|
-
);
|
|
494
|
-
}
|
|
79
|
+
<EntityList entity={productEntity} userRole={user?.role} />
|
|
80
|
+
<EntityCardList entity={productEntity} basePath="/shop" cols={[1, 2, 3, 4]} />
|
|
81
|
+
<EntityFormRenderer entity={productEntity} operation="create" onSubmit={handleSubmit} cancelPath="/products" />
|
|
82
|
+
<EntityDisplayRenderer entity={productEntity} id={id} />
|
|
495
83
|
```
|
|
496
84
|
|
|
497
|
-
|
|
498
|
-
- Shows all inquiries in a responsive card grid (1 column mobile, 2 columns desktop)
|
|
499
|
-
- Displays inquiry message preview (first 150 chars)
|
|
500
|
-
- Shows customer info inline (name, email, phone)
|
|
501
|
-
- One-click actions:
|
|
502
|
-
- **Email** - Opens `mailto:` link (includes subject if `carId` present)
|
|
503
|
-
- **Call** - Opens `tel:` link
|
|
504
|
-
- **View Customer** - Navigates to customer detail page
|
|
505
|
-
- **Mark Responded** - Updates inquiry status to `'responded'` (only shown if not already responded)
|
|
506
|
-
- Status badges with icons (✓ for responded, ⏰ for pending)
|
|
507
|
-
- Auto-sorted by priority: available → draft → responded → deleted (then by date, newest first)
|
|
508
|
-
|
|
509
|
-
**Features:**
|
|
510
|
-
- Fetches all inquiries and customers in parallel
|
|
511
|
-
- Efficient customer lookup via Map (no N+1 queries)
|
|
512
|
-
- Loading state with spinner
|
|
513
|
-
- Empty state message
|
|
514
|
-
- Responsive grid layout
|
|
85
|
+
Use `lookup_symbol("ComponentName")` for full props.
|
|
515
86
|
|
|
516
87
|
---
|
|
517
88
|
|
|
518
|
-
## 5.
|
|
519
|
-
|
|
520
|
-
### EntityList
|
|
521
|
-
- `basePath` **(optional)** - Base path for view/edit/create. Defaults to `/${collection}`. View/Edit = basePath/:id, Create = basePath/new
|
|
522
|
-
- `onClick` **(optional)** - Called when user clicks a row. If provided, overrides navigation (e.g. open sheet)
|
|
523
|
-
- `userRole` **(optional)** - Current user role for field visibility
|
|
524
|
-
|
|
525
|
-
### EntityCardList
|
|
526
|
-
- `basePath` **(optional)** - Base path for view. Defaults to `/${collection}`. View = basePath/:id
|
|
527
|
-
- `onClick` **(optional)** - Called when user clicks a card. If provided, overrides navigation (e.g. open sheet)
|
|
528
|
-
- `cols` **(optional)** - `[mobile, tablet, desktop, wide]` (default: `[1, 2, 3, 4]`)
|
|
529
|
-
- `staleTime` **(optional)** - Cache stale time in ms (default: 30 min)
|
|
530
|
-
- `filter` **(optional)** - `(item: any) => boolean` - Client-side filter
|
|
531
|
-
- `hideFilters` **(optional)** - Hide filters section
|
|
532
|
-
|
|
533
|
-
**Favorites:** Automatically enabled - heart icons on cards, favorites filter toggle in filters section. Uses `useEntityFavorites` hook internally with `entity.collection` as storage key.
|
|
534
|
-
|
|
535
|
-
### EntityFormRenderer
|
|
536
|
-
- `entity` **(required)** - Entity definition
|
|
537
|
-
- `onSubmit` **(required)** - `(data: any) => Promise<void>` - Submit handler
|
|
538
|
-
- `defaultValues` **(optional)** - Initial form values (for edit mode)
|
|
539
|
-
- `operation` **(optional)** - `'create' | 'edit'` (auto-detected from defaultValues)
|
|
540
|
-
- `viewerRole` **(optional)** - Role for field visibility/editability
|
|
541
|
-
- `cancelPath` **(optional)** - Path to navigate on cancel (defaults to `/${collection}`)
|
|
542
|
-
- `onCancel` **(optional)** - Cancel button handler (takes precedence over cancelPath)
|
|
543
|
-
- `cancelText` **(optional)** - Cancel button text (null to hide)
|
|
544
|
-
|
|
545
|
-
### EntityDisplayRenderer
|
|
546
|
-
- `entity` **(required)** - Entity definition
|
|
547
|
-
- `id` **(required)** - Entity ID to fetch and display
|
|
548
|
-
- `viewerRole` **(optional)** - Role for field visibility
|
|
89
|
+
## 5. Visibility & Access
|
|
549
90
|
|
|
550
|
-
|
|
91
|
+
### Field Visibility (who sees what)
|
|
551
92
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
- `textarea` - Multi-line text input
|
|
562
|
-
- `richtext` - Rich text editor
|
|
563
|
-
|
|
564
|
-
### Numbers
|
|
565
|
-
- `number` - Numeric input
|
|
566
|
-
- `currency` - Single amount with currency symbol (value: number; options: `fieldSpecific.currency`)
|
|
567
|
-
- `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.
|
|
568
|
-
- `range` - Slider input
|
|
569
|
-
- `year` - Year input (combobox: type or select from dropdown)
|
|
570
|
-
|
|
571
|
-
### Boolean
|
|
572
|
-
- `checkbox` - Checkbox input
|
|
573
|
-
- `boolean` - Alias for checkbox
|
|
574
|
-
- `switch` - Toggle switch
|
|
575
|
-
|
|
576
|
-
### Dates & Time
|
|
577
|
-
- `date` - Date picker
|
|
578
|
-
- `datetime-local` - Date and time picker
|
|
579
|
-
- `time` - Time picker
|
|
580
|
-
- `week` - Week picker
|
|
581
|
-
- `month` - Month picker
|
|
582
|
-
- `timestamp` - Timestamp (Firestore Timestamp)
|
|
583
|
-
|
|
584
|
-
### Selection
|
|
585
|
-
- `select` - Dropdown select
|
|
586
|
-
- `combobox` - Searchable dropdown
|
|
587
|
-
- `multiselect` - Multiple selection dropdown
|
|
588
|
-
- `radio` - Radio button group
|
|
589
|
-
|
|
590
|
-
### Files & Media
|
|
591
|
-
- `file` - Single file upload (deferred: uploads on form submit)
|
|
592
|
-
- `files` - Multiple file uploads (deferred: uploads on form submit)
|
|
593
|
-
- `document` - Document upload (PDF, etc.) (deferred: uploads on form submit)
|
|
594
|
-
- `documents` - Multiple document uploads (deferred: uploads on form submit)
|
|
595
|
-
- `image` - Single image upload (deferred: uploads on form submit, optimistic UI updates)
|
|
596
|
-
- `images` - Multiple image uploads (deferred: uploads on form submit, optimistic UI updates)
|
|
597
|
-
|
|
598
|
-
**Note:** File uploads use deferred uploads when inside `EntityFormRenderer` - files upload to storage when you submit the form. Images show immediately with optimistic updates (blob URLs) and automatically replace with storage URLs after upload completes.
|
|
599
|
-
|
|
600
|
-
### Complex Types
|
|
601
|
-
- `geopoint` - Geographic coordinates (lat/lng)
|
|
602
|
-
- `address` - Address input with autocomplete
|
|
603
|
-
- `map` - Map picker
|
|
604
|
-
- `array` - Array of text inputs
|
|
605
|
-
|
|
606
|
-
### Special
|
|
607
|
-
- `avatar` - Avatar image upload
|
|
608
|
-
- `badge` - Badge display
|
|
609
|
-
- `hidden` - Hidden field (not displayed)
|
|
610
|
-
- `submit` - Submit button (uncontrolled)
|
|
611
|
-
- `reset` - Reset button (uncontrolled)
|
|
612
|
-
|
|
613
|
-
### Price field type (structured)
|
|
614
|
-
|
|
615
|
-
Use `type: 'price'` when you need amount, currency, VAT label, and optional discount in one field. Replaces separate "price" + "specialPrice" number fields.
|
|
616
|
-
|
|
617
|
-
**Definition:**
|
|
618
|
-
```typescript
|
|
619
|
-
price: {
|
|
620
|
-
name: 'price',
|
|
621
|
-
label: 'price',
|
|
622
|
-
type: 'price',
|
|
623
|
-
visibility: 'guest',
|
|
624
|
-
validation: { required: true },
|
|
625
|
-
options: {
|
|
626
|
-
fieldSpecific: {
|
|
627
|
-
defaultCurrency: 'EUR', // Omit → 'EUR'. Used when value.currency is unset
|
|
628
|
-
currencies: ['EUR', 'USD'], // Omit → list = [EUR, USD, GBP, CHF, JPY, KRW]. One item e.g. ['EUR'] → no dropdown, single currency
|
|
629
|
-
optionsTitle: 'Price options', // Omit → 'Price options'. Aria-label for the expand button
|
|
630
|
-
},
|
|
631
|
-
},
|
|
632
|
-
},
|
|
633
|
-
```
|
|
634
|
-
|
|
635
|
-
**Value shape (stored in DB):**
|
|
636
|
-
```ts
|
|
637
|
-
{ amount: number; currency?: string; vatIncluded?: boolean; discountPercent?: number }
|
|
638
|
-
```
|
|
639
|
-
- Only `discountPercent` is stored for discount; effective price is computed as `amount * (1 - discountPercent/100)` for display.
|
|
640
|
-
|
|
641
|
-
**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.
|
|
642
|
-
|
|
643
|
-
**Display:** Formatted amount + "VAT Incl." when `vatIncluded`. When `discountPercent > 0`: original amount crossed out + effective price.
|
|
644
|
-
|
|
645
|
-
**Templates:** `CarCardListTemplate`, `CarDetailTemplate`, `ProductCardListTemplate` use the price field when present and show a "Sale" (or `price.badge.sale` i18n) badge when discounted.
|
|
646
|
-
|
|
647
|
-
**Filtering:** Range filter on `amount`; optional "deals" filter (items with `discountPercent > 0`).
|
|
93
|
+
| Level | Who |
|
|
94
|
+
|-------|-----|
|
|
95
|
+
| `'guest'` | Everyone |
|
|
96
|
+
| `'user'` | Authenticated |
|
|
97
|
+
| `'admin'` | Admins |
|
|
98
|
+
| `'super'` | Super admins |
|
|
99
|
+
| `'technical'` | Admins only, read-only in forms |
|
|
100
|
+
| `'owner'` | Stakeholders only (see `ownership`) |
|
|
101
|
+
| `'hidden'` | Never |
|
|
648
102
|
|
|
649
|
-
###
|
|
103
|
+
### Entity Access (who can CRUD)
|
|
650
104
|
|
|
651
105
|
```typescript
|
|
652
|
-
|
|
653
|
-
type: 'select',
|
|
654
|
-
validation: {
|
|
655
|
-
options: [
|
|
656
|
-
{ value: 'electronics', label: 'Electronics' },
|
|
657
|
-
{ value: 'clothing', label: 'Clothing' }
|
|
658
|
-
]
|
|
659
|
-
}
|
|
660
|
-
}
|
|
106
|
+
access: { create: 'admin', read: 'guest', update: 'admin', delete: 'admin' }
|
|
661
107
|
```
|
|
662
108
|
|
|
663
|
-
###
|
|
664
|
-
|
|
665
|
-
Use `type: 'year'` for year inputs (combobox with type-or-select UX):
|
|
666
|
-
|
|
667
|
-
```typescript
|
|
668
|
-
year: {
|
|
669
|
-
type: 'year',
|
|
670
|
-
validation: {
|
|
671
|
-
required: true,
|
|
672
|
-
min: 1900, // Optional: defaults to 1900
|
|
673
|
-
max: 2100 // Optional: defaults to current year + 10
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
```
|
|
109
|
+
### Ownership
|
|
677
110
|
|
|
678
|
-
|
|
111
|
+
For marketplace patterns. See `lookup_symbol("defineEntity")` → `ownership`.
|
|
679
112
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
type: 'switch',
|
|
683
|
-
options: {
|
|
684
|
-
fieldSpecific: {
|
|
685
|
-
uncheckedValue: 'Manual',
|
|
686
|
-
checkedValue: 'Automatic',
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
```
|
|
113
|
+
**Firebase:** Enforced by generated secure Firebase Functions (server-side).
|
|
114
|
+
**Supabase:** Enforced via RLS policies — generated by `dndev setup supabase`.
|
|
691
115
|
|
|
692
116
|
---
|
|
693
117
|
|
|
694
|
-
##
|
|
695
|
-
|
|
696
|
-
A custom field type (when the requirement is not a built-in type) needs up to four things: **(1)** entity field with your custom `type` and `validation.schema`, **(2)** form component(s) so the field is editable, **(3)** optional **display** formatter so list/card/detail views render the value correctly, **(4)** optional **filter** so the field appears in EntityFilters with the right filter UI. All four are wired through a single `registerFieldType({ ... })` call.
|
|
697
|
-
|
|
698
|
-
> **Custom type strings work out of the box.** Just use any string as the `type` — no declaration file or module augmentation needed. The framework accepts any string while still autocompleting built-in types.
|
|
699
|
-
>
|
|
700
|
-
> `CustomFieldOptionsMap` augmentation (see below) is only needed if you want type-checked `options.fieldSpecific` for your custom type.
|
|
701
|
-
|
|
702
|
-
### Form (required for editable fields)
|
|
703
|
-
|
|
704
|
-
You must register a **controlled component** so the field appears in forms. Optionally register an **uncontrolled component** (e.g. for submit/reset-style fields).
|
|
118
|
+
## 6. Multi-Tenancy (Scope)
|
|
705
119
|
|
|
706
120
|
```typescript
|
|
707
|
-
|
|
708
|
-
import type { ControlledFieldProps } from '@donotdev/crud';
|
|
709
|
-
import { defineEntity } from '@donotdev/core';
|
|
710
|
-
import * as v from 'valibot';
|
|
711
|
-
|
|
712
|
-
function RepairOperationsField({
|
|
713
|
-
fieldConfig,
|
|
714
|
-
control,
|
|
715
|
-
errors,
|
|
716
|
-
t,
|
|
717
|
-
}: ControlledFieldProps) {
|
|
718
|
-
// REQUIRED: Use framework's useController (not react-hook-form's) - ensures type compatibility
|
|
719
|
-
const { field, fieldState } = useController({
|
|
720
|
-
name: fieldConfig.name,
|
|
721
|
-
control: control,
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
const value = (field.value as any) || [];
|
|
725
|
-
|
|
726
|
-
return (
|
|
727
|
-
<div>
|
|
728
|
-
<label>{t(fieldConfig.label)}</label>
|
|
729
|
-
{/* Your custom UI here */}
|
|
730
|
-
{fieldState?.error && (
|
|
731
|
-
<span className="error">{fieldState.error.message}</span>
|
|
732
|
-
)}
|
|
733
|
-
</div>
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// Minimal registration (form only)
|
|
738
|
-
registerFieldType({
|
|
739
|
-
type: 'repairOperations',
|
|
740
|
-
controlledComponent: RepairOperationsField,
|
|
741
|
-
});
|
|
121
|
+
registerScopeProvider('company', () => companyStore.getState().currentCompanyId);
|
|
742
122
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
collection: 'cars',
|
|
747
|
-
fields: {
|
|
748
|
-
repairs: {
|
|
749
|
-
name: 'repairs',
|
|
750
|
-
label: 'repairs',
|
|
751
|
-
type: 'repairOperations',
|
|
752
|
-
visibility: 'admin',
|
|
753
|
-
validation: {
|
|
754
|
-
required: false,
|
|
755
|
-
schema: v.nullish(v.array(v.object({
|
|
756
|
-
operation: v.string(),
|
|
757
|
-
cost: v.number(),
|
|
758
|
-
})))
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
}
|
|
123
|
+
export const clientEntity = defineEntity({
|
|
124
|
+
scope: { field: 'companyId', provider: 'company' },
|
|
125
|
+
// ... (companyId auto-added, all CRUD ops auto-scoped)
|
|
762
126
|
});
|
|
763
127
|
```
|
|
764
128
|
|
|
765
|
-
|
|
129
|
+
See `lookup_symbol("registerScopeProvider")`.
|
|
766
130
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
Without a **displayFormatter**, list/card/detail views show the raw value (or a fallback). To control how the value is rendered in read-only views, pass `displayFormatter` in the same `registerFieldType` call. Signature: `(value, fieldConfig, t, options?) => string | ReactNode`. Use `options?.compact` for list/card vs detail.
|
|
131
|
+
---
|
|
770
132
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
133
|
+
## What's Available
|
|
134
|
+
|
|
135
|
+
### Components (`@donotdev/ui`)
|
|
136
|
+
|
|
137
|
+
| Component | One-liner |
|
|
138
|
+
|-----------|-----------|
|
|
139
|
+
| `EntityList` | Table list with search, filters, pagination, routing |
|
|
140
|
+
| `EntityCardList` | Card grid with search, filters, favorites, routing |
|
|
141
|
+
| `EntityFormRenderer` | Auto-generated form from entity (create/edit) |
|
|
142
|
+
| `EntityDisplayRenderer` | Read-only detail view, auto-fetches by ID |
|
|
143
|
+
| `EntityRecommendations` | "You may also like" related items |
|
|
144
|
+
|
|
145
|
+
### Templates (`@donotdev/templates`)
|
|
146
|
+
|
|
147
|
+
| Template | One-liner |
|
|
148
|
+
|----------|-----------|
|
|
149
|
+
| `ProductCardListTemplate` | Shop card grid with price, image, sale badges |
|
|
150
|
+
| `CarCardListTemplate` | Automotive listing with mileage, year |
|
|
151
|
+
| `CarDetailTemplate` | Car detail with gallery, specs, price |
|
|
152
|
+
| `InquiryFormTemplate` | Contact form (Customer + Inquiry, GDPR) |
|
|
153
|
+
| `InquiryAdminTemplate` | Admin dashboard for inquiries |
|
|
154
|
+
| `HomeTemplate` | Landing page |
|
|
155
|
+
| `LoginTemplate` | Auth login page |
|
|
156
|
+
| `DashboardTemplate` | Admin dashboard |
|
|
157
|
+
| `CheckoutTemplate` | Stripe checkout |
|
|
158
|
+
| `BillingSuccessTemplate` | Post-checkout success |
|
|
159
|
+
| `UserSubscriptionTemplate` | Manage subscription |
|
|
160
|
+
|
|
161
|
+
### Hooks (`@donotdev/crud`)
|
|
162
|
+
|
|
163
|
+
| Hook | One-liner |
|
|
164
|
+
|------|-----------|
|
|
165
|
+
| `useCrud` | CRUD actions: `add`, `update`, `delete`, `get`, `set`, `query` |
|
|
166
|
+
| `useCrudList` | Real-time list with search, filters, sort applied |
|
|
167
|
+
| `useCrudCardList` | Same as `useCrudList`, optimized for public card views |
|
|
168
|
+
| `useCrudFilters` | Filter state per collection, auto-consumed by list hooks |
|
|
169
|
+
| `useEntityForm` | Form state for custom form layouts |
|
|
170
|
+
| `useEntityField` | Single-field control for custom form layouts |
|
|
171
|
+
| `useEntityFavorites` | Local favorites (localStorage) |
|
|
172
|
+
| `useRelatedItems` | Fetch related items by reference field |
|
|
173
|
+
| `useFileUpload` | File upload with progress tracking |
|
|
174
|
+
| `useUnsavedChangesWarning` | Warn before leaving with unsaved changes |
|
|
175
|
+
|
|
176
|
+
### Utilities (`@donotdev/crud`)
|
|
177
|
+
|
|
178
|
+
| Utility | One-liner |
|
|
179
|
+
|---------|-----------|
|
|
180
|
+
| `formatValue` | Format any field value for display |
|
|
181
|
+
| `applyFilters` / `applySearch` / `applySort` | Processing pipeline (what `useCrudList` does internally) |
|
|
182
|
+
| `registerFieldType` | Register custom field type |
|
|
183
|
+
| `validateEntity` | Validate entity data against schemas |
|
|
184
|
+
| `isFieldEditable` / `getFieldsForOperation` | Field visibility helpers |
|
|
185
|
+
|
|
186
|
+
### Routing (`@donotdev/ui`)
|
|
187
|
+
|
|
188
|
+
| Symbol | One-liner |
|
|
189
|
+
|--------|-----------|
|
|
190
|
+
| `useNavigate` | Programmatic navigation (Vite/Next.js auto-detected) |
|
|
191
|
+
| `useParams` | Read route params |
|
|
192
|
+
| `Link` | Declarative navigation |
|
|
193
|
+
| `AuthGuard` | Protect routes by auth + role |
|
|
194
|
+
|
|
195
|
+
**Convention:** `/${collection}` (list) · `/${collection}/new` (create) · `/${collection}/:id` (detail/edit)
|
|
780
196
|
|
|
781
|
-
|
|
197
|
+
---
|
|
782
198
|
|
|
783
|
-
|
|
199
|
+
## Built-in Field Types
|
|
784
200
|
|
|
785
|
-
|
|
|
786
|
-
|
|
787
|
-
| `
|
|
788
|
-
| `
|
|
789
|
-
|
|
|
790
|
-
| `
|
|
791
|
-
| `
|
|
792
|
-
|
|
|
201
|
+
| Category | Types |
|
|
202
|
+
|----------|-------|
|
|
203
|
+
| **Text** | `text`, `email`, `tel`, `url`, `color`, `password`, `textarea`, `richtext` |
|
|
204
|
+
| **Number** | `number`, `currency`, `price`, `range`, `year` |
|
|
205
|
+
| **Boolean** | `checkbox`, `boolean`, `switch` |
|
|
206
|
+
| **Date/Time** | `date`, `datetime-local`, `time`, `week`, `month`, `timestamp` |
|
|
207
|
+
| **Selection** | `select`, `combobox`, `multiselect`, `radio` |
|
|
208
|
+
| **Files** | `file`, `files`, `document`, `documents`, `image`, `images` |
|
|
209
|
+
| **Complex** | `geopoint`, `address`, `map`, `array` |
|
|
210
|
+
| **Special** | `avatar`, `badge`, `hidden`, `submit`, `reset` |
|
|
793
211
|
|
|
794
|
-
|
|
795
|
-
registerFieldType({
|
|
796
|
-
type: 'repairOperations',
|
|
797
|
-
controlledComponent: RepairOperationsField,
|
|
798
|
-
displayFormatter: (value, fieldConfig, t, options) => { /* ... */ },
|
|
799
|
-
filterable: true,
|
|
800
|
-
filterType: 'text', // or 'range', 'select', 'multiselect', 'address', 'none'
|
|
801
|
-
});
|
|
802
|
-
```
|
|
212
|
+
---
|
|
803
213
|
|
|
804
|
-
|
|
214
|
+
## Custom Field Types (escape hatch)
|
|
805
215
|
|
|
806
|
-
|
|
216
|
+
When built-in types don't fit, register your own with `registerFieldType`. One call wires form component + display formatter + filter.
|
|
807
217
|
|
|
808
218
|
```typescript
|
|
809
219
|
import { registerFieldType, useController } from '@donotdev/crud';
|
|
810
220
|
import type { ControlledFieldProps } from '@donotdev/crud';
|
|
811
|
-
import { defineEntity } from '@donotdev/core';
|
|
812
|
-
import * as v from 'valibot';
|
|
813
221
|
|
|
814
|
-
function StatusTierField({ fieldConfig, control,
|
|
222
|
+
function StatusTierField({ fieldConfig, control, t }: ControlledFieldProps) {
|
|
815
223
|
const { field, fieldState } = useController({ name: fieldConfig.name, control });
|
|
816
224
|
return (
|
|
817
|
-
<
|
|
818
|
-
<
|
|
819
|
-
<
|
|
820
|
-
|
|
821
|
-
<option value="basic">Basic</option>
|
|
822
|
-
<option value="premium">Premium</option>
|
|
823
|
-
</select>
|
|
824
|
-
{fieldState?.error && <span className="error">{fieldState.error.message}</span>}
|
|
825
|
-
</div>
|
|
225
|
+
<select value={field.value ?? ''} onChange={(e) => field.onChange(e.target.value)}>
|
|
226
|
+
<option value="basic">Basic</option>
|
|
227
|
+
<option value="premium">Premium</option>
|
|
228
|
+
</select>
|
|
826
229
|
);
|
|
827
230
|
}
|
|
828
231
|
|
|
829
232
|
registerFieldType({
|
|
830
233
|
type: 'statusTier',
|
|
831
234
|
controlledComponent: StatusTierField,
|
|
832
|
-
displayFormatter: (value) =>
|
|
235
|
+
displayFormatter: (value) => value ? String(value).charAt(0).toUpperCase() + String(value).slice(1) : '—',
|
|
833
236
|
filterable: true,
|
|
834
237
|
filterType: 'select',
|
|
835
238
|
});
|
|
836
|
-
|
|
837
|
-
export const productEntity = defineEntity({
|
|
838
|
-
name: 'Product',
|
|
839
|
-
collection: 'products',
|
|
840
|
-
fields: {
|
|
841
|
-
statusTier: {
|
|
842
|
-
name: 'statusTier',
|
|
843
|
-
label: 'statusTier',
|
|
844
|
-
type: 'statusTier',
|
|
845
|
-
visibility: 'guest',
|
|
846
|
-
validation: { schema: v.optional(v.picklist(['basic', 'premium'])) },
|
|
847
|
-
},
|
|
848
|
-
},
|
|
849
|
-
});
|
|
850
239
|
```
|
|
851
240
|
|
|
852
|
-
|
|
241
|
+
Then use in entity: `tier: { name: 'tier', label: 'tier', type: 'statusTier', visibility: 'guest' }`
|
|
853
242
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
```typescript
|
|
857
|
-
// e.g. in src/types/crud.d.ts or next to your entity
|
|
858
|
-
import '@donotdev/core';
|
|
859
|
-
declare module '@donotdev/core' {
|
|
860
|
-
interface CustomFieldOptionsMap {
|
|
861
|
-
'repairOperations': { maxItems?: number };
|
|
862
|
-
'isousou-address': { extractDistrictCode?: boolean };
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
```
|
|
866
|
-
|
|
867
|
-
Then in entity fields you can use `type: 'repairOperations'` and `options: { fieldSpecific: { maxItems: 10 } }` without type errors or `as any`.
|
|
868
|
-
|
|
869
|
-
### Custom Schemas (No Custom UI)
|
|
870
|
-
|
|
871
|
-
For custom validation without custom UI, just define the schema:
|
|
872
|
-
|
|
873
|
-
```typescript
|
|
874
|
-
import { defineEntity } from '@donotdev/core';
|
|
875
|
-
import * as v from 'valibot';
|
|
876
|
-
|
|
877
|
-
export const carEntity = defineEntity({
|
|
878
|
-
name: 'Car',
|
|
879
|
-
collection: 'cars',
|
|
880
|
-
fields: {
|
|
881
|
-
// Custom array field with inline schema
|
|
882
|
-
repairs: {
|
|
883
|
-
name: 'repairs',
|
|
884
|
-
label: 'repairs',
|
|
885
|
-
type: 'text', // Can be any type, schema takes precedence
|
|
886
|
-
visibility: 'admin',
|
|
887
|
-
validation: {
|
|
888
|
-
required: false,
|
|
889
|
-
// Custom Valibot schema - takes priority over type-based schema
|
|
890
|
-
schema: v.array(
|
|
891
|
-
v.object({
|
|
892
|
-
operation: v.string(),
|
|
893
|
-
cost: v.number(),
|
|
894
|
-
date: v.string(), // ISO date string
|
|
895
|
-
})
|
|
896
|
-
)
|
|
897
|
-
}
|
|
898
|
-
},
|
|
899
|
-
|
|
900
|
-
// Custom object field
|
|
901
|
-
metadata: {
|
|
902
|
-
name: 'metadata',
|
|
903
|
-
label: 'metadata',
|
|
904
|
-
type: 'map',
|
|
905
|
-
visibility: 'admin',
|
|
906
|
-
validation: {
|
|
907
|
-
schema: v.object({
|
|
908
|
-
source: v.string(),
|
|
909
|
-
importedAt: v.string(),
|
|
910
|
-
tags: v.array(v.string()),
|
|
911
|
-
})
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
});
|
|
916
|
-
```
|
|
917
|
-
|
|
918
|
-
**How It Works:**
|
|
919
|
-
- `validation.schema` is the **single source of truth** - works everywhere (client + server)
|
|
920
|
-
- Takes priority over built-in type schemas
|
|
921
|
-
- Entity is self-contained - no separate registrations needed
|
|
922
|
-
|
|
923
|
-
---
|
|
924
|
-
|
|
925
|
-
## 8. Hooks API
|
|
926
|
-
|
|
927
|
-
```typescript
|
|
928
|
-
import { useCrud, useCrudList, useCrudCardList } from '@donotdev/crud';
|
|
929
|
-
|
|
930
|
-
// Actions
|
|
931
|
-
const { add, update, delete: remove, get } = useCrud(productEntity);
|
|
932
|
-
|
|
933
|
-
// List (auto-fetch for tables)
|
|
934
|
-
const { items, loading, refresh } = useCrudList(productEntity);
|
|
935
|
-
|
|
936
|
-
// Cards (optimized for public)
|
|
937
|
-
const { items, loading, refresh } = useCrudCardList(productEntity);
|
|
938
|
-
```
|
|
939
|
-
|
|
940
|
-
---
|
|
941
|
-
|
|
942
|
-
## 9. Custom Form Layouts
|
|
943
|
-
|
|
944
|
-
Need a custom layout instead of `EntityFormRenderer`? Use `useEntityForm` directly.
|
|
945
|
-
|
|
946
|
-
```tsx
|
|
947
|
-
import { useId, useEffect } from 'react';
|
|
948
|
-
import { useEntityForm, useCrud, UploadProvider, useController } from '@donotdev/crud';
|
|
949
|
-
import { productEntity } from 'entities';
|
|
950
|
-
|
|
951
|
-
export function CustomProductForm() {
|
|
952
|
-
const formId = useId();
|
|
953
|
-
const { add } = useCrud(productEntity);
|
|
954
|
-
const {
|
|
955
|
-
control,
|
|
956
|
-
handleSubmit,
|
|
957
|
-
fields,
|
|
958
|
-
formStatus, // 'idle' | 'uploading' | 'submitting' | ...
|
|
959
|
-
uploadProgress, // 0-100 during uploads
|
|
960
|
-
cleanup
|
|
961
|
-
} = useEntityForm(productEntity, { formId });
|
|
962
|
-
|
|
963
|
-
useEffect(() => cleanup, [cleanup]);
|
|
964
|
-
|
|
965
|
-
return (
|
|
966
|
-
<UploadProvider formId={formId}>
|
|
967
|
-
<form onSubmit={handleSubmit(add)}>
|
|
968
|
-
{/* Your custom layout here */}
|
|
969
|
-
<Controller control={control} name="name" render={({ field }) => (
|
|
970
|
-
<input {...field} placeholder="Product name" />
|
|
971
|
-
)} />
|
|
972
|
-
|
|
973
|
-
<button type="submit" disabled={formStatus !== 'idle'}>
|
|
974
|
-
{formStatus === 'uploading' ? `Uploading ${uploadProgress}%...` : 'Save'}
|
|
975
|
-
</button>
|
|
976
|
-
</form>
|
|
977
|
-
</UploadProvider>
|
|
978
|
-
);
|
|
979
|
-
}
|
|
980
|
-
```
|
|
981
|
-
|
|
982
|
-
**Key points:**
|
|
983
|
-
- Pass `formId` to get automatic upload handling and loading states
|
|
984
|
-
- Wrap with `UploadProvider` if using file/image fields
|
|
985
|
-
- `handleSubmit` automatically uploads files before validation
|
|
243
|
+
See `lookup_symbol("registerFieldType")`, `lookup_symbol("ControlledFieldProps")`.
|
|
986
244
|
|
|
987
245
|
---
|
|
988
246
|
|
|
989
|
-
##
|
|
990
|
-
|
|
991
|
-
**Namespace:** Automatically set to `entity-${entity.name.toLowerCase()}` (e.g., `entity-product`)
|
|
247
|
+
## i18n
|
|
992
248
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
```typescript
|
|
996
|
-
export const productEntity = defineEntity({
|
|
997
|
-
name: 'Product',
|
|
998
|
-
collection: 'products',
|
|
999
|
-
namespace: 'custom-namespace', // Optional: override default
|
|
1000
|
-
fields: { ... }
|
|
1001
|
-
});
|
|
1002
|
-
```
|
|
1003
|
-
|
|
1004
|
-
**Default behavior:** If not specified, the namespace is automatically set to `entity-${entity.name.toLowerCase()}`.
|
|
1005
|
-
|
|
1006
|
-
**Translation file:** `locales/entity-product_en.json` (or your custom namespace)
|
|
1007
|
-
```json
|
|
1008
|
-
{
|
|
1009
|
-
"fields": {
|
|
1010
|
-
"name": "Product Name",
|
|
1011
|
-
"price": "Price"
|
|
1012
|
-
},
|
|
1013
|
-
"name": "Product"
|
|
1014
|
-
}
|
|
1015
|
-
```
|
|
1016
|
-
|
|
1017
|
-
Field `label` → translation key in `fields.*`
|
|
1018
|
-
|
|
1019
|
-
### Status Field Translations
|
|
1020
|
-
|
|
1021
|
-
The `status` field uses CRUD namespace translations with entity-specific overrides:
|
|
1022
|
-
|
|
1023
|
-
**Translation Fallback Order:**
|
|
1024
|
-
1. **Entity namespace** (`entity-{name}`) - User overrides take priority
|
|
1025
|
-
2. **CRUD namespace** (`crud`) - Framework defaults
|
|
1026
|
-
|
|
1027
|
-
**Framework Defaults** (in `crud` namespace):
|
|
1028
|
-
- `crud:status.draft` → "Draft"
|
|
1029
|
-
- `crud:status.available` → "Available"
|
|
1030
|
-
- `crud:status.deleted` → "Deleted"
|
|
1031
|
-
|
|
1032
|
-
**Override in Entity Translation File:**
|
|
249
|
+
**Namespace:** `entity-${entity.name.toLowerCase()}` (customizable via `namespace`).
|
|
1033
250
|
|
|
1034
251
|
```json
|
|
1035
|
-
|
|
1036
|
-
{
|
|
1037
|
-
"status": {
|
|
1038
|
-
"draft": "New",
|
|
1039
|
-
"available": "Read",
|
|
1040
|
-
"deleted": "Closed"
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
```
|
|
1044
|
-
|
|
1045
|
-
**How it works:**
|
|
1046
|
-
- Framework components (`EntityList`, `EntityFormRenderer`, etc.) automatically use `[entity.namespace, 'crud']` translation namespaces
|
|
1047
|
-
- Status labels are translated via `translateLabel()` which tries entity namespace first, then falls back to `crud`
|
|
1048
|
-
- No `dndev` namespace fallback - CRUD is optional and doesn't pollute core framework translations
|
|
1049
|
-
|
|
1050
|
-
**Example:** For an `inquiry` entity:
|
|
1051
|
-
- If `entity-inquiry_en.json` has `"status.draft": "New"` → displays "New"
|
|
1052
|
-
- If not found → falls back to `crud:status.draft` → displays "Draft"
|
|
1053
|
-
- Never falls back to `dndev` namespace
|
|
1054
|
-
|
|
1055
|
-
---
|
|
1056
|
-
|
|
1057
|
-
## 11. Display Utilities
|
|
1058
|
-
|
|
1059
|
-
For custom displays, use `formatValue` from `@donotdev/crud`:
|
|
1060
|
-
|
|
1061
|
-
```tsx
|
|
1062
|
-
import { formatValue, translateFieldLabel } from '@donotdev/crud';
|
|
1063
|
-
import { useTranslation } from '@donotdev/core';
|
|
1064
|
-
import { productEntity } from 'entities';
|
|
1065
|
-
|
|
1066
|
-
function ProductCard({ item }) {
|
|
1067
|
-
const { t } = useTranslation(productEntity.namespace);
|
|
1068
|
-
|
|
1069
|
-
// Format any field value using entity config
|
|
1070
|
-
const priceDisplay = formatValue(
|
|
1071
|
-
item.price,
|
|
1072
|
-
productEntity.fields.price,
|
|
1073
|
-
t,
|
|
1074
|
-
{ compact: true } // compact mode for lists/cards
|
|
1075
|
-
);
|
|
1076
|
-
|
|
1077
|
-
// Get translated field label
|
|
1078
|
-
const priceLabel = translateFieldLabel('price', productEntity.fields.price, t);
|
|
1079
|
-
|
|
1080
|
-
return (
|
|
1081
|
-
<div>
|
|
1082
|
-
<span>{priceLabel}: {priceDisplay}</span>
|
|
1083
|
-
</div>
|
|
1084
|
-
);
|
|
1085
|
-
}
|
|
252
|
+
{ "fields": { "name": "Product Name", "price": "Price" }, "name": "Product" }
|
|
1086
253
|
```
|
|
1087
254
|
|
|
1088
|
-
|
|
255
|
+
Status labels: entity namespace → `crud` namespace fallback.
|
|
1089
256
|
|
|
1090
257
|
---
|
|
1091
258
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
| Route | Purpose |
|
|
1095
|
-
|-------|---------|
|
|
1096
|
-
| `/${collection}` | List |
|
|
1097
|
-
| `/${collection}/new` | Create |
|
|
1098
|
-
| `/${collection}/:id` | Detail/Edit |
|
|
259
|
+
**Define entity → register provider → register functions → drop components. Entity is the SSOT — everything flows from it.**
|