Add floating toolbar to validate step, clean up upload page and fix up start from scratch functionality

This commit is contained in:
2025-02-25 01:58:26 -05:00
parent 41058ff5c6
commit 5d7e05172d
5 changed files with 168 additions and 121 deletions

View File

@@ -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]);

View File

@@ -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:

View File

@@ -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>
) )
} }

View File

@@ -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>

View File

@@ -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>