@hed-hog/catalog 0.0.296 → 0.0.298

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.
@@ -16,8 +16,8 @@ import {
16
16
  SearchBar,
17
17
  StatsCards,
18
18
  } from '@/components/entity-list';
19
- import {
20
- AlertDialog,
19
+ import {
20
+ AlertDialog,
21
21
  AlertDialogAction,
22
22
  AlertDialogCancel,
23
23
  AlertDialogContent,
@@ -25,19 +25,19 @@ import {
25
25
  AlertDialogFooter,
26
26
  AlertDialogHeader,
27
27
  AlertDialogTitle,
28
- } from '@/components/ui/alert-dialog';
29
- import { Badge } from '@/components/ui/badge';
30
- import { Button } from '@/components/ui/button';
31
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
32
- import {
33
- Dialog,
34
- DialogContent,
35
- DialogDescription,
36
- DialogHeader,
37
- DialogTitle,
38
- } from '@/components/ui/dialog';
39
- import {
40
- Table,
28
+ } from '@/components/ui/alert-dialog';
29
+ import { Badge } from '@/components/ui/badge';
30
+ import { Button } from '@/components/ui/button';
31
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
32
+ import {
33
+ Dialog,
34
+ DialogContent,
35
+ DialogDescription,
36
+ DialogHeader,
37
+ DialogTitle,
38
+ } from '@/components/ui/dialog';
39
+ import {
40
+ Table,
41
41
  TableBody,
42
42
  TableCell,
43
43
  TableHead,
@@ -65,327 +65,327 @@ type ResourceStats = {
65
65
  active?: number;
66
66
  };
67
67
 
68
- type CatalogRecord = Record<string, unknown>;
69
- type ProductAttributePayload = {
70
- groups?: Array<{
71
- id: number | null;
72
- name: string;
73
- slug: string;
74
- attributes: Array<
75
- CatalogRecord & {
76
- options?: CatalogRecord[];
77
- value?: CatalogRecord | null;
78
- category_attribute?: CatalogRecord;
79
- }
80
- >;
81
- }>;
82
- };
83
-
84
- function humanizeValue(value: string) {
68
+ type CatalogRecord = Record<string, unknown>;
69
+ type ProductAttributePayload = {
70
+ groups?: Array<{
71
+ id: number | null;
72
+ name: string;
73
+ slug: string;
74
+ attributes: Array<
75
+ CatalogRecord & {
76
+ options?: CatalogRecord[];
77
+ value?: CatalogRecord | null;
78
+ category_attribute?: CatalogRecord;
79
+ }
80
+ >;
81
+ }>;
82
+ };
83
+
84
+ function humanizeValue(value: string) {
85
85
  return value
86
86
  .replace(/[_-]+/g, ' ')
87
- .replace(/\b\w/g, (char) => char.toUpperCase());
88
- }
89
-
90
- function CatalogProductPreviewDialog({
91
- open,
92
- onOpenChange,
93
- productId,
94
- }: {
95
- open: boolean;
96
- onOpenChange: (open: boolean) => void;
97
- productId: number | null;
98
- }) {
99
- const { request, currentLocaleCode } = useApp();
100
- const t = useTranslations('catalog');
101
- const isPt = currentLocaleCode?.startsWith('pt');
102
- const { data: product, isLoading: isLoadingProduct } = useQuery<CatalogRecord | null>({
103
- queryKey: ['catalog-product-preview', productId],
104
- queryFn: async () => {
105
- if (!productId) {
106
- return null;
107
- }
108
-
109
- const response = await request({
110
- url: `/catalog/products/${productId}`,
111
- });
112
-
113
- return (response.data ?? null) as CatalogRecord | null;
114
- },
115
- enabled: open && Boolean(productId),
116
- });
117
- const { data: attributes, isLoading: isLoadingAttributes } =
118
- useQuery<ProductAttributePayload | null>({
119
- queryKey: ['catalog-product-preview-attributes', productId],
120
- queryFn: async () => {
121
- if (!productId) {
122
- return null;
123
- }
124
-
125
- const response = await request({
126
- url: `/catalog/products/${productId}/attributes`,
127
- });
128
-
129
- return (response.data ?? null) as ProductAttributePayload | null;
130
- },
131
- enabled: open && Boolean(productId),
132
- });
133
-
134
- const formatAttributeValue = (attribute: CatalogRecord & {
135
- options?: CatalogRecord[];
136
- value?: CatalogRecord | null;
137
- }) => {
138
- const value = (attribute.value ?? null) as CatalogRecord | null;
139
-
140
- if (!value) {
141
- return '-';
142
- }
143
-
144
- const dataType = String(attribute.data_type ?? 'text');
145
-
146
- if (dataType === 'option') {
147
- const option = (attribute.options ?? []).find(
148
- (item) => Number(item.id ?? 0) === Number(value.attribute_option_id ?? 0),
149
- );
150
- return String(option?.label ?? option?.option_value ?? value.value_text ?? '-');
151
- }
152
-
153
- if (dataType === 'number') {
154
- const numberValue =
155
- value.value_number === null || value.value_number === undefined
156
- ? null
157
- : Number(value.value_number);
158
-
159
- if (numberValue === null) {
160
- return '-';
161
- }
162
-
163
- const unit = String(value.value_unit ?? attribute.unit ?? '').trim();
164
- return unit ? `${numberValue} ${unit}` : String(numberValue);
165
- }
166
-
167
- if (dataType === 'boolean') {
168
- if (value.value_boolean === null || value.value_boolean === undefined) {
169
- return '-';
170
- }
171
-
172
- return value.value_boolean
173
- ? t('resource.booleans.true')
174
- : t('resource.booleans.false');
175
- }
176
-
177
- return String(value.value_text ?? '-');
178
- };
179
-
180
- const renderJsonBlock = (value: unknown) => {
181
- if (!value || (typeof value === 'object' && Object.keys(value as object).length === 0)) {
182
- return null;
183
- }
184
-
185
- return (
186
- <pre className="overflow-x-auto rounded-md border bg-muted/30 p-3 text-xs">
187
- {JSON.stringify(value, null, 2)}
188
- </pre>
189
- );
190
- };
191
-
192
- const specSnapshotBlock = renderJsonBlock(product?.spec_snapshot_json);
193
- const comparisonSnapshotBlock = renderJsonBlock(product?.comparison_snapshot_json);
194
-
195
- return (
196
- <Dialog open={open} onOpenChange={onOpenChange}>
197
- <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-4xl">
198
- <DialogHeader>
199
- <DialogTitle>
200
- {String(product?.name ?? (isPt ? 'Visualizar produto' : 'View product'))}
201
- </DialogTitle>
202
- <DialogDescription>
203
- {isPt
204
- ? 'Dados básicos, atributos técnicos por grupo, imagens e snapshots secundários.'
205
- : 'Basic data, technical attributes by group, images, and secondary snapshots.'}
206
- </DialogDescription>
207
- </DialogHeader>
208
-
209
- {isLoadingProduct || isLoadingAttributes ? (
210
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
211
- <Eye className="size-4" />
212
- {isPt ? 'Carregando visualização do produto...' : 'Loading product preview...'}
213
- </div>
214
- ) : !product ? (
215
- <p className="text-sm text-muted-foreground">
216
- {isPt ? 'Produto não encontrado.' : 'Product not found.'}
217
- </p>
218
- ) : (
219
- <div className="space-y-6">
220
- <Card>
221
- <CardHeader>
222
- <CardTitle>{isPt ? 'Dados básicos do produto' : 'Product basics'}</CardTitle>
223
- </CardHeader>
224
- <CardContent className="grid gap-4 sm:grid-cols-2">
225
- <div>
226
- <div className="text-xs text-muted-foreground">
227
- {t('resource.fields.brandId')}
228
- </div>
229
- <div className="font-medium">
230
- {String(product.brand_name ?? product.brand_id ?? '-')}
231
- </div>
232
- </div>
233
- <div>
234
- <div className="text-xs text-muted-foreground">
235
- {t('resource.fields.catalogCategoryId')}
236
- </div>
237
- <div className="font-medium">
238
- {String(product.category_name ?? product.catalog_category_id ?? '-')}
239
- </div>
240
- </div>
241
- <div>
242
- <div className="text-xs text-muted-foreground">{t('resource.fields.status')}</div>
243
- <div className="font-medium">
244
- {String(product.status ?? '-')}
245
- </div>
246
- </div>
247
- <div>
248
- <div className="text-xs text-muted-foreground">{t('resource.fields.isActive')}</div>
249
- <div className="font-medium">
250
- {product.is_active === true
251
- ? t('resource.booleans.true')
252
- : t('resource.booleans.false')}
253
- </div>
254
- </div>
255
- <div>
256
- <div className="text-xs text-muted-foreground">{t('resource.fields.sku')}</div>
257
- <div className="font-medium">{String(product.sku ?? '-')}</div>
258
- </div>
259
- <div>
260
- <div className="text-xs text-muted-foreground">GTIN</div>
261
- <div className="font-medium">{String(product.gtin ?? '-')}</div>
262
- </div>
263
- </CardContent>
264
- </Card>
265
-
266
- <Card>
267
- <CardHeader>
268
- <CardTitle>{isPt ? 'Atributos técnicos' : 'Technical attributes'}</CardTitle>
269
- <CardDescription>
270
- {isPt
271
- ? 'Organizados pelos grupos configurados na categoria do produto.'
272
- : 'Organized by the groups configured for the product category.'}
273
- </CardDescription>
274
- </CardHeader>
275
- <CardContent className="space-y-6">
276
- {!(attributes?.groups?.length) ? (
277
- <p className="text-sm text-muted-foreground">
278
- {isPt
279
- ? 'Nenhum atributo técnico configurado para este produto.'
280
- : 'No technical attributes configured for this product.'}
281
- </p>
282
- ) : (
283
- attributes.groups.map((group) => (
284
- <div key={`${group.slug}-${group.id ?? 'general'}`} className="space-y-3">
285
- <div className="text-sm font-semibold">{group.name}</div>
286
- <div className="grid gap-3 sm:grid-cols-2">
287
- {group.attributes.map((attribute) => {
288
- const relation = (attribute.category_attribute ?? {}) as CatalogRecord;
289
- const badges = [
290
- relation.is_highlight
291
- ? isPt
292
- ? 'Destaque'
293
- : 'Highlight'
294
- : null,
295
- relation.is_filter_visible
296
- ? isPt
297
- ? 'Filtro'
298
- : 'Filter'
299
- : null,
300
- relation.is_comparison_visible
301
- ? isPt
302
- ? 'Comparação'
303
- : 'Comparison'
304
- : null,
305
- ].filter((item): item is string => Boolean(item));
306
-
307
- return (
308
- <div key={String(attribute.id)} className="rounded-lg border p-3">
309
- <div className="flex items-start justify-between gap-3">
310
- <div className="space-y-1">
311
- <div className="font-medium">
312
- {String(attribute.name ?? attribute.label ?? attribute.slug)}
313
- </div>
314
- <div className="text-sm text-muted-foreground">
315
- {formatAttributeValue(attribute)}
316
- </div>
317
- </div>
318
- {badges.length ? (
319
- <div className="flex flex-wrap justify-end gap-2">
320
- {badges.map((badge) => (
321
- <Badge key={`${attribute.id}-${badge}`} variant="secondary">
322
- {badge}
323
- </Badge>
324
- ))}
325
- </div>
326
- ) : null}
327
- </div>
328
- </div>
329
- );
330
- })}
331
- </div>
332
- </div>
333
- ))
334
- )}
335
- </CardContent>
336
- </Card>
337
-
338
- {Array.isArray(product.catalog_product_image) &&
339
- product.catalog_product_image.length > 0 ? (
340
- <Card>
341
- <CardHeader>
342
- <CardTitle>{isPt ? 'Imagens do produto' : 'Product images'}</CardTitle>
343
- </CardHeader>
344
- <CardContent className="grid gap-3 sm:grid-cols-2">
345
- {product.catalog_product_image.map((image) => (
346
- <div
347
- key={String(image.id)}
348
- className="rounded-lg border p-3 text-sm"
349
- >
350
- <div className="font-medium">
351
- #{String(image.id)} · {String(image.role ?? 'gallery')}
352
- </div>
353
- <div className="text-muted-foreground">
354
- File #{String(image.file_id ?? '-')}
355
- </div>
356
- </div>
357
- ))}
358
- </CardContent>
359
- </Card>
360
- ) : null}
361
-
362
- <Card>
363
- <CardHeader>
364
- <CardTitle>{isPt ? 'Snapshots secundários' : 'Secondary snapshots'}</CardTitle>
365
- <CardDescription>
366
- {isPt
367
- ? 'Mantidos como apoio para cache e leituras legadas.'
368
- : 'Kept as support for cache and legacy reads.'}
369
- </CardDescription>
370
- </CardHeader>
371
- <CardContent className="space-y-4">
372
- {specSnapshotBlock}
373
- {comparisonSnapshotBlock}
374
- {!specSnapshotBlock && !comparisonSnapshotBlock ? (
375
- <p className="text-sm text-muted-foreground">
376
- {isPt
377
- ? 'Nenhum snapshot secundário disponível.'
378
- : 'No secondary snapshots available.'}
379
- </p>
380
- ) : null}
381
- </CardContent>
382
- </Card>
383
- </div>
384
- )}
385
- </DialogContent>
386
- </Dialog>
387
- );
388
- }
87
+ .replace(/\b\w/g, (char) => char.toUpperCase());
88
+ }
89
+
90
+ function CatalogProductPreviewDialog({
91
+ open,
92
+ onOpenChange,
93
+ productId,
94
+ }: {
95
+ open: boolean;
96
+ onOpenChange: (open: boolean) => void;
97
+ productId: number | null;
98
+ }) {
99
+ const { request, currentLocaleCode } = useApp();
100
+ const t = useTranslations('catalog');
101
+ const isPt = currentLocaleCode?.startsWith('pt');
102
+ const { data: product, isLoading: isLoadingProduct } = useQuery<CatalogRecord | null>({
103
+ queryKey: ['catalog-product-preview', productId],
104
+ queryFn: async () => {
105
+ if (!productId) {
106
+ return null;
107
+ }
108
+
109
+ const response = await request({
110
+ url: `/catalog/products/${productId}`,
111
+ });
112
+
113
+ return (response.data ?? null) as CatalogRecord | null;
114
+ },
115
+ enabled: open && Boolean(productId),
116
+ });
117
+ const { data: attributes, isLoading: isLoadingAttributes } =
118
+ useQuery<ProductAttributePayload | null>({
119
+ queryKey: ['catalog-product-preview-attributes', productId],
120
+ queryFn: async () => {
121
+ if (!productId) {
122
+ return null;
123
+ }
124
+
125
+ const response = await request({
126
+ url: `/catalog/products/${productId}/attributes`,
127
+ });
128
+
129
+ return (response.data ?? null) as ProductAttributePayload | null;
130
+ },
131
+ enabled: open && Boolean(productId),
132
+ });
133
+
134
+ const formatAttributeValue = (attribute: CatalogRecord & {
135
+ options?: CatalogRecord[];
136
+ value?: CatalogRecord | null;
137
+ }) => {
138
+ const value = (attribute.value ?? null) as CatalogRecord | null;
139
+
140
+ if (!value) {
141
+ return '-';
142
+ }
143
+
144
+ const dataType = String(attribute.data_type ?? 'text');
145
+
146
+ if (dataType === 'option') {
147
+ const option = (attribute.options ?? []).find(
148
+ (item) => Number(item.id ?? 0) === Number(value.attribute_option_id ?? 0),
149
+ );
150
+ return String(option?.label ?? option?.option_value ?? value.value_text ?? '-');
151
+ }
152
+
153
+ if (dataType === 'number') {
154
+ const numberValue =
155
+ value.value_number === null || value.value_number === undefined
156
+ ? null
157
+ : Number(value.value_number);
158
+
159
+ if (numberValue === null) {
160
+ return '-';
161
+ }
162
+
163
+ const unit = String(value.value_unit ?? attribute.unit ?? '').trim();
164
+ return unit ? `${numberValue} ${unit}` : String(numberValue);
165
+ }
166
+
167
+ if (dataType === 'boolean') {
168
+ if (value.value_boolean === null || value.value_boolean === undefined) {
169
+ return '-';
170
+ }
171
+
172
+ return value.value_boolean
173
+ ? t('resource.booleans.true')
174
+ : t('resource.booleans.false');
175
+ }
176
+
177
+ return String(value.value_text ?? '-');
178
+ };
179
+
180
+ const renderJsonBlock = (value: unknown) => {
181
+ if (!value || (typeof value === 'object' && Object.keys(value as object).length === 0)) {
182
+ return null;
183
+ }
184
+
185
+ return (
186
+ <pre className="overflow-x-auto rounded-md border bg-muted/30 p-3 text-xs">
187
+ {JSON.stringify(value, null, 2)}
188
+ </pre>
189
+ );
190
+ };
191
+
192
+ const specSnapshotBlock = renderJsonBlock(product?.spec_snapshot_json);
193
+ const comparisonSnapshotBlock = renderJsonBlock(product?.comparison_snapshot_json);
194
+
195
+ return (
196
+ <Dialog open={open} onOpenChange={onOpenChange}>
197
+ <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-4xl">
198
+ <DialogHeader>
199
+ <DialogTitle>
200
+ {String(product?.name ?? (isPt ? 'Visualizar produto' : 'View product'))}
201
+ </DialogTitle>
202
+ <DialogDescription>
203
+ {isPt
204
+ ? 'Dados básicos, atributos técnicos por grupo, imagens e snapshots secundários.'
205
+ : 'Basic data, technical attributes by group, images, and secondary snapshots.'}
206
+ </DialogDescription>
207
+ </DialogHeader>
208
+
209
+ {isLoadingProduct || isLoadingAttributes ? (
210
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
211
+ <Eye className="size-4" />
212
+ {isPt ? 'Carregando visualização do produto...' : 'Loading product preview...'}
213
+ </div>
214
+ ) : !product ? (
215
+ <p className="text-sm text-muted-foreground">
216
+ {isPt ? 'Produto não encontrado.' : 'Product not found.'}
217
+ </p>
218
+ ) : (
219
+ <div className="space-y-6">
220
+ <Card>
221
+ <CardHeader>
222
+ <CardTitle>{isPt ? 'Dados básicos do produto' : 'Product basics'}</CardTitle>
223
+ </CardHeader>
224
+ <CardContent className="grid gap-4 sm:grid-cols-2">
225
+ <div>
226
+ <div className="text-xs text-muted-foreground">
227
+ {t('resource.fields.brandId')}
228
+ </div>
229
+ <div className="font-medium">
230
+ {String(product.brand_name ?? product.brand_id ?? '-')}
231
+ </div>
232
+ </div>
233
+ <div>
234
+ <div className="text-xs text-muted-foreground">
235
+ {t('resource.fields.catalogCategoryId')}
236
+ </div>
237
+ <div className="font-medium">
238
+ {String(product.category_name ?? product.catalog_category_id ?? '-')}
239
+ </div>
240
+ </div>
241
+ <div>
242
+ <div className="text-xs text-muted-foreground">{t('resource.fields.status')}</div>
243
+ <div className="font-medium">
244
+ {String(product.status ?? '-')}
245
+ </div>
246
+ </div>
247
+ <div>
248
+ <div className="text-xs text-muted-foreground">{t('resource.fields.isActive')}</div>
249
+ <div className="font-medium">
250
+ {product.is_active === true
251
+ ? t('resource.booleans.true')
252
+ : t('resource.booleans.false')}
253
+ </div>
254
+ </div>
255
+ <div>
256
+ <div className="text-xs text-muted-foreground">{t('resource.fields.sku')}</div>
257
+ <div className="font-medium">{String(product.sku ?? '-')}</div>
258
+ </div>
259
+ <div>
260
+ <div className="text-xs text-muted-foreground">GTIN</div>
261
+ <div className="font-medium">{String(product.gtin ?? '-')}</div>
262
+ </div>
263
+ </CardContent>
264
+ </Card>
265
+
266
+ <Card>
267
+ <CardHeader>
268
+ <CardTitle>{isPt ? 'Atributos técnicos' : 'Technical attributes'}</CardTitle>
269
+ <CardDescription>
270
+ {isPt
271
+ ? 'Organizados pelos grupos configurados na categoria do produto.'
272
+ : 'Organized by the groups configured for the product category.'}
273
+ </CardDescription>
274
+ </CardHeader>
275
+ <CardContent className="space-y-6">
276
+ {!(attributes?.groups?.length) ? (
277
+ <p className="text-sm text-muted-foreground">
278
+ {isPt
279
+ ? 'Nenhum atributo técnico configurado para este produto.'
280
+ : 'No technical attributes configured for this product.'}
281
+ </p>
282
+ ) : (
283
+ attributes.groups.map((group) => (
284
+ <div key={`${group.slug}-${group.id ?? 'general'}`} className="space-y-3">
285
+ <div className="text-sm font-semibold">{group.name}</div>
286
+ <div className="grid gap-3 sm:grid-cols-2">
287
+ {group.attributes.map((attribute) => {
288
+ const relation = (attribute.category_attribute ?? {}) as CatalogRecord;
289
+ const badges = [
290
+ relation.is_highlight
291
+ ? isPt
292
+ ? 'Destaque'
293
+ : 'Highlight'
294
+ : null,
295
+ relation.is_filter_visible
296
+ ? isPt
297
+ ? 'Filtro'
298
+ : 'Filter'
299
+ : null,
300
+ relation.is_comparison_visible
301
+ ? isPt
302
+ ? 'Comparação'
303
+ : 'Comparison'
304
+ : null,
305
+ ].filter((item): item is string => Boolean(item));
306
+
307
+ return (
308
+ <div key={String(attribute.id)} className="rounded-lg border p-3">
309
+ <div className="flex items-start justify-between gap-3">
310
+ <div className="space-y-1">
311
+ <div className="font-medium">
312
+ {String(attribute.name ?? attribute.label ?? attribute.slug)}
313
+ </div>
314
+ <div className="text-sm text-muted-foreground">
315
+ {formatAttributeValue(attribute)}
316
+ </div>
317
+ </div>
318
+ {badges.length ? (
319
+ <div className="flex flex-wrap justify-end gap-2">
320
+ {badges.map((badge) => (
321
+ <Badge key={`${attribute.id}-${badge}`} variant="secondary">
322
+ {badge}
323
+ </Badge>
324
+ ))}
325
+ </div>
326
+ ) : null}
327
+ </div>
328
+ </div>
329
+ );
330
+ })}
331
+ </div>
332
+ </div>
333
+ ))
334
+ )}
335
+ </CardContent>
336
+ </Card>
337
+
338
+ {Array.isArray(product.catalog_product_image) &&
339
+ product.catalog_product_image.length > 0 ? (
340
+ <Card>
341
+ <CardHeader>
342
+ <CardTitle>{isPt ? 'Imagens do produto' : 'Product images'}</CardTitle>
343
+ </CardHeader>
344
+ <CardContent className="grid gap-3 sm:grid-cols-2">
345
+ {product.catalog_product_image.map((image) => (
346
+ <div
347
+ key={String(image.id)}
348
+ className="rounded-lg border p-3 text-sm"
349
+ >
350
+ <div className="font-medium">
351
+ #{String(image.id)} · {String(image.role ?? 'gallery')}
352
+ </div>
353
+ <div className="text-muted-foreground">
354
+ File #{String(image.file_id ?? '-')}
355
+ </div>
356
+ </div>
357
+ ))}
358
+ </CardContent>
359
+ </Card>
360
+ ) : null}
361
+
362
+ <Card>
363
+ <CardHeader>
364
+ <CardTitle>{isPt ? 'Snapshots secundários' : 'Secondary snapshots'}</CardTitle>
365
+ <CardDescription>
366
+ {isPt
367
+ ? 'Mantidos como apoio para cache e leituras legadas.'
368
+ : 'Kept as support for cache and legacy reads.'}
369
+ </CardDescription>
370
+ </CardHeader>
371
+ <CardContent className="space-y-4">
372
+ {specSnapshotBlock}
373
+ {comparisonSnapshotBlock}
374
+ {!specSnapshotBlock && !comparisonSnapshotBlock ? (
375
+ <p className="text-sm text-muted-foreground">
376
+ {isPt
377
+ ? 'Nenhum snapshot secundário disponível.'
378
+ : 'No secondary snapshots available.'}
379
+ </p>
380
+ ) : null}
381
+ </CardContent>
382
+ </Card>
383
+ </div>
384
+ )}
385
+ </DialogContent>
386
+ </Dialog>
387
+ );
388
+ }
389
389
 
390
390
  export default function CatalogResourcePage() {
391
391
  const params = useParams<{ resource: string }>();
@@ -398,10 +398,10 @@ export default function CatalogResourcePage() {
398
398
  const [filterValue, setFilterValue] = useState('all');
399
399
  const [page, setPage] = useState(1);
400
400
  const [pageSize, setPageSize] = useState(12);
401
- const [sheetOpen, setSheetOpen] = useState(false);
402
- const [deleteId, setDeleteId] = useState<number | null>(null);
403
- const [editingRecordId, setEditingRecordId] = useState<number | null>(null);
404
- const [viewingRecordId, setViewingRecordId] = useState<number | null>(null);
401
+ const [sheetOpen, setSheetOpen] = useState(false);
402
+ const [deleteId, setDeleteId] = useState<number | null>(null);
403
+ const [editingRecordId, setEditingRecordId] = useState<number | null>(null);
404
+ const [viewingRecordId, setViewingRecordId] = useState<number | null>(null);
405
405
  const resourceTitle = config
406
406
  ? t(`resources.${config.translationKey}.title`)
407
407
  : t('unsupportedResource');
@@ -682,14 +682,14 @@ export default function CatalogResourcePage() {
682
682
  setSheetOpen(true);
683
683
  };
684
684
 
685
- const openEdit = (record: CatalogRecord) => {
686
- setEditingRecordId(Number(record.id));
687
- setSheetOpen(true);
688
- };
689
-
690
- const openView = (record: CatalogRecord) => {
691
- setViewingRecordId(Number(record.id));
692
- };
685
+ const openEdit = (record: CatalogRecord) => {
686
+ setEditingRecordId(Number(record.id));
687
+ setSheetOpen(true);
688
+ };
689
+
690
+ const openView = (record: CatalogRecord) => {
691
+ setViewingRecordId(Number(record.id));
692
+ };
693
693
 
694
694
  const handleDelete = async () => {
695
695
  if (!deleteId) {
@@ -853,20 +853,20 @@ export default function CatalogResourcePage() {
853
853
  </TableCell>
854
854
  ))}
855
855
  <TableCell className="text-right">
856
- <div className="flex justify-end gap-2">
857
- {resource === 'products' ? (
858
- <Button
859
- variant="outline"
860
- size="icon"
861
- onClick={() => openView(record)}
862
- >
863
- <Eye className="size-4" />
864
- </Button>
865
- ) : null}
866
- <Button
867
- variant="outline"
868
- size="icon"
869
- onClick={() => openEdit(record)}
856
+ <div className="flex justify-end gap-2">
857
+ {resource === 'products' ? (
858
+ <Button
859
+ variant="outline"
860
+ size="icon"
861
+ onClick={() => openView(record)}
862
+ >
863
+ <Eye className="size-4" />
864
+ </Button>
865
+ ) : null}
866
+ <Button
867
+ variant="outline"
868
+ size="icon"
869
+ onClick={() => openEdit(record)}
870
870
  >
871
871
  <Pencil className="size-4" />
872
872
  </Button>
@@ -909,21 +909,21 @@ export default function CatalogResourcePage() {
909
909
  description={getCardDescription(record)}
910
910
  badges={getBadges(record)}
911
911
  metadata={getCardMetadata(record)}
912
- actions={[
913
- ...(resource === 'products'
914
- ? [
915
- {
916
- label: t('openResource'),
917
- onClick: () => openView(record),
918
- variant: 'outline' as const,
919
- icon: <Eye className="size-4" />,
920
- },
921
- ]
922
- : []),
923
- {
924
- label: t('edit'),
925
- onClick: () => openEdit(record),
926
- variant: 'outline',
912
+ actions={[
913
+ ...(resource === 'products'
914
+ ? [
915
+ {
916
+ label: t('openResource'),
917
+ onClick: () => openView(record),
918
+ variant: 'outline' as const,
919
+ icon: <Eye className="size-4" />,
920
+ },
921
+ ]
922
+ : []),
923
+ {
924
+ label: t('edit'),
925
+ onClick: () => openEdit(record),
926
+ variant: 'outline',
927
927
  icon: <Pencil className="size-4" />,
928
928
  },
929
929
  {
@@ -950,7 +950,7 @@ export default function CatalogResourcePage() {
950
950
  </div>
951
951
  )}
952
952
  </div>
953
- <CatalogResourceFormSheet
953
+ <CatalogResourceFormSheet
954
954
  open={sheetOpen}
955
955
  onOpenChange={(open) => {
956
956
  setSheetOpen(open);
@@ -967,21 +967,21 @@ export default function CatalogResourcePage() {
967
967
  setEditingRecordId(null);
968
968
  await refetch();
969
969
  }}
970
- />
971
-
972
- {resource === 'products' ? (
973
- <CatalogProductPreviewDialog
974
- open={viewingRecordId !== null}
975
- onOpenChange={(open) => {
976
- if (!open) {
977
- setViewingRecordId(null);
978
- }
979
- }}
980
- productId={viewingRecordId}
981
- />
982
- ) : null}
983
-
984
- <AlertDialog
970
+ />
971
+
972
+ {resource === 'products' ? (
973
+ <CatalogProductPreviewDialog
974
+ open={viewingRecordId !== null}
975
+ onOpenChange={(open) => {
976
+ if (!open) {
977
+ setViewingRecordId(null);
978
+ }
979
+ }}
980
+ productId={viewingRecordId}
981
+ />
982
+ ) : null}
983
+
984
+ <AlertDialog
985
985
  open={deleteId !== null}
986
986
  onOpenChange={(open) => !open && setDeleteId(null)}
987
987
  >