Files
rowy/src/components/TableModals/ImportAirtableWizard/Step1Columns.tsx
2022-11-17 18:17:43 +11:00

349 lines
12 KiB
TypeScript

import { useState } from "react";
import { useAtom } from "jotai";
import useMemoValue from "use-memo-value";
import { find, findIndex, camelCase, isEqual } from "lodash-es";
import {
Grid,
Typography,
Divider,
FormControlLabel,
Checkbox,
Chip,
FormControl,
RadioGroup,
Radio,
Stack,
Box,
} from "@mui/material";
import ArrowIcon from "@mui/icons-material/ArrowForward";
import { TableColumn as TableColumnIcon } from "@src/assets/icons";
import { IStepProps } from ".";
import { AirtableConfig } from "@src/components/TableModals/ImportAirtableWizard";
import FadeList from "@src/components/TableModals/ScrollableList";
import Column, {
COLUMN_HEADER_HEIGHT,
} from "@src/components/Table/Mock/Column";
import ColumnSelect from "@src/components/Table/ColumnSelect";
import {
tableScope,
tableSchemaAtom,
tableColumnsOrderedAtom,
} from "@src/atoms/tableScope";
import { FieldType } from "@src/constants/fields";
import { getFieldProp } from "@src/components/fields";
import { suggestType } from "@src/components/TableModals/ImportAirtableWizard/utils";
export default function Step1Columns({
airtableData,
config,
updateConfig,
setConfig,
isXs,
}: IStepProps) {
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
const tableColumns = useMemoValue(
tableColumnsOrdered
.filter((column) => column.type !== FieldType.id)
.map((column) => ({ label: column.name, value: column.fieldName })),
isEqual
);
const [selectedFields, setSelectedFields] = useState(
config.pairs.map((pair) => pair.fieldKey)
);
// When a field is selected to be imported
const handleSelect =
(field: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
const checked = e.target.checked;
if (checked) {
setSelectedFields((x) => [...x, field]);
// Try to match the field to a column in the table
const match =
find(tableColumns, (column) =>
column.label.toLowerCase().includes(field.toLowerCase())
)?.value ?? null;
const columnKey = camelCase(field);
const columnConfig: Partial<AirtableConfig> = {
pairs: [],
newColumns: [],
};
columnConfig.pairs = [
{ fieldKey: field, columnKey: match ?? columnKey },
];
if (!match) {
columnConfig.newColumns = [
{
name: field,
fieldName: columnKey,
key: columnKey,
type:
suggestType(airtableData.records, field) || FieldType.shortText,
index: -1,
config: {},
},
];
}
updateConfig(columnConfig);
} else {
const newValue = [...selectedFields];
newValue.splice(newValue.indexOf(field), 1);
setSelectedFields(newValue);
// Check if this pair was already pushed to main config
const configPair = find(config.pairs, { fieldKey: field });
const configIndex = findIndex(config.pairs, { fieldKey: field });
// Delete matching newColumn if it was created
if (configPair) {
const newColumnIndex = findIndex(config.newColumns, {
key: configPair.columnKey,
});
if (newColumnIndex > -1) {
const newColumns = [...config.newColumns];
newColumns.splice(newColumnIndex, 1);
setConfig((config) => ({ ...config, newColumns }));
}
}
// Delete pair from main config
if (configIndex > -1) {
const newConfig = [...config.pairs];
newConfig.splice(configIndex, 1);
setConfig((config) => ({ ...config, pairs: newConfig }));
}
}
};
// When a field is mapped to a new column
const handleChange = (fieldKey: string) => (value: string) => {
if (!value) return;
const columnKey = !!tableSchema.columns?.[value] ? value : camelCase(value);
if (columnKey === "") return;
// Check if this pair already exists in config
const configIndex = findIndex(config.pairs, { fieldKey });
console.log(columnKey, configIndex);
if (configIndex > -1) {
const pairs = [...config.pairs];
pairs[configIndex].columnKey = columnKey;
setConfig((config) => ({ ...config, pairs }));
} else {
updateConfig({
pairs: [{ fieldKey, columnKey }],
});
}
if (!tableSchema.columns?.[value]) {
updateConfig({
newColumns: [
{
name: value,
fieldName: columnKey,
key: columnKey,
type:
suggestType(airtableData.records, fieldKey) ||
FieldType.shortText,
index: -1,
config: {},
},
],
});
}
};
const fieldKeys = Object.keys(airtableData.records[0].fields);
return (
<div>
<Grid container spacing={7}>
{!isXs && (
<Grid item xs={12} sm={6}>
<Typography variant="subtitle2" gutterBottom component="h2">
Select columns ({config.pairs.length} of {fieldKeys.length})
</Typography>
</Grid>
)}
<Grid item xs={12} sm={6}>
<Typography variant="subtitle2" gutterBottom component="h2">
Table columns
</Typography>
</Grid>
</Grid>
<Divider />
<FadeList>
{fieldKeys.map((field) => {
const selected = selectedFields.indexOf(field) > -1;
const columnKey =
find(config.pairs, { fieldKey: field })?.columnKey ?? null;
const matchingColumn = columnKey
? tableSchema.columns?.[columnKey] ??
find(config.newColumns, { key: columnKey }) ??
null
: null;
const isNewColumn = !!find(config.newColumns, { key: columnKey });
return (
<Grid container key={field} component="li" wrap="nowrap">
<Grid item xs>
<FormControlLabel
key={field}
control={
<Checkbox
checked={selected}
aria-label={`Select column ${field}`}
onChange={handleSelect(field)}
color="secondary"
/>
}
label={<Column label={field} />}
sx={{
marginRight: 0,
flex: 1,
alignItems: "center",
"& .MuiFormControlLabel-label": { mt: 0, flex: 1 },
}}
/>
</Grid>
<Grid
item
sx={{
width: (theme) => theme.spacing(7),
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<ArrowIcon color="disabled" sx={{ color: "secondary.main" }} />
</Grid>
<Grid item xs>
{selected && (
<ColumnSelect
multiple={false}
value={columnKey}
onChange={handleChange(field) as any}
TextFieldProps={{
hiddenLabel: true,
SelectProps: {
renderValue: () => {
if (!columnKey) return "Select or add column";
else
return (
<Stack
direction="row"
gap={1}
alignItems="center"
>
<Box sx={{ width: 24, height: 24 }}>
{!isNewColumn ? (
getFieldProp("icon", matchingColumn?.type)
) : (
<TableColumnIcon color="disabled" />
)}
</Box>
{matchingColumn?.name}
{isNewColumn && (
<Chip
label="New"
color="primary"
size="small"
variant="outlined"
style={{
marginLeft: "auto",
pointerEvents: "none",
height: 24,
fontWeight: "normal",
}}
/>
)}
</Stack>
);
},
sx: [
{
backgroundColor: "background.default",
border: (theme) =>
`1px solid ${theme.palette.divider}`,
borderRadius: 0,
boxShadow: "none",
"& .MuiSelect-select": {
boxSizing: "border-box",
height: COLUMN_HEADER_HEIGHT - 2,
typography: "caption",
fontWeight: "medium",
lineHeight: "28px",
},
color: "text.secondary",
"&:hover": {
backgroundColor: "background.default",
color: "text.primary",
boxShadow: "none",
},
"&::before": { content: "none" },
"&::after": { pointerEvents: "none" },
},
!columnKey && { color: "text.disabled" },
],
},
sx: { "& .MuiInputLabel-root": { display: "none" } },
}}
clearable={false}
displayEmpty
freeText
AddButtonProps={{ children: "Create column…" }}
AddDialogProps={{
title: "Create column",
textFieldLabel: "Column name",
}}
/>
)}
</Grid>
</Grid>
);
})}
</FadeList>
<Grid container marginTop={2}>
<Typography variant="subtitle2" gutterBottom component="h2">
Document Ids (Optional)
</Typography>
<Divider />
<Grid item xs={12}>
<FormControl>
<RadioGroup
defaultValue="recordId"
name="radio-buttons-group"
sx={{ flexDirection: "row" }}
onChange={(e) => {
const documentId = e.currentTarget.value as "auto" | "recordId";
setConfig((prev: AirtableConfig) => ({ ...prev, documentId }));
}}
>
<FormControlLabel
value="recordId"
control={<Radio />}
label="Use Airtable Record ID"
/>
<FormControlLabel
value="auto"
control={<Radio />}
label="Auto-Generated"
/>
</RadioGroup>
</FormControl>
</Grid>
</Grid>
</div>
);
}