@donotdev/cli 0.0.16 → 0.0.18

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.
Files changed (87) hide show
  1. package/dependencies-matrix.json +38 -26
  2. package/dist/bin/commands/bump.js +9 -2
  3. package/dist/bin/commands/create-app.js +185 -81
  4. package/dist/bin/commands/create-project.js +186 -85
  5. package/dist/bin/commands/deploy.js +51 -20
  6. package/dist/bin/commands/doctor.js +249 -56
  7. package/dist/bin/commands/emu.js +18 -20
  8. package/dist/bin/commands/make-admin.js +30 -10
  9. package/dist/bin/commands/setup.js +512 -122
  10. package/dist/bin/commands/type-check.d.ts.map +1 -1
  11. package/dist/bin/commands/type-check.js +7 -3
  12. package/dist/bin/commands/type-check.js.map +1 -1
  13. package/dist/bin/dndev.js +9 -6
  14. package/dist/bin/donotdev.js +35 -20
  15. package/dist/index.js +262 -129
  16. package/package.json +1 -1
  17. package/templates/root-consumer/.claude/commands/brainstorm.md.example +15 -1
  18. package/templates/root-consumer/.claude/commands/build.md.example +24 -2
  19. package/templates/root-consumer/.claude/commands/design.md.example +17 -0
  20. package/templates/root-consumer/.claude/commands/polish.md.example +17 -0
  21. package/templates/root-consumer/AI.md.example +50 -18
  22. package/templates/root-consumer/guides/dndev/ENV_SETUP.md.example +6 -6
  23. package/templates/root-consumer/guides/dndev/INDEX.md.example +2 -2
  24. package/templates/root-consumer/guides/dndev/SETUP_AUTH.md.example +13 -6
  25. package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +149 -1086
  26. package/templates/root-consumer/guides/dndev/SETUP_FIREBASE.md.example +68 -16
  27. package/templates/root-consumer/guides/dndev/SETUP_FUNCTIONS.md.example +6 -111
  28. package/templates/root-consumer/guides/dndev/SETUP_PAGES.md.example +64 -0
  29. package/templates/root-consumer/guides/dndev/SETUP_SUPABASE.md.example +123 -32
  30. package/templates/root-consumer/guides/dndev/SETUP_VERCEL.md.example +108 -91
  31. package/templates/root-consumer/guides/dndev/advanced/EMULATORS.md.example +2 -2
  32. package/templates/root-consumer/guides/wai-way/blueprints/0_brainstorm.md.example +1 -1
  33. package/dist/bin/commands/firebase-setup.d.ts +0 -6
  34. package/dist/bin/commands/firebase-setup.d.ts.map +0 -1
  35. package/dist/bin/commands/firebase-setup.js +0 -7
  36. package/dist/bin/commands/firebase-setup.js.map +0 -1
  37. package/dist/bin/commands/supabase-setup.d.ts +0 -6
  38. package/dist/bin/commands/supabase-setup.d.ts.map +0 -1
  39. package/dist/bin/commands/supabase-setup.js +0 -7
  40. package/dist/bin/commands/supabase-setup.js.map +0 -1
  41. package/templates/functions-firebase/functions-firebase/README.md.example +0 -123
  42. package/templates/functions-firebase/functions-firebase/build.mjs.example +0 -5
  43. package/templates/functions-firebase/functions-firebase/src/auth/getCustomClaims.ts.example +0 -19
  44. package/templates/functions-firebase/functions-firebase/src/auth/getUserAuthStatus.ts.example +0 -21
  45. package/templates/functions-firebase/functions-firebase/src/auth/index.ts.example +0 -11
  46. package/templates/functions-firebase/functions-firebase/src/auth/removeCustomClaims.ts.example +0 -21
  47. package/templates/functions-firebase/functions-firebase/src/auth/setCustomClaims.ts.example +0 -21
  48. package/templates/functions-firebase/functions-firebase/src/billing/handleStripeWebhook.ts.example +0 -24
  49. package/templates/functions-firebase/functions-firebase/src/billing/index.ts.example +0 -10
  50. package/templates/functions-firebase/functions-firebase/src/billing/processPaymentSuccess.ts.example +0 -14
  51. package/templates/functions-firebase/functions-firebase/src/billing/refreshSubscriptionStatus.ts.example +0 -14
  52. package/templates/functions-firebase/functions-firebase/src/index.ts.example +0 -39
  53. package/templates/functions-firebase/functions-firebase/src/oauth/checkGitHubAccess.ts.example +0 -14
  54. package/templates/functions-firebase/functions-firebase/src/oauth/disconnect.ts.example +0 -14
  55. package/templates/functions-firebase/functions-firebase/src/oauth/exchangeToken.ts.example +0 -14
  56. package/templates/functions-firebase/functions-firebase/src/oauth/getConnections.ts.example +0 -14
  57. package/templates/functions-firebase/functions-firebase/src/oauth/grantGitHubAccess.ts.example +0 -14
  58. package/templates/functions-firebase/functions-firebase/src/oauth/index.ts.example +0 -17
  59. package/templates/functions-firebase/functions-firebase/src/oauth/refreshToken.ts.example +0 -14
  60. package/templates/functions-firebase/functions-firebase/src/oauth/revokeGitHubAccess.ts.example +0 -14
  61. package/templates/functions-firebase/functions-firebase/tsconfig.json.example +0 -21
  62. package/templates/functions-vercel/functions-vercel/README.md.example +0 -116
  63. package/templates/functions-vercel/functions-vercel/build.mjs.example +0 -52
  64. package/templates/functions-vercel/functions-vercel/src/api/auth/getCustomClaims.ts.example +0 -20
  65. package/templates/functions-vercel/functions-vercel/src/api/auth/getUserAuthStatus.ts.example +0 -20
  66. package/templates/functions-vercel/functions-vercel/src/api/auth/removeCustomClaims.ts.example +0 -20
  67. package/templates/functions-vercel/functions-vercel/src/api/auth/setCustomClaims.ts.example +0 -20
  68. package/templates/functions-vercel/functions-vercel/src/api/billing/handleStripeWebhook.ts.example +0 -20
  69. package/templates/functions-vercel/functions-vercel/src/api/billing/processPaymentSuccess.ts.example +0 -20
  70. package/templates/functions-vercel/functions-vercel/src/api/billing/refreshSubscriptionStatus.ts.example +0 -20
  71. package/templates/functions-vercel/functions-vercel/src/api/crud/createEntity.ts.example +0 -20
  72. package/templates/functions-vercel/functions-vercel/src/api/crud/deleteEntity.ts.example +0 -20
  73. package/templates/functions-vercel/functions-vercel/src/api/crud/getEntity.ts.example +0 -20
  74. package/templates/functions-vercel/functions-vercel/src/api/crud/listEntities.ts.example +0 -20
  75. package/templates/functions-vercel/functions-vercel/src/api/crud/updateEntity.ts.example +0 -20
  76. package/templates/functions-vercel/functions-vercel/src/api/oauth/checkGitHubAccess.ts.example +0 -20
  77. package/templates/functions-vercel/functions-vercel/src/api/oauth/disconnect.ts.example +0 -20
  78. package/templates/functions-vercel/functions-vercel/src/api/oauth/exchangeToken.ts.example +0 -20
  79. package/templates/functions-vercel/functions-vercel/src/api/oauth/getConnections.ts.example +0 -20
  80. package/templates/functions-vercel/functions-vercel/src/api/oauth/grantGitHubAccess.ts.example +0 -20
  81. package/templates/functions-vercel/functions-vercel/src/api/oauth/refreshToken.ts.example +0 -20
  82. package/templates/functions-vercel/functions-vercel/src/api/oauth/revokeGitHubAccess.ts.example +0 -20
  83. package/templates/functions-vercel/functions-vercel/tsconfig.json.example +0 -21
  84. package/templates/functions-vercel/functions-vercel/vercel.json.example +0 -14
  85. package/templates/github/github/workflows/firebase-deploy.yml.example +0 -79
  86. /package/templates/functions-firebase/{functions-firebase/.env.example.example → .env.example} +0 -0
  87. /package/templates/functions-vercel/{functions-vercel/.env.example.example → .env.example} +0 -0
@@ -1,243 +1,52 @@
1
1
  # Setup: CRUD Operations
2
2
 
3
- **CRUD = Create, Read, Update, Delete.** Framework handles data ops with built-in security.
3
+ > **API details:** `lookup_symbol("symbolName")` via MCP for full props, return types, and examples from JSDoc.
4
4
 
5
5
  ---
6
6
 
7
- ## Quick Import Reference
7
+ ## 1. Define Entity (SSOT)
8
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
-
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
- // namespace: 'entity-product', // Optional: defaults to entity-${name.toLowerCase()}
17
+ search: { fields: ['name', 'description'] },
18
+ defaultSort: { field: 'createdAt', direction: 'desc' },
61
19
  fields: {
62
- name: {
63
- name: 'name',
64
- label: 'name',
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
122
-
123
- ```typescript
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.
27
+ **Auto-added fields:** `id`, `createdAt`, `updatedAt`, `createdById`, `updatedById`, `status`.
149
28
 
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
- ## 3. Visibility & Access
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
- import { generateFirestoreRuleCondition } from '@donotdev/core';
214
-
215
- const condition = generateFirestoreRuleCondition(scheduleEntity.ownership);
216
- // Paste into firestore.rules: allow read, update: if <condition>;
217
- ```
218
-
219
- **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.
36
+ // config/providers.ts
37
+ import { configureProviders } from '@donotdev/core';
38
+ import { FirestoreAdapter } from '@donotdev/firebase'; // or SupabaseCrudAdapter
220
39
 
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
- }
40
+ configureProviders({ crud: new FirestoreAdapter() });
229
41
  ```
230
42
 
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. Register Functions (Backend)
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,949 +57,203 @@ initializeApp();
248
57
  export const crud = createCrudFunctions(entities);
249
58
  ```
250
59
 
251
- **Deploy:** `dndev deploy`
252
-
253
- ---
254
-
255
- ## 4. Frontend Usage
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
- export default function ContactPage() {
446
- return (
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
- **What it does:**
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
- **Requirements:**
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
- #### Inquiry Admin Dashboard (Specialized Template)
74
+ ## 4. Use Components
477
75
 
478
- For admin pages to review and respond to inquiries with one-click actions:
76
+ Drop components on a page. Entity drives the UI no configuration needed.
479
77
 
480
78
  ```tsx
481
- import { InquiryAdminTemplate } from '@donotdev/templates';
482
- import { customerEntity, inquiryEntity } from 'entities';
483
-
484
- export default function InquiriesAdminPage() {
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
- **What it does:**
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
515
-
516
- ---
517
-
518
- ## 5. Component Props
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
85
+ Use `lookup_symbol("ComponentName")` for full props.
549
86
 
550
87
  ---
551
88
 
552
- ## 6. Field Types
553
-
554
- ### Text Inputs
555
- - `text` - Single-line text input
556
- - `email` - Email input with validation
557
- - `tel` - Phone number input
558
- - `url` - URL input
559
- - `color` - Color picker
560
- - `password` - Password input (masked)
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.
89
+ ## 5. Visibility & Access
644
90
 
645
- **Templates:** `CarCardListTemplate`, `CarDetailTemplate`, `ProductCardListTemplate` use the price field when present and show a "Sale" (or `price.badge.sale` i18n) badge when discounted.
91
+ ### Field Visibility (who sees what)
646
92
 
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
- ### Select Options
103
+ ### Entity Access (who can CRUD)
650
104
 
651
105
  ```typescript
652
- category: {
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
- ### Year Field
109
+ ### Ownership
664
110
 
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
- ```
111
+ For marketplace patterns. See `lookup_symbol("defineEntity")` `ownership`.
677
112
 
678
- ### Switch with Custom Values
679
-
680
- ```typescript
681
- transmission: {
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
- ## 7. Custom Fields & Schemas
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
- import { registerFieldType, useController } from '@donotdev/crud';
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
- // Entity with schema
744
- export const carEntity = defineEntity({
745
- name: 'Car',
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
- **Important:** Custom controlled components receive `control` prop, NOT `field` prop. You MUST use `useController` to get `field` and `fieldState`. Define schema in `validation.schema` — it's the single source of truth.
129
+ See `lookup_symbol("registerScopeProvider")`.
766
130
 
767
- ### Display (list/card/detail)
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
- ```typescript
772
- // Example: format a custom object for display
773
- displayFormatter: (value, fieldConfig, t, options) => {
774
- if (value == null) return '';
775
- const arr = Array.isArray(value) ? value : [];
776
- if (options?.compact) return `${arr.length} item(s)`;
777
- return arr.map((item: any) => `${item.operation}: ${item.cost}`).join(', ') || '—';
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
- ### Filter (EntityFilters)
197
+ ---
782
198
 
783
- For the field to appear in the list/card **filters** section (EntityFilters), set **filterable: true** and **filterType** in `registerFieldType`. The filter type determines the filter UI:
199
+ ## Built-in Field Types
784
200
 
785
- | filterType | Use for |
786
- |----------------|--------|
787
- | `'text'` | Free text search |
788
- | `'range'` | Numbers, dates (min/max) |
789
- | `'select'` | Single choice (e.g. enum) |
790
- | `'multiselect'`| Multiple choices |
791
- | `'address'` | Address-based filter |
792
- | `'none'` | Not filterable (omit or set filterable: false) |
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
- ```typescript
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
- ### Full example: form + display + filter
214
+ ## Custom Field Types (escape hatch)
805
215
 
806
- One registration with form, displayFormatter, and filter:
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, errors, t }: ControlledFieldProps) {
222
+ function StatusTierField({ fieldConfig, control, t }: ControlledFieldProps) {
815
223
  const { field, fieldState } = useController({ name: fieldConfig.name, control });
816
224
  return (
817
- <div>
818
- <label>{t(fieldConfig.label)}</label>
819
- <select value={field.value ?? ''} onChange={(e) => field.onChange(e.target.value)}>
820
- <option value="">—</option>
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) => (value ? String(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
- ```
851
-
852
- ### Typing custom field options
853
-
854
- To get type-checked `options.fieldSpecific` for custom types (e.g. `extractDistrictCode: boolean`), augment the framework's `CustomFieldOptionsMap` in your app:
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
- ### Reference Select Fields (Entity Lookup)
870
-
871
- A common pattern: a field stores a **reference ID** (e.g. `author_ref`) pointing to another entity (e.g. `Author`). The form needs a select/combobox populated with that entity's records, and list/detail views need to display a human-readable label instead of a raw ID.
872
-
873
- The framework's built-in `reference` type uses `fieldSpecific.displayField` and `fieldSpecific.searchFields`. For **custom** reference types (e.g. `author-select`), follow the same pattern with `fieldSpecific.labelFields`.
874
-
875
- **Step 1: Augment `CustomFieldOptionsMap`** so `labelFields` is typed:
876
-
877
- ```typescript
878
- // entities/crud.d.ts
879
- import '@donotdev/core';
880
- declare module '@donotdev/core' {
881
- interface CustomFieldOptionsMap {
882
- 'author-select': { labelFields: string[] };
883
- }
884
- }
885
- ```
886
-
887
- **Step 2: Declare `labelFields` in the entity definition** (SSOT — never hardcode field names in components):
888
-
889
- ```typescript
890
- // entities/book.ts
891
- import { defineEntity } from '@donotdev/core';
892
-
893
- export const bookEntity = defineEntity({
894
- name: 'Book',
895
- collection: 'books',
896
- fields: {
897
- author_ref: {
898
- name: 'author_ref',
899
- label: 'fields.author',
900
- type: 'author-select',
901
- visibility: 'admin',
902
- validation: { required: false, reference: 'authors' },
903
- options: { fieldSpecific: { labelFields: ['first_name', 'last_name'] } },
904
- },
905
- // ...
906
- },
907
- });
908
239
  ```
909
240
 
910
- **Step 3: Build labels from `labelFields`** in your shared formatters:
241
+ Then use in entity: `tier: { name: 'tier', label: 'tier', type: 'statusTier', visibility: 'guest' }`
911
242
 
912
- ```typescript
913
- // entities/formatters.ts
914
- /**
915
- * Build a display label from a record using labelFields from entity field config.
916
- * Handles Supabase fieldMapper camelCase conversion: tries camelCase first, then snake_case.
917
- */
918
- export function buildRecordLabel(
919
- record: Record<string, unknown>,
920
- labelFields: string[],
921
- ): string {
922
- return labelFields
923
- .map((field) => {
924
- const camel = field.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
925
- return record[camel] ?? record[field] ?? '';
926
- })
927
- .join(' ')
928
- .trim();
929
- }
930
- ```
931
-
932
- **Step 4: Read `labelFields` in your custom component** (never hardcode field names):
933
-
934
- ```typescript
935
- // components/fields/AuthorSelectField.tsx
936
- import { buildRecordLabel } from 'entities/formatters';
937
-
938
- // Inside the component:
939
- const labelFields: string[] =
940
- fieldConfig.options?.fieldSpecific?.labelFields ?? [];
941
-
942
- const options = useMemo(() => {
943
- return items.map((item) => ({
944
- value: String(item.id ?? ''),
945
- label: buildRecordLabel(item, labelFields),
946
- }));
947
- }, [items, labelFields]);
948
- ```
949
-
950
- **Step 5: Use the same utility for `displayFormatter`** so list/detail views resolve IDs to labels:
951
-
952
- ```typescript
953
- // In registerFieldType or in your display formatter
954
- displayFormatter: (value, config) => {
955
- if (value == null) return '';
956
- const options = config?.validation?.options;
957
- if (Array.isArray(options)) {
958
- const match = options.find((opt: any) => opt.value === value);
959
- if (match?.label) return String(match.label);
960
- }
961
- return String(value);
962
- }
963
- ```
964
-
965
- > **Key principle:** Entity field definitions are SSOT. Components read `fieldConfig.options.fieldSpecific.labelFields` — they never hardcode which fields to display. This means changing the label format (e.g. adding `company_name`) only requires editing the entity definition, not every component.
966
-
967
- ### Custom Schemas (No Custom UI)
968
-
969
- For custom validation without custom UI, just define the schema:
970
-
971
- ```typescript
972
- import { defineEntity } from '@donotdev/core';
973
- import * as v from 'valibot';
974
-
975
- export const carEntity = defineEntity({
976
- name: 'Car',
977
- collection: 'cars',
978
- fields: {
979
- // Custom array field with inline schema
980
- repairs: {
981
- name: 'repairs',
982
- label: 'repairs',
983
- type: 'text', // Can be any type, schema takes precedence
984
- visibility: 'admin',
985
- validation: {
986
- required: false,
987
- // Custom Valibot schema - takes priority over type-based schema
988
- schema: v.array(
989
- v.object({
990
- operation: v.string(),
991
- cost: v.number(),
992
- date: v.string(), // ISO date string
993
- })
994
- )
995
- }
996
- },
997
-
998
- // Custom object field
999
- metadata: {
1000
- name: 'metadata',
1001
- label: 'metadata',
1002
- type: 'map',
1003
- visibility: 'admin',
1004
- validation: {
1005
- schema: v.object({
1006
- source: v.string(),
1007
- importedAt: v.string(),
1008
- tags: v.array(v.string()),
1009
- })
1010
- }
1011
- }
1012
- }
1013
- });
1014
- ```
1015
-
1016
- **How It Works:**
1017
- - `validation.schema` is the **single source of truth** - works everywhere (client + server)
1018
- - Takes priority over built-in type schemas
1019
- - Entity is self-contained - no separate registrations needed
1020
-
1021
- ---
1022
-
1023
- ## 8. Hooks API
1024
-
1025
- ```typescript
1026
- import { useCrud, useCrudList, useCrudCardList } from '@donotdev/crud';
1027
-
1028
- // Actions
1029
- const { add, update, delete: remove, get } = useCrud(productEntity);
1030
-
1031
- // List (auto-fetch for tables)
1032
- const { items, loading, refresh } = useCrudList(productEntity);
1033
-
1034
- // Cards (optimized for public)
1035
- const { items, loading, refresh } = useCrudCardList(productEntity);
1036
- ```
1037
-
1038
- ---
1039
-
1040
- ## 9. Custom Form Layouts
1041
-
1042
- Need a custom layout instead of `EntityFormRenderer`? Use `useEntityForm` directly.
1043
-
1044
- ```tsx
1045
- import { useId, useEffect } from 'react';
1046
- import { useEntityForm, useCrud, UploadProvider, useController } from '@donotdev/crud';
1047
- import { productEntity } from 'entities';
1048
-
1049
- export function CustomProductForm() {
1050
- const formId = useId();
1051
- const { add } = useCrud(productEntity);
1052
- const {
1053
- control,
1054
- handleSubmit,
1055
- fields,
1056
- formStatus, // 'idle' | 'uploading' | 'submitting' | ...
1057
- uploadProgress, // 0-100 during uploads
1058
- cleanup
1059
- } = useEntityForm(productEntity, { formId });
1060
-
1061
- useEffect(() => cleanup, [cleanup]);
1062
-
1063
- return (
1064
- <UploadProvider formId={formId}>
1065
- <form onSubmit={handleSubmit(add)}>
1066
- {/* Your custom layout here */}
1067
- <Controller control={control} name="name" render={({ field }) => (
1068
- <input {...field} placeholder="Product name" />
1069
- )} />
1070
-
1071
- <button type="submit" disabled={formStatus !== 'idle'}>
1072
- {formStatus === 'uploading' ? `Uploading ${uploadProgress}%...` : 'Save'}
1073
- </button>
1074
- </form>
1075
- </UploadProvider>
1076
- );
1077
- }
1078
- ```
1079
-
1080
- **Key points:**
1081
- - Pass `formId` to get automatic upload handling and loading states
1082
- - Wrap with `UploadProvider` if using file/image fields
1083
- - `handleSubmit` automatically uploads files before validation
243
+ See `lookup_symbol("registerFieldType")`, `lookup_symbol("ControlledFieldProps")`.
1084
244
 
1085
245
  ---
1086
246
 
1087
- ## 10. i18n
1088
-
1089
- **Namespace:** Automatically set to `entity-${entity.name.toLowerCase()}` (e.g., `entity-product`)
1090
-
1091
- You can customize it by setting `namespace` in your entity definition:
1092
-
1093
- ```typescript
1094
- export const productEntity = defineEntity({
1095
- name: 'Product',
1096
- collection: 'products',
1097
- namespace: 'custom-namespace', // Optional: override default
1098
- fields: { ... }
1099
- });
1100
- ```
1101
-
1102
- **Default behavior:** If not specified, the namespace is automatically set to `entity-${entity.name.toLowerCase()}`.
1103
-
1104
- **Translation file:** `locales/entity-product_en.json` (or your custom namespace)
1105
- ```json
1106
- {
1107
- "fields": {
1108
- "name": "Product Name",
1109
- "price": "Price"
1110
- },
1111
- "name": "Product"
1112
- }
1113
- ```
1114
-
1115
- Field `label` → translation key in `fields.*`
1116
-
1117
- ### Status Field Translations
1118
-
1119
- The `status` field uses CRUD namespace translations with entity-specific overrides:
247
+ ## i18n
1120
248
 
1121
- **Translation Fallback Order:**
1122
- 1. **Entity namespace** (`entity-{name}`) - User overrides take priority
1123
- 2. **CRUD namespace** (`crud`) - Framework defaults
1124
-
1125
- **Framework Defaults** (in `crud` namespace):
1126
- - `crud:status.draft` → "Draft"
1127
- - `crud:status.available` → "Available"
1128
- - `crud:status.deleted` → "Deleted"
1129
-
1130
- **Override in Entity Translation File:**
249
+ **Namespace:** `entity-${entity.name.toLowerCase()}` (customizable via `namespace`).
1131
250
 
1132
251
  ```json
1133
- // locales/entity-inquiry_en.json
1134
- {
1135
- "status": {
1136
- "draft": "New",
1137
- "available": "Read",
1138
- "deleted": "Closed"
1139
- }
1140
- }
1141
- ```
1142
-
1143
- **How it works:**
1144
- - Framework components (`EntityList`, `EntityFormRenderer`, etc.) automatically use `[entity.namespace, 'crud']` translation namespaces
1145
- - Status labels are translated via `translateLabel()` which tries entity namespace first, then falls back to `crud`
1146
- - No `dndev` namespace fallback - CRUD is optional and doesn't pollute core framework translations
1147
-
1148
- **Example:** For an `inquiry` entity:
1149
- - If `entity-inquiry_en.json` has `"status.draft": "New"` → displays "New"
1150
- - If not found → falls back to `crud:status.draft` → displays "Draft"
1151
- - Never falls back to `dndev` namespace
1152
-
1153
- ---
1154
-
1155
- ## 11. Display Utilities
1156
-
1157
- For custom displays, use `formatValue` from `@donotdev/crud`:
1158
-
1159
- ```tsx
1160
- import { formatValue, translateFieldLabel } from '@donotdev/crud';
1161
- import { useTranslation } from '@donotdev/core';
1162
- import { productEntity } from 'entities';
1163
-
1164
- function ProductCard({ item }) {
1165
- const { t } = useTranslation(productEntity.namespace);
1166
-
1167
- // Format any field value using entity config
1168
- const priceDisplay = formatValue(
1169
- item.price,
1170
- productEntity.fields.price,
1171
- t,
1172
- { compact: true } // compact mode for lists/cards
1173
- );
1174
-
1175
- // Get translated field label
1176
- const priceLabel = translateFieldLabel('price', productEntity.fields.price, t);
1177
-
1178
- return (
1179
- <div>
1180
- <span>{priceLabel}: {priceDisplay}</span>
1181
- </div>
1182
- );
1183
- }
252
+ { "fields": { "name": "Product Name", "price": "Price" }, "name": "Product" }
1184
253
  ```
1185
254
 
1186
- **`formatValue` handles all field types:** dates, numbers, selects (translates option labels), images, files, etc.
255
+ Status labels: entity namespace `crud` namespace fallback.
1187
256
 
1188
257
  ---
1189
258
 
1190
- ## 12. Routing Convention
1191
-
1192
- | Route | Purpose |
1193
- |-------|---------|
1194
- | `/${collection}` | List |
1195
- | `/${collection}/new` | Create |
1196
- | `/${collection}/:id` | Detail/Edit |
259
+ **Define entity → register provider → register functions → drop components. Entity is the SSOT — everything flows from it.**