Import company and person from csv file (#1236)
* feat: wip implement back-end call csv import * fix: rebase IconBrandTwitter missing * feat: person and company csv import * fix: test & clean * fix: clean & test
This commit is contained in:
@ -0,0 +1,291 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ContinueButton } from '@/spreadsheet-import/components/ContinueButton';
|
||||
import { Heading } from '@/spreadsheet-import/components/Heading';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import type { Field, RawData } from '@/spreadsheet-import/types';
|
||||
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
|
||||
import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns';
|
||||
import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableData';
|
||||
import { setColumn } from '@/spreadsheet-import/utils/setColumn';
|
||||
import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn';
|
||||
import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn';
|
||||
import { ButtonVariant } from '@/ui/button/components/Button';
|
||||
import { useDialog } from '@/ui/dialog/hooks/useDialog';
|
||||
import { Modal } from '@/ui/modal/components/Modal';
|
||||
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
||||
|
||||
import { ColumnGrid } from './components/ColumnGrid';
|
||||
import { TemplateColumn } from './components/TemplateColumn';
|
||||
import { UserTableColumn } from './components/UserTableColumn';
|
||||
|
||||
const StyledContent = styled(Modal.Content)`
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledColumnsContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledColumns = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledColumn = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
`;
|
||||
|
||||
export type MatchColumnsProps<T extends string> = {
|
||||
data: RawData[];
|
||||
headerValues: RawData;
|
||||
onContinue: (data: any[], rawData: RawData[], columns: Columns<T>) => void;
|
||||
};
|
||||
|
||||
export enum ColumnType {
|
||||
empty,
|
||||
ignored,
|
||||
matched,
|
||||
matchedCheckbox,
|
||||
matchedSelect,
|
||||
matchedSelectOptions,
|
||||
}
|
||||
|
||||
export type MatchedOptions<T> = {
|
||||
entry: string;
|
||||
value: T;
|
||||
};
|
||||
|
||||
type EmptyColumn = { type: ColumnType.empty; index: number; header: string };
|
||||
type IgnoredColumn = {
|
||||
type: ColumnType.ignored;
|
||||
index: number;
|
||||
header: string;
|
||||
};
|
||||
type MatchedColumn<T> = {
|
||||
type: ColumnType.matched;
|
||||
index: number;
|
||||
header: string;
|
||||
value: T;
|
||||
};
|
||||
type MatchedSwitchColumn<T> = {
|
||||
type: ColumnType.matchedCheckbox;
|
||||
index: number;
|
||||
header: string;
|
||||
value: T;
|
||||
};
|
||||
export type MatchedSelectColumn<T> = {
|
||||
type: ColumnType.matchedSelect;
|
||||
index: number;
|
||||
header: string;
|
||||
value: T;
|
||||
matchedOptions: Partial<MatchedOptions<T>>[];
|
||||
};
|
||||
export type MatchedSelectOptionsColumn<T> = {
|
||||
type: ColumnType.matchedSelectOptions;
|
||||
index: number;
|
||||
header: string;
|
||||
value: T;
|
||||
matchedOptions: MatchedOptions<T>[];
|
||||
};
|
||||
|
||||
export type Column<T extends string> =
|
||||
| EmptyColumn
|
||||
| IgnoredColumn
|
||||
| MatchedColumn<T>
|
||||
| MatchedSwitchColumn<T>
|
||||
| MatchedSelectColumn<T>
|
||||
| MatchedSelectOptionsColumn<T>;
|
||||
|
||||
export type Columns<T extends string> = Column<T>[];
|
||||
|
||||
export const MatchColumnsStep = <T extends string>({
|
||||
data,
|
||||
headerValues,
|
||||
onContinue,
|
||||
}: MatchColumnsProps<T>) => {
|
||||
const { enqueueDialog } = useDialog();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const dataExample = data.slice(0, 2);
|
||||
const { fields, autoMapHeaders, autoMapDistance } =
|
||||
useSpreadsheetImportInternal<T>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [columns, setColumns] = useState<Columns<T>>(
|
||||
// Do not remove spread, it indexes empty array elements, otherwise map() skips over them
|
||||
([...headerValues] as string[]).map((value, index) => ({
|
||||
type: ColumnType.empty,
|
||||
index,
|
||||
header: value ?? '',
|
||||
})),
|
||||
);
|
||||
const onIgnore = useCallback(
|
||||
(columnIndex: number) => {
|
||||
setColumns(
|
||||
columns.map((column, index) =>
|
||||
columnIndex === index ? setIgnoreColumn<T>(column) : column,
|
||||
),
|
||||
);
|
||||
},
|
||||
[columns, setColumns],
|
||||
);
|
||||
|
||||
const onRevertIgnore = useCallback(
|
||||
(columnIndex: number) => {
|
||||
setColumns(
|
||||
columns.map((column, index) =>
|
||||
columnIndex === index ? setColumn(column) : column,
|
||||
),
|
||||
);
|
||||
},
|
||||
[columns, setColumns],
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
(value: T, columnIndex: number) => {
|
||||
if (value === 'do-not-import') {
|
||||
if (columns[columnIndex].type === ColumnType.ignored) {
|
||||
onRevertIgnore(columnIndex);
|
||||
} else {
|
||||
onIgnore(columnIndex);
|
||||
}
|
||||
} else {
|
||||
const field = fields.find(
|
||||
(field) => field.key === value,
|
||||
) as unknown as Field<T>;
|
||||
const existingFieldIndex = columns.findIndex(
|
||||
(column) => 'value' in column && column.value === field.key,
|
||||
);
|
||||
setColumns(
|
||||
columns.map<Column<T>>((column, index) => {
|
||||
if (columnIndex === index) {
|
||||
return setColumn(column, field, data);
|
||||
} else if (index === existingFieldIndex) {
|
||||
enqueueSnackBar('Columns cannot duplicate', {
|
||||
title: 'Another column unselected',
|
||||
variant: 'error',
|
||||
});
|
||||
return setColumn(column);
|
||||
} else {
|
||||
return column;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
[columns, onRevertIgnore, onIgnore, fields, data, enqueueSnackBar],
|
||||
);
|
||||
|
||||
const onSubChange = useCallback(
|
||||
(value: string, columnIndex: number, entry: string) => {
|
||||
setColumns(
|
||||
columns.map((column, index) =>
|
||||
columnIndex === index && 'matchedOptions' in column
|
||||
? setSubColumn(column, entry, value)
|
||||
: column,
|
||||
),
|
||||
);
|
||||
},
|
||||
[columns, setColumns],
|
||||
);
|
||||
const unmatchedRequiredFields = useMemo(
|
||||
() => findUnmatchedRequiredFields(fields, columns),
|
||||
[fields, columns],
|
||||
);
|
||||
|
||||
const handleAlertOnContinue = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
await onContinue(normalizeTableData(columns, data, fields), data, columns);
|
||||
setIsLoading(false);
|
||||
}, [onContinue, columns, data, fields]);
|
||||
|
||||
const handleOnContinue = useCallback(async () => {
|
||||
if (unmatchedRequiredFields.length > 0) {
|
||||
enqueueDialog({
|
||||
title: 'Not all columns matched',
|
||||
message:
|
||||
'There are required columns that are not matched or ignored. Do you want to continue?',
|
||||
children: (
|
||||
<StyledColumnsContainer>
|
||||
<StyledColumns>Columns not matched:</StyledColumns>
|
||||
{unmatchedRequiredFields.map((field) => (
|
||||
<StyledColumn key={field}>{field}</StyledColumn>
|
||||
))}
|
||||
</StyledColumnsContainer>
|
||||
),
|
||||
buttons: [
|
||||
{ title: 'Cancel' },
|
||||
{
|
||||
title: 'Continue',
|
||||
onClick: handleAlertOnContinue,
|
||||
variant: ButtonVariant.Primary,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
setIsLoading(true);
|
||||
await onContinue(
|
||||
normalizeTableData(columns, data, fields),
|
||||
data,
|
||||
columns,
|
||||
);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [
|
||||
unmatchedRequiredFields,
|
||||
enqueueDialog,
|
||||
handleAlertOnContinue,
|
||||
onContinue,
|
||||
columns,
|
||||
data,
|
||||
fields,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoMapHeaders) {
|
||||
setColumns(getMatchedColumns(columns, fields, data, autoMapDistance));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContent>
|
||||
<Heading
|
||||
title="Match Columns"
|
||||
description="Select the correct field for each column you'd like to import."
|
||||
/>
|
||||
<ColumnGrid
|
||||
columns={columns}
|
||||
renderUserColumn={(columns, columnIndex) => (
|
||||
<UserTableColumn
|
||||
column={columns[columnIndex]}
|
||||
entries={dataExample.map(
|
||||
(row) => row[columns[columnIndex].index],
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
renderTemplateColumn={(columns, columnIndex) => (
|
||||
<TemplateColumn
|
||||
columns={columns}
|
||||
columnIndex={columnIndex}
|
||||
onChange={onChange}
|
||||
onSubChange={onSubChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledContent>
|
||||
<ContinueButton
|
||||
isLoading={isLoading}
|
||||
onContinue={handleOnContinue}
|
||||
title="Next"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import type { Columns } from '../MatchColumnsStep';
|
||||
|
||||
const GridContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
height: 0px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Grid = styled.div`
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||
width: 75%;
|
||||
`;
|
||||
|
||||
type HeightProps = {
|
||||
height?: `${number}px`;
|
||||
};
|
||||
|
||||
const GridRow = styled.div<HeightProps>`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: ${({ height = '64px' }) => height};
|
||||
`;
|
||||
|
||||
type PositionProps = {
|
||||
position: 'left' | 'right';
|
||||
};
|
||||
|
||||
const GridCell = styled.div<PositionProps>`
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
padding-top: ${({ theme }) => theme.spacing(4)};
|
||||
${({ position, theme }) => {
|
||||
if (position === 'left') {
|
||||
return `
|
||||
padding-left: ${theme.spacing(4)};
|
||||
padding-right: ${theme.spacing(2)};
|
||||
`;
|
||||
}
|
||||
return `
|
||||
padding-left: ${theme.spacing(2)};
|
||||
padding-right: ${theme.spacing(4)};
|
||||
`;
|
||||
}};
|
||||
`;
|
||||
|
||||
const GridHeader = styled.div<PositionProps>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
box-sizing: border-box;
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: flex;
|
||||
flex: 1;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
padding-left: ${({ theme }) => theme.spacing(4)};
|
||||
padding-right: ${({ theme }) => theme.spacing(4)};
|
||||
${({ position, theme }) => {
|
||||
if (position === 'left') {
|
||||
return `border-top-left-radius: calc(${theme.border.radius.md} - 1px);`;
|
||||
}
|
||||
return `border-top-right-radius: calc(${theme.border.radius.md} - 1px);`;
|
||||
}};
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
type ColumnGridProps<T extends string> = {
|
||||
columns: Columns<T>;
|
||||
renderUserColumn: (
|
||||
columns: Columns<T>,
|
||||
columnIndex: number,
|
||||
) => React.ReactNode;
|
||||
renderTemplateColumn: (
|
||||
columns: Columns<T>,
|
||||
columnIndex: number,
|
||||
) => React.ReactNode;
|
||||
};
|
||||
|
||||
export const ColumnGrid = <T extends string>({
|
||||
columns,
|
||||
renderUserColumn,
|
||||
renderTemplateColumn,
|
||||
}: ColumnGridProps<T>) => {
|
||||
return (
|
||||
<>
|
||||
<GridContainer>
|
||||
<Grid>
|
||||
<GridRow height="29px">
|
||||
<GridHeader position="left">Imported data</GridHeader>
|
||||
<GridHeader position="right">Twenty fields</GridHeader>
|
||||
</GridRow>
|
||||
{columns.map((column, index) => {
|
||||
const userColumn = renderUserColumn(columns, index);
|
||||
const templateColumn = renderTemplateColumn(columns, index);
|
||||
|
||||
if (React.isValidElement(userColumn)) {
|
||||
return (
|
||||
<GridRow key={index}>
|
||||
<GridCell position="left">{userColumn}</GridCell>
|
||||
<GridCell position="right">{templateColumn}</GridCell>
|
||||
</GridRow>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</Grid>
|
||||
</GridContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { SelectOption } from '@/spreadsheet-import/types';
|
||||
import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions';
|
||||
|
||||
import type {
|
||||
MatchedOptions,
|
||||
MatchedSelectColumn,
|
||||
MatchedSelectOptionsColumn,
|
||||
} from '../MatchColumnsStep';
|
||||
|
||||
const Container = styled.div`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const SelectLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
interface Props<T> {
|
||||
option: MatchedOptions<T> | Partial<MatchedOptions<T>>;
|
||||
column: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T>;
|
||||
onSubChange: (val: T, index: number, option: string) => void;
|
||||
}
|
||||
|
||||
export const SubMatchingSelect = <T extends string>({
|
||||
option,
|
||||
column,
|
||||
onSubChange,
|
||||
}: Props<T>) => {
|
||||
const { fields } = useSpreadsheetImportInternal<T>();
|
||||
const options = getFieldOptions(fields, column.value) as SelectOption[];
|
||||
const value = options.find((opt) => opt.value === option.value);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<SelectLabel>{option.entry}</SelectLabel>
|
||||
<MatchColumnSelect
|
||||
value={value}
|
||||
placeholder="Select..."
|
||||
onChange={(value) =>
|
||||
onSubChange(value?.value as T, column.index, option.entry ?? '')
|
||||
}
|
||||
options={options}
|
||||
name={option.entry}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,160 @@
|
||||
// TODO: We should create our own accordion component
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton as ChakraAccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
} from '@chakra-ui/accordion';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import type { Fields } from '@/spreadsheet-import/types';
|
||||
import { IconChevronDown, IconForbid } from '@/ui/icon';
|
||||
|
||||
import type { Column, Columns } from '../MatchColumnsStep';
|
||||
import { ColumnType } from '../MatchColumnsStep';
|
||||
|
||||
import { SubMatchingSelect } from './SubMatchingSelect';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 10px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const AccordionButton = styled(ChakraAccordionButton)`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.accent.secondary};
|
||||
border: none;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-sizing: border-box;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.accent.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const AccordionContainer = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const AccordionLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
flex: 1;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
const getAccordionTitle = <T extends string>(
|
||||
fields: Fields<T>,
|
||||
column: Column<T>,
|
||||
) => {
|
||||
const fieldLabel = fields.find(
|
||||
(field) => 'value' in column && field.key === column.value,
|
||||
)?.label;
|
||||
|
||||
return `Match ${fieldLabel} (${
|
||||
'matchedOptions' in column && column.matchedOptions.length
|
||||
} Unmatched)`;
|
||||
};
|
||||
|
||||
type TemplateColumnProps<T extends string> = {
|
||||
columns: Columns<T>;
|
||||
columnIndex: number;
|
||||
onChange: (val: T, index: number) => void;
|
||||
onSubChange: (val: T, index: number, option: string) => void;
|
||||
};
|
||||
|
||||
export const TemplateColumn = <T extends string>({
|
||||
columns,
|
||||
columnIndex,
|
||||
onChange,
|
||||
onSubChange,
|
||||
}: TemplateColumnProps<T>) => {
|
||||
const { fields } = useSpreadsheetImportInternal<T>();
|
||||
const column = columns[columnIndex];
|
||||
const isIgnored = column.type === ColumnType.ignored;
|
||||
const isSelect = 'matchedOptions' in column;
|
||||
const fieldOptions = fields.map(({ icon, label, key }) => {
|
||||
const isSelected =
|
||||
columns.findIndex((column) => {
|
||||
if ('value' in column) {
|
||||
return column.value === key;
|
||||
}
|
||||
|
||||
return false;
|
||||
}) !== -1;
|
||||
|
||||
return {
|
||||
icon,
|
||||
value: key,
|
||||
label,
|
||||
disabled: isSelected,
|
||||
} as const;
|
||||
});
|
||||
const selectOptions = [
|
||||
{
|
||||
icon: <IconForbid />,
|
||||
value: 'do-not-import',
|
||||
label: 'Do not import',
|
||||
},
|
||||
...fieldOptions,
|
||||
];
|
||||
const selectValue = fieldOptions.find(
|
||||
({ value }) => 'value' in column && column.value === value,
|
||||
);
|
||||
const ignoreValue = selectOptions.find(
|
||||
({ value }) => value === 'do-not-import',
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<MatchColumnSelect
|
||||
placeholder="Select column..."
|
||||
value={isIgnored ? ignoreValue : selectValue}
|
||||
onChange={(value) => onChange(value?.value as T, column.index)}
|
||||
options={selectOptions}
|
||||
name={column.header}
|
||||
/>
|
||||
{isSelect && (
|
||||
<AccordionContainer>
|
||||
<Accordion allowMultiple width="100%">
|
||||
<AccordionItem border="none" py={1}>
|
||||
<AccordionButton data-testid="accordion-button">
|
||||
<AccordionLabel>
|
||||
{getAccordionTitle<T>(fields, column)}
|
||||
</AccordionLabel>
|
||||
<AccordionIcon as={IconChevronDown} />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} pr={3} display="flex" flexDir="column">
|
||||
{column.matchedOptions.map((option) => (
|
||||
<SubMatchingSelect
|
||||
option={option}
|
||||
column={column}
|
||||
onSubChange={onSubChange}
|
||||
key={option.entry}
|
||||
/>
|
||||
))}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</AccordionContainer>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,50 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import type { RawData } from '@/spreadsheet-import/types';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
|
||||
import type { Column } from '../MatchColumnsStep';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Value = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const Example = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
type UserTableColumnProps<T extends string> = {
|
||||
column: Column<T>;
|
||||
entries: RawData;
|
||||
};
|
||||
|
||||
export const UserTableColumn = <T extends string>({
|
||||
column,
|
||||
entries,
|
||||
}: UserTableColumnProps<T>) => {
|
||||
const { header } = column;
|
||||
const entry = entries.find(assertNotNull);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Value>{header}</Value>
|
||||
{entry && <Example>{`ex: ${entry}`}</Example>}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user