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 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,25 +291,45 @@ 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])
|
||||
|
||||
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])
|
||||
// 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]);
|
||||
|
||||
const handleAlertOnContinue = useCallback(async () => {
|
||||
setShowUnmatchedFieldsAlert(false)
|
||||
// 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 () => {
|
||||
setIsLoading(true)
|
||||
await onContinue(normalizeTableData(columns, data, fields), data, columns, globalSelections)
|
||||
// Normalize the data with global selections before continuing
|
||||
const normalizedData = normalizeTableData(columns, data, fields)
|
||||
await onContinue(normalizedData, data, columns, globalSelections)
|
||||
setIsLoading(false)
|
||||
}, [onContinue, columns, data, fields, globalSelections])
|
||||
|
||||
@@ -278,173 +344,198 @@ 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">
|
||||
{/* Global Selections Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Global Selections</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Supplier</label>
|
||||
<Select
|
||||
value={globalSelections.supplier}
|
||||
onValueChange={(value) => setGlobalSelections(prev => ({ ...prev, supplier: value }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select supplier..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fieldOptions?.suppliers?.map((supplier: any) => (
|
||||
<SelectItem key={supplier.value} value={supplier.value}>
|
||||
{supplier.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Company</label>
|
||||
<Select
|
||||
value={globalSelections.company}
|
||||
onValueChange={(value) => {
|
||||
setGlobalSelections(prev => ({
|
||||
...prev,
|
||||
company: value,
|
||||
line: undefined,
|
||||
subline: undefined
|
||||
}))
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select company..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fieldOptions?.companies?.map((company: any) => (
|
||||
<SelectItem key={company.value} value={company.value}>
|
||||
{company.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Line</label>
|
||||
<Select
|
||||
value={globalSelections.line}
|
||||
onValueChange={(value) => {
|
||||
setGlobalSelections(prev => ({
|
||||
...prev,
|
||||
line: value,
|
||||
subline: undefined
|
||||
}))
|
||||
}}
|
||||
disabled={!globalSelections.company}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select line..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{productLines?.map((line: any) => (
|
||||
<SelectItem key={line.value} value={line.value}>
|
||||
{line.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Sub Line</label>
|
||||
<Select
|
||||
value={globalSelections.subline}
|
||||
onValueChange={(value) => setGlobalSelections(prev => ({ ...prev, subline: value }))}
|
||||
disabled={!globalSelections.line}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select sub line..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sublines?.map((subline: any) => (
|
||||
<SelectItem key={subline.value} value={subline.value}>
|
||||
{subline.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<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 pb-4">
|
||||
{/* Global Selections Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Global Selections</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Supplier</label>
|
||||
<Select
|
||||
value={globalSelections.supplier}
|
||||
onValueChange={(value) => setGlobalSelections(prev => ({ ...prev, supplier: value }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select supplier..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fieldOptions?.suppliers?.map((supplier: any) => (
|
||||
<SelectItem key={supplier.value} value={supplier.value}>
|
||||
{supplier.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex-1">
|
||||
<ColumnGrid
|
||||
columns={columns}
|
||||
onContinue={handleOnContinue}
|
||||
onBack={onBack}
|
||||
isLoading={isLoading}
|
||||
userColumn={(column) => (
|
||||
<UserTableColumn
|
||||
column={column}
|
||||
onIgnore={onIgnore}
|
||||
onRevertIgnore={onRevertIgnore}
|
||||
entries={dataExample.map((row) => row[column.index])}
|
||||
/>
|
||||
)}
|
||||
templateColumn={(column) => <TemplateColumn column={column} onChange={onChange} onSubChange={onSubChange} />}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Company</label>
|
||||
<Select
|
||||
value={globalSelections.company}
|
||||
onValueChange={(value) => {
|
||||
setGlobalSelections(prev => ({
|
||||
...prev,
|
||||
company: value,
|
||||
line: undefined,
|
||||
subline: undefined
|
||||
}))
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select company..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fieldOptions?.companies?.map((company: any) => (
|
||||
<SelectItem key={company.value} value={company.value}>
|
||||
{company.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Line</label>
|
||||
<Select
|
||||
value={globalSelections.line}
|
||||
onValueChange={(value) => {
|
||||
setGlobalSelections(prev => ({
|
||||
...prev,
|
||||
line: value,
|
||||
subline: undefined
|
||||
}))
|
||||
}}
|
||||
disabled={!globalSelections.company}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select line..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{productLines?.map((line: any) => (
|
||||
<SelectItem key={line.value} value={line.value}>
|
||||
{line.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Sub Line</label>
|
||||
<Select
|
||||
value={globalSelections.subline}
|
||||
onValueChange={(value) => setGlobalSelections(prev => ({ ...prev, subline: value }))}
|
||||
disabled={!globalSelections.line}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select sub line..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sublines?.map((subline: any) => (
|
||||
<SelectItem key={subline.value} value={subline.value}>
|
||||
{subline.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex-1">
|
||||
<ColumnGrid
|
||||
columns={columns}
|
||||
onContinue={handleOnContinue}
|
||||
onBack={onBack}
|
||||
isLoading={isLoading}
|
||||
userColumn={(column) => (
|
||||
<UserTableColumn
|
||||
column={column}
|
||||
onIgnore={onIgnore}
|
||||
onRevertIgnore={onRevertIgnore}
|
||||
entries={dataExample.map((row) => row[column.index])}
|
||||
/>
|
||||
)}
|
||||
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>
|
||||
<div className="border-t bg-muted px-8 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{onBack && (
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
{translations.matchColumnsStep.backButtonTitle}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border-t bg-muted px-8 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{onBack && (
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
{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"
|
||||
@@ -456,6 +547,6 @@ export const MatchColumnsStep = <T extends string>({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) || []
|
||||
}
|
||||
|
||||
@@ -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,27 +148,29 @@ 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>
|
||||
<SelectHeaderTable
|
||||
data={localData}
|
||||
selectedRows={selectedRows}
|
||||
setSelectedRows={setSelectedRows}
|
||||
/>
|
||||
<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 && (
|
||||
|
||||
Reference in New Issue
Block a user