Просмотр исходного кода

fix(dashboard): Implement entity duplicator dialog

Michael Bromley 4 месяцев назад
Родитель
Сommit
0328d46289

+ 37 - 23
packages/dashboard/src/app/common/duplicate-bulk-action.tsx

@@ -1,4 +1,5 @@
 import { useMutation } from '@tanstack/react-query';
+import { ConfigurableOperationInput } from '@vendure/common/lib/generated-types';
 import { CopyIcon } from 'lucide-react';
 import { useState } from 'react';
 import { toast } from 'sonner';
@@ -8,13 +9,13 @@ import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-ta
 import { api } from '@/vdb/graphql/api.js';
 import { duplicateEntityDocument } from '@/vdb/graphql/common-operations.js';
 import { Trans, useLingui } from '@/vdb/lib/trans.js';
+import { DuplicateEntityDialog } from './duplicate-entity-dialog.js';
 
 interface DuplicateBulkActionProps {
     entityType: 'Product' | 'Collection' | 'Facet' | 'Promotion';
     duplicatorCode: string;
-    duplicatorArguments?: Array<{ name: string; value: string }>;
     requiredPermissions: string[];
-    entityName: string; // For display purposes in error messages
+    entityName: string;
     onSuccess?: () => void;
     selection: any[];
     table: any;
@@ -23,23 +24,28 @@ interface DuplicateBulkActionProps {
 export function DuplicateBulkAction({
     entityType,
     duplicatorCode,
-    duplicatorArguments = [],
     requiredPermissions,
     entityName,
     onSuccess,
     selection,
     table,
-}: DuplicateBulkActionProps) {
+}: Readonly<DuplicateBulkActionProps>) {
     const { refetchPaginatedList } = usePaginatedList();
     const { i18n } = useLingui();
     const [isDuplicating, setIsDuplicating] = useState(false);
     const [progress, setProgress] = useState({ completed: 0, total: 0 });
+    const [dialogOpen, setDialogOpen] = useState(false);
 
     const { mutateAsync } = useMutation({
         mutationFn: api.mutate(duplicateEntityDocument),
     });
 
-    const handleDuplicate = async () => {
+    const handleStartDuplication = () => {
+        if (isDuplicating) return;
+        setDialogOpen(true);
+    };
+
+    const handleConfirmDuplication = async (duplicatorInput: ConfigurableOperationInput) => {
         if (isDuplicating) return;
 
         setIsDuplicating(true);
@@ -61,10 +67,7 @@ export function DuplicateBulkAction({
                         input: {
                             entityName: entityType,
                             entityId: entity.id,
-                            duplicatorInput: {
-                                code: duplicatorCode,
-                                arguments: duplicatorArguments,
-                            },
+                            duplicatorInput,
                         },
                     });
 
@@ -116,19 +119,30 @@ export function DuplicateBulkAction({
     };
 
     return (
-        <DataTableBulkActionItem
-            requiresPermission={requiredPermissions}
-            onClick={handleDuplicate}
-            label={
-                isDuplicating ? (
-                    <Trans>
-                        Duplicating... ({progress.completed}/{progress.total})
-                    </Trans>
-                ) : (
-                    <Trans>Duplicate</Trans>
-                )
-            }
-            icon={CopyIcon}
-        />
+        <>
+            <DataTableBulkActionItem
+                requiresPermission={requiredPermissions}
+                onClick={handleStartDuplication}
+                label={
+                    isDuplicating ? (
+                        <Trans>
+                            Duplicating... ({progress.completed}/{progress.total})
+                        </Trans>
+                    ) : (
+                        <Trans>Duplicate</Trans>
+                    )
+                }
+                icon={CopyIcon}
+            />
+            <DuplicateEntityDialog
+                open={dialogOpen}
+                onOpenChange={setDialogOpen}
+                entityType={entityType}
+                entityName={entityName}
+                entities={selection}
+                duplicatorCode={duplicatorCode}
+                onConfirm={handleConfirmDuplication}
+            />
+        </>
     );
 }

+ 117 - 0
packages/dashboard/src/app/common/duplicate-entity-dialog.tsx

@@ -0,0 +1,117 @@
+import { ConfigurableOperationInput as ConfigurableOperationInputComponent } from '@/vdb/components/shared/configurable-operation-input.js';
+import { Button } from '@/vdb/components/ui/button.js';
+import {
+    Dialog,
+    DialogContent,
+    DialogFooter,
+    DialogHeader,
+    DialogTitle,
+} from '@/vdb/components/ui/dialog.js';
+import { api } from '@/vdb/graphql/api.js';
+import { getEntityDuplicatorsDocument } from '@/vdb/graphql/common-operations.js';
+import { Trans } from '@/vdb/lib/trans.js';
+import { useQuery } from '@tanstack/react-query';
+import { ConfigurableOperationInput } from '@vendure/common/lib/generated-types';
+import React, { useState } from 'react';
+
+interface DuplicateEntityDialogProps {
+    open: boolean;
+    onOpenChange: (open: boolean) => void;
+    entityType: 'Product' | 'Collection' | 'Facet' | 'Promotion';
+    entityName: string;
+    entities: Array<{ id: string; name?: string }>;
+    duplicatorCode: string;
+    onConfirm: (duplicatorInput: ConfigurableOperationInput) => void;
+}
+
+export function DuplicateEntityDialog({
+    open,
+    onOpenChange,
+    entityType,
+    entityName,
+    duplicatorCode,
+    onConfirm,
+}: Readonly<DuplicateEntityDialogProps>) {
+    const [selectedDuplicator, setSelectedDuplicator] = useState<ConfigurableOperationInput | undefined>();
+
+    const { data } = useQuery({
+        queryKey: ['entityDuplicators'],
+        queryFn: () => api.query(getEntityDuplicatorsDocument),
+        staleTime: 1000 * 60 * 60 * 5,
+    });
+
+    // Find the duplicator that matches the provided code and supports this entity type
+    const matchingDuplicator = data?.entityDuplicators?.find(
+        duplicator => duplicator.code === duplicatorCode && duplicator.forEntities.includes(entityType),
+    );
+
+    // Auto-initialize the duplicator when found
+    React.useEffect(() => {
+        if (matchingDuplicator && !selectedDuplicator) {
+            setSelectedDuplicator({
+                code: matchingDuplicator.code,
+                arguments:
+                    matchingDuplicator.args?.map(arg => ({
+                        name: arg.name,
+                        value: arg.defaultValue != null ? arg.defaultValue.toString() : '',
+                    })) || [],
+            });
+        }
+    }, [matchingDuplicator, selectedDuplicator]);
+
+    const onDuplicatorValueChange = (newVal: ConfigurableOperationInput) => {
+        setSelectedDuplicator(newVal);
+    };
+
+    const handleConfirm = () => {
+        if (!selectedDuplicator) return;
+        onConfirm(selectedDuplicator);
+        onOpenChange(false);
+        setSelectedDuplicator(undefined);
+    };
+
+    const handleCancel = () => {
+        onOpenChange(false);
+        setSelectedDuplicator(undefined);
+    };
+
+    return (
+        <Dialog open={open} onOpenChange={onOpenChange}>
+            <DialogContent className="sm:max-w-lg">
+                <DialogHeader>
+                    <DialogTitle>
+                        <Trans>Duplicate {entityName.toLowerCase()}s</Trans>
+                    </DialogTitle>
+                </DialogHeader>
+
+                <div className="space-y-4">
+                    {selectedDuplicator && matchingDuplicator && (
+                        <ConfigurableOperationInputComponent
+                            operationDefinition={matchingDuplicator}
+                            value={selectedDuplicator}
+                            onChange={onDuplicatorValueChange}
+                            removable={false}
+                        />
+                    )}
+
+                    {!matchingDuplicator && (
+                        <div className="text-sm text-muted-foreground">
+                            <Trans>
+                                No duplicator found with code "{duplicatorCode}" for {entityName}s
+                            </Trans>
+                        </div>
+                    )}
+                </div>
+
+                <DialogFooter>
+                    <Button variant="outline" onClick={handleCancel}>
+                        <Trans>Cancel</Trans>
+                    </Button>
+                    <Button onClick={handleConfirm} disabled={!selectedDuplicator}>
+                        <Trans>Duplicate</Trans>
+                    </Button>
+                </DialogFooter>
+            </DialogContent>
+        </Dialog>
+    );
+}

+ 19 - 0
packages/dashboard/src/lib/graphql/common-operations.ts

@@ -1,3 +1,5 @@
+import { configArgDefinitionFragment } from '@/vdb/graphql/fragments.js';
+
 import { graphql } from './graphql.js';
 
 export const duplicateEntityDocument = graphql(`
@@ -16,3 +18,20 @@ export const duplicateEntityDocument = graphql(`
         }
     }
 `);
+
+export const getEntityDuplicatorsDocument = graphql(
+    `
+        query GetEntityDuplicators {
+            entityDuplicators {
+                code
+                description
+                requiresPermission
+                forEntities
+                args {
+                    ...ConfigArgDefinition
+                }
+            }
+        }
+    `,
+    [configArgDefinitionFragment],
+);

+ 23 - 13
packages/dashboard/src/lib/graphql/fragments.ts

@@ -34,23 +34,32 @@ export const configurableOperationFragment = graphql(`
 
 export type ConfigurableOperationFragment = ResultOf<typeof configurableOperationFragment>;
 
-export const configurableOperationDefFragment = graphql(`
-    fragment ConfigurableOperationDef on ConfigurableOperationDefinition {
-        args {
-            name
-            type
-            required
-            defaultValue
-            list
-            ui
-            label
-            description
-        }
-        code
+export const configArgDefinitionFragment = graphql(`
+    fragment ConfigArgDefinition on ConfigArgDefinition {
+        name
+        type
+        required
+        defaultValue
+        list
+        ui
+        label
         description
     }
 `);
 
+export const configurableOperationDefFragment = graphql(
+    `
+        fragment ConfigurableOperationDef on ConfigurableOperationDefinition {
+            args {
+                ...ConfigArgDefinition
+            }
+            code
+            description
+        }
+    `,
+    [configArgDefinitionFragment],
+);
+
 export const errorResultFragment = graphql(`
     fragment ErrorResult on ErrorResult {
         errorCode
@@ -59,3 +68,4 @@ export const errorResultFragment = graphql(`
 `);
 
 export type ConfigurableOperationDefFragment = ResultOf<typeof configurableOperationDefFragment>;
+export type ConfigArgDefFragment = ResultOf<typeof configArgDefinitionFragment>;