소스 검색

fix(dashboard): Display custom fields on product variant prices (#4180)

Co-authored-by: Michael Bromley <michael@michaelbromley.co.uk>

(cherry picked from commit d34804cdfd5a990982e2eb189cd5a9959fa5d94d)
mehringer68 1 일 전
부모
커밋
11192d3a34

+ 9 - 3
packages/dashboard/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts

@@ -32,6 +32,13 @@ export const productVariantListDocument = graphql(
     [assetFragment],
 );
 
+export const productVariantPriceFragment = graphql(`
+    fragment ProductVariantPrice on ProductVariantPrice {
+        currencyCode
+        price
+    }
+`);
+
 export const productVariantDetailDocument = graphql(
     `
         query ProductVariantDetail($id: ID!) {
@@ -86,8 +93,7 @@ export const productVariantDetailDocument = graphql(
                 price
                 priceWithTax
                 prices {
-                    currencyCode
-                    price
+                    ...ProductVariantPrice
                 }
                 trackInventory
                 outOfStockThreshold
@@ -105,7 +111,7 @@ export const productVariantDetailDocument = graphql(
             }
         }
     `,
-    [assetFragment],
+    [assetFragment, productVariantPriceFragment],
 );
 
 export const createProductVariantDocument = graphql(`

+ 49 - 30
packages/dashboard/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx

@@ -1,6 +1,7 @@
 import { MoneyInput } from '@/vdb/components/data-input/money-input.js';
 import { NumberInput } from '@/vdb/components/data-input/number-input.js';
 import { AssignedFacetValues } from '@/vdb/components/shared/assigned-facet-values.js';
+import { CustomFieldsForm } from '@/vdb/components/shared/custom-fields-form.js';
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
 import { EntityAssets } from '@/vdb/components/shared/entity-assets.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
@@ -12,8 +13,10 @@ import { Button } from '@/vdb/components/ui/button.js';
 import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '@/vdb/components/ui/form.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
+import { Separator } from '@/vdb/components/ui/separator.js';
 import { Switch } from '@/vdb/components/ui/switch.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
+import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
 import {
     CustomFieldsPageBlock,
     DetailFormGrid,
@@ -34,6 +37,7 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router';
 import { VariablesOf } from 'gql.tada';
 import { Trash } from 'lucide-react';
 import { toast } from 'sonner';
+
 import { AddCurrencyDropdown } from './components/add-currency-dropdown.js';
 import { AddStockLocationDropdown } from './components/add-stock-location-dropdown.js';
 import { VariantPriceDetail } from './components/variant-price-detail.js';
@@ -50,7 +54,10 @@ export const Route = createFileRoute('/_authenticated/_product-variants/product-
     component: ProductVariantDetailPage,
     loader: detailPageRouteLoader({
         pageId,
-        queryDocument: productVariantDetailDocument,
+        queryDocument: () =>
+            addCustomFields(productVariantDetailDocument, {
+                includeNestedFragments: ['ProductVariantPrice'],
+            }),
         breadcrumb(_isNew, entity, location) {
             if ((location.search as any).from === 'product') {
                 return [
@@ -81,7 +88,9 @@ function ProductVariantDetailPage() {
 
     const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
         pageId,
-        queryDocument: productVariantDetailDocument,
+        queryDocument: addCustomFields(productVariantDetailDocument, {
+            includeNestedFragments: ['ProductVariantPrice'],
+        }),
         createDocument: createProductVariantDocument,
         updateDocument: updateProductVariantDocument,
         setValuesForUpdate: entity => {
@@ -169,6 +178,7 @@ function ProductVariantDetailPage() {
                 currencyCode,
                 price: 0,
                 delete: false,
+                customFields: {},
             } as PriceInput;
             form.setValue('prices', [...currentPrices, newPrice], {
                 shouldDirty: true,
@@ -274,37 +284,46 @@ function ProductVariantDetailPage() {
                             </div>
                         );
                         return (
-                            <DetailFormGrid key={price.currencyCode}>
-                                <div className="flex gap-1 items-end">
-                                    <FormFieldWrapper
-                                        control={form.control}
-                                        name={`prices.${actualIndex}.price`}
-                                        label={priceLabel}
-                                        render={({ field }) => (
-                                            <MoneyInput {...field} currency={price.currencyCode} />
+                            <div key={price.currencyCode} className="space-y-6">
+                                {displayIndex > 0 && <Separator className="my-4" />}
+                                <DetailFormGrid key={price.currencyCode}>
+                                    <div className="flex gap-1 items-end">
+                                        <FormFieldWrapper
+                                            control={form.control}
+                                            name={`prices.${actualIndex}.price`}
+                                            label={priceLabel}
+                                            render={({ field }) => (
+                                                <MoneyInput {...field} currency={price.currencyCode} />
+                                            )}
+                                        />
+                                        {activePrices.length > 1 && (
+                                            <Button
+                                                type="button"
+                                                variant="ghost"
+                                                size="sm"
+                                                onClick={() => handleRemoveCurrency(actualIndex)}
+                                                className="h-6 w-6 p-0 mb-2 hover:text-destructive hover:bg-destructive-100"
+                                            >
+                                                <Trash className="size-4" />
+                                            </Button>
                                         )}
+                                    </div>
+                                    <VariantPriceDetail
+                                        priceIncludesTax={activeChannel?.pricesIncludeTax ?? false}
+                                        price={price.price}
+                                        currencyCode={
+                                            price.currencyCode ?? activeChannel?.defaultCurrencyCode ?? ''
+                                        }
+                                        taxCategoryId={taxCategoryId}
                                     />
-                                    {activePrices.length > 1 && (
-                                        <Button
-                                            type="button"
-                                            variant="ghost"
-                                            size="sm"
-                                            onClick={() => handleRemoveCurrency(actualIndex)}
-                                            className="h-6 w-6 p-0 mb-2 hover:text-destructive hover:bg-destructive-100"
-                                        >
-                                            <Trash className="size-4" />
-                                        </Button>
-                                    )}
-                                </div>
-                                <VariantPriceDetail
-                                    priceIncludesTax={activeChannel?.pricesIncludeTax ?? false}
-                                    price={price.price}
-                                    currencyCode={
-                                        price.currencyCode ?? activeChannel?.defaultCurrencyCode ?? ''
-                                    }
-                                    taxCategoryId={taxCategoryId}
+                                </DetailFormGrid>
+                                {/* Custom fields for ProductVariantPrice */}
+                                <CustomFieldsForm
+                                    entityType="ProductVariantPrice"
+                                    control={form.control}
+                                    formPathPrefix={`prices.${actualIndex}`}
                                 />
-                            </DetailFormGrid>
+                            </div>
                         );
                     })}
                     {unusedCurrencies.length ? (

+ 1 - 0
packages/dashboard/src/lib/components/data-input/index.ts

@@ -4,6 +4,7 @@ export * from './customer-group-input.js';
 export * from './datetime-input.js';
 export * from './facet-value-input.js';
 export * from './money-input.js';
+export * from './number-input.js';
 export * from './rich-text-input.js';
 export * from './select-with-options.js';
 

+ 2 - 0
packages/dashboard/src/lib/framework/extension-api/input-component-extensions.tsx

@@ -4,6 +4,7 @@ import {
     CustomerGroupInput,
     FacetValueInput,
     MoneyInput,
+    NumberInput,
     ProductMultiInput,
     RichTextInput,
     SelectWithOptions,
@@ -39,6 +40,7 @@ inputComponents.set('relation-form-input', DefaultRelationInput);
 inputComponents.set('select-form-input', SelectWithOptions);
 inputComponents.set('product-multi-form-input', ProductMultiInput);
 inputComponents.set('combination-mode-form-input', CombinationModeInput);
+inputComponents.set('number-form-input', NumberInput);
 inputComponents.set('text-form-input', TextInput);
 
 export function getInputComponent(id: string | undefined): DashboardFormComponent | undefined {

+ 4 - 1
packages/dashboard/src/lib/framework/form-engine/form-schema-tools.ts

@@ -309,7 +309,10 @@ export function createFormSchemaFromFields(
                 customFieldConfigs && customFieldConfigs.length > 0
                     ? processCustomFieldsSchema(customFieldConfigs, isTranslationContext)
                     : {};
-            schemaConfig[field.name] = z.object(customFieldsSchema).optional();
+            // Use .passthrough() to preserve custom field values that aren't in the schema
+            // This is essential for nested entities (e.g., ProductVariantPrice) whose custom
+            // field configs aren't passed to the parent form schema builder
+            schemaConfig[field.name] = z.object(customFieldsSchema).passthrough().optional();
         } else if (field.typeInfo) {
             const isNestedTranslationContext = field.name === 'translations' || isTranslationContext;
             let nestedType: ZodType = createFormSchemaFromFields(

+ 6 - 4
packages/dashboard/src/lib/framework/page/detail-page-route-loader.tsx

@@ -16,7 +16,7 @@ export interface DetailPageRouteLoaderConfig<T extends TypedDocumentNode<any, an
      * the detail query document) get correctly applied at the route loader level.
      */
     pageId?: string;
-    queryDocument: T;
+    queryDocument: T | (() => T);
     breadcrumb: (
         isNew: boolean,
         entity: DetailEntity<T>,
@@ -38,12 +38,14 @@ export function detailPageRouteLoader<T extends TypedDocumentNode<any, any>>({
         params: any;
         location: ParsedLocation;
     }) => {
+        const resolvedQueryDocument = typeof queryDocument === 'function' ? queryDocument() : queryDocument;
+
         if (!params.id) {
             throw new Error('ID param is required');
         }
         const isNew = params.id === NEW_ENTITY_PATH;
         const { extendedQuery: extendedQueryDocument } = extendDetailFormQuery(
-            addCustomFields(queryDocument),
+            addCustomFields(resolvedQueryDocument),
             pageId,
         );
         const result = isNew
@@ -53,8 +55,8 @@ export function detailPageRouteLoader<T extends TypedDocumentNode<any, any>>({
                   { id: params.id },
               );
 
-        const entityField = getQueryName(queryDocument);
-        const entityName = getQueryTypeFieldInfo(queryDocument)?.type;
+        const entityField = getQueryName(resolvedQueryDocument);
+        const entityName = getQueryTypeFieldInfo(resolvedQueryDocument)?.type;
 
         if (!isNew && !result[entityField]) {
             throw new Error(`${entityName} with the ID ${params.id} was not found`);

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 9 - 0
packages/dashboard/src/lib/graphql/graphql-env.d.ts


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.