Add required fields display to match columns step

This commit is contained in:
2025-02-24 22:31:57 -05:00
parent 6bf93d33ea
commit 54a87ca3dc
3 changed files with 294 additions and 197 deletions

View File

@@ -35,6 +35,13 @@ import {
import { useQuery } from "@tanstack/react-query"
import config from "@/config"
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> = {
data: RawData[]
@@ -120,13 +127,12 @@ export const MatchColumnsStep = <T extends string>({
initialGlobalSelections
}: MatchColumnsProps<T>) => {
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 [columns, setColumns] = useState<Columns<T>>(
// 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 ?? "" })),
)
const [showUnmatchedFieldsAlert, setShowUnmatchedFieldsAlert] = useState(false)
const [globalSelections, setGlobalSelections] = useState<GlobalSelections>(initialGlobalSelections || {})
// Initialize with any provided global selections
@@ -237,6 +243,46 @@ export const MatchColumnsStep = <T extends string>({
},
[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(() => {
// Convert the fields to the expected type
const fieldsArray = Array.isArray(fields) ? fields : [fields]
@@ -245,26 +291,46 @@ export const MatchColumnsStep = <T extends string>({
key: field.key as TsDeepReadonly<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])
// 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 () => {
if (unmatchedRequiredFields.length > 0) {
setShowUnmatchedFieldsAlert(true)
} else {
setIsLoading(true)
// Normalize the data with global selections before continuing
const normalizedData = normalizeTableData(columns, data, fields)
await onContinue(normalizedData, data, columns, globalSelections)
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])
useEffect(
@@ -278,45 +344,11 @@ export const MatchColumnsStep = <T extends string>({
)
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-1 overflow-hidden">
<div className="h-full flex flex-col">
<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 */}
<Card>
<CardHeader>
@@ -435,6 +467,59 @@ export const MatchColumnsStep = <T extends string>({
templateColumn={(column) => <TemplateColumn column={column} onChange={onChange} onSubChange={onSubChange} />}
/>
</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>
@@ -446,6 +531,12 @@ export const MatchColumnsStep = <T extends string>({
{translations.matchColumnsStep.backButtonTitle}
</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
className="ml-auto"
disabled={isLoading}
@@ -456,6 +547,6 @@ export const MatchColumnsStep = <T extends string>({
</div>
</div>
</div>
</>
</div>
)
}

View File

@@ -1,8 +1,15 @@
import type { Fields } from "../../../types"
import type { Columns } from "../MatchColumnsStep"
export const findUnmatchedRequiredFields = <T extends string>(fields: Fields<T>, columns: Columns<T>) =>
fields
.filter((field) => field.validations?.some((validation) => validation.rule === "required"))
export const findUnmatchedRequiredFields = <T extends string>(fields: Fields<T>, columns: Columns<T>) => {
// Get all required fields
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)
.map((field) => field.label) || []
.map((field) => field.key) || []
}

View File

@@ -108,10 +108,7 @@ export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps
removedRows.push({
index,
reason: "Duplicate",
row,
normalizedRow,
rowStr,
headerStr: selectedHeaderStr
row
});
return false;
}
@@ -151,28 +148,30 @@ export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps
}, [localData, selectedRows, toast]);
return (
<div className="flex flex-col">
<div className="px-8 py-6">
<div className="flex flex-col h-[calc(100vh-9.5rem)]">
<div className="px-8 py-6 bg-background">
<h2 className="text-2xl font-semibold text-foreground">
{translations.selectHeaderStep.title}
</h2>
</div>
<div className="flex-1 px-8 mb-12 overflow-auto">
<div className="flex justify-end mb-4">
<div className="flex-1 flex flex-col min-h-0">
<div className="px-8 mb-4 flex justify-end">
<Button
variant="outline"
variant="default"
size="sm"
onClick={discardEmptyAndDuplicateRows}
>
Remove Empty/Duplicates
</Button>
</div>
<div className="px-8 flex-1 overflow-auto">
<SelectHeaderTable
data={localData}
selectedRows={selectedRows}
setSelectedRows={setSelectedRows}
/>
</div>
</div>
<div className="flex items-center justify-between border-t bg-muted px-8 py-4 mt-2">
{onBack && (
<Button variant="outline" onClick={onBack}>