@hed-hog/catalog 0.0.293 → 0.0.294
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +409 -379
- package/dist/catalog-resource.config.d.ts.map +1 -1
- package/dist/catalog-resource.config.js +51 -24
- package/dist/catalog-resource.config.js.map +1 -1
- package/dist/catalog.controller.d.ts +420 -0
- package/dist/catalog.controller.d.ts.map +1 -1
- package/dist/catalog.controller.js +98 -0
- package/dist/catalog.controller.js.map +1 -1
- package/dist/catalog.module.d.ts.map +1 -1
- package/dist/catalog.module.js +5 -1
- package/dist/catalog.module.js.map +1 -1
- package/dist/catalog.service.d.ts +216 -1
- package/dist/catalog.service.d.ts.map +1 -1
- package/dist/catalog.service.js +1111 -5
- package/dist/catalog.service.js.map +1 -1
- package/hedhog/data/catalog_attribute.yaml +202 -0
- package/hedhog/data/catalog_attribute_option.yaml +109 -0
- package/hedhog/data/catalog_category.yaml +47 -0
- package/hedhog/data/catalog_category_attribute.yaml +209 -0
- package/hedhog/data/menu.yaml +133 -99
- package/hedhog/data/route.yaml +72 -8
- package/hedhog/frontend/app/[resource]/page.tsx.ejs +391 -33
- package/hedhog/frontend/app/_components/catalog-ai-form-assist-dialog.tsx.ejs +340 -0
- package/hedhog/frontend/app/_components/catalog-resource-form-sheet.tsx.ejs +907 -92
- package/hedhog/frontend/app/_lib/catalog-resources.tsx.ejs +929 -1161
- package/hedhog/frontend/messages/en.json +389 -299
- package/hedhog/frontend/messages/pt.json +389 -299
- package/hedhog/table/catalog_attribute.yaml +67 -52
- package/hedhog/table/catalog_attribute_option.yaml +40 -0
- package/hedhog/table/catalog_category.yaml +40 -0
- package/hedhog/table/catalog_category_attribute.yaml +37 -31
- package/hedhog/table/catalog_comparison.yaml +19 -22
- package/hedhog/table/catalog_product.yaml +30 -28
- package/hedhog/table/catalog_product_attribute_value.yaml +44 -31
- package/hedhog/table/catalog_product_category.yaml +13 -13
- package/hedhog/table/catalog_score_criterion.yaml +42 -25
- package/hedhog/table/catalog_seo_page_rule.yaml +10 -10
- package/hedhog/table/catalog_similarity_rule.yaml +33 -20
- package/hedhog/table/catalog_site.yaml +21 -13
- package/hedhog/table/catalog_site_category.yaml +12 -12
- package/package.json +6 -6
- package/src/catalog-resource.config.ts +132 -105
- package/src/catalog.controller.ts +91 -24
- package/src/catalog.module.ts +16 -12
- package/src/catalog.service.ts +1569 -56
|
@@ -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,11 +25,19 @@ import {
|
|
|
25
25
|
AlertDialogFooter,
|
|
26
26
|
AlertDialogHeader,
|
|
27
27
|
AlertDialogTitle,
|
|
28
|
-
} from '@/components/ui/alert-dialog';
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
|
|
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,
|
|
33
41
|
TableBody,
|
|
34
42
|
TableCell,
|
|
35
43
|
TableHead,
|
|
@@ -57,13 +65,327 @@ type ResourceStats = {
|
|
|
57
65
|
active?: number;
|
|
58
66
|
};
|
|
59
67
|
|
|
60
|
-
type CatalogRecord = Record<string, unknown>;
|
|
61
|
-
|
|
62
|
-
|
|
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) {
|
|
63
85
|
return value
|
|
64
86
|
.replace(/[_-]+/g, ' ')
|
|
65
|
-
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
66
|
-
}
|
|
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
|
+
}
|
|
67
389
|
|
|
68
390
|
export default function CatalogResourcePage() {
|
|
69
391
|
const params = useParams<{ resource: string }>();
|
|
@@ -76,9 +398,10 @@ export default function CatalogResourcePage() {
|
|
|
76
398
|
const [filterValue, setFilterValue] = useState('all');
|
|
77
399
|
const [page, setPage] = useState(1);
|
|
78
400
|
const [pageSize, setPageSize] = useState(12);
|
|
79
|
-
const [sheetOpen, setSheetOpen] = useState(false);
|
|
80
|
-
const [deleteId, setDeleteId] = useState<number | null>(null);
|
|
81
|
-
const [editingRecordId, setEditingRecordId] = 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);
|
|
82
405
|
const resourceTitle = config
|
|
83
406
|
? t(`resources.${config.translationKey}.title`)
|
|
84
407
|
: t('unsupportedResource');
|
|
@@ -359,10 +682,14 @@ export default function CatalogResourcePage() {
|
|
|
359
682
|
setSheetOpen(true);
|
|
360
683
|
};
|
|
361
684
|
|
|
362
|
-
const openEdit = (record: CatalogRecord) => {
|
|
363
|
-
setEditingRecordId(Number(record.id));
|
|
364
|
-
setSheetOpen(true);
|
|
365
|
-
};
|
|
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
|
+
};
|
|
366
693
|
|
|
367
694
|
const handleDelete = async () => {
|
|
368
695
|
if (!deleteId) {
|
|
@@ -526,11 +853,20 @@ export default function CatalogResourcePage() {
|
|
|
526
853
|
</TableCell>
|
|
527
854
|
))}
|
|
528
855
|
<TableCell className="text-right">
|
|
529
|
-
<div className="flex justify-end gap-2">
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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)}
|
|
534
870
|
>
|
|
535
871
|
<Pencil className="size-4" />
|
|
536
872
|
</Button>
|
|
@@ -573,11 +909,21 @@ export default function CatalogResourcePage() {
|
|
|
573
909
|
description={getCardDescription(record)}
|
|
574
910
|
badges={getBadges(record)}
|
|
575
911
|
metadata={getCardMetadata(record)}
|
|
576
|
-
actions={[
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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',
|
|
581
927
|
icon: <Pencil className="size-4" />,
|
|
582
928
|
},
|
|
583
929
|
{
|
|
@@ -604,7 +950,7 @@ export default function CatalogResourcePage() {
|
|
|
604
950
|
</div>
|
|
605
951
|
)}
|
|
606
952
|
</div>
|
|
607
|
-
<CatalogResourceFormSheet
|
|
953
|
+
<CatalogResourceFormSheet
|
|
608
954
|
open={sheetOpen}
|
|
609
955
|
onOpenChange={(open) => {
|
|
610
956
|
setSheetOpen(open);
|
|
@@ -621,9 +967,21 @@ export default function CatalogResourcePage() {
|
|
|
621
967
|
setEditingRecordId(null);
|
|
622
968
|
await refetch();
|
|
623
969
|
}}
|
|
624
|
-
/>
|
|
625
|
-
|
|
626
|
-
|
|
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
|
|
627
985
|
open={deleteId !== null}
|
|
628
986
|
onOpenChange={(open) => !open && setDeleteId(null)}
|
|
629
987
|
>
|