Shadcn conversion + styling, match columns page
This commit is contained in:
@@ -27,7 +27,7 @@ import {
|
|||||||
import { Loader2, X, RefreshCw, AlertTriangle, RefreshCcw, Hourglass } from "lucide-react";
|
import { Loader2, X, RefreshCw, AlertTriangle, RefreshCcw, Hourglass } from "lucide-react";
|
||||||
import config from "../../config";
|
import config from "../../config";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Table, TableBody, TableCell, TableRow, TableHeader, TableHead } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
|
||||||
|
|
||||||
interface ImportProgress {
|
interface ImportProgress {
|
||||||
status: "running" | "error" | "complete" | "cancelled";
|
status: "running" | "error" | "complete" | "cancelled";
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||||
import { useToast } from "@chakra-ui/react"
|
|
||||||
import { UserTableColumn } from "./components/UserTableColumn"
|
import { UserTableColumn } from "./components/UserTableColumn"
|
||||||
import { useRsi } from "../../hooks/useRsi"
|
import { useRsi } from "../../hooks/useRsi"
|
||||||
import { TemplateColumn } from "./components/TemplateColumn"
|
import { TemplateColumn } from "./components/TemplateColumn"
|
||||||
@@ -11,6 +10,7 @@ import { normalizeTableData } from "./utils/normalizeTableData"
|
|||||||
import type { Field, RawData } from "../../types"
|
import type { Field, RawData } from "../../types"
|
||||||
import { getMatchedColumns } from "./utils/getMatchedColumns"
|
import { getMatchedColumns } from "./utils/getMatchedColumns"
|
||||||
import { findUnmatchedRequiredFields } from "./utils/findUnmatchedRequiredFields"
|
import { findUnmatchedRequiredFields } from "./utils/findUnmatchedRequiredFields"
|
||||||
|
import { toast } from "sonner"
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -80,7 +80,6 @@ export const MatchColumnsStep = <T extends string>({
|
|||||||
onContinue,
|
onContinue,
|
||||||
onBack,
|
onBack,
|
||||||
}: MatchColumnsProps<T>) => {
|
}: MatchColumnsProps<T>) => {
|
||||||
const toast = useToast()
|
|
||||||
const dataExample = data.slice(0, 2)
|
const dataExample = data.slice(0, 2)
|
||||||
const { fields, autoMapHeaders, autoMapSelectValues, autoMapDistance, translations, allowInvalidSubmit } = useRsi<T>()
|
const { fields, autoMapHeaders, autoMapSelectValues, autoMapDistance, translations, allowInvalidSubmit } = useRsi<T>()
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
@@ -92,24 +91,24 @@ export const MatchColumnsStep = <T extends string>({
|
|||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(value: T, columnIndex: number) => {
|
(value: T, columnIndex: number) => {
|
||||||
const field = fields.find((field) => field.key === value) as unknown as Field<T>
|
const field = fields.find((field: Field<T>) => field.key === value)
|
||||||
|
if (!field) return
|
||||||
|
|
||||||
const existingFieldIndex = columns.findIndex((column) => "value" in column && column.value === field.key)
|
const existingFieldIndex = columns.findIndex((column) => "value" in column && column.value === field.key)
|
||||||
|
|
||||||
setColumns(
|
setColumns(
|
||||||
columns.map<Column<T>>((column, index) => {
|
columns.map<Column<T>>((column, index) => {
|
||||||
columnIndex === index ? setColumn(column, field, data) : column
|
|
||||||
if (columnIndex === index) {
|
if (columnIndex === index) {
|
||||||
|
// Set the new column value
|
||||||
return setColumn(column, field, data, autoMapSelectValues)
|
return setColumn(column, field, data, autoMapSelectValues)
|
||||||
} else if (index === existingFieldIndex) {
|
} else if (index === existingFieldIndex) {
|
||||||
toast({
|
// Clear the old column that had this field
|
||||||
status: "warning",
|
toast.warning(translations.matchColumnsStep.duplicateColumnWarningTitle, {
|
||||||
variant: "left-accent",
|
|
||||||
position: "bottom-left",
|
|
||||||
title: translations.matchColumnsStep.duplicateColumnWarningTitle,
|
|
||||||
description: translations.matchColumnsStep.duplicateColumnWarningDescription,
|
description: translations.matchColumnsStep.duplicateColumnWarningDescription,
|
||||||
isClosable: true,
|
|
||||||
})
|
})
|
||||||
return setColumn(column)
|
return setColumn(column)
|
||||||
} else {
|
} else {
|
||||||
|
// Leave other columns unchanged
|
||||||
return column
|
return column
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -120,7 +119,6 @@ export const MatchColumnsStep = <T extends string>({
|
|||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
fields,
|
fields,
|
||||||
toast,
|
|
||||||
translations.matchColumnsStep.duplicateColumnWarningDescription,
|
translations.matchColumnsStep.duplicateColumnWarningDescription,
|
||||||
translations.matchColumnsStep.duplicateColumnWarningTitle,
|
translations.matchColumnsStep.duplicateColumnWarningTitle,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import type React from "react"
|
import type React from "react"
|
||||||
import type { Column, Columns } from "../MatchColumnsStep"
|
import type { Column, Columns } from "../MatchColumnsStep"
|
||||||
import { Box, Flex, Heading, ModalBody, Text, useStyleConfig } from "@chakra-ui/react"
|
|
||||||
import { FadingWrapper } from "../../../components/FadingWrapper"
|
|
||||||
import { ContinueButton } from "../../../components/ContinueButton"
|
|
||||||
import { useRsi } from "../../../hooks/useRsi"
|
import { useRsi } from "../../../hooks/useRsi"
|
||||||
import type { themeOverrides } from "../../../theme"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
|
||||||
|
|
||||||
type ColumnGridProps<T extends string> = {
|
type ColumnGridProps<T extends string> = {
|
||||||
columns: Columns<T>
|
columns: Columns<T>
|
||||||
@@ -15,8 +13,6 @@ type ColumnGridProps<T extends string> = {
|
|||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Styles = (typeof themeOverrides)["components"]["MatchColumnsStep"]["baseStyle"]
|
|
||||||
|
|
||||||
export const ColumnGrid = <T extends string>({
|
export const ColumnGrid = <T extends string>({
|
||||||
columns,
|
columns,
|
||||||
userColumn,
|
userColumn,
|
||||||
@@ -26,52 +22,82 @@ export const ColumnGrid = <T extends string>({
|
|||||||
isLoading,
|
isLoading,
|
||||||
}: ColumnGridProps<T>) => {
|
}: ColumnGridProps<T>) => {
|
||||||
const { translations } = useRsi()
|
const { translations } = useRsi()
|
||||||
const styles = useStyleConfig("MatchColumnsStep") as Styles
|
const columnWidth = 250
|
||||||
|
const gap = 16
|
||||||
|
const totalWidth = columns.length * columnWidth + (columns.length - 1) * gap
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex h-[calc(100vh-9.5rem)] flex-col">
|
||||||
<ModalBody flexDir="column" p={8} overflow="auto">
|
<div className="flex-1 overflow-hidden">
|
||||||
<Heading sx={styles.heading}>{translations.matchColumnsStep.title}</Heading>
|
<div className="px-8 py-6">
|
||||||
<Flex
|
<div className="mb-8">
|
||||||
flex={1}
|
<h2 className="text-3xl font-semibold text-foreground">
|
||||||
display="grid"
|
{translations.matchColumnsStep.title}
|
||||||
gridTemplateRows="auto auto auto 1fr"
|
</h2>
|
||||||
gridTemplateColumns={`0.75rem repeat(${columns.length}, minmax(18rem, auto)) 0.75rem`}
|
</div>
|
||||||
|
<ScrollArea className="relative">
|
||||||
|
<div className="space-y-8" style={{ width: totalWidth }}>
|
||||||
|
{/* Your table section */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-4 text-lg font-medium text-foreground">
|
||||||
|
{translations.matchColumnsStep.userTableTitle}
|
||||||
|
</h3>
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
className="grid gap-4"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${columns.length}, ${columnWidth}px)`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box gridColumn={`1/${columns.length + 3}`}>
|
|
||||||
<Text sx={styles.title}>{translations.matchColumnsStep.userTableTitle}</Text>
|
|
||||||
</Box>
|
|
||||||
{columns.map((column, index) => (
|
{columns.map((column, index) => (
|
||||||
<Box gridRow="2/3" gridColumn={`${index + 2}/${index + 3}`} pt={3} key={column.header + index}>
|
<div key={column.header + index} className="relative">
|
||||||
{userColumn(column)}
|
{userColumn(column)}
|
||||||
</Box>
|
</div>
|
||||||
))}
|
))}
|
||||||
<FadingWrapper gridColumn={`1/${columns.length + 3}`} gridRow="2/3" />
|
</div>
|
||||||
<Box gridColumn={`1/${columns.length + 3}`} mt={7}>
|
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-background" />
|
||||||
<Text sx={styles.title}>{translations.matchColumnsStep.templateTitle}</Text>
|
</div>
|
||||||
</Box>
|
</div>
|
||||||
<FadingWrapper gridColumn={`1/${columns.length + 3}`} gridRow="4/5" />
|
|
||||||
{columns.map((column, index) => (
|
{/* Will become section */}
|
||||||
<Box
|
<div>
|
||||||
gridRow="4/5"
|
<h3 className="mb-4 text-lg font-medium text-foreground">
|
||||||
gridColumn={`${index + 2}/${index + 3}`}
|
{translations.matchColumnsStep.templateTitle}
|
||||||
key={column.header + index}
|
</h3>
|
||||||
py="1.125rem"
|
<div
|
||||||
pl={2}
|
className="grid gap-4"
|
||||||
pr={3}
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${columns.length}, ${columnWidth}px)`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
{columns.map((column, index) => (
|
||||||
|
<div key={column.header + index}>
|
||||||
{templateColumn(column)}
|
{templateColumn(column)}
|
||||||
</Box>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</div>
|
||||||
</ModalBody>
|
</div>
|
||||||
<ContinueButton
|
</div>
|
||||||
isLoading={isLoading}
|
<ScrollBar orientation="horizontal" />
|
||||||
onContinue={onContinue}
|
</ScrollArea>
|
||||||
onBack={onBack}
|
</div>
|
||||||
title={translations.matchColumnsStep.nextButtonTitle}
|
</div>
|
||||||
backTitle={translations.matchColumnsStep.backButtonTitle}
|
<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>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
className="ml-auto"
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={() => onContinue([])}
|
||||||
|
>
|
||||||
|
{translations.matchColumnsStep.nextButtonTitle}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,96 +1,139 @@
|
|||||||
import {
|
|
||||||
Flex,
|
|
||||||
Text,
|
|
||||||
Accordion,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionButton,
|
|
||||||
AccordionIcon,
|
|
||||||
Box,
|
|
||||||
AccordionPanel,
|
|
||||||
useStyleConfig,
|
|
||||||
} from "@chakra-ui/react"
|
|
||||||
import { useRsi } from "../../../hooks/useRsi"
|
import { useRsi } from "../../../hooks/useRsi"
|
||||||
import type { Column } from "../MatchColumnsStep"
|
import type { Column } from "../MatchColumnsStep"
|
||||||
import { ColumnType } from "../MatchColumnsStep"
|
import { ColumnType } from "../MatchColumnsStep"
|
||||||
import { MatchIcon } from "./MatchIcon"
|
import type { Fields, Field } from "../../../types"
|
||||||
import type { Fields } from "../../../types"
|
import {
|
||||||
import type { Translations } from "../../../translationsRSIProps"
|
Card,
|
||||||
import { MatchColumnSelect } from "../../../components/Selects/MatchColumnSelect"
|
CardContent,
|
||||||
import { SubMatchingSelect } from "./SubMatchingSelect"
|
CardHeader,
|
||||||
import type { Styles } from "./ColumnGrid"
|
} from "@/components/ui/card"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion"
|
||||||
|
import { Check } from "lucide-react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const getAccordionTitle = <T extends string>(fields: Fields<T>, column: Column<T>, translations: Translations) => {
|
type TemplateColumnProps<T extends string> = {
|
||||||
|
column: Column<T>
|
||||||
|
onChange: (value: T, columnIndex: number) => void
|
||||||
|
onSubChange: (value: string, columnIndex: number, entry: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAccordionTitle = <T extends string>(fields: Fields<T>, column: Column<T>, translations: any) => {
|
||||||
const fieldLabel = fields.find((field) => "value" in column && field.key === column.value)!.label
|
const fieldLabel = fields.find((field) => "value" in column && field.key === column.value)!.label
|
||||||
return `${translations.matchColumnsStep.matchDropdownTitle} ${fieldLabel} (${
|
return `${translations.matchColumnsStep.matchDropdownTitle} ${fieldLabel} (${
|
||||||
"matchedOptions" in column && column.matchedOptions.filter((option) => !option.value).length
|
"matchedOptions" in column && column.matchedOptions.filter((option) => !option.value).length
|
||||||
} ${translations.matchColumnsStep.unmatched})`
|
} ${translations.matchColumnsStep.unmatched})`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TemplateColumnProps<T extends string> = {
|
|
||||||
onChange: (val: T, index: number) => void
|
|
||||||
onSubChange: (val: T, index: number, option: string) => void
|
|
||||||
column: Column<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TemplateColumn = <T extends string>({ column, onChange, onSubChange }: TemplateColumnProps<T>) => {
|
export const TemplateColumn = <T extends string>({ column, onChange, onSubChange }: TemplateColumnProps<T>) => {
|
||||||
const { translations, fields } = useRsi<T>()
|
const { translations, fields } = useRsi<T>()
|
||||||
const styles = useStyleConfig("MatchColumnsStep") as Styles
|
|
||||||
const isIgnored = column.type === ColumnType.ignored
|
const isIgnored = column.type === ColumnType.ignored
|
||||||
const isChecked =
|
const isChecked =
|
||||||
column.type === ColumnType.matched ||
|
column.type === ColumnType.matched ||
|
||||||
column.type === ColumnType.matchedCheckbox ||
|
column.type === ColumnType.matchedCheckbox ||
|
||||||
column.type === ColumnType.matchedSelectOptions
|
column.type === ColumnType.matchedSelectOptions
|
||||||
const isSelect = "matchedOptions" in column
|
const isSelect = "matchedOptions" in column
|
||||||
const selectOptions = fields.map(({ label, key }) => ({ value: key, label }))
|
const selectOptions = fields.map(({ label, key }: { label: string; key: string }) => ({ value: key, label }))
|
||||||
const selectValue = selectOptions.find(({ value }) => "value" in column && column.value === value)
|
const selectValue = column.type === ColumnType.empty ? undefined :
|
||||||
|
selectOptions.find(({ value }: { value: string }) => "value" in column && column.value === value)?.value
|
||||||
|
|
||||||
|
if (isIgnored) {
|
||||||
return (
|
return (
|
||||||
<Flex minH={10} w="100%" flexDir="column" justifyContent="center">
|
<Card className="h-full opacity-50">
|
||||||
{isIgnored ? (
|
<CardHeader className="p-4">
|
||||||
<Text sx={styles.selectColumn.text}>{translations.matchColumnsStep.ignoredColumnText}</Text>
|
<p className="text-sm text-muted-foreground">
|
||||||
) : (
|
{translations.matchColumnsStep.ignoredColumnText}
|
||||||
<>
|
</p>
|
||||||
<Flex alignItems="center" minH={10} w="100%">
|
</CardHeader>
|
||||||
<Box flex={1}>
|
</Card>
|
||||||
<MatchColumnSelect
|
)
|
||||||
placeholder={translations.matchColumnsStep.selectPlaceholder}
|
}
|
||||||
value={selectValue}
|
|
||||||
onChange={(value) => onChange(value?.value as T, column.index)}
|
return (
|
||||||
options={selectOptions}
|
<Card className="h-full">
|
||||||
name={column.header}
|
<CardHeader className="flex flex-row items-center justify-between space-x-2 p-4">
|
||||||
/>
|
<div className="flex-1">
|
||||||
</Box>
|
<Select
|
||||||
<MatchIcon isChecked={isChecked} />
|
key={`select-${column.index}-${("value" in column ? column.value : "empty")}`}
|
||||||
</Flex>
|
value={selectValue}
|
||||||
{isSelect && (
|
onValueChange={(value) => onChange(value as T, column.index)}
|
||||||
<Flex width="100%">
|
>
|
||||||
<Accordion allowMultiple width="100%">
|
<SelectTrigger className="w-full">
|
||||||
<AccordionItem border="none" py={1}>
|
<SelectValue placeholder={translations.matchColumnsStep.selectPlaceholder} />
|
||||||
<AccordionButton
|
</SelectTrigger>
|
||||||
_hover={{ bg: "transparent" }}
|
<SelectContent
|
||||||
_focus={{ boxShadow: "none" }}
|
side="bottom"
|
||||||
px={0}
|
align="start"
|
||||||
py={4}
|
className="z-[1500]"
|
||||||
data-testid="accordion-button"
|
>
|
||||||
>
|
{selectOptions.map((option: { value: string; label: string }) => (
|
||||||
<AccordionIcon />
|
<SelectItem key={option.value} value={option.value}>
|
||||||
<Box textAlign="left">
|
{option.label}
|
||||||
<Text sx={styles.selectColumn.accordionLabel}>
|
</SelectItem>
|
||||||
{getAccordionTitle<T>(fields, column, translations)}
|
))}
|
||||||
</Text>
|
</SelectContent>
|
||||||
</Box>
|
</Select>
|
||||||
</AccordionButton>
|
</div>
|
||||||
<AccordionPanel pb={4} pr={3} display="flex" flexDir="column">
|
{isChecked && (
|
||||||
{column.matchedOptions.map((option) => (
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10">
|
||||||
<SubMatchingSelect option={option} column={column} onSubChange={onSubChange} key={option.entry} />
|
<Check className="h-4 w-4 text-primary" />
|
||||||
))}
|
</div>
|
||||||
</AccordionPanel>
|
)}
|
||||||
</AccordionItem>
|
</CardHeader>
|
||||||
</Accordion>
|
{isSelect && (
|
||||||
</Flex>
|
<CardContent className="p-4">
|
||||||
)}
|
<Accordion type="multiple" className="w-full">
|
||||||
</>
|
<AccordionItem value="options" className="border-none">
|
||||||
)}
|
<AccordionTrigger className="py-2 text-sm hover:no-underline">
|
||||||
</Flex>
|
{getAccordionTitle<T>(fields, column, translations)}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{column.matchedOptions.map((option) => (
|
||||||
|
<div key={option.entry} className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{option.entry}
|
||||||
|
</p>
|
||||||
|
<Select
|
||||||
|
value={option.value}
|
||||||
|
onValueChange={(value) => onSubChange(value, column.index, option.entry!)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder={translations.matchColumnsStep.subSelectPlaceholder} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
className="z-[1000]"
|
||||||
|
>
|
||||||
|
{fields
|
||||||
|
.find((field: Field<T>) => "value" in column && field.key === column.value)
|
||||||
|
?.fieldType.options.map((option: { value: string; label: string }) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Box, Flex, IconButton, Text, useStyleConfig } from "@chakra-ui/react"
|
import { Button } from "@/components/ui/button"
|
||||||
import { CgClose, CgUndo } from "react-icons/cg"
|
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
||||||
|
import { X, Undo2 } from "lucide-react"
|
||||||
import type { Column } from "../MatchColumnsStep"
|
import type { Column } from "../MatchColumnsStep"
|
||||||
import { ColumnType } from "../MatchColumnsStep"
|
import { ColumnType } from "../MatchColumnsStep"
|
||||||
import { dataAttr } from "@chakra-ui/utils"
|
|
||||||
import type { Styles } from "./ColumnGrid"
|
|
||||||
import type { RawData } from "../../../types"
|
import type { RawData } from "../../../types"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
type UserTableColumnProps<T extends string> = {
|
type UserTableColumnProps<T extends string> = {
|
||||||
column: Column<T>
|
column: Column<T>
|
||||||
@@ -14,7 +14,6 @@ type UserTableColumnProps<T extends string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UserTableColumn = <T extends string>(props: UserTableColumnProps<T>) => {
|
export const UserTableColumn = <T extends string>(props: UserTableColumnProps<T>) => {
|
||||||
const styles = useStyleConfig("MatchColumnsStep") as Styles
|
|
||||||
const {
|
const {
|
||||||
column: { header, index, type },
|
column: { header, index, type },
|
||||||
entries,
|
entries,
|
||||||
@@ -22,33 +21,38 @@ export const UserTableColumn = <T extends string>(props: UserTableColumnProps<T>
|
|||||||
onRevertIgnore,
|
onRevertIgnore,
|
||||||
} = props
|
} = props
|
||||||
const isIgnored = type === ColumnType.ignored
|
const isIgnored = type === ColumnType.ignored
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Card className={cn(
|
||||||
<Flex px={6} justifyContent="space-between" alignItems="center" mb={4}>
|
"h-full transition-opacity",
|
||||||
<Text sx={styles.userTable.header} data-ignored={dataAttr(isIgnored)}>
|
isIgnored && "opacity-50"
|
||||||
|
)}>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-x-2 p-4">
|
||||||
|
<h4 className="truncate font-medium leading-none">
|
||||||
{header}
|
{header}
|
||||||
</Text>
|
</h4>
|
||||||
{type === ColumnType.ignored ? (
|
<Button
|
||||||
<IconButton
|
variant="ghost"
|
||||||
aria-label="Ignore column"
|
size="icon"
|
||||||
icon={<CgUndo />}
|
onClick={() => isIgnored ? onRevertIgnore(index) : onIgnore(index)}
|
||||||
onClick={() => onRevertIgnore(index)}
|
className="h-8 w-8 shrink-0"
|
||||||
{...styles.userTable.ignoreButton}
|
>
|
||||||
/>
|
{isIgnored ? <Undo2 className="h-4 w-4" /> : <X className="h-4 w-4" />}
|
||||||
) : (
|
</Button>
|
||||||
<IconButton
|
</CardHeader>
|
||||||
aria-label="Ignore column"
|
<CardContent className="space-y-2 p-4">
|
||||||
icon={<CgClose />}
|
{entries.slice(0, 3).map((entry, i) => (
|
||||||
onClick={() => onIgnore(index)}
|
<div
|
||||||
{...styles.userTable.ignoreButton}
|
key={`${entry || ""}-${i}`}
|
||||||
/>
|
className={cn(
|
||||||
|
"truncate px-3 py-2 text-sm",
|
||||||
|
isIgnored ? "text-muted-foreground" : "text-foreground"
|
||||||
)}
|
)}
|
||||||
</Flex>
|
>
|
||||||
{entries.map((entry, index) => (
|
{entry || ""}
|
||||||
<Text key={(entry || "") + index} sx={styles.userTable.cell} data-ignored={dataAttr(isIgnored)}>
|
</div>
|
||||||
{entry}
|
|
||||||
</Text>
|
|
||||||
))}
|
))}
|
||||||
</Box>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ export const Steps = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="hidden border-b bg-muted px-8 py-6 md:block lg:px-24 xl:px-36">
|
<div className="hidden border-b bg-muted px-4 py-6 md:block">
|
||||||
<nav className="mx-auto flex items-center justify-between" aria-label="Steps">
|
<nav className="mx-auto flex items-center justify-center gap-4 lg:gap-24" aria-label="Steps">
|
||||||
{steps.map((key, index) => {
|
{steps.map((key, index) => {
|
||||||
const isActive = index === activeStep
|
const isActive = index === activeStep
|
||||||
const isCompleted = index < activeStep
|
const isCompleted = index < activeStep
|
||||||
@@ -73,15 +73,13 @@ export const Steps = () => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className={`ml-2 text-sm font-medium w-24 ${
|
<span className={`ml-2 text-sm font-medium ${
|
||||||
isActive ? 'text-foreground' : 'text-muted-foreground'
|
isActive ? 'text-foreground' : 'text-muted-foreground'
|
||||||
}`}>
|
}`}>
|
||||||
{translations[key].title}
|
{translations[key].title}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{index < steps.length - 1 && (
|
|
||||||
<Separator className="ml-2 mr-0 bg-muted-foreground/80" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
|||||||
<div
|
<div
|
||||||
{...getRootProps()}
|
{...getRootProps()}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full flex-1 flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted p-12",
|
"flex h-full w-full flex-1 flex-col items-center justify-center rounded-lg border-2 border-dashed border-secondary-foreground/30 bg-muted/90 p-12",
|
||||||
isDragActive && "border-primary bg-muted/50"
|
isDragActive && "border-primary bg-muted"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<input {...getInputProps()} data-testid="rsi-dropzone" />
|
<input {...getInputProps()} data-testid="rsi-dropzone" />
|
||||||
{isDragActive ? (
|
{isDragActive ? (
|
||||||
<p className="text-lg text-muted-foreground">
|
<p className="text-lg text-muted-foreground mb-1 py-6">
|
||||||
{translations.uploadStep.dropzone.activeDropzoneTitle}
|
{translations.uploadStep.dropzone.activeDropzoneTitle}
|
||||||
</p>
|
</p>
|
||||||
) : loading || isLoading ? (
|
) : loading || isLoading ? (
|
||||||
@@ -75,7 +75,7 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
|||||||
<p className="mb-4 text-lg text-muted-foreground">
|
<p className="mb-4 text-lg text-muted-foreground">
|
||||||
{translations.uploadStep.dropzone.title}
|
{translations.uploadStep.dropzone.title}
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={open} variant="secondary">
|
<Button onClick={open} variant="default">
|
||||||
{translations.uploadStep.dropzone.buttonTitle}
|
{translations.uploadStep.dropzone.buttonTitle}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ export const themeOverrides = {
|
|||||||
Modal: {
|
Modal: {
|
||||||
baseStyle: {
|
baseStyle: {
|
||||||
dialog: {
|
dialog: {
|
||||||
borderRadius: "lg",
|
borderRadius: "md",
|
||||||
bg: "background",
|
bg: "background",
|
||||||
fontSize: "lg",
|
fontSize: "lg",
|
||||||
color: "textColor",
|
color: "textColor",
|
||||||
@@ -360,7 +360,7 @@ export const themeOverrides = {
|
|||||||
minH: "calc(var(--chakra-vh) - 4rem)",
|
minH: "calc(var(--chakra-vh) - 4rem)",
|
||||||
maxW: "calc(var(--chakra-vw) - 4rem)",
|
maxW: "calc(var(--chakra-vw) - 4rem)",
|
||||||
my: "2rem",
|
my: "2rem",
|
||||||
borderRadius: "3xl",
|
borderRadius: "lg",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { ReactSpreadsheetImport } from "@/lib/react-spreadsheet-import/src";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { ReactSpreadsheetImport } from "@/lib/react-spreadsheet-import/src";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Box, Card, ScrollArea } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Code } from "@/components/ui/code";
|
import { Code } from "@/components/ui/code";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
export function Import() {
|
const IMPORT_FIELDS = [
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [importedData, setImportedData] = useState<any[] | null>(null);
|
|
||||||
|
|
||||||
const fields = [
|
|
||||||
{
|
{
|
||||||
label: "Name",
|
label: "Name",
|
||||||
key: "name",
|
key: "name",
|
||||||
@@ -59,41 +57,72 @@ export function Import() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function Import() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [importedData, setImportedData] = useState<any[] | null>(null);
|
||||||
|
|
||||||
const handleData = async (data: any, file: File) => {
|
const handleData = async (data: any, file: File) => {
|
||||||
|
try {
|
||||||
console.log("Imported Data:", data);
|
console.log("Imported Data:", data);
|
||||||
console.log("File:", file);
|
console.log("File:", file);
|
||||||
setImportedData(data);
|
setImportedData(data);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
toast.success("Data imported successfully");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to import data");
|
||||||
|
console.error("Import error:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4">
|
<motion.div
|
||||||
<div className="flex justify-between items-center mb-6">
|
layout
|
||||||
<h1 className="text-2xl font-bold">Import Data</h1>
|
transition={{
|
||||||
<Button onClick={() => setIsOpen(true)}>
|
layout: {
|
||||||
Import Spreadsheet
|
duration: 0.15,
|
||||||
|
ease: [0.4, 0, 0.2, 1]
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="container mx-auto py-6 space-y-4"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
layout="position"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
className="flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">Add New Products</h1>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<Card className="max-w-[400px]">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Import Data</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Button onClick={() => setIsOpen(true)} className="w-full">
|
||||||
|
Upload Spreadsheet
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{importedData && (
|
{importedData && (
|
||||||
<div className="mt-6">
|
|
||||||
<h2 className="text-xl font-semibold mb-4">Imported Data</h2>
|
|
||||||
<Card>
|
<Card>
|
||||||
<ScrollArea className="h-[500px] w-full rounded-md border p-4">
|
<CardHeader>
|
||||||
<Code>
|
<CardTitle>Preview Imported Data</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Code className="p-4 w-full rounded-md border">
|
||||||
{JSON.stringify(importedData, null, 2)}
|
{JSON.stringify(importedData, null, 2)}
|
||||||
</Code>
|
</Code>
|
||||||
</ScrollArea>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ReactSpreadsheetImport
|
<ReactSpreadsheetImport
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={() => setIsOpen(false)}
|
onClose={() => setIsOpen(false)}
|
||||||
onSubmit={handleData}
|
onSubmit={handleData}
|
||||||
fields={fields}
|
fields={IMPORT_FIELDS}
|
||||||
/>
|
/>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/zsh
|
#!/bin/zsh
|
||||||
|
|
||||||
#Clear previous mount in case it’s still there
|
#Clear previous mount in case it’s still there
|
||||||
umount "/Users/matt/Library/Mobile Documents/com~apple~CloudDocs/Dev/inventory/inventory-server"
|
umount '/Users/matt/Library/Mobile Documents/com~apple~CloudDocs/Dev/inventory/inventory-server'
|
||||||
|
|
||||||
#Mount
|
#Mount
|
||||||
sshfs matt@dashboard.kent.pw:/var/www/html/inventory -p 22122 "/Users/matt/Library/Mobile Documents/com~apple~CloudDocs/Dev/inventory/inventory-server/"
|
sshfs matt@dashboard.kent.pw:/var/www/html/inventory -p 22122 '/Users/matt/Library/Mobile Documents/com~apple~CloudDocs/Dev/inventory/inventory-server/'
|
||||||
Reference in New Issue
Block a user