Add floating toolbar to validate step, clean up upload page and fix up start from scratch functionality
This commit is contained in:
@@ -201,10 +201,6 @@ export const MatchColumnsStep = <T extends string>({
|
|||||||
}, [columns]);
|
}, [columns]);
|
||||||
|
|
||||||
// Get the first value from the sample data for a column
|
// Get the first value from the sample data for a column
|
||||||
const getFirstValueFromColumn = useCallback((column: Column<T>) => {
|
|
||||||
if (!data.length || !data[0]) return null;
|
|
||||||
return data[0][column.index];
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
// Get mapped company value (if company is mapped to a column)
|
// Get mapped company value (if company is mapped to a column)
|
||||||
const mappedCompanyColumn = useMemo(() => findMappedColumnForField('company'), [findMappedColumnForField]);
|
const mappedCompanyColumn = useMemo(() => findMappedColumnForField('company'), [findMappedColumnForField]);
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export type StepState =
|
|||||||
type: StepType.validateData
|
type: StepType.validateData
|
||||||
data: any[]
|
data: any[]
|
||||||
globalSelections?: GlobalSelections
|
globalSelections?: GlobalSelections
|
||||||
|
isFromScratch?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -82,6 +83,12 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
|
|||||||
: undefined
|
: undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleStartFromScratch = useCallback(() => {
|
||||||
|
if (onNext) {
|
||||||
|
onNext({ type: StepType.validateData, data: [{}], isFromScratch: true })
|
||||||
|
}
|
||||||
|
}, [onNext])
|
||||||
|
|
||||||
switch (state.type) {
|
switch (state.type) {
|
||||||
case StepType.upload:
|
case StepType.upload:
|
||||||
return (
|
return (
|
||||||
@@ -107,6 +114,7 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
|
|||||||
onNext({ type: StepType.selectSheet, workbook })
|
onNext({ type: StepType.selectSheet, workbook })
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
setInitialState={onNext}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case StepType.selectSheet:
|
case StepType.selectSheet:
|
||||||
@@ -187,6 +195,7 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
globalSelections={state.globalSelections}
|
globalSelections={state.globalSelections}
|
||||||
|
isFromScratch={state.isFromScratch}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ import { useRsi } from "../../hooks/useRsi"
|
|||||||
import { DropZone } from "./components/DropZone"
|
import { DropZone } from "./components/DropZone"
|
||||||
import { ExampleTable } from "./components/ExampleTable"
|
import { ExampleTable } from "./components/ExampleTable"
|
||||||
import { FadingOverlay } from "./components/FadingOverlay"
|
import { FadingOverlay } from "./components/FadingOverlay"
|
||||||
|
import { StepType } from "../UploadFlow"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
|
||||||
type UploadProps = {
|
type UploadProps = {
|
||||||
onContinue: (data: XLSX.WorkBook, file: File) => Promise<void>
|
onContinue: (data: XLSX.WorkBook, file: File) => Promise<void>
|
||||||
|
setInitialState?: (state: { type: StepType; data: any[] }) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UploadStep = ({ onContinue }: UploadProps) => {
|
export const UploadStep = ({ onContinue, setInitialState }: UploadProps) => {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const { translations, fields } = useRsi()
|
const { translations, fields } = useRsi()
|
||||||
|
|
||||||
@@ -22,18 +26,39 @@ export const UploadStep = ({ onContinue }: UploadProps) => {
|
|||||||
[onContinue],
|
[onContinue],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleStartFromScratch = useCallback(() => {
|
||||||
|
if (setInitialState) {
|
||||||
|
setInitialState({ type: StepType.validateData, data: [{}], isFromScratch: true })
|
||||||
|
}
|
||||||
|
}, [setInitialState])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-8 flex flex-col items-center max-w-xl mx-auto">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{translations.uploadStep.title}</h2>
|
<h2 className="text-3xl font-semibold mb-8 text-center">{translations.uploadStep.title}</h2>
|
||||||
<p className="text-lg mb-2">{translations.uploadStep.manifestTitle}</p>
|
|
||||||
<p className="text-muted-foreground mb-6">{translations.uploadStep.manifestDescription}</p>
|
<div className="w-full space-y-8">
|
||||||
<div className="relative mb-0 border-t rounded-lg h-[80px]">
|
<div className="border rounded-lg p-6 flex flex-col items-center">
|
||||||
<div className="absolute inset-0">
|
<h3 className="text-lg font-medium mb-4">Upload spreadsheet file</h3>
|
||||||
<ExampleTable fields={fields} />
|
<DropZone onContinue={handleOnContinue} isLoading={isLoading} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<div className="bg-muted h-px w-16"></div>
|
||||||
|
<span className="px-3 text-muted-foreground text-sm font-medium">OR</span>
|
||||||
|
<div className="bg-muted h-px w-16"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button
|
||||||
|
onClick={handleStartFromScratch}
|
||||||
|
variant="outline"
|
||||||
|
className="min-w-[200px]"
|
||||||
|
disabled={!setInitialState}
|
||||||
|
>
|
||||||
|
Start from scratch
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<FadingOverlay />
|
|
||||||
</div>
|
</div>
|
||||||
<DropZone onContinue={handleOnContinue} isLoading={isLoading} />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useRsi } from "../../hooks/useRsi"
|
|||||||
import type { Meta, Error } from "./types"
|
import type { Meta, Error } from "./types"
|
||||||
import { addErrorsAndRunHooks } from "./utils/dataMutations"
|
import { addErrorsAndRunHooks } from "./utils/dataMutations"
|
||||||
import type { Data, SelectOption, Result, Fields, Field } from "../../types"
|
import type { Data, SelectOption, Result, Fields, Field } from "../../types"
|
||||||
import { Check, ChevronsUpDown, ArrowDown, AlertCircle, Loader2 } from "lucide-react"
|
import { Check, ChevronsUpDown, ArrowDown, AlertCircle, Loader2, X, Plus } from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -106,6 +106,7 @@ type Props<T extends string> = {
|
|||||||
file: File
|
file: File
|
||||||
onBack?: () => void
|
onBack?: () => void
|
||||||
globalSelections?: GlobalSelections
|
globalSelections?: GlobalSelections
|
||||||
|
isFromScratch?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the local Field type declaration since we're importing it
|
// Remove the local Field type declaration since we're importing it
|
||||||
@@ -1052,7 +1053,8 @@ export const ValidationStep = <T extends string>({
|
|||||||
initialData,
|
initialData,
|
||||||
file,
|
file,
|
||||||
onBack,
|
onBack,
|
||||||
globalSelections
|
globalSelections,
|
||||||
|
isFromScratch
|
||||||
}: Props<T>) => {
|
}: Props<T>) => {
|
||||||
const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi<T>();
|
const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi<T>();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -1862,94 +1864,40 @@ export const ValidationStep = <T extends string>({
|
|||||||
<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 pt-6">
|
<div className="px-8 pt-6">
|
||||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2">
|
<div className="mb-6 flex flex-col gap-4">
|
||||||
<h2 className="text-3xl font-semibold text-foreground">
|
<div className="flex flex-wrap items-center justify-between">
|
||||||
{translations.validationStep.title}
|
<h2 className="text-3xl font-semibold text-foreground">
|
||||||
</h2>
|
{translations.validationStep.title}
|
||||||
<div className="flex flex-wrap items-center gap-4">
|
</h2>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-4">
|
||||||
<Select
|
{isFromScratch && (
|
||||||
value={selectedTemplateId || ""}
|
<Button
|
||||||
onValueChange={(value) => {
|
variant="outline"
|
||||||
setSelectedTemplateId(value);
|
onClick={() => {
|
||||||
const selectedRows = Object.keys(rowSelection).map(Number);
|
// Create a new empty row
|
||||||
if (selectedRows.length === 0) {
|
const newRow = {} as RowData<T>;
|
||||||
toast({
|
// Add __index property with a unique string id
|
||||||
title: "No rows selected",
|
newRow.__index = `row-${Date.now()}`;
|
||||||
description: "Please select rows to apply the template to",
|
// Add the new row and validate
|
||||||
variant: "destructive",
|
const newData = [...data, newRow];
|
||||||
});
|
updateData(newData);
|
||||||
return;
|
}}
|
||||||
}
|
className="flex items-center gap-1"
|
||||||
applyTemplate(value, selectedRows);
|
>
|
||||||
}}
|
<Plus className="h-4 w-4" />
|
||||||
>
|
Add Row
|
||||||
<SelectTrigger className="w-[200px]">
|
</Button>
|
||||||
<SelectValue placeholder="Apply template to selected" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{templates?.map((template) => (
|
|
||||||
<SelectItem key={template.id} value={template.id.toString()}>
|
|
||||||
{template.company} - {template.product_type}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
const selectedRows = Object.keys(rowSelection);
|
|
||||||
if (selectedRows.length !== 1) {
|
|
||||||
toast({
|
|
||||||
title: "Invalid selection",
|
|
||||||
description: "Please select exactly one row to save as a template",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setShowSaveTemplateDialog(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Save Selected as Template
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={deleteSelectedRows}
|
|
||||||
disabled={Object.keys(rowSelection).length === 0}
|
|
||||||
>
|
|
||||||
{translations.validationStep.discardButtonTitle}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleAiValidation}
|
|
||||||
disabled={isAiValidating || data.length === 0}
|
|
||||||
>
|
|
||||||
{isAiValidating && (
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
)}
|
)}
|
||||||
AI Validate
|
<div className="flex items-center gap-2">
|
||||||
</Button>
|
<Switch
|
||||||
<Button
|
checked={filterByErrors}
|
||||||
variant="outline"
|
onCheckedChange={setFilterByErrors}
|
||||||
size="sm"
|
id="filter-errors"
|
||||||
onClick={showCurrentPrompt}
|
/>
|
||||||
disabled={data.length === 0}
|
<label htmlFor="filter-errors" className="text-sm text-muted-foreground">
|
||||||
>
|
{translations.validationStep.filterSwitchTitle}
|
||||||
Show Prompt
|
</label>
|
||||||
</Button>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Switch
|
|
||||||
checked={filterByErrors}
|
|
||||||
onCheckedChange={setFilterByErrors}
|
|
||||||
id="filter-errors"
|
|
||||||
/>
|
|
||||||
<label htmlFor="filter-errors" className="text-sm text-muted-foreground">
|
|
||||||
{translations.validationStep.filterSwitchTitle}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2017,6 +1965,66 @@ export const ValidationStep = <T extends string>({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Floating action bar for selected items */}
|
||||||
|
{Object.keys(rowSelection).length > 0 && (
|
||||||
|
<div className="fixed bottom-24 left-1/2 transform -translate-x-1/2 z-50 animate-in fade-in slide-in-from-bottom-5 duration-300">
|
||||||
|
<div className="bg-card shadow-xl rounded-lg border border-muted px-4 py-3 flex items-center gap-3">
|
||||||
|
<div className="mr-2 bg-muted text-primary px-2 py-1 rounded-md text-xs font-medium border border-primary">
|
||||||
|
{Object.keys(rowSelection).length} selected
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={selectedTemplateId || ""}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSelectedTemplateId(value);
|
||||||
|
const selectedRows = Object.keys(rowSelection).map(Number);
|
||||||
|
applyTemplate(value, selectedRows);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px] h-8 bg-card text-xs font-semibold">
|
||||||
|
<SelectValue placeholder="Apply template" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{templates?.map((template) => (
|
||||||
|
<SelectItem key={template.id} value={template.id.toString()}>
|
||||||
|
{template.company} - {template.product_type}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{Object.keys(rowSelection).length === 1 && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowSaveTemplateDialog(true)}
|
||||||
|
>
|
||||||
|
Save as Template
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant={isFromScratch ? "destructive" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={deleteSelectedRows}
|
||||||
|
>
|
||||||
|
{isFromScratch ? "Delete Row" : translations.validationStep.discardButtonTitle}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setRowSelection({})}
|
||||||
|
className="h-9 w-9"
|
||||||
|
title="Clear selection"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="border-t bg-muted px-8 py-4">
|
<div className="border-t bg-muted px-8 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{onBack && (
|
{onBack && (
|
||||||
@@ -2024,13 +2032,32 @@ export const ValidationStep = <T extends string>({
|
|||||||
{translations.validationStep.backButtonTitle}
|
{translations.validationStep.backButtonTitle}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
className="ml-auto"
|
<Button
|
||||||
disabled={isSubmitting}
|
variant="outline"
|
||||||
onClick={onContinue}
|
size="sm"
|
||||||
>
|
onClick={showCurrentPrompt}
|
||||||
{translations.validationStep.nextButtonTitle}
|
disabled={data.length === 0}
|
||||||
</Button>
|
>
|
||||||
|
Show Prompt
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleAiValidation}
|
||||||
|
disabled={isAiValidating || data.length === 0}
|
||||||
|
>
|
||||||
|
{isAiValidating && (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
)}
|
||||||
|
AI Validate
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={onContinue}
|
||||||
|
>
|
||||||
|
{translations.validationStep.nextButtonTitle}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -561,17 +561,7 @@ export function Import() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<Button onClick={() => setIsOpen(true)} className="w-full">
|
<Button onClick={() => setIsOpen(true)} className="w-full">
|
||||||
Upload Spreadsheet
|
Begin Import
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setStartFromScratch(true);
|
|
||||||
setIsOpen(true);
|
|
||||||
}}
|
|
||||||
variant="outline"
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
Start From Scratch
|
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user