Просмотр исходного кода

feat(dashboard): Add facet value control to prod detail

Michael Bromley 10 месяцев назад
Родитель
Сommit
d4bf96e5eb

+ 98 - 0
packages/dashboard/src/components/shared/assigned-facet-values.tsx

@@ -0,0 +1,98 @@
+import { Button } from '@/components/ui/button.js';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card.js';
+import { Plus, X } from 'lucide-react';
+import { Badge } from '@/components/ui/badge.js';
+import { Trans } from '@lingui/react/macro';
+import { useState } from 'react';
+
+// Interface for facet value type
+interface FacetValue {
+    id: string;
+    name: string;
+    code: string;
+    facet: {
+        id: string;
+        name: string;
+        code: string;
+    };
+}
+
+interface FacetValueChipProps {
+    facetValue: FacetValue;
+    removable?: boolean;
+    onRemove?: (id: string) => void;
+}
+
+// Individual facet value chip component
+function FacetValueChip({ facetValue, removable = true, onRemove }: FacetValueChipProps) {
+    const fullText = `${facetValue.facet.name}: ${facetValue.name}`;
+
+    return (
+        <Badge variant="outline" className="mr-2 mb-2 flex items-center gap-1">
+            <span className="max-w-[200px] truncate" title={fullText}>
+                {fullText}
+            </span>
+            {removable && (
+                <button
+                    type="button"
+                    className="inline-flex h-4 w-4 items-center justify-center rounded-full hover:bg-muted/20"
+                    onClick={() => onRemove?.(facetValue.id)}
+                    aria-label={`Remove ${facetValue.facet.name}: ${facetValue.name}`}
+                >
+                    <X className="h-3 w-3" />
+                </button>
+            )}
+        </Badge>
+    );
+}
+
+interface AssignedFacetValuesProps {
+    facetValues: FacetValue[];
+    value: id[];
+    canUpdate?: boolean;
+    onBlur?: () => void;
+    onChange?: (value: FacetValue[]) => void;
+}
+
+export function AssignedFacetValues({
+    value = [],
+    facetValues,
+    canUpdate = true,
+    onBlur,
+    onChange,
+}: AssignedFacetValuesProps) {
+    function onRemoveHandler(id: string) {
+        onChange?.(value.filter(fvId => fvId !== id));
+    }
+
+    function onAddHandler(id: string) {
+        // onChange?.([...value, id]);
+    }
+
+    return (
+        <>
+            <div className="flex flex-wrap">
+                {value.map(id => {
+                    const facetValue = facetValues.find(fv => fv.id === id);
+                    if (!facetValue) return null;
+                    return (
+                        <FacetValueChip
+                            key={facetValue.id}
+                            facetValue={facetValue}
+                            removable={canUpdate}
+                            onRemove={onRemoveHandler}
+                        />
+                    );
+                })}
+            </div>
+            {canUpdate && (
+                <div>
+                    <Button variant="outline" size="sm" className="mt-2" onClick={onAddHandler}>
+                        <Plus className="h-4 w-4 mr-1" />
+                        <Trans>Add facets</Trans>
+                    </Button>
+                </div>
+            )}
+        </>
+    );
+}

+ 166 - 97
packages/dashboard/src/routes/_authenticated/products_.$id.tsx

@@ -1,7 +1,9 @@
 import { ContentLanguageSelector } from '@/components/layout/content-language-selector.js';
+import { AssignedFacetValues } from '@/components/shared/assigned-facet-values.js';
 import { EntityAssets } from '@/components/shared/entity-assets.js';
+import { TranslatableFormField } from '@/components/shared/translatable-form-field.js';
 import { Button } from '@/components/ui/button.js';
-import { Card, CardContent, CardHeader } from '@/components/ui/card.js';
+import { Card, CardContent } from '@/components/ui/card.js';
 import {
     Form,
     FormControl,
@@ -15,13 +17,14 @@ import { Input } from '@/components/ui/input.js';
 import { Switch } from '@/components/ui/switch.js';
 import { Textarea } from '@/components/ui/textarea.js';
 import { useGeneratedForm } from '@/framework/form-engine/use-generated-form.js';
-import { TranslatableFormField } from '@/components/shared/translatable-form-field.js';
 import { DetailPage, getDetailQueryOptions } from '@/framework/page/detail-page.js';
 import { api } from '@/graphql/api.js';
 import { assetFragment } from '@/graphql/fragments.js';
 import { graphql } from '@/graphql/graphql.js';
-import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
+import { Trans } from '@lingui/react/macro';
+import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
 import { createFileRoute } from '@tanstack/react-router';
+import { useEffect } from 'react';
 import { toast } from 'sonner';
 
 export const Route = createFileRoute('/_authenticated/products_/$id')({
@@ -58,6 +61,16 @@ const productDetailFragment = graphql(
                 slug
                 description
             }
+            facetValues {
+                id
+                name
+                code
+                facet {
+                    id
+                    name
+                    code
+                }
+            }
         }
     `,
     [assetFragment],
@@ -87,7 +100,9 @@ const updateProductDocument = graphql(
 
 export function ProductDetailPage() {
     const params = Route.useParams();
-    const detailQuery = useSuspenseQuery(getDetailQueryOptions(productDetailDocument, { id: params.id }));
+    const queryClient = useQueryClient();
+    const detailQueryOptions = getDetailQueryOptions(productDetailDocument, { id: params.id });
+    const detailQuery = useSuspenseQuery(detailQueryOptions);
     const entity = detailQuery.data.product;
     const updateMutation = useMutation({
         mutationFn: api.mutate(updateProductDocument),
@@ -95,6 +110,8 @@ export function ProductDetailPage() {
             toast('Updated', {
                 position: 'top-right',
             });
+            form.reset();
+            queryClient.invalidateQueries(detailQueryOptions.queryKey);
         },
         onError: err => {
             console.error(err);
@@ -109,6 +126,7 @@ export function ProductDetailPage() {
             enabled: entity.enabled,
             featuredAssetId: entity.featuredAsset?.id,
             assetIds: entity.assets.map(asset => asset.id),
+            facetValueIds: entity.facetValues.map(facetValue => facetValue.id),
             translations: entity.translations.map(translation => ({
                 id: translation.id,
                 languageCode: translation.languageCode,
@@ -124,107 +142,158 @@ export function ProductDetailPage() {
 
     return (
         <DetailPage title={entity?.name ?? ''} route={Route} entity={entity}>
-            <ContentLanguageSelector className="mb-4" />
             <Form {...form}>
                 <form onSubmit={submitHandler} className="space-y-8">
-                    <Card className="">
-                        <CardHeader />
-                        <CardContent>
-                            <FormField
-                                control={form.control}
-                                name="enabled"
-                                render={({ field }) => (
-                                    <FormItem>
-                                        <FormLabel>Enabled</FormLabel>
-                                        <FormControl>
-                                            <Switch checked={field.value} onCheckedChange={field.onChange} />
-                                        </FormControl>
-                                        <FormDescription>This is your public display name.</FormDescription>
-                                        <FormMessage />
-                                    </FormItem>
-                                )}
-                            />
-                            <FormField
-                                control={form.control}
-                                name="featuredAssetId"
-                                render={({ field }) => (
-                                    <FormItem>
-                                        <FormLabel>featuredAssetId</FormLabel>
-                                        <FormControl>
-                                            <Input placeholder="" {...field} />
-                                        </FormControl>
-                                        <FormDescription></FormDescription>
-                                        <FormMessage />
-                                    </FormItem>
-                                )}
-                            />
-                            <TranslatableFormField
-                                control={form.control}
-                                name="name"
-                                render={({ field }) => (
-                                    <FormItem>
-                                        <FormLabel>name</FormLabel>
-                                        <FormControl>
-                                            <Input placeholder="" {...field} />
-                                        </FormControl>
-                                        <FormDescription></FormDescription>
-                                        <FormMessage />
-                                    </FormItem>
-                                )}
-                            />
-                            <TranslatableFormField
-                                control={form.control}
-                                name="slug"
-                                render={({ field }) => (
-                                    <FormItem>
-                                        <FormLabel>Slug</FormLabel>
-                                        <FormControl>
-                                            <Input placeholder="" {...field} />
-                                        </FormControl>
-                                        <FormDescription></FormDescription>
-                                        <FormMessage />
-                                    </FormItem>
-                                )}
-                            />
-                            <TranslatableFormField
-                                control={form.control}
-                                name="description"
-                                render={({ field }) => (
+                    <div className="flex justify-between">
+                        <ContentLanguageSelector className="mb-4" />
+                        <Button
+                            type="submit"
+                            disabled={
+                                !form.formState.isDirty || !form.formState.isValid || updateMutation.isPending
+                            }
+                        >
+                            Submit
+                        </Button>
+                    </div>
+
+                    <div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
+                        <div className="lg:col-span-3 flex flex-col gap-4">
+                            <Card className="">
+                                <CardContent className="pt-6">
+                                    <div className="flex flex-col gap-4">
+                                        <div className="md:flex w-full gap-4">
+                                            <div className="w-1/2">
+                                                <TranslatableFormField
+                                                    control={form.control}
+                                                    name="name"
+                                                    render={({ field }) => (
+                                                        <FormItem>
+                                                            <FormLabel>
+                                                                <Trans>Product name</Trans>
+                                                            </FormLabel>
+                                                            <FormControl>
+                                                                <Input placeholder="" {...field} />
+                                                            </FormControl>
+                                                            <FormDescription></FormDescription>
+                                                            <FormMessage />
+                                                        </FormItem>
+                                                    )}
+                                                />
+                                            </div>
+                                            <div className="w-1/2">
+                                                <TranslatableFormField
+                                                    control={form.control}
+                                                    name="slug"
+                                                    render={({ field }) => (
+                                                        <FormItem>
+                                                            <FormLabel>
+                                                                <Trans>Slug</Trans>
+                                                            </FormLabel>
+                                                            <FormControl>
+                                                                <Input placeholder="" {...field} />
+                                                            </FormControl>
+                                                            <FormDescription></FormDescription>
+                                                            <FormMessage />
+                                                        </FormItem>
+                                                    )}
+                                                />
+                                            </div>
+                                        </div>
+                                        <TranslatableFormField
+                                            control={form.control}
+                                            name="description"
+                                            render={({ field }) => (
+                                                <FormItem>
+                                                    <FormLabel>
+                                                        <Trans>Description</Trans>
+                                                    </FormLabel>
+                                                    <FormControl>
+                                                        <Textarea className="resize-none" {...field} />
+                                                    </FormControl>
+                                                    <FormDescription></FormDescription>
+                                                    <FormMessage />
+                                                </FormItem>
+                                            )}
+                                        />
+                                    </div>
+                                </CardContent>
+                            </Card>
+                        </div>
+                        <div className="lg:col-span-1 flex flex-col gap-4">
+                            <Card className="">
+                                <CardContent className="pt-6">
+                                    <FormField
+                                        control={form.control}
+                                        name="enabled"
+                                        render={({ field }) => (
+                                            <FormItem>
+                                                <FormLabel>
+                                                    <Trans>Enabled</Trans>
+                                                </FormLabel>
+                                                <FormControl>
+                                                    <Switch
+                                                        checked={field.value}
+                                                        onCheckedChange={field.onChange}
+                                                    />
+                                                </FormControl>
+                                                <FormDescription>
+                                                    <Trans>
+                                                        When enabled, a product is available in the shop
+                                                    </Trans>
+                                                </FormDescription>
+                                                <FormMessage />
+                                            </FormItem>
+                                        )}
+                                    />
+                                </CardContent>
+                            </Card>
+                            <Card className="">
+                                <CardContent className="pt-6">
+                                    <FormField
+                                        control={form.control}
+                                        name="facetValueIds"
+                                        render={({ field }) => (
+                                            <FormItem>
+                                                <FormLabel>
+                                                    <Trans>Facet values</Trans>
+                                                </FormLabel>
+                                                <FormControl>
+                                                    <AssignedFacetValues
+                                                        facetValues={entity.facetValues}
+                                                        {...field}
+                                                    />
+                                                </FormControl>
+                                                <FormMessage />
+                                            </FormItem>
+                                        )}
+                                    />
+                                </CardContent>
+                            </Card>
+                            <Card className="">
+                                <CardContent className="pt-6">
                                     <FormItem>
-                                        <FormLabel>Description</FormLabel>
+                                        <FormLabel>
+                                            <Trans>Assets</Trans>
+                                        </FormLabel>
                                         <FormControl>
-                                            <Textarea className="resize-none" {...field} />
+                                            <EntityAssets
+                                                assets={entity?.assets}
+                                                featuredAsset={entity?.featuredAsset}
+                                                compact={true}
+                                                value={form.getValues()}
+                                                onChange={value => {
+                                                    form.setValue('featuredAssetId', value.featuredAssetId);
+                                                    form.setValue('assetIds', value.assetIds);
+                                                }}
+                                            />
                                         </FormControl>
                                         <FormDescription></FormDescription>
                                         <FormMessage />
                                     </FormItem>
-                                )}
-                            />
-                        </CardContent>
-                    </Card>
-                    <Card className="">
-                        <CardHeader />
-                        <CardContent>
-                            <FormItem>
-                                <FormLabel>Assets</FormLabel>
-                                <FormControl>
-                                    <EntityAssets
-                                        assets={entity?.assets}
-                                        featuredAsset={entity?.featuredAsset}
-                                        compact={false}
-                                        value={form.getValues()}
-                                        onChange={value => {
-                                            form.setValue('featuredAssetId', value.featuredAssetId);
-                                            form.setValue('assetIds', value.assetIds);
-                                        }}
-                                    />
-                                </FormControl>
-                                <FormDescription></FormDescription>
-                                <FormMessage />
-                            </FormItem>
-                        </CardContent>
-                    </Card>
-                    <Button type="submit">Submit</Button>
+                                </CardContent>
+                            </Card>
+                        </div>
+                    </div>
                 </form>
             </Form>
         </DetailPage>