Browse Source

refactor(dashboard): Extract some shared components

Michael Bromley 10 months ago
parent
commit
036870e140

+ 5 - 0
packages/dashboard/src/lib/components/data-display/json.tsx

@@ -0,0 +1,5 @@
+import { JsonEditor } from 'json-edit-react';
+
+export function Json({ value }: { value: any }) {
+    return <JsonEditor data={value} />;
+}

+ 330 - 0
packages/dashboard/src/lib/components/shared/customer-address-form.tsx

@@ -0,0 +1,330 @@
+import { Button } from '@vendure/dashboard';
+import { Checkbox } from '@vendure/dashboard';
+import {
+    Form,
+    FormControl,
+    FormDescription,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage,
+} from '@vendure/dashboard';
+import { Input } from '@vendure/dashboard';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@vendure/dashboard';
+import { api } from '@vendure/dashboard';
+import { graphql } from '@vendure/dashboard';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Trans, useLingui } from '@lingui/react/macro';
+import { useQuery } from '@tanstack/react-query';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+
+// 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(),
+});
+
+export type AddressFormValues = z.infer<typeof addressFormSchema>;
+
+interface CustomerAddressFormProps<T = any> {
+    address?: T;
+    setValuesForUpdate?: (values: T) => AddressFormValues;
+    onSubmit?: (values: AddressFormValues) => void;
+    onCancel?: () => void;
+}
+
+export function CustomerAddressForm<T>({ address, setValuesForUpdate, onSubmit, onCancel }: CustomerAddressFormProps<T>) {
+    const { i18n } = useLingui();
+
+    // Fetch available countries
+    const { data: countriesData, isLoading: isLoadingCountries } = useQuery({
+        queryKey: ['availableCountries'],
+        queryFn: () => api.query(getAvailableCountriesDocument),
+        staleTime: 1000 * 60 * 60 * 24, // 24 hours
+    });
+
+    // Set up form with react-hook-form and zod
+    const form = useForm<AddressFormValues>({
+        resolver: zodResolver(addressFormSchema),
+        values: address ? setValuesForUpdate?.(address) : undefined,
+    });
+
+    return (
+        <Form {...form}>
+            <form
+                onSubmit={e => {
+                    e.stopPropagation();
+                    onSubmit && form.handleSubmit(onSubmit)(e);
+                }}
+                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 }) => (
+                            <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 }) => (
+                            <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 }) => (
+                            <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 }) => (
+                            <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 }) => (
+                            <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 }) => (
+                            <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 }) => (
+                            <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 }) => (
+                            <FormItem>
+                                <FormLabel>
+                                    <Trans>Country</Trans>
+                                </FormLabel>
+                                <Select
+                                    onValueChange={field.onChange}
+                                    defaultValue={field.value || undefined}
+                                    value={field.value || undefined}
+                                    disabled={isLoadingCountries}
+                                >
+                                    <FormControl>
+                                        <SelectTrigger>
+                                            <SelectValue placeholder={i18n.t('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 }) => (
+                            <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 }) => (
+                            <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 }) => (
+                            <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">
+                        <Trans>Save Address</Trans>
+                    </Button>
+                </div>
+            </form>
+        </Form>
+    );
+}

+ 95 - 0
packages/dashboard/src/lib/components/shared/option-value-input.tsx

@@ -0,0 +1,95 @@
+import { useFieldArray } from "react-hook-form";
+import { useFormContext } from "react-hook-form";
+import { useState } from "react";
+import { Input } from "@/components/ui/input.js";
+import { Button } from "@/components/ui/button.js";
+import { Badge } from "@/components/ui/badge.js";
+import { Plus, X } from "lucide-react";
+
+interface OptionValue {
+    value: string;
+    id: string;
+}
+
+interface FormValues {
+    optionGroups: {
+        name: string;
+        values: OptionValue[];
+    }[];
+    variants: Record<string, {
+        enabled: boolean;
+        sku: string;
+        price: string;
+        stock: string;
+    }>;
+}
+
+interface OptionValueInputProps {
+    groupIndex: number;
+    disabled?: boolean;
+}
+
+export function OptionValueInput({ groupIndex, disabled = false }: OptionValueInputProps) {
+    const { control, watch } = useFormContext<FormValues>();
+    const { fields, append, remove } = useFieldArray({
+        control,
+        name: `optionGroups.${groupIndex}.values`,
+    });
+
+    const [newValue, setNewValue] = useState('');
+
+    const handleAddValue = () => {
+        if (newValue.trim() && !fields.some(f => f.value === newValue.trim())) {
+            append({ value: newValue.trim(), id: crypto.randomUUID() });
+            setNewValue('');
+        }
+    };
+
+    const handleKeyPress = (e: React.KeyboardEvent) => {
+        if (e.key === 'Enter') {
+            e.preventDefault();
+            handleAddValue();
+        }
+    };
+
+    return (
+        <div className="space-y-2">
+            <div className="flex items-center gap-2">
+                <Input
+                    value={newValue}
+                    onChange={e => setNewValue(e.target.value)}
+                    onKeyDown={handleKeyPress}
+                    placeholder="Enter value and press Enter"
+                    disabled={disabled}
+                    className="flex-1"
+                />
+                <Button
+                    type="button"
+                    variant="outline"
+                    size="sm"
+                    onClick={handleAddValue}
+                    disabled={disabled || !newValue.trim()}
+                >
+                    <Plus className="h-4 w-4" />
+                </Button>
+            </div>
+
+            <div className="flex flex-wrap gap-2">
+                {fields.map((field, index) => (
+                    <Badge key={field.id} variant="secondary" className="flex items-center gap-1 py-1 px-2">
+                        {field.value}
+                        <Button
+                            type="button"
+                            variant="ghost"
+                            size="sm"
+                            className="h-4 w-4 p-0 ml-1"
+                            onClick={() => remove(index)}
+                        >
+                            <X className="h-3 w-3" />
+                        </Button>
+                    </Badge>
+                ))}
+            </div>
+        </div>
+    );
+}