Browse Source

feat(dashboard): Implement add facet value

Michael Bromley 5 months ago
parent
commit
4b24f24625

+ 146 - 0
packages/dashboard/src/app/routes/_authenticated/_facets/components/add-facet-value-dialog.tsx

@@ -0,0 +1,146 @@
+import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
+import { Button } from '@/vdb/components/ui/button.js';
+import {
+    Dialog,
+    DialogContent,
+    DialogFooter,
+    DialogHeader,
+    DialogTitle,
+    DialogTrigger,
+} from '@/vdb/components/ui/dialog.js';
+import { Form } from '@/vdb/components/ui/form.js';
+import { Input } from '@/vdb/components/ui/input.js';
+import { api } from '@/vdb/graphql/api.js';
+import { graphql } from '@/vdb/graphql/graphql.js';
+import { Trans, useLingui } from '@/vdb/lib/trans.js';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useMutation } from '@tanstack/react-query';
+import { Plus } from 'lucide-react';
+import { useCallback, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { toast } from 'sonner';
+import * as z from 'zod';
+
+const createFacetValuesDocument = graphql(`
+    mutation CreateFacetValues($input: [CreateFacetValueInput!]!) {
+        createFacetValues(input: $input) {
+            id
+            name
+            code
+        }
+    }
+`);
+
+const formSchema = z.object({
+    name: z.string().min(1, 'Name is required'),
+    code: z.string().min(1, 'Code is required'),
+});
+
+type FormValues = z.infer<typeof formSchema>;
+
+export function AddFacetValueDialog({
+    facetId,
+    onSuccess,
+}: Readonly<{
+    facetId: string;
+    onSuccess?: () => void;
+}>) {
+    const [open, setOpen] = useState(false);
+    const { i18n } = useLingui();
+
+    const form = useForm<FormValues>({
+        resolver: zodResolver(formSchema),
+        defaultValues: {
+            name: '',
+            code: '',
+        },
+    });
+
+    const createFacetValueMutation = useMutation({
+        mutationFn: api.mutate(createFacetValuesDocument),
+        onSuccess: () => {
+            toast.success(i18n.t('Successfully created facet value'));
+            setOpen(false);
+            form.reset();
+            onSuccess?.();
+        },
+        onError: error => {
+            toast.error(i18n.t('Failed to create facet value'), {
+                description: error instanceof Error ? error.message : i18n.t('Unknown error'),
+            });
+        },
+    });
+
+    const onSubmit = useCallback(
+        (values: FormValues) => {
+            createFacetValueMutation.mutate({
+                input: [
+                    {
+                        facetId,
+                        code: values.code,
+                        translations: [
+                            {
+                                languageCode: 'en',
+                                name: values.name,
+                            },
+                        ],
+                    },
+                ],
+            });
+        },
+        [createFacetValueMutation, facetId],
+    );
+
+    return (
+        <Dialog open={open} onOpenChange={setOpen}>
+            <DialogTrigger asChild>
+                <Button variant="outline">
+                    <Plus className="mr-2 h-4 w-4" />
+                    <Trans>Add facet value</Trans>
+                </Button>
+            </DialogTrigger>
+            <DialogContent>
+                <DialogHeader>
+                    <DialogTitle>
+                        <Trans>Add facet value</Trans>
+                    </DialogTitle>
+                </DialogHeader>
+                <Form {...form}>
+                    <form
+                        onSubmit={e => {
+                            e.stopPropagation();
+                            form.handleSubmit(onSubmit)(e);
+                        }}
+                        className="space-y-4"
+                    >
+                        <FormFieldWrapper
+                            control={form.control}
+                            name="name"
+                            label={<Trans>Name</Trans>}
+                            render={({ field }) => <Input {...field} />}
+                        />
+                        <FormFieldWrapper
+                            control={form.control}
+                            name="code"
+                            label={<Trans>Code</Trans>}
+                            render={({ field }) => <Input {...field} />}
+                        />
+                        <DialogFooter>
+                            <Button 
+                                type="submit" 
+                                disabled={
+                                    createFacetValueMutation.isPending || 
+                                    !form.formState.isValid || 
+                                    !form.watch('name').trim() || 
+                                    !form.watch('code').trim()
+                                }
+                            >
+                                <Trans>Create facet value</Trans>
+                            </Button>
+                        </DialogFooter>
+                    </form>
+                </Form>
+            </DialogContent>
+        </Dialog>
+    );
+}

+ 82 - 62
packages/dashboard/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx

@@ -5,7 +5,8 @@ import { addCustomFields } from '@/vdb/framework/document-introspection/add-cust
 import { graphql } from '@/vdb/graphql/graphql.js';
 import { Trans } from '@/vdb/lib/trans.js';
 import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
-import { useState } from 'react';
+import { useRef, useState } from 'react';
+import { AddFacetValueDialog } from './add-facet-value-dialog.js';
 import { EditFacetValue } from './edit-facet-value.js';
 
 export const facetValueListDocument = graphql(`
@@ -26,76 +27,95 @@ export const facetValueListDocument = graphql(`
 
 export interface FacetValuesTableProps {
     facetId: string;
+    registerRefresher?: (refresher: () => void) => void;
 }
 
-export function FacetValuesTable({ facetId }: Readonly<FacetValuesTableProps>) {
+export function FacetValuesTable({ facetId, registerRefresher }: Readonly<FacetValuesTableProps>) {
     const [sorting, setSorting] = useState<SortingState>([]);
     const [page, setPage] = useState(1);
     const [pageSize, setPageSize] = useState(10);
     const [filters, setFilters] = useState<ColumnFiltersState>([]);
+    const refreshRef = useRef<() => void>(() => {});
 
     return (
-        <PaginatedListDataTable
-            listQuery={addCustomFields(facetValueListDocument)}
-            page={page}
-            itemsPerPage={pageSize}
-            sorting={sorting}
-            columnFilters={filters}
-            onPageChange={(_, page, perPage) => {
-                setPage(page);
-                setPageSize(perPage);
-            }}
-            onSortChange={(_, sorting) => {
-                setSorting(sorting);
-            }}
-            onFilterChange={(_, filters) => {
-                setFilters(filters);
-            }}
-            transformVariables={variables => {
-                const filter = variables.options?.filter ?? {};
-                return {
-                    options: {
-                        filter: {
-                            ...filter,
-                            facetId: { eq: facetId },
+        <>
+            <PaginatedListDataTable
+                listQuery={addCustomFields(facetValueListDocument)}
+                page={page}
+                itemsPerPage={pageSize}
+                sorting={sorting}
+                columnFilters={filters}
+                onPageChange={(_, page, perPage) => {
+                    setPage(page);
+                    setPageSize(perPage);
+                }}
+                onSortChange={(_, sorting) => {
+                    setSorting(sorting);
+                }}
+                onFilterChange={(_, filters) => {
+                    setFilters(filters);
+                }}
+                registerRefresher={refresher => {
+                    refreshRef.current = refresher;
+                    registerRefresher?.(refresher);
+                }}
+                transformVariables={variables => {
+                    const filter = variables.options?.filter ?? {};
+                    return {
+                        options: {
+                            filter: {
+                                ...filter,
+                                facetId: { eq: facetId },
+                            },
+                            sort: variables.options?.sort,
+                            take: pageSize,
+                            skip: (page - 1) * pageSize,
+                        },
+                    };
+                }}
+                onSearchTermChange={searchTerm => {
+                    return {
+                        name: {
+                            contains: searchTerm,
+                        },
+                    };
+                }}
+                additionalColumns={{
+                    actions: {
+                        header: 'Actions',
+                        cell: ({ row }) => {
+                            const [open, setOpen] = useState(false);
+                            const facetValue = row.original;
+                            return (
+                                <Popover open={open} onOpenChange={setOpen}>
+                                    <PopoverTrigger asChild>
+                                        <Button type="button" variant="outline" size="sm">
+                                            <Trans>Edit</Trans>
+                                        </Button>
+                                    </PopoverTrigger>
+                                    <PopoverContent className="w-80">
+                                        <EditFacetValue
+                                            facetValueId={facetValue.id}
+                                            onSuccess={() => {
+                                                setOpen(false);
+                                                refreshRef.current?.();
+                                            }}
+                                        />
+                                    </PopoverContent>
+                                </Popover>
+                            );
                         },
-                        sort: variables.options?.sort,
-                        take: pageSize,
-                        skip: (page - 1) * pageSize,
-                    },
-                };
-            }}
-            onSearchTermChange={searchTerm => {
-                return {
-                    name: {
-                        contains: searchTerm,
-                    },
-                };
-            }}
-            additionalColumns={{
-                actions: {
-                    header: 'Actions',
-                    cell: ({ row }) => {
-                        const [open, setOpen] = useState(false);
-                        const facetValue = row.original;
-                        return (
-                            <Popover open={open} onOpenChange={setOpen}>
-                                <PopoverTrigger asChild>
-                                    <Button type="button" variant="outline" size="sm">
-                                        <Trans>Edit</Trans>
-                                    </Button>
-                                </PopoverTrigger>
-                                <PopoverContent className="w-80">
-                                    <EditFacetValue
-                                        facetValueId={facetValue.id}
-                                        onSuccess={() => setOpen(false)}
-                                    />
-                                </PopoverContent>
-                            </Popover>
-                        );
                     },
-                },
-            }}
-        />
+                }}
+            />
+            <div className="mt-4">
+                <AddFacetValueDialog
+                    facetId={facetId}
+                    onSuccess={() => {
+                        refreshRef.current?.();
+                    }}
+                />
+            </div>
+        </>
     );
 }