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]);
// 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)
const mappedCompanyColumn = useMemo(() => findMappedColumnForField('company'), [findMappedColumnForField]);

View File

@@ -43,6 +43,7 @@ export type StepState =
type: StepType.validateData
data: any[]
globalSelections?: GlobalSelections
isFromScratch?: boolean
}
interface Props {
@@ -82,6 +83,12 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
: undefined
)
const handleStartFromScratch = useCallback(() => {
if (onNext) {
onNext({ type: StepType.validateData, data: [{}], isFromScratch: true })
}
}, [onNext])
switch (state.type) {
case StepType.upload:
return (
@@ -107,6 +114,7 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
onNext({ type: StepType.selectSheet, workbook })
}
}}
setInitialState={onNext}
/>
)
case StepType.selectSheet:
@@ -187,6 +195,7 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
}
}}
globalSelections={state.globalSelections}
isFromScratch={state.isFromScratch}
/>
)
default:

View File

@@ -4,12 +4,16 @@ import { useRsi } from "../../hooks/useRsi"
import { DropZone } from "./components/DropZone"
import { ExampleTable } from "./components/ExampleTable"
import { FadingOverlay } from "./components/FadingOverlay"
import { StepType } from "../UploadFlow"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
type UploadProps = {
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 { translations, fields } = useRsi()
@@ -22,18 +26,39 @@ export const UploadStep = ({ onContinue }: UploadProps) => {
[onContinue],
)
const handleStartFromScratch = useCallback(() => {
if (setInitialState) {
setInitialState({ type: StepType.validateData, data: [{}], isFromScratch: true })
}
}, [setInitialState])
return (
<div className="p-6">
<h2 className="text-2xl font-semibold mb-4">{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="relative mb-0 border-t rounded-lg h-[80px]">
<div className="absolute inset-0">
<ExampleTable fields={fields} />
<div className="p-8 flex flex-col items-center max-w-xl mx-auto">
<h2 className="text-3xl font-semibold mb-8 text-center">{translations.uploadStep.title}</h2>
<div className="w-full space-y-8">
<div className="border rounded-lg p-6 flex flex-col items-center">
<h3 className="text-lg font-medium mb-4">Upload spreadsheet file</h3>
<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>
<FadingOverlay />
</div>
<DropZone onContinue={handleOnContinue} isLoading={isLoading} />
</div>
)
}

View File

@@ -3,7 +3,7 @@ import { useRsi } from "../../hooks/useRsi"
import type { Meta, Error } from "./types"
import { addErrorsAndRunHooks } from "./utils/dataMutations"
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 {
Command,
@@ -106,6 +106,7 @@ type Props<T extends string> = {
file: File
onBack?: () => void
globalSelections?: GlobalSelections
isFromScratch?: boolean
}
// Remove the local Field type declaration since we're importing it
@@ -1052,7 +1053,8 @@ export const ValidationStep = <T extends string>({
initialData,
file,
onBack,
globalSelections
globalSelections,
isFromScratch
}: Props<T>) => {
const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi<T>();
const { toast } = useToast();
@@ -1862,94 +1864,40 @@ export const ValidationStep = <T extends string>({
<div className="flex-1 overflow-hidden">
<div className="h-full flex flex-col">
<div className="px-8 pt-6">
<div className="mb-6 flex flex-wrap items-center justify-between gap-2">
<h2 className="text-3xl font-semibold text-foreground">
{translations.validationStep.title}
</h2>
<div className="flex flex-wrap items-center gap-4">
<div className="flex items-center gap-2">
<Select
value={selectedTemplateId || ""}
onValueChange={(value) => {
setSelectedTemplateId(value);
const selectedRows = Object.keys(rowSelection).map(Number);
if (selectedRows.length === 0) {
toast({
title: "No rows selected",
description: "Please select rows to apply the template to",
variant: "destructive",
});
return;
}
applyTemplate(value, selectedRows);
}}
>
<SelectTrigger className="w-[200px]">
<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" />
<div className="mb-6 flex flex-col gap-4">
<div className="flex flex-wrap items-center justify-between">
<h2 className="text-3xl font-semibold text-foreground">
{translations.validationStep.title}
</h2>
<div className="flex items-center gap-4">
{isFromScratch && (
<Button
variant="outline"
onClick={() => {
// Create a new empty row
const newRow = {} as RowData<T>;
// Add __index property with a unique string id
newRow.__index = `row-${Date.now()}`;
// Add the new row and validate
const newData = [...data, newRow];
updateData(newData);
}}
className="flex items-center gap-1"
>
<Plus className="h-4 w-4" />
Add Row
</Button>
)}
AI Validate
</Button>
<Button
variant="outline"
size="sm"
onClick={showCurrentPrompt}
disabled={data.length === 0}
>
Show Prompt
</Button>
<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 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>
@@ -2017,6 +1965,66 @@ export const ValidationStep = <T extends string>({
</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="flex items-center justify-between">
{onBack && (
@@ -2024,13 +2032,32 @@ export const ValidationStep = <T extends string>({
{translations.validationStep.backButtonTitle}
</Button>
)}
<Button
className="ml-auto"
disabled={isSubmitting}
onClick={onContinue}
>
{translations.validationStep.nextButtonTitle}
</Button>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={showCurrentPrompt}
disabled={data.length === 0}
>
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>

View File

@@ -561,17 +561,7 @@ export function Import() {
</CardHeader>
<CardContent className="space-y-4">
<Button onClick={() => setIsOpen(true)} className="w-full">
Upload Spreadsheet
</Button>
<Button
onClick={() => {
setStartFromScratch(true);
setIsOpen(true);
}}
variant="outline"
className="w-full"
>
Start From Scratch
Begin Import
</Button>
</CardContent>
</Card>