mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
feat(airtable-migration): clean up for pr
This commit is contained in:
@@ -100,16 +100,27 @@ export const tableModalAtom = atomWithHash<
|
||||
| "export"
|
||||
| "importExisting"
|
||||
| "importCsv"
|
||||
| "importAirtable"
|
||||
| null
|
||||
>("tableModal", null, { replaceState: true });
|
||||
|
||||
export type ImportCsvData = { columns: string[]; rows: Record<string, any>[] };
|
||||
export type ImportAirtableData = { records: Record<string, any>[] };
|
||||
|
||||
/** Store import CSV popover and wizard state */
|
||||
export const importCsvAtom = atom<{
|
||||
importType: "csv" | "tsv";
|
||||
csvData: ImportCsvData | null;
|
||||
}>({ importType: "csv", csvData: null });
|
||||
|
||||
/** Store import Airtable popover and wizard state */
|
||||
export const importAirtableAtom = atom<{
|
||||
airtableData: ImportAirtableData | null;
|
||||
apiKey: string;
|
||||
baseId: string;
|
||||
tableId: string;
|
||||
}>({ airtableData: null, apiKey: "", baseId: "", tableId: "" });
|
||||
|
||||
/** Store side drawer open state */
|
||||
export const sideDrawerOpenAtom = atom(false);
|
||||
|
||||
|
||||
@@ -53,13 +53,6 @@ export interface IStepProps {
|
||||
|
||||
export const airtableFieldParser = (fieldType: FieldType) => {
|
||||
switch (fieldType) {
|
||||
case FieldType.percentage:
|
||||
return (v: string) => {
|
||||
const numValue = parseFloat(v && v.includes("%") ? v.slice(0, -1) : v);
|
||||
return isNaN(numValue) ? null : numValue / 100;
|
||||
};
|
||||
case FieldType.multiSelect:
|
||||
return (v: string[]) => v;
|
||||
case FieldType.date:
|
||||
case FieldType.dateTime:
|
||||
return (v: string) => {
|
||||
@@ -67,7 +60,7 @@ export const airtableFieldParser = (fieldType: FieldType) => {
|
||||
return isValidDate(date) ? date.getTime() : null;
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
return (v: string) => v;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -176,31 +169,31 @@ export default function ImportAirtableWizard({ onClose }: ITableModalProps) {
|
||||
|
||||
// Airtable Rate Limits: 5 req/sec
|
||||
const RATE_LIMIT = { REQ_PER_SECOND: 5 };
|
||||
const fetcher = async (i: number, offset?: string): Promise<void> => {
|
||||
const fetcher = async (i: number = 0, offset?: string): Promise<void> => {
|
||||
console.log(i, offset, promises);
|
||||
if (offset) {
|
||||
const { records, offset: nextPage } = await fetchRecords(offset);
|
||||
snackbarProgressRef.current?.setTarget(
|
||||
(prev) => prev + records.length
|
||||
);
|
||||
promises.push(
|
||||
bulkAddRows({
|
||||
rows: parseRecords(records),
|
||||
collection: tableSettings.collection,
|
||||
}).then(() => {
|
||||
countRef.current += records.length;
|
||||
snackbarProgressRef.current?.setProgress(
|
||||
(prev) => prev + records.length
|
||||
);
|
||||
})
|
||||
);
|
||||
if (i < RATE_LIMIT.REQ_PER_SECOND - 1) {
|
||||
promises.push(fetcher(++i, nextPage));
|
||||
} else {
|
||||
promises.push(timeout(1050).then(() => fetcher(0, nextPage)));
|
||||
}
|
||||
const { records, offset: nextPage } = await fetchRecords(offset);
|
||||
snackbarProgressRef.current?.setTarget((prev) => prev + records.length);
|
||||
promises.push(
|
||||
bulkAddRows({
|
||||
rows: parseRecords(records),
|
||||
collection: tableSettings.collection,
|
||||
}).then(() => {
|
||||
countRef.current += records.length;
|
||||
snackbarProgressRef.current?.setProgress(
|
||||
(prev) => prev + records.length
|
||||
);
|
||||
})
|
||||
);
|
||||
if (!nextPage) {
|
||||
return;
|
||||
}
|
||||
if (i < RATE_LIMIT.REQ_PER_SECOND - 1) {
|
||||
promises.push(fetcher(++i, nextPage));
|
||||
} else {
|
||||
promises.push(timeout(1050).then(() => fetcher(0, nextPage)));
|
||||
}
|
||||
};
|
||||
|
||||
const resolveAll = async (): Promise<void[]> => {
|
||||
return Promise.all(promises).then((result) => {
|
||||
if (result.length === promises.length) {
|
||||
@@ -215,10 +208,7 @@ export default function ImportAirtableWizard({ onClose }: ITableModalProps) {
|
||||
for (const col of config.newColumns)
|
||||
promises.push(addColumn({ config: col }));
|
||||
|
||||
const { records, offset: nextPage } = await fetchRecords();
|
||||
snackbarProgressRef.current?.setTarget(records.length);
|
||||
|
||||
await fetcher(1, nextPage);
|
||||
await fetcher();
|
||||
await resolveAll();
|
||||
|
||||
enqueueSnackbar(
|
||||
|
||||
@@ -95,8 +95,8 @@ export default function Step1Columns({
|
||||
};
|
||||
|
||||
const handleChange = (fieldKey: string) => (value: string) => {
|
||||
if (!value) return;
|
||||
const columnKey = !!tableSchema.columns?.[value] ? value : camelCase(value);
|
||||
console.log(config);
|
||||
// Check if this pair already exists in config
|
||||
const configIndex = findIndex(config.pairs, { fieldKey });
|
||||
if (configIndex > -1) {
|
||||
|
||||
@@ -136,11 +136,9 @@ export default function Step2NewColumns({
|
||||
<Grid item xs style={{ overflow: "hidden" }}>
|
||||
<Cell
|
||||
field={config.newColumns[fieldToEdit].key}
|
||||
value={
|
||||
airtableFieldParser(config.newColumns[fieldToEdit].type)?.(
|
||||
cell
|
||||
) ?? cell
|
||||
}
|
||||
value={airtableFieldParser(
|
||||
config.newColumns[fieldToEdit].type
|
||||
)?.(cell)}
|
||||
type={config.newColumns[fieldToEdit].type}
|
||||
name={config.newColumns[fieldToEdit].name}
|
||||
/>
|
||||
|
||||
@@ -63,10 +63,7 @@ export default function Step3Preview({ airtableData, config }: IStepProps) {
|
||||
<Cell
|
||||
key={fieldKey + i}
|
||||
field={columnKey}
|
||||
value={
|
||||
airtableFieldParser(type)?.(record.fields[fieldKey]) ??
|
||||
record.fields[fieldKey]
|
||||
}
|
||||
value={airtableFieldParser(type)?.(record.fields[fieldKey])}
|
||||
type={type}
|
||||
name={name}
|
||||
/>
|
||||
|
||||
2
src/components/TableModals/ImportAirtableWizard/index.ts
Normal file
2
src/components/TableModals/ImportAirtableWizard/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./ImportAirtableWizard";
|
||||
export { default } from "./ImportAirtableWizard";
|
||||
@@ -15,6 +15,8 @@ const WebhooksModal = lazy(() => import("./WebhooksModal" /* webpackChunkName: "
|
||||
const ImportExistingWizard = lazy(() => import("./ImportExistingWizard" /* webpackChunkName: "TableModals-ImportExistingWizard" */));
|
||||
// prettier-ignore
|
||||
const ImportCsvWizard = lazy(() => import("./ImportCsvWizard" /* webpackChunkName: "TableModals-ImportCsvWizard" */));
|
||||
// prettier-ignore
|
||||
const ImportAirtableWizard = lazy(() => import("./ImportAirtableWizard" /* webpackChunkName: "TableModals-ImportAirtableWizard" */));
|
||||
|
||||
export interface ITableModalProps {
|
||||
onClose: () => void;
|
||||
@@ -34,6 +36,8 @@ export default function TableModals() {
|
||||
if (tableModal === "importExisting")
|
||||
return <ImportExistingWizard onClose={onClose} />;
|
||||
if (tableModal === "importCsv") return <ImportCsvWizard onClose={onClose} />;
|
||||
if (tableModal === "importAirtable")
|
||||
return <ImportAirtableWizard onClose={onClose} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
tableSettingsAtom,
|
||||
tableModalAtom,
|
||||
importCsvAtom,
|
||||
importAirtableAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
|
||||
@@ -51,19 +52,23 @@ export interface IImportCsvProps {
|
||||
export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
const [userRoles] = useAtom(userRolesAtom, globalScope);
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const [{ importType, csvData }, setImportCsv] = useAtom(
|
||||
const [{ importType: importTypeCsv, csvData }, setImportCsv] = useAtom(
|
||||
importCsvAtom,
|
||||
tableScope
|
||||
);
|
||||
const [{ airtableData, baseId, tableId, apiKey }, setImportAirtable] =
|
||||
useAtom(importAirtableAtom, tableScope);
|
||||
|
||||
const openTableModal = useSetAtom(tableModalAtom, tableScope);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const importTypeRef = useRef(importType);
|
||||
const importTypeRef = useRef(importTypeCsv);
|
||||
const importMethodRef = useRef(ImportMethod.upload);
|
||||
const [open, setOpen] = useState<HTMLButtonElement | null>(null);
|
||||
const [tab, setTab] = useState("upload");
|
||||
|
||||
const [error, setError] = useState<string | any>("");
|
||||
const [airtableError, setAirtableError] = useState<any>({});
|
||||
const validCsv =
|
||||
csvData !== null && csvData?.columns.length > 0 && csvData?.rows.length > 0;
|
||||
|
||||
@@ -171,42 +176,44 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
const [airtable, setAirtable] = useState<any>({
|
||||
apiKey: "",
|
||||
baseID: "",
|
||||
tableID: "",
|
||||
});
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [validConnection, setValidConnection] = useState(false);
|
||||
const handleAirtableConnection = () => {
|
||||
if (!airtable.apiKey) {
|
||||
setError({ apiKey: { message: "API Key is missing!" } });
|
||||
if (!apiKey) {
|
||||
setAirtableError({ apiKey: { message: "API Key is missing!" } });
|
||||
return;
|
||||
}
|
||||
if (!airtable.baseID) {
|
||||
setError({ baseID: { message: "Base ID is missing!" } });
|
||||
if (!baseId) {
|
||||
setAirtableError({ baseId: { message: "Base ID is missing!" } });
|
||||
return;
|
||||
}
|
||||
if (!airtable.tableID) {
|
||||
setError({ tableID: { message: "Table ID is missing!" } });
|
||||
if (!tableId) {
|
||||
setAirtableError({ tableId: { message: "Table ID is missing!" } });
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
fetch(
|
||||
`https://api.airtable.com/v0/${airtable.baseID}/${airtable.tableID}?maxRecords=1`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${airtable.apiKey}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
fetch(`https://api.airtable.com/v0/${baseId}/${tableId}?maxRecords=20`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((body) => setData(body.records))
|
||||
.then((body) => {
|
||||
const { error } = body;
|
||||
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
console.log(body);
|
||||
setImportAirtable((prev) => ({ ...prev, airtableData: body }));
|
||||
openTableModal("importAirtable");
|
||||
})
|
||||
.then(() => {
|
||||
setValidConnection(true);
|
||||
setLoading(false);
|
||||
setError("");
|
||||
setAirtableError(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -401,15 +408,15 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
fullWidth
|
||||
label="Airtable API Key"
|
||||
placeholder="Insert your API key here"
|
||||
value={airtable.apiKey}
|
||||
onChange={(e) => {
|
||||
setAirtable((airtable: any) => ({
|
||||
...airtable,
|
||||
value={apiKey}
|
||||
onChange={(e) =>
|
||||
setImportAirtable((prev) => ({
|
||||
...prev,
|
||||
apiKey: e.currentTarget.value,
|
||||
}));
|
||||
}}
|
||||
helperText={error?.apiKey?.message}
|
||||
error={!!error?.apiKey?.message}
|
||||
}))
|
||||
}
|
||||
helperText={airtableError?.apiKey?.message}
|
||||
error={!!airtableError?.apiKey?.message}
|
||||
/>
|
||||
<TextField
|
||||
variant="filled"
|
||||
@@ -417,15 +424,15 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
fullWidth
|
||||
label="Airtable Base ID"
|
||||
placeholder="Insert your Base ID here"
|
||||
value={airtable.baseID}
|
||||
value={baseId}
|
||||
onChange={(e) => {
|
||||
setAirtable((airtable: any) => ({
|
||||
...airtable,
|
||||
baseID: e.currentTarget.value,
|
||||
setImportAirtable((prev) => ({
|
||||
...prev,
|
||||
baseId: e.currentTarget.value,
|
||||
}));
|
||||
}}
|
||||
helperText={error?.baseID?.message}
|
||||
error={!!error?.baseID?.message}
|
||||
helperText={airtableError?.baseId?.message}
|
||||
error={!!airtableError?.baseId?.message}
|
||||
/>
|
||||
<TextField
|
||||
variant="filled"
|
||||
@@ -433,20 +440,16 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
fullWidth
|
||||
label="Airtable Table Name or ID"
|
||||
placeholder="Insert your Table Name or ID here"
|
||||
value={airtable.tableID}
|
||||
value={tableId}
|
||||
onChange={(e) => {
|
||||
setAirtable((prev: any) => ({
|
||||
...airtable,
|
||||
tableID: e.currentTarget.value,
|
||||
setImportAirtable((prev) => ({
|
||||
...prev,
|
||||
tableId: e.currentTarget.value,
|
||||
}));
|
||||
}}
|
||||
helperText={error?.tableID?.message}
|
||||
error={!!error?.tableID?.message}
|
||||
helperText={airtableError?.tableId?.message}
|
||||
error={!!airtableError?.tableId?.message}
|
||||
/>
|
||||
{data &&
|
||||
data.map((record: any) => (
|
||||
<div>Airtable record: {record.id}</div>
|
||||
))}
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
|
||||
@@ -454,9 +457,7 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={
|
||||
importMethodRef.current === "airtable"
|
||||
? loading && validConnection
|
||||
: !validCsv
|
||||
importMethodRef.current === "airtable" ? loading : !validCsv
|
||||
}
|
||||
sx={{
|
||||
mt: -4,
|
||||
@@ -467,25 +468,16 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
}}
|
||||
onClick={() => {
|
||||
if (importMethodRef.current === "airtable") {
|
||||
if (!data) {
|
||||
handleAirtableConnection();
|
||||
} else {
|
||||
console.log("hello");
|
||||
openTableModal("importCsv");
|
||||
}
|
||||
handleAirtableConnection();
|
||||
} else {
|
||||
openTableModal("importCsv");
|
||||
logEvent(analytics, `import_${importMethodRef.current}`, {
|
||||
type: importTypeRef.current,
|
||||
});
|
||||
}
|
||||
logEvent(analytics, `import_${importMethodRef.current}`, {
|
||||
type: importTypeRef.current,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{importMethodRef.current === "airtable"
|
||||
? data
|
||||
? "Continue"
|
||||
: "Test Connection"
|
||||
: "Continue"}
|
||||
Continue
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user