@donotdev/cli 0.0.8 → 0.0.9

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