sanity-plugin-spreadsheet-wizard-x
Version:
Spreadsheet Wizard is a magnificient Sanity plugin that help you EXPORT & IMPORT spreadsheets. A fantastic bridge between Sanity & your spreadsheet software of choice. (Google Sheets... Excel... Numbers etc.)
342 lines (332 loc) • 11.1 kB
JavaScript
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import {
screenTypes,
CSV_TOOLS_DELIMETER,
ACCEPTED_TYPES,
SANITY_META_TYPES,
} from "./constants.js";
import {
fieldsAndDataProcessed,
isJsonString,
getEmptyValueFor,
fieldsOfParticularSchema,
transformMe,
} from "./utils.js";
import Layout from "./Layout.js";
import { client } from "./CSVTools.js";
import { Heading, Stack, Select, Button } from "@sanity/ui";
import { UploadSimple } from "phosphor-react";
import { DataTable } from "./Export";
const Import = ({ handleScreenChange }) => {
return (
<Stack
style={{
width: "100%",
justifySelf: "center",
paddingBottom: "4rem",
}}
space={3}
>
<p style={{ marginBlockStart: "0" }}>
Imports are supported by file that look like the ones we export,
so you might want to see that first!{" "}
</p>
<ImportForm />
</Stack>
);
};
async function uploadToSanity(
arrayOfObjects,
setProcessing,
setError,
setSuccess
) {
const size = 10;
if (arrayOfObjects.length < size) {
await uploadToSanityBackEnd(
arrayOfObjects,
setProcessing,
setError,
setSuccess
);
} else {
for (let i = 0; i <= arrayOfObjects.length - size; i += size) {
await uploadToSanityBackEnd(
arrayOfObjects.slice(i, i + size),
setProcessing,
setError,
setSuccess
);
if (i + 1 < arrayOfObjects.length - size)
setProcessing(
"waiting for .5 sec before trying next batch to avoid sanity api limit"
);
await new Promise((r) => setTimeout(r, 500));
setProcessing("");
}
}
}
function uploadToSanityBackEnd(
arrayOfObjects,
setProcessing,
setError,
setSuccess
) {
// Remove empty lines & objects without ids
// arrayOfObjects = arrayOfObjects.filter((item) => !!item._id);
setProcessing("...uploading...");
setSuccess("");
setError("");
return Promise.all(
arrayOfObjects.map((object) => {
const unsetFields = Object.keys(object).filter(
(key) => object[key] === getEmptyValueFor("")
);
return client
.patch(object._id)
.set(object)
.unset(unsetFields)
.commit()
.then((t) => {
numberOfSuccesses++;
console.log("success", object._id);
})
.catch((e) => {
// If ID is specified, and got document not found
if (object._id) {
const objectWithoutEmptyValues = Object.keys(
object
).reduce(
(prev, curr) =>
object[curr] === getEmptyValueFor("")
? prev
: { ...prev, [curr]: object[curr] },
{}
);
client
.createIfNotExists(objectWithoutEmptyValues)
.then((t) => {
console.log(
"success",
objectWithoutEmptyValues._id
);
})
.catch((e) => {
console.log(
"if you are trying to create new docs by specifying ids manually, make sure you are providing type as well"
);
console.log(e);
});
} else {
console.log("error:", object);
}
});
})
)
.then((msg) => {
setSuccess(
`${msg.length} docs added/modified! ${
arrayOfObjects.length > msg.length
? " Failed" +
(arrayOfObjects.length - msg.length) +
" modifications, check console"
: ""
}`
);
setProcessing("");
console.log(msg);
})
.catch((err) => {
setError("error, check console for details");
console.log(err);
console.log(arrayOfObjects);
setProcessing("");
});
}
function transformToProperType(record) {
const recordType = record._type;
const recordTypes = fieldsOfParticularSchema(recordType);
return Object.keys(record).reduce((prev, curr) => {
/// ignore metatypes
if (curr[0] === "_") {
return { ...prev, [curr]: record[curr] };
}
return {
...prev,
[curr]: transformMe(
record[curr], // my value
recordTypes.find((x) => x.name === curr).type // my type on this document
),
};
}, {});
}
function processCSVLines(linesOfStrings, onSuccess, onFail) {
linesOfStrings = linesOfStrings.map((thing) =>
thing.replace("\r", "").replace("\n", "").replace("\r\n", "")
);
const heading = linesOfStrings[0].split(CSV_TOOLS_DELIMETER);
function makeObject(index, myValue) {
const thing = ACCEPTED_TYPES.find((x) => x.name === heading[index]);
if (thing?.query) {
const { value, ...other } = {
...thing.structure,
[thing.structure.value.name]: myValue,
};
return { [thing.name]: other };
} else {
const thing = {
[heading[index]]: isJsonString(myValue)
? JSON.parse(myValue)
: myValue
? String(myValue)
: getEmptyValueFor(""),
};
return thing;
}
}
// Check if heading contains ID and TYPE
if (heading.includes("_id")) {
onSuccess(
linesOfStrings
.slice(1)
.map((item) => {
const thing = item.split(CSV_TOOLS_DELIMETER);
return thing.reduce(
(prev, current, index) => ({
...prev,
...makeObject(index, current),
}),
{}
);
})
.filter((item) => !!item._id)
.filter((x) => !!x._type)
.map(transformToProperType) // only take those rows with _id
);
} else {
onFail("_id not provided!");
}
}
const ImportForm = () => {
const [error, setError] = useState("");
const [processing, setProcessing] = useState("");
const [success, setSuccess] = useState("");
const [myData, setMyData] = useState(null);
const [rawData, setRawData] = useState(null);
function readFromInputFile(onRead) {
const file = document.querySelector("#csvFile").files[0];
const reader = new FileReader();
reader.onload = (e) => onRead(e);
reader.readAsText(file, "utf-8");
}
function handleFile(e) {
if (e.target.value) {
readFromInputFile((e) =>
processCSVLines(
e.target.result.split("\n"),
(data) => {
setMyData(processAndSetMyData(data));
setRawData(data);
},
setError
)
);
}
}
function processAndSetMyData(data) {
const metaTypes = SANITY_META_TYPES.filter(
(thing) => thing.required
).map((x) => x.name);
const allHeadings = data
.reduce((prev, curr) => [...prev, ...Object.keys(curr)], [])
.filter((x) => !metaTypes.includes(x))
.reduce(
(prev, curr) => (prev.includes(curr) ? prev : [...prev, curr]),
[]
);
const headingsWithTypes = allHeadings.map((x) => ({
name: x,
type: "string",
}));
return fieldsAndDataProcessed(
data,
headingsWithTypes,
metaTypes,
headingsWithTypes
);
}
async function handlePaste() {
setError("");
const text = await navigator.clipboard
.readText()
.then((data) => data?.split("\n"));
if (text) {
processCSVLines(
text,
(data) => {
setMyData(processAndSetMyData(data));
setRawData(data);
},
setError
);
}
}
function handleUpload(e) {
setError("");
// handleUpload
if (rawData) {
uploadToSanity(rawData, setProcessing, setError, setSuccess);
} else {
setError("No Data");
}
}
return (
<>
<form>
{error ? <p> Error: {error} </p> : null}
{processing ? <p> {processing} </p> : null}
{success ? <p> {success} </p> : null}
<div></div>
<div style={{ width: "100%", background: "#eee" }}>
<input
style={{ width: "100%", padding: "20px" }}
required={true}
id="csvFile"
name="file"
type="file"
accept=".tsv"
onChange={handleFile}
/>
</div>
<Button
style={{ marginTop: "1.5rem", width: "100%" }}
icon={UploadSimple}
onClick={handleUpload}
text="Submit"
tone="positive"
mode="ghost"
disabled={!myData}
padding={4}
/>
<Button
style={{ marginTop: "1.5rem", width: "100%" }}
icon={UploadSimple}
text="Pate from clipboard"
tone="positive"
mode="ghost"
onClick={handlePaste}
padding={4}
/>
<DataTableWrapper data={myData} />
</form>
</>
);
};
const DataTableWrapper = ({ data }) => {
return ReactDOM.createPortal(
<div>{data ? <DataTable data={data} /> : null}</div>,
document.getElementById("spreadsheet")
);
};
export default Import;