Browse Source

fix(dashboard): Restore placeholder for VendureImage

Michael Bromley 10 months ago
parent
commit
a23991a97c

+ 3 - 3
packages/dashboard/src/components/data-table/data-table.tsx

@@ -124,9 +124,9 @@ export function DataTable<TData, TValue>({
                                 <DataTableFacetedFilter
                                     key={key}
                                     column={table.getColumn(key)}
-                                    title={filter.title}
-                                    options={filter.options}
-                                    optionsFn={filter.optionsFn}
+                                    title={filter?.title}
+                                    options={filter?.options}
+                                    optionsFn={filter?.optionsFn}
                                 />
                             ))}
                         </Suspense>

+ 42 - 1
packages/dashboard/src/components/shared/vendure-image.tsx

@@ -1,4 +1,5 @@
 import { cn } from '@/lib/utils.js';
+import { Image } from 'lucide-react';
 import React from 'react';
 
 export interface AssetLike {
@@ -42,7 +43,7 @@ export function VendureImage({
     ...imgProps
 }: VendureImageProps) {
     if (!asset) {
-        return fallback ? <>{fallback}</> : null;
+        return fallback ? <>{fallback}</> : <PlaceholderImage preset={preset} width={width} height={height} className={className} />;
     }
 
     // Build the URL with query parameters
@@ -87,6 +88,46 @@ export function VendureImage({
     );
 }
 
+export function PlaceholderImage({
+    width = 100,
+    height = 100,
+    preset = null,
+    className,
+    ...props
+}: React.ImgHTMLAttributes<HTMLImageElement> & { preset?: ImagePreset }) {
+    if (preset) {
+        switch (preset) {
+            case 'tiny':
+                width = 50;
+                height = 50;
+                break;
+            case 'thumb':
+                width = 150;
+                height = 150;
+                break;
+            case 'small':
+                width = 300;
+                height = 300;
+                break;
+            case 'medium':
+                width = 500;
+                height = 500;
+                break;
+            case 'large':
+                width = 800;
+                height = 800;
+                break;
+            default:
+                break;
+        }
+    }
+    return (
+        <div className={cn(className, 'rounded-sm bg-muted')} style={{ width, height }} {...props}>
+            <Image className="w-full h-full text-muted-foreground" />
+        </div>
+    );
+}
+
 // Convenience components for common use cases
 export function Thumbnail({
     asset,

+ 3 - 2
packages/dashboard/src/routes/__root.tsx

@@ -1,10 +1,11 @@
 import { AuthContext } from '@/providers/auth.js';
-import { createRootRouteWithContext, Outlet, retainSearchParams } from '@tanstack/react-router';
+import { QueryClient } from '@tanstack/react-query';
+import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
 import { TanStackRouterDevtools } from '@tanstack/router-devtools';
-import * as React from 'react';
 
 export interface MyRouterContext {
     auth: AuthContext;
+    queryClient: QueryClient;
 }
 
 export const Route = createRootRouteWithContext<MyRouterContext>()({

+ 75 - 0
packages/dashboard/src/routes/_authenticated/_customers/components/customer-address-card.tsx

@@ -0,0 +1,75 @@
+import { ResultOf } from "@/graphql/graphql.js";
+
+import { addressFragment } from "../customers.graphql.js";
+import { DialogContent, Dialog, DialogTrigger, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog.js";
+import { Trans } from "@lingui/react/macro";
+import { CustomerAddressForm } from "./customer-address-form.js";
+import { EditIcon, TrashIcon } from "lucide-react";
+
+export function CustomerAddressCard({ 
+  address, 
+  editable = false,
+  deletable = false
+}: { 
+  address: ResultOf<typeof addressFragment>
+  editable?: boolean
+  deletable?: boolean
+}) {
+  return (
+    <div className="border border-border rounded-md p-4 relative">
+      {(address.defaultShippingAddress || address.defaultBillingAddress) && (
+        <div className="absolute top-2 right-2 flex gap-1">
+          {address.defaultShippingAddress && (
+            <span className="text-xs px-2 py-1 bg-primary/10 text-primary rounded-md">Default Shipping</span>
+          )}
+          {address.defaultBillingAddress && (
+            <span className="text-xs px-2 py-1 bg-primary/10 text-primary rounded-md">Default Billing</span>
+          )}
+        </div>
+      )}
+      
+      <div className="flex flex-col gap-1">
+        <div className="font-semibold">{address.fullName}</div>
+        {address.company && <div>{address.company}</div>}
+        <div>{address.streetLine1}</div>
+        {address.streetLine2 && <div>{address.streetLine2}</div>}
+        <div>
+          {address.city}
+          {address.province && `, ${address.province}`}
+          {address.postalCode && ` ${address.postalCode}`}
+        </div>
+        <div>{address.country.name}</div>
+        {address.phoneNumber && <div>{address.phoneNumber}</div>}
+      </div>
+
+      {(editable || deletable) && (
+        <div className="flex gap-2 mt-3 pt-3 border-t border-border">
+          {editable && (
+            <Dialog>
+            <DialogTrigger>
+                <EditIcon className="w-4 h-4"/>
+            </DialogTrigger>
+            <DialogContent>
+              <DialogHeader>
+                <DialogTitle>Edit Address</DialogTitle>
+                <DialogDescription>
+                  <Trans>Edit the address details below.</Trans>
+                </DialogDescription>
+              </DialogHeader>
+              <CustomerAddressForm address={address} />
+            </DialogContent>
+          </Dialog>
+          )}
+          {deletable && (
+            <button 
+              onClick={() => {}}
+              className="text-xs text-muted-foreground hover:text-foreground"
+            >
+              <TrashIcon className="w-4 h-4 text-destructive"/>
+            </button>
+          )}
+        </div>
+      )}
+    </div>
+  );
+}

+ 342 - 0
packages/dashboard/src/routes/_authenticated/_customers/components/customer-address-form.tsx

@@ -0,0 +1,342 @@
+import { Button } from '@/components/ui/button.js';
+import { Checkbox } from '@/components/ui/checkbox.js';
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form.js';
+import { Input } from '@/components/ui/input.js';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select.js';
+import { api } from '@/graphql/api.js';
+import { graphql, VariablesOf } from '@/graphql/graphql.js';
+import { Trans, useLingui } from '@lingui/react/macro';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { useForm, ControllerRenderProps, FieldPath } from 'react-hook-form';
+import { z } from 'zod';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { toast } from 'sonner';
+import { updateCustomerAddressDocument } from '../customers.graphql.js';
+
+// Query document to fetch available countries
+const getAvailableCountriesDocument = graphql(`
+  query GetAvailableCountries {
+    countries(options: { filter: { enabled: { eq: true } } }) {
+      items {
+        id
+        code
+        name
+      }
+    }
+  }
+`);
+
+// Define the form schema using zod
+const addressFormSchema = z.object({
+  id: z.string(),
+  fullName: z.string().optional(),
+  company: z.string().optional(),
+  streetLine1: z.string().min(1, { message: "Street address is required" }),
+  streetLine2: z.string().optional(),
+  city: z.string().min(1, { message: "City is required" }),
+  province: z.string().optional(),
+  postalCode: z.string().optional(),
+  countryCode: z.string().min(1, { message: "Country is required" }),
+  phoneNumber: z.string().optional(),
+  defaultShippingAddress: z.boolean().default(false),
+  defaultBillingAddress: z.boolean().default(false),
+  customFields: z.any().optional()
+});
+
+type AddressFormValues = z.infer<typeof addressFormSchema>;
+
+interface CustomerAddressFormProps {
+  address?: VariablesOf<typeof updateCustomerAddressDocument>['input'];
+  onSuccess?: () => void;
+  onCancel?: () => void;
+}
+
+export function CustomerAddressForm({ address, onSuccess, onCancel }: CustomerAddressFormProps) {
+  const { i18n } = useLingui();
+  const queryClient = useQueryClient();
+
+  // Fetch available countries
+  const { data: countriesData, isLoading: isLoadingCountries } = useQuery({
+    queryKey: ['availableCountries'],
+    queryFn: () => api.query(getAvailableCountriesDocument),
+  });
+
+  // Set up form with react-hook-form and zod
+  const form = useForm<AddressFormValues>({
+    resolver: zodResolver(addressFormSchema),
+    defaultValues: {
+      id: address?.id || '',
+      fullName: address?.fullName || '',
+      company: address?.company || '',
+      streetLine1: address?.streetLine1 || '',
+      streetLine2: address?.streetLine2 || '',
+      city: address?.city || '',
+      province: address?.province || '',
+      postalCode: address?.postalCode || '',
+      countryCode: address?.countryCode || '',
+      phoneNumber: address?.phoneNumber || '',
+      defaultShippingAddress: address?.defaultShippingAddress || false,
+      defaultBillingAddress: address?.defaultBillingAddress || false,
+      customFields: address?.customFields || {}
+    }
+  });
+
+  // Set up mutation for updating address
+  const { mutate: updateAddress, isPending } = useMutation({
+    mutationFn: api.mutate(updateCustomerAddressDocument),
+    onSuccess: () => {
+      toast.success(i18n.t('Address updated successfully'));
+      // Invalidate customer detail query to refresh the data
+      queryClient.invalidateQueries({ queryKey: ['GetCustomerDetail'] });
+      if (onSuccess) {
+        onSuccess();
+      }
+    },
+    onError: (error) => {
+      toast.error(i18n.t('Failed to update address'));
+      console.error('Error updating address:', error);
+    }
+  });
+
+  // Form submission handler
+  const onSubmit = (values: AddressFormValues) => {
+    // Type assertion to handle the mutation parameters
+    updateAddress({
+      input: {
+        id: values.id,
+        fullName: values.fullName,
+        company: values.company,
+        streetLine1: values.streetLine1,
+        streetLine2: values.streetLine2,
+        city: values.city,
+        province: values.province,
+        postalCode: values.postalCode,
+        countryCode: values.countryCode,
+        phoneNumber: values.phoneNumber,
+        defaultShippingAddress: values.defaultShippingAddress,
+        defaultBillingAddress: values.defaultBillingAddress,
+        customFields: values.customFields
+      }
+    } as any);
+  };
+
+  return (
+    <Form {...form}>
+      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+          {/* Full Name */}
+          <FormField
+            control={form.control}
+            name="fullName"
+            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
+              <FormItem>
+                <FormLabel><Trans>Full Name</Trans></FormLabel>
+                <FormControl>
+                  <Input placeholder="John Doe" {...field} value={field.value || ''} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* Company */}
+          <FormField
+            control={form.control}
+            name="company"
+            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
+              <FormItem>
+                <FormLabel><Trans>Company</Trans></FormLabel>
+                <FormControl>
+                  <Input placeholder="Company (optional)" {...field} value={field.value || ''} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* Street Line 1 */}
+          <FormField
+            control={form.control}
+            name="streetLine1"
+            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
+              <FormItem>
+                <FormLabel><Trans>Street Address</Trans></FormLabel>
+                <FormControl>
+                  <Input placeholder="123 Main St" {...field} value={field.value || ''} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* Street Line 2 */}
+          <FormField
+            control={form.control}
+            name="streetLine2"
+            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
+              <FormItem>
+                <FormLabel><Trans>Apartment, suite, etc.</Trans></FormLabel>
+                <FormControl>
+                  <Input placeholder="Apt 4B (optional)" {...field} value={field.value || ''} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* City */}
+          <FormField
+            control={form.control}
+            name="city"
+            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
+              <FormItem>
+                <FormLabel><Trans>City</Trans></FormLabel>
+                <FormControl>
+                  <Input placeholder="City" {...field} value={field.value || ''} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* Province/State */}
+          <FormField
+            control={form.control}
+            name="province"
+            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
+              <FormItem>
+                <FormLabel><Trans>State/Province</Trans></FormLabel>
+                <FormControl>
+                  <Input placeholder="State/Province (optional)" {...field} value={field.value || ''} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* Postal Code */}
+          <FormField
+            control={form.control}
+            name="postalCode"
+            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
+              <FormItem>
+                <FormLabel><Trans>Postal Code</Trans></FormLabel>
+                <FormControl>
+                  <Input placeholder="Postal Code" {...field} value={field.value || ''} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* Country */}
+          <FormField
+            control={form.control}
+            name="countryCode"
+            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
+              <FormItem>
+                <FormLabel><Trans>Country</Trans></FormLabel>
+                <Select 
+                  onValueChange={field.onChange} 
+                  defaultValue={field.value || undefined}
+                  disabled={isLoadingCountries}
+                >
+                  <FormControl>
+                    <SelectTrigger>
+                      <SelectValue placeholder="Select a country" />
+                    </SelectTrigger>
+                  </FormControl>
+                  <SelectContent>
+                    {countriesData?.countries.items.map((country) => (
+                      <SelectItem key={country.code} value={country.code}>
+                        {country.name}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* Phone Number */}
+          <FormField
+            control={form.control}
+            name="phoneNumber"
+            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
+              <FormItem>
+                <FormLabel><Trans>Phone Number</Trans></FormLabel>
+                <FormControl>
+                  <Input placeholder="Phone (optional)" {...field} value={field.value || ''} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+        </div>
+
+        {/* Default Address Checkboxes */}
+        <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
+          <FormField
+            control={form.control}
+            name="defaultShippingAddress"
+            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
+              <FormItem className="flex flex-row items-start space-x-3 space-y-0">
+                <FormControl>
+                  <Checkbox
+                    checked={field.value}
+                    onCheckedChange={field.onChange}
+                  />
+                </FormControl>
+                <div className="space-y-1 leading-none">
+                  <FormLabel><Trans>Default Shipping Address</Trans></FormLabel>
+                  <FormDescription>
+                    <Trans>Use as the default shipping address</Trans>
+                  </FormDescription>
+                </div>
+              </FormItem>
+            )}
+          />
+
+          <FormField
+            control={form.control}
+            name="defaultBillingAddress"
+            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
+              <FormItem className="flex flex-row items-start space-x-3 space-y-0">
+                <FormControl>
+                  <Checkbox
+                    checked={field.value}
+                    onCheckedChange={field.onChange}
+                  />
+                </FormControl>
+                <div className="space-y-1 leading-none">
+                  <FormLabel><Trans>Default Billing Address</Trans></FormLabel>
+                  <FormDescription>
+                    <Trans>Use as the default billing address</Trans>
+                  </FormDescription>
+                </div>
+              </FormItem>
+            )}
+          />
+        </div>
+
+        {/* Form Actions */}
+        <div className="flex justify-end gap-2 pt-4">
+          {onCancel && (
+            <Button type="button" variant="outline" onClick={onCancel}>
+              <Trans>Cancel</Trans>
+            </Button>
+          )}
+          <Button type="submit" disabled={isPending}>
+            {isPending ? (
+              <Trans>Saving...</Trans>
+            ) : (
+              <Trans>Save Address</Trans>
+            )}
+          </Button>
+        </div>
+      </form>
+    </Form>
+  );
+}
+

+ 97 - 0
packages/dashboard/src/routes/_authenticated/_customers/customers.graphql.ts

@@ -1,4 +1,5 @@
 import { graphql } from '@/graphql/graphql.js';
+import { gql } from 'awesome-graphql-client';
 
 export const customerListDocument = graphql(`
     query GetCustomerList($options: CustomerListOptions) {
@@ -19,3 +20,99 @@ export const customerListDocument = graphql(`
         }
     }
 `);
+
+export const addressFragment = graphql(`
+    fragment Address on Address {
+        id
+        createdAt
+        updatedAt
+        fullName
+        company
+        streetLine1
+        streetLine2
+        city
+        province
+        postalCode
+        country {
+            id
+            code
+            name
+        }
+        phoneNumber
+        defaultShippingAddress
+        defaultBillingAddress
+    }
+`);
+
+export const customerDetailDocument = graphql(
+    `
+        query GetCustomerDetail($id: ID!) {
+            customer(id: $id) {
+                id
+                createdAt
+                updatedAt
+                title
+                firstName
+                lastName
+                phoneNumber
+                emailAddress
+                user {
+                    id
+                    identifier
+                    verified
+                    lastLogin
+                }
+                addresses {
+                    ...Address
+                }
+            }
+        }
+    `,
+    [addressFragment],
+);
+
+export const createCustomerDocument = graphql(`
+    mutation CreateCustomer($input: CreateCustomerInput!) {
+        createCustomer(input: $input) {
+            __typename
+            ... on Customer {
+                id
+            }
+            ... on ErrorResult {
+                errorCode
+                message
+            }
+        }
+    }
+`);
+
+export const updateCustomerDocument = graphql(`
+    mutation UpdateCustomer($input: UpdateCustomerInput!) {
+        updateCustomer(input: $input) {
+            __typename
+            ... on Customer {
+                id
+            }
+            ... on ErrorResult {
+                errorCode
+                message
+            }
+        }
+    }
+`);
+
+export const createCustomerAddressDocument = graphql(`
+    mutation CreateCustomerAddress($customerId: ID!, $input: CreateAddressInput!) {
+        createCustomerAddress(customerId: $customerId, input: $input) {
+            id
+        }
+    }
+`);
+
+export const updateCustomerAddressDocument = graphql(`
+    mutation UpdateCustomerAddress($input: UpdateAddressInput!) {
+        updateCustomerAddress(input: $input) {
+            id
+        }
+    }
+`);

+ 1 - 1
packages/dashboard/src/routes/_authenticated/_customers/customers.tsx

@@ -55,7 +55,7 @@ export function CustomerListPage() {
                         return (
                             <Button asChild variant="ghost">
                                 <Link
-                                    to="/_authenticated/_customers/customers/$id"
+                                    to="/customers/$id"
                                     params={{ id: row.original.id }}
                                 >
                                     {value}

+ 222 - 0
packages/dashboard/src/routes/_authenticated/_customers/customers_.$id.tsx

@@ -0,0 +1,222 @@
+import { ContentLanguageSelector } from '@/components/layout/content-language-selector.js';
+import { ErrorPage } from '@/components/shared/error-page.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+import { TranslatableFormField } from '@/components/shared/translatable-form-field.js';
+import { Button } from '@/components/ui/button.js';
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form.js';
+import { Input } from '@/components/ui/input.js';
+import { NEW_ENTITY_PATH } from '@/constants.js';
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import {
+    CustomFieldsPageBlock,
+    Page,
+    PageActionBar,
+    PageBlock,
+    PageLayout,
+    PageTitle,
+} from '@/framework/layout-engine/page-layout.js';
+import { getDetailQueryOptions, useDetailPage } from '@/framework/page/use-detail-page.js';
+import { Trans, useLingui } from '@lingui/react/macro';
+import { createFileRoute, useNavigate } from '@tanstack/react-router';
+import { toast } from 'sonner';
+import {
+    createCustomerDocument,
+    customerDetailDocument,
+    updateCustomerDocument,
+} from './customers.graphql.js';
+import { CustomerAddressCard } from './components/customer-address-card.js';
+
+export const Route = createFileRoute('/_authenticated/_customers/customers_/$id')({
+    component: CustomerDetailPage,
+    loader: async ({ context, params }) => {
+        const isNew = params.id === NEW_ENTITY_PATH;
+        const result = isNew
+            ? null
+            : await context.queryClient.ensureQueryData(
+                  getDetailQueryOptions(addCustomFields(customerDetailDocument), { id: params.id }),
+                  { id: params.id },
+              );
+        if (!isNew && !result.customer) {
+            throw new Error(`Customer with the ID ${params.id} was not found`);
+        }
+        return {
+            breadcrumb: [
+                { path: '/customers', label: 'Customers' },
+                isNew ? (
+                    <Trans>New customer</Trans>
+                ) : (
+                    `${result.customer.firstName} ${result.customer.lastName}`
+                ),
+            ],
+        };
+    },
+    errorComponent: ({ error }) => <ErrorPage message={error.message} />,
+});
+
+export function CustomerDetailPage() {
+    const params = Route.useParams();
+    const navigate = useNavigate();
+    const creatingNewEntity = params.id === NEW_ENTITY_PATH;
+    const { i18n } = useLingui();
+
+    const { form, submitHandler, entity, isPending } = useDetailPage({
+        queryDocument: addCustomFields(customerDetailDocument),
+        entityField: 'customer',
+        createDocument: createCustomerDocument,
+        updateDocument: updateCustomerDocument,
+        setValuesForUpdate: entity => {
+            return {
+                id: entity.id,
+                title: entity.title,
+                emailAddress: entity.emailAddress,
+                firstName: entity.firstName,
+                lastName: entity.lastName,
+                phoneNumber: entity.phoneNumber,
+                addresses: entity.addresses,
+                customFields: entity.customFields,
+            };
+        },
+        params: { id: params.id },
+        onSuccess: async data => {
+            if (data.__typename === 'Customer') {
+                toast(i18n.t('Successfully updated customer'), {
+                    position: 'top-right',
+                });
+                form.reset(form.getValues());
+                if (creatingNewEntity) {
+                    await navigate({ to: `../${data?.id}`, from: Route.id });
+                }
+            } else {
+                toast(i18n.t('Failed to update customer'), {
+                    position: 'top-right',
+                    description: data.message,
+                });
+            }
+        },
+        onError: err => {
+            toast(i18n.t('Failed to update customer'), {
+                position: 'top-right',
+                description: err instanceof Error ? err.message : 'Unknown error',
+            });
+        },
+    });
+
+    const customerName = entity ? `${entity.firstName} ${entity.lastName}` : '';
+
+    return (
+        <Page>
+            <PageTitle>{creatingNewEntity ? <Trans>New customer</Trans> : customerName}</PageTitle>
+            <Form {...form}>
+                <form onSubmit={submitHandler} className="space-y-8">
+                    <PageActionBar>
+                        <div></div>
+                        <PermissionGuard requires={['UpdateCustomer']}>
+                            <Button
+                                type="submit"
+                                disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                            >
+                                <Trans>Update</Trans>
+                            </Button>
+                        </PermissionGuard>
+                    </PageActionBar>
+                    <PageLayout>
+                        {/*  <PageBlock column="side"></PageBlock> */}
+                        <PageBlock column="main">
+                            <div className="md:grid md:grid-cols-2 w-full gap-4">
+                                <FormField
+                                    control={form.control}
+                                    name="title"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>Title</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <Input placeholder="" {...field} />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                                <div></div>
+                                <FormField
+                                    control={form.control}
+                                    name="firstName"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>First name</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <Input placeholder="" {...field} />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                                <FormField
+                                    control={form.control}
+                                    name="lastName"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>Last name</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <Input placeholder="" {...field} />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                                <FormField
+                                    control={form.control}
+                                    name="emailAddress"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>Email address</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <Input placeholder="" {...field} />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                                <FormField
+                                    control={form.control}
+                                    name="phoneNumber"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>Phone number</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <Input placeholder="" {...field} />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                            </div>
+                        </PageBlock>
+                        <CustomFieldsPageBlock column="main" entityType="Customer" control={form.control} />
+                        <PageBlock column="main" title={<Trans>Addresses</Trans>}>
+                            <div className="md:grid md:grid-cols-2 gap-4">
+                                {entity?.addresses?.map(address => (
+                                    <CustomerAddressCard
+                                        key={address.id}
+                                        address={address}
+                                        editable
+                                        deletable
+                                    />
+                                ))}
+                            </div>
+                        </PageBlock>
+                    </PageLayout>
+                </form>
+            </Form>
+        </Page>
+    );
+}

+ 1 - 1
packages/dashboard/src/routes/_authenticated/_facets/facets_.$id.tsx

@@ -60,7 +60,7 @@ export function FacetDetailPage() {
     const creatingNewEntity = params.id === NEW_ENTITY_PATH;
     const { i18n } = useLingui();
 
-    const { form, submitHandler, entity, isPending, refreshEntity } = useDetailPage({
+    const { form, submitHandler, entity, isPending } = useDetailPage({
         queryDocument: addCustomFields(facetDetailDocument),
         entityField: 'facet',
         createDocument: createFacetDocument,