Add required fields display to match columns step
This commit is contained in:
@@ -35,6 +35,13 @@ import {
|
|||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import config from "@/config"
|
import config from "@/config"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { CheckCircle2, AlertCircle, HelpCircle } from "lucide-react"
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip"
|
||||||
|
|
||||||
export type MatchColumnsProps<T extends string> = {
|
export type MatchColumnsProps<T extends string> = {
|
||||||
data: RawData[]
|
data: RawData[]
|
||||||
@@ -120,13 +127,12 @@ export const MatchColumnsStep = <T extends string>({
|
|||||||
initialGlobalSelections
|
initialGlobalSelections
|
||||||
}: MatchColumnsProps<T>) => {
|
}: MatchColumnsProps<T>) => {
|
||||||
const dataExample = data.slice(0, 2)
|
const dataExample = data.slice(0, 2)
|
||||||
const { fields, autoMapHeaders, autoMapSelectValues, autoMapDistance, translations, allowInvalidSubmit } = useRsi<T>()
|
const { fields, autoMapHeaders, autoMapSelectValues, autoMapDistance, translations } = useRsi<T>()
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [columns, setColumns] = useState<Columns<T>>(
|
const [columns, setColumns] = useState<Columns<T>>(
|
||||||
// Do not remove spread, it indexes empty array elements, otherwise map() skips over them
|
// Do not remove spread, it indexes empty array elements, otherwise map() skips over them
|
||||||
([...headerValues] as string[]).map((value, index) => ({ type: ColumnType.empty, index, header: value ?? "" })),
|
([...headerValues] as string[]).map((value, index) => ({ type: ColumnType.empty, index, header: value ?? "" })),
|
||||||
)
|
)
|
||||||
const [showUnmatchedFieldsAlert, setShowUnmatchedFieldsAlert] = useState(false)
|
|
||||||
const [globalSelections, setGlobalSelections] = useState<GlobalSelections>(initialGlobalSelections || {})
|
const [globalSelections, setGlobalSelections] = useState<GlobalSelections>(initialGlobalSelections || {})
|
||||||
|
|
||||||
// Initialize with any provided global selections
|
// Initialize with any provided global selections
|
||||||
@@ -237,6 +243,46 @@ export const MatchColumnsStep = <T extends string>({
|
|||||||
},
|
},
|
||||||
[columns, setColumns],
|
[columns, setColumns],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Get all required fields
|
||||||
|
const requiredFields = useMemo(() => {
|
||||||
|
// Convert the fields to the expected type
|
||||||
|
const fieldsArray = Array.isArray(fields) ? fields : [fields];
|
||||||
|
|
||||||
|
// Log the fields for debugging
|
||||||
|
console.log("All fields:", fieldsArray);
|
||||||
|
|
||||||
|
// Log validation rules for each field
|
||||||
|
fieldsArray.forEach(field => {
|
||||||
|
if (field.validations && Array.isArray(field.validations)) {
|
||||||
|
console.log(`Field ${field.key} validations:`, field.validations.map((v: any) => v.rule));
|
||||||
|
} else {
|
||||||
|
console.log(`Field ${field.key} has no validations`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for required fields based on validations
|
||||||
|
const required = fieldsArray.filter(field => {
|
||||||
|
// Check if the field has validations
|
||||||
|
if (!field.validations || !Array.isArray(field.validations)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for any validation with rule: 'required' or required: true
|
||||||
|
const isRequired = field.validations.some(
|
||||||
|
(v: any) => v.rule === 'required' || v.required === true
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Field ${field.key} required:`, isRequired, field.validations);
|
||||||
|
|
||||||
|
return isRequired;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Required fields:", required);
|
||||||
|
return required;
|
||||||
|
}, [fields]);
|
||||||
|
|
||||||
|
// Get unmatched required fields
|
||||||
const unmatchedRequiredFields = useMemo(() => {
|
const unmatchedRequiredFields = useMemo(() => {
|
||||||
// Convert the fields to the expected type
|
// Convert the fields to the expected type
|
||||||
const fieldsArray = Array.isArray(fields) ? fields : [fields]
|
const fieldsArray = Array.isArray(fields) ? fields : [fields]
|
||||||
@@ -245,26 +291,46 @@ export const MatchColumnsStep = <T extends string>({
|
|||||||
key: field.key as TsDeepReadonly<T>
|
key: field.key as TsDeepReadonly<T>
|
||||||
})) as unknown as Fields<T>
|
})) as unknown as Fields<T>
|
||||||
|
|
||||||
return findUnmatchedRequiredFields(typedFields, columns)
|
const unmatched = findUnmatchedRequiredFields(typedFields, columns);
|
||||||
|
console.log("Unmatched required fields:", unmatched);
|
||||||
|
return unmatched;
|
||||||
}, [fields, columns])
|
}, [fields, columns])
|
||||||
|
|
||||||
|
// Get matched required fields
|
||||||
|
const matchedRequiredFields = useMemo(() => {
|
||||||
|
const matched = requiredFields
|
||||||
|
.map(field => field.key)
|
||||||
|
.filter(key => {
|
||||||
|
// Type assertion to handle the DeepReadonly<T> vs string type mismatch
|
||||||
|
return !unmatchedRequiredFields.includes(key as any);
|
||||||
|
});
|
||||||
|
console.log("Matched required fields:", matched);
|
||||||
|
return matched;
|
||||||
|
}, [requiredFields, unmatchedRequiredFields]);
|
||||||
|
|
||||||
|
// Get field label by key
|
||||||
|
const getFieldLabel = useCallback((key: string) => {
|
||||||
|
const fieldsArray = Array.isArray(fields) ? fields : [fields];
|
||||||
|
const field = fieldsArray.find(f => f.key === key);
|
||||||
|
return field?.label || key;
|
||||||
|
}, [fields]);
|
||||||
|
|
||||||
|
// Check if a field is covered by global selections
|
||||||
|
const isFieldCoveredByGlobalSelections = useCallback((key: string) => {
|
||||||
|
const isCovered = (key === 'supplier' && globalSelections.supplier) ||
|
||||||
|
(key === 'company' && globalSelections.company) ||
|
||||||
|
(key === 'line' && globalSelections.line) ||
|
||||||
|
(key === 'subline' && globalSelections.subline);
|
||||||
|
console.log(`Field ${key} covered by global selections:`, isCovered);
|
||||||
|
return isCovered;
|
||||||
|
}, [globalSelections]);
|
||||||
|
|
||||||
const handleOnContinue = useCallback(async () => {
|
const handleOnContinue = useCallback(async () => {
|
||||||
if (unmatchedRequiredFields.length > 0) {
|
|
||||||
setShowUnmatchedFieldsAlert(true)
|
|
||||||
} else {
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
// Normalize the data with global selections before continuing
|
// Normalize the data with global selections before continuing
|
||||||
const normalizedData = normalizeTableData(columns, data, fields)
|
const normalizedData = normalizeTableData(columns, data, fields)
|
||||||
await onContinue(normalizedData, data, columns, globalSelections)
|
await onContinue(normalizedData, data, columns, globalSelections)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
|
||||||
}, [unmatchedRequiredFields.length, onContinue, columns, data, fields, globalSelections])
|
|
||||||
|
|
||||||
const handleAlertOnContinue = useCallback(async () => {
|
|
||||||
setShowUnmatchedFieldsAlert(false)
|
|
||||||
setIsLoading(true)
|
|
||||||
await onContinue(normalizeTableData(columns, data, fields), data, columns, globalSelections)
|
|
||||||
setIsLoading(false)
|
|
||||||
}, [onContinue, columns, data, fields, globalSelections])
|
}, [onContinue, columns, data, fields, globalSelections])
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
@@ -278,45 +344,11 @@ export const MatchColumnsStep = <T extends string>({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<AlertDialog open={showUnmatchedFieldsAlert} onOpenChange={setShowUnmatchedFieldsAlert}>
|
|
||||||
<AlertDialogPortal>
|
|
||||||
<AlertDialogOverlay className="z-[1400]" />
|
|
||||||
<AlertDialogContent className="z-[1500]">
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>
|
|
||||||
{translations.alerts.unmatchedRequiredFields.headerTitle}
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<AlertDialogDescription>
|
|
||||||
{translations.alerts.unmatchedRequiredFields.bodyText}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{translations.alerts.unmatchedRequiredFields.listTitle}{" "}
|
|
||||||
<span className="font-bold">
|
|
||||||
{unmatchedRequiredFields.join(", ")}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>
|
|
||||||
{translations.alerts.unmatchedRequiredFields.cancelButtonTitle}
|
|
||||||
</AlertDialogCancel>
|
|
||||||
{allowInvalidSubmit && (
|
|
||||||
<AlertDialogAction onClick={handleAlertOnContinue}>
|
|
||||||
{translations.alerts.unmatchedRequiredFields.continueButtonTitle}
|
|
||||||
</AlertDialogAction>
|
|
||||||
)}
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialogPortal>
|
|
||||||
</AlertDialog>
|
|
||||||
<div className="flex h-[calc(100vh-10rem)] flex-col">
|
<div className="flex h-[calc(100vh-10rem)] flex-col">
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="px-8 py-6 flex-1 overflow-auto">
|
<div className="px-8 py-6 flex-1 overflow-auto">
|
||||||
<div className="space-y-8">
|
<div className="space-y-8 pb-4">
|
||||||
{/* Global Selections Section */}
|
{/* Global Selections Section */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -435,6 +467,59 @@ export const MatchColumnsStep = <T extends string>({
|
|||||||
templateColumn={(column) => <TemplateColumn column={column} onChange={onChange} onSubChange={onSubChange} />}
|
templateColumn={(column) => <TemplateColumn column={column} onChange={onChange} onSubChange={onSubChange} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Required Fields Checklist */}
|
||||||
|
<Card className="mt-8">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
Required Fields
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="max-w-xs">
|
||||||
|
These fields are required for product import. You can either map them from your spreadsheet or set them globally above.
|
||||||
|
{unmatchedRequiredFields.length > 0 && " Missing fields will need to be filled in during validation."}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{requiredFields.length > 0 ? (
|
||||||
|
requiredFields.map(field => {
|
||||||
|
const isMatched = matchedRequiredFields.includes(field.key);
|
||||||
|
const isCoveredByGlobal = isFieldCoveredByGlobalSelections(field.key);
|
||||||
|
const isAccountedFor = isMatched || isCoveredByGlobal;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={field.key} className="flex items-center gap-2">
|
||||||
|
{isAccountedFor ? (
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="h-5 w-5 text-amber-500" />
|
||||||
|
)}
|
||||||
|
<span className={isAccountedFor ? "text-green-700 dark:text-green-400" : "text-amber-700 dark:text-amber-400"}>
|
||||||
|
{getFieldLabel(field.key)}
|
||||||
|
</span>
|
||||||
|
{isCoveredByGlobal && (
|
||||||
|
<span className="text-xs text-muted-foreground">(set globally)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="col-span-2 text-muted-foreground">
|
||||||
|
No required fields found in the configuration.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -446,6 +531,12 @@ export const MatchColumnsStep = <T extends string>({
|
|||||||
{translations.matchColumnsStep.backButtonTitle}
|
{translations.matchColumnsStep.backButtonTitle}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{unmatchedRequiredFields.length > 0 && (
|
||||||
|
<span className="text-sm text-amber-600 dark:text-amber-400">
|
||||||
|
{unmatchedRequiredFields.length} required field(s) missing
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
className="ml-auto"
|
className="ml-auto"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@@ -456,6 +547,6 @@ export const MatchColumnsStep = <T extends string>({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import type { Fields } from "../../../types"
|
import type { Fields } from "../../../types"
|
||||||
import type { Columns } from "../MatchColumnsStep"
|
import type { Columns } from "../MatchColumnsStep"
|
||||||
|
|
||||||
export const findUnmatchedRequiredFields = <T extends string>(fields: Fields<T>, columns: Columns<T>) =>
|
export const findUnmatchedRequiredFields = <T extends string>(fields: Fields<T>, columns: Columns<T>) => {
|
||||||
fields
|
// Get all required fields
|
||||||
.filter((field) => field.validations?.some((validation) => validation.rule === "required"))
|
const requiredFields = fields
|
||||||
|
.filter((field) => field.validations?.some((validation: any) =>
|
||||||
|
validation.rule === "required" || validation.required === true
|
||||||
|
))
|
||||||
|
|
||||||
|
// Find which required fields are not matched in columns
|
||||||
|
return requiredFields
|
||||||
.filter((field) => columns.findIndex((column) => "value" in column && column.value === field.key) === -1)
|
.filter((field) => columns.findIndex((column) => "value" in column && column.value === field.key) === -1)
|
||||||
.map((field) => field.label) || []
|
.map((field) => field.key) || []
|
||||||
|
}
|
||||||
|
|||||||
@@ -108,10 +108,7 @@ export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps
|
|||||||
removedRows.push({
|
removedRows.push({
|
||||||
index,
|
index,
|
||||||
reason: "Duplicate",
|
reason: "Duplicate",
|
||||||
row,
|
row
|
||||||
normalizedRow,
|
|
||||||
rowStr,
|
|
||||||
headerStr: selectedHeaderStr
|
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -151,28 +148,30 @@ export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps
|
|||||||
}, [localData, selectedRows, toast]);
|
}, [localData, selectedRows, toast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col h-[calc(100vh-9.5rem)]">
|
||||||
<div className="px-8 py-6">
|
<div className="px-8 py-6 bg-background">
|
||||||
<h2 className="text-2xl font-semibold text-foreground">
|
<h2 className="text-2xl font-semibold text-foreground">
|
||||||
{translations.selectHeaderStep.title}
|
{translations.selectHeaderStep.title}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 px-8 mb-12 overflow-auto">
|
<div className="flex-1 flex flex-col min-h-0">
|
||||||
<div className="flex justify-end mb-4">
|
<div className="px-8 mb-4 flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={discardEmptyAndDuplicateRows}
|
onClick={discardEmptyAndDuplicateRows}
|
||||||
>
|
>
|
||||||
Remove Empty/Duplicates
|
Remove Empty/Duplicates
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="px-8 flex-1 overflow-auto">
|
||||||
<SelectHeaderTable
|
<SelectHeaderTable
|
||||||
data={localData}
|
data={localData}
|
||||||
selectedRows={selectedRows}
|
selectedRows={selectedRows}
|
||||||
setSelectedRows={setSelectedRows}
|
setSelectedRows={setSelectedRows}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex items-center justify-between border-t bg-muted px-8 py-4 mt-2">
|
<div className="flex items-center justify-between border-t bg-muted px-8 py-4 mt-2">
|
||||||
{onBack && (
|
{onBack && (
|
||||||
<Button variant="outline" onClick={onBack}>
|
<Button variant="outline" onClick={onBack}>
|
||||||
|
|||||||
Reference in New Issue
Block a user