mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
exportable csv
This commit is contained in:
@@ -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",
|
||||
|
||||
23
cloud_functions/functions/src/algolia.ts
Normal file
23
cloud_functions/functions/src/algolia.ts
Normal 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;
|
||||
}
|
||||
);
|
||||
109
cloud_functions/functions/src/export.ts
Normal file
109
cloud_functions/functions/src/export.ts
Normal 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;
|
||||
}
|
||||
);
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
166
www/src/components/ExportCSV.tsx
Normal file
166
www/src/components/ExportCSV.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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, [
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user