Sfoglia il codice sorgente

feat(dashboard): Implement tabbed interface for custom fields in forms

David Höck 6 mesi fa
parent
commit
68af675724

+ 141 - 67
packages/dashboard/src/lib/components/shared/custom-fields-form.tsx

@@ -7,12 +7,15 @@ import {
     FormMessage,
 } from '@/components/ui/form.js';
 import { Input } from '@/components/ui/input.js';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs.js';
 import { CustomFormComponent } from '@/framework/form-engine/custom-form-component.js';
 import { useCustomFieldConfig } from '@/hooks/use-custom-field-config.js';
 import { useUserSettings } from '@/hooks/use-user-settings.js';
+import { useLingui } from '@/lib/trans.js';
 import { customFieldConfigFragment } from '@/providers/server-config.js';
 import { CustomFieldType } from '@vendure/common/lib/shared-types';
 import { ResultOf } from 'gql.tada';
+import React, { useMemo } from 'react';
 import { Control, ControllerRenderProps } from 'react-hook-form';
 import { Switch } from '../ui/switch.js';
 import { TranslatableFormField } from './translatable-form-field.js';
@@ -29,6 +32,7 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Custom
     const {
         settings: { displayLanguage },
     } = useUserSettings();
+    const { i18n } = useLingui();
 
     const getTranslation = (input: Array<{ languageCode: string; value: string }> | null | undefined) => {
         return input?.find(t => t.languageCode === displayLanguage)?.value;
@@ -42,18 +46,80 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Custom
             : `customFields.${fieldDefName}`;
     };
 
+    // Group custom fields by tabs
+    const groupedFields = useMemo(() => {
+        if (!customFields) return [];
+
+        const tabMap = new Map<string, CustomFieldConfig[]>();
+        const defaultTabName = '__default_tab__';
+
+        for (const field of customFields) {
+            const tabName = field.ui?.tab ?? defaultTabName;
+            if (tabMap.has(tabName)) {
+                tabMap.get(tabName)?.push(field);
+            } else {
+                tabMap.set(tabName, [field]);
+            }
+        }
+
+        return Array.from(tabMap.entries())
+            .sort((a, b) => (a[0] === defaultTabName ? -1 : 1))
+            .map(([tabName, customFields]) => ({
+                tabName: tabName === defaultTabName ? 'general' : tabName,
+                customFields,
+            }));
+    }, [customFields]);
+
+    // Check if we should show tabs (more than one tab or at least one field has a tab)
+    const shouldShowTabs = useMemo(() => {
+        if (!customFields) return false;
+        const hasTabbedFields = customFields.some(field => field.ui?.tab);
+        return hasTabbedFields || groupedFields.length > 1;
+    }, [customFields, groupedFields.length]);
+
+    if (!shouldShowTabs) {
+        // Single tab view - use the original grid layout
+        return (
+            <div className="grid grid-cols-2 gap-4">
+                {customFields?.map(fieldDef => (
+                    <CustomFieldItem
+                        key={fieldDef.name}
+                        fieldDef={fieldDef}
+                        control={control}
+                        fieldName={getFieldName(fieldDef.name)}
+                        getTranslation={getTranslation}
+                    />
+                ))}
+            </div>
+        );
+    }
+
+    // Tabbed view
     return (
-        <div className="grid grid-cols-2 gap-4">
-            {customFields?.map(fieldDef => (
-                <CustomFieldItem
-                    key={fieldDef.name}
-                    fieldDef={fieldDef}
-                    control={control}
-                    fieldName={getFieldName(fieldDef.name)}
-                    getTranslation={getTranslation}
-                />
+        <Tabs defaultValue={groupedFields[0]?.tabName} className="w-full">
+            <TabsList>
+                {groupedFields.map(group => (
+                    <TabsTrigger key={group.tabName} value={group.tabName}>
+                        {group.tabName === 'general' ? i18n.t('General') : group.tabName}
+                    </TabsTrigger>
+                ))}
+            </TabsList>
+            {groupedFields.map(group => (
+                <TabsContent key={group.tabName} value={group.tabName} className="mt-4">
+                    <div className="grid grid-cols-2 gap-4">
+                        {group.customFields.map(fieldDef => (
+                            <CustomFieldItem
+                                key={fieldDef.name}
+                                fieldDef={fieldDef}
+                                control={control}
+                                fieldName={getFieldName(fieldDef.name)}
+                                getTranslation={getTranslation}
+                            />
+                        ))}
+                    </div>
+                </TabsContent>
             ))}
-        </div>
+        </Tabs>
     );
 }
 
@@ -69,83 +135,91 @@ interface CustomFieldItemProps {
 function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: CustomFieldItemProps) {
     const hasCustomFormComponent = fieldDef.ui && fieldDef.ui.component;
     const isLocaleField = fieldDef.type === 'localeString' || fieldDef.type === 'localeText';
+    const shouldBeFullWidth = fieldDef.ui?.fullWidth === true;
+    const containerClassName = shouldBeFullWidth ? 'col-span-2' : '';
 
     // For locale fields, always use TranslatableFormField regardless of custom components
     if (isLocaleField) {
         return (
-            <TranslatableFormField
-                control={control}
-                name={fieldName}
-                render={({ field, ...props }) => (
-                    <FormItem>
-                        <FormLabel>{getTranslation(fieldDef.label) ?? field.name}</FormLabel>
-                        <FormControl>
-                            {hasCustomFormComponent ? (
-                                <CustomFormComponent
-                                    fieldDef={fieldDef}
-                                    fieldProps={{
-                                        ...props,
-                                        field: {
-                                            ...field,
-                                            disabled: fieldDef.readonly ?? false,
-                                        },
-                                    }}
-                                />
-                            ) : (
-                                <FormInputForType fieldDef={fieldDef} field={field} />
-                            )}
-                        </FormControl>
-                        <FormDescription>{getTranslation(fieldDef.description)}</FormDescription>
-                        <FormMessage />
-                    </FormItem>
-                )}
-            />
+            <div className={containerClassName}>
+                <TranslatableFormField
+                    control={control}
+                    name={fieldName}
+                    render={({ field, ...props }) => (
+                        <FormItem>
+                            <FormLabel>{getTranslation(fieldDef.label) ?? field.name}</FormLabel>
+                            <FormControl>
+                                {hasCustomFormComponent ? (
+                                    <CustomFormComponent
+                                        fieldDef={fieldDef}
+                                        fieldProps={{
+                                            ...props,
+                                            field: {
+                                                ...field,
+                                                disabled: fieldDef.readonly ?? false,
+                                            },
+                                        }}
+                                    />
+                                ) : (
+                                    <FormInputForType fieldDef={fieldDef} field={field} />
+                                )}
+                            </FormControl>
+                            <FormDescription>{getTranslation(fieldDef.description)}</FormDescription>
+                            <FormMessage />
+                        </FormItem>
+                    )}
+                />
+            </div>
         );
     }
 
     // For non-locale fields with custom components
     if (hasCustomFormComponent) {
         return (
+            <div className={containerClassName}>
+                <FormField
+                    control={control}
+                    name={fieldName}
+                    render={fieldProps => (
+                        <CustomFieldFormItem
+                            fieldDef={fieldDef}
+                            getTranslation={getTranslation}
+                            fieldName={fieldProps.field.name}
+                        >
+                            <CustomFormComponent
+                                fieldDef={fieldDef}
+                                fieldProps={{
+                                    ...fieldProps,
+                                    field: {
+                                        ...fieldProps.field,
+                                        disabled: fieldDef.readonly ?? false,
+                                    },
+                                }}
+                            />
+                        </CustomFieldFormItem>
+                    )}
+                />
+            </div>
+        );
+    }
+
+    // For regular fields without custom components
+    return (
+        <div className={containerClassName}>
             <FormField
                 control={control}
                 name={fieldName}
-                render={fieldProps => (
+                render={({ field }) => (
                     <CustomFieldFormItem
                         fieldDef={fieldDef}
                         getTranslation={getTranslation}
-                        fieldName={fieldProps.field.name}
+                        fieldName={field.name}
                     >
-                        <CustomFormComponent
-                            fieldDef={fieldDef}
-                            fieldProps={{
-                                ...fieldProps,
-                                field: {
-                                    ...fieldProps.field,
-                                    disabled: fieldDef.readonly ?? false,
-                                },
-                            }}
-                        />
+                        <FormInputForType fieldDef={fieldDef} field={field} />
                     </CustomFieldFormItem>
                 )}
             />
-        );
-    }
-
-    // For regular fields without custom components
-    return (
-        <FormField
-            control={control}
-            name={fieldName}
-            render={({ field }) => (
-                <CustomFieldFormItem
-                    fieldDef={fieldDef}
-                    getTranslation={getTranslation}
-                    fieldName={field.name}
-                >
-                    <FormInputForType fieldDef={fieldDef} field={field} />
-                </CustomFieldFormItem>
-            )}
-        />
+        </div>
     );
 }
 

+ 2 - 1
packages/dev-server/test-plugins/reviews/reviews-plugin.ts

@@ -54,7 +54,7 @@ import { ProductReview } from './entities/product-review.entity';
             public: true,
             type: 'relation',
             entity: ProductReview,
-            ui: { tab: 'Reviews', component: 'review-selector-form-input' },
+            ui: { tab: 'Reviews', component: 'review-selector-form-input', fullWidth: true },
             inverseSide: undefined,
         });
         config.customFields.Product.push({
@@ -62,6 +62,7 @@ import { ProductReview } from './entities/product-review.entity';
             label: [{ languageCode: LanguageCode.en, value: 'Translatable text' }],
             public: true,
             type: 'localeText',
+            ui: { tab: 'Reviews' },
         });
 
         config.customFields.ProductReview = [