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]);
|
||||
|
||||
// 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]);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
<FadingOverlay />
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,85 +1864,30 @@ 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">
|
||||
<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 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>
|
||||
<div className="flex items-center gap-4">
|
||||
{isFromScratch && (
|
||||
<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);
|
||||
// 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"
|
||||
>
|
||||
Save Selected as Template
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Row
|
||||
</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
|
||||
</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}
|
||||
@@ -1954,6 +1901,7 @@ export const ValidationStep = <T extends string>({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-8 pb-6 flex-1 min-h-0">
|
||||
<div className="rounded-md border h-full flex flex-col overflow-hidden">
|
||||
<div className="flex-1 overflow-auto">
|
||||
@@ -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,8 +2032,26 @@ export const ValidationStep = <T extends string>({
|
||||
{translations.validationStep.backButtonTitle}
|
||||
</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
|
||||
className="ml-auto"
|
||||
disabled={isSubmitting}
|
||||
onClick={onContinue}
|
||||
>
|
||||
@@ -2034,5 +2060,6 @@ export const ValidationStep = <T extends string>({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user