mirror of
https://github.com/rowyio/rowy.git
synced 2026-07-13 05:48:53 +02:00
Merge branch 'master' into develop
* master: (46 commits) derivative column field renderer derivative column config UI reposition table fav button noreferrer typo fix connect table field configuration fix: csv import supporting the new columns structure fix:adding new columns to the end of the table fix: prevent table settings from crashing when roles are empty fix: update column options mapping in filters create and update table settings fix sideDrawer single select fix side drawer column restructure migration button update table connect popper tableConnect and subtable columns settings column setttings setup+ single/multiselect fields delete github workflow docker using yarn dockerfile test2 docker typo ...
This commit is contained in:
0
.github/workflows/firebase_hosting.yml
vendored
0
.github/workflows/firebase_hosting.yml
vendored
18
Dockerfile
18
Dockerfile
@@ -1,13 +1,5 @@
|
||||
# Stage 1
|
||||
FROM node:8 as react-build
|
||||
WORKDIR /www
|
||||
COPY . ./
|
||||
RUN yarn
|
||||
RUN yarn build
|
||||
|
||||
# Stage 2 - the production environment
|
||||
FROM nginx:alpine
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=react-build /app/build /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
FROM node:10.15.1
|
||||
WORKDIR cloud_functions/functions
|
||||
COPY cloud_functions/functions/package.json ./
|
||||
RUN yarn install
|
||||
COPY . .
|
||||
@@ -20,12 +20,15 @@ if (serviceAccount) {
|
||||
});
|
||||
const db = admin.firestore();
|
||||
|
||||
const main = async deployRequestPath => {
|
||||
await db.doc(deployRequestPath).update({ deployedAt: serverTimestamp() });
|
||||
const main = async (deployRequestPath: string, currentBuild) => {
|
||||
await db.doc(deployRequestPath).update({
|
||||
deployedAt: serverTimestamp(),
|
||||
currentBuild: currentBuild ?? "",
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
main(process.argv[2])
|
||||
main(process.argv[2], process.argv[3])
|
||||
.catch(err => console.log(err))
|
||||
.then(() => console.log("this will succeed"))
|
||||
.catch(() => "obligatory catch");
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
steps:
|
||||
# - name: "gcr.io/kaniko-project/executor:latest"
|
||||
# args:
|
||||
# - --destination=gcr.io/$PROJECT_ID/image
|
||||
# - --cache=true
|
||||
# - --cache-ttl=24h
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
@@ -34,6 +39,7 @@ steps:
|
||||
args:
|
||||
- "updateStatus"
|
||||
- "${_REQUEST_DOC_PATH}"
|
||||
- "${_FUNCTION_CONFIG}"
|
||||
dir: "cloud_functions/functions"
|
||||
|
||||
substitutions:
|
||||
|
||||
1
firetable
Submodule
1
firetable
Submodule
Submodule firetable added at 655a4ec398
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@antlerengineering/components": "^0.2.7",
|
||||
"@antlerengineering/multiselect": "^0.3.9",
|
||||
"@antlerengineering/multiselect": "^0.3.14",
|
||||
"@date-io/date-fns": "1.x",
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
@@ -16,30 +16,30 @@ export default function Firetable(props: SvgIconProps) {
|
||||
const strokeColor = "#fff";
|
||||
return (
|
||||
<svg width="140" height="32" viewBox="0 0 140 32">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<rect width="32" height="32" fill={primaryColor} rx="2.667" />
|
||||
<g fill={strokeColor}>
|
||||
<g fill-rule="nonzero">
|
||||
<g fillRule="nonzero">
|
||||
<path d="M5.333 18.667V24A2.667 2.667 0 0 0 8 26.667h5.333v-8h-8zM6.667 20H12v5.333H8A1.333 1.333 0 0 1 6.67 24.1l-.003-.1v-4zM18.667 18.667v8H24A2.667 2.667 0 0 0 26.667 24v-5.333h-8zM20 25.333V20h5.333v4c0 .703-.544 1.279-1.233 1.33l-.1.003h-4z" />
|
||||
<path d="M12 18.667v8h8v-8h-8zM13.333 20h5.334v5.333h-5.334V20z" />
|
||||
</g>
|
||||
<path
|
||||
fill-rule="nonzero"
|
||||
fillRule="nonzero"
|
||||
d="M18.667 12v8h8v-8h-8zM20 13.333h5.333v5.334H20v-5.334z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="nonzero"
|
||||
fillRule="nonzero"
|
||||
d="M12 12v8h8v-8h-8zm1.333 1.333h5.334v5.334h-5.334v-5.334z"
|
||||
/>
|
||||
<path
|
||||
stroke={strokeColor}
|
||||
stroke-width="1.333"
|
||||
strokeWidth="1.333"
|
||||
d="M19.333 6v6.667H26V6h-6.667zm2.577.667h1.512L25.672 12h-2.143l-.85-2.075L21.826 12h-2.165l2.248-5.333z"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
fill={primaryColor}
|
||||
fill-rule="nonzero"
|
||||
fillRule="nonzero"
|
||||
d="M43.467 26v-9.813h2.586V13.36h-2.586v-2.693c0-1.547 1.093-2.267 2.16-2.267 1.2 0 2.053.693 2.053 1.68v.187c0 .906.693 1.573 1.573 1.573s1.627-.693 1.627-1.573v-.294c0-2.24-1.787-4.453-5.253-4.453-2.987 0-5.36 1.76-5.36 4.88v2.96H37.76v2.827h2.507V26h3.2zm7.413 0V13.36h-3.2V26h3.2zm6.773 0v-8.267s.88-1.706 2.827-1.706c.933 0 1.52.293 1.52.293l1.12-2.88s-1.04-.4-2.187-.4c-2.346 0-3.44 2.16-3.44 2.16v-1.84h-3.04V26h3.2zm13.067.32c3.067 0 5.067-1.813 5.067-1.813l-1.6-2.4-.015.014c-.146.14-1.377 1.266-3.452 1.266-2 0-3.307-1.44-3.547-2.56h9.44c.027-.48.027-.96.027-1.2 0-3.894-2.907-6.587-6.373-6.587-3.654 0-6.347 2.933-6.347 6.64s2.96 6.64 6.8 6.64zm2.72-7.813h-6.267c.107-1.12 1.04-2.56 3.094-2.56 2.106 0 3.12 1.466 3.173 2.56zm11.333 7.813c1.84 0 3.04-.827 3.04-.827L86.64 22.88s-.613.453-1.333.453c-.854 0-1.654-.453-1.654-2V16.24h3.814v-2.88h-3.814V9.6h-3.2v3.76h-2.48v2.88h2.48v5.6c0 2.693 1.867 4.48 4.32 4.48zm9.28 0c2.854 0 4.16-1.92 4.16-1.92V26h2.96v-8.24c0-2.88-2.133-4.72-5.76-4.72-2.64 0-4.986 1.147-4.986 1.147l1.04 2.64s1.946-.934 3.786-.934c1.12 0 2.72.534 2.72 2.187v.72s-1.2-.907-3.52-.907c-2.746 0-4.96 1.76-4.96 4.187 0 2.693 2.187 4.24 4.56 4.24zm.987-2.667c-.987 0-2.293-.293-2.293-1.626 0-1.36 1.36-1.787 2.346-1.787 1.734 0 2.88.64 2.88.64v1.307s-1.013 1.466-2.933 1.466zm16.48 2.667c3.28 0 6.16-2.747 6.16-6.64s-2.88-6.64-6.16-6.64c-2.4 0-3.573 1.653-3.573 1.653V5.84h-3.2V26h2.96v-1.653s1.093 1.973 3.813 1.973zm-.56-2.987c-2.107 0-3.013-1.76-3.013-1.76v-3.786s.906-1.76 3.013-1.76c2.16 0 3.467 1.573 3.467 3.653s-1.307 3.653-3.467 3.653zM123.6 26V5.84h-3.2V26h3.2zm9.52.32c3.067 0 5.067-1.813 5.067-1.813l-1.6-2.4-.015.014c-.146.14-1.377 1.266-3.452 1.266-2 0-3.307-1.44-3.547-2.56h9.44c.027-.48.027-.96.027-1.2 0-3.894-2.907-6.587-6.373-6.587-3.654 0-6.347 2.933-6.347 6.64s2.96 6.64 6.8 6.64zm2.72-7.813h-6.267c.107-1.12 1.04-2.56 3.094-2.56 2.106 0 3.12 1.466 3.173 2.56z"
|
||||
/>
|
||||
</g>
|
||||
|
||||
10
www/src/assets/icons/CellResize.tsx
Normal file
10
www/src/assets/icons/CellResize.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon";
|
||||
|
||||
export default function FileDownload(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox="0 0 24 24" {...props}>
|
||||
<path d="M23 15h-2v2h2v-2zm0-4h-2v2h2v-2zm0 8h-2v2c1 0 2-1 2-2zM15 3h-2v2h2V3zm8 4h-2v2h2V7zm-2-4v2h2c0-1-1-2-2-2zM3 21h8v-6H1v4c0 1.1.9 2 2 2zM3 7H1v2h2V7zm12 12h-2v2h2v-2zm4-16h-2v2h2V3zm0 16h-2v2h2v-2zM3 3C2 3 1 4 1 5h2V3zm0 8H1v2h2v-2zm8-8H9v2h2V3zM7 3H5v2h2V3z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
13
www/src/assets/icons/ColumnPlusAfter.tsx
Normal file
13
www/src/assets/icons/ColumnPlusAfter.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon";
|
||||
|
||||
export default function FileDownload(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox="-2 -2 24 24" {...props}>
|
||||
<path
|
||||
fillRule="nonzero"
|
||||
d="M9 0a2 2 0 012 2v16a2 2 0 01-2 2H0V0h9M2 8v4h7V8H2m0 6v4h7v-4H2M2 2v4h7V2H2m11 7h3V6h2v3h3v2h-3v3h-2v-3h-3V9z"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
13
www/src/assets/icons/ColumnPlusBefore.tsx
Normal file
13
www/src/assets/icons/ColumnPlusBefore.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon";
|
||||
|
||||
export default function FileDownload(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox="-2 -2 24 24" {...props}>
|
||||
<path
|
||||
fillRule="nonzero"
|
||||
d="M12 0a2 2 0 00-2 2v16a2 2 0 002 2h9V0h-9m7 8v4h-7V8h7m0 6v4h-7v-4h7m0-12v4h-7V2h7M8 9H5V6H3v3H0v2h3v3h2v-3h3V9z"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
13
www/src/assets/icons/ColumnRemove.tsx
Normal file
13
www/src/assets/icons/ColumnRemove.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon";
|
||||
|
||||
export default function FileDownload(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox="-2 -2 24 24" {...props}>
|
||||
<path
|
||||
fillRule="nonzero"
|
||||
d="M2 0h7a2 2 0 012 2v16a2 2 0 01-2 2H2a2 2 0 01-2-2V2a2 2 0 012-2m0 8v4h7V8H2m0 6v4h7v-4H2M2 2v4h7V2H2m13.59 8L13 7.41 14.41 6 17 8.59 19.59 6 21 7.41 18.41 10 21 12.59 19.59 14 17 11.41 14.41 14 13 12.59 15.59 10z"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
13
www/src/assets/icons/Freeze.tsx
Normal file
13
www/src/assets/icons/Freeze.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon";
|
||||
|
||||
export default function FileDownload(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox="-2 -2 24 24" {...props}>
|
||||
<path
|
||||
d="M18 0a2 2 0 012 2v16a2 2 0 01-2 2H2a2 2 0 01-2-2V2A2 2 0 011.135.196l.047-.074.051.03C1.469.054 1.728 0 2 0h16zm0 14h-7v4h7v-4zm-16 .992V18h4.813L2 14.992zM9 14H4.187L9 17.008V14zm9-6h-7v4h7V8zM2 8.992V12h4.813L2 8.992zM9 8H4.187L9 11.007V8zm9-6h-7v4h7V2zM2 2.992V6h4.814L2 2.992zM9 2H4.188L9 5.007V2z"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
13
www/src/assets/icons/Unfreeze.tsx
Normal file
13
www/src/assets/icons/Unfreeze.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon";
|
||||
|
||||
export default function FileDownload(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox="-2 -2 24 24" {...props}>
|
||||
<path
|
||||
d="M2 20a2 2 0 01-2-2V2a2 2 0 012-2h16a2 2 0 012 2v16a2 2 0 01-2 2H2zm16-6h-7v4h7v-4zm-9 0H2v4h7v-4zm9-6h-7v4h7V8zM9 8H2v4h7V8zm9-6h-7v4h7V2zM9 2H2v4h7V2z"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -42,13 +42,14 @@ export default function PopupContents({
|
||||
multiple = true,
|
||||
row,
|
||||
}: IPopupContentsProps) {
|
||||
const index = collectionPath ?? config.index; //temporary for pre column restructure migration
|
||||
const classes = useStyles();
|
||||
|
||||
const { userClaims } = useFiretableContext();
|
||||
|
||||
const algoliaIndex = useMemo(() => {
|
||||
return searchClient.initIndex(collectionPath);
|
||||
}, [collectionPath]);
|
||||
return searchClient.initIndex(index);
|
||||
}, [index]);
|
||||
|
||||
// Algolia search query
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -62,11 +63,10 @@ export default function PopupContents({
|
||||
console.log("SEARCH", query, algoliaIndex, row);
|
||||
|
||||
const data = { ...userClaims, ...row };
|
||||
const filters = config.filters.replace(
|
||||
/\{\{(.*?)\}\}/g,
|
||||
(m, k) => data[k]
|
||||
);
|
||||
console.log(filters);
|
||||
const filters = config?.filters
|
||||
? config?.filters.replace(/\{\{(.*?)\}\}/g, (m, k) => data[k])
|
||||
: "";
|
||||
|
||||
const resp = await algoliaIndex.search(query, {
|
||||
filters,
|
||||
});
|
||||
@@ -86,7 +86,7 @@ export default function PopupContents({
|
||||
const { _highlightResult, ...snapshot } = hit;
|
||||
const output = {
|
||||
snapshot,
|
||||
docPath: `${collectionPath}/${snapshot.objectID}`,
|
||||
docPath: `${index}/${snapshot.objectID}`,
|
||||
};
|
||||
|
||||
if (multiple) onChange([...value, output]);
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import _camelCase from "lodash/camelCase";
|
||||
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
import useRouter from "../hooks/useRouter";
|
||||
import MultiSelect from "@antlerengineering/multiselect";
|
||||
import {
|
||||
Tooltip,
|
||||
Fab,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
TextField,
|
||||
Button,
|
||||
Select,
|
||||
} from "@material-ui/core";
|
||||
import { useFiretableContext } from "../contexts/firetableContext";
|
||||
|
||||
export interface ICreateTableDialogProps {
|
||||
/** Optional class overrides */
|
||||
classes?: Partial<Record<"fab", string>>;
|
||||
/** Function to handle table creation */
|
||||
createTable: (tableName: string, collectionName: string) => void;
|
||||
}
|
||||
|
||||
export default function CreateTableDialog({
|
||||
classes = {},
|
||||
createTable,
|
||||
}: ICreateTableDialogProps) {
|
||||
const router = useRouter();
|
||||
const { userClaims } = useFiretableContext();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [tableName, setTableName] = useState("");
|
||||
const [collectionName, setCollectionName] = useState("");
|
||||
useEffect(() => {
|
||||
setCollectionName(_camelCase(tableName));
|
||||
}, [tableName]);
|
||||
function handleClickOpen() {
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setTableName("");
|
||||
setCollectionName("");
|
||||
setOpen(false);
|
||||
}
|
||||
function handleCreate() {
|
||||
createTable(tableName, collectionName);
|
||||
if (router.location.pathname === "/") {
|
||||
router.history.push(`table/${collectionName}`);
|
||||
} else {
|
||||
router.history.push(collectionName);
|
||||
}
|
||||
|
||||
handleClose();
|
||||
}
|
||||
if (userClaims && userClaims.roles?.includes("ADMIN"))
|
||||
return (
|
||||
<div>
|
||||
<Tooltip title="Create a table">
|
||||
<Fab
|
||||
className={classes.fab}
|
||||
color="secondary"
|
||||
aria-label="Create table"
|
||||
onClick={handleClickOpen}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
|
||||
<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>
|
||||
<Select label="Section" />
|
||||
<TextField
|
||||
autoFocus
|
||||
onChange={e => {
|
||||
setTableName(e.target.value);
|
||||
}}
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="Table Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
variant="filled"
|
||||
/>
|
||||
<TextField
|
||||
value={collectionName}
|
||||
onChange={e => {
|
||||
setCollectionName(e.target.value);
|
||||
}}
|
||||
variant="filled"
|
||||
margin="dense"
|
||||
id="collection"
|
||||
label="Collection Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Description"
|
||||
id="description"
|
||||
variant="filled"
|
||||
fullWidth
|
||||
multiline={true}
|
||||
/>
|
||||
<MultiSelect
|
||||
freeText={true}
|
||||
options={["PROGRAM", "RECRUITING", "COACHING", "ADMIN"]}
|
||||
multiple
|
||||
value={[]}
|
||||
selectAll
|
||||
label="Roles"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreate} color="primary">
|
||||
Create
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
else return <></>;
|
||||
}
|
||||
@@ -21,7 +21,15 @@ export default function Autosave({ values, errors }: IAutosaveProps) {
|
||||
const { tableState, sideDrawerRef } = useFiretableContext();
|
||||
|
||||
const getEditables = value =>
|
||||
_pick(value, tableState?.columns?.map(c => c.key) ?? []);
|
||||
_pick(
|
||||
value,
|
||||
(tableState &&
|
||||
(Array.isArray(tableState?.columns)
|
||||
? tableState?.columns
|
||||
: Object.values(tableState?.columns)
|
||||
).map(c => c.key)) ??
|
||||
[]
|
||||
);
|
||||
|
||||
const [debouncedValue] = useDebounce(getEditables(values), 1000, {
|
||||
equalityFn: _isEqual,
|
||||
|
||||
@@ -44,8 +44,8 @@ function Action({
|
||||
field,
|
||||
form,
|
||||
editable,
|
||||
callableName,
|
||||
}: FieldProps<any> & { callableName: string; editable?: boolean }) {
|
||||
config,
|
||||
}: FieldProps<any> & { config: { callableName: string }; editable?: boolean }) {
|
||||
const classes = useStyles();
|
||||
const { ref, ...docData } = form.values;
|
||||
|
||||
@@ -57,7 +57,7 @@ function Action({
|
||||
console.log("RUN");
|
||||
|
||||
cloudFunction(
|
||||
callableName,
|
||||
config.callableName,
|
||||
{
|
||||
ref: { path: ref.path, id: ref.id },
|
||||
row: sanitiseRowData(Object.assign({}, docData)),
|
||||
@@ -69,7 +69,7 @@ function Action({
|
||||
if (cellValue) form.setFieldValue(field.name, cellValue);
|
||||
},
|
||||
error => {
|
||||
console.error("ERROR", callableName, error);
|
||||
console.error("ERROR", config.callableName, error);
|
||||
setIsRunning(false);
|
||||
snack.open({ message: JSON.stringify(error), severity: "error" });
|
||||
}
|
||||
@@ -93,14 +93,14 @@ function Action({
|
||||
<a
|
||||
href={field.value.status}
|
||||
target="_blank"
|
||||
rel="noopener noreferer"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{field.value.status}
|
||||
</a>
|
||||
) : hasRan ? (
|
||||
field.value.status
|
||||
) : (
|
||||
sanitiseCallableName(callableName)
|
||||
sanitiseCallableName(config.callableName)
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
@@ -10,8 +10,13 @@ export default function MultiSelect({
|
||||
field,
|
||||
form,
|
||||
editable,
|
||||
config,
|
||||
...props
|
||||
}: FieldProps<string[]> & MultiSelectProps<string> & { editable?: boolean }) {
|
||||
}: FieldProps<string[]> &
|
||||
MultiSelectProps<string> & {
|
||||
config: { options: string[] };
|
||||
editable?: boolean;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
const handleDelete = (index: number) => () => {
|
||||
@@ -24,6 +29,7 @@ export default function MultiSelect({
|
||||
<>
|
||||
<MultiSelectA
|
||||
{...props}
|
||||
options={config.options ?? []}
|
||||
multiple
|
||||
value={field.value ? field.value : []}
|
||||
onChange={value => form.setFieldValue(field.name, value)}
|
||||
|
||||
@@ -14,8 +14,13 @@ export default function SingleSelect({
|
||||
field,
|
||||
form,
|
||||
editable,
|
||||
config,
|
||||
...props
|
||||
}: FieldProps<string> & MultiSelectProps<string> & { editable: boolean }) {
|
||||
}: FieldProps<string> &
|
||||
MultiSelectProps<string> & {
|
||||
config: { options: string[] };
|
||||
editable: boolean;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
const handleChange = value => form.setFieldValue(field.name, value);
|
||||
@@ -24,6 +29,7 @@ export default function SingleSelect({
|
||||
<>
|
||||
<MultiSelect
|
||||
{...props}
|
||||
options={config.options}
|
||||
multiple={false}
|
||||
value={field.value}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -35,33 +35,34 @@ export default function SubTable({
|
||||
form,
|
||||
field,
|
||||
label,
|
||||
parentLabel,
|
||||
}: FieldProps<any> & { parentLabel?: string; label: string }) {
|
||||
config,
|
||||
}: FieldProps<any> & { config: { parentLabel?: string[] }; label: string }) {
|
||||
const classes = useStyles();
|
||||
|
||||
const router = useRouter();
|
||||
const parentLabels = queryString.parse(router.location.search).parentLabel;
|
||||
|
||||
const _label = config?.parentLabel
|
||||
? config.parentLabel.reduce((acc, curr) => {
|
||||
if (acc !== "") return `${acc} - ${form.values[curr]}`;
|
||||
else return form.values[curr];
|
||||
}, "")
|
||||
: "";
|
||||
let subTablePath = "";
|
||||
if (parentLabels)
|
||||
subTablePath =
|
||||
encodeURIComponent(`${form.values.ref.path}/${field.name}`) +
|
||||
`?parentLabel=${parentLabels},${
|
||||
parentLabel ? form.values[parentLabel] : ""
|
||||
}`;
|
||||
`?parentLabel=${parentLabels},${label}`;
|
||||
else
|
||||
subTablePath =
|
||||
encodeURIComponent(`${form.values.ref.path}/${field.name}`) +
|
||||
`?parentLabel=${
|
||||
parentLabel ? encodeURIComponent(form.values[parentLabel]) : ""
|
||||
}`;
|
||||
`?parentLabel=${encodeURIComponent(_label)}`;
|
||||
|
||||
return (
|
||||
<Grid container wrap="nowrap">
|
||||
<Grid container alignItems="center" className={classes.labelContainer}>
|
||||
<Typography variant="body1">
|
||||
{label}
|
||||
{parentLabel && `: ${form.values[parentLabel]}`}
|
||||
{`: ${_label}`}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function Url(props: TextFieldProps) {
|
||||
component="a"
|
||||
href={props.field.value}
|
||||
target="_blank"
|
||||
rel="noopener noreferer"
|
||||
rel="noopener noreferrer"
|
||||
style={{ width: 56 }}
|
||||
disabled={!props.field.value}
|
||||
>
|
||||
|
||||
@@ -91,48 +91,48 @@ export type Field = {
|
||||
};
|
||||
export type Fields = (Field | ((values: Values) => Field))[];
|
||||
|
||||
const initializeValue = type => {
|
||||
switch (type) {
|
||||
case FieldType.singleSelect:
|
||||
case FieldType.multiSelect:
|
||||
case FieldType.image:
|
||||
case FieldType.file:
|
||||
return [];
|
||||
case FieldType.date:
|
||||
case FieldType.dateTime:
|
||||
return null;
|
||||
|
||||
case FieldType.checkbox:
|
||||
return false;
|
||||
|
||||
case FieldType.number:
|
||||
return 0;
|
||||
|
||||
case FieldType.json:
|
||||
return {};
|
||||
break;
|
||||
|
||||
case FieldType.shortText:
|
||||
case FieldType.longText:
|
||||
case FieldType.email:
|
||||
case FieldType.phone:
|
||||
case FieldType.url:
|
||||
case FieldType.code:
|
||||
case FieldType.richText:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const getInitialValues = (fields: Fields): Values =>
|
||||
fields.reduce((acc, _field) => {
|
||||
const field = _isFunction(_field) ? _field({}) : _field;
|
||||
if (!field.name) return acc;
|
||||
|
||||
let value: any = "";
|
||||
|
||||
switch (field.type) {
|
||||
case FieldType.singleSelect:
|
||||
case FieldType.multiSelect:
|
||||
case FieldType.image:
|
||||
case FieldType.file:
|
||||
value = [];
|
||||
break;
|
||||
|
||||
case FieldType.date:
|
||||
case FieldType.dateTime:
|
||||
value = null;
|
||||
break;
|
||||
|
||||
case FieldType.checkbox:
|
||||
value = false;
|
||||
break;
|
||||
|
||||
case FieldType.number:
|
||||
value = 0;
|
||||
break;
|
||||
|
||||
case FieldType.json:
|
||||
value = {};
|
||||
break;
|
||||
|
||||
case FieldType.shortText:
|
||||
case FieldType.longText:
|
||||
case FieldType.email:
|
||||
case FieldType.phone:
|
||||
case FieldType.url:
|
||||
case FieldType.code:
|
||||
case FieldType.richText:
|
||||
default:
|
||||
break;
|
||||
let _type = field.type;
|
||||
if (field.config && field.config.renderFieldType) {
|
||||
_type = field.config.renderFieldType;
|
||||
}
|
||||
const value = initializeValue(_type);
|
||||
|
||||
return { ...acc, [field.name]: value };
|
||||
}, {});
|
||||
@@ -179,13 +179,16 @@ export default function Form({ fields, values }: IFormProps) {
|
||||
? _field(values)
|
||||
: _field;
|
||||
const { type, ...fieldProps } = field;
|
||||
|
||||
let _type = type;
|
||||
if (field.config && field.config.renderFieldType) {
|
||||
_type = field.config.renderFieldType;
|
||||
}
|
||||
// TODO: handle get initial field value for when a field is later
|
||||
// shown to prevent uncontrolled components becoming controlled
|
||||
|
||||
let renderedField: React.ReactNode = null;
|
||||
|
||||
switch (type) {
|
||||
switch (_type) {
|
||||
case FieldType.shortText:
|
||||
case FieldType.longText:
|
||||
case FieldType.email:
|
||||
@@ -336,7 +339,7 @@ export default function Form({ fields, values }: IFormProps) {
|
||||
return (
|
||||
<FieldWrapper
|
||||
key={fieldProps.name ?? i}
|
||||
type={type}
|
||||
type={_type}
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import clsx from "clsx";
|
||||
import _isNil from "lodash/isNil";
|
||||
import _sortBy from "lodash/sortBy";
|
||||
import _findIndex from "lodash/findIndex";
|
||||
|
||||
import { Drawer, Fab } from "@material-ui/core";
|
||||
@@ -53,54 +54,50 @@ export default function SideDrawer() {
|
||||
};
|
||||
|
||||
// Map columns to form fields
|
||||
const fields = tableState?.columns?.map(column => {
|
||||
const field: Field = {
|
||||
type: column.type,
|
||||
name: column.key,
|
||||
label: column.name,
|
||||
};
|
||||
|
||||
switch (column.type) {
|
||||
case FieldType.longText:
|
||||
field.fieldVariant = "long";
|
||||
break;
|
||||
const fields =
|
||||
tableState?.columns &&
|
||||
(Array.isArray(tableState?.columns)
|
||||
? tableState?.columns
|
||||
: _sortBy(Object.values(tableState?.columns), "index")
|
||||
).map(column => {
|
||||
const field: Field = {
|
||||
type: column.type,
|
||||
name: column.key,
|
||||
label: column.name,
|
||||
};
|
||||
|
||||
case FieldType.email:
|
||||
field.fieldVariant = "email";
|
||||
break;
|
||||
switch (column.type) {
|
||||
case FieldType.longText:
|
||||
field.fieldVariant = "long";
|
||||
break;
|
||||
|
||||
case FieldType.phone:
|
||||
field.fieldVariant = "phone";
|
||||
break;
|
||||
case FieldType.email:
|
||||
field.fieldVariant = "email";
|
||||
break;
|
||||
|
||||
case FieldType.number:
|
||||
field.fieldVariant = "number";
|
||||
break;
|
||||
case FieldType.phone:
|
||||
field.fieldVariant = "phone";
|
||||
break;
|
||||
|
||||
case FieldType.singleSelect:
|
||||
case FieldType.multiSelect:
|
||||
field.options = column.options;
|
||||
break;
|
||||
case FieldType.number:
|
||||
field.fieldVariant = "number";
|
||||
break;
|
||||
|
||||
case FieldType.connectTable:
|
||||
field.collectionPath = column.collectionPath;
|
||||
field.config = column.config;
|
||||
break;
|
||||
case FieldType.singleSelect:
|
||||
case FieldType.multiSelect:
|
||||
case FieldType.connectTable:
|
||||
case FieldType.subTable:
|
||||
case FieldType.action:
|
||||
field.config = column.config;
|
||||
break;
|
||||
|
||||
case FieldType.subTable:
|
||||
field.parentLabel = column.parentLabel;
|
||||
break;
|
||||
|
||||
case FieldType.action:
|
||||
field.callableName = column.callableName;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
field.editable = column.editable;
|
||||
return field;
|
||||
});
|
||||
default:
|
||||
break;
|
||||
}
|
||||
field.editable = column.editable;
|
||||
return field;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={clsx(open && classes.open, disabled && classes.disabled)}>
|
||||
|
||||
@@ -66,6 +66,7 @@ interface StyledCardProps {
|
||||
label?: string;
|
||||
};
|
||||
secondaryAction?: React.ReactNode;
|
||||
headerAction?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function StyledCard({
|
||||
@@ -77,6 +78,7 @@ export default function StyledCard({
|
||||
primaryButton,
|
||||
primaryLink,
|
||||
secondaryAction,
|
||||
headerAction,
|
||||
}: StyledCardProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
@@ -107,11 +109,14 @@ export default function StyledCard({
|
||||
{overline}
|
||||
</Typography>
|
||||
)}
|
||||
{title && (
|
||||
<Typography variant="h5" className={classes.title}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
<Grid container direction="row" justify="space-between">
|
||||
{title && (
|
||||
<Typography variant="h5" className={classes.title}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{headerAction && headerAction}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{imageSource && (
|
||||
|
||||
@@ -65,13 +65,14 @@ const useStyles = makeStyles(theme =>
|
||||
})
|
||||
);
|
||||
|
||||
// TODO: REMOVE THIS OLD COMPONENT
|
||||
export default function ColumnEditor() {
|
||||
const {
|
||||
tableState,
|
||||
tableActions,
|
||||
selectedColumnHeader,
|
||||
setSelectedColumnHeader,
|
||||
} = useFiretableContext();
|
||||
} = useFiretableContext() as any;
|
||||
const actions = tableActions!.column;
|
||||
const { column, anchorEl } = selectedColumnHeader ?? {};
|
||||
|
||||
@@ -176,7 +177,7 @@ export default function ColumnEditor() {
|
||||
]);
|
||||
|
||||
const deleteColumn = () => {
|
||||
actions.remove(configColumnIndex);
|
||||
actions.remove(column?.key);
|
||||
handleClose();
|
||||
clearValues();
|
||||
};
|
||||
|
||||
@@ -90,17 +90,21 @@ const ColumnHeader: Column<any>["headerRenderer"] = ({ column }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const {
|
||||
setSelectedColumnHeader,
|
||||
tableState,
|
||||
tableActions,
|
||||
userClaims,
|
||||
columnMenuRef,
|
||||
} = useFiretableContext();
|
||||
if (!setSelectedColumnHeader || !tableState || !tableActions) return null;
|
||||
if (!columnMenuRef || !tableState || !tableActions) return null;
|
||||
const { orderBy } = tableState;
|
||||
|
||||
const handleClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => setSelectedColumnHeader({ column, anchorEl: event.currentTarget });
|
||||
) =>
|
||||
columnMenuRef?.current?.setSelectedColumnHeader({
|
||||
column,
|
||||
anchorEl: event.currentTarget,
|
||||
});
|
||||
|
||||
const isSorted = orderBy?.[0]?.key === (column.key as string);
|
||||
const isAsc = isSorted && orderBy?.[0]?.direction === "asc";
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from "react";
|
||||
import { createStyles, makeStyles } from "@material-ui/core/styles";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import AceEditor from "react-ace";
|
||||
|
||||
import "ace-builds/src-noconflict/mode-javascript";
|
||||
import "ace-builds/src-noconflict/theme-monokai";
|
||||
const useStyles = makeStyles(Theme =>
|
||||
createStyles({
|
||||
root: {},
|
||||
})
|
||||
);
|
||||
|
||||
const heightCalc = (code: string, minHeight: number, maxHeight: number) => {
|
||||
const codeHeight = code.split("\n").length * 20;
|
||||
console.log({ codeHeight });
|
||||
if (codeHeight <= maxHeight && codeHeight >= minHeight) return codeHeight;
|
||||
else if (codeHeight > maxHeight) return maxHeight;
|
||||
else if (codeHeight < minHeight) return minHeight;
|
||||
};
|
||||
export default function CodeEditor(props: any) {
|
||||
const { handleChange, script } = props;
|
||||
console.log({ script });
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Grid container direction="column" className={classes.root}>
|
||||
<AceEditor
|
||||
key={`column-code-editor`}
|
||||
placeholder="insert code"
|
||||
mode="javascript"
|
||||
theme="monokai"
|
||||
name={"code-editor"}
|
||||
//onLoad={this.onLoad}
|
||||
onChange={handleChange}
|
||||
fontSize={14}
|
||||
showPrintMargin={true}
|
||||
height={`${heightCalc(script ?? "", 100, 600)}px`}
|
||||
//showGutter={true}
|
||||
highlightActiveLine={true}
|
||||
value={script}
|
||||
setOptions={{
|
||||
enableBasicAutocompletion: false,
|
||||
enableLiveAutocompletion: false,
|
||||
enableSnippets: false,
|
||||
showLineNumbers: true,
|
||||
tabSize: 2,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { FieldType } from "constants/fields";
|
||||
import MultiSelect from "@antlerengineering/multiselect";
|
||||
import { db } from "../../../../firebase";
|
||||
const ColumnSelector = ({
|
||||
tableColumns,
|
||||
handleChange,
|
||||
validTypes,
|
||||
table,
|
||||
value,
|
||||
label,
|
||||
}: {
|
||||
tableColumns?: any[];
|
||||
handleChange: any;
|
||||
validTypes?: FieldType[];
|
||||
table?: string;
|
||||
value: any;
|
||||
label?: string;
|
||||
}) => {
|
||||
const [columns, setColumns] = useState(tableColumns ?? []);
|
||||
const getColumns = async table => {
|
||||
const tableConfigDoc = await db
|
||||
.doc(`_FIRETABLE_/settings/schema/${table}`)
|
||||
.get();
|
||||
const tableConfig = tableConfigDoc.data();
|
||||
|
||||
if (tableConfig) setColumns(tableConfig.columns ?? []);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (table) {
|
||||
console.log({ table });
|
||||
getColumns(table);
|
||||
}
|
||||
}, [table]);
|
||||
const options = columns
|
||||
? Object.values(columns)
|
||||
.filter(col => (validTypes ? validTypes.includes(col.type) : true))
|
||||
.map(col => ({ value: col.key, label: col.name }))
|
||||
: [];
|
||||
return (
|
||||
<MultiSelect
|
||||
label={label}
|
||||
onChange={handleChange}
|
||||
value={value ?? []}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColumnSelector;
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { createStyles, makeStyles } from "@material-ui/core/styles";
|
||||
import Chip from "@material-ui/core/Chip";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import _includes from "lodash/includes";
|
||||
import _camelCase from "lodash/camelCase";
|
||||
|
||||
import AddIcon from "@material-ui/icons/AddCircle";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import InputAdornment from "@material-ui/core/InputAdornment";
|
||||
const useStyles = makeStyles(Theme =>
|
||||
createStyles({
|
||||
root: {},
|
||||
field: {
|
||||
width: "100%",
|
||||
},
|
||||
chipsContainer: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
maxWidth: "95%",
|
||||
padding: Theme.spacing(1),
|
||||
},
|
||||
chip: {
|
||||
margin: Theme.spacing(0.5),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function OptionsInput(props: any) {
|
||||
const { options, handleChange } = props;
|
||||
const classes = useStyles();
|
||||
const [newOption, setNewOption] = useState("");
|
||||
const handleAdd = () => {
|
||||
// setOptions([...options, newOption]);
|
||||
if (newOption !== "") {
|
||||
handleChange([...options, newOption]);
|
||||
setNewOption("");
|
||||
}
|
||||
};
|
||||
const handleDelete = (optionToDelete: string) => () =>
|
||||
handleChange(options.filter((option: string) => option !== optionToDelete));
|
||||
|
||||
return (
|
||||
<Grid container direction="column" className={classes.root}>
|
||||
<Grid item>
|
||||
<TextField
|
||||
value={newOption}
|
||||
className={classes.field}
|
||||
label={props.placeholder ?? "New Option"}
|
||||
onChange={e => {
|
||||
setNewOption(e.target.value);
|
||||
}}
|
||||
onKeyPress={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
handleAdd();
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="add new"
|
||||
onClick={(e: any) => {
|
||||
handleAdd();
|
||||
}}
|
||||
>
|
||||
{<AddIcon />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item className={classes.chipsContainer}>
|
||||
{options.map((option: string) => {
|
||||
return (
|
||||
<Chip
|
||||
key={option}
|
||||
label={option}
|
||||
onDelete={handleDelete(option)}
|
||||
className={classes.chip}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
41
www/src/components/Table/ColumnMenu/FieldsDropdown.tsx
Normal file
41
www/src/components/Table/ColumnMenu/FieldsDropdown.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
|
||||
import { TextField, MenuItem, ListItemIcon } from "@material-ui/core";
|
||||
|
||||
import { FIELDS, FieldType } from "constants/fields";
|
||||
|
||||
/**
|
||||
* Returns dropdown component of all available types
|
||||
*/
|
||||
const FieldsDropdown = (props: { value: FieldType | null; onChange: any }) => {
|
||||
const { value, onChange } = props;
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
value={value ? value : ""}
|
||||
onChange={onChange}
|
||||
inputProps={{ name: "type", id: "type" }}
|
||||
label="Field type"
|
||||
>
|
||||
{FIELDS.map(
|
||||
(field: { icon: JSX.Element; name: string; type: FieldType }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={`select-field-${field.name}`}
|
||||
id={`select-field-${field.type}`}
|
||||
value={field.type}
|
||||
>
|
||||
<ListItemIcon style={{ verticalAlign: "text-bottom" }}>
|
||||
{field.icon}
|
||||
</ListItemIcon>
|
||||
{field.name}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TextField>
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldsDropdown;
|
||||
119
www/src/components/Table/ColumnMenu/MenuContents.tsx
Normal file
119
www/src/components/Table/ColumnMenu/MenuContents.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import {
|
||||
createStyles,
|
||||
makeStyles,
|
||||
MenuItem,
|
||||
ListItemIcon,
|
||||
ListSubheader,
|
||||
Divider,
|
||||
} from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles(theme =>
|
||||
createStyles({
|
||||
subheader: {
|
||||
...theme.typography.overline,
|
||||
color: theme.palette.text.disabled,
|
||||
padding: theme.spacing(1, 1.25),
|
||||
paddingTop: theme.spacing(2) + 1,
|
||||
|
||||
cursor: "default",
|
||||
userSelect: "none",
|
||||
|
||||
"&:focus": { outline: 0 },
|
||||
},
|
||||
|
||||
menuItem: {
|
||||
minHeight: 42,
|
||||
padding: theme.spacing(0.75, 1.25),
|
||||
|
||||
...theme.typography.h6,
|
||||
fontSize: "0.875rem",
|
||||
color: theme.palette.text.secondary,
|
||||
transition: theme.transitions.create(["background-color", "color"], {
|
||||
duration: theme.transitions.duration.shortest,
|
||||
}),
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.text.primary,
|
||||
color: "#f1f1f3",
|
||||
},
|
||||
},
|
||||
menuItemIcon: {
|
||||
minWidth: 24,
|
||||
marginRight: theme.spacing(1.25),
|
||||
color: "inherit",
|
||||
},
|
||||
|
||||
menuItemActive: { color: theme.palette.text.primary },
|
||||
menuItemError: {
|
||||
color: theme.palette.error.main,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.error.main,
|
||||
color: theme.palette.error.contrastText,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export interface IMenuContentsProps {
|
||||
menuItems: {
|
||||
type?: string;
|
||||
label?: string;
|
||||
activeLabel?: string;
|
||||
icon?: JSX.Element;
|
||||
activeIcon?: JSX.Element;
|
||||
onClick?: () => void;
|
||||
active?: boolean;
|
||||
color?: "error";
|
||||
}[];
|
||||
}
|
||||
|
||||
export default function MenuContents({ menuItems }: IMenuContentsProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
{menuItems.map((item, index) => {
|
||||
if (item.type === "subheader")
|
||||
return (
|
||||
<>
|
||||
{index !== 0 && <Divider />}
|
||||
<ListSubheader
|
||||
key={index}
|
||||
className={classes.subheader}
|
||||
disableGutters
|
||||
disableSticky
|
||||
>
|
||||
{item.label}
|
||||
</ListSubheader>
|
||||
</>
|
||||
);
|
||||
|
||||
let icon: JSX.Element = item.icon ?? <></>;
|
||||
if (item.active && !!item.activeIcon) icon = item.activeIcon;
|
||||
|
||||
return (
|
||||
<>
|
||||
{index !== 0 && <Divider />}
|
||||
<MenuItem
|
||||
key={index}
|
||||
onClick={item.onClick}
|
||||
className={clsx(
|
||||
classes.menuItem,
|
||||
item.active && classes.menuItemActive,
|
||||
item.color === "error" && classes.menuItemError
|
||||
)}
|
||||
>
|
||||
<ListItemIcon className={classes.menuItemIcon}>
|
||||
{icon}
|
||||
</ListItemIcon>
|
||||
{item.active ? item.activeLabel : item.label}
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
84
www/src/components/Table/ColumnMenu/NameChange.tsx
Normal file
84
www/src/components/Table/ColumnMenu/NameChange.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { useState } from "react";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Dialog from "@material-ui/core/Dialog";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import DialogActions from "@material-ui/core/DialogActions";
|
||||
import DialogContent from "@material-ui/core/DialogContent";
|
||||
import { Typography, IconButton } from "@material-ui/core";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
export default function FormDialog({
|
||||
name,
|
||||
fieldName,
|
||||
open,
|
||||
handleClose,
|
||||
handleSave,
|
||||
}: {
|
||||
name: string;
|
||||
fieldName: string;
|
||||
open: boolean;
|
||||
handleClose: Function;
|
||||
handleSave: Function;
|
||||
}) {
|
||||
const [newName, setName] = useState(name);
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={(e, r) => {
|
||||
handleClose();
|
||||
}}
|
||||
aria-labelledby="form-dialog-title"
|
||||
>
|
||||
<DialogContent>
|
||||
<Grid
|
||||
container
|
||||
justify="space-between"
|
||||
alignContent="flex-start"
|
||||
direction="row"
|
||||
>
|
||||
<Typography variant="h6">Rename Column</Typography>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Typography variant="overline">{name}</Typography>
|
||||
<TextField
|
||||
value={newName}
|
||||
autoFocus
|
||||
variant="filled"
|
||||
id="name"
|
||||
label="Column Header"
|
||||
type="text"
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
setName(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSave(fieldName, { name: newName });
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
www/src/components/Table/ColumnMenu/NewColumn.tsx
Normal file
103
www/src/components/Table/ColumnMenu/NewColumn.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, { useState } from "react";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Dialog from "@material-ui/core/Dialog";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import DialogActions from "@material-ui/core/DialogActions";
|
||||
import DialogContent from "@material-ui/core/DialogContent";
|
||||
import { Typography, IconButton } from "@material-ui/core";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
import { FieldType } from "constants/fields";
|
||||
import FieldsDropdown from "./FieldsDropdown";
|
||||
import _camel from "lodash/camelCase";
|
||||
|
||||
export default function FormDialog({
|
||||
open,
|
||||
data,
|
||||
handleClose,
|
||||
handleSave,
|
||||
}: {
|
||||
open: boolean;
|
||||
data: any;
|
||||
handleClose: Function;
|
||||
handleSave: Function;
|
||||
}) {
|
||||
const [type, setType] = useState(FieldType.shortText);
|
||||
const [name, setName] = useState("");
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={(e, r) => {
|
||||
handleClose();
|
||||
}}
|
||||
aria-labelledby="new-column"
|
||||
>
|
||||
<DialogContent>
|
||||
<Grid
|
||||
container
|
||||
justify="space-between"
|
||||
alignContent="flex-start"
|
||||
direction="row"
|
||||
>
|
||||
<Typography variant="h6">Add new column</Typography>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Typography variant="overline">Column Name</Typography>
|
||||
<TextField
|
||||
value={name}
|
||||
autoFocus
|
||||
variant="filled"
|
||||
id="name"
|
||||
label="Column Header"
|
||||
type="text"
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
setName(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Typography variant="overline">Column Type</Typography>
|
||||
|
||||
<FieldsDropdown
|
||||
value={type}
|
||||
onChange={(newType: any) => {
|
||||
setType(newType.target.value);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const fieldName = _camel(name);
|
||||
handleSave(fieldName, {
|
||||
type,
|
||||
name,
|
||||
fieldName,
|
||||
key: fieldName,
|
||||
config: {},
|
||||
...data.initializeColumn,
|
||||
});
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
208
www/src/components/Table/ColumnMenu/Settings.tsx
Normal file
208
www/src/components/Table/ColumnMenu/Settings.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import React, { useState } from "react";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import Dialog from "@material-ui/core/Dialog";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import DialogActions from "@material-ui/core/DialogActions";
|
||||
import DialogContent from "@material-ui/core/DialogContent";
|
||||
import { Typography, IconButton, TextField, Switch } from "@material-ui/core";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
import { FieldType } from "constants/fields";
|
||||
import OptionsInput from "./ConfigFields/OptionsInput";
|
||||
import { useFiretableContext } from "contexts/firetableContext";
|
||||
import MultiSelect from "@antlerengineering/multiselect";
|
||||
|
||||
import _sortBy from "lodash/sortBy";
|
||||
import CodeEditor from "./ConfigFields/CodeEditor";
|
||||
import FieldsDropdown from "./FieldsDropdown";
|
||||
import ColumnSelector from "./ConfigFields/ColumnSelector";
|
||||
|
||||
const ConfigForm = ({ type, config, handleChange }) => {
|
||||
const { tableState, tables } = useFiretableContext();
|
||||
|
||||
if (!tableState) return <></>;
|
||||
const { columns } = tableState;
|
||||
switch (type) {
|
||||
case FieldType.singleSelect:
|
||||
case FieldType.multiSelect:
|
||||
return (
|
||||
<>
|
||||
<OptionsInput
|
||||
options={config.options ?? []}
|
||||
handleChange={handleChange("options")}
|
||||
/>
|
||||
<Typography variant="overline">ADD NEW?</Typography>
|
||||
<Grid container direction="row" justify="space-between">
|
||||
<Typography variant="subtitle1">
|
||||
User can add new options.
|
||||
</Typography>
|
||||
<Switch
|
||||
checked={config.freeText}
|
||||
onClick={() => {
|
||||
handleChange("freeText")(!config.freeText);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
case FieldType.connectTable:
|
||||
const tableOptions = _sortBy(
|
||||
tables?.map(t => ({
|
||||
label: `${t.section} - ${t.name}`,
|
||||
value: t.collection,
|
||||
})) ?? [],
|
||||
"label"
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MultiSelect
|
||||
options={tableOptions}
|
||||
freeText={false}
|
||||
value={config.index}
|
||||
onChange={handleChange("index")}
|
||||
multiple={false}
|
||||
/>
|
||||
<ColumnSelector
|
||||
label={"Primary Keys"}
|
||||
value={config.primaryKeys}
|
||||
table={config.index}
|
||||
handleChange={handleChange("primaryKeys")}
|
||||
validTypes={[FieldType.shortText, FieldType.singleSelect]}
|
||||
/>
|
||||
<TextField
|
||||
label="filter template"
|
||||
name="filters"
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
handleChange("filters")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case FieldType.subTable:
|
||||
return (
|
||||
<ColumnSelector
|
||||
label={"Parent Label"}
|
||||
value={config.parentLabel}
|
||||
tableColumns={
|
||||
columns
|
||||
? Array.isArray(columns)
|
||||
? columns
|
||||
: Object.values(columns)
|
||||
: []
|
||||
}
|
||||
handleChange={handleChange("parentLabel")}
|
||||
validTypes={[FieldType.shortText, FieldType.singleSelect]}
|
||||
/>
|
||||
);
|
||||
case FieldType.derivative:
|
||||
return (
|
||||
<>
|
||||
<ColumnSelector
|
||||
label={"Listener fields"}
|
||||
value={config.listenerFields}
|
||||
tableColumns={
|
||||
columns
|
||||
? Array.isArray(columns)
|
||||
? columns
|
||||
: Object.values(columns)
|
||||
: []
|
||||
}
|
||||
handleChange={handleChange("listenerFields")}
|
||||
/>
|
||||
<CodeEditor
|
||||
script={config.script}
|
||||
handleChange={handleChange("script")}
|
||||
/>
|
||||
<FieldsDropdown
|
||||
value={config.renderFieldType}
|
||||
onChange={(newType: any) => {
|
||||
handleChange("renderFieldType")(newType.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
export default function FormDialog({
|
||||
name,
|
||||
fieldName,
|
||||
type,
|
||||
open,
|
||||
config,
|
||||
handleClose,
|
||||
handleSave,
|
||||
}: {
|
||||
name: string;
|
||||
fieldName: string;
|
||||
type: FieldType;
|
||||
open: boolean;
|
||||
config: any;
|
||||
handleClose: Function;
|
||||
handleSave: Function;
|
||||
}) {
|
||||
const [newConfig, setNewConfig] = useState(config ?? {});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={(e, r) => {
|
||||
handleClose();
|
||||
}}
|
||||
aria-labelledby="form-column-settings"
|
||||
>
|
||||
<DialogContent>
|
||||
<Grid
|
||||
container
|
||||
justify="space-between"
|
||||
alignContent="flex-start"
|
||||
direction="row"
|
||||
>
|
||||
<Typography variant="h6">{name}: Settings</Typography>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Typography variant="overline"></Typography>
|
||||
|
||||
{
|
||||
<ConfigForm
|
||||
type={type}
|
||||
handleChange={key => update => {
|
||||
console.log(key, update);
|
||||
setNewConfig({ ...newConfig, [key]: update });
|
||||
}}
|
||||
config={newConfig}
|
||||
/>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSave(fieldName, { config: newConfig });
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
www/src/components/Table/ColumnMenu/TypeChange.tsx
Normal file
83
www/src/components/Table/ColumnMenu/TypeChange.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React, { useState } from "react";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import Dialog from "@material-ui/core/Dialog";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import DialogActions from "@material-ui/core/DialogActions";
|
||||
import DialogContent from "@material-ui/core/DialogContent";
|
||||
import { Typography, IconButton } from "@material-ui/core";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
import { FieldType } from "constants/fields";
|
||||
import FieldsDropdown from "./FieldsDropdown";
|
||||
|
||||
export default function FormDialog({
|
||||
name,
|
||||
fieldName,
|
||||
type,
|
||||
open,
|
||||
handleClose,
|
||||
handleSave,
|
||||
}: {
|
||||
name: string;
|
||||
fieldName: string;
|
||||
type: FieldType;
|
||||
open: boolean;
|
||||
handleClose: Function;
|
||||
handleSave: Function;
|
||||
}) {
|
||||
const [newType, setType] = useState(type);
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={(e, r) => {
|
||||
handleClose();
|
||||
}}
|
||||
aria-labelledby="form-dialog-title"
|
||||
>
|
||||
<DialogContent>
|
||||
<Grid
|
||||
container
|
||||
justify="space-between"
|
||||
alignContent="flex-start"
|
||||
direction="row"
|
||||
>
|
||||
<Typography variant="h6">Change Column Type</Typography>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Typography variant="overline">Current Column: {name}</Typography>
|
||||
|
||||
<FieldsDropdown
|
||||
value={newType}
|
||||
onChange={(newType: any) => {
|
||||
setType(newType.target.value);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSave(fieldName, { type: newType });
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
311
www/src/components/Table/ColumnMenu/index.tsx
Normal file
311
www/src/components/Table/ColumnMenu/index.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { createStyles, makeStyles, Menu } from "@material-ui/core";
|
||||
import LockOpenIcon from "@material-ui/icons/LockOpen";
|
||||
import LockIcon from "@material-ui/icons/Lock";
|
||||
import VisibilityOffIcon from "@material-ui/icons/VisibilityOff";
|
||||
import VisibilityIcon from "@material-ui/icons/Visibility";
|
||||
import FreezeIcon from "assets/icons/Freeze";
|
||||
import UnfreezeIcon from "assets/icons/Unfreeze";
|
||||
import CellResizeIcon from "assets/icons/CellResize";
|
||||
import ArrowDownwardIcon from "@material-ui/icons/ArrowDownward";
|
||||
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import ReorderIcon from "@material-ui/icons/Reorder";
|
||||
import SettingsIcon from "@material-ui/icons/Settings";
|
||||
import ColumnPlusBeforeIcon from "assets/icons/ColumnPlusBefore";
|
||||
import ColumnPlusAfterIcon from "assets/icons/ColumnPlusAfter";
|
||||
import ColumnRemoveIcon from "assets/icons/ColumnRemove";
|
||||
|
||||
import MenuContents from "./MenuContents";
|
||||
import NameChange from "./NameChange";
|
||||
import NewColumn from "./NewColumn";
|
||||
import TypeChange from "./TypeChange";
|
||||
import Settings from "./Settings";
|
||||
|
||||
import { useFiretableContext } from "contexts/firetableContext";
|
||||
import { FIELDS, FieldType } from "constants/fields";
|
||||
import _find from "lodash/find";
|
||||
import { Column } from "react-data-grid";
|
||||
import { PopoverProps } from "@material-ui/core";
|
||||
|
||||
const INITIAL_MODAL = { type: "", data: {} };
|
||||
|
||||
enum ModalStates {
|
||||
nameChange = "NAME_CHANGE",
|
||||
typeChange = "TYPE_CHANGE",
|
||||
new = "NEW_COLUMN",
|
||||
settings = "COLUMN_SETTINGS",
|
||||
}
|
||||
|
||||
type SelectedColumnHeader = {
|
||||
column: Column<any> & { [key: string]: any };
|
||||
anchorEl: PopoverProps["anchorEl"];
|
||||
};
|
||||
export type ColumnMenuRef = {
|
||||
selectedColumnHeader: SelectedColumnHeader | null;
|
||||
setSelectedColumnHeader: React.Dispatch<
|
||||
React.SetStateAction<SelectedColumnHeader | null>
|
||||
>;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(theme =>
|
||||
createStyles({
|
||||
paper: {
|
||||
// TODO: change this if we need to support a dark mode
|
||||
backgroundColor: "#f1f1f3",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function ColumnMenu() {
|
||||
const classes = useStyles();
|
||||
const [modal, setModal] = useState(INITIAL_MODAL);
|
||||
const { tableState, tableActions, columnMenuRef } = useFiretableContext();
|
||||
|
||||
const [selectedColumnHeader, setSelectedColumnHeader] = useState<any>(null);
|
||||
if (columnMenuRef)
|
||||
columnMenuRef.current = {
|
||||
selectedColumnHeader,
|
||||
setSelectedColumnHeader,
|
||||
} as any;
|
||||
|
||||
const { column, anchorEl } = (selectedColumnHeader ?? {}) as any;
|
||||
|
||||
useEffect(() => {
|
||||
if (column && column.type === FieldType.last) {
|
||||
setModal({
|
||||
type: ModalStates.new,
|
||||
data: {
|
||||
initializeColumn: { index: column.index ? column.index + 1 : 0 },
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [column]);
|
||||
if (!tableState || !tableActions) return null;
|
||||
const { orderBy } = tableState;
|
||||
|
||||
const actions = tableActions!.column;
|
||||
|
||||
const handleClose = () => {
|
||||
if (!setSelectedColumnHeader) return;
|
||||
setSelectedColumnHeader({
|
||||
column: column!,
|
||||
anchorEl: null,
|
||||
});
|
||||
setTimeout(() => setSelectedColumnHeader(null), 300);
|
||||
};
|
||||
|
||||
if (!column) return null;
|
||||
const isSorted = orderBy?.[0]?.key === (column.key as string);
|
||||
const isAsc = isSorted && orderBy?.[0]?.direction === "asc";
|
||||
const menuItems = [
|
||||
{
|
||||
type: "subheader",
|
||||
label: column.name,
|
||||
},
|
||||
{
|
||||
label: "Lock",
|
||||
activeLabel: "Locked (unlock)",
|
||||
icon: <LockOpenIcon />,
|
||||
activeIcon: <LockIcon />,
|
||||
onClick: () => {
|
||||
actions.update(column.key, { editable: !column.editable });
|
||||
handleClose();
|
||||
},
|
||||
active: !column.editable,
|
||||
},
|
||||
{
|
||||
label: "Hide",
|
||||
activeLabel: "Show",
|
||||
icon: <VisibilityOffIcon />,
|
||||
activeIcon: <VisibilityIcon />,
|
||||
onClick: () => {
|
||||
actions.update(column.key, { hidden: !column.hidden });
|
||||
handleClose();
|
||||
},
|
||||
active: column.hidden,
|
||||
},
|
||||
{
|
||||
label: "Freeze",
|
||||
activeLabel: "Unfreeze",
|
||||
icon: <FreezeIcon />,
|
||||
activeIcon: <UnfreezeIcon />,
|
||||
onClick: () => {
|
||||
actions.update(column.key, { fixed: !column.fixed });
|
||||
handleClose();
|
||||
},
|
||||
active: column.fixed,
|
||||
},
|
||||
{
|
||||
label: "Enable resize",
|
||||
activeLabel: "Disable resize",
|
||||
icon: <CellResizeIcon />,
|
||||
onClick: () => {
|
||||
actions.update(column.key, { resizable: !column.resizable });
|
||||
handleClose();
|
||||
},
|
||||
active: column.resizable,
|
||||
},
|
||||
{
|
||||
label: "Sort: Decreasing",
|
||||
activeLabel: "Sorted: Decreasing",
|
||||
icon: <ArrowDownwardIcon />,
|
||||
onClick: () => {
|
||||
tableActions.table.orderBy(
|
||||
isSorted && !isAsc ? [] : [{ key: column.key, direction: "desc" }]
|
||||
);
|
||||
handleClose();
|
||||
},
|
||||
active: isSorted && !isAsc,
|
||||
},
|
||||
{
|
||||
label: "Sort: Increasing",
|
||||
activeLabel: "Sorted: Increasing",
|
||||
icon: <ArrowUpwardIcon />,
|
||||
onClick: () => {
|
||||
tableActions.table.orderBy(
|
||||
isSorted && isAsc ? [] : [{ key: column.key, direction: "asc" }]
|
||||
);
|
||||
handleClose();
|
||||
},
|
||||
active: isSorted && isAsc,
|
||||
},
|
||||
{ type: "subheader", label: "Edit" },
|
||||
{
|
||||
label: "Rename",
|
||||
icon: <EditIcon />,
|
||||
onClick: () => {
|
||||
setModal({ type: ModalStates.nameChange, data: {} });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Edit Type: ${column?.type}`,
|
||||
// TODO: This is based off the cell type
|
||||
icon: _find(FIELDS, { type: column.type })?.icon,
|
||||
onClick: () => {
|
||||
setModal({ type: ModalStates.typeChange, data: { column } });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Column Settings`,
|
||||
// TODO: This is based off the cell type
|
||||
icon: <SettingsIcon />,
|
||||
onClick: () => {
|
||||
setModal({ type: ModalStates.settings, data: { column } });
|
||||
},
|
||||
},
|
||||
// {
|
||||
// label: "Re-order",
|
||||
// icon: <ReorderIcon />,
|
||||
// onClick: () => alert("REORDER"),
|
||||
// },
|
||||
{
|
||||
label: "Add New to Left",
|
||||
icon: <ColumnPlusBeforeIcon />,
|
||||
onClick: () =>
|
||||
setModal({
|
||||
type: ModalStates.new,
|
||||
data: {
|
||||
initializeColumn: { index: column.index ? column.index - 1 : 0 },
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Add New to Right",
|
||||
icon: <ColumnPlusAfterIcon />,
|
||||
onClick: () =>
|
||||
setModal({
|
||||
type: ModalStates.new,
|
||||
data: {
|
||||
initializeColumn: { index: column.index ? column.index + 1 : 0 },
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Delete Column",
|
||||
icon: <ColumnRemoveIcon />,
|
||||
onClick: () => {
|
||||
actions.remove(column.key);
|
||||
handleClose();
|
||||
},
|
||||
color: "error" as "error",
|
||||
},
|
||||
];
|
||||
|
||||
const clearModal = () => {
|
||||
setModal(INITIAL_MODAL);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{column.type !== FieldType.last && (
|
||||
<Menu
|
||||
id="simple-menu"
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
getContentAnchorEl={null}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
classes={{ paper: classes.paper }}
|
||||
MenuListProps={{ disablePadding: true }}
|
||||
>
|
||||
<MenuContents menuItems={menuItems} />
|
||||
</Menu>
|
||||
)}
|
||||
{column && (
|
||||
<>
|
||||
<NameChange
|
||||
name={column.name}
|
||||
fieldName={column.key as string}
|
||||
open={modal.type === ModalStates.nameChange}
|
||||
handleClose={clearModal}
|
||||
handleSave={(key, update) => {
|
||||
actions.update(key, update);
|
||||
clearModal();
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
|
||||
<TypeChange
|
||||
name={column.name}
|
||||
fieldName={column.key as string}
|
||||
open={modal.type === ModalStates.typeChange}
|
||||
type={column.type}
|
||||
handleClose={clearModal}
|
||||
handleSave={(key, update) => {
|
||||
actions.update(key, update);
|
||||
clearModal();
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
<Settings
|
||||
config={column.config}
|
||||
name={column.name}
|
||||
fieldName={column.key as string}
|
||||
open={modal.type === ModalStates.settings}
|
||||
type={column.type}
|
||||
handleClose={clearModal}
|
||||
handleSave={(key, update) => {
|
||||
actions.update(key, update);
|
||||
clearModal();
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
<NewColumn
|
||||
open={modal.type === ModalStates.new}
|
||||
data={modal.data}
|
||||
handleClose={clearModal}
|
||||
handleSave={(key, update) => {
|
||||
actions.update(key, update);
|
||||
clearModal();
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -21,11 +21,7 @@ import { SnackContext } from "contexts/snackContext";
|
||||
import { useFiretableContext } from "contexts/firetableContext";
|
||||
import { db } from "../../firebase";
|
||||
import { FieldType } from "constants/fields";
|
||||
async function asyncForEach(array, callback) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
}
|
||||
|
||||
const ITEM_HEIGHT = 48;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
const MenuProps = {
|
||||
@@ -223,7 +219,10 @@ export default function ExportCSV() {
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
{tableState?.columns &&
|
||||
tableState?.columns.map((column: any) => (
|
||||
(Array.isArray(tableState?.columns)
|
||||
? tableState?.columns
|
||||
: Object.values(tableState?.columns)
|
||||
).map((column: any) => (
|
||||
<MenuItem key={column.key} value={column}>
|
||||
{column.name}
|
||||
</MenuItem>
|
||||
|
||||
@@ -210,8 +210,9 @@ const Filters = ({ columns, setFilters }: any) => {
|
||||
return (
|
||||
<MultiSelect
|
||||
multiple
|
||||
freeText={false}
|
||||
onChange={value => setQuery(query => ({ ...query, value }))}
|
||||
options={selectedColumn.options}
|
||||
options={selectedColumn.config.options ?? []}
|
||||
label=""
|
||||
value={Array.isArray(query?.value) ? query.value : []}
|
||||
TextFieldProps={{ hiddenLabel: true }}
|
||||
@@ -221,10 +222,11 @@ const Filters = ({ columns, setFilters }: any) => {
|
||||
return (
|
||||
<MultiSelect
|
||||
multiple={false}
|
||||
freeText={false}
|
||||
onChange={value => {
|
||||
if (value !== null) setQuery(query => ({ ...query, value }));
|
||||
}}
|
||||
options={selectedColumn.options}
|
||||
options={selectedColumn.config.options ?? []}
|
||||
label=""
|
||||
value={typeof query?.value === "string" ? query.value : null}
|
||||
TextFieldProps={{ hiddenLabel: true }}
|
||||
@@ -237,7 +239,7 @@ const Filters = ({ columns, setFilters }: any) => {
|
||||
multiple
|
||||
onChange={value => setQuery(query => ({ ...query, value }))}
|
||||
value={query.value as string[]}
|
||||
options={selectedColumn.options}
|
||||
options={selectedColumn.config.options}
|
||||
label={""}
|
||||
searchable={false}
|
||||
freeText={true}
|
||||
@@ -401,7 +403,7 @@ const Filters = ({ columns, setFilters }: any) => {
|
||||
>
|
||||
{/* <Button color="primary">+ ADD FILTER</Button> */}
|
||||
<Button
|
||||
disabled={query.key == ""}
|
||||
disabled={query.key === ""}
|
||||
onClick={() => {
|
||||
setFilters([]);
|
||||
setQuery({
|
||||
|
||||
@@ -39,12 +39,16 @@ const useStyles = makeStyles(theme =>
|
||||
const FinalColumnHeader: Column<any>["headerRenderer"] = ({ column }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const { setSelectedColumnHeader } = useFiretableContext();
|
||||
if (!setSelectedColumnHeader) return null;
|
||||
const { columnMenuRef } = useFiretableContext();
|
||||
if (!columnMenuRef) return null;
|
||||
|
||||
const handleClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => setSelectedColumnHeader({ column, anchorEl: event.currentTarget });
|
||||
) =>
|
||||
columnMenuRef?.current?.setSelectedColumnHeader({
|
||||
column,
|
||||
anchorEl: event.currentTarget,
|
||||
});
|
||||
|
||||
return (
|
||||
<Grid
|
||||
|
||||
@@ -192,9 +192,11 @@ export default function ImportCSV(props: any) {
|
||||
id: "column-keys",
|
||||
}}
|
||||
>
|
||||
{tableState?.columns.map((column: any) => (
|
||||
<MenuItem value={column.key}>{column.name}</MenuItem>
|
||||
))}
|
||||
{Object.values(tableState?.columns as any).map(
|
||||
(column: any) => (
|
||||
<MenuItem value={column.key}>{column.name}</MenuItem>
|
||||
)
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<IconButton
|
||||
|
||||
101
www/src/components/Table/MigrateButton.tsx
Normal file
101
www/src/components/Table/MigrateButton.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React from "react";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { Grid, Button, Tooltip } from "@material-ui/core";
|
||||
import { useFiretableContext } from "contexts/firetableContext";
|
||||
import { db } from "../../firebase";
|
||||
import AlertIcon from "@material-ui/icons/Warning";
|
||||
const MigrateButton = ({ columns, needsMigration }) => {
|
||||
const { tableState } = useFiretableContext();
|
||||
|
||||
const configDocPath = tableState?.config.tableConfig.path;
|
||||
const handleColumnMigration = async () => {
|
||||
console.log({ columns });
|
||||
|
||||
const newColumns = columns.reduce((acc, currCol, currIndex) => {
|
||||
const baseCol = {
|
||||
...currCol,
|
||||
fieldName: currCol.key,
|
||||
index: currIndex,
|
||||
};
|
||||
if (currCol.type === FieldType.connectTable) {
|
||||
const index = currCol.collectionPath;
|
||||
delete baseCol.collectionPath;
|
||||
return {
|
||||
...acc,
|
||||
[currCol.key]: { ...baseCol, config: { ...currCol.config, index } },
|
||||
};
|
||||
} else if (currCol.type === FieldType.subTable) {
|
||||
const parentLabel = [currCol.parentLabel];
|
||||
delete baseCol.parentLabel;
|
||||
return {
|
||||
...acc,
|
||||
[currCol.key]: {
|
||||
...baseCol,
|
||||
config: { ...currCol.config, parentLabel },
|
||||
},
|
||||
};
|
||||
} else if (currCol.type === FieldType.action) {
|
||||
const callableName = currCol.callableName;
|
||||
delete baseCol.callableName;
|
||||
return {
|
||||
...acc,
|
||||
[currCol.key]: {
|
||||
...baseCol,
|
||||
config: { ...currCol.config, callableName },
|
||||
},
|
||||
};
|
||||
} else if (
|
||||
[FieldType.multiSelect, FieldType.singleSelect].includes(currCol.type)
|
||||
) {
|
||||
const options = currCol.options;
|
||||
delete baseCol.options;
|
||||
return {
|
||||
...acc,
|
||||
[currCol.key]: {
|
||||
...baseCol,
|
||||
config: { ...currCol.config, options },
|
||||
},
|
||||
};
|
||||
} else return { ...acc, [currCol.key]: baseCol };
|
||||
}, {});
|
||||
await db
|
||||
.doc(configDocPath)
|
||||
.update({ columns: newColumns, oldColumns: columns });
|
||||
window.location.reload();
|
||||
};
|
||||
if (needsMigration)
|
||||
return (
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
title={
|
||||
"press this to restructure columns to support new column menus"
|
||||
}
|
||||
>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
onClick={handleColumnMigration}
|
||||
>
|
||||
{" "}
|
||||
<AlertIcon /> Migrate columns <AlertIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
);
|
||||
else return <></>;
|
||||
// else
|
||||
// return (
|
||||
// <Button
|
||||
// onClick={async () => {
|
||||
// const tableConfigDoc = await db.doc(configDocPath).get();
|
||||
// const tableConfig = tableConfigDoc.data();
|
||||
// await db
|
||||
// .doc(configDocPath)
|
||||
// .update({ columns: tableConfig?.oldColumns });
|
||||
// }}
|
||||
// >
|
||||
// Undo
|
||||
// </Button>
|
||||
// );
|
||||
};
|
||||
export default MigrateButton;
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
MenuItem,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip,
|
||||
} from "@material-ui/core";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
|
||||
@@ -19,6 +20,7 @@ import { FireTableFilter } from "hooks/useFiretable";
|
||||
import { DRAWER_COLLAPSED_WIDTH } from "components/SideDrawer";
|
||||
import { useFiretableContext } from "contexts/firetableContext";
|
||||
import { FieldType } from "constants/fields";
|
||||
import MigrateButton from "./MigrateButton";
|
||||
|
||||
export const TABLE_HEADER_HEIGHT = 56;
|
||||
|
||||
@@ -55,11 +57,8 @@ const useStyles = makeStyles(theme =>
|
||||
);
|
||||
|
||||
interface ITableHeaderProps {
|
||||
collection: string;
|
||||
rowHeight: number;
|
||||
updateConfig: Function;
|
||||
|
||||
columns: any;
|
||||
filters: FireTableFilter[];
|
||||
}
|
||||
|
||||
@@ -67,14 +66,18 @@ interface ITableHeaderProps {
|
||||
* TODO: Make this properly mobile responsive, not just horizontally scrolling
|
||||
*/
|
||||
export default function TableHeader({
|
||||
collection,
|
||||
rowHeight,
|
||||
updateConfig,
|
||||
columns,
|
||||
|
||||
filters,
|
||||
}: ITableHeaderProps) {
|
||||
const classes = useStyles();
|
||||
const { tableActions, userClaims } = useFiretableContext();
|
||||
const { tableActions, tableState } = useFiretableContext();
|
||||
if (!tableState || !tableState.columns) return <></>;
|
||||
const { columns } = tableState;
|
||||
|
||||
const needsMigration = Array.isArray(columns) && columns.length !== 0;
|
||||
const tempColumns = needsMigration ? columns : Object.values(columns);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
@@ -84,10 +87,11 @@ export default function TableHeader({
|
||||
wrap="nowrap"
|
||||
className={classes.root}
|
||||
>
|
||||
<MigrateButton needsMigration={needsMigration} columns={tempColumns} />
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const initialVal = columns.reduce((acc, currCol) => {
|
||||
const initialVal = tempColumns.reduce((acc, currCol) => {
|
||||
if (currCol.type === FieldType.checkbox) {
|
||||
return { ...acc, [currCol.key]: false };
|
||||
} else {
|
||||
@@ -108,7 +112,7 @@ export default function TableHeader({
|
||||
|
||||
<Grid item>
|
||||
<Filters
|
||||
columns={columns}
|
||||
columns={tempColumns}
|
||||
tableFilters={filters}
|
||||
setFilters={tableActions?.table.filter}
|
||||
/>
|
||||
@@ -160,11 +164,10 @@ export default function TableHeader({
|
||||
<ImportCSV />
|
||||
</Grid>
|
||||
{/* )} */}
|
||||
{userClaims && !userClaims.roles?.includes("READONLY") && (
|
||||
<Grid item>
|
||||
<ExportCSV />
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item>
|
||||
<ExportCSV />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,9 +12,12 @@ import TextEditor from "./TextEditor";
|
||||
* @param column Must have column `type`
|
||||
*/
|
||||
export const getEditor = (column: any) => {
|
||||
const { type } = column;
|
||||
|
||||
switch (type) {
|
||||
const { type, config } = column;
|
||||
let _type = type;
|
||||
if (config && config.renderFieldType) {
|
||||
_type = config.renderFieldType;
|
||||
}
|
||||
switch (_type) {
|
||||
// Can be edited without double-clicking
|
||||
case FieldType.date:
|
||||
case FieldType.dateTime:
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function Action({
|
||||
const classes = useStyles();
|
||||
|
||||
const { createdAt, updatedAt, id, ref, ...docData } = row;
|
||||
const { callableName } = column as any;
|
||||
const { callableName, config } = column as any;
|
||||
const action = !value
|
||||
? "run"
|
||||
: value.undo
|
||||
@@ -60,9 +60,8 @@ export default function Action({
|
||||
column,
|
||||
action,
|
||||
};
|
||||
console.log(data);
|
||||
cloudFunction(
|
||||
callableName,
|
||||
callableName ?? config.callableName,
|
||||
data,
|
||||
response => {
|
||||
const { message, cellValue, success } = response.data;
|
||||
@@ -71,7 +70,7 @@ export default function Action({
|
||||
if (cellValue) onSubmit(cellValue);
|
||||
},
|
||||
error => {
|
||||
console.error("ERROR", callableName, error);
|
||||
console.error("ERROR", callableName ?? config.callableName, error);
|
||||
setIsRunning(false);
|
||||
snack.open({ message: JSON.stringify(error), severity: "error" });
|
||||
}
|
||||
@@ -127,13 +126,13 @@ export default function Action({
|
||||
>
|
||||
<Grid item xs className={classes.labelContainer}>
|
||||
{hasRan && isUrl(value.status) ? (
|
||||
<a href={value.status} target="_blank" rel="noopener noreferer">
|
||||
<a href={value.status} target="_blank" rel="noopener noreferrer">
|
||||
{value.status}
|
||||
</a>
|
||||
) : hasRan ? (
|
||||
value.status
|
||||
) : (
|
||||
sanitiseCallableName(callableName)
|
||||
sanitiseCallableName(callableName ?? config.callableName)
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
} from "@material-ui/core";
|
||||
import { green } from "@material-ui/core/colors";
|
||||
|
||||
import Confirmation from "components/Confirmation";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
const useStyles = makeStyles(theme =>
|
||||
createStyles({
|
||||
root: { paddingLeft: theme.spacing(1.5) },
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ export default function ConnectTable({
|
||||
|
||||
const { collectionPath, config } = column as any;
|
||||
const { dataGridRef } = useFiretableContext();
|
||||
|
||||
const disabled = !column.editable || config.isLocked;
|
||||
if (!config || !config.primaryKeys) return <></>;
|
||||
const disabled = !column.editable || config?.isLocked;
|
||||
|
||||
// Render chips
|
||||
const renderValue = value => (
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function MultiSelect({
|
||||
}: CustomCellProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const { options } = column as any;
|
||||
const { config } = column as any;
|
||||
const { dataGridRef } = useFiretableContext();
|
||||
|
||||
// Support SingleSelect field
|
||||
@@ -99,11 +99,11 @@ export default function MultiSelect({
|
||||
value === undefined || value === null ? (isSingle ? null : []) : value
|
||||
}
|
||||
onChange={onSubmit}
|
||||
freeText={false}
|
||||
freeText={!isSingle && config.freeText}
|
||||
multiple={!isSingle as any}
|
||||
label={column.name}
|
||||
labelPlural={column.name}
|
||||
options={options}
|
||||
options={config.options ?? []}
|
||||
disabled={column.editable === false}
|
||||
onOpen={handleOpen}
|
||||
TextFieldProps={
|
||||
|
||||
@@ -19,7 +19,15 @@ const useStyles = makeStyles(theme =>
|
||||
export default function SubTable({ column, row }: CustomCellProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const { parentLabel } = column as any;
|
||||
const { parentLabel, config } = column as any;
|
||||
const label = parentLabel
|
||||
? row[parentLabel]
|
||||
: config.parentLabel
|
||||
? config.parentLabel.reduce((acc, curr) => {
|
||||
if (acc !== "") return `${acc} - ${row[curr]}`;
|
||||
else return row[curr];
|
||||
}, "")
|
||||
: "";
|
||||
const fieldName = column.key as string;
|
||||
|
||||
const router = useRouter();
|
||||
@@ -31,11 +39,11 @@ export default function SubTable({ column, row }: CustomCellProps) {
|
||||
if (parentLabels)
|
||||
subTablePath =
|
||||
encodeURIComponent(`${row.ref.path}/${fieldName}`) +
|
||||
`?parentLabel=${parentLabels},${row[parentLabel]}`;
|
||||
`?parentLabel=${parentLabels},${label}`;
|
||||
else
|
||||
subTablePath =
|
||||
encodeURIComponent(`${row.ref.path}/${fieldName}`) +
|
||||
`?parentLabel=${encodeURIComponent(row[parentLabel])}`;
|
||||
`?parentLabel=${encodeURIComponent(label)}`;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
@@ -46,7 +54,7 @@ export default function SubTable({ column, row }: CustomCellProps) {
|
||||
className={clsx("cell-collapse-padding", classes.root)}
|
||||
>
|
||||
<Grid item xs className={classes.labelContainer}>
|
||||
{column.name}: {row[parentLabel]}
|
||||
{column.name}: {label}
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
|
||||
@@ -47,7 +47,11 @@ const Percentage = lazy(() =>
|
||||
* @param column Must have column `type`
|
||||
*/
|
||||
export const getFormatter = (column: any) => {
|
||||
switch (column.type) {
|
||||
let _type = column.type;
|
||||
if (column.config.renderFieldType) {
|
||||
_type = column.config.renderFieldType;
|
||||
}
|
||||
switch (_type) {
|
||||
case FieldType.date:
|
||||
case FieldType.dateTime:
|
||||
return withCustomCell(DatePicker);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, { lazy, Suspense, useEffect, useRef } from "react";
|
||||
|
||||
import _orderBy from "lodash/orderBy";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import _isEmpty from "lodash/isEmpty";
|
||||
|
||||
@@ -16,6 +18,7 @@ import Loading from "components/Loading";
|
||||
import SubTableBreadcrumbs, { BREADCRUMBS_HEIGHT } from "./SubTableBreadcrumbs";
|
||||
import TableHeader, { TABLE_HEADER_HEIGHT } from "./TableHeader";
|
||||
import ColumnHeader from "./ColumnHeader";
|
||||
import ColumnMenu from "./ColumnMenu";
|
||||
import FinalColumnHeader from "./FinalColumnHeader";
|
||||
import FinalColumn, { useFinalColumnStyles } from "./formatters/FinalColumn";
|
||||
|
||||
@@ -27,14 +30,11 @@ import { getFormatter } from "./formatters";
|
||||
import { getEditor } from "./editors";
|
||||
|
||||
import useWindowSize from "hooks/useWindowSize";
|
||||
import { DRAWER_WIDTH, DRAWER_COLLAPSED_WIDTH } from "components/SideDrawer";
|
||||
import { DRAWER_COLLAPSED_WIDTH } from "components/SideDrawer";
|
||||
import { APP_BAR_HEIGHT } from "components/Navigation";
|
||||
import useStyles from "./styles";
|
||||
|
||||
// const Hotkeys = lazy(() => import("./HotKeys" /* webpackChunkName: "HotKeys" */));
|
||||
const ColumnEditor = lazy(() =>
|
||||
import("./ColumnEditor" /* webpackChunkName: "ColumnEditor" */)
|
||||
);
|
||||
const { DraggableContainer } = DraggableHeader;
|
||||
|
||||
export type FiretableColumn = Column<any> & {
|
||||
@@ -99,24 +99,28 @@ export default function Table({ collection, filters }: ITableProps) {
|
||||
|
||||
let columns: FiretableColumn[] = [];
|
||||
if (!tableState.loadingColumns && tableState.columns) {
|
||||
columns = tableState.columns
|
||||
.filter((column: any) => !column.hidden)
|
||||
.map((column: any, index) => ({
|
||||
draggable: true,
|
||||
editable: true,
|
||||
resizable: true,
|
||||
frozen: column.fixed,
|
||||
headerRenderer: ColumnHeader,
|
||||
formatter: getFormatter(column),
|
||||
editor: getEditor(column),
|
||||
...column,
|
||||
width: column.width ? (column.width > 380 ? 380 : column.width) : 150,
|
||||
}));
|
||||
columns = _orderBy(
|
||||
Object.values(tableState.columns).filter(
|
||||
(column: any) => !column.hidden && column.key
|
||||
),
|
||||
"index"
|
||||
).map((column: any, index) => ({
|
||||
draggable: true,
|
||||
editable: true,
|
||||
resizable: true,
|
||||
frozen: column.fixed,
|
||||
headerRenderer: ColumnHeader,
|
||||
formatter: getFormatter(column),
|
||||
editor: getEditor(column),
|
||||
...column,
|
||||
width: column.width ? (column.width > 380 ? 380 : column.width) : 150,
|
||||
}));
|
||||
columns.push({
|
||||
isNew: true,
|
||||
key: "new",
|
||||
name: "Add column",
|
||||
type: FieldType.last,
|
||||
index: columns.length,
|
||||
width: 160,
|
||||
headerRenderer: FinalColumnHeader,
|
||||
cellClass: finalColumnClasses.cell,
|
||||
@@ -126,7 +130,6 @@ export default function Table({ collection, filters }: ITableProps) {
|
||||
}
|
||||
|
||||
const rowHeight = tableState.config.rowHeight;
|
||||
|
||||
const rows = tableState.rows;
|
||||
const rowGetter = (rowIdx: number) => rows[rowIdx];
|
||||
|
||||
@@ -147,10 +150,8 @@ export default function Table({ collection, filters }: ITableProps) {
|
||||
{inSubTable && <SubTableBreadcrumbs collection={collection} />}
|
||||
|
||||
<TableHeader
|
||||
collection={collection}
|
||||
rowHeight={rowHeight}
|
||||
updateConfig={tableActions.table.updateConfig}
|
||||
columns={columns}
|
||||
filters={filters}
|
||||
/>
|
||||
|
||||
@@ -214,9 +215,7 @@ export default function Table({ collection, filters }: ITableProps) {
|
||||
<Loading message="Fetching columns" />
|
||||
)}
|
||||
|
||||
<Suspense fallback={<Loading message="Loading helpers" />}>
|
||||
<ColumnEditor />
|
||||
</Suspense>
|
||||
<ColumnMenu />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
170
www/src/components/TableSettingsDialog.tsx
Normal file
170
www/src/components/TableSettingsDialog.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import _camelCase from "lodash/camelCase";
|
||||
|
||||
import useRouter from "../hooks/useRouter";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
TextField,
|
||||
Button,
|
||||
Select,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { useFiretableContext } from "../contexts/firetableContext";
|
||||
import OptionsInput from "./Table/ColumnMenu/ConfigFields/OptionsInput";
|
||||
export enum TableSettingsDialogModes {
|
||||
create,
|
||||
update,
|
||||
}
|
||||
export interface ICreateTableDialogProps {
|
||||
/** dialog Modes create or udpate table */
|
||||
|
||||
mode: TableSettingsDialogModes | null;
|
||||
clearDialog: () => void;
|
||||
data: {
|
||||
collection: string;
|
||||
roles: string[];
|
||||
section: string;
|
||||
description: string;
|
||||
name: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
const FORM_EMPTY_STATE = {
|
||||
name: "",
|
||||
section: "",
|
||||
collection: "",
|
||||
description: "",
|
||||
roles: ["ADMIN"],
|
||||
};
|
||||
export default function TableSettingsDialog({
|
||||
mode,
|
||||
clearDialog,
|
||||
data,
|
||||
}: ICreateTableDialogProps) {
|
||||
const { settingsActions } = useFiretableContext();
|
||||
const router = useRouter();
|
||||
const open = mode !== null;
|
||||
|
||||
const [formState, setForm] = useState(FORM_EMPTY_STATE);
|
||||
const handleChange = (key: string, value: any) =>
|
||||
setForm({ ...formState, [key]: value });
|
||||
useEffect(() => {
|
||||
if (mode === TableSettingsDialogModes.create)
|
||||
handleChange("collection", _camelCase(formState.name));
|
||||
}, [formState.name]);
|
||||
|
||||
function handleClose() {
|
||||
setForm(FORM_EMPTY_STATE);
|
||||
clearDialog();
|
||||
}
|
||||
function handleCreate() {
|
||||
settingsActions?.createTable(formState);
|
||||
|
||||
if (router.location.pathname === "/") {
|
||||
router.history.push(`table/${formState.collection}`);
|
||||
} else {
|
||||
router.history.push(formState.collection);
|
||||
}
|
||||
handleClose();
|
||||
}
|
||||
async function handleUpdate() {
|
||||
await Promise.all([settingsActions?.updateTable(formState), handleClose()]);
|
||||
window.location.reload();
|
||||
}
|
||||
useEffect(() => {
|
||||
if (data) setForm(data);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="form-dialog-title"
|
||||
>
|
||||
<DialogTitle id="form-dialog-title">
|
||||
{mode === TableSettingsDialogModes.create
|
||||
? "New table"
|
||||
: "Update Table"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{mode === TableSettingsDialogModes.create
|
||||
? "Create a new Table"
|
||||
: `Updating table with collection key:${formState.collection}`}
|
||||
</DialogContentText>
|
||||
|
||||
<TextField
|
||||
autoFocus
|
||||
onChange={e => handleChange("name", e.target.value)}
|
||||
value={formState.name}
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="Table Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
variant="filled"
|
||||
/>
|
||||
{mode === TableSettingsDialogModes.create && (
|
||||
<TextField
|
||||
value={formState.collection}
|
||||
onChange={e => handleChange("collection", e.target.value)}
|
||||
variant="filled"
|
||||
margin="dense"
|
||||
id="collection"
|
||||
label="Collection Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
value={formState.section}
|
||||
onChange={e => handleChange("section", e.target.value)}
|
||||
variant="filled"
|
||||
margin="dense"
|
||||
id="section"
|
||||
label="Section Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Description"
|
||||
id="description"
|
||||
variant="filled"
|
||||
fullWidth
|
||||
multiline={true}
|
||||
value={formState.description}
|
||||
onChange={e => handleChange("description", e.target.value)}
|
||||
/>
|
||||
<OptionsInput
|
||||
placeholder={"Add Role"}
|
||||
options={formState.roles ?? []}
|
||||
handleChange={update => handleChange("roles", update)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
{mode === TableSettingsDialogModes.create && (
|
||||
<Button onClick={handleCreate} color="primary">
|
||||
Create
|
||||
</Button>
|
||||
)}
|
||||
{mode === TableSettingsDialogModes.update && (
|
||||
<Button onClick={handleUpdate} color="primary">
|
||||
Update
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import RichTextIcon from "@material-ui/icons/TextFormat";
|
||||
import ColorIcon from "@material-ui/icons/Colorize";
|
||||
import SliderIcon from "assets/icons/Slider";
|
||||
import CodeIcon from "@material-ui/icons/Code";
|
||||
import DerivativeIcon from "@material-ui/icons/Functions";
|
||||
import UserIcon from "@material-ui/icons/Person";
|
||||
|
||||
export {
|
||||
@@ -94,6 +95,7 @@ export enum FieldType {
|
||||
json = "JSON",
|
||||
|
||||
user = "USER",
|
||||
derivative = "DERIVATIVE",
|
||||
|
||||
last = "LAST",
|
||||
}
|
||||
@@ -147,6 +149,7 @@ export const FIELDS = [
|
||||
{ icon: <JsonIcon />, name: "JSON", type: FieldType.json },
|
||||
{ icon: <UserIcon />, name: "User", type: FieldType.user },
|
||||
{ icon: <CodeIcon />, name: "Code", type: FieldType.code },
|
||||
{ icon: <DerivativeIcon />, name: "Derivative", type: FieldType.derivative },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useState, useContext, useEffect, useRef } from "react";
|
||||
import _groupBy from "lodash/groupBy";
|
||||
|
||||
import { Column, DataGridHandle } from "react-data-grid";
|
||||
import { PopoverProps } from "@material-ui/core";
|
||||
import _sortBy from "lodash/sortBy";
|
||||
import { DataGridHandle } from "react-data-grid";
|
||||
import firebase from "firebase/app";
|
||||
import useFiretable, {
|
||||
FiretableActions,
|
||||
@@ -12,11 +11,7 @@ import useSettings from "hooks/useSettings";
|
||||
import { useAppContext } from "./appContext";
|
||||
import { useSnackContext } from "./snackContext";
|
||||
import { SideDrawerRef } from "components/SideDrawer";
|
||||
|
||||
type SelectedColumnHeader = {
|
||||
column: Column<any> & { [key: string]: any };
|
||||
anchorEl: PopoverProps["anchorEl"];
|
||||
};
|
||||
import { ColumnMenuRef } from "components/Table/ColumnMenu";
|
||||
|
||||
export type Table = {
|
||||
collection: string;
|
||||
@@ -37,19 +32,32 @@ interface FiretableContextProps {
|
||||
fieldName: string,
|
||||
value: any
|
||||
) => void;
|
||||
createTable: Function;
|
||||
userClaims: any;
|
||||
settingsActions: {
|
||||
createTable: (data: {
|
||||
collection: string;
|
||||
name: string;
|
||||
description: string;
|
||||
roles: string[];
|
||||
section: string;
|
||||
}) => void;
|
||||
updateTable: (data: {
|
||||
collection: string;
|
||||
name: string;
|
||||
description: string;
|
||||
roles: string[];
|
||||
section: string;
|
||||
}) => Promise<any>;
|
||||
deleteTable: (collection: string) => void;
|
||||
};
|
||||
|
||||
// TODO: Investigate if this can be moved out of this context
|
||||
selectedColumnHeader: SelectedColumnHeader | null;
|
||||
setSelectedColumnHeader: React.Dispatch<
|
||||
React.SetStateAction<SelectedColumnHeader | null>
|
||||
>;
|
||||
userClaims: any;
|
||||
|
||||
// A ref to the data grid. Contains data grid functions
|
||||
dataGridRef: React.RefObject<DataGridHandle>;
|
||||
// A ref to the side drawer state. Prevents unnecessary re-renders
|
||||
sideDrawerRef: React.MutableRefObject<SideDrawerRef | undefined>;
|
||||
// A ref to the column menu. Prevents unnecessary re-renders
|
||||
columnMenuRef: React.MutableRefObject<ColumnMenuRef | undefined>;
|
||||
}
|
||||
|
||||
const firetableContext = React.createContext<Partial<FiretableContextProps>>(
|
||||
@@ -83,36 +91,35 @@ export const FiretableContextProvider: React.FC = ({ children }) => {
|
||||
const { tableState, tableActions } = useFiretable();
|
||||
const [tables, setTables] = useState<FiretableContextProps["tables"]>();
|
||||
const [sections, setSections] = useState<FiretableContextProps["sections"]>();
|
||||
const [settings, createTable] = useSettings();
|
||||
const [settings, settingsActions] = useSettings();
|
||||
const [userRoles, setUserRoles] = useState<null | string[]>();
|
||||
const [userClaims, setUserClaims] = useState<any>();
|
||||
const [
|
||||
selectedColumnHeader,
|
||||
setSelectedColumnHeader,
|
||||
] = useState<SelectedColumnHeader | null>(null);
|
||||
|
||||
const { currentUser } = useAppContext();
|
||||
useEffect(() => {
|
||||
const { tables } = settings;
|
||||
if (tables && userRoles && !sections) {
|
||||
const filteredTables = tables.filter(
|
||||
table =>
|
||||
!table.roles || table.roles.some(role => userRoles.includes(role))
|
||||
);
|
||||
const filteredTables = _sortBy(tables, "name")
|
||||
.filter(
|
||||
table =>
|
||||
!table.roles || table.roles.some(role => userRoles.includes(role))
|
||||
)
|
||||
.map(table => ({
|
||||
...table,
|
||||
section: table.section ? table.section.toUpperCase().trim() : "OTHER",
|
||||
}));
|
||||
|
||||
const sections = _groupBy(filteredTables, "section");
|
||||
|
||||
setSections(sections);
|
||||
const _sections = _groupBy(filteredTables, "section");
|
||||
setSections(_sections);
|
||||
setTables(filteredTables);
|
||||
}
|
||||
}, [settings, userRoles]);
|
||||
}, [settings, userRoles, sections]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser && !userClaims) {
|
||||
currentUser.getIdTokenResult(true).then(results => {
|
||||
setUserRoles(results.claims.roles || []);
|
||||
setUserClaims(results.claims);
|
||||
// setUserRegions(results.claims.regions || []);
|
||||
});
|
||||
}
|
||||
}, [currentUser]);
|
||||
@@ -145,7 +152,7 @@ export const FiretableContextProvider: React.FC = ({ children }) => {
|
||||
open({
|
||||
message: `You don't have permissions to make this change`,
|
||||
severity: "error",
|
||||
duration: 3000,
|
||||
duration: 2000,
|
||||
position: { horizontal: "center", vertical: "top" },
|
||||
});
|
||||
}
|
||||
@@ -156,6 +163,7 @@ export const FiretableContextProvider: React.FC = ({ children }) => {
|
||||
// A ref to the data grid. Contains data grid functions
|
||||
const dataGridRef = useRef<DataGridHandle>(null);
|
||||
const sideDrawerRef = useRef<SideDrawerRef>();
|
||||
const columnMenuRef = useRef<ColumnMenuRef>();
|
||||
|
||||
return (
|
||||
<firetableContext.Provider
|
||||
@@ -163,14 +171,13 @@ export const FiretableContextProvider: React.FC = ({ children }) => {
|
||||
tableState,
|
||||
tableActions,
|
||||
updateCell,
|
||||
createTable,
|
||||
settingsActions,
|
||||
tables,
|
||||
sections,
|
||||
userClaims,
|
||||
selectedColumnHeader,
|
||||
setSelectedColumnHeader,
|
||||
dataGridRef,
|
||||
sideDrawerRef,
|
||||
columnMenuRef,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -22,8 +22,12 @@ db.enablePersistence({ synchronizeTabs: true });
|
||||
|
||||
export const bucket = firebase.storage();
|
||||
export const functions = firebase.functions();
|
||||
|
||||
export const googleProvider = new firebase.auth.GoogleAuthProvider().setCustomParameters(
|
||||
{
|
||||
prompt: "select_account",
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
export const deleteField = firebase.firestore.FieldValue.delete;
|
||||
|
||||
@@ -13,13 +13,16 @@ const documentIntialState = {
|
||||
ref: null,
|
||||
loading: true,
|
||||
};
|
||||
|
||||
const documentReducer = (prevState: any, newProps: any) => {
|
||||
switch (newProps.action) {
|
||||
case DocActions.clear:
|
||||
return documentIntialState;
|
||||
case DocActions.update:
|
||||
// takes data object form the dispatcher and updates doc
|
||||
|
||||
prevState.ref.set({ ...newProps.data }, { merge: true });
|
||||
|
||||
return { ...prevState, doc: { ...prevState.doc, ...newProps.data } };
|
||||
case DocActions.delete:
|
||||
prevState.ref.delete();
|
||||
|
||||
@@ -23,7 +23,7 @@ export type FiretableActions = {
|
||||
export type FiretableState = {
|
||||
orderBy: FiretableOrderBy;
|
||||
tablePath: string;
|
||||
config: { rowHeight: number };
|
||||
config: { rowHeight: number; tableConfig: any };
|
||||
columns: any[];
|
||||
rows: { [key: string]: any }[];
|
||||
queryLimit: number;
|
||||
@@ -72,7 +72,7 @@ const useFiretable = (
|
||||
tablePath: tableState.path,
|
||||
filters: tableState.filters,
|
||||
columns: tableConfig.columns,
|
||||
config: { rowHeight: tableConfig.rowHeight },
|
||||
config: { rowHeight: tableConfig.rowHeight, tableConfig },
|
||||
rows: tableState.rows,
|
||||
queryLimit: tableState.limit,
|
||||
loadingRows: tableState.loading,
|
||||
|
||||
@@ -106,7 +106,6 @@ const useTable = (initialOverrides: any) => {
|
||||
const ref = doc.ref;
|
||||
return { ...data, id, ref };
|
||||
});
|
||||
console.log(tableState.path, { orderBy, rows });
|
||||
tableDispatch({
|
||||
rows,
|
||||
loading: false,
|
||||
@@ -221,6 +220,7 @@ const useTable = (initialOverrides: any) => {
|
||||
if (tableCollection !== tableState.path) {
|
||||
tableDispatch({
|
||||
path: tableCollection,
|
||||
rows: [],
|
||||
orderBy: [],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,8 +4,12 @@ import useDoc, { DocActions } from "../useDoc";
|
||||
import { FieldType } from "constants/fields";
|
||||
import _camelCase from "lodash/camelCase";
|
||||
import _findIndex from "lodash/findIndex";
|
||||
import _find from "lodash/find";
|
||||
import _sortBy from "lodash/sortBy";
|
||||
import { arrayMover } from "../../util/fns";
|
||||
import { db } from "../../firebase";
|
||||
import { db, deleteField } from "../../firebase";
|
||||
|
||||
//import
|
||||
|
||||
const formatPathRegex = /\/[^\/]+\/([^\/]+)/g;
|
||||
|
||||
@@ -45,13 +49,14 @@ const useTableConfig = (tablePath?: string) => {
|
||||
*/
|
||||
const add = (name: string, type: FieldType, data?: any) => {
|
||||
//TODO: validation
|
||||
|
||||
//console.log("tableConfigState", tableConfigState);
|
||||
const { columns } = tableConfigState;
|
||||
const newIndex = Object.keys(columns).length;
|
||||
let updatedColumns = columns;
|
||||
const key = _camelCase(name);
|
||||
updatedColumns[key] = { name, key, type, ...data, index: newIndex };
|
||||
documentDispatch({
|
||||
action: DocActions.update,
|
||||
data: { columns: [...columns, { name, key, type, ...data }] },
|
||||
data: { columns: updatedColumns },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -61,8 +66,21 @@ const useTableConfig = (tablePath?: string) => {
|
||||
*/
|
||||
const [resize] = useDebouncedCallback((index: number, width: number) => {
|
||||
const { columns } = tableConfigState;
|
||||
columns[index].width = width;
|
||||
documentDispatch({ action: DocActions.update, data: { columns } });
|
||||
const numberOfFixedColumns = Object.values(columns).filter(
|
||||
(col: any) => col.fixed && !col.hidden
|
||||
).length;
|
||||
const columnsArray = _sortBy(
|
||||
Object.values(columns).filter((col: any) => !col.hidden && !col.fixed),
|
||||
"index"
|
||||
);
|
||||
let column: any = columnsArray[index - numberOfFixedColumns];
|
||||
column.width = width;
|
||||
let updatedColumns = columns;
|
||||
updatedColumns[column.key] = column;
|
||||
documentDispatch({
|
||||
action: DocActions.update,
|
||||
data: { columns: updatedColumns },
|
||||
});
|
||||
}, 1000);
|
||||
type updatable = { field: string; value: unknown };
|
||||
|
||||
@@ -70,20 +88,30 @@ const useTableConfig = (tablePath?: string) => {
|
||||
* @param index of column.
|
||||
* @param {updatable[]} updatables properties to be updated
|
||||
*/
|
||||
const updateColumn = (index: number, updatables: updatable[]) => {
|
||||
const updateColumn = (key: string, updates: any) => {
|
||||
const { columns } = tableConfigState;
|
||||
updatables.forEach((updatable: updatable) => {
|
||||
columns[index][updatable.field] = updatable.value;
|
||||
|
||||
const updatedColumns = {
|
||||
...columns,
|
||||
[key]: { ...columns[key], ...updates },
|
||||
};
|
||||
|
||||
documentDispatch({
|
||||
action: DocActions.update,
|
||||
data: { columns: updatedColumns },
|
||||
});
|
||||
documentDispatch({ action: DocActions.update, data: { columns } });
|
||||
};
|
||||
/** remove column by index
|
||||
* @param index of column.
|
||||
*/
|
||||
const remove = (index: number) => {
|
||||
const remove = (key: string) => {
|
||||
const { columns } = tableConfigState;
|
||||
columns.splice(index, 1);
|
||||
documentDispatch({ action: DocActions.update, data: { columns } });
|
||||
let updatedColumns = columns;
|
||||
updatedColumns[key] = deleteField();
|
||||
documentDispatch({
|
||||
action: DocActions.update,
|
||||
data: { columns: updatedColumns },
|
||||
});
|
||||
};
|
||||
/** reorder columns by key
|
||||
* @param draggedColumnKey column being repositioned.
|
||||
@@ -91,13 +119,18 @@ const useTableConfig = (tablePath?: string) => {
|
||||
*/
|
||||
const reorder = (draggedColumnKey: string, droppedColumnKey: string) => {
|
||||
const { columns } = tableConfigState;
|
||||
const draggedColumnIndex = _findIndex(columns, ["key", draggedColumnKey]);
|
||||
const droppedColumnIndex = _findIndex(columns, ["key", droppedColumnKey]);
|
||||
const reorderedColumns = [...columns];
|
||||
arrayMover(reorderedColumns, draggedColumnIndex, droppedColumnIndex);
|
||||
console.log(columns[draggedColumnKey], columns[droppedColumnKey]);
|
||||
const oldIndex = columns[draggedColumnKey].index;
|
||||
const newIndex = columns[droppedColumnKey].index;
|
||||
const columnsArray = _sortBy(Object.values(columns), "index");
|
||||
arrayMover(columnsArray, oldIndex, newIndex);
|
||||
let updatedColumns = columns;
|
||||
columnsArray.forEach((column: any, index) => {
|
||||
updatedColumns[column.key] = { ...column, index };
|
||||
});
|
||||
documentDispatch({
|
||||
action: DocActions.update,
|
||||
data: { columns: reorderedColumns },
|
||||
data: { columns: updatedColumns },
|
||||
});
|
||||
};
|
||||
/** changing table configuration used for things such as row height
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect } from "react";
|
||||
import useDoc, { DocActions } from "./useDoc";
|
||||
import { db } from "../firebase";
|
||||
import _groupBy from "lodash/groupBy";
|
||||
import _find from "lodash/find";
|
||||
|
||||
const useSettings = () => {
|
||||
const [settingsState, documentDispatch] = useDoc({
|
||||
@@ -22,26 +23,65 @@ const useSettings = () => {
|
||||
}
|
||||
}, [settingsState]);
|
||||
|
||||
const createTable = (name: string, collection: string) => {
|
||||
const createTable = (data: {
|
||||
name: string;
|
||||
collection: string;
|
||||
description: string;
|
||||
roles: string[];
|
||||
}) => {
|
||||
const { tables } = settingsState;
|
||||
// updates the setting doc
|
||||
if (tables) {
|
||||
documentDispatch({
|
||||
action: DocActions.update,
|
||||
data: { tables: [...tables, { name, collection }] },
|
||||
});
|
||||
} else {
|
||||
db.doc("_FIRETABLE_/settings").set(
|
||||
{ tables: [{ name, collection }] },
|
||||
{ merge: true }
|
||||
);
|
||||
}
|
||||
|
||||
db.doc("_FIRETABLE_/settings").set(
|
||||
{ tables: tables ? [...tables, data] : [data] },
|
||||
{ merge: true }
|
||||
);
|
||||
|
||||
//create the firetable collection doc with empty columns
|
||||
db.collection("_FIRETABLE_/settings/schema")
|
||||
.doc(collection)
|
||||
.set({ name, columns: [] });
|
||||
.doc(data.collection)
|
||||
.set({ ...data, columns: [] }, { merge: true });
|
||||
};
|
||||
return [settingsState, createTable];
|
||||
|
||||
const updateTable = (data: {
|
||||
name: string;
|
||||
collection: string;
|
||||
description: string;
|
||||
roles: string[];
|
||||
}) => {
|
||||
const { tables } = settingsState;
|
||||
const table = tables.filter(t => t.collection === data.collection)[0];
|
||||
return Promise.all([
|
||||
db.doc("_FIRETABLE_/settings").set(
|
||||
{
|
||||
tables: tables
|
||||
? [
|
||||
...tables.filter(table => table.collection !== data.collection),
|
||||
{ table, ...data },
|
||||
]
|
||||
: [data],
|
||||
},
|
||||
{ merge: true }
|
||||
),
|
||||
//update the firetable collection doc with empty columns
|
||||
db
|
||||
.collection("_FIRETABLE_/settings/schema")
|
||||
.doc(data.collection)
|
||||
.set({ ...data }, { merge: true }),
|
||||
]);
|
||||
};
|
||||
const deleteTable = (collection: string) => {
|
||||
const { tables } = settingsState;
|
||||
|
||||
db.doc("_FIRETABLE_/settings").update({
|
||||
tables: tables.filter(table => table.collection !== collection),
|
||||
});
|
||||
db.collection("_FIRETABLE_/settings/schema")
|
||||
.doc(collection)
|
||||
.delete();
|
||||
};
|
||||
const settingsActions = { createTable, updateTable, deleteTable };
|
||||
return [settingsState, settingsActions];
|
||||
};
|
||||
|
||||
export default useSettings;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import _groupBy from "lodash/groupBy";
|
||||
import _find from "lodash/find";
|
||||
import {
|
||||
@@ -9,8 +9,13 @@ import {
|
||||
Link,
|
||||
Typography,
|
||||
Divider,
|
||||
Fab,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
} from "@material-ui/core";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import Favorite from "@material-ui/icons/Favorite";
|
||||
import FavoriteBorder from "@material-ui/icons/FavoriteBorder";
|
||||
import SecurityIcon from "@material-ui/icons/Security";
|
||||
@@ -18,11 +23,14 @@ import AppBar from "components/AppBar";
|
||||
import Loading from "components/Loading";
|
||||
import EmptyState from "components/EmptyState";
|
||||
import StyledCard from "components/StyledCard";
|
||||
import CreateTableDialog from "components/CreateTableDialog";
|
||||
|
||||
import routes from "constants/routes";
|
||||
import { useFiretableContext } from "contexts/firetableContext";
|
||||
import { useAppContext } from "contexts/appContext";
|
||||
import { DocActions } from "hooks/useDoc";
|
||||
import TableSettingsDialog, {
|
||||
TableSettingsDialogModes,
|
||||
} from "components/TableSettingsDialog";
|
||||
const useStyles = makeStyles(theme =>
|
||||
createStyles({
|
||||
root: { minHeight: "100vh", paddingBottom: theme.spacing(8) },
|
||||
@@ -55,30 +63,50 @@ const useStyles = makeStyles(theme =>
|
||||
[theme.breakpoints.down("md")]: { minHeight: 180 },
|
||||
},
|
||||
|
||||
createTableContainer: {
|
||||
alignSelf: "flex-end",
|
||||
marginLeft: "auto",
|
||||
createTable: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
createTableFab: {
|
||||
fab: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
|
||||
borderRadius: theme.shape.borderRadius * 2,
|
||||
"& svg": { width: "2em", height: "2em" },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const regionalFilter = (regional, userClaims) =>
|
||||
regional && userClaims?.regions && !userClaims?.regions?.includes("GLOBAL")
|
||||
? `?filters=%5B%7B%22key%22%3A%22region%22%2C%22operator%22%3A%22%3D%3D%22%2C%22value%22%3A%22${userClaims?.regions[0]}%22%7D%5D`
|
||||
: "";
|
||||
const TablesView = () => {
|
||||
const [settingsDialogState, setSettingsDialogState] = useState<{
|
||||
mode: null | TableSettingsDialogModes;
|
||||
data: null | {
|
||||
collection: string;
|
||||
description: string;
|
||||
roles: string[];
|
||||
name: string;
|
||||
section: string;
|
||||
};
|
||||
}>({
|
||||
mode: null,
|
||||
data: null,
|
||||
});
|
||||
const clearDialog = () =>
|
||||
setSettingsDialogState({
|
||||
mode: null,
|
||||
data: null,
|
||||
});
|
||||
const classes = useStyles();
|
||||
const { sections, createTable, userClaims } = useFiretableContext();
|
||||
const { sections, userClaims } = useFiretableContext();
|
||||
const { userDoc } = useAppContext();
|
||||
|
||||
const favs = userDoc.state.doc?.favoriteTables
|
||||
? userDoc.state.doc.favoriteTables
|
||||
: [];
|
||||
|
||||
const TableCard = props => {
|
||||
const { table } = props;
|
||||
const TableCard = ({ table }) => {
|
||||
const checked = Boolean(_find(favs, table));
|
||||
return (
|
||||
<Grid key={table.name} item xs={12} sm={6} md={4}>
|
||||
@@ -86,18 +114,7 @@ const TablesView = () => {
|
||||
className={classes.card}
|
||||
overline={table.section}
|
||||
title={table.name}
|
||||
bodyContent={table.description}
|
||||
primaryLink={{
|
||||
to: `${routes.table}/${table.collection}${
|
||||
table.regional &&
|
||||
userClaims?.regions &&
|
||||
!userClaims?.regions.includes("GLOBAL")
|
||||
? `?filters=%5B%7B%22key%22%3A%22region%22%2C%22operator%22%3A%22%3D%3D%22%2C%22value%22%3A%22${userClaims?.regions[0]}%22%7D%5D`
|
||||
: ""
|
||||
}`,
|
||||
label: "Open",
|
||||
}}
|
||||
secondaryAction={
|
||||
headerAction={
|
||||
<Checkbox
|
||||
onClick={e => {
|
||||
userDoc.dispatch({
|
||||
@@ -115,59 +132,61 @@ const TablesView = () => {
|
||||
name="checkedH"
|
||||
/>
|
||||
}
|
||||
bodyContent={table.description}
|
||||
primaryLink={{
|
||||
to: `${routes.table}/${table.collection}${regionalFilter(
|
||||
table?.regional ?? false,
|
||||
userClaims
|
||||
)}`,
|
||||
label: "Open",
|
||||
}}
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
setSettingsDialogState({
|
||||
mode: TableSettingsDialogModes.update,
|
||||
data: table,
|
||||
})
|
||||
}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<main className={classes.root}>
|
||||
<AppBar />
|
||||
<Container>
|
||||
{(!userClaims?.roles || userClaims.roles.length === 0) && (
|
||||
<EmptyState
|
||||
Icon={SecurityIcon}
|
||||
message={"You don't have any permissions specified"}
|
||||
description={
|
||||
<>
|
||||
Please contact the Assistant <em>to</em> the Regional Manager of
|
||||
your branch then{" "}
|
||||
<Link
|
||||
component="button"
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
variant="body2"
|
||||
style={{ verticalAlign: "baseline" }}
|
||||
>
|
||||
refresh this page
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{favs.length !== 0 && (
|
||||
<div key={"favorites"} className={classes.section}>
|
||||
<Typography variant="overline">favorites</Typography>
|
||||
<Divider className={classes.divider} />
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
justify="flex-start"
|
||||
className={classes.cardGrid}
|
||||
>
|
||||
{favs.map(table => (
|
||||
<TableCard table={table} />
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
)}
|
||||
{sections ? (
|
||||
Object.keys(sections).map(sectionName => (
|
||||
<div key={sectionName} className={classes.section}>
|
||||
<Typography variant="overline">
|
||||
{sectionName == "undefined" ? "Other" : sectionName}
|
||||
</Typography>
|
||||
<>
|
||||
<main className={classes.root}>
|
||||
<AppBar />
|
||||
<Container>
|
||||
{(!userClaims?.roles || userClaims.roles.length === 0) && (
|
||||
<EmptyState
|
||||
Icon={SecurityIcon}
|
||||
message={"You don't have any permissions specified"}
|
||||
description={
|
||||
<>
|
||||
Please contact the Assistant <em>to</em> the Regional Manager
|
||||
of your branch then{" "}
|
||||
<Link
|
||||
component="button"
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
variant="body2"
|
||||
style={{ verticalAlign: "baseline" }}
|
||||
>
|
||||
refresh this page
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{favs.length !== 0 && (
|
||||
<div key={"favorites"} className={classes.section}>
|
||||
<Typography variant="overline">favorites</Typography>
|
||||
<Divider className={classes.divider} />
|
||||
<Grid
|
||||
container
|
||||
@@ -175,28 +194,60 @@ const TablesView = () => {
|
||||
justify="flex-start"
|
||||
className={classes.cardGrid}
|
||||
>
|
||||
{sections[sectionName].map(table => (
|
||||
<TableCard table={table} />
|
||||
{favs.map(table => (
|
||||
<TableCard key={table.collection} table={table} />
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
)}
|
||||
{sections ? (
|
||||
Object.keys(sections).map(sectionName => (
|
||||
<div key={sectionName} className={classes.section}>
|
||||
<Typography variant="overline">
|
||||
{sectionName === "undefined" ? "Other" : sectionName}
|
||||
</Typography>
|
||||
<Divider className={classes.divider} />
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
justify="flex-start"
|
||||
className={classes.cardGrid}
|
||||
>
|
||||
{sections[sectionName].map(table => (
|
||||
<TableCard key={table.collection} table={table} />
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
<div className={classes.createTable}>
|
||||
<Tooltip title="Create a table">
|
||||
<Fab
|
||||
className={classes.fab}
|
||||
color="secondary"
|
||||
aria-label="Create table"
|
||||
onClick={() => {
|
||||
setSettingsDialogState({
|
||||
mode: TableSettingsDialogModes.create,
|
||||
data: null,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Container>
|
||||
</main>
|
||||
|
||||
<Grid item className={classes.createTableContainer}>
|
||||
<CreateTableDialog
|
||||
createTable={(tableName: string, collectionName: string) => {
|
||||
if (createTable) {
|
||||
createTable(tableName, collectionName);
|
||||
}
|
||||
}}
|
||||
classes={{ fab: classes.createTableFab }}
|
||||
/>
|
||||
</Grid>
|
||||
</Container>
|
||||
</main>
|
||||
<TableSettingsDialog
|
||||
clearDialog={clearDialog}
|
||||
mode={settingsDialogState.mode}
|
||||
data={settingsDialogState.data}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user