exportable csv

This commit is contained in:
shams mosowi
2019-10-31 17:12:47 +11:00
parent 6f31b6b744
commit 690b8c1ada
11 changed files with 342 additions and 28 deletions

View File

@@ -16,9 +16,11 @@
"main": "lib/index.js",
"dependencies": {
"@types/algoliasearch": "^3.34.5",
"@types/json2csv": "^4.5.0",
"algoliasearch": "^3.35.1",
"firebase-admin": "~7.0.0",
"firebase-functions": "^2.3.0"
"firebase-functions": "^2.3.0",
"json2csv": "^4.5.4"
},
"devDependencies": {
"husky": "^3.0.9",

View File

@@ -0,0 +1,23 @@
import * as algolia from "algoliasearch";
import * as functions from "firebase-functions";
import { env } from "./config";
export const updateAlgoliaRecord = functions.https.onCall(
async (data: any, context: any) => {
const client = algolia(env.algolia.appid, env.algolia.apikey);
const index = client.initIndex(data.collection);
await index.partialUpdateObject({
objectID: data.id,
...data.doc,
});
return true;
}
);
export const deleteAlgoliaRecord = functions.https.onCall(
async (data: any, context: any) => {
const client = algolia(env.algolia.appid, env.algolia.apikey);
const index = client.initIndex(data.collection);
await index.deleteObject(data.id);
return true;
}
);

View File

@@ -0,0 +1,109 @@
import { parse as json2csv } from "json2csv";
import * as functions from "firebase-functions";
import { db } from "./config";
import * as admin from "firebase-admin";
const enum FieldType {
simpleText = "SIMPLE_TEXT",
longText = "LONG_TEXT",
email = "EMAIL",
PhoneNumber = "PHONE_NUMBER",
checkBox = "CHECK_BOX",
date = "DATE",
dateTime = "DATE_TIME",
number = "NUMBER",
url = "URL",
color = "COLOR",
rating = "RATING",
image = "IMAGE",
file = "FILE",
singleSelect = "SINGLE_SELECT",
multiSelect = "MULTI_SELECT",
documentSelect = "DOCUMENT_SELECT",
last = "LAST",
}
export const exportTable = functions.https.onCall(
async (
request: {
collectionPath: string;
filters: {
key: string;
operator: "==" | "<" | ">" | ">=" | "<=";
value: string;
}[];
limit?: number;
sort?:
| { field: string; direction: "asc" | "desc" }[]
| { field: string; direction: "asc" | "desc" };
columns: { key: string; type: FieldType; config: any }[];
},
response
) => {
const { collectionPath, filters, sort, limit, columns } = request;
console.log(request);
console.log("columns", columns);
// set query path
let query:
| admin.firestore.CollectionReference
| admin.firestore.Query = db.collection(collectionPath);
// add filters
filters.forEach(filter => {
query = query.where(filter.key, filter.operator, filter.value);
});
// optional order results
if (sort) {
if (Array.isArray(sort)) {
sort.forEach(order => {
query = query.orderBy(order.field, order.direction);
});
} else {
query = query.orderBy(sort.field, sort.direction);
}
}
//optional set query limit
if (limit) query = query.limit(limit);
const querySnapshot = await query.get();
const docs = querySnapshot.docs.map(doc => doc.data());
const data = docs.map((doc: any) => {
const selectedColumnsReducer = (accumulator: any, currentColumn: any) => {
switch (currentColumn.type) {
case FieldType.multiSelect:
return {
...accumulator,
[currentColumn.key]: doc[currentColumn.key].join(),
};
case FieldType.file:
case FieldType.image:
return {
...accumulator,
[currentColumn.key]: doc[currentColumn.key]
.map((item: { downloadURL: string }) => item.downloadURL)
.join(),
};
case FieldType.documentSelect:
return {
...accumulator,
[currentColumn.key]: doc[currentColumn.key]
.map((item: any) =>
currentColumn.config.primaryKeys.reduce(
(labelAccumulator: string, currentKey: any) =>
`${labelAccumulator} ${item.snapshot[currentKey]}`,
""
)
)
.join(),
};
default:
return {
...accumulator,
[currentColumn.key]: doc[currentColumn.key],
};
}
};
return columns.reduce(selectedColumnsReducer, {});
});
console.log("data", data);
const csv = json2csv(data);
return csv;
}
);

View File

@@ -1,28 +1,7 @@
import * as algolia from "algoliasearch";
import * as functions from "firebase-functions";
import * as maps from "./maps";
import * as claims from "./claims";
import { env, auth } from "./config";
export const updateAlgoliaRecord = functions.https.onCall(
async (data: any, context: any) => {
const client = algolia(env.algolia.appid, env.algolia.apikey);
const index = client.initIndex(data.collection);
await index.partialUpdateObject({
objectID: data.id,
...data.doc,
});
return true;
}
);
export const deleteAlgoliaRecord = functions.https.onCall(
async (data: any, context: any) => {
const client = algolia(env.algolia.appid, env.algolia.apikey);
const index = client.initIndex(data.collection);
await index.deleteObject(data.id);
return true;
}
);
import { auth } from "./config";
exports.setUserAsAdmin = functions.auth.user().onCreate(async user => {
// check if email is from antler domain and is verified then add an admin custom token
@@ -46,3 +25,6 @@ exports.setUserAsAdmin = functions.auth.user().onCreate(async user => {
export const MAPS = maps;
export const CLAIMS = claims;
export { exportTable } from "./export";
export { updateAlgoliaRecord, deleteAlgoliaRecord } from "./algolia";

View File

@@ -11,6 +11,7 @@
"@types/algoliasearch": "^3.34.2",
"@types/backbone": "^1.4.1",
"@types/cash": "^0.0.3",
"@types/file-saver": "^2.0.1",
"@types/jest": "24.0.18",
"@types/lodash": "^4.14.138",
"@types/node": "12.7.4",
@@ -23,6 +24,7 @@
"algoliasearch": "^3.34.0",
"csv-parse": "^4.4.6",
"date-fns": "^2.0.0-beta.5",
"file-saver": "^2.0.2",
"firebase": "^6.6.0",
"grapesjs-react": "^2.0.2",
"hotkeys-js": "^3.7.2",

View File

@@ -0,0 +1,166 @@
import React, { useState, useCallback } from "react";
import _camelCase from "lodash/camelCase";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import Grid from "@material-ui/core/Grid";
import IconButton from "@material-ui/core/IconButton";
import Dialog from "@material-ui/core/Dialog";
import Select from "@material-ui/core/Select";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import AddCSVIcon from "@material-ui/icons/PlaylistAdd";
import ArrowIcon from "@material-ui/icons/TrendingFlatOutlined";
import AddIcon from "@material-ui/icons/Add";
import DeleteIcon from "@material-ui/icons/Delete";
import { makeStyles, createStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import Input from "@material-ui/core/Input";
import ListItemText from "@material-ui/core/ListItemText";
import Checkbox from "@material-ui/core/Checkbox";
import Chip from "@material-ui/core/Chip";
import CloudIcon from "@material-ui/icons/CloudDownload";
import { exportTable } from "../firebase/callables";
import { saveAs } from "file-saver";
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250,
},
},
};
const useStyles = makeStyles(theme =>
createStyles({
root: {
display: "flex",
flexWrap: "wrap",
},
formControl: {
margin: theme.spacing(1),
width: 400,
},
selectEmpty: {
marginTop: theme.spacing(2),
},
keyPair: {
flexGrow: 2,
display: "flex",
justifyItems: "space-between",
},
cloudIcon: {
fontSize: 64,
},
uploadContainer: {
margin: "auto",
},
chips: {
display: "flex",
flexWrap: "wrap",
width: 400,
},
chip: {},
})
);
interface Props {
columns: any;
collection: string;
}
export default function ExportCSV(props: Props) {
const { columns, collection } = props;
const classes = useStyles();
const [open, setOpen] = useState(false);
const [csvColumns, setCSVColumns] = useState<any[]>([]);
const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
setCSVColumns(event.target.value as any[]);
};
function handleClickOpen() {
setOpen(true);
}
function handleClose() {
setOpen(false);
}
async function handleExport() {
const data = await exportTable({
collectionPath: collection,
filters: [],
columns: csvColumns,
});
var blob = new Blob([data.data], {
type: "text/csv;charset=utf-8",
});
saveAs(blob, `${collection}.csv`);
}
return (
<div>
<Button color="secondary" onClick={handleClickOpen}>
Export CSV <CloudIcon />
</Button>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">Export table into CSV</DialogTitle>
<DialogContent>
<FormControl className={classes.formControl}>
<InputLabel id="demo-mutiple-chip-label">
Exportable columns
</InputLabel>
<Select
id="demo-mutiple-chip"
multiple
value={csvColumns}
onChange={handleChange}
input={<Input id="select-multiple-chip" />}
renderValue={selected => (
<div className={classes.chips}>
{(selected as any[]).map(value => (
<Chip
key={value.key}
label={value.name}
className={classes.chip}
/>
))}
</div>
)}
MenuProps={MenuProps}
>
{columns.map((column: any) => (
<MenuItem key={column.key} value={column}>
{column.name}
</MenuItem>
))}
</Select>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button
onClick={handleExport}
disabled={csvColumns.length === 0}
color="primary"
>
Export
</Button>
</DialogActions>
</Dialog>
</div>
);
}

View File

@@ -9,7 +9,7 @@ const useStyles = makeStyles((theme: Theme) =>
root: {
position: "relative",
display: "flex",
// flexWrap: "wrap",
flexWrap: "wrap",
alignItems: "center",
},
typography: {
@@ -69,7 +69,7 @@ const DocSelect = (props: Props) => {
<Chip
key={doc.docPath}
label={config.primaryKeys.map(
(key: any) => `${doc.snapshot[key]} `
(key: string) => `${doc.snapshot[key]} `
)}
//onClick={handleClick}
onDelete={() => {

View File

@@ -87,7 +87,7 @@ const Image = (props: Props) => {
<input {...getInputProps()} />
{value &&
files.map((file: { name: string; downloadURL: string }) => (
<Tooltip title="Click to delete">
<Tooltip title="Click to delete" key={file.downloadURL}>
<div
onClick={e => {
const index = _findIndex(value, [

View File

@@ -5,6 +5,7 @@ import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Typography from "@material-ui/core/Typography";
import ImportCSV from "components/ImportCSV";
import ExportCSV from "components/ExportCSV";
import Button from "@material-ui/core/Button";
import { createStyles, makeStyles } from "@material-ui/core/styles";
import AddIcon from "@material-ui/icons/AddCircle";
@@ -36,8 +37,14 @@ const useStyles = makeStyles(Theme => {
});
});
interface Props {}
const TableHeader = (props: any) => {
interface Props {
collection: string;
rowHeight: number;
updateConfig: Function;
addRow: Function;
columns: any;
}
const TableHeader = (props: Props) => {
const { collection, rowHeight, updateConfig, columns, addRow } = props;
const classes = useStyles();
return (
@@ -73,6 +80,8 @@ const TableHeader = (props: any) => {
</FormControl>
</div>
<div className={classes.tableActions}>
<ExportCSV columns={columns} collection={collection} />
<ImportCSV columns={columns} addRow={addRow} />
<Button
color="secondary"

View File

@@ -3,6 +3,7 @@ import { functions } from "./index";
export enum CLOUD_FUNCTIONS {
updateAlgoliaRecord = "updateAlgoliaRecord",
deleteAlgoliaRecord = "deleteAlgoliaRecord",
exportTable = "exportTable",
}
export const cloudFunction = (
@@ -35,3 +36,13 @@ export const algoliaUpdateDoc = (data: {
export const algoliaDeleteDoc = functions.httpsCallable(
CLOUD_FUNCTIONS.deleteAlgoliaRecord
);
export const exportTable = (data: {
collectionPath: string;
filters: {
key: string;
operator: "==" | "<" | ">" | ">=" | "<=";
value: string;
}[];
columns: any[];
}) => functions.httpsCallable(CLOUD_FUNCTIONS.exportTable)(data);

View File

@@ -1564,6 +1564,11 @@
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==
"@types/file-saver@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.1.tgz#e18eb8b069e442f7b956d313f4fadd3ef887354e"
integrity sha512-g1QUuhYVVAamfCifK7oB7G3aIl4BbOyzDOqVyUfEr4tfBKrXfeH+M+Tg7HKCXSrbzxYdhyCP7z9WbKo0R2hBCw==
"@types/history@*":
version "4.7.3"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.3.tgz#856c99cdc1551d22c22b18b5402719affec9839a"
@@ -5067,6 +5072,11 @@ file-saver@^1.3.2:
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8"
integrity sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg==
file-saver@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.2.tgz#06d6e728a9ea2df2cce2f8d9e84dfcdc338ec17a"
integrity sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw==
file-selector@^0.1.11:
version "0.1.12"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.12.tgz#fe726547be219a787a9dcc640575a04a032b1fd0"