Browse Source

feat(dashboard): Implement basic content language handling

Michael Bromley 10 months ago
parent
commit
718c0aef14

+ 6 - 3
packages/dashboard/src/app-providers.tsx

@@ -1,5 +1,6 @@
-import { AuthProvider } from '@/auth.js';
-import { I18nProvider } from '@/i18n/i18n-provider.js';
+import { AuthProvider } from '@/providers/auth.js';
+import { I18nProvider } from '@/providers/i18n-provider.js';
+import { ServerConfigProvider } from '@/providers/server-config.js';
 import { routeTree } from '@/routeTree.gen.js';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { createRouter } from '@tanstack/react-router';
@@ -29,7 +30,9 @@ export function AppProviders({ children }: { children: React.ReactNode }) {
     return (
         <I18nProvider>
             <QueryClientProvider client={queryClient}>
-                <AuthProvider>{children}</AuthProvider>
+                <ServerConfigProvider>
+                    <AuthProvider>{children}</AuthProvider>
+                </ServerConfigProvider>
             </QueryClientProvider>
         </I18nProvider>
     );

+ 35 - 0
packages/dashboard/src/components/content-language-selector.tsx

@@ -0,0 +1,35 @@
+import * as React from 'react';
+import { useServerConfig } from '@/providers/server-config';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { cn } from '@/lib/utils';
+
+interface ContentLanguageSelectorProps {
+    value?: string;
+    onChange: (value: string) => void;
+    className?: string;
+}
+
+export function ContentLanguageSelector({ value, onChange, className }: ContentLanguageSelectorProps) {
+    const serverConfig = useServerConfig();
+
+    // Fallback to empty array if serverConfig is null
+    const languages = serverConfig?.availableLanguages || [];
+
+    // If no value is provided but languages are available, use the first language
+    const currentValue = value || (languages.length > 0 ? languages[0] : '');
+
+    return (
+        <Select value={currentValue} onValueChange={onChange}>
+            <SelectTrigger className={cn('w-[200px]', className)}>
+                <SelectValue placeholder="Select language" />
+            </SelectTrigger>
+            <SelectContent>
+                {languages.map(language => (
+                    <SelectItem key={language} value={language}>
+                        {language}
+                    </SelectItem>
+                ))}
+            </SelectContent>
+        </Select>
+    );
+}

+ 1 - 1
packages/dashboard/src/components/nav-user.tsx

@@ -1,6 +1,6 @@
 'use client';
 
-import { useAuth } from '@/auth.js';
+import { useAuth } from '@/providers/auth.js';
 import { Route } from '@/routes/_authenticated.js';
 import { useRouter } from '@tanstack/react-router';
 import { BadgeCheck, Bell, ChevronsUpDown, CreditCard, LogOut, Sparkles } from 'lucide-react';

+ 149 - 137
packages/dashboard/src/components/ui/form.tsx

@@ -1,165 +1,177 @@
-import * as React from "react"
-import * as LabelPrimitive from "@radix-ui/react-label"
-import { Slot } from "@radix-ui/react-slot"
+import * as React from 'react';
+import * as LabelPrimitive from '@radix-ui/react-label';
+import { Slot } from '@radix-ui/react-slot';
 import {
-  Controller,
-  FormProvider,
-  useFormContext,
-  useFormState,
-  type ControllerProps,
-  type FieldPath,
-  type FieldValues,
-} from "react-hook-form"
+    Controller,
+    FormProvider,
+    useFormContext,
+    useFormState,
+    type ControllerProps,
+    type FieldPath,
+    type FieldValues,
+} from 'react-hook-form';
 
-import { cn } from "@/lib/utils"
-import { Label } from "@/components/ui/label"
+import { cn } from '@/lib/utils';
+import { Label } from '@/components/ui/label';
 
-const Form = FormProvider
+const Form = FormProvider;
 
 type FormFieldContextValue<
-  TFieldValues extends FieldValues = FieldValues,
-  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+    TFieldValues extends FieldValues = FieldValues,
+    TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
 > = {
-  name: TName
-}
+    name: TName;
+};
 
-const FormFieldContext = React.createContext<FormFieldContextValue>(
-  {} as FormFieldContextValue
-)
+const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
 
 const FormField = <
-  TFieldValues extends FieldValues = FieldValues,
-  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+    TFieldValues extends FieldValues = FieldValues,
+    TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
 >({
-  ...props
+    ...props
 }: ControllerProps<TFieldValues, TName>) => {
-  return (
-    <FormFieldContext.Provider value={{ name: props.name }}>
-      <Controller {...props} />
-    </FormFieldContext.Provider>
-  )
-}
+    return (
+        <FormFieldContext.Provider value={{ name: props.name }}>
+            <Controller {...props} />
+        </FormFieldContext.Provider>
+    );
+};
+
+export type TranslatableFormFieldProps = {};
+
+const TranslatableFormField = <
+    TFieldValues extends FieldValues & { translations?: Array<{ languageCode: string }> | null } = FieldValues,
+>({
+    name,
+    languageCode,
+    ...props
+}: Omit<ControllerProps<TFieldValues>, 'name'> & { 
+    name: keyof Omit<NonNullable<TFieldValues['translations']>[number], 'languageCode'>;
+    languageCode: string; 
+}) => {
+    const index = props.control?._formValues?.translations?.findIndex(
+        (translation: any) => translation?.languageCode === languageCode,
+    );
+    if (index === undefined || index === -1) {
+        return null;
+    }
+    const translationName = `translations.${index}.${String(name)}` as FieldPath<TFieldValues>;
+    return (
+        <FormFieldContext.Provider value={{ name: translationName }}>
+            <Controller {...props} name={translationName} key={translationName} />
+        </FormFieldContext.Provider>
+    );
+};
 
 const useFormField = () => {
-  const fieldContext = React.useContext(FormFieldContext)
-  const itemContext = React.useContext(FormItemContext)
-  const { getFieldState } = useFormContext()
-  const formState = useFormState({ name: fieldContext.name })
-  const fieldState = getFieldState(fieldContext.name, formState)
-
-  if (!fieldContext) {
-    throw new Error("useFormField should be used within <FormField>")
-  }
-
-  const { id } = itemContext
-
-  return {
-    id,
-    name: fieldContext.name,
-    formItemId: `${id}-form-item`,
-    formDescriptionId: `${id}-form-item-description`,
-    formMessageId: `${id}-form-item-message`,
-    ...fieldState,
-  }
-}
+    const fieldContext = React.useContext(FormFieldContext);
+    const itemContext = React.useContext(FormItemContext);
+    const { getFieldState } = useFormContext();
+    const formState = useFormState({ name: fieldContext.name });
+    const fieldState = getFieldState(fieldContext.name, formState);
+
+    if (!fieldContext) {
+        throw new Error('useFormField should be used within <FormField>');
+    }
+
+    const { id } = itemContext;
+
+    return {
+        id,
+        name: fieldContext.name,
+        formItemId: `${id}-form-item`,
+        formDescriptionId: `${id}-form-item-description`,
+        formMessageId: `${id}-form-item-message`,
+        ...fieldState,
+    };
+};
 
 type FormItemContextValue = {
-  id: string
-}
+    id: string;
+};
+
+const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
 
-const FormItemContext = React.createContext<FormItemContextValue>(
-  {} as FormItemContextValue
-)
-
-function FormItem({ className, ...props }: React.ComponentProps<"div">) {
-  const id = React.useId()
-
-  return (
-    <FormItemContext.Provider value={{ id }}>
-      <div
-        data-slot="form-item"
-        className={cn("grid gap-2", className)}
-        {...props}
-      />
-    </FormItemContext.Provider>
-  )
+function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
+    const id = React.useId();
+
+    return (
+        <FormItemContext.Provider value={{ id }}>
+            <div data-slot="form-item" className={cn('grid gap-2', className)} {...props} />
+        </FormItemContext.Provider>
+    );
 }
 
-function FormLabel({
-  className,
-  ...props
-}: React.ComponentProps<typeof LabelPrimitive.Root>) {
-  const { error, formItemId } = useFormField()
-
-  return (
-    <Label
-      data-slot="form-label"
-      data-error={!!error}
-      className={cn("data-[error=true]:text-destructive-foreground", className)}
-      htmlFor={formItemId}
-      {...props}
-    />
-  )
+function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
+    const { error, formItemId } = useFormField();
+
+    return (
+        <Label
+            data-slot="form-label"
+            data-error={!!error}
+            className={cn('data-[error=true]:text-destructive-foreground', className)}
+            htmlFor={formItemId}
+            {...props}
+        />
+    );
 }
 
 function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
-  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
-
-  return (
-    <Slot
-      data-slot="form-control"
-      id={formItemId}
-      aria-describedby={
-        !error
-          ? `${formDescriptionId}`
-          : `${formDescriptionId} ${formMessageId}`
-      }
-      aria-invalid={!!error}
-      {...props}
-    />
-  )
+    const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
+
+    return (
+        <Slot
+            data-slot="form-control"
+            id={formItemId}
+            aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
+            aria-invalid={!!error}
+            {...props}
+        />
+    );
 }
 
-function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
-  const { formDescriptionId } = useFormField()
-
-  return (
-    <p
-      data-slot="form-description"
-      id={formDescriptionId}
-      className={cn("text-muted-foreground text-sm", className)}
-      {...props}
-    />
-  )
+function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
+    const { formDescriptionId } = useFormField();
+
+    return (
+        <p
+            data-slot="form-description"
+            id={formDescriptionId}
+            className={cn('text-muted-foreground text-sm', className)}
+            {...props}
+        />
+    );
 }
 
-function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
-  const { error, formMessageId } = useFormField()
-  const body = error ? String(error?.message ?? "") : props.children
-
-  if (!body) {
-    return null
-  }
-
-  return (
-    <p
-      data-slot="form-message"
-      id={formMessageId}
-      className={cn("text-destructive-foreground text-sm", className)}
-      {...props}
-    >
-      {body}
-    </p>
-  )
+function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
+    const { error, formMessageId } = useFormField();
+    const body = error ? String(error?.message ?? '') : props.children;
+
+    if (!body) {
+        return null;
+    }
+
+    return (
+        <p
+            data-slot="form-message"
+            id={formMessageId}
+            className={cn('text-destructive-foreground text-sm', className)}
+            {...props}
+        >
+            {body}
+        </p>
+    );
 }
 
 export {
-  useFormField,
-  Form,
-  FormItem,
-  FormLabel,
-  FormControl,
-  FormDescription,
-  FormMessage,
-  FormField,
-}
+    useFormField,
+    Form,
+    FormItem,
+    FormLabel,
+    FormControl,
+    FormDescription,
+    FormMessage,
+    FormField,
+    TranslatableFormField,
+};

+ 0 - 1
packages/dashboard/src/framework/internal/form-engine/use-generated-form.tsx

@@ -40,7 +40,6 @@ export function useGeneratedForm<
     const updateFields = getOperationVariablesFields(document);
     const schema = createFormSchemaFromFields(updateFields);
     const defaultValues = getDefaultValuesFromFields(updateFields);
-    console.log(`defaultValues`, defaultValues);
     const form = useForm({
         resolver: zodResolver(schema),
         defaultValues,

+ 3 - 2
packages/dashboard/src/framework/internal/page/detail-page.tsx

@@ -20,13 +20,14 @@ export function getDetailQueryOptions<T, V extends Variables = Variables>(
 
 export interface DetailPageProps extends PageProps {
     entity: any;
+    children?: React.ReactNode;
 }
 
-export function DetailPage({ title, entity }: DetailPageProps) {
+export function DetailPage({ title, entity, children }: DetailPageProps) {
     return (
         <div>
             <h1 className="text-2xl font-bold">{title}</h1>
-            <pre className="max-w-lg overflow-scroll">{JSON.stringify(entity, null, 2)}</pre>
+            {children}
         </div>
     );
 }

+ 23 - 0
packages/dashboard/src/framework/internal/page/page.tsx

@@ -0,0 +1,23 @@
+export function Page({ children, ...props }: React.ComponentProps<'div'>) {
+    return (
+        <div data-slot="page" {...props}>
+            {children}
+        </div>
+    );
+}
+
+export function PageMain({ children, ...props }: React.ComponentProps<'main'>) {
+    return (
+        <main data-slot="page-main" {...props}>
+            {children}
+        </main>
+    );
+}
+
+export function PageSide({ children, ...props }: React.ComponentProps<'aside'>) {
+    return (
+        <aside data-slot="page-side" {...props}>
+            {children}
+        </aside>
+    );
+}

+ 2 - 2
packages/dashboard/src/main.tsx

@@ -1,8 +1,8 @@
 import { AppProviders, queryClient, router } from '@/app-providers.js';
-import { useAuth } from '@/auth.js';
+import { useAuth } from '@/providers/auth.js';
 import { useDashboardExtensions } from '@/framework/internal/extension-api/use-dashboard-extensions.js';
 import { useExtendedRouter } from '@/framework/internal/page/use-extended-router.js';
-import { defaultLocale, dynamicActivate } from '@/i18n/i18n-provider.js';
+import { defaultLocale, dynamicActivate } from '@/providers/i18n-provider.js';
 
 import '@/framework/defaults.js';
 import { RouterProvider } from '@tanstack/react-router';

+ 0 - 0
packages/dashboard/src/auth.tsx → packages/dashboard/src/providers/auth.tsx


+ 1 - 1
packages/dashboard/src/i18n/i18n-provider.tsx → packages/dashboard/src/providers/i18n-provider.tsx

@@ -13,7 +13,7 @@ export const defaultLocale = 'en';
  * @param locale any locale string
  */
 export async function dynamicActivate(locale: string, onActivate?: () => void) {
-    const { messages } = await import(`./locales/${locale}.po`);
+    const { messages } = await import(`../i18n/locales/${locale}.po`);
     i18n.load(locale, messages);
     i18n.activate(locale);
     onActivate?.();

+ 281 - 0
packages/dashboard/src/providers/server-config.tsx

@@ -0,0 +1,281 @@
+import { api } from '@/graphql/api.js';
+import { graphql } from '@/graphql/graphql.js';
+import { useQuery } from '@tanstack/react-query';
+import { ResultOf } from 'gql.tada';
+import React from 'react';
+
+export const customFieldConfigFragment = graphql(`
+    fragment CustomFieldConfig on CustomField {
+        name
+        type
+        list
+        description {
+            languageCode
+            value
+        }
+        label {
+            languageCode
+            value
+        }
+        readonly
+        nullable
+        requiresPermission
+        ui
+    }
+`);
+
+export const stringCustomFieldFragment = graphql(
+    `
+        fragment StringCustomField on StringCustomFieldConfig {
+            ...CustomFieldConfig
+            pattern
+            options {
+                label {
+                    languageCode
+                    value
+                }
+                value
+            }
+        }
+    `,
+    [customFieldConfigFragment],
+);
+export const localeStringCustomFieldFragment = graphql(
+    `
+        fragment LocaleStringCustomField on LocaleStringCustomFieldConfig {
+            ...CustomFieldConfig
+            pattern
+        }
+    `,
+    [customFieldConfigFragment],
+);
+export const textCustomFieldFragment = graphql(
+    `
+        fragment TextCustomField on TextCustomFieldConfig {
+            ...CustomFieldConfig
+        }
+    `,
+    [customFieldConfigFragment],
+);
+export const localeTextCustomFieldFragment = graphql(
+    `
+        fragment LocaleTextCustomField on LocaleTextCustomFieldConfig {
+            ...CustomFieldConfig
+        }
+    `,
+    [customFieldConfigFragment],
+);
+export const booleanCustomFieldFragment = graphql(
+    `
+        fragment BooleanCustomField on BooleanCustomFieldConfig {
+            ...CustomFieldConfig
+        }
+    `,
+    [customFieldConfigFragment],
+);
+export const intCustomFieldFragment = graphql(
+    `
+        fragment IntCustomField on IntCustomFieldConfig {
+            ...CustomFieldConfig
+            intMin: min
+            intMax: max
+            intStep: step
+        }
+    `,
+    [customFieldConfigFragment],
+);
+export const floatCustomFieldFragment = graphql(
+    `
+        fragment FloatCustomField on FloatCustomFieldConfig {
+            ...CustomFieldConfig
+            floatMin: min
+            floatMax: max
+            floatStep: step
+        }
+    `,
+    [customFieldConfigFragment],
+);
+export const dateTimeCustomFieldFragment = graphql(
+    `
+        fragment DateTimeCustomField on DateTimeCustomFieldConfig {
+            ...CustomFieldConfig
+            datetimeMin: min
+            datetimeMax: max
+            datetimeStep: step
+        }
+    `,
+    [customFieldConfigFragment],
+);
+export const relationCustomFieldFragment = graphql(
+    `
+        fragment RelationCustomField on RelationCustomFieldConfig {
+            ...CustomFieldConfig
+            entity
+            scalarFields
+        }
+    `,
+    [customFieldConfigFragment],
+);
+
+export const structCustomFieldFragment = graphql(
+    `
+        fragment StructCustomField on StructCustomFieldConfig {
+            ...CustomFieldConfig
+            fields {
+                ... on StructField {
+                    name
+                    type
+                    list
+                    description {
+                        languageCode
+                        value
+                    }
+                    label {
+                        languageCode
+                        value
+                    }
+                    ui
+                }
+                ... on StringStructFieldConfig {
+                    pattern
+                    options {
+                        label {
+                            languageCode
+                            value
+                        }
+                        value
+                    }
+                }
+                ... on IntStructFieldConfig {
+                    intMin: min
+                    intMax: max
+                    intStep: step
+                }
+                ... on FloatStructFieldConfig {
+                    floatMin: min
+                    floatMax: max
+                    floatStep: step
+                }
+                ... on DateTimeStructFieldConfig {
+                    datetimeMin: min
+                    datetimeMax: max
+                    datetimeStep: step
+                }
+            }
+        }
+    `,
+    [customFieldConfigFragment],
+);
+
+export const allCustomFieldsFragment = graphql(
+    `
+        fragment CustomFields on CustomField {
+            ... on StringCustomFieldConfig {
+                ...StringCustomField
+            }
+            ... on LocaleStringCustomFieldConfig {
+                ...LocaleStringCustomField
+            }
+            ... on TextCustomFieldConfig {
+                ...TextCustomField
+            }
+            ... on LocaleTextCustomFieldConfig {
+                ...LocaleTextCustomField
+            }
+            ... on BooleanCustomFieldConfig {
+                ...BooleanCustomField
+            }
+            ... on IntCustomFieldConfig {
+                ...IntCustomField
+            }
+            ... on FloatCustomFieldConfig {
+                ...FloatCustomField
+            }
+            ... on DateTimeCustomFieldConfig {
+                ...DateTimeCustomField
+            }
+            ... on RelationCustomFieldConfig {
+                ...RelationCustomField
+            }
+            ... on StructCustomFieldConfig {
+                ...StructCustomField
+            }
+        }
+    `,
+    [
+        stringCustomFieldFragment,
+        localeStringCustomFieldFragment,
+        textCustomFieldFragment,
+        localeTextCustomFieldFragment,
+        booleanCustomFieldFragment,
+        intCustomFieldFragment,
+        floatCustomFieldFragment,
+        dateTimeCustomFieldFragment,
+        relationCustomFieldFragment,
+        structCustomFieldFragment,
+    ],
+);
+
+export const getServerConfigDocument = graphql(
+    `
+        query GetServerConfig {
+            globalSettings {
+                id
+                availableLanguages
+                serverConfig {
+                    moneyStrategyPrecision
+                    orderProcess {
+                        name
+                        to
+                    }
+                    permittedAssetTypes
+                    permissions {
+                        name
+                        description
+                        assignable
+                    }
+                    entityCustomFields {
+                        entityName
+                        customFields {
+                            ...CustomFields
+                        }
+                    }
+                }
+            }
+        }
+    `,
+    [allCustomFieldsFragment],
+);
+
+type QueryResult = ResultOf<typeof getServerConfigDocument>['globalSettings']['serverConfig'];
+
+export interface ServerConfig {
+    availableLanguages: string[];
+    moneyStrategyPrecision: QueryResult['moneyStrategyPrecision'];
+    orderProcess: QueryResult['orderProcess'];
+    permittedAssetTypes: QueryResult['permittedAssetTypes'];
+    permissions: QueryResult['permissions'];
+    entityCustomFields: QueryResult['entityCustomFields'];
+}
+
+// create a provider for the global settings
+export const ServerConfigProvider = ({ children }: { childred: React.ReactNode }) => {
+    const { data } = useQuery({
+        queryKey: ['getServerConfig'],
+        queryFn: () => api.query(getServerConfigDocument),
+    });
+    const value: ServerConfig = {
+        availableLanguages: data?.globalSettings.availableLanguages ?? [],
+        moneyStrategyPrecision: data?.globalSettings.serverConfig.moneyStrategyPrecision ?? 2,
+        orderProcess: data?.globalSettings.serverConfig.orderProcess ?? [],
+        permittedAssetTypes: data?.globalSettings.serverConfig.permittedAssetTypes ?? [],
+        permissions: data?.globalSettings.serverConfig.permissions ?? [],
+        entityCustomFields: data?.globalSettings.serverConfig.entityCustomFields ?? [],
+    };
+
+    return <ServerConfigContext.Provider value={value}>{children}</ServerConfigContext.Provider>;
+};
+
+export const useServerConfig = () => React.useContext(ServerConfigContext);
+
+const ServerConfigContext = React.createContext<ServerConfig | null>(null);

+ 1 - 1
packages/dashboard/src/routes/__root.tsx

@@ -1,4 +1,4 @@
-import { AuthContext } from '@/auth.js';
+import { AuthContext } from '@/providers/auth.js';
 import { createRootRouteWithContext, Outlet, retainSearchParams } from '@tanstack/react-router';
 import { TanStackRouterDevtools } from '@tanstack/router-devtools';
 import * as React from 'react';

+ 79 - 63
packages/dashboard/src/routes/_authenticated/products_.$id.tsx

@@ -1,5 +1,6 @@
+import { ContentLanguageSelector } from '@/components/content-language-selector.js';
 import { Button } from '@/components/ui/button.js';
-import { Card, CardContent } from '@/components/ui/card.js';
+import { Card, CardContent, CardHeader } from '@/components/ui/card.js';
 import {
     Form,
     FormControl,
@@ -8,6 +9,7 @@ import {
     FormItem,
     FormLabel,
     FormMessage,
+    TranslatableFormField,
 } from '@/components/ui/form.js';
 import { Input } from '@/components/ui/input.js';
 import { Switch } from '@/components/ui/switch.js';
@@ -15,10 +17,10 @@ import { useGeneratedForm } from '@/framework/internal/form-engine/use-generated
 import { DetailPage, getDetailQueryOptions } from '@/framework/internal/page/detail-page.js';
 import { api } from '@/graphql/api.js';
 import { graphql } from '@/graphql/graphql.js';
+import { useServerConfig } from '@/providers/server-config.js';
 import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
 import { createFileRoute } from '@tanstack/react-router';
 import React from 'react';
-import { FieldValues } from 'react-hook-form';
 
 export const Route = createFileRoute('/_authenticated/products_/$id')({
     component: ProductDetailPage,
@@ -93,6 +95,10 @@ export function ProductDetailPage() {
             console.error(err);
         },
     });
+    const [contentLanguage, setContentLanguage] = React.useState('en');
+
+    const serverConfig = useServerConfig();
+
     const { form, submitHandler } = useGeneratedForm({
         document: updateProductDocument,
         entity,
@@ -115,66 +121,76 @@ export function ProductDetailPage() {
     });
 
     return (
-        <>
-            <DetailPage title={entity?.name ?? ''} route={Route} entity={entity}></DetailPage>
-            {entity && (
-                <Form {...form}>
-                    <form onSubmit={submitHandler} className="space-y-8">
-                        <Card className="">
-                            <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>
-                                    )}
-                                />
-                                <FormField
-                                    control={form.control}
-                                    name={`translations.${0}.name`}
-                                    render={({ field }) => (
-                                        <FormItem>
-                                            <FormLabel>name</FormLabel>
-                                            <FormControl>
-                                                <Input placeholder="" {...field} />
-                                            </FormControl>
-                                            <FormDescription></FormDescription>
-                                            <FormMessage />
-                                        </FormItem>
-                                    )}
-                                />
-                            </CardContent>
-                        </Card>
-                        <Button type="submit">Submit</Button>
-                    </form>
-                </Form>
-            )}
-        </>
+        <DetailPage title={entity?.name ?? ''} route={Route} entity={entity}>
+            <ContentLanguageSelector value={contentLanguage} onChange={setContentLanguage} />
+            <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"
+                                languageCode={contentLanguage}
+                                render={({ field }) => (
+                                    <FormItem>
+                                        <FormLabel>name</FormLabel>
+                                        <FormControl>
+                                            <Input placeholder="" {...field} />
+                                        </FormControl>
+                                        <FormDescription></FormDescription>
+                                        <FormMessage />
+                                    </FormItem>
+                                )}
+                            />
+                            <TranslatableFormField
+                                control={form.control}
+                                name="slug"
+                                languageCode={contentLanguage}
+                                render={({ field }) => (
+                                    <FormItem>
+                                        <FormLabel>Slug</FormLabel>
+                                        <FormControl>
+                                            <Input placeholder="" {...field} />
+                                        </FormControl>
+                                        <FormDescription></FormDescription>
+                                        <FormMessage />
+                                    </FormItem>
+                                )}
+                            />
+                        </CardContent>
+                    </Card>
+                    <Button type="submit">Submit</Button>
+                </form>
+            </Form>
+        </DetailPage>
     );
 }

+ 1 - 1
packages/dashboard/src/routes/login.tsx

@@ -1,4 +1,4 @@
-import { useAuth } from '@/auth.js';
+import { useAuth } from '@/providers/auth.js';
 import { LoginForm } from '@/components/login-form';
 import { createFileRoute, Navigate, redirect, useRouterState } from '@tanstack/react-router';
 import * as React from 'react';