mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
pulled master
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,6 +17,7 @@
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
||||
75
README.md
75
README.md
@@ -1,48 +1,63 @@
|
||||
## Firetable
|
||||
|
||||
Firtable is a simple CMS for Google Firebase.
|
||||
Firetable is a simple CMS for Google Firebase.
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
## Setup instructions
|
||||
|
||||
## Available Scripts
|
||||
create a firebase project
|
||||
|
||||
In the project directory, you can run:
|
||||
- enable firestore
|
||||
- enable google auth
|
||||
create an algolia project
|
||||
|
||||
### `npm start`
|
||||
Cloud functions setup
|
||||
|
||||
Runs the app in the development mode.<br>
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
set environment variables
|
||||
|
||||
The page will reload if you make edits.<br>
|
||||
You will also see any lint errors in the console.
|
||||
```
|
||||
firebase functions:config:set algolia.app=YOUR_APP_ID algolia.key=ADMIN_API_KEY
|
||||
```
|
||||
|
||||
### `npm test`
|
||||
deploy the following callable cloud functions to update and delete algolia records
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br>
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
```
|
||||
const functions = require("firebase-functions");
|
||||
const env = functions.config();
|
||||
const algolia = require("algoliasearch");
|
||||
exports.updateAlgoliaRecord = functions.https.onCall(async (data, context) => {
|
||||
const client = algolia(env.algolia.appid, env.algolia.apikey);
|
||||
const index = client.initIndex(data.collection);
|
||||
await index.partialUpdateObject(Object.assign({ objectID: data.id }, data.doc));
|
||||
return true;
|
||||
});
|
||||
|
||||
### `npm run build`
|
||||
exports.deleteAlgoliaRecord = functions.https.onCall(async (data, context) => {
|
||||
const client = algolia(env.algolia.appid, env.algolia.apikey);
|
||||
const index = client.initIndex(data.collection);
|
||||
await index.deleteObject(data.id);
|
||||
return true;
|
||||
});
|
||||
```
|
||||
|
||||
Builds the app for production to the `build` folder.<br>
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
Clone repo
|
||||
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
add .env file to the project directory
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
```
|
||||
REACT_APP_FIREBASE_PROJECT_NAME =
|
||||
REACT_APP_FIREBASE_PROJECT_KEY =
|
||||
REACT_APP_ALGOLIA_APP_ID =
|
||||
REACT_APP_ALGOLIA_SEARCH_KEY =
|
||||
```
|
||||
|
||||
### `npm run eject`
|
||||
install dependencies
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
```
|
||||
yarn
|
||||
```
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
Run project locally
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
```
|
||||
yarn start
|
||||
```
|
||||
|
||||
31
ROADMAP.md
31
ROADMAP.md
@@ -1,6 +1,6 @@
|
||||
# Firetable Roadmap
|
||||
|
||||
## POC
|
||||
## POC ✅
|
||||
|
||||
### Initial fields:
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- phone(string) ✅
|
||||
- url(string) ✅
|
||||
- Number(number) ✅
|
||||
- long text(string)
|
||||
- long text(string) ✅
|
||||
|
||||
### Functionality:
|
||||
|
||||
@@ -25,44 +25,50 @@
|
||||
|
||||
### additional fields:
|
||||
|
||||
- single select(string)
|
||||
- [https://material-ui.com/components/chips/#chip-array] Multiple select(array of strings)
|
||||
- single select(string)✅
|
||||
- Multiple select(array of strings)✅
|
||||
- date(Firebase timestamp)✅
|
||||
- time(Firebase timestamp)✅
|
||||
- file(firebase storage url string)
|
||||
- image(firebase storage url string)
|
||||
- file (single) 🏗️(missing status indicator)
|
||||
- image (single) 🏗️(missing status indicator)
|
||||
- single select reference(DocReference)
|
||||
- multi select reference(DocReference)
|
||||
- rating ✅
|
||||
|
||||
### Functionality:
|
||||
|
||||
- Hide/Show columns
|
||||
- Delete columns
|
||||
- Edit columns
|
||||
- Delete tables
|
||||
- Edit tables
|
||||
- Hide tables
|
||||
- Delete columns✅
|
||||
- Edit columns✅
|
||||
- Fixed column
|
||||
- Hide/Show columns
|
||||
- resizable column ✅
|
||||
- keyboard Navigation:
|
||||
- Up key to move to the cell above ✅
|
||||
- Down key to move to the cell bellow, if last cell create a new row ✅
|
||||
- Tab to go to the next cell ✅
|
||||
- column / table Create/edit validation
|
||||
- Delete tables
|
||||
- Edit tables
|
||||
- Hide tables
|
||||
- On new table add, refresh view to the table view✅
|
||||
- import csv to table
|
||||
|
||||
## V1
|
||||
|
||||
### additional fields:
|
||||
|
||||
- file (multi)
|
||||
- image (multi)
|
||||
- Duration
|
||||
- Percentage(number)
|
||||
- Slider(number)
|
||||
- Table(Document[])
|
||||
- Rich Text(html string)
|
||||
|
||||
### Functionality:
|
||||
|
||||
- Sort rows
|
||||
- reorder columns
|
||||
- Locked columns
|
||||
- Table view only mode
|
||||
- SubCollection tables
|
||||
@@ -89,7 +95,6 @@
|
||||
|
||||
### Functionality:
|
||||
|
||||
- import csv to table
|
||||
- Themes
|
||||
- Table templates
|
||||
- Dialog View of a row
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"@material-ui/icons": "^4.4.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.26",
|
||||
"@material-ui/pickers": "^3.2.5",
|
||||
"@types/algoliasearch": "^3.34.2",
|
||||
"@types/jest": "24.0.18",
|
||||
"@types/lodash": "^4.14.138",
|
||||
"@types/node": "12.7.4",
|
||||
@@ -15,10 +16,15 @@
|
||||
"@types/react": "^16.9.2",
|
||||
"@types/react-data-grid": "^4.0.3",
|
||||
"@types/react-dom": "16.9.0",
|
||||
"@types/react-instantsearch-dom": "^5.2.6",
|
||||
"@types/react-router-dom": "^4.3.5",
|
||||
"@types/react-sortable-hoc": "^0.6.5",
|
||||
"@types/react-virtualized": "^9.21.4",
|
||||
<<<<<<< HEAD
|
||||
"@types/xlsx": "^0.0.36",
|
||||
=======
|
||||
"algoliasearch": "^3.34.0",
|
||||
>>>>>>> 388c726eacf3bd64f948944e2771b63a8afddedc
|
||||
"array-move": "^2.1.0",
|
||||
"attr-accept": "^1.1.3",
|
||||
"convert-csv-to-json": "^0.0.15",
|
||||
|
||||
@@ -7,7 +7,7 @@ import AuthView from "./views/AuthView";
|
||||
import TableView from "./views/TableView";
|
||||
import TablesView from "./views/TablesView";
|
||||
|
||||
import { BrowserRouter as Router, Route } from "react-router-dom";
|
||||
import { Route } from "react-router-dom";
|
||||
import { AuthProvider } from "./AuthProvider";
|
||||
import CustomBrowserRouter from "./util/CustomBrowserRouter";
|
||||
import PrivateRoute from "./util/PrivateRoute";
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import _camelCase from "lodash/camelCase";
|
||||
|
||||
import {
|
||||
Button,
|
||||
TextField,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Fab,
|
||||
} from "@material-ui/core";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
|
||||
// TODO: Create an interface for props
|
||||
export default function ColumnDialog(props: any) {
|
||||
const { classes, columnName, updateColumn } = props;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
function handleClickOpen() {
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setOpen(false);
|
||||
}
|
||||
function handleUpdate() {
|
||||
// updateColumn(tableName, collectionName);
|
||||
handleClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Fab
|
||||
className={classes.fabButton}
|
||||
color="secondary"
|
||||
aria-label="add"
|
||||
onClick={handleClickOpen}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="form-dialog-title"
|
||||
>
|
||||
<DialogTitle id="form-dialog-title">New table</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>Create a new Table</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
onChange={e => {
|
||||
// setTableName(e.target.value);
|
||||
}}
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="Table Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleUpdate} color="primary">
|
||||
update
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
import _camelCase from "lodash/camelCase";
|
||||
|
||||
import useRouter from "../hooks/useRouter";
|
||||
import {
|
||||
Button,
|
||||
TextField,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
|
||||
// TODO: Create an interface for props
|
||||
export default function CreateTableDialog(props: any) {
|
||||
const router = useRouter();
|
||||
const { classes, createTable } = props;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [tableName, setTableName] = useState("");
|
||||
@@ -33,6 +34,7 @@ export default function CreateTableDialog(props: any) {
|
||||
}
|
||||
function handleCreate() {
|
||||
createTable(tableName, collectionName);
|
||||
router.history.push(collectionName);
|
||||
handleClose();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const CheckBox = (props: Props) => {
|
||||
name={`checkBox-controlled-${row.id}`}
|
||||
checked={!!value}
|
||||
onChange={e => {
|
||||
onSubmit(row.ref, !value);
|
||||
onSubmit(!value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ const Date = (props: Props) => {
|
||||
const { value, row, onSubmit, fieldType } = props;
|
||||
function handleDateChange(date: Date | null) {
|
||||
if (date) {
|
||||
onSubmit(row.ref, date);
|
||||
onSubmit(date);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
120
src/components/Fields/DocSelect.tsx
Normal file
120
src/components/Fields/DocSelect.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
import Popper from "@material-ui/core/Popper";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import ClickAwayListener from "@material-ui/core/ClickAwayListener";
|
||||
import algoliasearch from "algoliasearch/lite";
|
||||
import { TextField } from "@material-ui/core";
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
process.env.REACT_APP_ALGOLIA_APP_ID
|
||||
? process.env.REACT_APP_ALGOLIA_APP_ID
|
||||
: "",
|
||||
process.env.REACT_APP_ALGOLIA_SEARCH_KEY
|
||||
? process.env.REACT_APP_ALGOLIA_SEARCH_KEY
|
||||
: ""
|
||||
);
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
position: "relative",
|
||||
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
typography: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
textArea: {
|
||||
fontSize: 14,
|
||||
minWidth: 230,
|
||||
},
|
||||
paper: { minWidth: 200 },
|
||||
})
|
||||
);
|
||||
interface Props {
|
||||
value: any;
|
||||
row: { ref: firebase.firestore.DocumentReference; id: string };
|
||||
onSubmit: Function;
|
||||
collectionPath: string;
|
||||
}
|
||||
|
||||
const DocSelect = (props: Props) => {
|
||||
const { value, row, onSubmit, collectionPath } = props;
|
||||
const [query, setQuery] = useState(value ? value : "");
|
||||
const [hits, setHits] = useState<{}>([]);
|
||||
const algoliaIndex = searchClient.initIndex(collectionPath);
|
||||
const search = async (query: string) => {
|
||||
const resp = await algoliaIndex.search({ query });
|
||||
setHits(resp.hits);
|
||||
};
|
||||
useEffect(() => {
|
||||
search(query);
|
||||
}, [query]);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const classes = useStyles();
|
||||
|
||||
const handleClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const id = open ? "no-transition-popper" : undefined;
|
||||
const onClickAway = (event: any) => {
|
||||
if (event.target.id !== id) {
|
||||
// onSubmit();
|
||||
// setAnchorEl(null);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<ClickAwayListener onClickAway={onClickAway}>
|
||||
<div>
|
||||
<IconButton onClick={handleClick}>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
{value}
|
||||
<Popper id={id} open={open} anchorEl={anchorEl}>
|
||||
<Paper>
|
||||
<TextField
|
||||
id={id}
|
||||
placeholder={`searching ${collectionPath}`}
|
||||
onChange={(e: any) => {
|
||||
setQuery(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{/* <InstantSearch
|
||||
indexName={collectionPath}
|
||||
searchClient={searchClient}
|
||||
>
|
||||
|
||||
<SearchBox /> */}
|
||||
|
||||
{/* </InstantSearch> */}
|
||||
</div>
|
||||
</Paper>
|
||||
</Popper>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Hit = (props: any) => {
|
||||
return (
|
||||
<div>
|
||||
<h3>{props.hit.firstName}</h3>
|
||||
<p>{props.hit.email}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocSelect;
|
||||
71
src/components/Fields/File.tsx
Normal file
71
src/components/Fields/File.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import useUploader from "../../hooks/useFiretable/useUploader";
|
||||
|
||||
import { FieldType } from ".";
|
||||
import Chip from "@material-ui/core/Chip";
|
||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
|
||||
// TODO: indicate state completion / error
|
||||
// TODO: Create an interface for props
|
||||
|
||||
interface Props {
|
||||
value: any;
|
||||
row: { ref: firebase.firestore.DocumentReference; id: string };
|
||||
onSubmit: Function;
|
||||
fieldType: FieldType;
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
chip: {},
|
||||
})
|
||||
);
|
||||
|
||||
const File = (props: Props) => {
|
||||
const { fieldName, value, row, onSubmit } = props;
|
||||
const classes = useStyles();
|
||||
const [uploaderState, upload] = useUploader();
|
||||
const onDrop = useCallback(acceptedFiles => {
|
||||
// Do something with the files
|
||||
const imageFile = acceptedFiles[0];
|
||||
if (imageFile) {
|
||||
upload(row.ref, fieldName, [imageFile]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
multiple: false,
|
||||
});
|
||||
const handleDelete = () => {
|
||||
onSubmit([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...getRootProps()}>
|
||||
<input {...getInputProps()} />
|
||||
{value && value.length !== 0 ? (
|
||||
<Chip
|
||||
key={value[0].name}
|
||||
label={value[0].name}
|
||||
className={classes.chip}
|
||||
onClick={() => {
|
||||
window.open(value[0].downloadURL);
|
||||
}}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
) : isDragActive ? (
|
||||
<p>Drop the files here ...</p>
|
||||
) : (
|
||||
<p>click to select files</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default File;
|
||||
@@ -11,17 +11,18 @@ interface Props {
|
||||
row: { ref: firebase.firestore.DocumentReference; id: string };
|
||||
onSubmit: Function;
|
||||
fieldType: FieldType;
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
const Image = (props: any) => {
|
||||
const { columnData, cellData, cellActions, rowData, rowIndex } = props;
|
||||
const Image = (props: Props) => {
|
||||
const { fieldName, value, row } = props;
|
||||
const [uploaderState, upload] = useUploader();
|
||||
const [localImage, setLocalImage] = useState<string | null>(null);
|
||||
const onDrop = useCallback(acceptedFiles => {
|
||||
// Do something with the files
|
||||
const imageFile = acceptedFiles[0];
|
||||
if (imageFile) {
|
||||
upload(rowData.ref, columnData.fieldName, [imageFile]);
|
||||
upload(row.ref, fieldName, [imageFile]);
|
||||
let url = URL.createObjectURL(imageFile);
|
||||
setLocalImage(url);
|
||||
}
|
||||
@@ -37,10 +38,10 @@ const Image = (props: any) => {
|
||||
<input {...getInputProps()} />
|
||||
{localImage ? (
|
||||
<div>
|
||||
<img style={{ height: "150px" }} src={localImage} />
|
||||
<img style={{ height: "80px" }} src={localImage} />
|
||||
</div>
|
||||
) : cellData ? (
|
||||
<img style={{ height: "150px" }} src={cellData[0].downloadURL} />
|
||||
) : value ? (
|
||||
<img style={{ height: "80px" }} src={value[0].downloadURL} />
|
||||
) : isDragActive ? (
|
||||
<p>Drop the files here ...</p>
|
||||
) : (
|
||||
|
||||
@@ -1,22 +1,89 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import ExpandIcon from "@material-ui/icons/AspectRatio";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
import Popper from "@material-ui/core/Popper";
|
||||
import Fade from "@material-ui/core/Fade";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import TextareaAutosize from "@material-ui/core/TextareaAutosize";
|
||||
import ClickAwayListener from "@material-ui/core/ClickAwayListener";
|
||||
import { onSubmit } from "components/Table/grid-fns";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
position: "relative",
|
||||
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
typography: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
textArea: {
|
||||
fontSize: 14,
|
||||
minWidth: 230,
|
||||
},
|
||||
})
|
||||
);
|
||||
interface Props {
|
||||
value: firebase.firestore.Timestamp | null;
|
||||
row: any;
|
||||
value: any;
|
||||
row: { ref: firebase.firestore.DocumentReference; id: string };
|
||||
onSubmit: Function;
|
||||
}
|
||||
|
||||
const UrlLink = (props: any) => {
|
||||
const { value, cellActions } = props;
|
||||
return value ? (
|
||||
<>
|
||||
<IconButton>
|
||||
<ExpandIcon />
|
||||
</IconButton>
|
||||
<p>{value}</p>
|
||||
</>
|
||||
) : null;
|
||||
const LongText = (props: Props) => {
|
||||
const { value, row, onSubmit } = props;
|
||||
const [text, setText] = useState(value ? value : "");
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const classes = useStyles();
|
||||
|
||||
const handleClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const id = open ? "no-transition-popper" : undefined;
|
||||
const onClickAway = (event: any) => {
|
||||
if (event.target.id !== id) {
|
||||
onSubmit(text);
|
||||
setAnchorEl(null);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<ClickAwayListener onClickAway={onClickAway}>
|
||||
<div>
|
||||
<IconButton onClick={handleClick}>
|
||||
<ExpandIcon />
|
||||
</IconButton>
|
||||
{text}
|
||||
<Popper id={id} open={open} anchorEl={anchorEl}>
|
||||
<Paper>
|
||||
<Typography className={classes.typography}>
|
||||
<TextareaAutosize
|
||||
id={id}
|
||||
className={classes.textArea}
|
||||
rowsMax={6}
|
||||
aria-label="maximum height"
|
||||
placeholder="enter text"
|
||||
defaultValue={text}
|
||||
autoFocus
|
||||
onKeyPress={(e: any) => {
|
||||
setText(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Popper>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default UrlLink;
|
||||
export default LongText;
|
||||
|
||||
118
src/components/Fields/MultiSelect.tsx
Normal file
118
src/components/Fields/MultiSelect.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import React from "react";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import WarningIcon from "@material-ui/icons/Warning";
|
||||
import { Select } from "@material-ui/core";
|
||||
import {
|
||||
createStyles,
|
||||
makeStyles,
|
||||
useTheme,
|
||||
Theme,
|
||||
} from "@material-ui/core/styles";
|
||||
|
||||
import Input from "@material-ui/core/Input";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import MenuItem from "@material-ui/core/MenuItem";
|
||||
import Chip from "@material-ui/core/Chip";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
formControl: {
|
||||
margin: theme.spacing(1),
|
||||
minWidth: 120,
|
||||
maxWidth: 300,
|
||||
},
|
||||
chips: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
chip: {
|
||||
margin: 2,
|
||||
},
|
||||
noLabel: {
|
||||
marginTop: theme.spacing(3),
|
||||
},
|
||||
noOptions: {
|
||||
position: "absolute",
|
||||
top: -15,
|
||||
},
|
||||
})
|
||||
);
|
||||
interface Props {
|
||||
value: string[] | null;
|
||||
row: { ref: firebase.firestore.DocumentReference; id: string };
|
||||
options: string[];
|
||||
onSubmit: Function;
|
||||
}
|
||||
const ITEM_HEIGHT = 48;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
const MenuProps = {
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
|
||||
width: 250,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const MultiSelect = (props: Props) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const { value, row, options, onSubmit } = props;
|
||||
const handleChange = (e: any, v: any) => {
|
||||
if (!value) {
|
||||
// creates new array
|
||||
onSubmit([v.props.value]);
|
||||
} else if (!value.includes(v.props.value)) {
|
||||
// adds to array
|
||||
onSubmit([...value, v.props.value]);
|
||||
} else {
|
||||
// removes from array
|
||||
let _updatedValues = [...value];
|
||||
const index = _updatedValues.indexOf(v.props.value);
|
||||
_updatedValues.splice(index, 1);
|
||||
onSubmit(_updatedValues);
|
||||
}
|
||||
};
|
||||
|
||||
if (options && options.length !== 0)
|
||||
return (
|
||||
<Select
|
||||
multiple
|
||||
value={value ? value : []}
|
||||
defaultValue={[]}
|
||||
onChange={handleChange}
|
||||
input={<Input id="select-multiple-chip" />}
|
||||
renderValue={selected => (
|
||||
<div className={classes.chips}>
|
||||
{(selected as string[]).map(value => (
|
||||
<Chip key={value} label={value} className={classes.chip} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
{options.map(option => (
|
||||
<MenuItem key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<Grid className={classes.noOptions} direction="row">
|
||||
{/* <Grid item>
|
||||
<WarningIcon />
|
||||
</Grid>{" "} */}
|
||||
<Grid item>
|
||||
<Typography>add options!</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
export default MultiSelect;
|
||||
@@ -17,7 +17,7 @@ const Rating = (props: Props) => {
|
||||
name={`rating-controlled-${row.id}`}
|
||||
value={value}
|
||||
onChange={(event, newValue) => {
|
||||
onSubmit(row.ref, newValue);
|
||||
onSubmit(newValue);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ const UrlLink = (props: Props) => {
|
||||
return value ? (
|
||||
<>
|
||||
<EditIcon />
|
||||
<a href={value} target="_blank">
|
||||
<a href={value} target="_blank" rel="noopener noreferrer">
|
||||
{value}
|
||||
</a>
|
||||
</>
|
||||
|
||||
@@ -28,6 +28,10 @@ export enum FieldType {
|
||||
rating = "RATING",
|
||||
image = "IMAGE",
|
||||
file = "FILE",
|
||||
singleSelect = "SINGLE_SELECT",
|
||||
multiSelect = "MULTI_SELECT",
|
||||
documentSelect = "DOCUMENT_SELECT",
|
||||
documentsSelect = "DOCUMENTS_SELECT",
|
||||
}
|
||||
|
||||
export const FIELDS = [
|
||||
@@ -43,6 +47,10 @@ export const FIELDS = [
|
||||
{ icon: <RatingIcon />, name: "Rating", type: FieldType.rating },
|
||||
{ icon: <ImageIcon />, name: "Image", type: FieldType.image },
|
||||
{ icon: <FileIcon />, name: "File", type: FieldType.file },
|
||||
{ icon: <FileIcon />, name: "Single Select", type: FieldType.singleSelect },
|
||||
{ icon: <FileIcon />, name: "Multi Select", type: FieldType.multiSelect },
|
||||
{ icon: <FileIcon />, name: "Doc Select", type: FieldType.documentSelect },
|
||||
{ icon: <FileIcon />, name: "Docs Select", type: FieldType.documentsSelect },
|
||||
];
|
||||
|
||||
export const getFieldIcon = (type: FieldType) => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import CreateTableDialog from "./CreateTableDialog";
|
||||
|
||||
import useSettings from "../hooks/useSettings";
|
||||
import useRouter from "../hooks/useRouter";
|
||||
import TablesContext from "../contexts/tablesContext";
|
||||
|
||||
const useStyles = makeStyles(theme =>
|
||||
createStyles({
|
||||
@@ -63,69 +64,71 @@ const Navigation = (props: any) => {
|
||||
const classes = useStyles();
|
||||
const [settings, createTable] = useSettings();
|
||||
return (
|
||||
<React.Fragment>
|
||||
<CssBaseline />
|
||||
<Paper square className={classes.paper}>
|
||||
<Typography className={classes.text} variant="h5" gutterBottom>
|
||||
{props.header}
|
||||
</Typography>
|
||||
</Paper>
|
||||
{props.children}
|
||||
<AppBar position="fixed" color="primary" className={classes.appBar}>
|
||||
<Toolbar>
|
||||
<IconButton edge="start" color="inherit" aria-label="open drawer">
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
{!settings.tables ? (
|
||||
<>
|
||||
<Skeleton
|
||||
variant="rect"
|
||||
width={120}
|
||||
height={40}
|
||||
className={classes.skeleton}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rect"
|
||||
width={120}
|
||||
height={40}
|
||||
className={classes.skeleton}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rect"
|
||||
width={120}
|
||||
height={40}
|
||||
className={classes.skeleton}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rect"
|
||||
width={120}
|
||||
height={40}
|
||||
className={classes.skeleton}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{settings.tables.map(
|
||||
(table: { name: string; collection: string }) => (
|
||||
<Button
|
||||
key={table.collection}
|
||||
onClick={() => {
|
||||
router.history.push(table.collection);
|
||||
}}
|
||||
className={classes.button}
|
||||
>
|
||||
{table.name}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<TablesContext.Provider value={{ value: settings.tables }}>
|
||||
<React.Fragment>
|
||||
<CssBaseline />
|
||||
<Paper square className={classes.paper}>
|
||||
<Typography className={classes.text} variant="h5" gutterBottom>
|
||||
{props.header}
|
||||
</Typography>
|
||||
</Paper>
|
||||
{props.children}
|
||||
<AppBar position="fixed" color="primary" className={classes.appBar}>
|
||||
<Toolbar>
|
||||
<IconButton edge="start" color="inherit" aria-label="open drawer">
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
{!settings.tables ? (
|
||||
<>
|
||||
<Skeleton
|
||||
variant="rect"
|
||||
width={120}
|
||||
height={40}
|
||||
className={classes.skeleton}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rect"
|
||||
width={120}
|
||||
height={40}
|
||||
className={classes.skeleton}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rect"
|
||||
width={120}
|
||||
height={40}
|
||||
className={classes.skeleton}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rect"
|
||||
width={120}
|
||||
height={40}
|
||||
className={classes.skeleton}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{settings.tables.map(
|
||||
(table: { name: string; collection: string }) => (
|
||||
<Button
|
||||
key={table.collection}
|
||||
onClick={() => {
|
||||
router.history.push(table.collection);
|
||||
}}
|
||||
className={classes.button}
|
||||
>
|
||||
{table.name}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<CreateTableDialog classes={classes} createTable={createTable} />
|
||||
<div className={classes.grow} />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</React.Fragment>
|
||||
<CreateTableDialog classes={classes} createTable={createTable} />
|
||||
<div className={classes.grow} />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</React.Fragment>
|
||||
</TablesContext.Provider>
|
||||
);
|
||||
};
|
||||
export default Navigation;
|
||||
|
||||
170
src/components/Table/ColumnEditor/DocInput.tsx
Normal file
170
src/components/Table/ColumnEditor/DocInput.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
import Chip from "@material-ui/core/Chip";
|
||||
import { TextField, Grid, Divider, Select } from "@material-ui/core";
|
||||
import TablesContext from "../../../contexts/tablesContext";
|
||||
import MenuItem from "@material-ui/core/MenuItem";
|
||||
import useTableConfig from "../../../hooks/useFiretable/useTableConfig";
|
||||
import FormControl from "@material-ui/core/FormControl";
|
||||
import InputLabel from "@material-ui/core/InputLabel";
|
||||
import Input from "@material-ui/core/Input";
|
||||
|
||||
const useStyles = makeStyles(Theme =>
|
||||
createStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexWrap: "wrap",
|
||||
maxWidth: 300,
|
||||
padding: Theme.spacing(1),
|
||||
},
|
||||
chip: {
|
||||
margin: Theme.spacing(0.5),
|
||||
},
|
||||
formControl: {
|
||||
margin: Theme.spacing(1),
|
||||
minWidth: 120,
|
||||
maxWidth: 300,
|
||||
},
|
||||
chips: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const ITEM_HEIGHT = 48;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
const MenuProps = {
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
|
||||
width: 250,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function DocInput(props: any) {
|
||||
const { collectionPath, setValue } = props;
|
||||
const [tableConfig, tableConfigActions] = useTableConfig(collectionPath);
|
||||
|
||||
const [columns, setColumns] = React.useState<{ key: string; name: string }[]>(
|
||||
[]
|
||||
);
|
||||
const [primaryKeys, setPrimaryKeys] = React.useState<string[]>([]);
|
||||
const [secondaryKeys, setSecondaryKeys] = React.useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(tableConfig);
|
||||
setColumns(tableConfig.columns);
|
||||
}, [tableConfig.columns]);
|
||||
|
||||
const classes = useStyles();
|
||||
const tables = useContext(TablesContext);
|
||||
const onChange = (e: any, v: any) => {
|
||||
setValue("collectionPath", v.props.value);
|
||||
setPrimaryKeys([]);
|
||||
setSecondaryKeys([]);
|
||||
setColumns([]);
|
||||
tableConfigActions.setTable(v.props.value);
|
||||
};
|
||||
useEffect(() => {
|
||||
setValue("resultsConfig", {
|
||||
primaryKeys,
|
||||
secondaryKeys,
|
||||
});
|
||||
}, [primaryKeys, secondaryKeys]);
|
||||
if (tables.value)
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
value={collectionPath ? collectionPath : null}
|
||||
onChange={onChange}
|
||||
id={`select-key`}
|
||||
inputProps={{
|
||||
name: "Table",
|
||||
id: "table",
|
||||
}}
|
||||
>
|
||||
{tables.value.map((table: { collection: string; table: string }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
id={`select-collection-${table.collection}`}
|
||||
value={table.collection}
|
||||
>
|
||||
<>{table.collection}</>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="select-primary-multiple-chip">
|
||||
Primary Text
|
||||
</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
value={primaryKeys}
|
||||
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
|
||||
setPrimaryKeys(event.target.value as string[]);
|
||||
}}
|
||||
input={<Input id="select-primary-multiple-chip" />}
|
||||
renderValue={selected => (
|
||||
<div className={classes.chips}>
|
||||
{(selected as string[]).map(value => (
|
||||
<Chip key={value} label={value} className={classes.chip} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
{columns &&
|
||||
columns.length !== 0 &&
|
||||
columns.map(column => (
|
||||
<MenuItem
|
||||
id={`select-primary-column-${column.key}`}
|
||||
key={column.key}
|
||||
value={column.key}
|
||||
>
|
||||
{column.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="select-secondary-multiple-chip">
|
||||
Secondary Text
|
||||
</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
value={secondaryKeys}
|
||||
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
|
||||
setSecondaryKeys(event.target.value as string[]);
|
||||
}}
|
||||
input={<Input id="select-secondary-multiple-chip" />}
|
||||
renderValue={selected => (
|
||||
<div className={classes.chips}>
|
||||
{(selected as string[]).map(value => (
|
||||
<Chip key={value} label={value} className={classes.chip} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
{columns &&
|
||||
columns.length !== 0 &&
|
||||
columns.map(column => (
|
||||
<MenuItem
|
||||
id={`select-secondary-column-${column.key}`}
|
||||
key={column.key}
|
||||
value={column.key}
|
||||
>
|
||||
{column.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</>
|
||||
);
|
||||
else return <div />;
|
||||
}
|
||||
70
src/components/Table/ColumnEditor/SelectOptionsInput.tsx
Normal file
70
src/components/Table/ColumnEditor/SelectOptionsInput.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
import Chip from "@material-ui/core/Chip";
|
||||
import { TextField, Grid, Divider } from "@material-ui/core";
|
||||
import _includes from "lodash/includes";
|
||||
import _camelCase from "lodash/camelCase";
|
||||
|
||||
const useStyles = makeStyles(Theme =>
|
||||
createStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexWrap: "wrap",
|
||||
maxWidth: 300,
|
||||
padding: Theme.spacing(1),
|
||||
},
|
||||
chip: {
|
||||
margin: Theme.spacing(0.5),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function SelectOptionsInput(props: any) {
|
||||
const { options, setValue } = props;
|
||||
const classes = useStyles();
|
||||
|
||||
const handleAdd = (newOption: string) => {
|
||||
// setOptions([...options, newOption]);
|
||||
setValue("options", [...options, newOption]);
|
||||
};
|
||||
const handleDelete = (optionToDelete: string) => () => {
|
||||
const newOptions = options.filter(
|
||||
(option: string) => option !== optionToDelete
|
||||
);
|
||||
setValue("options", newOptions);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setValue({ data: { options } });
|
||||
}, [options]);
|
||||
|
||||
return (
|
||||
<Grid container direction="column" className={classes.root}>
|
||||
<Grid item>
|
||||
<TextField
|
||||
label="New Option"
|
||||
onKeyPress={(e: any) => {
|
||||
const value = e.target.value;
|
||||
if (e.key === "Enter") {
|
||||
handleAdd(value);
|
||||
e.target.value = "";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
{options.map((option: string) => {
|
||||
return (
|
||||
<Chip
|
||||
key={option}
|
||||
label={option}
|
||||
onDelete={handleDelete(option)}
|
||||
className={classes.chip}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useContext, useState } from "react";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import InputLabel from "@material-ui/core/InputLabel";
|
||||
@@ -11,8 +11,8 @@ import Fade from "@material-ui/core/Fade";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import ClickAwayListener from "@material-ui/core/ClickAwayListener";
|
||||
import { createStyles, makeStyles } from "@material-ui/core/styles";
|
||||
import { TextField, Grid } from "@material-ui/core";
|
||||
import { FieldsDropDown, isFieldType } from "../Fields";
|
||||
import { TextField, Grid, Select } from "@material-ui/core";
|
||||
import { FieldsDropDown, isFieldType, FieldType } from "../../Fields";
|
||||
import ToggleButton from "@material-ui/lab/ToggleButton";
|
||||
import ToggleButtonGroup from "@material-ui/lab/ToggleButtonGroup";
|
||||
|
||||
@@ -20,9 +20,12 @@ import LockIcon from "@material-ui/icons/Lock";
|
||||
import LockOpenIcon from "@material-ui/icons/LockOpen";
|
||||
import VisibilityIcon from "@material-ui/icons/Visibility";
|
||||
import VisibilityOffIcon from "@material-ui/icons/VisibilityOff";
|
||||
import FormatItalicIcon from "@material-ui/icons/FormatItalic";
|
||||
import FormatUnderlinedIcon from "@material-ui/icons/FormatUnderlined";
|
||||
import FormatColorFillIcon from "@material-ui/icons/FormatColorFill";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import SelectOptionsInput from "./SelectOptionsInput";
|
||||
|
||||
import DocInput from "./DocInput";
|
||||
|
||||
const useStyles = makeStyles(Theme =>
|
||||
createStyles({
|
||||
@@ -66,13 +69,16 @@ const useStyles = makeStyles(Theme =>
|
||||
})
|
||||
);
|
||||
|
||||
const HeaderPopper = (props: any) => {
|
||||
const ColumnEditor = (props: any) => {
|
||||
const { anchorEl, column, handleClose, actions } = props;
|
||||
const [values, setValues] = React.useState({
|
||||
|
||||
const [values, setValues] = useState({
|
||||
type: null,
|
||||
name: "",
|
||||
options: [],
|
||||
collectionPath: "",
|
||||
});
|
||||
const [flags, setFlags] = React.useState(() => [""]);
|
||||
const [flags, setFlags] = useState(() => [""]);
|
||||
const classes = useStyles();
|
||||
|
||||
function handleChange(
|
||||
@@ -91,7 +97,7 @@ const HeaderPopper = (props: any) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (column && !column.isNew)
|
||||
if (column && !column.isNew) {
|
||||
setValues(oldValues => ({
|
||||
...oldValues,
|
||||
name: column.name,
|
||||
@@ -99,11 +105,25 @@ const HeaderPopper = (props: any) => {
|
||||
key: column.key,
|
||||
isNew: column.isNew,
|
||||
}));
|
||||
if (column.options) {
|
||||
setValue("options", column.options);
|
||||
} else {
|
||||
setValue("options", []);
|
||||
}
|
||||
if (column.collectionPath) {
|
||||
setValue("collectionPath", column.collectionPath);
|
||||
}
|
||||
}
|
||||
}, [column]);
|
||||
const clearValues = () => {
|
||||
setValues({ type: null, name: "", options: [], collectionPath: "" });
|
||||
};
|
||||
const onClickAway = (event: any) => {
|
||||
const dropDownClicked = isFieldType(event.target.dataset.value);
|
||||
if (!dropDownClicked) {
|
||||
const elementId = event.target.id;
|
||||
console.log(event, elementId);
|
||||
if (!elementId.includes("select")) {
|
||||
handleClose();
|
||||
clearValues();
|
||||
}
|
||||
};
|
||||
const handleToggle = (
|
||||
@@ -114,9 +134,43 @@ const HeaderPopper = (props: any) => {
|
||||
};
|
||||
|
||||
const createNewColumn = () => {
|
||||
const { name, type } = values;
|
||||
actions.add(name, type);
|
||||
const { name, type, options, collectionPath } = values;
|
||||
|
||||
actions.add(name, type, { options, collectionPath });
|
||||
|
||||
handleClose();
|
||||
clearValues();
|
||||
};
|
||||
const deleteColumn = () => {
|
||||
actions.remove(props.column.idx);
|
||||
handleClose();
|
||||
clearValues();
|
||||
};
|
||||
|
||||
const updateColumn = () => {
|
||||
let updatables: { field: string; value: any }[] = [
|
||||
{ field: "name", value: values.name },
|
||||
{ field: "type", value: values.type },
|
||||
// { field: "resizable", value: flags.includes("resizable") },
|
||||
];
|
||||
if (
|
||||
values.type === FieldType.multiSelect ||
|
||||
values.type === FieldType.singleSelect
|
||||
) {
|
||||
updatables.push({ field: "options", value: values.options });
|
||||
}
|
||||
if (
|
||||
values.type === FieldType.documentSelect ||
|
||||
values.type === FieldType.documentsSelect
|
||||
) {
|
||||
updatables.push({
|
||||
field: "collectionPath",
|
||||
value: values.collectionPath,
|
||||
});
|
||||
}
|
||||
actions.update(props.column.idx, updatables);
|
||||
handleClose();
|
||||
clearValues();
|
||||
};
|
||||
|
||||
if (column) {
|
||||
@@ -132,6 +186,8 @@ const HeaderPopper = (props: any) => {
|
||||
<Fade {...TransitionProps} timeout={350}>
|
||||
<Paper className={classes.container}>
|
||||
<Grid container direction="column">
|
||||
{/*
|
||||
// TODO: functional flags
|
||||
<ToggleButtonGroup
|
||||
size="small"
|
||||
value={flags}
|
||||
@@ -139,33 +195,32 @@ const HeaderPopper = (props: any) => {
|
||||
onChange={handleToggle}
|
||||
arial-label="column settings"
|
||||
>
|
||||
<ToggleButton value="editable" aria-label="bold">
|
||||
<ToggleButton value="editable" aria-label="editable">
|
||||
{flags.includes("editable") ? (
|
||||
<LockOpenIcon />
|
||||
) : (
|
||||
<LockIcon />
|
||||
)}
|
||||
</ToggleButton>
|
||||
<ToggleButton value="visible" aria-label="italic">
|
||||
<ToggleButton value="visible" aria-label="visible">
|
||||
{flags.includes("visible") ? (
|
||||
<VisibilityIcon />
|
||||
) : (
|
||||
<VisibilityOffIcon />
|
||||
)}
|
||||
</ToggleButton>
|
||||
<ToggleButton value="freeze" aria-label="underlined">
|
||||
<ToggleButton value="freeze" aria-label="freeze">
|
||||
<FormatUnderlinedIcon />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="resize" aria-label="color">
|
||||
<ToggleButton value="resizable" aria-label="resizable">
|
||||
<FormatColorFillIcon />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</ToggleButtonGroup> */}
|
||||
|
||||
<TextField
|
||||
label="Column name"
|
||||
name="name"
|
||||
defaultValue={values.name}
|
||||
// onChange={handleChange}
|
||||
onChange={e => {
|
||||
setValue("name", e.target.value);
|
||||
}}
|
||||
@@ -174,10 +229,42 @@ const HeaderPopper = (props: any) => {
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="Field-select">Field Type</InputLabel>
|
||||
{FieldsDropDown(values.type, handleChange)}
|
||||
{column.isNew && (
|
||||
<Button onClick={createNewColumn}>Add</Button>
|
||||
|
||||
{(values.type === FieldType.singleSelect ||
|
||||
values.type === FieldType.multiSelect) && (
|
||||
<SelectOptionsInput
|
||||
setValue={setValue}
|
||||
options={values.options}
|
||||
/>
|
||||
)}
|
||||
<Button color="secondary" onClick={handleClose}>
|
||||
{(values.type === FieldType.documentSelect ||
|
||||
values.type === FieldType.documentsSelect) && (
|
||||
<DocInput
|
||||
setValue={setValue}
|
||||
collectionPath={values.collectionPath}
|
||||
/>
|
||||
)}
|
||||
{column.isNew ? (
|
||||
<Button onClick={createNewColumn}>Add</Button>
|
||||
) : (
|
||||
<Button onClick={updateColumn}>update</Button>
|
||||
)}
|
||||
{!column.isNew && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={deleteColumn}
|
||||
>
|
||||
<DeleteIcon /> Delete
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
clearValues();
|
||||
}}
|
||||
>
|
||||
cancel
|
||||
</Button>
|
||||
</FormControl>
|
||||
@@ -192,4 +279,4 @@ const HeaderPopper = (props: any) => {
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export default HeaderPopper;
|
||||
export default ColumnEditor;
|
||||
153
src/components/Table/grid-fns.tsx
Normal file
153
src/components/Table/grid-fns.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import React from "react";
|
||||
import { FieldType } from "../Fields";
|
||||
|
||||
import Date from "../Fields/Date";
|
||||
import Rating from "../Fields/Rating";
|
||||
import CheckBox from "../Fields/CheckBox";
|
||||
import UrlLink from "../Fields/UrlLink";
|
||||
|
||||
import { Editors } from "react-data-grid-addons";
|
||||
import MultiSelect from "../Fields/MultiSelect";
|
||||
import Image from "../Fields/Image";
|
||||
import File from "../Fields/File";
|
||||
import LongText from "../Fields/LongText";
|
||||
import DocSelect from "../Fields/DocSelect";
|
||||
import { CLOUD_FUNCTIONS } from "firebase/callables";
|
||||
import { functions } from "../../firebase";
|
||||
|
||||
const algoliaUpdateDoc = functions.httpsCallable(
|
||||
CLOUD_FUNCTIONS.updateAlgoliaRecord
|
||||
);
|
||||
const { AutoComplete } = Editors;
|
||||
|
||||
export const editable = (fieldType: FieldType) => {
|
||||
switch (fieldType) {
|
||||
case FieldType.date:
|
||||
case FieldType.dateTime:
|
||||
case FieldType.rating:
|
||||
case FieldType.checkBox:
|
||||
case FieldType.multiSelect:
|
||||
case FieldType.image:
|
||||
case FieldType.file:
|
||||
case FieldType.longText:
|
||||
case FieldType.documentSelect:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
};
|
||||
export const onSubmit = (key: string, row: any) => async (value: any) => {
|
||||
const collection = row.ref.parent.path;
|
||||
const data = { collection, id: row.ref.id, doc: { [key]: value } };
|
||||
if (value !== null || value !== undefined) {
|
||||
row.ref.update({ [key]: value });
|
||||
const callableRes = await algoliaUpdateDoc(data);
|
||||
console.log(callableRes);
|
||||
}
|
||||
};
|
||||
|
||||
export const DateFormatter = (key: string, fieldType: FieldType) => (
|
||||
props: any
|
||||
) => {
|
||||
return (
|
||||
<Date
|
||||
{...props}
|
||||
onSubmit={onSubmit(key, props.row)}
|
||||
fieldType={fieldType}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const onGridRowsUpdated = (props: any) => {
|
||||
const { fromRowData, updated } = props;
|
||||
onSubmit(Object.keys(updated)[0], fromRowData)(Object.values(updated)[0]);
|
||||
};
|
||||
export const onCellSelected = (args: any) => {
|
||||
console.log(args);
|
||||
};
|
||||
export const cellFormatter = (column: any) => {
|
||||
const { type, key, options } = column;
|
||||
switch (type) {
|
||||
case FieldType.date:
|
||||
case FieldType.dateTime:
|
||||
return DateFormatter(key, type);
|
||||
case FieldType.rating:
|
||||
return (props: any) => {
|
||||
return (
|
||||
<Rating
|
||||
{...props}
|
||||
onSubmit={onSubmit(key, props.row)}
|
||||
value={typeof props.value === "number" ? props.value : 0}
|
||||
/>
|
||||
);
|
||||
};
|
||||
case FieldType.checkBox:
|
||||
return (props: any) => {
|
||||
return <CheckBox {...props} onSubmit={onSubmit(key, props.row)} />;
|
||||
};
|
||||
case FieldType.url:
|
||||
return (props: any) => {
|
||||
return <UrlLink {...props} />;
|
||||
};
|
||||
case FieldType.multiSelect:
|
||||
return (props: any) => {
|
||||
return (
|
||||
<MultiSelect
|
||||
{...props}
|
||||
onSubmit={onSubmit(key, props.row)}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
case FieldType.image:
|
||||
return (props: any) => {
|
||||
return (
|
||||
<Image
|
||||
{...props}
|
||||
onSubmit={onSubmit(key, props.row)}
|
||||
fieldName={key}
|
||||
/>
|
||||
);
|
||||
};
|
||||
case FieldType.file:
|
||||
return (props: any) => {
|
||||
return (
|
||||
<File
|
||||
{...props}
|
||||
onSubmit={onSubmit(key, props.row)}
|
||||
fieldName={key}
|
||||
/>
|
||||
);
|
||||
};
|
||||
case FieldType.longText:
|
||||
return (props: any) => {
|
||||
return <LongText {...props} onSubmit={onSubmit(key, props.row)} />;
|
||||
};
|
||||
case FieldType.documentSelect:
|
||||
return (props: any) => {
|
||||
return (
|
||||
<DocSelect
|
||||
{...props}
|
||||
onSubmit={onSubmit(key, props.row)}
|
||||
collectionPath={column.collectionPath}
|
||||
/>
|
||||
);
|
||||
};
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const singleSelectEditor = (options: string[]) => {
|
||||
if (options) {
|
||||
const _options = options.map(option => ({
|
||||
id: option,
|
||||
value: option,
|
||||
title: option,
|
||||
text: option,
|
||||
}));
|
||||
return <AutoComplete options={_options} />;
|
||||
}
|
||||
|
||||
return <AutoComplete options={[]} />;
|
||||
};
|
||||
@@ -1,20 +1,26 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import ReactDataGrid from "react-data-grid";
|
||||
|
||||
import useFiretable from "../../hooks/useFiretable";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
|
||||
import { createStyles, Theme, makeStyles } from "@material-ui/core/styles";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
|
||||
import Button from "@material-ui/core/Button";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import HeaderPopper from "./HeaderPopper";
|
||||
import { FieldType, getFieldIcon } from "../Fields";
|
||||
|
||||
import Date from "../Fields/Date";
|
||||
import Rating from "../Fields/Rating";
|
||||
import CheckBox from "../Fields/CheckBox";
|
||||
import UrlLink from "../Fields/UrlLink";
|
||||
import ColumnEditor from "./ColumnEditor/index";
|
||||
import { functions } from "../../firebase";
|
||||
|
||||
import {
|
||||
cellFormatter,
|
||||
onCellSelected,
|
||||
onGridRowsUpdated,
|
||||
singleSelectEditor,
|
||||
editable,
|
||||
} from "./grid-fns";
|
||||
|
||||
import { CLOUD_FUNCTIONS } from "firebase/callables";
|
||||
const deleteAlgoliaRecord = functions.httpsCallable(
|
||||
CLOUD_FUNCTIONS.deleteAlgoliaRecord
|
||||
);
|
||||
|
||||
const useStyles = makeStyles(Theme =>
|
||||
createStyles({
|
||||
@@ -32,66 +38,12 @@ const useStyles = makeStyles(Theme =>
|
||||
})
|
||||
);
|
||||
|
||||
const copyPaste = (props: any) => {
|
||||
console.log(props);
|
||||
};
|
||||
const editable = (fieldType: FieldType) => {
|
||||
switch (fieldType) {
|
||||
case FieldType.date:
|
||||
case FieldType.dateTime:
|
||||
case FieldType.rating:
|
||||
case FieldType.checkBox:
|
||||
return false;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
};
|
||||
const onSubmit = (key: string) => (
|
||||
ref: firebase.firestore.DocumentReference,
|
||||
value: any
|
||||
) => {
|
||||
if (value !== null || value !== undefined) {
|
||||
ref.update({ [key]: value });
|
||||
}
|
||||
};
|
||||
|
||||
const DateFormatter = (key: string, fieldType: FieldType) => (props: any) => {
|
||||
return <Date {...props} onSubmit={onSubmit(key)} fieldType={fieldType} />;
|
||||
};
|
||||
|
||||
const formatter = (fieldType: FieldType, key: string) => {
|
||||
switch (fieldType) {
|
||||
case FieldType.date:
|
||||
case FieldType.dateTime:
|
||||
return DateFormatter(key, fieldType);
|
||||
case FieldType.rating:
|
||||
return (props: any) => {
|
||||
return (
|
||||
<Rating
|
||||
{...props}
|
||||
onSubmit={onSubmit(key)}
|
||||
value={typeof props.value === "number" ? props.value : 0}
|
||||
/>
|
||||
);
|
||||
};
|
||||
case FieldType.checkBox:
|
||||
return (props: any) => {
|
||||
return <CheckBox {...props} onSubmit={onSubmit(key)} />;
|
||||
};
|
||||
case FieldType.url:
|
||||
return (props: any) => {
|
||||
return <UrlLink {...props} onSubmit={onSubmit(key)} />;
|
||||
};
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function Table(props: any) {
|
||||
const { collection } = props;
|
||||
const { tableState, tableActions } = useFiretable(collection);
|
||||
|
||||
useEffect(() => {
|
||||
tableActions.set(collection);
|
||||
}, [collection]);
|
||||
const classes = useStyles();
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
@@ -109,14 +61,6 @@ function Table(props: any) {
|
||||
setHeader(headerProps);
|
||||
};
|
||||
|
||||
const onGridRowsUpdated = (props: any) => {
|
||||
const { fromRowData, updated } = props;
|
||||
fromRowData.ref.update(updated);
|
||||
};
|
||||
const onCellSelected = (args: any) => {
|
||||
// handleCloseHeader();
|
||||
console.log(args);
|
||||
};
|
||||
const headerRenderer = (props: any) => {
|
||||
const { column } = props;
|
||||
switch (column.key) {
|
||||
@@ -152,13 +96,14 @@ function Table(props: any) {
|
||||
if (tableState.columns) {
|
||||
let columns = tableState.columns.map((column: any) => ({
|
||||
width: 220,
|
||||
resizable: true,
|
||||
key: column.fieldName,
|
||||
name: column.columnName,
|
||||
editable: editable(column.type),
|
||||
resizeable: true,
|
||||
resizable: true,
|
||||
headerRenderer: headerRenderer,
|
||||
formatter: formatter(column.type, column.fieldName),
|
||||
formatter: cellFormatter(column),
|
||||
editor:
|
||||
column.type === FieldType.singleSelect
|
||||
? singleSelectEditor(column.options)
|
||||
: false,
|
||||
...column,
|
||||
}));
|
||||
columns.push({
|
||||
@@ -167,25 +112,52 @@ function Table(props: any) {
|
||||
name: "Add column",
|
||||
width: 160,
|
||||
headerRenderer: headerRenderer,
|
||||
formatter: (props: any) => (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
props.row.ref.delete();
|
||||
await deleteAlgoliaRecord({
|
||||
id: props.row.ref.id,
|
||||
collection: props.row.ref.parent.path,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete row
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
const rows = tableState.rows;
|
||||
const rows = tableState.rows; //.map((row: any) => ({ height: 100, ...row }));
|
||||
return (
|
||||
<>
|
||||
<ReactDataGrid
|
||||
rowHeight={40}
|
||||
columns={columns}
|
||||
rowGetter={i => rows[i]}
|
||||
rowsCount={rows.length}
|
||||
onGridRowsUpdated={onGridRowsUpdated}
|
||||
enableCellSelect={true}
|
||||
onCellCopyPaste={copyPaste}
|
||||
minHeight={500}
|
||||
onCellSelected={onCellSelected}
|
||||
onColumnResize={(idx, width) =>
|
||||
tableActions.column.resize(idx, width)
|
||||
}
|
||||
emptyRowsView={() => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
backgroundColor: "#ddd",
|
||||
padding: "100px",
|
||||
}}
|
||||
>
|
||||
<h3>no data to show</h3>
|
||||
<Button onClick={tableActions.row.add}>Add Row</Button>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Button onClick={tableActions.row.add}>Add Row</Button>
|
||||
<HeaderPopper
|
||||
<ColumnEditor
|
||||
handleClose={handleCloseHeader}
|
||||
anchorEl={anchorEl}
|
||||
column={header && header.column}
|
||||
|
||||
11
src/contexts/tablesContext.ts
Normal file
11
src/contexts/tablesContext.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
interface TablesContextInterface {
|
||||
value: any[] | undefined;
|
||||
}
|
||||
|
||||
const TablesContext = React.createContext<TablesContextInterface>({
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
export default TablesContext;
|
||||
@@ -1,6 +1,9 @@
|
||||
import { functions } from "./index";
|
||||
|
||||
export enum CLOUD_FUNCTIONS {}
|
||||
export enum CLOUD_FUNCTIONS {
|
||||
updateAlgoliaRecord = "updateAlgoliaRecord",
|
||||
deleteAlgoliaRecord = "deleteAlgoliaRecord",
|
||||
}
|
||||
|
||||
export const cloudFunction = (
|
||||
name: string,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
const STAGING_PROJECT_NAME = "antler-vc";
|
||||
const PRODUCTION_PROJECT_NAME = STAGING_PROJECT_NAME;
|
||||
|
||||
const stagingKey = "AIzaSyCADXbyMviWpJ_jPp4leEYMffL70Ahxo_k";
|
||||
const productionKey = stagingKey;
|
||||
|
||||
export const stagingConfig = {
|
||||
apiKey: stagingKey,
|
||||
authDomain: `${STAGING_PROJECT_NAME}.firebaseapp.com`,
|
||||
databaseURL: `https://${STAGING_PROJECT_NAME}.firebaseio.com`,
|
||||
projectId: STAGING_PROJECT_NAME,
|
||||
storageBucket: `${STAGING_PROJECT_NAME}.appspot.com`,
|
||||
messagingSenderId: "236015562107",
|
||||
};
|
||||
|
||||
export const productionConfig = stagingConfig;
|
||||
@@ -4,18 +4,18 @@ import "firebase/firestore";
|
||||
import "firebase/functions";
|
||||
import "firebase/storage";
|
||||
|
||||
import { productionConfig, stagingConfig } from "./config";
|
||||
const config = {
|
||||
apiKey: process.env.REACT_APP_FIREBASE_PROJECT_KEY,
|
||||
authDomain: `${process.env.REACT_APP_FIREBASE_PROJECT_NAME}.firebaseapp.com`,
|
||||
databaseURL: `https://${process.env.REACT_APP_FIREBASE_PROJECT_NAME}.firebaseio.com`,
|
||||
projectId: process.env.REACT_APP_FIREBASE_PROJECT_NAME,
|
||||
storageBucket: `${process.env.REACT_APP_FIREBASE_PROJECT_NAME}.appspot.com`,
|
||||
};
|
||||
|
||||
if (process.env.REACT_APP_ENV === "PRODUCTION") {
|
||||
console.log("production");
|
||||
firebase.initializeApp(productionConfig);
|
||||
} else {
|
||||
console.log("staging");
|
||||
firebase.initializeApp(stagingConfig);
|
||||
}
|
||||
firebase.initializeApp(config);
|
||||
|
||||
export const auth = firebase.auth();
|
||||
export const db = firebase.firestore();
|
||||
export const bucket = firebase.storage();
|
||||
export const functions = firebase.app().functions();
|
||||
export const functions = firebase.functions();
|
||||
export const googleProvider = new firebase.auth.GoogleAuthProvider();
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import useTable from "./useTable";
|
||||
import useTableConfig from "./useTableConfig";
|
||||
import useCell, { Cell } from "./useCell";
|
||||
|
||||
export type FiretableActions = {
|
||||
cell: { set: Function; update: Function };
|
||||
column: { add: Function; resize: Function; rename: Function };
|
||||
column: {
|
||||
add: Function;
|
||||
resize: Function;
|
||||
rename: Function;
|
||||
remove: Function;
|
||||
update: Function;
|
||||
};
|
||||
row: { add: any; delete: Function };
|
||||
table: { set: Function };
|
||||
set: Function;
|
||||
};
|
||||
|
||||
export type FiretableState = {
|
||||
cell: Cell;
|
||||
columns: any;
|
||||
rows: any;
|
||||
};
|
||||
@@ -20,27 +23,28 @@ const useFiretable = (collectionName: string) => {
|
||||
const [tableState, tableActions] = useTable({
|
||||
path: collectionName,
|
||||
});
|
||||
const [cellState, cellActions] = useCell({});
|
||||
|
||||
const setTable = (collectionName: string) => {
|
||||
configActions.setTable(collectionName);
|
||||
tableActions.setTable(collectionName);
|
||||
cellActions.set(null);
|
||||
};
|
||||
const state: FiretableState = {
|
||||
cell: cellState.cell,
|
||||
columns: tableConfig.columns,
|
||||
rows: tableState.rows,
|
||||
};
|
||||
const actions: FiretableActions = {
|
||||
cell: { ...cellActions },
|
||||
column: {
|
||||
add: configActions.add,
|
||||
resize: configActions.resize,
|
||||
rename: configActions.rename,
|
||||
update: configActions.update,
|
||||
remove: configActions.remove,
|
||||
},
|
||||
row: { add: tableActions.addRow, delete: tableActions.deleteRow },
|
||||
table: { set: setTable },
|
||||
row: {
|
||||
add: tableActions.addRow,
|
||||
delete: tableActions.deleteRow,
|
||||
},
|
||||
set: setTable,
|
||||
};
|
||||
|
||||
return { tableState: state, tableActions: actions };
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { db } from "../../firebase";
|
||||
import { useEffect, useReducer } from "react";
|
||||
import equals from "ramda/es/equals";
|
||||
|
||||
export type Cell = {
|
||||
fieldName: string;
|
||||
rowIndex: number;
|
||||
docRef: firebase.firestore.DocumentReference;
|
||||
value: any;
|
||||
};
|
||||
|
||||
const cellReducer = (prevState: any, newProps: any) => {
|
||||
return { ...prevState, ...newProps };
|
||||
};
|
||||
const cellIntialState = {
|
||||
prevCell: null,
|
||||
cell: null,
|
||||
};
|
||||
|
||||
const updateCell = (cell: Cell) => {
|
||||
cell.docRef.update({ [cell.fieldName]: cell.value });
|
||||
};
|
||||
const useCell = (intialOverrides: any) => {
|
||||
const [cellState, cellDispatch] = useReducer(cellReducer, {
|
||||
...cellIntialState,
|
||||
...intialOverrides,
|
||||
});
|
||||
useEffect(() => {
|
||||
const { prevCell, updatedValue } = cellState;
|
||||
// check for change
|
||||
if (
|
||||
updatedValue !== null &&
|
||||
updatedValue !== undefined &&
|
||||
prevCell &&
|
||||
prevCell.value !== updatedValue
|
||||
) {
|
||||
updateCell({ ...prevCell, value: updatedValue });
|
||||
cellDispatch({ updatedValue: null });
|
||||
}
|
||||
}, [cellState.cell]);
|
||||
const set = (cell: Cell | null) => {
|
||||
cellDispatch({ prevCell: cellState.cell, cell });
|
||||
};
|
||||
const update = (value: any) => {
|
||||
cellDispatch({ updatedValue: value });
|
||||
};
|
||||
|
||||
const updateFirestore = (cell: Cell) => {
|
||||
cellDispatch({ cell: null });
|
||||
updateCell(cell);
|
||||
};
|
||||
const actions = { set, update, updateFirestore };
|
||||
|
||||
return [cellState, actions];
|
||||
};
|
||||
|
||||
export default useCell;
|
||||
@@ -2,7 +2,6 @@ import { db } from "../../firebase";
|
||||
|
||||
import { useEffect, useReducer } from "react";
|
||||
import equals from "ramda/es/equals";
|
||||
import { Cell } from "./useCell";
|
||||
import firebase from "firebase/app";
|
||||
const CAP = 500;
|
||||
|
||||
@@ -11,7 +10,7 @@ const tableReducer = (prevState: any, newProps: any) => {
|
||||
switch (newProps.type) {
|
||||
case "more":
|
||||
if (prevState.limit < prevState.cap)
|
||||
// rows count hardcap
|
||||
// rows count hardCap
|
||||
return { ...prevState, limit: prevState.limit + 10 };
|
||||
else return { ...prevState };
|
||||
default:
|
||||
@@ -21,7 +20,7 @@ const tableReducer = (prevState: any, newProps: any) => {
|
||||
return { ...prevState, ...newProps };
|
||||
}
|
||||
};
|
||||
const tableIntialState = {
|
||||
const tableInitialState = {
|
||||
rows: [],
|
||||
prevFilters: null,
|
||||
prevPath: null,
|
||||
@@ -34,10 +33,10 @@ const tableIntialState = {
|
||||
cap: CAP,
|
||||
};
|
||||
|
||||
const useTable = (intialOverrides: any) => {
|
||||
const useTable = (initialOverrides: any) => {
|
||||
const [tableState, tableDispatch] = useReducer(tableReducer, {
|
||||
...tableIntialState,
|
||||
...intialOverrides,
|
||||
...tableInitialState,
|
||||
...initialOverrides,
|
||||
});
|
||||
const getRows = (
|
||||
filters: {
|
||||
@@ -144,6 +143,7 @@ const useTable = (intialOverrides: any) => {
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
};
|
||||
|
||||
const tableActions = { deleteRow, setTable, addRow };
|
||||
return [tableState, tableActions];
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect } from "react";
|
||||
import useDoc, { DocActions } from "../useDoc";
|
||||
import { FieldType } from "../../components/Fields";
|
||||
import _camelCase from "lodash/camelCase";
|
||||
|
||||
const useTableConfig = (tablePath: string) => {
|
||||
const [tableConfigState, documentDispatch] = useDoc({
|
||||
path: `${tablePath}/_FIRETABLE_`,
|
||||
@@ -11,17 +12,18 @@ const useTableConfig = (tablePath: string) => {
|
||||
if (doc && columns !== doc.columns) {
|
||||
documentDispatch({ columns: doc.columns });
|
||||
}
|
||||
}, [tableConfigState]);
|
||||
}, [tableConfigState.doc]);
|
||||
|
||||
const setTable = (table: string) => {
|
||||
documentDispatch({ path: `${table}/_FIRETABLE_`, columns: [], doc: null });
|
||||
};
|
||||
const add = (name: string, type: FieldType) => {
|
||||
const add = (name: string, type: FieldType, data?: any) => {
|
||||
//TODO: validation
|
||||
const { columns } = tableConfigState;
|
||||
const key = _camelCase(name);
|
||||
documentDispatch({
|
||||
action: DocActions.update,
|
||||
data: { columns: [...columns, { name, key, type }] },
|
||||
data: { columns: [...columns, { name, key, type, ...data }] },
|
||||
});
|
||||
};
|
||||
const resize = (index: number, width: number) => {
|
||||
@@ -29,12 +31,25 @@ const useTableConfig = (tablePath: string) => {
|
||||
columns[index].width = width;
|
||||
documentDispatch({ action: DocActions.update, data: { columns } });
|
||||
};
|
||||
const rename = () => {};
|
||||
const update = (index: number, updatables: any) => {
|
||||
const { columns } = tableConfigState;
|
||||
updatables.forEach((updatable: any) => {
|
||||
columns[index][updatable.field] = updatable.value;
|
||||
});
|
||||
documentDispatch({ action: DocActions.update, data: { columns } });
|
||||
};
|
||||
|
||||
const remove = (index: number) => {
|
||||
const { columns } = tableConfigState;
|
||||
columns.splice(index, 1);
|
||||
documentDispatch({ action: DocActions.update, data: { columns } });
|
||||
};
|
||||
const actions = {
|
||||
update,
|
||||
add,
|
||||
resize,
|
||||
rename,
|
||||
setTable,
|
||||
remove,
|
||||
};
|
||||
return [tableConfigState, actions];
|
||||
};
|
||||
|
||||
@@ -17,10 +17,17 @@ const useSettings = () => {
|
||||
const createTable = (name: string, collection: string) => {
|
||||
const { tables } = settingsState;
|
||||
// updates the setting doc
|
||||
documentDispatch({
|
||||
action: DocActions.update,
|
||||
data: { tables: [...tables, { name, collection }] },
|
||||
});
|
||||
if (tables) {
|
||||
documentDispatch({
|
||||
action: DocActions.update,
|
||||
data: { tables: [...tables, { name, collection }] },
|
||||
});
|
||||
} else {
|
||||
db.doc("_FIRETABLE_/settings").set(
|
||||
{ tables: [{ name, collection }] },
|
||||
{ merge: true }
|
||||
);
|
||||
}
|
||||
//create the firetable collection doc with empty columns
|
||||
db.collection(collection)
|
||||
.doc("_FIRETABLE_")
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
import { maxWidth } from "@material-ui/system";
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
Button,
|
||||
Typography,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React from "react";
|
||||
|
||||
import Navigation from "../components/Navigation";
|
||||
import Table from "../components/Table";
|
||||
|
||||
@@ -36,7 +36,7 @@ const useStyles = makeStyles(() =>
|
||||
|
||||
// TODO: Create an interface for props
|
||||
const TablesView = (props: any) => {
|
||||
const [settings, createTable] = useSettings();
|
||||
const [settings] = useSettings();
|
||||
const tables = settings.tables;
|
||||
const classes = useStyles();
|
||||
const router = useRouter();
|
||||
|
||||
125
yarn.lock
125
yarn.lock
@@ -1586,6 +1586,18 @@
|
||||
"@svgr/plugin-svgo" "^4.3.1"
|
||||
loader-utils "^1.2.3"
|
||||
|
||||
"@types/algoliasearch-helper@*":
|
||||
version "2.26.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/algoliasearch-helper/-/algoliasearch-helper-2.26.1.tgz#60cf377e7cb4bd9a55f7eba35182792763230a24"
|
||||
integrity sha512-JN1wq/yLxxBcc6MeSe57F9Aqv8wL964L0nBOUTSQ5OECzWxaECuGYV06VnGKn/c+9AGB97RAgqx2PUbYflZNqA==
|
||||
dependencies:
|
||||
"@types/algoliasearch" "*"
|
||||
|
||||
"@types/algoliasearch@*", "@types/algoliasearch@^3.34.2":
|
||||
version "3.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/algoliasearch/-/algoliasearch-3.34.2.tgz#f3daed26185a21b77632c28835e340147532f575"
|
||||
integrity sha512-a+ztY3iL+Dpor7wYaF4CO6obUYcVEyXue1ppQklP1VCUP+VGZyzMcYiZodNs9DFV1HEOW5VCLTIqiZ4ikQpKzA==
|
||||
|
||||
"@types/babel__core@^7.1.0":
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30"
|
||||
@@ -1730,6 +1742,22 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-instantsearch-core@*":
|
||||
version "5.2.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-instantsearch-core/-/react-instantsearch-core-5.2.9.tgz#c8050dedefcb86ff01a0ff584c1801f5b165fe4a"
|
||||
integrity sha512-tem7Vc8R1SZlt5hxW/xAJT3jUbfwBEg6ZYf9lWr8DtccFRZkzbC8dVhV9os3Fq73hqYXWImB/sS3/wfRqwdW2g==
|
||||
dependencies:
|
||||
"@types/algoliasearch-helper" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-instantsearch-dom@^5.2.6":
|
||||
version "5.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-instantsearch-dom/-/react-instantsearch-dom-5.2.6.tgz#b47fc2c19b1aa8df06a95f67d01f5485abace8f0"
|
||||
integrity sha512-gx7QBnL+rG50IUaIoj2SVwXT4O5HVOayQoEqiWF1RU1py43E+Wtlyi6WCdTEfEOz0sj6kWNEXAPMhfz+vjl9mQ==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
"@types/react-instantsearch-core" "*"
|
||||
|
||||
"@types/react-router-dom@^4.3.5":
|
||||
version "4.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.5.tgz#72f229967690c890d00f96e6b85e9ee5780db31f"
|
||||
@@ -2100,6 +2128,11 @@ after@0.8.2:
|
||||
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
|
||||
integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=
|
||||
|
||||
agentkeepalive@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-2.2.0.tgz#c5d1bd4b129008f1163f236f86e5faea2026e2ef"
|
||||
integrity sha1-xdG9SxKQCPEWPyNvhuX66iAm4u8=
|
||||
|
||||
ajv-errors@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
|
||||
@@ -2120,6 +2153,27 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5:
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
algoliasearch@^3.34.0:
|
||||
version "3.34.0"
|
||||
resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-3.34.0.tgz#02eb97bd6718e3a2c71121b9c3b655a35a4ba645"
|
||||
integrity sha512-s8LDedkTWTAWR5uCWgJzGxDkCrqiej5iE4Tc2iCV+ONOO35i5qnVdieKg5gv2VDXBE7IP0YoqfAq/CC0V8PA+Q==
|
||||
dependencies:
|
||||
agentkeepalive "^2.2.0"
|
||||
debug "^2.6.9"
|
||||
envify "^4.0.0"
|
||||
es6-promise "^4.1.0"
|
||||
events "^1.1.0"
|
||||
foreach "^2.0.5"
|
||||
global "^4.3.2"
|
||||
inherits "^2.0.1"
|
||||
isarray "^2.0.1"
|
||||
load-script "^1.0.0"
|
||||
object-keys "^1.0.11"
|
||||
querystring-es3 "^0.2.1"
|
||||
reduce "^1.0.1"
|
||||
semver "^5.1.0"
|
||||
tunnel-agent "^0.6.0"
|
||||
|
||||
alphanum-sort@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
|
||||
@@ -4334,6 +4388,11 @@ dom-storage@2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/dom-storage/-/dom-storage-2.1.0.tgz#00fb868bc9201357ea243c7bcfd3304c1e34ea39"
|
||||
integrity sha512-g6RpyWXzl0RR6OTElHKBl7nwnK87GUyZMYC7JWsB/IA73vpqK2K6LT39x4VepLxlSsWBFrPVLnsSR5Jyty0+2Q==
|
||||
|
||||
dom-walk@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
|
||||
integrity sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=
|
||||
|
||||
domain-browser@^1.1.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
|
||||
@@ -4610,6 +4669,14 @@ entities@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
|
||||
integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
|
||||
|
||||
envify@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/envify/-/envify-4.1.0.tgz#f39ad3db9d6801b4e6b478b61028d3f0b6819f7e"
|
||||
integrity sha512-IKRVVoAYr4pIx4yIWNsz9mOsboxlNXiu7TNBnem/K/uTHdkyzXWDzHCK7UTolqBbgaBz0tQHsD3YNls0uIIjiw==
|
||||
dependencies:
|
||||
esprima "^4.0.0"
|
||||
through "~2.3.4"
|
||||
|
||||
errno@^0.1.3, errno@~0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
|
||||
@@ -4667,6 +4734,11 @@ es6-iterator@2.0.3, es6-iterator@~2.0.3:
|
||||
es5-ext "^0.10.35"
|
||||
es6-symbol "^3.1.1"
|
||||
|
||||
es6-promise@^4.1.0:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
|
||||
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
|
||||
|
||||
es6-symbol@^3.1.1, es6-symbol@~3.1.1:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.2.tgz#859fdd34f32e905ff06d752e7171ddd4444a7ed1"
|
||||
@@ -4927,6 +4999,11 @@ eventemitter3@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
|
||||
integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
|
||||
|
||||
events@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
|
||||
integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
|
||||
|
||||
events@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88"
|
||||
@@ -5372,6 +5449,11 @@ for-own@^0.1.3:
|
||||
dependencies:
|
||||
for-in "^1.0.1"
|
||||
|
||||
foreach@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
|
||||
integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
|
||||
|
||||
forever-agent@~0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||
@@ -5633,6 +5715,14 @@ global-prefix@^3.0.0:
|
||||
kind-of "^6.0.2"
|
||||
which "^1.3.1"
|
||||
|
||||
global@^4.3.2:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
|
||||
integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
|
||||
dependencies:
|
||||
min-document "^2.19.0"
|
||||
process "^0.11.10"
|
||||
|
||||
globals@^11.1.0, globals@^11.7.0:
|
||||
version "11.12.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||
@@ -6598,6 +6688,11 @@ isarray@2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
|
||||
integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=
|
||||
|
||||
isarray@^2.0.1:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
|
||||
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
@@ -7490,11 +7585,14 @@ lines-and-columns@^1.1.6:
|
||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
|
||||
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
|
||||
|
||||
<<<<<<< HEAD
|
||||
listenercount@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937"
|
||||
integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=
|
||||
|
||||
=======
|
||||
>>>>>>> 388c726eacf3bd64f948944e2771b63a8afddedc
|
||||
load-json-file@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
|
||||
@@ -7526,6 +7624,11 @@ load-json-file@^4.0.0:
|
||||
pify "^3.0.0"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
load-script@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4"
|
||||
integrity sha1-BJGTngvuVkPuSUp+PaPSuscMbKQ=
|
||||
|
||||
loader-fs-cache@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.2.tgz#54cedf6b727e1779fd8f01205f05f6e88706f086"
|
||||
@@ -7871,6 +7974,13 @@ mimic-fn@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
||||
|
||||
min-document@^2.19.0:
|
||||
version "2.19.0"
|
||||
resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
|
||||
integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=
|
||||
dependencies:
|
||||
dom-walk "^0.1.0"
|
||||
|
||||
mini-create-react-context@^0.3.0:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189"
|
||||
@@ -8368,7 +8478,7 @@ object-is@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
|
||||
integrity sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=
|
||||
|
||||
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
|
||||
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.0, object-keys@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
|
||||
@@ -9887,7 +9997,7 @@ qs@~6.5.2:
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
||||
|
||||
querystring-es3@^0.2.0:
|
||||
querystring-es3@^0.2.0, querystring-es3@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
|
||||
integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=
|
||||
@@ -10363,6 +10473,13 @@ recursive-readdir@2.2.2:
|
||||
dependencies:
|
||||
minimatch "3.0.4"
|
||||
|
||||
reduce@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/reduce/-/reduce-1.0.2.tgz#0cd680ad3ffe0b060e57a5c68bdfce37168d361b"
|
||||
integrity sha512-xX7Fxke/oHO5IfZSk77lvPa/7bjMh9BuCk4OOoX5XTXrM7s0Z+MkPfSDfz0q7r91BhhGSs8gii/VEN/7zhCPpQ==
|
||||
dependencies:
|
||||
object-keys "^1.1.0"
|
||||
|
||||
redux@^3.7.1:
|
||||
version "3.7.2"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
|
||||
@@ -10843,7 +10960,7 @@ semver-compare@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
|
||||
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
@@ -11743,7 +11860,7 @@ through2@^2.0.0:
|
||||
readable-stream "~2.3.6"
|
||||
xtend "~4.0.1"
|
||||
|
||||
through@^2.3.6:
|
||||
through@^2.3.6, through@~2.3.4:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
|
||||
|
||||
Reference in New Issue
Block a user