mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
Merge branch 'develop' into FT_function
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -46,5 +46,4 @@ node_modules/
|
||||
cloud_functions/functions/src/functionConfig.json
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
*-firebase.json
|
||||
*-firebase.json
|
||||
|
||||
@@ -19,13 +19,11 @@ const historySnapshot = async (data, sparkContext) => {
|
||||
triggerType === "delete"
|
||||
) {
|
||||
try {
|
||||
await change.before.ref
|
||||
.collection("historySnapshots")
|
||||
.add({
|
||||
...change.before.data(),
|
||||
archivedAt: new Date(),
|
||||
archiveEvent: triggerType,
|
||||
});
|
||||
await change.before.ref.collection("historySnapshots").add({
|
||||
...change.before.data(),
|
||||
archivedAt: new Date(),
|
||||
archiveEvent: triggerType,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
- Sort and filter by row values
|
||||
- Resize and rename columns
|
||||
|
||||
- 27 different column types.
|
||||
[Read more](https://github.com/AntlerVC/firetable/wiki/Column-Types)
|
||||
- 27 different field types.
|
||||
[Read more](https://github.com/AntlerVC/firetable/wiki/Field-Types)
|
||||
|
||||
- Basic types: Short Text, Long Text, Email, Phone, URL…
|
||||
- Custom UI pickers: Date, Checkbox, Single Select, Multi Select…
|
||||
|
||||
@@ -4,19 +4,13 @@ module.exports = {
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
],
|
||||
extends: ["plugin:import/errors", "plugin:import/warnings"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "tsconfig.json",
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: [
|
||||
"@typescript-eslint",
|
||||
"import",
|
||||
],
|
||||
plugins: ["@typescript-eslint", "import"],
|
||||
rules: {
|
||||
"@typescript-eslint/adjacent-overload-signatures": "error",
|
||||
"@typescript-eslint/no-empty-function": "error",
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
{
|
||||
"name": "firetable",
|
||||
"version": "0.1.0",
|
||||
"name": "Firetable",
|
||||
"version": "1.3.0",
|
||||
"homepage": "https://firetable.io/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/AntlerVC/firetable.git"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@antlerengineering/form-builder": "^0.6.2",
|
||||
"@antlerengineering/multiselect": "^0.7.8",
|
||||
"@antlerengineering/form-builder": "^0.9.7",
|
||||
"@antlerengineering/multiselect": "^0.8.2",
|
||||
"@date-io/date-fns": "1.x",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
@@ -13,7 +18,6 @@
|
||||
"@mdi/js": "^5.8.55",
|
||||
"@monaco-editor/react": "^3.5.5",
|
||||
"@tinymce/tinymce-react": "^3.4.0",
|
||||
"ace-builds": "^1.4.11",
|
||||
"algoliasearch": "^4.1.0",
|
||||
"chroma-js": "^2.1.0",
|
||||
"csv-parse": "^4.4.6",
|
||||
@@ -26,9 +30,7 @@
|
||||
"json2csv": "^5.0.1",
|
||||
"lodash": "^4.17.20",
|
||||
"query-string": "^6.8.3",
|
||||
"ramda": "^0.26.1",
|
||||
"react": "^16.9.0",
|
||||
"react-ace": "^9.1.1",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
"react-color": "^2.17.3",
|
||||
"react-data-grid": "^7.0.0-canary.27",
|
||||
@@ -96,6 +98,7 @@
|
||||
"@types/use-persisted-state": "^0.3.0",
|
||||
"firebase-tools": "^8.12.1",
|
||||
"husky": "^4.2.5",
|
||||
"monaco-editor": "^0.21.2",
|
||||
"playwright": "^1.5.2",
|
||||
"prettier": "^2.1.1",
|
||||
"pretty-quick": "^3.0.0"
|
||||
|
||||
@@ -333,6 +333,7 @@ export const defaultOverrides = (theme: Theme): ThemeOptions => ({
|
||||
|
||||
valueLabel: {
|
||||
top: -22,
|
||||
left: "calc(-25%)",
|
||||
...theme.typography.caption,
|
||||
color: theme.palette.primary.main,
|
||||
|
||||
|
||||
106
www/src/components/CodeEditor.tsx
Normal file
106
www/src/components/CodeEditor.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { useRef, useMemo, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import Editor, { monaco } from "@monaco-editor/react";
|
||||
|
||||
import { useTheme, createStyles, makeStyles } from "@material-ui/core/styles";
|
||||
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
editorWrapper: { position: "relative" },
|
||||
resizeIcon: {
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
saveButton: {
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export interface ICodeEditorProps {
|
||||
onChange: (value: string) => void;
|
||||
value: string;
|
||||
height?: number;
|
||||
wrapperProps?: Partial<React.HTMLAttributes<HTMLDivElement>>;
|
||||
disabled?: boolean;
|
||||
editorOptions?: any;
|
||||
}
|
||||
|
||||
export default function CodeEditor({
|
||||
onChange,
|
||||
value,
|
||||
height = 400,
|
||||
wrapperProps,
|
||||
disabled,
|
||||
editorOptions,
|
||||
}: ICodeEditorProps) {
|
||||
const theme = useTheme();
|
||||
const [initialEditorValue] = useState(value ?? "");
|
||||
const { tableState } = useFiretableContext();
|
||||
const classes = useStyles();
|
||||
|
||||
const editorRef = useRef<any>();
|
||||
|
||||
function handleEditorDidMount(_, editor) {
|
||||
editorRef.current = editor;
|
||||
}
|
||||
|
||||
function listenEditorChanges() {
|
||||
setTimeout(() => {
|
||||
editorRef.current?.onDidChangeModelContent((ev) => {
|
||||
onChange(editorRef.current.getValue());
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
useMemo(async () => {
|
||||
monaco
|
||||
.init()
|
||||
.then((monacoInstance) => {
|
||||
monacoInstance.languages.typescript.javascriptDefaults.setDiagnosticsOptions(
|
||||
{
|
||||
noSemanticValidation: true,
|
||||
noSyntaxValidation: false,
|
||||
}
|
||||
);
|
||||
// compiler options
|
||||
monacoInstance.languages.typescript.javascriptDefaults.setCompilerOptions(
|
||||
{
|
||||
target: monacoInstance.languages.typescript.ScriptTarget.ES5,
|
||||
allowNonTsExtensions: true,
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error(
|
||||
"An error occurred during initialization of Monaco: ",
|
||||
error
|
||||
)
|
||||
);
|
||||
listenEditorChanges();
|
||||
}, [tableState?.columns]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...wrapperProps}
|
||||
className={clsx(classes.editorWrapper, wrapperProps?.className)}
|
||||
>
|
||||
<Editor
|
||||
theme={theme.palette.type}
|
||||
height={height}
|
||||
editorDidMount={handleEditorDidMount}
|
||||
language="javascript"
|
||||
value={initialEditorValue}
|
||||
options={{
|
||||
readOnly: disabled,
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
...editorOptions,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import _camelCase from "lodash/camelCase";
|
||||
|
||||
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||
import createStyles from "@material-ui/core/styles/createStyles";
|
||||
import Drawer from "@material-ui/core/Drawer";
|
||||
import List from "@material-ui/core/List";
|
||||
import Divider from "@material-ui/core/Divider";
|
||||
import ListItem from "@material-ui/core/ListItem";
|
||||
import ListItemIcon from "@material-ui/core/ListItemIcon";
|
||||
import ListItemText from "@material-ui/core/ListItemText";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
|
||||
import { FIELDS } from "constants/fields";
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
list: {
|
||||
width: 250,
|
||||
},
|
||||
fields: {
|
||||
paddingLeft: 15,
|
||||
|
||||
paddingRight: 15,
|
||||
},
|
||||
|
||||
fullList: {
|
||||
width: "auto",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// TODO: Create an interface for props
|
||||
export default function ColumnDrawer(props: any) {
|
||||
const { addColumn, columns } = props;
|
||||
const classes = useStyles();
|
||||
const [drawerState, toggleDrawer] = useState(false);
|
||||
const [columnName, setColumnName] = useState("");
|
||||
const [fieldName, setFieldName] = useState("");
|
||||
useEffect(() => {
|
||||
setFieldName(_camelCase(columnName));
|
||||
}, [columnName]);
|
||||
const drawer = () => (
|
||||
<div
|
||||
className={classes.list}
|
||||
role="presentation"
|
||||
onClick={() => {
|
||||
// toggleDrawer(false);
|
||||
}}
|
||||
>
|
||||
<List className={classes.fields}>
|
||||
<TextField
|
||||
autoFocus
|
||||
onChange={(e) => {
|
||||
setColumnName(e.target.value);
|
||||
}}
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="Column Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
value={fieldName}
|
||||
onChange={(e) => {
|
||||
setFieldName(e.target.value);
|
||||
}}
|
||||
margin="dense"
|
||||
id="field"
|
||||
label="Field Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
/>
|
||||
</List>
|
||||
<Divider />
|
||||
<List>
|
||||
{FIELDS.map((field: any) => (
|
||||
<ListItem
|
||||
button
|
||||
onClick={() => {
|
||||
addColumn(columnName, fieldName, field.type);
|
||||
}}
|
||||
key={field.type}
|
||||
>
|
||||
<ListItemIcon>{field.icon}</ListItemIcon>
|
||||
<ListItemText primary={field.name} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton
|
||||
aria-label="add"
|
||||
onClick={() => {
|
||||
toggleDrawer(true);
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
|
||||
<Drawer
|
||||
anchor="right"
|
||||
open={drawerState}
|
||||
onClose={() => {
|
||||
toggleDrawer(false);
|
||||
}}
|
||||
>
|
||||
{drawer()}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import React, { useContext } from "react";
|
||||
import { IDatePicker, DATE_PICKER_EMPTY_STATE } from "./props";
|
||||
const DatePickerContext = React.createContext<IDatePicker>(
|
||||
DATE_PICKER_EMPTY_STATE
|
||||
);
|
||||
export default DatePickerContext;
|
||||
|
||||
export const useDatePicker = () => useContext(DatePickerContext);
|
||||
@@ -1,43 +0,0 @@
|
||||
import React, { useState, lazy, Suspense } from "react";
|
||||
|
||||
import { datePickerProps } from "./props";
|
||||
const Dialog = lazy(
|
||||
() => import("./Dialog" /* webpackChunkName: "DatePickerDialog" */)
|
||||
);
|
||||
import ConfirmationContext from "./Context";
|
||||
interface IConfirmationProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ConfirmationProvider: React.FC<IConfirmationProviderProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [state, setState] = useState<datePickerProps>();
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleClose = () => {
|
||||
setState(undefined);
|
||||
setOpen(false);
|
||||
};
|
||||
const setDate = (props: datePickerProps) => {
|
||||
setState(props);
|
||||
setOpen(true);
|
||||
};
|
||||
return (
|
||||
<ConfirmationContext.Provider
|
||||
value={{
|
||||
props: state,
|
||||
open,
|
||||
handleClose,
|
||||
setDate,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
<Suspense fallback={null}>
|
||||
<Dialog {...state} open={open} handleClose={handleClose} />
|
||||
</Suspense>
|
||||
</ConfirmationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationProvider;
|
||||
@@ -1 +0,0 @@
|
||||
export { useDatePicker } from "./Context";
|
||||
@@ -1,24 +0,0 @@
|
||||
export type datePickerProps =
|
||||
| {
|
||||
title?: string;
|
||||
customBody?: string;
|
||||
body?: string;
|
||||
cancel?: string;
|
||||
confirm?: string | JSX.Element;
|
||||
confirmationCommand?: string;
|
||||
handleConfirm: () => void;
|
||||
open?: Boolean;
|
||||
}
|
||||
| undefined;
|
||||
export interface IDatePicker {
|
||||
props?: datePickerProps;
|
||||
handleClose: () => void;
|
||||
open: boolean;
|
||||
setDate: (props: datePickerProps) => void;
|
||||
}
|
||||
export const DATE_PICKER_EMPTY_STATE = {
|
||||
props: undefined,
|
||||
open: false,
|
||||
handleClose: () => {},
|
||||
setDate: () => {},
|
||||
};
|
||||
@@ -10,9 +10,11 @@ import {
|
||||
Menu,
|
||||
Link as MuiLink,
|
||||
MenuItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
ListItemIcon,
|
||||
// Divider,
|
||||
Divider,
|
||||
} from "@material-ui/core";
|
||||
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
|
||||
import ArrowRightIcon from "@material-ui/icons/ArrowRight";
|
||||
@@ -20,23 +22,26 @@ import CheckIcon from "@material-ui/icons/Check";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import routes from "constants/routes";
|
||||
import meta from "../../../package.json";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
spacer: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
},
|
||||
|
||||
iconButton: {},
|
||||
avatar: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
"$iconButton &": {
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
},
|
||||
|
||||
paper: { minWidth: 160 },
|
||||
displayName: {
|
||||
display: "block",
|
||||
padding: theme.spacing(1, 2),
|
||||
userSelect: "none",
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
|
||||
// divider: { margin: theme.spacing(1, 2) },
|
||||
divider: { margin: theme.spacing(1, 2) },
|
||||
|
||||
secondaryAction: { pointerEvents: "none" },
|
||||
secondaryIcon: {
|
||||
@@ -50,6 +55,13 @@ const useStyles = makeStyles((theme) =>
|
||||
theme.palette.background.paper,
|
||||
marginTop: theme.spacing(-1),
|
||||
},
|
||||
|
||||
version: {
|
||||
display: "block",
|
||||
padding: theme.spacing(1, 2),
|
||||
userSelect: "none",
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -68,10 +80,18 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
setTheme,
|
||||
setThemeOverridden,
|
||||
} = useAppContext();
|
||||
if (!currentUser || !userDoc || !userDoc?.state?.doc) return null;
|
||||
if (!currentUser || !userDoc || !userDoc?.state?.doc)
|
||||
return <div className={classes.spacer} />;
|
||||
|
||||
const displayName = userDoc?.state?.doc?.user?.displayName;
|
||||
const avatarUrl = userDoc?.state?.doc?.user?.photoURL;
|
||||
const email = userDoc?.state?.doc?.user?.email;
|
||||
|
||||
const avatar = avatarUrl ? (
|
||||
<Avatar src={avatarUrl} className={classes.avatar} />
|
||||
) : (
|
||||
<AccountCircleIcon color="secondary" />
|
||||
);
|
||||
|
||||
const changeTheme = (option: "system" | "light" | "dark") => {
|
||||
switch (option) {
|
||||
@@ -103,12 +123,9 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
{...props}
|
||||
ref={anchorEl}
|
||||
onClick={() => setOpen(true)}
|
||||
className={classes.iconButton}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<Avatar src={avatarUrl} className={classes.avatar} />
|
||||
) : (
|
||||
<AccountCircleIcon />
|
||||
)}
|
||||
{avatar}
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
@@ -122,18 +139,17 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
onClose={() => setOpen(false)}
|
||||
classes={{ paper: classes.paper }}
|
||||
>
|
||||
{displayName && (
|
||||
<MuiLink
|
||||
variant="overline"
|
||||
className={classes.displayName}
|
||||
component="a"
|
||||
href={`https://console.firebase.google.com/project/${process.env.REACT_APP_FIREBASE_PROJECT_ID}/firestore/data~2F_FT_USERS~2F${currentUser.uid}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{displayName}
|
||||
</MuiLink>
|
||||
)}
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={`https://console.firebase.google.com/project/${process.env.REACT_APP_FIREBASE_PROJECT_ID}/firestore/data~2F_FT_USERS~2F${currentUser.uid}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<ListItemAvatar>{avatar}</ListItemAvatar>
|
||||
<ListItemText primary={displayName} secondary={email} />
|
||||
</MenuItem>
|
||||
|
||||
<Divider className={classes.divider} />
|
||||
|
||||
<MenuItem onClick={(e) => setThemeSubMenu(e.target)}>
|
||||
Theme
|
||||
@@ -173,8 +189,21 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
)}
|
||||
|
||||
<MenuItem component={Link} to={routes.signOut}>
|
||||
Sign Out
|
||||
Sign out
|
||||
</MenuItem>
|
||||
|
||||
<Divider className={classes.divider} />
|
||||
|
||||
<MuiLink
|
||||
variant="caption"
|
||||
component="a"
|
||||
href={meta.repository.url.replace(".git", "") + "/releases"}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className={classes.version}
|
||||
>
|
||||
{meta.name} v{meta.version}
|
||||
</MuiLink>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
|
||||
27
www/src/components/ProjectSettings/form.tsx
Normal file
27
www/src/components/ProjectSettings/form.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
|
||||
import * as yup from "yup";
|
||||
import { FIELDS } from "@antlerengineering/form-builder";
|
||||
import HelperText from "../HelperText";
|
||||
|
||||
export const settings = () => [
|
||||
{ type: FIELDS.heading, label: "Cloud build configuration" },
|
||||
{
|
||||
type: FIELDS.text,
|
||||
name: "cloudBuild.branch",
|
||||
label: "FT Branch",
|
||||
//validation: yup.string().required("Required"),
|
||||
},
|
||||
{
|
||||
type: FIELDS.description,
|
||||
description: (
|
||||
<HelperText>Firetable branch to build cloud functions from</HelperText>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: FIELDS.text,
|
||||
name: "cloudBuild.triggerId",
|
||||
label: "Trigger Id",
|
||||
//validation: yup.string().required("Required"),
|
||||
},
|
||||
];
|
||||
74
www/src/components/ProjectSettings/index.tsx
Normal file
74
www/src/components/ProjectSettings/index.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import _camelCase from "lodash/camelCase";
|
||||
import _find from "lodash/find";
|
||||
|
||||
import { makeStyles, createStyles } from "@material-ui/core";
|
||||
|
||||
import { FormDialog } from "@antlerengineering/form-builder";
|
||||
import { settings } from "./form";
|
||||
|
||||
import useDoc, { DocActions } from "hooks/useDoc";
|
||||
|
||||
const FORM_EMPTY_STATE = {
|
||||
cloudBuild: {
|
||||
branch: "test",
|
||||
triggerId: "",
|
||||
},
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
buttonGrid: { padding: theme.spacing(3, 0) },
|
||||
button: { width: 160 },
|
||||
|
||||
formFooter: {
|
||||
marginTop: theme.spacing(4),
|
||||
|
||||
"& button": {
|
||||
paddingLeft: theme.spacing(1.5),
|
||||
display: "flex",
|
||||
},
|
||||
},
|
||||
collectionName: { fontFamily: theme.typography.fontFamilyMono },
|
||||
})
|
||||
);
|
||||
|
||||
export default function SettingsDialog({
|
||||
open,
|
||||
handleClose,
|
||||
}: {
|
||||
open: boolean;
|
||||
handleClose: () => void;
|
||||
}) {
|
||||
const [settingsDocState, settingsDocDispatch] = useDoc({
|
||||
path: "_FIRETABLE_/settings",
|
||||
});
|
||||
|
||||
const [formState, setForm] = useState<any>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!settingsDocState.loading) {
|
||||
const cloudBuild = settingsDocState?.doc?.cloudBuild;
|
||||
setForm(cloudBuild ? { cloudBuild } : FORM_EMPTY_STATE);
|
||||
}
|
||||
}, [settingsDocState.doc, open]);
|
||||
|
||||
const handleSubmit = (values) => {
|
||||
settingsDocDispatch({ action: DocActions.update, data: values });
|
||||
handleClose();
|
||||
};
|
||||
|
||||
if (!formState) return <></>;
|
||||
return (
|
||||
<>
|
||||
<FormDialog
|
||||
onClose={handleClose}
|
||||
open={open}
|
||||
title={"Project Settings"}
|
||||
fields={settings()}
|
||||
values={formState}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -47,8 +47,13 @@ const useStyles = makeStyles((theme) =>
|
||||
},
|
||||
},
|
||||
|
||||
"& .tox-sidebar-wrap": {
|
||||
margin: 1,
|
||||
},
|
||||
|
||||
"& .tox-toolbar-overlord, & .tox-edit-area__iframe, & .tox-toolbar__primary": {
|
||||
background: "transparent",
|
||||
borderRadius: theme.shape.borderRadius - 1,
|
||||
},
|
||||
|
||||
"& .tox-toolbar__primary": { padding: theme.spacing(0.5, 0) },
|
||||
@@ -93,12 +98,17 @@ const useStyles = makeStyles((theme) =>
|
||||
})
|
||||
);
|
||||
|
||||
export interface IRichTextProps {
|
||||
export interface IRichTextEditorProps {
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function RichText({ value, onChange }: IRichTextProps) {
|
||||
export default function RichTextEditor({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
}: IRichTextEditorProps) {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
const [focus, setFocus] = useState(false);
|
||||
@@ -106,6 +116,7 @@ export default function RichText({ value, onChange }: IRichTextProps) {
|
||||
return (
|
||||
<div className={clsx(classes.root, focus && classes.focus)}>
|
||||
<Editor
|
||||
disabled={disabled}
|
||||
init={{
|
||||
minHeight: 300,
|
||||
menubar: false,
|
||||
@@ -117,9 +128,10 @@ export default function RichText({ value, onChange }: IRichTextProps) {
|
||||
content_css: [
|
||||
"https://use.typekit.net/ngg8buf.css",
|
||||
"https://fonts.googleapis.com/css?family=Open+Sans:400,400i,700,700i&display=swap",
|
||||
theme.palette.type === "light"
|
||||
? "/static/tinymce_content.css"
|
||||
: "/static/tinymce_content-dark.css",
|
||||
// theme.palette.type === "light"
|
||||
// ?
|
||||
"/static/tinymce_content.css",
|
||||
// : "/static/tinymce_content-dark.css",
|
||||
],
|
||||
}}
|
||||
value={value}
|
||||
@@ -2,73 +2,72 @@ import { useEffect } from "react";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import _isEqual from "lodash/isEqual";
|
||||
import _pick from "lodash/pick";
|
||||
import _omitBy from "lodash/omitBy";
|
||||
import _pickBy from "lodash/pickBy";
|
||||
import _isUndefined from "lodash/isUndefined";
|
||||
import _reduce from "lodash/reduce";
|
||||
|
||||
import { Control, useWatch } from "react-hook-form";
|
||||
import { Control, UseFormMethods, useWatch } from "react-hook-form";
|
||||
import { Values } from "./utils";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { useFiretableContext, firetableUser } from "contexts/FiretableContext";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
import { FiretableState } from "hooks/useFiretable";
|
||||
|
||||
export interface IAutosaveProps {
|
||||
export interface IAutosaveProps
|
||||
extends Pick<UseFormMethods, "reset" | "formState"> {
|
||||
control: Control;
|
||||
defaultValues: Values;
|
||||
docRef: firebase.firestore.DocumentReference;
|
||||
row: any;
|
||||
}
|
||||
|
||||
const getEditables = (values: Values, tableState?: FiretableState) =>
|
||||
_pick(
|
||||
values,
|
||||
(tableState &&
|
||||
(Array.isArray(tableState?.columns)
|
||||
? tableState?.columns
|
||||
: Object.values(tableState?.columns)
|
||||
).map((c) => c.key)) ??
|
||||
[]
|
||||
);
|
||||
|
||||
export default function Autosave({
|
||||
control,
|
||||
defaultValues,
|
||||
docRef,
|
||||
row,
|
||||
reset,
|
||||
formState,
|
||||
}: IAutosaveProps) {
|
||||
const { currentUser } = useAppContext();
|
||||
const { tableState } = useFiretableContext();
|
||||
const { tableState, updateCell } = useFiretableContext();
|
||||
|
||||
const values = useWatch({ control });
|
||||
|
||||
const getEditables = (value) =>
|
||||
_pick(
|
||||
value,
|
||||
(tableState &&
|
||||
(Array.isArray(tableState?.columns)
|
||||
? tableState?.columns
|
||||
: Object.values(tableState?.columns)
|
||||
).map((c) => c.key)) ??
|
||||
[]
|
||||
);
|
||||
|
||||
const [debouncedValue] = useDebounce(getEditables(values), 1000, {
|
||||
const [debouncedValue] = useDebounce(getEditables(values, tableState), 1000, {
|
||||
equalityFn: _isEqual,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!row || !row.ref) return;
|
||||
if (row.ref.id !== docRef.id) return;
|
||||
if (!updateCell) return;
|
||||
|
||||
// Get only fields that have changed and
|
||||
// Remove undefined value to prevent Firestore crash
|
||||
const updatedValues = _omitBy(
|
||||
_omitBy(debouncedValue, _isUndefined),
|
||||
(value, key) => _isEqual(value, row[key])
|
||||
// Get only fields that have had their value updated by the user
|
||||
const updatedValues = _pickBy(
|
||||
_pickBy(debouncedValue, (_, key) => formState.dirtyFields[key]),
|
||||
(value, key) => !_isEqual(value, row[key])
|
||||
);
|
||||
|
||||
if (Object.keys(updatedValues).length === 0) return;
|
||||
|
||||
const _ft_updatedAt = new Date();
|
||||
const _ft_updatedBy = firetableUser(currentUser);
|
||||
row.ref
|
||||
.update({
|
||||
...updatedValues,
|
||||
_ft_updatedAt,
|
||||
updatedAt: _ft_updatedAt,
|
||||
_ft_updatedBy,
|
||||
updatedBy: _ft_updatedBy,
|
||||
})
|
||||
.then(() => console.log("Updated row", row.ref.id, updatedValues));
|
||||
Object.entries(updatedValues).forEach(([key, value]) =>
|
||||
updateCell(
|
||||
row.ref,
|
||||
key,
|
||||
value,
|
||||
// After the cell is updated, set this field to be not dirty
|
||||
// so it doesn’t get updated again when a different field in the form
|
||||
// is updated + make sure the new value is kept after reset
|
||||
() => reset({ ...values, [key]: value })
|
||||
)
|
||||
);
|
||||
}, [debouncedValue]);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -9,21 +9,23 @@ import {
|
||||
} from "@material-ui/core";
|
||||
import DebugIcon from "@material-ui/icons/BugReportOutlined";
|
||||
import LaunchIcon from "@material-ui/icons/Launch";
|
||||
import LockIcon from "@material-ui/icons/Lock";
|
||||
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import FieldSkeleton from "./FieldSkeleton";
|
||||
|
||||
import { FieldType, getFieldIcon } from "constants/fields";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { getFieldProp } from "components/fields";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
header: {
|
||||
marginBottom: theme.spacing(1),
|
||||
color: theme.palette.text.disabled,
|
||||
"& svg": { display: "block" },
|
||||
},
|
||||
iconContainer: {
|
||||
marginRight: theme.spacing(0.5),
|
||||
"& svg": { display: "block" },
|
||||
},
|
||||
|
||||
disabledText: {
|
||||
@@ -43,6 +45,7 @@ export interface IFieldWrapperProps {
|
||||
name?: string;
|
||||
label?: React.ReactNode;
|
||||
debugText?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function FieldWrapper({
|
||||
@@ -51,6 +54,7 @@ export default function FieldWrapper({
|
||||
name,
|
||||
label,
|
||||
debugText,
|
||||
disabled,
|
||||
}: IFieldWrapperProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
@@ -65,11 +69,16 @@ export default function FieldWrapper({
|
||||
htmlFor={`sidedrawer-field-${name}`}
|
||||
>
|
||||
<Grid item className={classes.iconContainer}>
|
||||
{type === "debug" ? <DebugIcon /> : getFieldIcon(type)}
|
||||
{type === "debug" ? <DebugIcon /> : getFieldProp("icon", type)}
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<Typography variant="caption">{label}</Typography>
|
||||
</Grid>
|
||||
{disabled && (
|
||||
<Grid item>
|
||||
<LockIcon />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<ErrorBoundary fullScreen={false} basic>
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import React from "react";
|
||||
import { Controller, useWatch } from "react-hook-form";
|
||||
import { IFieldProps } from "../utils";
|
||||
|
||||
import { createStyles, makeStyles, Grid, Typography } from "@material-ui/core";
|
||||
import { sanitiseCallableName, isUrl } from "utils/fns";
|
||||
|
||||
import ActionFab from "../../../Table/formatters/Action/ActionFab";
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {},
|
||||
|
||||
labelGridItem: { width: `calc(100% - 56px - ${theme.spacing(2)}px)` },
|
||||
labelContainer: {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor:
|
||||
theme.palette.type === "light"
|
||||
? "rgba(0, 0, 0, 0.09)"
|
||||
: "rgba(255, 255, 255, 0.09)",
|
||||
padding: theme.spacing(9 / 8, 1, 9 / 8, 1.5),
|
||||
|
||||
textAlign: "left",
|
||||
minHeight: 56,
|
||||
},
|
||||
label: {
|
||||
whiteSpace: "normal",
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export interface IActionProps extends IFieldProps {
|
||||
config: { callableName: string };
|
||||
}
|
||||
|
||||
function Action({ control, name, docRef, editable, config }: IActionProps) {
|
||||
const classes = useStyles();
|
||||
const docData = useWatch({ control });
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ onChange, onBlur, value }) => {
|
||||
const hasRan = value && value.status;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
alignItems="center"
|
||||
wrap="nowrap"
|
||||
className={classes.root}
|
||||
spacing={2}
|
||||
>
|
||||
<Grid item xs className={classes.labelGridItem}>
|
||||
<Grid
|
||||
container
|
||||
alignItems="center"
|
||||
className={classes.labelContainer}
|
||||
>
|
||||
<Typography variant="body1" className={classes.label}>
|
||||
{hasRan && isUrl(value.status) ? (
|
||||
<a
|
||||
href={value.status}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{value.status}
|
||||
</a>
|
||||
) : hasRan ? (
|
||||
value.status
|
||||
) : (
|
||||
sanitiseCallableName(name)
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<ActionFab
|
||||
row={{ ...docData, ref: docRef }}
|
||||
column={{ config, key: name }}
|
||||
onSubmit={onChange}
|
||||
value={value}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Action;
|
||||
@@ -1,121 +0,0 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { IFieldProps } from "../utils";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import AceEditor from "react-ace";
|
||||
import "ace-builds/src-noconflict/mode-javascript";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
|
||||
import { makeStyles, createStyles, Button } from "@material-ui/core";
|
||||
import CornerResizeIcon from "assets/icons/CornerResize";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
editorWrapper: { position: "relative" },
|
||||
|
||||
editor: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
resize: "vertical",
|
||||
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
},
|
||||
|
||||
resizeIcon: {
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
|
||||
saveButton: {
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export interface IControlledCodeProps {
|
||||
onChange: (...event: any[]) => void;
|
||||
onBlur: () => void;
|
||||
value: any;
|
||||
}
|
||||
|
||||
function ControlledCode({ onChange, onBlur, value }: IControlledCodeProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
useEffect(() => {
|
||||
if (value !== localValue) setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
const autoSave = false;
|
||||
const handleChange = autoSave
|
||||
? (value) => onChange(value)
|
||||
: (value) => setLocalValue(value);
|
||||
|
||||
const editor = useRef<AceEditor>(null);
|
||||
const handleResize = () => {
|
||||
if (!editor.current) return;
|
||||
editor.current.editor.resize();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.editorWrapper} onMouseUp={handleResize}>
|
||||
<AceEditor
|
||||
placeholder="Type code here…"
|
||||
mode="javascript"
|
||||
theme="github"
|
||||
//onLoad={this.onLoad}
|
||||
onChange={handleChange}
|
||||
fontSize={13}
|
||||
width="100%"
|
||||
height="150px"
|
||||
showGutter
|
||||
highlightActiveLine
|
||||
showPrintMargin
|
||||
value={autoSave ? value : localValue}
|
||||
setOptions={{
|
||||
enableBasicAutocompletion: false,
|
||||
enableLiveAutocompletion: false,
|
||||
enableSnippets: false,
|
||||
showLineNumbers: true,
|
||||
tabSize: 2,
|
||||
}}
|
||||
className={classes.editor}
|
||||
ref={editor}
|
||||
/>
|
||||
<CornerResizeIcon className={classes.resizeIcon} />
|
||||
</div>
|
||||
|
||||
{!autoSave && value !== localValue && (
|
||||
<>
|
||||
<Prompt
|
||||
when={true}
|
||||
message={(location) =>
|
||||
`You have some unsaved code changes, Are you sure you want to discard and continue?`
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => onChange(localValue)}
|
||||
className={classes.saveButton}
|
||||
variant="contained"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Code({ control, docRef, name, ...props }: IFieldProps) {
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={(renderProps) => <ControlledCode {...props} {...renderProps} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from "react";
|
||||
import { IFieldProps } from "../utils";
|
||||
|
||||
import { createStyles, makeStyles, Typography } from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
labelContainer: {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor:
|
||||
theme.palette.type === "light"
|
||||
? "rgba(0, 0, 0, 0.09)"
|
||||
: "rgba(255, 255, 255, 0.09)",
|
||||
padding: theme.spacing(9 / 8, 1, 9 / 8, 1.5),
|
||||
|
||||
textAlign: "left",
|
||||
minHeight: 56,
|
||||
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
label: {
|
||||
whiteSpace: "normal",
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
userSelect: "all",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function Id({ docRef }: IFieldProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.labelContainer}>
|
||||
<Typography variant="body1" className={classes.label}>
|
||||
{docRef.id}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import React from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { IFieldProps } from "../utils";
|
||||
import ReactJson from "react-json-view";
|
||||
|
||||
import { makeStyles, createStyles, useTheme } from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
backgroundColor:
|
||||
theme.palette.type === "light"
|
||||
? "rgba(0, 0, 0, 0.09)"
|
||||
: "rgba(255, 255, 255, 0.09)",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
padding: theme.spacing(2),
|
||||
|
||||
margin: 0,
|
||||
width: "100%",
|
||||
minHeight: 56,
|
||||
overflowX: "auto",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const isValidJson = (val: any) => {
|
||||
try {
|
||||
if (typeof val === "string") JSON.parse(val);
|
||||
else JSON.stringify(val);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const ThemedJSONEditor = ({ value, handleEdit }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<ReactJson
|
||||
src={isValidJson(value) ? value : {}}
|
||||
onEdit={handleEdit}
|
||||
onAdd={handleEdit}
|
||||
onDelete={handleEdit}
|
||||
theme={{
|
||||
base00: "rgba(0, 0, 0, 0)",
|
||||
base01: theme.palette.background.default,
|
||||
base02: theme.palette.divider,
|
||||
base03: "#93a1a1",
|
||||
base04: theme.palette.text.disabled,
|
||||
base05: theme.palette.text.secondary,
|
||||
base06: "#073642",
|
||||
base07: theme.palette.text.primary,
|
||||
base08: "#d33682",
|
||||
base09: "#cb4b16",
|
||||
base0A: "#dc322f",
|
||||
base0B: "#859900",
|
||||
base0C: "#6c71c4",
|
||||
base0D: theme.palette.text.secondary,
|
||||
base0E: "#2aa198",
|
||||
base0F: "#268bd2",
|
||||
}}
|
||||
iconStyle="triangle"
|
||||
style={{
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default function JsonEditor({ control, name }: IFieldProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ onChange, onBlur, value }) => {
|
||||
const handleEdit = (edit) => {
|
||||
onChange(edit.updated_src);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<ThemedJSONEditor value={value} handleEdit={handleEdit} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { IFieldProps } from "../utils";
|
||||
|
||||
import { makeStyles, createStyles, Grid } from "@material-ui/core";
|
||||
import { Rating as MuiRating } from "@material-ui/lab";
|
||||
import StarBorderIcon from "@material-ui/icons/StarBorder";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
backgroundColor:
|
||||
theme.palette.type === "light"
|
||||
? "rgba(0, 0, 0, 0.09)"
|
||||
: "rgba(255, 255, 255, 0.09)",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
padding: theme.spacing(0, 2),
|
||||
|
||||
margin: 0,
|
||||
width: "100%",
|
||||
height: 56,
|
||||
},
|
||||
|
||||
rating: { color: theme.palette.text.secondary },
|
||||
iconEmpty: { color: theme.palette.text.secondary },
|
||||
})
|
||||
);
|
||||
|
||||
export default function Rating({ control, name, editable }: IFieldProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ onChange, onBlur, value }) => (
|
||||
<Grid container alignItems="center" className={classes.root}>
|
||||
<MuiRating
|
||||
disabled={editable === false}
|
||||
name={name}
|
||||
id={`sidedrawer-field-${name}`}
|
||||
value={typeof value === "number" ? value : 0}
|
||||
onChange={(event, newValue) => {
|
||||
onChange(newValue);
|
||||
}}
|
||||
emptyIcon={<StarBorderIcon fontSize="inherit" />}
|
||||
classes={{ root: classes.rating, iconEmpty: classes.iconEmpty }}
|
||||
// TODO: Make this customisable in column settings
|
||||
max={4}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { IFieldProps } from "../utils";
|
||||
|
||||
import _RichText from "components/RichText";
|
||||
|
||||
export default function RichText({ control, name }: IFieldProps) {
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ onChange, onBlur, value }) => (
|
||||
<_RichText value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import React from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { IFieldProps } from "../utils";
|
||||
|
||||
import { useTheme } from "@material-ui/core";
|
||||
|
||||
import MultiSelect, { MultiSelectProps } from "@antlerengineering/multiselect";
|
||||
import FormattedChip from "components/FormattedChip";
|
||||
|
||||
export type ISingleSelectProps = IFieldProps &
|
||||
Omit<
|
||||
MultiSelectProps<string>,
|
||||
"name" | "multiple" | "value" | "onChange" | "options"
|
||||
> & {
|
||||
config?: {
|
||||
options: string[];
|
||||
freeText?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Uses the MultiSelect UI, but writes values as a string,
|
||||
* not an array of strings
|
||||
*/
|
||||
export default function SingleSelect({
|
||||
control,
|
||||
docRef,
|
||||
name,
|
||||
editable,
|
||||
config,
|
||||
...props
|
||||
}: ISingleSelectProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ onChange, onBlur, value }) => (
|
||||
<>
|
||||
<MultiSelect
|
||||
{...props}
|
||||
options={config?.options ?? []}
|
||||
multiple={false}
|
||||
value={Array.isArray(value) ? value[0] : value}
|
||||
onChange={onChange}
|
||||
disabled={editable === false}
|
||||
TextFieldProps={{
|
||||
label: "",
|
||||
hiddenLabel: true,
|
||||
onBlur,
|
||||
}}
|
||||
searchable
|
||||
freeText={config?.freeText}
|
||||
/>
|
||||
|
||||
{value?.length > 0 && (
|
||||
<div style={{ marginTop: theme.spacing(1) }}>
|
||||
<FormattedChip size="medium" label={value} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import React from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { IFieldProps } from "../utils";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
FormControl,
|
||||
Slider as MuiSlider,
|
||||
SliderProps,
|
||||
Grid,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: { display: "flex" },
|
||||
slider: { display: "block" },
|
||||
|
||||
thumb: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
marginTop: -7,
|
||||
marginLeft: -8,
|
||||
},
|
||||
|
||||
valueLabel: {
|
||||
top: -22,
|
||||
...theme.typography.overline,
|
||||
color: theme.palette.primary.main,
|
||||
|
||||
"& > *": {
|
||||
width: "auto",
|
||||
minWidth: 24,
|
||||
height: 24,
|
||||
|
||||
whiteSpace: "nowrap",
|
||||
borderRadius: 500,
|
||||
|
||||
padding: theme.spacing(0, 0.75, 0, 1),
|
||||
},
|
||||
"& *": { transform: "none" },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export interface ISliderProps extends IFieldProps, Omit<SliderProps, "name"> {
|
||||
units?: string;
|
||||
minLabel?: React.ReactNode;
|
||||
maxLabel?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Slider({
|
||||
control,
|
||||
docRef,
|
||||
name,
|
||||
units,
|
||||
minLabel,
|
||||
maxLabel,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...props
|
||||
}: ISliderProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ onChange, onBlur, value }) => {
|
||||
const handleChange = (_: any, value: number | number[]) => {
|
||||
onChange(value);
|
||||
onBlur();
|
||||
};
|
||||
|
||||
const getAriaValueText = (value: number) =>
|
||||
`${value}${units ? " " + units : ""}`;
|
||||
|
||||
const getValueLabelFormat = (value: number) =>
|
||||
`${value}${units ? " " + units : ""}`;
|
||||
|
||||
return (
|
||||
<FormControl className={classes.root}>
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
<Grid item>
|
||||
<Typography
|
||||
variant="overline"
|
||||
component="span"
|
||||
color="textSecondary"
|
||||
>
|
||||
{minLabel ?? `${min}${units ? " " + units : ""}`}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs>
|
||||
<MuiSlider
|
||||
valueLabelDisplay="auto"
|
||||
min={min}
|
||||
max={max}
|
||||
getAriaValueText={getAriaValueText}
|
||||
valueLabelFormat={getValueLabelFormat}
|
||||
{...props}
|
||||
value={value ?? min}
|
||||
onClick={onBlur}
|
||||
onChange={handleChange}
|
||||
classes={{
|
||||
root: classes.slider,
|
||||
thumb: classes.thumb,
|
||||
valueLabel: classes.valueLabel,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Typography
|
||||
variant="overline"
|
||||
component="span"
|
||||
color="textSecondary"
|
||||
>
|
||||
{maxLabel ?? `${max}${units ? " " + units : ""}`}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import React from "react";
|
||||
import { Controller, useWatch } from "react-hook-form";
|
||||
import { IFieldProps } from "../utils";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import queryString from "query-string";
|
||||
import useRouter from "hooks/useRouter";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
Grid,
|
||||
Typography,
|
||||
IconButton,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import LaunchIcon from "@material-ui/icons/Launch";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
labelContainer: {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor:
|
||||
theme.palette.type === "light"
|
||||
? "rgba(0, 0, 0, 0.09)"
|
||||
: "rgba(255, 255, 255, 0.09)",
|
||||
padding: theme.spacing(9 / 8, 1, 9 / 8, 1.5),
|
||||
|
||||
textAlign: "left",
|
||||
minHeight: 56,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export interface ISubTableProps extends IFieldProps {
|
||||
config: { parentLabel?: string[] };
|
||||
label: string;
|
||||
}
|
||||
|
||||
export default function SubTable({
|
||||
control,
|
||||
name,
|
||||
docRef,
|
||||
label,
|
||||
config,
|
||||
}: ISubTableProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const values = useWatch({ control });
|
||||
|
||||
const router = useRouter();
|
||||
const parentLabels = queryString.parse(router.location.search).parentLabel;
|
||||
const _label = config?.parentLabel
|
||||
? config.parentLabel.reduce((acc, curr) => {
|
||||
if (acc !== "") return `${acc} - ${values[curr]}`;
|
||||
else return values[curr];
|
||||
}, "")
|
||||
: "";
|
||||
let subTablePath = "";
|
||||
if (parentLabels)
|
||||
subTablePath =
|
||||
encodeURIComponent(`${docRef.path}/${name}`) +
|
||||
`?parentLabel=${parentLabels},${label}`;
|
||||
else
|
||||
subTablePath =
|
||||
encodeURIComponent(`${docRef.path}/${name}`) +
|
||||
`?parentLabel=${encodeURIComponent(_label)}`;
|
||||
|
||||
return (
|
||||
<Grid container wrap="nowrap">
|
||||
<Grid container alignItems="center" className={classes.labelContainer}>
|
||||
<Typography variant="body1">
|
||||
{label}
|
||||
{`: ${_label}`}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<IconButton
|
||||
component={Link}
|
||||
to={subTablePath}
|
||||
style={{ width: 56 }}
|
||||
disabled={!subTablePath}
|
||||
>
|
||||
<LaunchIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import React from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { IFieldProps } from "../utils";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
TextField,
|
||||
FilledTextFieldProps,
|
||||
} from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
multiline: { padding: theme.spacing(2.25, 1.5) },
|
||||
})
|
||||
);
|
||||
|
||||
export interface ITextProps
|
||||
extends IFieldProps,
|
||||
Omit<FilledTextFieldProps, "variant" | "name"> {
|
||||
fieldVariant?: "short" | "long" | "email" | "phone" | "number" | "url";
|
||||
config: { maxLength: string };
|
||||
}
|
||||
|
||||
export default function Text({
|
||||
control,
|
||||
name,
|
||||
docRef,
|
||||
fieldVariant = "short",
|
||||
editable,
|
||||
config,
|
||||
...props
|
||||
}: ITextProps) {
|
||||
const classes = useStyles();
|
||||
let variantProps = {};
|
||||
const { maxLength } = config;
|
||||
switch (fieldVariant) {
|
||||
case "long":
|
||||
variantProps = {
|
||||
multiline: true,
|
||||
InputProps: { classes: { multiline: classes.multiline } },
|
||||
inputProps: { rowsMin: 5, maxLength },
|
||||
};
|
||||
break;
|
||||
|
||||
case "email":
|
||||
variantProps = {
|
||||
// type: "email",
|
||||
inputProps: { autoComplete: "email", maxLength },
|
||||
};
|
||||
break;
|
||||
|
||||
case "phone":
|
||||
// TODO: add mask, validation
|
||||
variantProps = {
|
||||
type: "tel",
|
||||
inputProps: { autoComplete: "tel", maxLength },
|
||||
};
|
||||
break;
|
||||
|
||||
case "number":
|
||||
variantProps = { inputMode: "numeric", pattern: "[0-9]*" };
|
||||
break;
|
||||
|
||||
case "short":
|
||||
default:
|
||||
variantProps = { inputProps: { maxLength } };
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ onChange, onBlur, value }) => {
|
||||
const handleChange = (e) => {
|
||||
if (fieldVariant === "number") onChange(Number(e.target.value));
|
||||
else onChange(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<TextField
|
||||
variant="filled"
|
||||
fullWidth
|
||||
margin="none"
|
||||
placeholder={props.label as string}
|
||||
{...variantProps}
|
||||
{...props}
|
||||
onChange={handleChange}
|
||||
onBlur={onBlur}
|
||||
value={value}
|
||||
id={`sidedrawer-field-${name}`}
|
||||
label=""
|
||||
hiddenLabel
|
||||
disabled={editable === false}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import React from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { IFieldProps } from "../utils";
|
||||
|
||||
import {
|
||||
Grid,
|
||||
TextField,
|
||||
FilledTextFieldProps,
|
||||
IconButton,
|
||||
} from "@material-ui/core";
|
||||
import LaunchIcon from "@material-ui/icons/Launch";
|
||||
|
||||
export interface IUrlProps
|
||||
extends IFieldProps,
|
||||
Omit<FilledTextFieldProps, "name" | "variant"> {}
|
||||
|
||||
export default function Url({ control, name, docRef, ...props }: IUrlProps) {
|
||||
return (
|
||||
<Grid container wrap="nowrap">
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ onChange, onBlur, value }) => (
|
||||
<>
|
||||
<TextField
|
||||
variant="filled"
|
||||
fullWidth
|
||||
margin="none"
|
||||
placeholder={props.label as string}
|
||||
type="url"
|
||||
{...props}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
value={value || ""}
|
||||
id={`sidedrawer-field-${name}`}
|
||||
label=""
|
||||
hiddenLabel
|
||||
/>
|
||||
<IconButton
|
||||
component="a"
|
||||
href={value as string}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ width: 56 }}
|
||||
disabled={!value}
|
||||
>
|
||||
<LaunchIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import React from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { IFieldProps } from "../utils";
|
||||
|
||||
import {
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Grid,
|
||||
Typography,
|
||||
Avatar,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import { format } from "date-fns";
|
||||
import { DATE_TIME_FORMAT } from "constants/dates";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
labelContainer: {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor:
|
||||
theme.palette.type === "light"
|
||||
? "rgba(0, 0, 0, 0.09)"
|
||||
: "rgba(255, 255, 255, 0.09)",
|
||||
padding: theme.spacing(9 / 8, 1, 9 / 8, 1.5),
|
||||
|
||||
textAlign: "left",
|
||||
minHeight: 56,
|
||||
|
||||
cursor: "default",
|
||||
},
|
||||
|
||||
avatar: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
marginRight: theme.spacing(1.5),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function User({ control, name }: IFieldProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ value }) => (
|
||||
<Grid container alignItems="center" className={classes.labelContainer}>
|
||||
<Grid item>
|
||||
<Avatar
|
||||
alt="Avatar"
|
||||
src={value.photoURL}
|
||||
className={classes.avatar}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="body2">
|
||||
{value.displayName} ({value.email})
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{format(value.timestamp.toDate(), DATE_TIME_FORMAT)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
48
www/src/components/SideDrawer/Form/Reset.tsx
Normal file
48
www/src/components/SideDrawer/Form/Reset.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useEffect } from "react";
|
||||
import { UseFormMethods } from "react-hook-form";
|
||||
import _pickBy from "lodash/pickBy";
|
||||
import _isEqual from "lodash/isEqual";
|
||||
|
||||
import { Values } from "./utils";
|
||||
|
||||
export interface IResetProps
|
||||
extends Pick<UseFormMethods, "formState" | "reset" | "getValues"> {
|
||||
defaultValues: Values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the form’s values and errors when the Firestore doc’s data updates
|
||||
*/
|
||||
export default function Reset({
|
||||
defaultValues,
|
||||
formState,
|
||||
reset,
|
||||
getValues,
|
||||
}: IResetProps) {
|
||||
useEffect(
|
||||
() => {
|
||||
const resetValues = { ...defaultValues };
|
||||
const currentValues = getValues();
|
||||
|
||||
// If the field is dirty, (i.e. the user input a value but it hasn’t been)
|
||||
// saved to the db yet, keep its current value and keep it marked as dirty
|
||||
for (const [field, isDirty] of Object.entries(formState.dirtyFields)) {
|
||||
if (isDirty) {
|
||||
resetValues[field] = currentValues[field];
|
||||
}
|
||||
}
|
||||
|
||||
// Compare currentValues to resetValues
|
||||
const diff = _pickBy(getValues(), (v, k) => !_isEqual(v, resetValues[k]));
|
||||
// Reset if needed & keep the current dirty fields
|
||||
if (Object.keys(diff).length > 0) {
|
||||
reset(resetValues, { isDirty: true, dirtyFields: true });
|
||||
}
|
||||
},
|
||||
// `defaultValues` is the `initialValue` of each field type +
|
||||
// the current value in the Firestore doc
|
||||
[JSON.stringify(defaultValues)]
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,142 +1,48 @@
|
||||
import React, { lazy } from "react";
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import _isFunction from "lodash/isFunction";
|
||||
import _sortBy from "lodash/sortBy";
|
||||
import _isEmpty from "lodash/isEmpty";
|
||||
|
||||
import { Grid } from "@material-ui/core";
|
||||
|
||||
import { Fields, Values, getInitialValues, Field } from "./utils";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { Values } from "./utils";
|
||||
import { getFieldProp } from "components/fields";
|
||||
import { IFieldConfig } from "components/fields/types";
|
||||
import Autosave from "./Autosave";
|
||||
import Reset from "./Reset";
|
||||
import FieldWrapper from "./FieldWrapper";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
|
||||
import Text from "./Fields/Text";
|
||||
const Url = lazy(
|
||||
() => import("./Fields/Url" /* webpackChunkName: "SideDrawer-Url" */)
|
||||
);
|
||||
const SingleSelect = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./Fields/SingleSelect" /* webpackChunkName: "SideDrawer-SingleSelect" */
|
||||
)
|
||||
);
|
||||
const MultiSelect = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./Fields/MultiSelect" /* webpackChunkName: "SideDrawer-MultiSelect" */
|
||||
)
|
||||
);
|
||||
const DatePicker = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./Fields/DatePicker" /* webpackChunkName: "SideDrawer-DatePicker" */
|
||||
)
|
||||
);
|
||||
const DateTimePicker = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./Fields/DateTimePicker" /* webpackChunkName: "SideDrawer-DateTimePicker" */
|
||||
)
|
||||
);
|
||||
const Checkbox = lazy(
|
||||
() =>
|
||||
import("./Fields/Checkbox" /* webpackChunkName: "SideDrawer-Checkbox" */)
|
||||
);
|
||||
const Rating = lazy(
|
||||
() => import("./Fields/Rating" /* webpackChunkName: "SideDrawer-Rating" */)
|
||||
);
|
||||
const Percentage = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./Fields/Percentage" /* webpackChunkName: "SideDrawer-Percentage" */
|
||||
)
|
||||
);
|
||||
const Color = lazy(
|
||||
() => import("./Fields/Color" /* webpackChunkName: "SideDrawer-Color" */)
|
||||
);
|
||||
const Slider = lazy(
|
||||
() => import("./Fields/Slider" /* webpackChunkName: "SideDrawer-Slider" */)
|
||||
);
|
||||
const ImageUploader = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./Fields/ImageUploader" /* webpackChunkName: "SideDrawer-ImageUploader" */
|
||||
)
|
||||
);
|
||||
const FileUploader = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./Fields/FileUploader" /* webpackChunkName: "SideDrawer-FileUploader" */
|
||||
)
|
||||
);
|
||||
const RichText = lazy(
|
||||
() =>
|
||||
import("./Fields/RichText" /* webpackChunkName: "SideDrawer-RichText" */)
|
||||
);
|
||||
const JsonEditor = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./Fields/JsonEditor" /* webpackChunkName: "SideDrawer-JsonEditor" */
|
||||
)
|
||||
);
|
||||
const ConnectTable = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./Fields/ConnectTable" /* webpackChunkName: "SideDrawer-ConnectTable" */
|
||||
)
|
||||
);
|
||||
const ConnectService = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./Fields/ConnectService" /* webpackChunkName: "SideDrawer-ConnectTable" */
|
||||
)
|
||||
);
|
||||
const Code = lazy(
|
||||
() => import("./Fields/Code" /* webpackChunkName: "SideDrawer-Code" */)
|
||||
);
|
||||
const SubTable = lazy(
|
||||
() =>
|
||||
import("./Fields/SubTable" /* webpackChunkName: "SideDrawer-SubTable" */)
|
||||
);
|
||||
const Action = lazy(
|
||||
() => import("./Fields/Action" /* webpackChunkName: "SideDrawer-Action" */)
|
||||
);
|
||||
const Id = lazy(
|
||||
() => import("./Fields/Id" /* webpackChunkName: "SideDrawer-Id" */)
|
||||
);
|
||||
const User = lazy(
|
||||
() => import("./Fields/User" /* webpackChunkName: "SideDrawer-User" */)
|
||||
);
|
||||
|
||||
export interface IFormProps {
|
||||
fields: Fields;
|
||||
values: Values;
|
||||
}
|
||||
|
||||
export default function Form({ fields, values }: IFormProps) {
|
||||
const initialValues = getInitialValues(fields);
|
||||
const { ref: docRef, ...rowValues } = values;
|
||||
const defaultValues = { ...initialValues, ...rowValues };
|
||||
|
||||
export default function Form({ values }: IFormProps) {
|
||||
const { tableState } = useFiretableContext();
|
||||
const { userDoc } = useAppContext();
|
||||
const userDocHiddenFields =
|
||||
userDoc.state.doc?.tables?.[`${tableState?.tablePath}`]?.hiddenFields ?? [];
|
||||
userDoc.state.doc?.tables?.[`${tableState!.tablePath}`]?.hiddenFields ?? [];
|
||||
|
||||
const { register, control } = useForm({
|
||||
const fields = _sortBy(Object.values(tableState!.columns), "index").filter(
|
||||
(f) => !userDocHiddenFields.includes(f.name)
|
||||
);
|
||||
|
||||
// Get initial values from fields config. This won’t be written to the db
|
||||
// when the SideDrawer is opened. Only dirty fields will be written
|
||||
const initialValues = fields.reduce(
|
||||
(a, { key, type }) => ({ ...a, [key]: getFieldProp("initialValue", type) }),
|
||||
{}
|
||||
);
|
||||
const { ref: docRef, ...rowValues } = values;
|
||||
const defaultValues = { ...initialValues, ...rowValues };
|
||||
|
||||
const { control, reset, formState, getValues } = useForm({
|
||||
mode: "onBlur",
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
// Update field values when Firestore document updates
|
||||
// useEffect(() => {
|
||||
// console.log("RESET", defaultValues);
|
||||
// reset(defaultValues);
|
||||
// }, [reset, JSON.stringify(rowValues)]);
|
||||
|
||||
// const { sideDrawerRef } = useFiretableContext();
|
||||
// useEffect(() => {
|
||||
// const column = sideDrawerRef?.current?.cell?.column;
|
||||
@@ -153,150 +59,55 @@ export default function Form({ fields, values }: IFormProps) {
|
||||
<form>
|
||||
<Autosave
|
||||
control={control}
|
||||
defaultValues={defaultValues}
|
||||
docRef={docRef}
|
||||
row={values}
|
||||
reset={reset}
|
||||
formState={formState}
|
||||
/>
|
||||
|
||||
<Reset
|
||||
formState={formState}
|
||||
reset={reset}
|
||||
defaultValues={defaultValues}
|
||||
getValues={getValues}
|
||||
/>
|
||||
|
||||
<Grid container spacing={4} direction="column" wrap="nowrap">
|
||||
{fields
|
||||
.filter((f) => !userDocHiddenFields.includes(f.name))
|
||||
.map((_field, i) => {
|
||||
// Call the field function with values if necessary
|
||||
// Otherwise, just use the field object
|
||||
const field: Field = _isFunction(_field) ? _field(values) : _field;
|
||||
const { type, ...fieldProps } = field;
|
||||
let _type = type;
|
||||
{fields.map((field, i) => {
|
||||
// Derivative/aggregate field support
|
||||
let type = field.type;
|
||||
if (field.config && field.config.renderFieldType) {
|
||||
type = field.config.renderFieldType;
|
||||
}
|
||||
|
||||
// Derivative/aggregate field support
|
||||
if (field.config && field.config.renderFieldType) {
|
||||
_type = field.config.renderFieldType;
|
||||
}
|
||||
const fieldComponent: IFieldConfig["SideDrawerField"] = getFieldProp(
|
||||
"SideDrawerField",
|
||||
type
|
||||
);
|
||||
|
||||
let fieldComponent: React.ComponentType<any> | null = null;
|
||||
// Should not reach this state
|
||||
if (_isEmpty(fieldComponent)) {
|
||||
// console.error('Could not find SideDrawerField component', field);
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (_type) {
|
||||
case FieldType.shortText:
|
||||
case FieldType.longText:
|
||||
case FieldType.email:
|
||||
case FieldType.phone:
|
||||
case FieldType.number:
|
||||
fieldComponent = Text;
|
||||
break;
|
||||
|
||||
case FieldType.url:
|
||||
fieldComponent = Url;
|
||||
break;
|
||||
|
||||
case FieldType.singleSelect:
|
||||
fieldComponent = SingleSelect;
|
||||
break;
|
||||
|
||||
case FieldType.multiSelect:
|
||||
fieldComponent = MultiSelect;
|
||||
break;
|
||||
|
||||
case FieldType.date:
|
||||
fieldComponent = DatePicker;
|
||||
break;
|
||||
|
||||
case FieldType.dateTime:
|
||||
fieldComponent = DateTimePicker;
|
||||
break;
|
||||
|
||||
case FieldType.checkbox:
|
||||
fieldComponent = Checkbox;
|
||||
break;
|
||||
|
||||
case FieldType.color:
|
||||
fieldComponent = Color;
|
||||
break;
|
||||
|
||||
case FieldType.slider:
|
||||
fieldComponent = Slider;
|
||||
break;
|
||||
|
||||
case FieldType.richText:
|
||||
fieldComponent = RichText;
|
||||
break;
|
||||
|
||||
case FieldType.image:
|
||||
fieldComponent = ImageUploader;
|
||||
break;
|
||||
|
||||
case FieldType.file:
|
||||
fieldComponent = FileUploader;
|
||||
break;
|
||||
|
||||
case FieldType.rating:
|
||||
fieldComponent = Rating;
|
||||
break;
|
||||
|
||||
case FieldType.percentage:
|
||||
fieldComponent = Percentage;
|
||||
break;
|
||||
|
||||
case FieldType.connectTable:
|
||||
fieldComponent = ConnectTable;
|
||||
break;
|
||||
|
||||
case FieldType.connectService:
|
||||
fieldComponent = ConnectService;
|
||||
break;
|
||||
|
||||
case FieldType.subTable:
|
||||
fieldComponent = SubTable;
|
||||
break;
|
||||
|
||||
case FieldType.action:
|
||||
fieldComponent = Action;
|
||||
break;
|
||||
|
||||
case FieldType.json:
|
||||
fieldComponent = JsonEditor;
|
||||
break;
|
||||
|
||||
case FieldType.code:
|
||||
fieldComponent = Code;
|
||||
break;
|
||||
|
||||
case FieldType.id:
|
||||
fieldComponent = Id;
|
||||
break;
|
||||
|
||||
case FieldType.user:
|
||||
fieldComponent = User;
|
||||
break;
|
||||
|
||||
case undefined:
|
||||
// default:
|
||||
return null;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Should not reach this state
|
||||
if (fieldComponent === null) {
|
||||
console.error("`fieldComponent` is null", field);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldWrapper
|
||||
key={fieldProps.name ?? i}
|
||||
type={_type}
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
>
|
||||
{React.createElement(fieldComponent, {
|
||||
...fieldProps,
|
||||
control,
|
||||
docRef,
|
||||
})}
|
||||
</FieldWrapper>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<FieldWrapper
|
||||
key={field.key ?? i}
|
||||
type={field.type}
|
||||
name={field.key}
|
||||
label={field.name}
|
||||
disabled={field.editable === false}
|
||||
>
|
||||
{React.createElement(fieldComponent, {
|
||||
column: field,
|
||||
control,
|
||||
docRef,
|
||||
disabled: field.editable === false,
|
||||
})}
|
||||
</FieldWrapper>
|
||||
);
|
||||
})}
|
||||
|
||||
<FieldWrapper
|
||||
type="debug"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Control } from "react-hook-form";
|
||||
import _isFunction from "lodash/isFunction";
|
||||
|
||||
import { makeStyles, createStyles } from "@material-ui/core";
|
||||
import { FieldType } from "constants/fields";
|
||||
|
||||
export interface IFieldProps {
|
||||
@@ -10,7 +9,7 @@ export interface IFieldProps {
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
export type Values = { [key: string]: any };
|
||||
export type Values = Record<string, any>;
|
||||
export type Field = {
|
||||
type?: FieldType;
|
||||
name: string;
|
||||
@@ -19,46 +18,25 @@ export type Field = {
|
||||
};
|
||||
export type Fields = (Field | ((values: Values) => Field))[];
|
||||
|
||||
export const initializeValue = (type) => {
|
||||
switch (type) {
|
||||
case FieldType.multiSelect:
|
||||
case FieldType.image:
|
||||
case FieldType.file:
|
||||
return [];
|
||||
export const useFieldStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor:
|
||||
theme.palette.type === "light"
|
||||
? "rgba(0, 0, 0, 0.09)"
|
||||
: "rgba(255, 255, 255, 0.09)",
|
||||
padding: theme.spacing(1, 1, 1, 1.5),
|
||||
|
||||
case FieldType.singleSelect:
|
||||
case FieldType.date:
|
||||
case FieldType.dateTime:
|
||||
return null;
|
||||
width: "100%",
|
||||
minHeight: 56,
|
||||
|
||||
case FieldType.checkbox:
|
||||
return false;
|
||||
display: "flex",
|
||||
textAlign: "left",
|
||||
alignItems: "center",
|
||||
|
||||
case FieldType.json:
|
||||
return {};
|
||||
|
||||
case FieldType.shortText:
|
||||
case FieldType.longText:
|
||||
case FieldType.email:
|
||||
case FieldType.phone:
|
||||
case FieldType.url:
|
||||
case FieldType.code:
|
||||
case FieldType.richText:
|
||||
case FieldType.number:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
export const getInitialValues = (fields: Fields): Values =>
|
||||
fields.reduce((acc, _field) => {
|
||||
const field = _isFunction(_field) ? _field({}) : _field;
|
||||
if (!field.name) return acc;
|
||||
let _type = field.type;
|
||||
if (field.config && field.config.renderFieldType) {
|
||||
_type = field.config.renderFieldType;
|
||||
}
|
||||
const value = initializeValue(_type);
|
||||
|
||||
return { ...acc, [field.name]: value };
|
||||
}, {});
|
||||
...theme.typography.body1,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import clsx from "clsx";
|
||||
import _isNil from "lodash/isNil";
|
||||
import _sortBy from "lodash/sortBy";
|
||||
import _isEmpty from "lodash/isEmpty";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { Drawer, Fab } from "@material-ui/core";
|
||||
import ChevronIcon from "@material-ui/icons/KeyboardArrowLeft";
|
||||
import ChevronUpIcon from "@material-ui/icons/KeyboardArrowUp";
|
||||
import ChevronDownIcon from "@material-ui/icons/KeyboardArrowDown";
|
||||
|
||||
import Form from "./Form";
|
||||
import { Field } from "./Form/utils";
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
|
||||
import { useStyles } from "./useStyles";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
import { FieldType } from "constants/fields";
|
||||
import useDoc from "hooks/useDoc";
|
||||
|
||||
export const DRAWER_WIDTH = 600;
|
||||
@@ -66,10 +65,7 @@ export default function SideDrawer() {
|
||||
|
||||
useEffect(() => {
|
||||
const rowRef = queryString.parse(window.location.search).rowRef as string;
|
||||
if (rowRef) {
|
||||
console.log(rowRef);
|
||||
dispatchUrlDoc({ path: decodeURIComponent(rowRef) });
|
||||
}
|
||||
if (rowRef) dispatchUrlDoc({ path: decodeURIComponent(rowRef) });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -89,52 +85,6 @@ export default function SideDrawer() {
|
||||
}
|
||||
}, [cell]);
|
||||
|
||||
// Map columns to form fields
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
switch (column.type) {
|
||||
case FieldType.longText:
|
||||
field.fieldVariant = "long";
|
||||
break;
|
||||
|
||||
case FieldType.email:
|
||||
field.fieldVariant = "email";
|
||||
break;
|
||||
|
||||
case FieldType.phone:
|
||||
field.fieldVariant = "phone";
|
||||
break;
|
||||
|
||||
case FieldType.number:
|
||||
field.fieldVariant = "number";
|
||||
break;
|
||||
|
||||
// case FieldType.singleSelect:
|
||||
// case FieldType.multiSelect:
|
||||
// case FieldType.connectTable:
|
||||
// case FieldType.subTable:
|
||||
// case FieldType.action:
|
||||
// break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
field.editable = column.editable;
|
||||
field.config = column.config;
|
||||
return field;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={clsx(open && classes.open, disabled && classes.disabled)}>
|
||||
<Drawer
|
||||
@@ -151,23 +101,16 @@ export default function SideDrawer() {
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<div className={classes.drawerContents}>
|
||||
{open && fields && urlDocState.doc ? (
|
||||
<Form
|
||||
key={urlDocState.path}
|
||||
fields={fields}
|
||||
values={urlDocState.doc ?? {}}
|
||||
/>
|
||||
) : (
|
||||
open &&
|
||||
fields &&
|
||||
cell && (
|
||||
{open &&
|
||||
(urlDocState.doc || cell) &&
|
||||
!_isEmpty(tableState?.columns) && (
|
||||
<Form
|
||||
key={cell.row}
|
||||
fields={fields}
|
||||
values={tableState?.rows[cell.row] ?? {}}
|
||||
key={urlDocState.path}
|
||||
values={
|
||||
urlDocState.doc ?? tableState?.rows[cell?.row ?? -1] ?? {}
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
18
www/src/components/StyledModal/Transition.tsx
Normal file
18
www/src/components/StyledModal/Transition.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
import { TransitionProps } from "@material-ui/core/transitions";
|
||||
import { Grow, Slide } from "@material-ui/core";
|
||||
|
||||
export const TransitionGrow = React.forwardRef(function Transition(
|
||||
props: TransitionProps & { children?: React.ReactElement<any, any> },
|
||||
ref: React.Ref<unknown>
|
||||
) {
|
||||
return <Grow ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
export const TransitionSlide = React.forwardRef(function Transition(
|
||||
props: TransitionProps & { children?: React.ReactElement<any, any> },
|
||||
ref: React.Ref<unknown>
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
249
www/src/components/StyledModal/index.tsx
Normal file
249
www/src/components/StyledModal/index.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Dialog,
|
||||
DialogProps,
|
||||
IconButton,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
// Tabs,
|
||||
// Tab,
|
||||
Divider,
|
||||
Grid,
|
||||
Button,
|
||||
ButtonProps,
|
||||
} from "@material-ui/core";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
|
||||
import { TransitionGrow, TransitionSlide } from "./Transition";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
paper: {
|
||||
userSelect: "none",
|
||||
overflowX: "hidden",
|
||||
},
|
||||
|
||||
paperFullScreen: {
|
||||
marginTop: theme.spacing(2),
|
||||
height: `calc(100% - ${theme.spacing(2)}px)`,
|
||||
borderTopLeftRadius: theme.shape.borderRadius * 2,
|
||||
borderTopRightRadius: theme.shape.borderRadius * 2,
|
||||
},
|
||||
|
||||
closeButton: {
|
||||
margin: theme.spacing(0.5),
|
||||
marginLeft: "auto",
|
||||
marginBottom: 0,
|
||||
display: "flex",
|
||||
},
|
||||
|
||||
title: {
|
||||
paddingTop: theme.spacing(8),
|
||||
paddingLeft: theme.spacing(8),
|
||||
color: theme.palette.text.secondary,
|
||||
|
||||
[theme.breakpoints.down("xs")]: {
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingLeft: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
divider: {
|
||||
margin: theme.spacing(0, 8),
|
||||
[theme.breakpoints.down("xs")]: { margin: theme.spacing(0, 2) },
|
||||
},
|
||||
|
||||
content: {
|
||||
padding: theme.spacing(3, 8, 6),
|
||||
[theme.breakpoints.down("xs")]: { padding: theme.spacing(2) },
|
||||
|
||||
"& > section + section": { marginTop: theme.spacing(4) },
|
||||
},
|
||||
|
||||
actions: {
|
||||
margin: theme.spacing(0, -2),
|
||||
padding: theme.spacing(0, 8, 2),
|
||||
|
||||
[theme.breakpoints.down("xs")]: {
|
||||
margin: theme.spacing(0, -0.5),
|
||||
padding: theme.spacing(1, 0),
|
||||
marginTop: "auto",
|
||||
},
|
||||
|
||||
"& button": { minWidth: 142 },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export interface IStyledModalProps extends Partial<Omit<DialogProps, "title">> {
|
||||
onClose: () => void;
|
||||
initialTab?: number;
|
||||
|
||||
title: React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
|
||||
tabs?: {
|
||||
label: string;
|
||||
content: React.ReactNode | React.ReactNodeArray;
|
||||
disabled?: boolean;
|
||||
}[];
|
||||
children?: React.ReactNode;
|
||||
bodyContent?: React.ReactNode;
|
||||
|
||||
actions?: {
|
||||
primary?: Partial<ButtonProps>;
|
||||
secondary?: Partial<ButtonProps>;
|
||||
};
|
||||
}
|
||||
|
||||
export default function StyledModal({
|
||||
onClose,
|
||||
initialTab = 0,
|
||||
title,
|
||||
header,
|
||||
footer,
|
||||
tabs,
|
||||
children,
|
||||
bodyContent,
|
||||
actions,
|
||||
...props
|
||||
}: IStyledModalProps) {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("xs"));
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// const [tab, setTab] = useState(initialTab)
|
||||
// const handleChangeTab = (_, newValue: number) => {
|
||||
// setTab(newValue)
|
||||
// if (scrollContainerRef?.current)
|
||||
// scrollContainerRef.current.scrollTo({ top: 0, left: 0 })
|
||||
// }
|
||||
|
||||
const [open, setOpen] = useState(true);
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
setTimeout(onClose, 300);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
TransitionComponent={isMobile ? TransitionSlide : TransitionGrow}
|
||||
onClose={handleClose}
|
||||
fullWidth
|
||||
fullScreen={isMobile}
|
||||
aria-labelledby="sub-modal-title"
|
||||
classes={{
|
||||
paper: classes.paper,
|
||||
paperFullScreen: classes.paperFullScreen,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Grid container>
|
||||
<Grid item xs>
|
||||
<DialogTitle id="sub-modal-title" className={classes.title}>
|
||||
{title}
|
||||
</DialogTitle>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
className={classes.closeButton}
|
||||
aria-label="Close"
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Divider className={classes.divider} />
|
||||
|
||||
{header}
|
||||
|
||||
{/* {tabs && (
|
||||
<div className={classes.tabsContainer}>
|
||||
<Tabs
|
||||
className={classes.tabs}
|
||||
value={tab}
|
||||
onChange={handleChangeTab}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="fullWidth"
|
||||
aria-label="full width tabs"
|
||||
action={actions =>
|
||||
setTimeout(() => actions?.updateIndicator(), 200)
|
||||
}
|
||||
>
|
||||
{tabs?.map((tab, index) => (
|
||||
<Tab
|
||||
key={`card-tab-${index}`}
|
||||
className={classes.tab}
|
||||
label={tab.label}
|
||||
disabled={tab.disabled}
|
||||
{...a11yProps(index)}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
<Divider className={clsx(classes.tabs, classes.tabDivider)} />
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
<DialogContent className={classes.content} ref={scrollContainerRef}>
|
||||
{/* {tabs && (
|
||||
<div className={classes.tabSection}>
|
||||
{tabs[tab].content && Array.isArray(tabs[tab].content) ? (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
wrap="nowrap"
|
||||
spacing={3}
|
||||
className={classes.tabContentGrid}
|
||||
>
|
||||
{(tabs?.[tab]?.content as string[])?.map((element, index) => (
|
||||
<Grid item key={`tab-content-${index}`}>
|
||||
{element}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
tabs[tab].content
|
||||
)}
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{children || bodyContent}
|
||||
</DialogContent>
|
||||
|
||||
{footer}
|
||||
|
||||
{actions && (
|
||||
<Grid
|
||||
container
|
||||
spacing={isMobile ? 1 : 4}
|
||||
justify="center"
|
||||
alignItems="center"
|
||||
className={classes.actions}
|
||||
>
|
||||
{actions.secondary && (
|
||||
<Grid item>
|
||||
<Button size="large" variant="outlined" {...actions.secondary} />
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{actions.primary && (
|
||||
<Grid item>
|
||||
<Button size="large" variant="contained" {...actions.primary} />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
import SortDescIcon from "@material-ui/icons/ArrowDownward";
|
||||
import DropdownIcon from "@material-ui/icons/ArrowDropDownCircle";
|
||||
|
||||
import { getFieldIcon, FieldType } from "constants/fields";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { getFieldProp } from "components/fields";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
import { FiretableOrderBy } from "hooks/useFiretable";
|
||||
|
||||
@@ -203,7 +204,7 @@ export default function DraggableHeaderRenderer<R>({
|
||||
navigator.clipboard.writeText(column.key as string);
|
||||
}}
|
||||
>
|
||||
{getFieldIcon((column as any).type)}
|
||||
{getFieldProp("icon", (column as any).type)}
|
||||
</Grid>
|
||||
</Tooltip>
|
||||
|
||||
@@ -258,7 +259,11 @@ export default function DraggableHeaderRenderer<R>({
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{userClaims?.roles?.includes("ADMIN") && (
|
||||
{(userClaims?.roles?.includes("ADMIN") ||
|
||||
(userClaims?.roles?.includes("OPS") &&
|
||||
[FieldType.multiSelect, FieldType.singleSelect].includes(
|
||||
(column as any).type
|
||||
))) && (
|
||||
<Grid item>
|
||||
<IconButton
|
||||
size="small"
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { IMenuModalProps } from "..";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
Typography,
|
||||
TextField,
|
||||
MenuItem,
|
||||
ListItemText,
|
||||
} from "@material-ui/core";
|
||||
import Subheading from "../Subheading";
|
||||
|
||||
import { getFieldProp } from "components/fields";
|
||||
import CodeEditor from "components/CodeEditor";
|
||||
import FormAutosave from "./FormAutosave";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
typeSelect: { marginBottom: theme.spacing(1) },
|
||||
typeSelectItem: { whiteSpace: "normal" },
|
||||
|
||||
codeEditorContainer: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
overflow: "hidden",
|
||||
},
|
||||
|
||||
mono: {
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export interface IDefaultValueInputProps extends IMenuModalProps {
|
||||
handleChange: (key: any) => (update: any) => void;
|
||||
}
|
||||
|
||||
export default function DefaultValueInput({
|
||||
config,
|
||||
handleChange,
|
||||
type,
|
||||
fieldName,
|
||||
...props
|
||||
}: IDefaultValueInputProps) {
|
||||
const classes = useStyles();
|
||||
const customFieldInput = getFieldProp("SideDrawerField", type);
|
||||
|
||||
const { control } = useForm({
|
||||
mode: "onBlur",
|
||||
defaultValues: {
|
||||
[fieldName]:
|
||||
config.defaultValue?.value ?? getFieldProp("initialValue", type),
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subheading>Default Value</Subheading>
|
||||
<Typography color="textSecondary" gutterBottom>
|
||||
The default value will be the initial value of this cell when a new row
|
||||
is added.
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
select
|
||||
label="Default Value Type"
|
||||
value={config.defaultValue?.type ?? "undefined"}
|
||||
onChange={(e) => handleChange("defaultValue.type")(e.target.value)}
|
||||
fullWidth
|
||||
className={classes.typeSelect}
|
||||
>
|
||||
<MenuItem value="undefined">
|
||||
<ListItemText
|
||||
primary="Undefined"
|
||||
secondary="No default value. The field will not appear in the row’s corresponding Firestore document by default."
|
||||
className={classes.typeSelectItem}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem value="null">
|
||||
<ListItemText
|
||||
primary="Null"
|
||||
secondary={
|
||||
<>
|
||||
Initialise as <span className={classes.mono}>null</span>.
|
||||
</>
|
||||
}
|
||||
className={classes.typeSelectItem}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem value="static">
|
||||
<ListItemText
|
||||
primary="Static"
|
||||
secondary="Set a specific default value for all cells in this column."
|
||||
className={classes.typeSelectItem}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem value="dynamic">
|
||||
<ListItemText
|
||||
primary="Dynamic (Requires Firetable Cloud Functions)"
|
||||
secondary="Write code to set the default value using this table’s Firetable Cloud Function. Setup is required."
|
||||
className={classes.typeSelectItem}
|
||||
/>
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
|
||||
{config.defaultValue?.type === "static" && customFieldInput && (
|
||||
<form>
|
||||
<FormAutosave
|
||||
control={control}
|
||||
handleSave={(values) =>
|
||||
handleChange("defaultValue.value")(values[fieldName])
|
||||
}
|
||||
/>
|
||||
|
||||
{React.createElement(customFieldInput, {
|
||||
column: { type, key: fieldName, ...props, ...config },
|
||||
control,
|
||||
docRef: {},
|
||||
disabled: false,
|
||||
})}
|
||||
</form>
|
||||
)}
|
||||
|
||||
{config.defaultValue?.type === "dynamic" && (
|
||||
<div className={classes.codeEditorContainer}>
|
||||
<CodeEditor
|
||||
height={120}
|
||||
value={config.defaultValue?.script}
|
||||
onChange={handleChange("defaultValue.script")}
|
||||
editorOptions={{
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useEffect } from "react";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import _isEqual from "lodash/isEqual";
|
||||
|
||||
import { Control, useWatch } from "react-hook-form";
|
||||
|
||||
export interface IAutosaveProps {
|
||||
control: Control;
|
||||
handleSave: (values: any) => void;
|
||||
}
|
||||
|
||||
export default function FormAutosave({ control, handleSave }: IAutosaveProps) {
|
||||
const values = useWatch({ control });
|
||||
|
||||
const [debouncedValue] = useDebounce(values, 1000, {
|
||||
equalityFn: _isEqual,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
handleSave(debouncedValue);
|
||||
}, [debouncedValue]);
|
||||
|
||||
return null;
|
||||
}
|
||||
147
www/src/components/Table/ColumnMenu/FieldSettings/index.tsx
Normal file
147
www/src/components/Table/ColumnMenu/FieldSettings/index.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import React, { useState, Suspense } from "react";
|
||||
import _sortBy from "lodash/sortBy";
|
||||
import _set from "lodash/set";
|
||||
import { IMenuModalProps } from "..";
|
||||
|
||||
import { Typography, FormControlLabel, Switch } from "@material-ui/core";
|
||||
|
||||
import StyledModal from "components/StyledModal";
|
||||
import { getFieldProp } from "components/fields";
|
||||
import Subheading from "../Subheading";
|
||||
import DefaultValueInput from "./DefaultValueInput";
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import Loading from "components/Loading";
|
||||
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
import { triggerCloudBuild } from "../../../../firebase/callables";
|
||||
import { useConfirmation } from "components/ConfirmationDialog";
|
||||
import { FieldType } from "constants/fields";
|
||||
|
||||
export default function FieldSettings(props: IMenuModalProps) {
|
||||
const {
|
||||
name,
|
||||
fieldName,
|
||||
type,
|
||||
open,
|
||||
config,
|
||||
handleClose,
|
||||
handleSave,
|
||||
} = props;
|
||||
|
||||
const [newConfig, setNewConfig] = useState(config ?? {});
|
||||
const customFieldSettings = getFieldProp("settings", type);
|
||||
const initializable = getFieldProp("initializable", type);
|
||||
|
||||
const { requestConfirmation } = useConfirmation();
|
||||
const { tableState } = useFiretableContext();
|
||||
|
||||
const handleChange = (key: string) => (update: any) => {
|
||||
const updatedConfig = _set({ ...newConfig }, key, update);
|
||||
setNewConfig(updatedConfig);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
maxWidth="md"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
title={`${name}: Settings`}
|
||||
children={
|
||||
<Suspense fallback={<Loading fullScreen={false} />}>
|
||||
<>
|
||||
{initializable && (
|
||||
<>
|
||||
<section>
|
||||
<Subheading>Required?</Subheading>
|
||||
<Typography color="textSecondary" paragraph>
|
||||
The row will not be created or updated unless all required
|
||||
values are set.
|
||||
</Typography>
|
||||
|
||||
<FormControlLabel
|
||||
value="required"
|
||||
label="Make this column required"
|
||||
labelPlacement="start"
|
||||
control={
|
||||
<Switch
|
||||
checked={newConfig["required"]}
|
||||
onChange={() =>
|
||||
setNewConfig({
|
||||
...newConfig,
|
||||
required: !Boolean(newConfig["required"]),
|
||||
})
|
||||
}
|
||||
name="required"
|
||||
/>
|
||||
}
|
||||
style={{
|
||||
marginLeft: 0,
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<ErrorBoundary fullScreen={false}>
|
||||
<DefaultValueInput
|
||||
handleChange={handleChange}
|
||||
{...props}
|
||||
config={newConfig}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
<section>
|
||||
{customFieldSettings &&
|
||||
React.createElement(customFieldSettings, {
|
||||
config: newConfig,
|
||||
handleChange,
|
||||
})}
|
||||
</section>
|
||||
|
||||
{/* {
|
||||
<ConfigForm
|
||||
type={type}
|
||||
|
||||
config={newConfig}
|
||||
/>
|
||||
} */}
|
||||
</>
|
||||
</Suspense>
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
onClick: () => {
|
||||
handleSave(fieldName, { config: newConfig });
|
||||
|
||||
if (
|
||||
type === FieldType.derivative &&
|
||||
config.script !== newConfig.script
|
||||
) {
|
||||
requestConfirmation({
|
||||
title: "Deploy Changes",
|
||||
body:
|
||||
"Would you like to redeploy the Cloud Function for this table now?",
|
||||
confirm: "Deploy",
|
||||
cancel: "Later",
|
||||
handleConfirm: async () => {
|
||||
const response = await triggerCloudBuild(
|
||||
tableState?.config.tableConfig.path
|
||||
);
|
||||
console.log(response);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
children: "Update",
|
||||
},
|
||||
secondary: {
|
||||
onClick: handleClose,
|
||||
children: "Cancel",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
TextFieldProps,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import { FIELDS, FieldType, FIELD_TYPE_DESCRIPTIONS } from "constants/fields";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { getFieldProp } from "components/fields";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
@@ -46,8 +47,10 @@ export default function FieldsDropdown({
|
||||
const classes = useStyles();
|
||||
|
||||
const options = optionsProp
|
||||
? FIELDS.filter((field) => optionsProp.indexOf(field.type) > -1)
|
||||
: FIELDS;
|
||||
? Object.values(FieldType).filter(
|
||||
(fieldType) => optionsProp.indexOf(fieldType) > -1
|
||||
)
|
||||
: Object.values(FieldType);
|
||||
|
||||
return (
|
||||
<TextField
|
||||
@@ -59,20 +62,20 @@ export default function FieldsDropdown({
|
||||
label={!hideLabel ? "Field Type" : ""}
|
||||
aria-label="Field Type"
|
||||
hiddenLabel={hideLabel}
|
||||
helperText={value && FIELD_TYPE_DESCRIPTIONS[value]}
|
||||
helperText={value && getFieldProp("description", value)}
|
||||
FormHelperTextProps={{ classes: { root: classes.helperText } }}
|
||||
className={className}
|
||||
>
|
||||
{options.map((field) => (
|
||||
{options.map((fieldType) => (
|
||||
<MenuItem
|
||||
key={`select-field-${field.name}`}
|
||||
id={`select-field-${field.type}`}
|
||||
value={field.type}
|
||||
key={`select-field-${getFieldProp("name", fieldType)}`}
|
||||
id={`select-field-${fieldType}`}
|
||||
value={fieldType}
|
||||
>
|
||||
<ListItemIcon className={classes.listItemIcon}>
|
||||
{field.icon}
|
||||
{getFieldProp("icon", fieldType)}
|
||||
</ListItemIcon>
|
||||
{field.name}
|
||||
{getFieldProp("name", fieldType)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
@@ -1,84 +1,49 @@
|
||||
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({
|
||||
import { IMenuModalProps } from ".";
|
||||
|
||||
import { TextField } from "@material-ui/core";
|
||||
|
||||
import StyledModal from "components/StyledModal";
|
||||
|
||||
export default function NameChange({
|
||||
name,
|
||||
fieldName,
|
||||
open,
|
||||
handleClose,
|
||||
handleSave,
|
||||
}: {
|
||||
name: string;
|
||||
fieldName: string;
|
||||
open: boolean;
|
||||
handleClose: Function;
|
||||
handleSave: Function;
|
||||
}) {
|
||||
}: IMenuModalProps) {
|
||||
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>
|
||||
<StyledModal
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
title="Rename Column"
|
||||
maxWidth="xs"
|
||||
children={
|
||||
<TextField
|
||||
value={newName}
|
||||
autoFocus
|
||||
variant="filled"
|
||||
id="name"
|
||||
label="Column Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
onClick: () => handleSave(fieldName, { name: newName }),
|
||||
children: "Update",
|
||||
},
|
||||
secondary: {
|
||||
onClick: handleClose,
|
||||
children: "Cancel",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,49 +1,24 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import _camel from "lodash/camelCase";
|
||||
import { IMenuModalProps } from ".";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
DialogContent,
|
||||
Grid,
|
||||
Button,
|
||||
TextField,
|
||||
DialogActions,
|
||||
} from "@material-ui/core";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
import { makeStyles, createStyles, TextField } from "@material-ui/core";
|
||||
|
||||
import StyledModal from "components/StyledModal";
|
||||
import { FieldType } from "constants/fields";
|
||||
import FieldsDropdown from "./FieldsDropdown";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: { userSelect: "none" },
|
||||
|
||||
closeButton: {
|
||||
position: "absolute",
|
||||
top: theme.spacing(0.5),
|
||||
right: theme.spacing(0.5),
|
||||
},
|
||||
|
||||
content: { paddingBottom: theme.spacing(1.5) },
|
||||
|
||||
helperText: {
|
||||
...theme.typography.body2,
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
|
||||
fieldKey: { fontFamily: theme.typography.fontFamilyMono },
|
||||
})
|
||||
);
|
||||
|
||||
export interface IFormDialogProps {
|
||||
open: boolean;
|
||||
data: any;
|
||||
handleClose: () => void;
|
||||
handleSave: (fieldKey: string, data: any) => void;
|
||||
export interface IFormDialogProps extends IMenuModalProps {
|
||||
data: Record<string, any>;
|
||||
}
|
||||
|
||||
export default function FormDialog({
|
||||
@@ -70,23 +45,15 @@ export default function FormDialog({
|
||||
}, [type]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
<StyledModal
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="add-new-column"
|
||||
title="Add New Column"
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
className={classes.root}
|
||||
>
|
||||
<DialogTitle id="add-new-column">Add New Column</DialogTitle>
|
||||
|
||||
<IconButton onClick={handleClose} className={classes.closeButton}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
|
||||
<DialogContent className={classes.content}>
|
||||
<Grid container spacing={3} direction="column" wrap="nowrap">
|
||||
<Grid item>
|
||||
children={
|
||||
<>
|
||||
<section>
|
||||
<TextField
|
||||
value={columnLabel}
|
||||
autoFocus
|
||||
@@ -99,9 +66,9 @@ export default function FormDialog({
|
||||
helperText="Set the user-facing name for this column."
|
||||
FormHelperTextProps={{ classes: { root: classes.helperText } }}
|
||||
/>
|
||||
</Grid>
|
||||
</section>
|
||||
|
||||
<Grid item>
|
||||
<section>
|
||||
<TextField
|
||||
value={fieldKey}
|
||||
variant="filled"
|
||||
@@ -120,28 +87,19 @@ export default function FormDialog({
|
||||
}
|
||||
FormHelperTextProps={{ classes: { root: classes.helperText } }}
|
||||
/>
|
||||
</Grid>
|
||||
</section>
|
||||
|
||||
<Grid item>
|
||||
<section>
|
||||
<FieldsDropdown
|
||||
value={type}
|
||||
onChange={(newType) => setType(newType.target.value as FieldType)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
onClick: () => {
|
||||
handleSave(fieldKey, {
|
||||
type,
|
||||
name: columnLabel,
|
||||
@@ -150,14 +108,15 @@ export default function FormDialog({
|
||||
config: {},
|
||||
...data.initializeColumn,
|
||||
});
|
||||
}}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
disabled={!columnLabel || !fieldKey || !type}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
},
|
||||
disabled: !columnLabel || !fieldKey || !type,
|
||||
children: "Add",
|
||||
},
|
||||
secondary: {
|
||||
onClick: handleClose,
|
||||
children: "Cancel",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
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) {
|
||||
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;
|
||||
@@ -1,90 +0,0 @@
|
||||
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.trim() !== "") {
|
||||
handleChange([...options, newOption.trim()]);
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,664 +0,0 @@
|
||||
import React, { useState, lazy, Suspense } 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 Slider from "@material-ui/core/Slider";
|
||||
import {
|
||||
Typography,
|
||||
IconButton,
|
||||
TextField,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Divider,
|
||||
} 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 FieldsDropdown from "../FieldsDropdown";
|
||||
import ColumnSelector from "./ConfigFields/ColumnSelector";
|
||||
import FieldSkeleton from "components/SideDrawer/Form/FieldSkeleton";
|
||||
|
||||
//import { ThemedJSONEditor } from "../../../SideDrawer/Form/Fields/JsonEditor";
|
||||
import { triggerCloudBuild } from "../../../../firebase/callables";
|
||||
import { useConfirmation } from "components/ConfirmationDialog";
|
||||
const CodeEditor = lazy(
|
||||
() => import("../../editors/CodeEditor" /* webpackChunkName: "CodeEditor" */)
|
||||
);
|
||||
const ConfigFields = ({
|
||||
fieldType,
|
||||
config,
|
||||
handleChange,
|
||||
tables,
|
||||
columns,
|
||||
roles,
|
||||
}) => {
|
||||
switch (fieldType) {
|
||||
case FieldType.longText:
|
||||
case FieldType.shortText:
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
type="number"
|
||||
value={config.maxLength}
|
||||
label={"Character Limit"}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "0") handleChange("maxLength")(null);
|
||||
else handleChange("maxLength")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
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.connectService:
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
label="Webservice Url"
|
||||
name="url"
|
||||
value={config.url}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
handleChange("url")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Results key Path"
|
||||
name="resultsKey"
|
||||
helperText="Can be specified as a key path"
|
||||
placeholder="data.results"
|
||||
value={config.resultsKey}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
handleChange("resultsKey")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Primary Key"
|
||||
name="primaryKey"
|
||||
value={config.primaryKey}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
handleChange("primaryKey")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Title Key (optional)"
|
||||
name="titleKey"
|
||||
value={config.titleKey}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
handleChange("titleKey")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="SubTitle Key (optional)"
|
||||
name="subtitleKey"
|
||||
value={config.subtitleKey}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
handleChange("subtitleKey")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config.multiple}
|
||||
onChange={() =>
|
||||
handleChange("multiple")(!Boolean(config.multiple))
|
||||
}
|
||||
name="select-multiple"
|
||||
/>
|
||||
}
|
||||
label="Enable multiple item selection"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
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
|
||||
value={config.filters}
|
||||
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.rating:
|
||||
return (
|
||||
<>
|
||||
<Typography variant="overline">Maximum number of stars</Typography>
|
||||
<Slider
|
||||
defaultValue={5}
|
||||
value={config.max}
|
||||
getAriaValueText={(v) => `${v} max stars`}
|
||||
aria-labelledby="max-slider"
|
||||
valueLabelDisplay="auto"
|
||||
onChange={(_, v) => {
|
||||
handleChange("max")(v);
|
||||
}}
|
||||
step={1}
|
||||
marks
|
||||
min={1}
|
||||
max={15}
|
||||
/>
|
||||
<Typography variant="overline">Slider precision stars</Typography>
|
||||
<Slider
|
||||
defaultValue={0.5}
|
||||
value={config.precision}
|
||||
getAriaValueText={(v) => `${v} rating step size`}
|
||||
aria-labelledby="precision-slider"
|
||||
valueLabelDisplay="auto"
|
||||
onChange={(_, v) => {
|
||||
handleChange("precision")(v);
|
||||
}}
|
||||
step={0.25}
|
||||
marks
|
||||
min={0.25}
|
||||
max={1}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case FieldType.action:
|
||||
return (
|
||||
<>
|
||||
<Typography variant="overline">Allowed roles</Typography>
|
||||
<Typography variant="body2">
|
||||
Authenticated user must have at least one of these to run the script
|
||||
</Typography>
|
||||
<MultiSelect
|
||||
label={"Allowed Roles"}
|
||||
options={roles}
|
||||
value={config.requiredRoles ?? []}
|
||||
onChange={handleChange("requiredRoles")}
|
||||
/>
|
||||
|
||||
<Typography variant="overline">Required fields</Typography>
|
||||
<Typography variant="body2">
|
||||
All of the selected fields must have a value for the script to run
|
||||
</Typography>
|
||||
<ColumnSelector
|
||||
label={"Required fields"}
|
||||
value={config.requiredFields}
|
||||
tableColumns={
|
||||
columns
|
||||
? Array.isArray(columns)
|
||||
? columns
|
||||
: Object.values(columns)
|
||||
: []
|
||||
}
|
||||
handleChange={handleChange("requiredFields")}
|
||||
/>
|
||||
<Divider />
|
||||
<Typography variant="overline">Confirmation Template</Typography>
|
||||
<Typography variant="body2">
|
||||
The action button will not ask for confirmation if this is left
|
||||
empty
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label="Confirmation Template"
|
||||
placeholder="Are sure you want to invest {{stockName}}?"
|
||||
value={config.confirmation}
|
||||
onChange={(e) => {
|
||||
handleChange("confirmation")(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config.isActionScript}
|
||||
onChange={() =>
|
||||
handleChange("isActionScript")(
|
||||
!Boolean(config.isActionScript)
|
||||
)
|
||||
}
|
||||
name="actionScript"
|
||||
/>
|
||||
}
|
||||
label="Set as an action script"
|
||||
/>
|
||||
{!Boolean(config.isActionScript) ? (
|
||||
<TextField
|
||||
label="callable name"
|
||||
name="callableName"
|
||||
value={config.callableName}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
handleChange("callableName")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="overline">action script</Typography>
|
||||
<Suspense fallback={<FieldSkeleton height={300} />}>
|
||||
<CodeEditor
|
||||
height={300}
|
||||
script={config.script}
|
||||
extraLibs={[
|
||||
[
|
||||
"declare class ref {",
|
||||
" /**",
|
||||
" * Reference object of the row running the action script",
|
||||
" */",
|
||||
"static id:string",
|
||||
"static path:string",
|
||||
"static parentId:string",
|
||||
"static tablePath:string",
|
||||
"}",
|
||||
].join("\n"),
|
||||
[
|
||||
"declare class actionParams {",
|
||||
" /**",
|
||||
" * actionParams are provided by dialog popup form",
|
||||
" */",
|
||||
(config.params ?? []).map((param) => {
|
||||
if (param) {
|
||||
const validationKeys = Object.keys(param.validation);
|
||||
if (validationKeys.includes("string")) {
|
||||
return `static ${param.name}:string`;
|
||||
} else if (validationKeys.includes("array")) {
|
||||
return `static ${param.name}:any[]`;
|
||||
} else return `static ${param.name}:any`;
|
||||
} else return "";
|
||||
}),
|
||||
"}",
|
||||
],
|
||||
]}
|
||||
handleChange={handleChange("script")}
|
||||
/>
|
||||
</Suspense>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config["redo.enabled"]}
|
||||
onChange={() =>
|
||||
handleChange("redo.enabled")(
|
||||
!Boolean(config["redo.enabled"])
|
||||
)
|
||||
}
|
||||
name="redo toggle"
|
||||
/>
|
||||
}
|
||||
label="enable redo(reruns the same script)"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config["undo.enabled"]}
|
||||
onChange={() =>
|
||||
handleChange("undo.enabled")(
|
||||
!Boolean(config["undo.enabled"])
|
||||
)
|
||||
}
|
||||
name="undo toggle"
|
||||
/>
|
||||
}
|
||||
label="enable undo"
|
||||
/>
|
||||
{config["undo.enabled"] && (
|
||||
<>
|
||||
<Typography variant="overline">
|
||||
Undo Confirmation Template
|
||||
</Typography>
|
||||
<TextField
|
||||
label="template"
|
||||
placeholder="are you sure you want to sell your stocks in {{stockName}}"
|
||||
value={config["undo.confirmation"]}
|
||||
onChange={(e) => {
|
||||
handleChange("undo.confirmation")(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<Typography variant="overline">Undo Action script</Typography>
|
||||
<Suspense fallback={<FieldSkeleton height={300} />}>
|
||||
<CodeEditor
|
||||
height={300}
|
||||
script={config["undo.script"]}
|
||||
handleChange={handleChange("undo.script")}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* <Typography variant="overline">
|
||||
Action Params Configuration
|
||||
</Typography>
|
||||
<ThemedJSONEditor
|
||||
value={config.params}
|
||||
handleEdit={(update) => {
|
||||
console.log({ update });
|
||||
if (Array.isArray(update.new_value)) {
|
||||
handleChange("params")(update.new_value);
|
||||
} else {
|
||||
handleChange("params")([]);
|
||||
}
|
||||
}}
|
||||
/> */}
|
||||
</>
|
||||
);
|
||||
case FieldType.aggregate:
|
||||
return (
|
||||
<>
|
||||
<ColumnSelector
|
||||
label={"Sub Tables"}
|
||||
validTypes={[FieldType.subTable]}
|
||||
value={config.subtables}
|
||||
tableColumns={
|
||||
columns
|
||||
? Array.isArray(columns)
|
||||
? columns
|
||||
: Object.values(columns)
|
||||
: []
|
||||
}
|
||||
handleChange={handleChange("subtables")}
|
||||
/>
|
||||
|
||||
<Typography variant="overline">Aggergate script</Typography>
|
||||
<Suspense fallback={<FieldSkeleton height={200} />}>
|
||||
<CodeEditor
|
||||
script={
|
||||
config.script ??
|
||||
`//triggerType: create | update | delete\n//aggregateState: the subtable accumenlator stored in the cell of this column\n//snapshot: the triggered document snapshot of the the subcollection\n//incrementor: short for firebase.firestore.FieldValue.increment(n);\n//This script needs to return the new aggregateState cell value.
|
||||
switch (triggerType){
|
||||
case "create":return {
|
||||
count:incrementor(1)
|
||||
}
|
||||
case "update":return {}
|
||||
case "delete":
|
||||
return {
|
||||
count:incrementor(-1)
|
||||
}
|
||||
}`
|
||||
}
|
||||
extraLibs={[
|
||||
` /**
|
||||
* increaments firestore field value
|
||||
*/",
|
||||
function incrementor(value:number):number {
|
||||
|
||||
}`,
|
||||
]}
|
||||
handleChange={handleChange("script")}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<Typography variant="overline">Field type of the output</Typography>
|
||||
<FieldsDropdown
|
||||
value={config.renderFieldType}
|
||||
options={Object.values(FieldType).filter(
|
||||
(f) =>
|
||||
![
|
||||
FieldType.derivative,
|
||||
FieldType.aggregate,
|
||||
FieldType.subTable,
|
||||
FieldType.action,
|
||||
].includes(f)
|
||||
)}
|
||||
onChange={(newType: any) => {
|
||||
handleChange("renderFieldType")(newType.target.value);
|
||||
}}
|
||||
/>
|
||||
{config.renderFieldType && (
|
||||
<>
|
||||
<Typography variant="overline">Rendered field config</Typography>
|
||||
<ConfigFields
|
||||
fieldType={config.renderFieldType}
|
||||
config={config}
|
||||
handleChange={handleChange}
|
||||
tables={tables}
|
||||
columns={columns}
|
||||
roles={roles}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
case FieldType.derivative:
|
||||
return (
|
||||
<>
|
||||
<ColumnSelector
|
||||
label={
|
||||
"Listener fields (this script runs when these fields change)"
|
||||
}
|
||||
value={config.listenerFields}
|
||||
tableColumns={
|
||||
columns
|
||||
? Array.isArray(columns)
|
||||
? columns
|
||||
: Object.values(columns)
|
||||
: []
|
||||
}
|
||||
handleChange={handleChange("listenerFields")}
|
||||
/>
|
||||
|
||||
<Typography variant="overline">derivative script</Typography>
|
||||
<Suspense fallback={<FieldSkeleton height={200} />}>
|
||||
<CodeEditor
|
||||
script={config.script}
|
||||
handleChange={handleChange("script")}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<Typography variant="overline">Field type of the output</Typography>
|
||||
<FieldsDropdown
|
||||
value={config.renderFieldType}
|
||||
options={Object.values(FieldType).filter(
|
||||
(f) =>
|
||||
![
|
||||
FieldType.derivative,
|
||||
FieldType.aggregate,
|
||||
FieldType.subTable,
|
||||
FieldType.action,
|
||||
].includes(f)
|
||||
)}
|
||||
onChange={(newType: any) => {
|
||||
handleChange("renderFieldType")(newType.target.value);
|
||||
}}
|
||||
/>
|
||||
{config.renderFieldType && (
|
||||
<>
|
||||
<Typography variant="overline"> Rendered field config</Typography>
|
||||
<ConfigFields
|
||||
fieldType={config.renderFieldType}
|
||||
config={config}
|
||||
handleChange={handleChange}
|
||||
tables={tables}
|
||||
columns={columns}
|
||||
roles={roles}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
const ConfigForm = ({ type, config, handleChange }) => {
|
||||
const { tableState, tables, roles } = useFiretableContext();
|
||||
|
||||
if (!tableState) return <></>;
|
||||
const { columns } = tableState;
|
||||
|
||||
return (
|
||||
<ConfigFields
|
||||
fieldType={type}
|
||||
columns={columns}
|
||||
config={config}
|
||||
handleChange={handleChange}
|
||||
tables={tables}
|
||||
roles={roles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 ?? {});
|
||||
const { requestConfirmation } = useConfirmation();
|
||||
const { tableState, tableActions } = useFiretableContext();
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
maxWidth="xl"
|
||||
open={open}
|
||||
onClose={(e, r) => {
|
||||
handleClose();
|
||||
}}
|
||||
aria-labelledby="form-column-settings"
|
||||
>
|
||||
<DialogContent>
|
||||
<Grid
|
||||
style={{ minWidth: 450 }}
|
||||
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) => {
|
||||
setNewConfig({ ...newConfig, [key]: update });
|
||||
}}
|
||||
config={newConfig}
|
||||
/>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSave(fieldName, { config: newConfig });
|
||||
if (
|
||||
type === FieldType.derivative &&
|
||||
config.script !== newConfig.script
|
||||
) {
|
||||
requestConfirmation({
|
||||
title: "Deploy Changes",
|
||||
body:
|
||||
"Would you like to redeploy the cloud function for this table now?",
|
||||
confirm: "Deploy",
|
||||
cancel: "later",
|
||||
handleConfirm: async () => {
|
||||
const response = await triggerCloudBuild(
|
||||
tableState?.config.tableConfig.path
|
||||
);
|
||||
console.log(response);
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
www/src/components/Table/ColumnMenu/Subheading.tsx
Normal file
21
www/src/components/Table/ColumnMenu/Subheading.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
|
||||
import { useTheme, Typography, TypographyProps } from "@material-ui/core";
|
||||
|
||||
export default function Subheading(props: TypographyProps<"h2">) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Typography
|
||||
variant="overline"
|
||||
display="block"
|
||||
gutterBottom
|
||||
component="h3"
|
||||
{...props}
|
||||
style={{
|
||||
color: theme.palette.text.disabled,
|
||||
...props.style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,83 +1,41 @@
|
||||
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 { IMenuModalProps } from ".";
|
||||
import StyledModal from "components/StyledModal";
|
||||
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;
|
||||
}) {
|
||||
}: IMenuModalProps) {
|
||||
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>
|
||||
return (
|
||||
<StyledModal
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
title="Change Column Type"
|
||||
children={
|
||||
<FieldsDropdown
|
||||
value={newType}
|
||||
onChange={(newType: any) => {
|
||||
setType(newType.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
onClick: () => handleSave(fieldName, { type: newType }),
|
||||
children: "Update",
|
||||
},
|
||||
secondary: {
|
||||
onClick: handleClose,
|
||||
children: "Cancel",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,10 +21,11 @@ import MenuContents from "./MenuContents";
|
||||
import NameChange from "./NameChange";
|
||||
import NewColumn from "./NewColumn";
|
||||
import TypeChange from "./TypeChange";
|
||||
import Settings from "./Settings";
|
||||
import FieldSettings from "./FieldSettings";
|
||||
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
import { FIELDS, FieldType } from "constants/fields";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { getFieldProp } from "components/fields";
|
||||
import _find from "lodash/find";
|
||||
import { Column } from "react-data-grid";
|
||||
import { PopoverProps } from "@material-ui/core";
|
||||
@@ -49,6 +50,18 @@ export type ColumnMenuRef = {
|
||||
>;
|
||||
};
|
||||
|
||||
export interface IMenuModalProps {
|
||||
name: string;
|
||||
fieldName: string;
|
||||
type: FieldType;
|
||||
|
||||
open: boolean;
|
||||
config: Record<string, any>;
|
||||
|
||||
handleClose: () => void;
|
||||
handleSave: (fieldName: string, config: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
paper: {
|
||||
@@ -99,9 +112,26 @@ export default function ColumnMenu() {
|
||||
setTimeout(() => setSelectedColumnHeader(null), 300);
|
||||
};
|
||||
|
||||
const isConfigurable = Boolean(
|
||||
getFieldProp("settings", column?.type) ||
|
||||
getFieldProp("initializable", column?.type)
|
||||
);
|
||||
|
||||
if (!column) return null;
|
||||
const isSorted = orderBy?.[0]?.key === (column.key as string);
|
||||
|
||||
const isSorted = orderBy?.[0]?.key === column.key;
|
||||
const isAsc = isSorted && orderBy?.[0]?.direction === "asc";
|
||||
|
||||
const clearModal = () => {
|
||||
setModal(INITIAL_MODAL);
|
||||
setTimeout(() => handleClose(), 300);
|
||||
};
|
||||
|
||||
const handleModalSave = (key: string, update: Record<string, any>) => {
|
||||
actions.update(key, update);
|
||||
clearModal();
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
type: "subheader",
|
||||
@@ -118,17 +148,6 @@ export default function ColumnMenu() {
|
||||
},
|
||||
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",
|
||||
@@ -185,9 +204,9 @@ export default function ColumnMenu() {
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Edit Type: ${column?.type}`,
|
||||
label: `Edit Type: ${getFieldProp("name", column.type)}`,
|
||||
// This is based off the cell type
|
||||
icon: _find(FIELDS, { type: column.type })?.icon,
|
||||
icon: getFieldProp("icon", column.type),
|
||||
onClick: () => {
|
||||
setModal({ type: ModalStates.typeChange, data: { column } });
|
||||
},
|
||||
@@ -199,6 +218,7 @@ export default function ColumnMenu() {
|
||||
onClick: () => {
|
||||
setModal({ type: ModalStates.settings, data: { column } });
|
||||
},
|
||||
disabled: !isConfigurable,
|
||||
},
|
||||
// {
|
||||
// label: "Re-order",
|
||||
@@ -227,6 +247,18 @@ export default function ColumnMenu() {
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Hide for Everyone",
|
||||
activeLabel: "Show",
|
||||
icon: <VisibilityOffIcon />,
|
||||
activeIcon: <VisibilityIcon />,
|
||||
onClick: () => {
|
||||
actions.update(column.key, { hidden: !column.hidden });
|
||||
handleClose();
|
||||
},
|
||||
active: column.hidden,
|
||||
color: "error" as "error",
|
||||
},
|
||||
{
|
||||
label: "Delete Column",
|
||||
icon: <ColumnRemoveIcon />,
|
||||
@@ -238,9 +270,16 @@ export default function ColumnMenu() {
|
||||
},
|
||||
];
|
||||
|
||||
const clearModal = () => {
|
||||
setModal(INITIAL_MODAL);
|
||||
setSelectedColumnHeader(null);
|
||||
const menuModalProps = {
|
||||
name: column.name,
|
||||
fieldName: column.key,
|
||||
type: column.type,
|
||||
|
||||
open: modal.type === ModalStates.typeChange,
|
||||
config: column.config,
|
||||
|
||||
handleClose: clearModal,
|
||||
handleSave: handleModalSave,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -264,51 +303,21 @@ export default function ColumnMenu() {
|
||||
{column && (
|
||||
<>
|
||||
<NameChange
|
||||
name={column.name}
|
||||
fieldName={column.key as string}
|
||||
{...menuModalProps}
|
||||
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}
|
||||
{...menuModalProps}
|
||||
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}
|
||||
<FieldSettings
|
||||
{...menuModalProps}
|
||||
open={modal.type === ModalStates.settings}
|
||||
type={column.type}
|
||||
handleClose={clearModal}
|
||||
handleSave={(key, update) => {
|
||||
actions.update(key, update);
|
||||
clearModal();
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
<NewColumn
|
||||
{...menuModalProps}
|
||||
open={modal.type === ModalStates.new}
|
||||
data={modal.data}
|
||||
handleClose={clearModal}
|
||||
handleSave={(key, update) => {
|
||||
actions.update(key, update);
|
||||
clearModal();
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -250,6 +250,7 @@ const Filters = () => {
|
||||
return (
|
||||
<MultiSelect
|
||||
multiple
|
||||
max={10}
|
||||
freeText={true}
|
||||
onChange={(value) => setQuery((query) => ({ ...query, value }))}
|
||||
options={
|
||||
@@ -287,6 +288,7 @@ const Filters = () => {
|
||||
multiple
|
||||
onChange={(value) => setQuery((query) => ({ ...query, value }))}
|
||||
value={query.value as string[]}
|
||||
max={10}
|
||||
options={
|
||||
selectedColumn.config.options
|
||||
? selectedColumn.config.options.sort()
|
||||
|
||||
@@ -18,12 +18,11 @@ import Filters from "../Filters";
|
||||
import ImportCSV from "./ImportCsv";
|
||||
import ExportCSV from "./ExportCsv";
|
||||
import TableSettings from "./TableSettings";
|
||||
|
||||
import HiddenFields from "../HiddenFields";
|
||||
import Sparks from "./Sparks";
|
||||
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
import { FieldType } from "constants/fields";
|
||||
import HiddenFields from "../HiddenFields";
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { useFiretableContext, firetableUser } from "contexts/FiretableContext";
|
||||
|
||||
export const TABLE_HEADER_HEIGHT = 56;
|
||||
|
||||
@@ -81,14 +80,12 @@ export default function TableHeader({
|
||||
updateConfig,
|
||||
}: ITableHeaderProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const { currentUser } = useAppContext();
|
||||
const { tableActions, tableState, userClaims } = useFiretableContext();
|
||||
|
||||
if (!tableState || !tableState.columns) return null;
|
||||
const { columns } = tableState;
|
||||
|
||||
const needsMigration = Array.isArray(columns) && columns.length !== 0;
|
||||
const tempColumns = needsMigration ? columns : Object.values(columns);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
@@ -101,14 +98,24 @@ export default function TableHeader({
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const initialVal = tempColumns.reduce((acc, currCol) => {
|
||||
if (currCol.type === FieldType.checkbox) {
|
||||
return { ...acc, [currCol.key]: false };
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, {});
|
||||
tableActions?.row.add(initialVal);
|
||||
const initialVal = Object.values(columns).reduce(
|
||||
(acc, column) => {
|
||||
if (column.config?.defaultValue?.type === "static") {
|
||||
return {
|
||||
...acc,
|
||||
[column.key]: column.config.defaultValue.value,
|
||||
};
|
||||
} else if (column.config?.defaultValue?.type === "null") {
|
||||
return { ...acc, [column.key]: null };
|
||||
} else return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
tableActions?.row.add({
|
||||
...initialVal,
|
||||
_ft_updatedBy: firetableUser(currentUser),
|
||||
_ft_createdBy: firetableUser(currentUser),
|
||||
});
|
||||
}}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { EditorProps } from "react-data-grid";
|
||||
import _findIndex from "lodash/findIndex";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
|
||||
import { makeStyles } from "@material-ui/core";
|
||||
import styles from "./styles";
|
||||
|
||||
const useStyles = makeStyles(styles);
|
||||
|
||||
/**
|
||||
* Allow the cell to be editable, but disable react-data-grid’s default
|
||||
* text editor to show. Opens the side drawer in the appropriate position.
|
||||
*
|
||||
* Hides the editor container so the cell below remains interactive inline.
|
||||
*
|
||||
* Use for cells that do not support any type of in-cell editing.
|
||||
*/
|
||||
function SideDrawerEditor_(props: EditorProps<any, any>) {
|
||||
useStyles();
|
||||
|
||||
const {
|
||||
column, // rowData
|
||||
} = props;
|
||||
const rowData = {};
|
||||
const { sideDrawerRef } = useFiretableContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (!sideDrawerRef?.current?.open && sideDrawerRef?.current?.setOpen)
|
||||
sideDrawerRef?.current?.setOpen(true);
|
||||
}, [column, rowData]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* react-data-grid requires the Editor component to be a class component
|
||||
* with getInputNode and getValue methods.
|
||||
*/
|
||||
class SideDrawerEditor extends React.Component<EditorProps<any, any>> {
|
||||
getInputNode = () => null;
|
||||
getValue = () => null;
|
||||
render = () => <SideDrawerEditor_ {...this.props} />;
|
||||
}
|
||||
|
||||
export default SideDrawerEditor;
|
||||
@@ -35,10 +35,11 @@ const useStyles = makeStyles((theme) =>
|
||||
export default function TextEditor({ row, column }: EditorProps<any>) {
|
||||
const classes = useStyles();
|
||||
|
||||
const type = (column as any).config?.renderFieldType ?? (column as any).type;
|
||||
|
||||
const cellValue = getCellValue(row, column.key);
|
||||
const defaultValue =
|
||||
(column as any).type === FieldType.percentage &&
|
||||
typeof cellValue === "number"
|
||||
type === FieldType.percentage && typeof cellValue === "number"
|
||||
? cellValue * 100
|
||||
: cellValue;
|
||||
|
||||
@@ -48,10 +49,7 @@ export default function TextEditor({ row, column }: EditorProps<any>) {
|
||||
return () => {
|
||||
const newValue = inputRef.current?.value;
|
||||
if (newValue !== undefined) {
|
||||
if (
|
||||
(column as any).type === FieldType.number ||
|
||||
(column as any).type === FieldType.percentage
|
||||
) {
|
||||
if (type === FieldType.number || type === FieldType.percentage) {
|
||||
row.ref.update({ [column.key]: Number(newValue) });
|
||||
} else {
|
||||
row.ref.update({ [column.key]: newValue });
|
||||
@@ -61,7 +59,7 @@ export default function TextEditor({ row, column }: EditorProps<any>) {
|
||||
}, []);
|
||||
|
||||
let inputType = "text";
|
||||
switch ((column as any).type) {
|
||||
switch (type) {
|
||||
case FieldType.email:
|
||||
inputType = "email";
|
||||
break;
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { FieldType } from "constants/fields";
|
||||
|
||||
import NullEditor from "./NullEditor";
|
||||
import SideDrawerEditor from "./SideDrawerEditor";
|
||||
import TextEditor from "./TextEditor";
|
||||
|
||||
/**
|
||||
* Gets the corresponding editor for each cell. Either:
|
||||
* - displays the default react-data-grid text editor,
|
||||
* - can be edited without double-clicking, or
|
||||
* - must be edited in the side drawer
|
||||
* @param column Must have column `type`
|
||||
*/
|
||||
export const getEditor = (column: any) => {
|
||||
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:
|
||||
case FieldType.checkbox:
|
||||
case FieldType.rating:
|
||||
case FieldType.image:
|
||||
case FieldType.file:
|
||||
case FieldType.singleSelect:
|
||||
case FieldType.multiSelect:
|
||||
case FieldType.color:
|
||||
return NullEditor;
|
||||
|
||||
// Can be edited without double-clicking; side drawer editor not implemented
|
||||
case FieldType.subTable:
|
||||
return NullEditor;
|
||||
|
||||
// Supports react-data-grid’s in-cell editing
|
||||
case FieldType.shortText:
|
||||
case FieldType.email:
|
||||
case FieldType.phone:
|
||||
case FieldType.url:
|
||||
case FieldType.number:
|
||||
case FieldType.percentage:
|
||||
return TextEditor;
|
||||
|
||||
// No in-cell editing; must open side drawer
|
||||
case FieldType.longText:
|
||||
case FieldType.richText:
|
||||
case FieldType.slider:
|
||||
case FieldType.json:
|
||||
case FieldType.connectTable:
|
||||
case FieldType.action:
|
||||
case FieldType.id:
|
||||
default:
|
||||
return SideDrawerEditor;
|
||||
}
|
||||
};
|
||||
40
www/src/components/Table/editors/withSideDrawerEditor.tsx
Normal file
40
www/src/components/Table/editors/withSideDrawerEditor.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { EditorProps } from "react-data-grid";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
import { IHeavyCellProps } from "components/fields/types";
|
||||
|
||||
import { getCellValue } from "utils/fns";
|
||||
|
||||
/**
|
||||
* Allow the cell to be editable, but disable react-data-grid’s default
|
||||
* text editor to show. Opens the side drawer in the appropriate position.
|
||||
*
|
||||
* Displays the current HeavyCell or HeavyCell since it overwrites cell contents.
|
||||
*
|
||||
* Use for cells that do not support any type of in-cell editing.
|
||||
*/
|
||||
export default function withSideDrawerEditor(
|
||||
HeavyCell?: React.ComponentType<IHeavyCellProps>
|
||||
) {
|
||||
return function SideDrawerEditor(props: EditorProps<any, any>) {
|
||||
const { row, column } = props;
|
||||
const { sideDrawerRef } = useFiretableContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (!sideDrawerRef?.current?.open && sideDrawerRef?.current?.setOpen)
|
||||
sideDrawerRef?.current?.setOpen(true);
|
||||
}, [column]);
|
||||
|
||||
return HeavyCell ? (
|
||||
<HeavyCell
|
||||
{...(props as any)}
|
||||
value={getCellValue(row, column.key)}
|
||||
name={column.name}
|
||||
type={(column as any).type}
|
||||
docRef={props.row.ref}
|
||||
onSubmit={() => {}}
|
||||
disabled={props.column.editable === false}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import React from "react";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
// Tooltip, Fade
|
||||
} from "@material-ui/core";
|
||||
|
||||
// import { useFiretableContext } from "contexts/FiretableContext";
|
||||
|
||||
// type StylesProps = { width: number; rowHeight: number };
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: "100%",
|
||||
maxHeight: "100%",
|
||||
padding: theme.spacing(0.5, 0),
|
||||
|
||||
whiteSpace: "pre-line",
|
||||
lineHeight: theme.typography.body2.lineHeight,
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
},
|
||||
|
||||
// tooltip: ({ width, rowHeight }: StylesProps) => ({
|
||||
// margin: `-${rowHeight - 1}px 0 0 -${theme.spacing(1.5)}px`,
|
||||
// padding: theme.spacing(0.5, 1.5),
|
||||
|
||||
// width: width - 1,
|
||||
// maxWidth: "none",
|
||||
// minHeight: rowHeight - 1,
|
||||
// overflowX: "hidden",
|
||||
|
||||
// background: theme.palette.background.paper,
|
||||
// borderRadius: 0,
|
||||
// boxShadow: theme.shadows[4],
|
||||
|
||||
// ...theme.typography.body2,
|
||||
// fontFamily: theme.typography.fontFamilyMono,
|
||||
// fontSize: "0.75rem",
|
||||
// color: theme.palette.text.primary,
|
||||
// whiteSpace: "pre-line",
|
||||
|
||||
// display: "flex",
|
||||
// alignItems: "center",
|
||||
// }),
|
||||
})
|
||||
);
|
||||
|
||||
export default function LongText({
|
||||
// column,
|
||||
value,
|
||||
}: CustomCellProps) {
|
||||
// const { tableState } = useFiretableContext();
|
||||
const classes = useStyles();
|
||||
// const classes = useStyles({
|
||||
// width: column.width,
|
||||
// rowHeight: tableState?.config?.rowHeight ?? 44,
|
||||
// });
|
||||
|
||||
if (!value || value === "") return null;
|
||||
|
||||
return (
|
||||
// <Tooltip
|
||||
// title={value}
|
||||
// enterDelay={1000}
|
||||
// placement="bottom-start"
|
||||
// PopperProps={{
|
||||
// modifiers: {
|
||||
// flip: { enabled: false },
|
||||
// preventOverflow: {
|
||||
// enabled: false,
|
||||
// boundariesElement: "scrollParent",
|
||||
// },
|
||||
// hide: { enabled: false },
|
||||
// },
|
||||
// }}
|
||||
// TransitionComponent={Fade}
|
||||
// classes={{ tooltip: classes.tooltip }}
|
||||
// >
|
||||
<div className={classes.root}>{value}</div>
|
||||
// </Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
import { ChromePicker } from "react-color";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
Grid,
|
||||
ButtonBase,
|
||||
Popover,
|
||||
} from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {},
|
||||
|
||||
colorIndicator: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
|
||||
boxShadow: `0 0 0 1px ${theme.palette.text.disabled} inset`,
|
||||
borderRadius: theme.shape.borderRadius / 2,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function Color({ value, onSubmit }: CustomCellProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const toggleOpen = (e: React.MouseEvent<HTMLElement>) =>
|
||||
setAnchorEl((s) => (s ? null : e.currentTarget));
|
||||
const handleClose = () => setAnchorEl(null);
|
||||
|
||||
const handleChangeComplete = (color) => onSubmit(color);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
container
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
className={classes.root}
|
||||
onDoubleClick={toggleOpen}
|
||||
>
|
||||
<Grid item>
|
||||
<ButtonBase
|
||||
className={classes.colorIndicator}
|
||||
style={{ backgroundColor: value?.hex }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleOpen(e);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs>
|
||||
{value?.hex}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Popover
|
||||
open={!!anchorEl}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "left" }}
|
||||
marginThreshold={12}
|
||||
>
|
||||
<ChromePicker
|
||||
color={value?.rgb}
|
||||
onChangeComplete={handleChangeComplete}
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
import _get from "lodash/get";
|
||||
import { createStyles, makeStyles, Grid, Chip } from "@material-ui/core";
|
||||
|
||||
import ConnectServiceSelect from "components/ConnectServiceSelect";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
minWidth: 0,
|
||||
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
disabled: {},
|
||||
|
||||
fullHeight: {
|
||||
height: "100%",
|
||||
font: "inherit",
|
||||
color: "inherit",
|
||||
letterSpacing: "inherit",
|
||||
},
|
||||
|
||||
select: {
|
||||
padding: theme.spacing(0, 3, 0, 1.5),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
|
||||
"&&": { paddingRight: theme.spacing(4) },
|
||||
|
||||
"$disabled &": { paddingRight: theme.spacing(0) },
|
||||
},
|
||||
icon: {
|
||||
marginRight: theme.spacing(1),
|
||||
"$disabled &": { display: "none" },
|
||||
},
|
||||
|
||||
chipList: {
|
||||
overflowX: "hidden",
|
||||
width: "100%",
|
||||
},
|
||||
chip: { cursor: "inherit" },
|
||||
})
|
||||
);
|
||||
|
||||
export default function ConnectService({
|
||||
rowIdx,
|
||||
column,
|
||||
value,
|
||||
onSubmit,
|
||||
row,
|
||||
docRef,
|
||||
}: CustomCellProps) {
|
||||
const classes = useStyles();
|
||||
const { config } = column as any;
|
||||
const { dataGridRef } = useFiretableContext();
|
||||
if (!config) return <></>;
|
||||
const disabled = !column.editable || config?.isLocked;
|
||||
const titleKey = config.titleKey ?? config.primaryKey;
|
||||
// Render chips
|
||||
const renderValue = (value) => (
|
||||
<Grid container spacing={1} wrap="nowrap" className={classes.chipList}>
|
||||
{value?.map((doc: any) => (
|
||||
<Grid item key={_get(doc, config.primaryKey)}>
|
||||
<Chip label={_get(doc, titleKey)} className={classes.chip} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const onClick = (e) => e.stopPropagation();
|
||||
const onClose = () => {
|
||||
if (dataGridRef?.current?.selectCell)
|
||||
dataGridRef.current.selectCell({ rowIdx, idx: column.idx });
|
||||
};
|
||||
|
||||
return (
|
||||
<ConnectServiceSelect
|
||||
row={row}
|
||||
value={value}
|
||||
onChange={onSubmit}
|
||||
config={config}
|
||||
docRef={docRef}
|
||||
TextFieldProps={{
|
||||
fullWidth: true,
|
||||
label: "",
|
||||
hiddenLabel: true,
|
||||
variant: "standard" as "filled",
|
||||
InputProps: {
|
||||
disableUnderline: true,
|
||||
classes: { root: classes.fullHeight },
|
||||
},
|
||||
SelectProps: {
|
||||
onClose,
|
||||
classes: {
|
||||
root: clsx(classes.fullHeight, classes.select),
|
||||
icon: clsx(classes.icon),
|
||||
},
|
||||
renderValue,
|
||||
MenuProps: {
|
||||
anchorOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
transformOrigin: { vertical: "top", horizontal: "left" },
|
||||
},
|
||||
},
|
||||
onClick,
|
||||
disabled,
|
||||
}}
|
||||
className={clsx(
|
||||
classes.fullHeight,
|
||||
classes.root,
|
||||
disabled && classes.disabled
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
|
||||
import { createStyles, makeStyles, Grid, Chip } from "@material-ui/core";
|
||||
|
||||
import ConnectTableSelect from "components/ConnectTableSelect";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
minWidth: 0,
|
||||
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
disabled: {},
|
||||
|
||||
fullHeight: {
|
||||
height: "100%",
|
||||
font: "inherit",
|
||||
color: "inherit",
|
||||
letterSpacing: "inherit",
|
||||
},
|
||||
|
||||
select: {
|
||||
padding: theme.spacing(0, 3, 0, 1.5),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
|
||||
"&&": { paddingRight: theme.spacing(4) },
|
||||
|
||||
"$disabled &": { paddingRight: theme.spacing(0) },
|
||||
},
|
||||
icon: {
|
||||
marginRight: theme.spacing(1),
|
||||
"$disabled &": { display: "none" },
|
||||
},
|
||||
|
||||
chipList: {
|
||||
overflowX: "hidden",
|
||||
width: "100%",
|
||||
},
|
||||
chip: { cursor: "inherit" },
|
||||
})
|
||||
);
|
||||
|
||||
export default function ConnectTable({
|
||||
rowIdx,
|
||||
column,
|
||||
value,
|
||||
onSubmit,
|
||||
row,
|
||||
}: CustomCellProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const { collectionPath, config } = column as any;
|
||||
const { dataGridRef } = useFiretableContext();
|
||||
if (!config || !config.primaryKeys) return <></>;
|
||||
const disabled = column.editable === false || config?.isLocked;
|
||||
|
||||
// Render chips
|
||||
const renderValue = () => (
|
||||
<Grid container spacing={1} wrap="nowrap" className={classes.chipList}>
|
||||
{Array.isArray(value) &&
|
||||
value.map((doc: any) => (
|
||||
<Grid item key={doc.docPath}>
|
||||
<Chip
|
||||
label={config.primaryKeys
|
||||
.map((key: string) => doc.snapshot[key])
|
||||
.join(" ")}
|
||||
className={classes.chip}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const onClick = (e) => e.stopPropagation();
|
||||
const onClose = () => {
|
||||
if (dataGridRef?.current?.selectCell)
|
||||
dataGridRef.current.selectCell({ rowIdx, idx: column.idx });
|
||||
};
|
||||
|
||||
return (
|
||||
<ConnectTableSelect
|
||||
row={row}
|
||||
column={column}
|
||||
value={value}
|
||||
onChange={onSubmit}
|
||||
collectionPath={collectionPath}
|
||||
config={config}
|
||||
editable={column.editable as boolean}
|
||||
TextFieldProps={{
|
||||
fullWidth: true,
|
||||
label: "",
|
||||
hiddenLabel: true,
|
||||
variant: "standard" as "filled",
|
||||
InputProps: {
|
||||
disableUnderline: true,
|
||||
classes: { root: classes.fullHeight },
|
||||
},
|
||||
SelectProps: {
|
||||
onClose,
|
||||
classes: {
|
||||
root: clsx(classes.fullHeight, classes.select),
|
||||
icon: clsx(classes.icon),
|
||||
},
|
||||
renderValue,
|
||||
MenuProps: {
|
||||
anchorOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
transformOrigin: { vertical: "top", horizontal: "left" },
|
||||
},
|
||||
},
|
||||
onClick,
|
||||
}}
|
||||
className={clsx(
|
||||
classes.fullHeight,
|
||||
classes.root,
|
||||
disabled && classes.disabled
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
import { makeStyles, createStyles } from "@material-ui/core";
|
||||
|
||||
export const timeDistance = (date1, date2) => {
|
||||
let distance = Math.abs(date1 - date2);
|
||||
const hours = Math.floor(distance / 3600000);
|
||||
distance -= hours * 3600000;
|
||||
const minutes = Math.floor(distance / 60000);
|
||||
distance -= minutes * 60000;
|
||||
const seconds = Math.floor(distance / 1000);
|
||||
return `${hours ? `${hours}:` : ""}${("0" + minutes).slice(-2)}:${(
|
||||
"0" + seconds
|
||||
).slice(-2)}`;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
height: "100%",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function Duration({
|
||||
rowIdx,
|
||||
column,
|
||||
value,
|
||||
onSubmit,
|
||||
}: CustomCellProps) {
|
||||
const classes = useStyles();
|
||||
const startDate = value?.start?.toDate();
|
||||
const endDate = value?.end?.toDate();
|
||||
if (!startDate && !endDate) {
|
||||
return <></>;
|
||||
}
|
||||
if (startDate && !endDate) {
|
||||
const now = new Date();
|
||||
const duration = timeDistance(startDate, now);
|
||||
|
||||
return <>{duration}</>;
|
||||
}
|
||||
if (startDate && endDate) {
|
||||
const duration = timeDistance(endDate, startDate);
|
||||
|
||||
return <>{duration}</>;
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from "react";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
|
||||
import { useTheme } from "@material-ui/core";
|
||||
|
||||
export default function Id({ docRef }: CustomCellProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<span style={{ fontFamily: theme.typography.fontFamilyMono }}>
|
||||
{docRef.id}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import React from "react";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
import jsonFormat from "json-format";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
// Tooltip, Fade
|
||||
} from "@material-ui/core";
|
||||
|
||||
// import { useFiretableContext } from "contexts/FiretableContext";
|
||||
|
||||
// type StylesProps = { width: number; rowHeight: number };
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: "100%",
|
||||
maxHeight: "100%",
|
||||
padding: theme.spacing(0.5, 0),
|
||||
|
||||
whiteSpace: "pre-line",
|
||||
lineHeight: theme.typography.body2.lineHeight,
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
wordBreak: "break-word",
|
||||
},
|
||||
|
||||
// tooltip: ({ width, rowHeight }: StylesProps) => ({
|
||||
// margin: `-${rowHeight - 1}px 0 0 -${theme.spacing(1.5)}px`,
|
||||
// padding: theme.spacing(0.5, 1.5),
|
||||
|
||||
// width: width - 1,
|
||||
// maxWidth: "none",
|
||||
// minHeight: rowHeight - 1,
|
||||
// overflowX: "scroll",
|
||||
|
||||
// background: theme.palette.background.paper,
|
||||
// borderRadius: 0,
|
||||
// boxShadow: theme.shadows[4],
|
||||
|
||||
// ...theme.typography.body2,
|
||||
// fontSize: "0.75rem",
|
||||
// color: theme.palette.text.primary,
|
||||
// fontFamily: theme.typography.fontFamilyMono,
|
||||
// wordBreak: "break-word",
|
||||
// whiteSpace: "pre-wrap",
|
||||
|
||||
// display: "flex",
|
||||
// alignItems: "center",
|
||||
// }),
|
||||
})
|
||||
);
|
||||
|
||||
export default function LongText({
|
||||
// column,
|
||||
value,
|
||||
}: CustomCellProps) {
|
||||
// const { tableState } = useFiretableContext();
|
||||
const classes = useStyles();
|
||||
// const classes = useStyles({
|
||||
// width: column.width,
|
||||
// rowHeight: tableState?.config?.rowHeight ?? 44,
|
||||
// });
|
||||
|
||||
if (!value) return null;
|
||||
|
||||
const formattedJson = jsonFormat(value, {
|
||||
type: "space",
|
||||
char: " ",
|
||||
size: 2,
|
||||
});
|
||||
|
||||
return (
|
||||
// <Tooltip
|
||||
// title={formattedJson}
|
||||
// enterDelay={1000}
|
||||
// onClick={(e) => e.stopPropagation()}
|
||||
// placement="bottom-start"
|
||||
// PopperProps={{
|
||||
// modifiers: {
|
||||
// flip: { enabled: false },
|
||||
// preventOverflow: {
|
||||
// enabled: false,
|
||||
// boundariesElement: "scrollParent",
|
||||
// },
|
||||
// hide: { enabled: false },
|
||||
// },
|
||||
// }}
|
||||
// TransitionComponent={Fade}
|
||||
// classes={{ tooltip: classes.tooltip }}
|
||||
// >
|
||||
<div className={classes.root}>{formattedJson}</div>
|
||||
// </Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import React from "react";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
// Tooltip, Fade
|
||||
} from "@material-ui/core";
|
||||
|
||||
// import { useFiretableContext } from "contexts/FiretableContext";
|
||||
|
||||
// type StylesProps = { width: number; rowHeight: number };
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: "100%",
|
||||
maxHeight: "100%",
|
||||
padding: theme.spacing(0.5, 0),
|
||||
|
||||
whiteSpace: "pre-line",
|
||||
lineHeight: theme.typography.body2.lineHeight,
|
||||
},
|
||||
|
||||
// tooltip: ({ width, rowHeight }: StylesProps) => ({
|
||||
// margin: `-${rowHeight - 1}px 0 0 -${theme.spacing(1.5)}px`,
|
||||
// padding: theme.spacing(0.5, 1.5),
|
||||
|
||||
// width: width - 1,
|
||||
// maxWidth: "none",
|
||||
// minHeight: rowHeight - 1,
|
||||
// overflowX: "hidden",
|
||||
|
||||
// background: theme.palette.background.paper,
|
||||
// borderRadius: 0,
|
||||
// boxShadow: theme.shadows[4],
|
||||
|
||||
// ...theme.typography.body2,
|
||||
// fontSize: "0.75rem",
|
||||
// color: theme.palette.text.primary,
|
||||
// whiteSpace: "pre-line",
|
||||
|
||||
// display: "flex",
|
||||
// alignItems: "center",
|
||||
// }),
|
||||
})
|
||||
);
|
||||
|
||||
export default function LongText({
|
||||
// column,
|
||||
value,
|
||||
}: CustomCellProps) {
|
||||
// const { tableState } = useFiretableContext();
|
||||
const classes = useStyles();
|
||||
// const classes = useStyles({
|
||||
// width: column.width,
|
||||
// rowHeight: tableState?.config?.rowHeight ?? 44,
|
||||
// });
|
||||
|
||||
if (!value || value === "") return null;
|
||||
|
||||
return (
|
||||
// <Tooltip
|
||||
// title={value}
|
||||
// enterDelay={1000}
|
||||
// placement="bottom-start"
|
||||
// PopperProps={{
|
||||
// modifiers: {
|
||||
// flip: { enabled: false },
|
||||
// preventOverflow: {
|
||||
// enabled: false,
|
||||
// boundariesElement: "scrollParent",
|
||||
// },
|
||||
// hide: { enabled: false },
|
||||
// },
|
||||
// }}
|
||||
// TransitionComponent={Fade}
|
||||
// classes={{ tooltip: classes.tooltip }}
|
||||
// >
|
||||
<div className={classes.root}>{value}</div>
|
||||
// </Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
Grid,
|
||||
Tooltip,
|
||||
Button,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import MultiSelect_ from "@antlerengineering/multiselect";
|
||||
import FormattedChip, { VARIANTS } from "components/FormattedChip";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
|
||||
inputBase: {
|
||||
height: "100%",
|
||||
font: "inherit",
|
||||
color: "inherit !important",
|
||||
letterSpacing: "inherit",
|
||||
},
|
||||
|
||||
select: {
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
whiteSpace: "pre-line",
|
||||
padding: theme.spacing(0, 4, 0, 1.5),
|
||||
"&&": { paddingRight: theme.spacing(4) },
|
||||
},
|
||||
selectSingleLabel: {
|
||||
maxHeight: "100%",
|
||||
overflow: "hidden",
|
||||
},
|
||||
icon: { right: theme.spacing(1) },
|
||||
|
||||
chipList: {
|
||||
overflowX: "hidden",
|
||||
width: "100%",
|
||||
},
|
||||
chip: { cursor: "inherit" },
|
||||
chipLabel: { whiteSpace: "nowrap" },
|
||||
})
|
||||
);
|
||||
|
||||
export const ConvertStringToArray = ({ value, onSubmit }) => (
|
||||
<>
|
||||
{value}
|
||||
<Tooltip title="It looks like this is a string, click to convert it to an array">
|
||||
<Button
|
||||
onClick={() => {
|
||||
onSubmit([value]);
|
||||
}}
|
||||
>
|
||||
fix
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
|
||||
export default function MultiSelect({
|
||||
rowIdx,
|
||||
column,
|
||||
value,
|
||||
onSubmit,
|
||||
}: CustomCellProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const { config } = column as any;
|
||||
const { dataGridRef } = useFiretableContext();
|
||||
|
||||
// Support SingleSelect field
|
||||
const isSingle = (column as any).type === FieldType.singleSelect;
|
||||
|
||||
let sanitisedValue: any;
|
||||
if (isSingle) {
|
||||
if (value === undefined || value === null || value === "")
|
||||
sanitisedValue = null;
|
||||
else if (Array.isArray(value)) sanitisedValue = value[0];
|
||||
else sanitisedValue = value;
|
||||
} else {
|
||||
if (value === undefined || value === null || value === "")
|
||||
sanitisedValue = [];
|
||||
else sanitisedValue = value;
|
||||
}
|
||||
|
||||
// Render chips or basic string
|
||||
const renderValue = isSingle
|
||||
? () =>
|
||||
typeof sanitisedValue === "string" &&
|
||||
VARIANTS.includes(sanitisedValue.toLowerCase()) ? (
|
||||
<FormattedChip label={sanitisedValue} className={classes.chip} />
|
||||
) : (
|
||||
<span className={classes.selectSingleLabel}>{sanitisedValue}</span>
|
||||
)
|
||||
: () => (
|
||||
<Grid container spacing={1} wrap="nowrap" className={classes.chipList}>
|
||||
{sanitisedValue?.map(
|
||||
(item) =>
|
||||
typeof item === "string" && (
|
||||
<Grid item key={item}>
|
||||
<FormattedChip
|
||||
label={item}
|
||||
className={classes.chip}
|
||||
classes={{ label: classes.chipLabel }}
|
||||
/>
|
||||
</Grid>
|
||||
)
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const handleOpen = () => {
|
||||
if (dataGridRef?.current?.selectCell)
|
||||
dataGridRef.current.selectCell({ rowIdx, idx: column.idx });
|
||||
};
|
||||
if (typeof value === "string" && value !== "" && !isSingle)
|
||||
return <ConvertStringToArray value={value} onSubmit={onSubmit} />;
|
||||
return (
|
||||
// <Tooltip
|
||||
// title={
|
||||
// value
|
||||
// ? Array.isArray(value)
|
||||
// ? value.length > 1
|
||||
// ? value.join(", ")
|
||||
// : ``
|
||||
// : value
|
||||
// : ``
|
||||
// }
|
||||
// enterDelay={100}
|
||||
// interactive
|
||||
// placement="bottom-start"
|
||||
// >
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MultiSelect_
|
||||
value={sanitisedValue}
|
||||
onChange={onSubmit}
|
||||
freeText={config.freeText}
|
||||
multiple={!isSingle as any}
|
||||
label={column.name}
|
||||
labelPlural={column.name}
|
||||
options={config.options ?? []}
|
||||
disabled={column.editable === false}
|
||||
onOpen={handleOpen}
|
||||
TextFieldProps={
|
||||
{
|
||||
label: "",
|
||||
hiddenLabel: true,
|
||||
variant: "standard",
|
||||
className: classes.root,
|
||||
InputProps: {
|
||||
disableUnderline: true,
|
||||
classes: { root: classes.inputBase },
|
||||
},
|
||||
SelectProps: {
|
||||
classes: {
|
||||
root: clsx(classes.root, classes.select),
|
||||
icon: classes.icon,
|
||||
},
|
||||
renderValue,
|
||||
MenuProps: {
|
||||
anchorOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
transformOrigin: { vertical: "top", horizontal: "left" },
|
||||
},
|
||||
},
|
||||
} as const
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
// </Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import React from "react";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
|
||||
import { makeStyles, createStyles } from "@material-ui/core";
|
||||
import { resultColorsScale } from "utils/color";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
resultColor: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
opacity: 0.5,
|
||||
|
||||
zIndex: 0,
|
||||
},
|
||||
|
||||
text: {
|
||||
textAlign: "right",
|
||||
color: theme.palette.text.primary,
|
||||
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function Percentage({ value }: CustomCellProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
if (typeof value === "number")
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classes.resultColor}
|
||||
style={{ backgroundColor: resultColorsScale(value).hex() }}
|
||||
/>
|
||||
<div className={classes.text}>{Math.round(value * 100)}%</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from "react";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
|
||||
import { makeStyles, createStyles } from "@material-ui/core";
|
||||
import MuiRating from "@material-ui/lab/Rating";
|
||||
import StarBorderIcon from "@material-ui/icons/StarBorder";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
rating: { color: theme.palette.text.secondary },
|
||||
iconEmpty: { color: theme.palette.text.secondary },
|
||||
})
|
||||
);
|
||||
|
||||
export default function Rating({
|
||||
row,
|
||||
column,
|
||||
value,
|
||||
onSubmit,
|
||||
}: CustomCellProps) {
|
||||
const classes = useStyles();
|
||||
const { max, precision } = ((column as any).config ?? {}) as {
|
||||
max: number;
|
||||
precision: number;
|
||||
};
|
||||
return (
|
||||
<MuiRating
|
||||
name={`${row.id}-${column.key as string}`}
|
||||
value={typeof value === "number" ? value : 0}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
disabled={column.editable === false}
|
||||
onChange={(_, newValue) => onSubmit(newValue)}
|
||||
emptyIcon={<StarBorderIcon />}
|
||||
// TODO: Make this customisable in config
|
||||
max={max ?? 5}
|
||||
precision={precision ?? 1}
|
||||
classes={{ root: classes.rating, iconEmpty: classes.iconEmpty }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import React from "react";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import queryString from "query-string";
|
||||
import useRouter from "hooks/useRouter";
|
||||
|
||||
import { createStyles, makeStyles, Grid, IconButton } from "@material-ui/core";
|
||||
import OpenIcon from "@material-ui/icons/OpenInNew";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: { padding: theme.spacing(0, 0.625, 0, 1) },
|
||||
labelContainer: { overflowX: "hidden" },
|
||||
})
|
||||
);
|
||||
|
||||
export default function SubTable({ column, row }: CustomCellProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
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 documentCount = row[fieldName]?.count ?? "";
|
||||
|
||||
const router = useRouter();
|
||||
const parentLabels = queryString.parse(router.location.search).parentLabel;
|
||||
|
||||
if (!row.ref) return null;
|
||||
|
||||
let subTablePath = "";
|
||||
if (parentLabels)
|
||||
subTablePath =
|
||||
encodeURIComponent(`${row.ref.path}/${fieldName}`) +
|
||||
`?parentLabel=${parentLabels},${label}`;
|
||||
else
|
||||
subTablePath =
|
||||
encodeURIComponent(`${row.ref.path}/${fieldName}`) +
|
||||
`?parentLabel=${encodeURIComponent(label)}`;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
wrap="nowrap"
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
className={clsx("cell-collapse-padding", classes.root)}
|
||||
>
|
||||
<Grid item xs className={classes.labelContainer}>
|
||||
{documentCount} {column.name}: {label}
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<IconButton
|
||||
component={Link}
|
||||
to={subTablePath}
|
||||
className="row-hover-iconButton"
|
||||
size="small"
|
||||
>
|
||||
<OpenIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from "react";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
|
||||
import { useTheme } from "@material-ui/core";
|
||||
|
||||
export default function Text({ value }: CustomCellProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<span //style={{ fontFamily: theme.typography.fontFamilyMono }}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import React, { lazy } from "react";
|
||||
import { FieldType } from "constants/fields";
|
||||
|
||||
import withCustomCell from "./withCustomCell";
|
||||
import Text from "./Text";
|
||||
|
||||
const MultiSelect = lazy(
|
||||
() => import("./MultiSelect" /* webpackChunkName: "MultiSelect" */)
|
||||
);
|
||||
const DatePicker = lazy(() => import("./Date" /* webpackChunkName: "Date" */));
|
||||
const Duration = lazy(
|
||||
() => import("./Duration" /* webpackChunkName: "Duration" */)
|
||||
);
|
||||
const Rating = lazy(() => import("./Rating" /* webpackChunkName: "Rating" */));
|
||||
const Checkbox = lazy(
|
||||
() => import("./Checkbox" /* webpackChunkName: "Checkbox" */)
|
||||
);
|
||||
const Url = lazy(() => import("./Url" /* webpackChunkName: "Url" */));
|
||||
const Image = lazy(() => import("./Image" /* webpackChunkName: "Image" */));
|
||||
const File = lazy(() => import("./File" /* webpackChunkName: "File" */));
|
||||
const LongText = lazy(
|
||||
() => import("./LongText" /* webpackChunkName: "LongText" */)
|
||||
);
|
||||
const Json = lazy(() => import("./Json" /* webpackChunkName: "Json" */));
|
||||
const User = lazy(() => import("./User" /* webpackChunkName: "User" */));
|
||||
const Code = lazy(() => import("./Code" /* webpackChunkName: "Code" */));
|
||||
const RichText = lazy(
|
||||
() => import("./RichText" /* webpackChunkName: "RichText" */)
|
||||
);
|
||||
const Color = lazy(() => import("./Color" /* webpackChunkName: "Color" */));
|
||||
const Action = lazy(() => import("./Action" /* webpackChunkName: "Action" */));
|
||||
const ConnectTable = lazy(
|
||||
() => import("./ConnectTable" /* webpackChunkName: "ConnectTable" */)
|
||||
);
|
||||
const ConnectService = lazy(() => import("./ConnectService"));
|
||||
const SubTable = lazy(
|
||||
() => import("./SubTable" /* webpackChunkName: "SubTable" */)
|
||||
);
|
||||
const Percentage = lazy(
|
||||
() => import("./Percentage" /* webpackChunkName: "Percentage" */)
|
||||
);
|
||||
const Id = lazy(() => import("./Id" /* webpackChunkName: "Id" */));
|
||||
|
||||
/**
|
||||
* Gets the corresponding formatter for each cell.
|
||||
* Cells can be edited:
|
||||
* - by displaying the default react-data-grid text editor,
|
||||
* - without double-clicking, or
|
||||
* - must be edited in the side drawer.
|
||||
*
|
||||
* This is implemented alongside the correct editor — see below.
|
||||
*
|
||||
* @param column Must have column `type`
|
||||
* @param readOnly Prevent the formatter from updating the cell value
|
||||
*/
|
||||
export const getFormatter = (column: any, readOnly: boolean = false) => {
|
||||
let _type = column.type;
|
||||
if (column.config?.renderFieldType) _type = column.config.renderFieldType;
|
||||
|
||||
switch (_type) {
|
||||
case FieldType.date:
|
||||
case FieldType.dateTime:
|
||||
return withCustomCell(DatePicker, readOnly);
|
||||
case FieldType.duration:
|
||||
return withCustomCell(Duration, readOnly);
|
||||
|
||||
case FieldType.rating:
|
||||
return withCustomCell(Rating, readOnly);
|
||||
case FieldType.percentage:
|
||||
return withCustomCell(Percentage, readOnly);
|
||||
|
||||
case FieldType.color:
|
||||
return withCustomCell(Color, readOnly);
|
||||
|
||||
case FieldType.checkbox:
|
||||
return withCustomCell(Checkbox, readOnly);
|
||||
|
||||
case FieldType.url:
|
||||
return withCustomCell(Url, readOnly);
|
||||
|
||||
case FieldType.action:
|
||||
return withCustomCell(Action, readOnly);
|
||||
|
||||
case FieldType.singleSelect:
|
||||
case FieldType.multiSelect:
|
||||
return withCustomCell(MultiSelect, readOnly);
|
||||
|
||||
case FieldType.image:
|
||||
return withCustomCell(Image, readOnly);
|
||||
|
||||
case FieldType.file:
|
||||
return withCustomCell(File, readOnly);
|
||||
|
||||
case FieldType.longText:
|
||||
return withCustomCell(LongText, readOnly);
|
||||
|
||||
case FieldType.json:
|
||||
return withCustomCell(Json, readOnly);
|
||||
|
||||
case FieldType.user:
|
||||
return withCustomCell(User, readOnly);
|
||||
|
||||
case FieldType.code:
|
||||
return withCustomCell(Code, readOnly);
|
||||
|
||||
case FieldType.richText:
|
||||
return withCustomCell(RichText, readOnly);
|
||||
|
||||
case FieldType.connectTable:
|
||||
return withCustomCell(ConnectTable, readOnly);
|
||||
|
||||
case FieldType.connectService:
|
||||
return withCustomCell(ConnectService, readOnly);
|
||||
|
||||
case FieldType.subTable:
|
||||
return withCustomCell(SubTable, readOnly);
|
||||
|
||||
case FieldType.id:
|
||||
return withCustomCell(Id, readOnly);
|
||||
|
||||
case FieldType.shortText:
|
||||
case FieldType.email:
|
||||
case FieldType.phone:
|
||||
case FieldType.number:
|
||||
case FieldType.slider:
|
||||
return withCustomCell(Text, readOnly);
|
||||
|
||||
default:
|
||||
return () => <div />;
|
||||
}
|
||||
};
|
||||
@@ -1,129 +0,0 @@
|
||||
import React, { Suspense, useState, useEffect } from "react";
|
||||
import { FormatterProps } from "react-data-grid";
|
||||
|
||||
// import { makeStyles, createStyles } from "@material-ui/core";
|
||||
import { Link } from "@material-ui/core";
|
||||
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
|
||||
import { FieldType } from "constants/fields";
|
||||
import { getCellValue } from "utils/fns";
|
||||
|
||||
// const useStyles = makeStyles((theme) =>
|
||||
// createStyles({
|
||||
// "@global": {
|
||||
// ".rdg-cell-mask.rdg-selected": {
|
||||
// // Prevent 3px-wide cell selection border when both react-data-grid
|
||||
// // cell selection mask and our custom one are active
|
||||
// boxShadow: `0 0 0 1px ${theme.palette.background.paper} inset`,
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// );
|
||||
|
||||
export type CustomCellProps = FormatterProps<any> & {
|
||||
value: any;
|
||||
onSubmit: (value: any) => void;
|
||||
docRef: firebase.firestore.DocumentReference;
|
||||
};
|
||||
|
||||
const BasicCell = ({ value, type, name }) => {
|
||||
switch (type) {
|
||||
case FieldType.singleSelect:
|
||||
case FieldType.shortText:
|
||||
case FieldType.longText:
|
||||
case FieldType.shortText:
|
||||
case FieldType.email:
|
||||
case FieldType.phone:
|
||||
case FieldType.number:
|
||||
case FieldType.slider:
|
||||
return typeof value === "string" ? <>{value}</> : <></>;
|
||||
case FieldType.url:
|
||||
return typeof value === "string" ? (
|
||||
<Link
|
||||
href={value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="always"
|
||||
style={{ fontWeight: "bold" }}
|
||||
>
|
||||
{value}
|
||||
</Link>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
case FieldType.subTable:
|
||||
return (
|
||||
<>
|
||||
{value && value.count} {name}:
|
||||
</>
|
||||
);
|
||||
case FieldType.checkbox:
|
||||
return <>{name}</>;
|
||||
case FieldType.action:
|
||||
return <>{value ? value.status : name}</>;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* HOC to wrap around custom cell formatters.
|
||||
* Displays react-data-grid’s blue selection border when the cell is selected.
|
||||
* @param Component The formatter component to display
|
||||
* @param readOnly Prevent the formatter from updating the cell value
|
||||
*/
|
||||
const withCustomCell = (
|
||||
Component: React.ComponentType<CustomCellProps>,
|
||||
readOnly: boolean = false
|
||||
) => (props: FormatterProps<any>) => {
|
||||
// useStyles();
|
||||
const { updateCell } = useFiretableContext();
|
||||
|
||||
const value = getCellValue(props.row, props.column.key as string);
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
// Initially display basicCell to improve scroll performance
|
||||
const basicCell = (
|
||||
<BasicCell
|
||||
value={localValue}
|
||||
name={(props.column as any).name}
|
||||
type={(props.column as any).type as FieldType}
|
||||
/>
|
||||
);
|
||||
|
||||
const [component, setComponent] = useState(basicCell);
|
||||
|
||||
// Switch to heavy cell component on mount
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setComponent(
|
||||
<ErrorBoundary fullScreen={false} basic wrap="nowrap">
|
||||
<Suspense fallback={basicCell}>
|
||||
<Component
|
||||
{...props}
|
||||
docRef={props.row.ref}
|
||||
value={localValue}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
});
|
||||
}, [localValue]);
|
||||
|
||||
const handleSubmit = (value: any) => {
|
||||
if (updateCell && !readOnly) {
|
||||
updateCell(props.row.ref, props.column.key as string, value);
|
||||
setLocalValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
return component;
|
||||
};
|
||||
|
||||
export default withCustomCell;
|
||||
@@ -1,35 +1,35 @@
|
||||
import React, { useEffect, useRef, useMemo, useState } from "react";
|
||||
|
||||
import _orderBy from "lodash/orderBy";
|
||||
import _isEmpty from "lodash/isEmpty";
|
||||
import _find from "lodash/find";
|
||||
import _difference from "lodash/difference";
|
||||
import _get from "lodash/get";
|
||||
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import BulkActions from "./BulkActions";
|
||||
|
||||
import "react-data-grid/dist/react-data-grid.css";
|
||||
import DataGrid, {
|
||||
Column,
|
||||
SelectColumn as _SelectColumn,
|
||||
} from "react-data-grid";
|
||||
import { formatSubTableName } from "../../utils/fns";
|
||||
|
||||
import Loading from "components/Loading";
|
||||
import TableHeader from "./TableHeader";
|
||||
import ColumnHeader from "./ColumnHeader";
|
||||
import ColumnMenu from "./ColumnMenu";
|
||||
import FinalColumnHeader from "./FinalColumnHeader";
|
||||
import FinalColumn from "./formatters/FinalColumn";
|
||||
import BulkActions from "./BulkActions";
|
||||
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
|
||||
import { getFieldProp } from "components/fields";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { getFormatter } from "./formatters";
|
||||
import { getEditor } from "./editors";
|
||||
import { formatSubTableName } from "utils/fns";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
import useWindowSize from "hooks/useWindowSize";
|
||||
import useStyles from "./styles";
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import _get from "lodash/get";
|
||||
|
||||
export type FiretableColumn = Column<any> & {
|
||||
isNew?: boolean;
|
||||
@@ -56,19 +56,7 @@ export default function Table() {
|
||||
?.hiddenFields ?? [];
|
||||
|
||||
const [columns, setColumns] = useState<FiretableColumn[]>([]);
|
||||
const lastColumn = {
|
||||
isNew: true,
|
||||
key: "new",
|
||||
name: "Add column",
|
||||
type: FieldType.last,
|
||||
index: columns.length ?? 0,
|
||||
width: 204,
|
||||
headerRenderer: FinalColumnHeader,
|
||||
headerCellClass: "final-column-header",
|
||||
cellClass: "final-column-cell",
|
||||
formatter: FinalColumn,
|
||||
editable: false,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!tableState?.loadingColumns && tableState?.columns) {
|
||||
const _columns = _orderBy(
|
||||
@@ -77,14 +65,28 @@ export default function Table() {
|
||||
),
|
||||
"index"
|
||||
)
|
||||
.map((column: any, index) => ({
|
||||
.map((column: any) => ({
|
||||
draggable: true,
|
||||
editable: true,
|
||||
resizable: true,
|
||||
frozen: column.fixed,
|
||||
headerRenderer: ColumnHeader,
|
||||
formatter: getFormatter(column),
|
||||
editor: getEditor(column),
|
||||
formatter:
|
||||
getFieldProp(
|
||||
"TableCell",
|
||||
column.config?.renderFieldType ?? column.type
|
||||
) ??
|
||||
function InDev() {
|
||||
return null;
|
||||
},
|
||||
editor:
|
||||
getFieldProp(
|
||||
"TableEditor",
|
||||
column.config?.renderFieldType ?? column.type
|
||||
) ??
|
||||
function InDev() {
|
||||
return null;
|
||||
},
|
||||
...column,
|
||||
width: column.width ? (column.width > 380 ? 380 : column.width) : 150,
|
||||
}))
|
||||
@@ -94,7 +96,19 @@ export default function Table() {
|
||||
// TODO: ENABLE ONCE BULK ACTIONS READY
|
||||
// SelectColumn,
|
||||
..._columns,
|
||||
lastColumn,
|
||||
{
|
||||
isNew: true,
|
||||
key: "new",
|
||||
name: "Add column",
|
||||
type: FieldType.last,
|
||||
index: _columns.length ?? 0,
|
||||
width: 204,
|
||||
headerRenderer: FinalColumnHeader,
|
||||
headerCellClass: "final-column-header",
|
||||
cellClass: "final-column-cell",
|
||||
formatter: FinalColumn,
|
||||
editable: false,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}, [
|
||||
@@ -223,7 +237,7 @@ export default function Table() {
|
||||
break;
|
||||
}
|
||||
}}
|
||||
onRowClick={(rowIdx, row, column) => {
|
||||
onRowClick={(rowIdx, column) => {
|
||||
if (sideDrawerRef?.current) {
|
||||
sideDrawerRef.current.setCell({
|
||||
row: rowIdx,
|
||||
@@ -231,8 +245,6 @@ export default function Table() {
|
||||
});
|
||||
}
|
||||
}}
|
||||
// TODO: Investigate why setting a numeric value causes
|
||||
// LOADING to pop up on screen when scrolling horizontally
|
||||
/>
|
||||
</DndProvider>
|
||||
) : (
|
||||
|
||||
@@ -4,8 +4,8 @@ import * as yup from "yup";
|
||||
import { FIELDS } from "@antlerengineering/form-builder";
|
||||
import { TableSettingsDialogModes } from "./index";
|
||||
|
||||
import HelperText from "./HelperText";
|
||||
import { Link, ListItemSecondaryAction, Typography } from "@material-ui/core";
|
||||
import HelperText from "../HelperText";
|
||||
import { Link, Typography } from "@material-ui/core";
|
||||
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||
import { MONO_FONT } from "Themes";
|
||||
|
||||
@@ -86,7 +86,6 @@ export const tableSettings = (
|
||||
</HelperText>
|
||||
),
|
||||
}),
|
||||
|
||||
{
|
||||
type: FIELDS.multiSelect,
|
||||
name: "section",
|
||||
@@ -96,7 +95,6 @@ export const tableSettings = (
|
||||
options: sections,
|
||||
validation: yup.string().required("Required"),
|
||||
},
|
||||
|
||||
{
|
||||
type: FIELDS.text,
|
||||
name: "description",
|
||||
@@ -104,7 +102,6 @@ export const tableSettings = (
|
||||
fieldVariant: "long",
|
||||
validation: yup.string(),
|
||||
},
|
||||
|
||||
{
|
||||
type: FIELDS.multiSelect,
|
||||
name: "roles",
|
||||
|
||||
@@ -5,7 +5,6 @@ import _find from "lodash/find";
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
Grid,
|
||||
Button,
|
||||
DialogContentText,
|
||||
} from "@material-ui/core";
|
||||
@@ -183,35 +182,39 @@ export default function TableSettingsDialog({
|
||||
...data,
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
customActions={
|
||||
<Grid
|
||||
container
|
||||
spacing={2}
|
||||
justify="center"
|
||||
className={classes.buttonGrid}
|
||||
>
|
||||
<Grid item>
|
||||
<Button
|
||||
size="large"
|
||||
variant="outlined"
|
||||
onClick={handleClose}
|
||||
className={classes.button}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
size="large"
|
||||
variant="contained"
|
||||
type="submit"
|
||||
className={classes.button}
|
||||
>
|
||||
{mode === TableSettingsDialogModes.create ? "Create" : "Update"}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
SubmitButtonProps={{
|
||||
children:
|
||||
mode === TableSettingsDialogModes.create ? "Create" : "Update",
|
||||
}}
|
||||
// customActions={
|
||||
// <Grid
|
||||
// container
|
||||
// spacing={2}
|
||||
// justify="center"
|
||||
// // className={classes.buttonGrid}
|
||||
// >
|
||||
// <Grid item>
|
||||
// <Button
|
||||
// size="large"
|
||||
// variant="outlined"
|
||||
// onClick={handleClose}
|
||||
// // className={classes.button}
|
||||
// >
|
||||
// Cancel
|
||||
// </Button>
|
||||
// </Grid>
|
||||
// <Grid item>
|
||||
// <Button
|
||||
// size="large"
|
||||
// variant="contained"
|
||||
// type="submit"
|
||||
// // className={classes.button}
|
||||
// >
|
||||
// {mode === TableSettingsDialogModes.create ? "Create" : "Update"}
|
||||
// </Button>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// }
|
||||
formFooter={
|
||||
mode === TableSettingsDialogModes.update ? (
|
||||
<div className={classes.formFooter}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
import { makeStyles, createStyles } from "@material-ui/core";
|
||||
|
||||
import { FieldType } from "constants/fields";
|
||||
import { getFormatter } from "components/Table/formatters";
|
||||
import { getFieldProp } from "components/fields";
|
||||
import EmptyState from "components/EmptyState";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
@@ -68,8 +68,7 @@ export default function Cell({
|
||||
...props
|
||||
}: ICellProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const formatter = type ? getFormatter({ type: type }, true) : null;
|
||||
const formatter = type ? getFieldProp("TableCell", type) : null;
|
||||
|
||||
return (
|
||||
<div className={classes.root} {...props}>
|
||||
@@ -83,6 +82,7 @@ export default function Cell({
|
||||
key: field,
|
||||
name,
|
||||
config: { options: [] },
|
||||
editable: false,
|
||||
} as any,
|
||||
row: { [field]: value },
|
||||
isRowSelected: false,
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
} from "@material-ui/core";
|
||||
import { fade } from "@material-ui/core/styles";
|
||||
|
||||
import { FieldType, getFieldIcon } from "constants/fields";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { getFieldProp } from "components/fields";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
@@ -88,7 +89,7 @@ export default function Column({
|
||||
{...props}
|
||||
className={clsx(classes.root, active && classes.active, props.className)}
|
||||
>
|
||||
{type && <Grid item>{getFieldIcon(type)}</Grid>}
|
||||
{type && <Grid item>{getFieldProp("icon", type)}</Grid>}
|
||||
|
||||
<Grid item xs className={classes.columnNameContainer}>
|
||||
<Typography
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import { ScrollSync, ScrollSyncPane } from "react-scroll-sync";
|
||||
import _find from "lodash/find";
|
||||
import { parseJSON } from "date-fns";
|
||||
|
||||
import { makeStyles, createStyles, Grid } from "@material-ui/core";
|
||||
|
||||
@@ -83,6 +82,7 @@ export default function Step4Preview({ csvData, config }: IStepProps) {
|
||||
|
||||
const columns = config.pairs.map(({ csvKey, columnKey }) => ({
|
||||
csvKey,
|
||||
columnKey,
|
||||
...(tableState!.columns[columnKey] ??
|
||||
_find(config.newColumns, { key: columnKey }) ??
|
||||
{}),
|
||||
@@ -105,17 +105,13 @@ export default function Step4Preview({ csvData, config }: IStepProps) {
|
||||
|
||||
<ScrollSyncPane>
|
||||
<Grid container wrap="nowrap" className={classes.data}>
|
||||
{columns.map(({ csvKey, name, type }) => (
|
||||
{columns.map(({ csvKey, name, columnKey, type }) => (
|
||||
<Grid item key={csvKey} className={classes.column}>
|
||||
{csvData.rows.map((row, i) => (
|
||||
<Cell
|
||||
key={csvKey + i}
|
||||
field={csvKey}
|
||||
value={
|
||||
type === FieldType.date || type === FieldType.dateTime
|
||||
? parseJSON(row[csvKey]).getTime()
|
||||
: row[csvKey]
|
||||
}
|
||||
field={columnKey}
|
||||
value={row[columnKey]}
|
||||
type={type}
|
||||
name={name}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useMemo } from "react";
|
||||
import _mergeWith from "lodash/mergeWith";
|
||||
import _find from "lodash/find";
|
||||
import { parseJSON } from "date-fns";
|
||||
@@ -20,6 +20,8 @@ import Step3Preview from "./Step3Preview";
|
||||
import { ColumnConfig } from "hooks/useFiretable/useTableConfig";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { useSnackContext } from "contexts/SnackContext";
|
||||
import { getFieldProp } from "components/fields";
|
||||
|
||||
export type CsvConfig = {
|
||||
pairs: { csvKey: string; columnKey: string }[];
|
||||
@@ -52,6 +54,7 @@ export default function ImportCsvWizard({
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
const { tableState, tableActions } = useFiretableContext();
|
||||
const { open: openSnackbar } = useSnackContext();
|
||||
|
||||
const [config, setConfig] = useState<CsvConfig>({
|
||||
pairs: [],
|
||||
@@ -65,29 +68,36 @@ export default function ImportCsvWizard({
|
||||
}));
|
||||
};
|
||||
|
||||
const handleFinish = () => {
|
||||
if (!tableState || !tableActions || !csvData) return;
|
||||
// Add any new columns to the end
|
||||
config.newColumns.forEach((col) =>
|
||||
setTimeout(() => {
|
||||
tableActions.column.add(col.name, col.type, col);
|
||||
})
|
||||
);
|
||||
// Add all new rows
|
||||
csvData.rows.forEach((row) => {
|
||||
const newRow = config.pairs.reduce((a, pair) => {
|
||||
const parsedRows: any[] = useMemo(() => {
|
||||
if (!tableState || !tableActions || !csvData) return [];
|
||||
return csvData.rows.map((row) =>
|
||||
config.pairs.reduce((a, pair) => {
|
||||
const matchingColumn =
|
||||
tableState.columns[pair.columnKey] ??
|
||||
_find(config.newColumns, { key: pair.columnKey });
|
||||
const value =
|
||||
matchingColumn.type === FieldType.date ||
|
||||
matchingColumn.type === FieldType.dateTime
|
||||
? parseJSON(row[pair.csvKey])
|
||||
: row[pair.csvKey];
|
||||
console.log({ type: matchingColumn.type });
|
||||
const csvFieldParser = getFieldProp(
|
||||
"csvImportParser",
|
||||
matchingColumn.type
|
||||
);
|
||||
const value = csvFieldParser
|
||||
? csvFieldParser(row[pair.csvKey])
|
||||
: row[pair.csvKey];
|
||||
return { ...a, [pair.columnKey]: value };
|
||||
}, {});
|
||||
tableActions.row.add(newRow);
|
||||
});
|
||||
}, {})
|
||||
);
|
||||
}, [csvData, tableState, tableActions, config]);
|
||||
|
||||
const handleFinish = () => {
|
||||
if (!tableState || !tableActions || !parsedRows) return;
|
||||
openSnackbar({ message: "Importing data…" });
|
||||
// Add all new rows — synchronous
|
||||
parsedRows?.forEach((newRow) => tableActions.row.add(newRow));
|
||||
|
||||
// Add any new columns to the end
|
||||
for (const col of config.newColumns) {
|
||||
tableActions.column.add(col.name, col.type, col);
|
||||
}
|
||||
// Close wizard
|
||||
setOpen(false);
|
||||
setTimeout(handleClose, 300);
|
||||
@@ -171,7 +181,7 @@ export default function ImportCsvWizard({
|
||||
"Preview your data with your configured columns. You can change column types by clicking “Edit Type” from the column menu at any time.",
|
||||
content: (
|
||||
<Step3Preview
|
||||
csvData={csvData}
|
||||
csvData={{ ...csvData, rows: parsedRows }}
|
||||
config={config}
|
||||
setConfig={setConfig}
|
||||
updateConfig={updateConfig}
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
//import Confirmation from "components/Confirmation";
|
||||
import _get from "lodash/get";
|
||||
import {
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Fab,
|
||||
CircularProgress,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import { Fab, FabProps, CircularProgress } from "@material-ui/core";
|
||||
import PlayIcon from "@material-ui/icons/PlayArrow";
|
||||
import RefreshIcon from "@material-ui/icons/Refresh";
|
||||
import UndoIcon from "@material-ui/icons/Undo";
|
||||
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
import { SnackContext } from "contexts/SnackContext";
|
||||
import { cloudFunction } from "firebase/callables";
|
||||
import { sanitiseRowData } from "utils/fns";
|
||||
import { formatPath } from "../../../../utils/fns";
|
||||
import { formatPath } from "utils/fns";
|
||||
import { useConfirmation } from "components/ConfirmationDialog";
|
||||
import { useActionParams } from "./FormDialog/Context";
|
||||
//import ParamsDialog from './ParamsDialog'
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
fab: { width: 36, height: 36 },
|
||||
})
|
||||
);
|
||||
|
||||
const replacer = (data: any) => (m: string, key: string) => {
|
||||
const objKey = key.split(":")[0];
|
||||
@@ -41,8 +30,22 @@ const getStateIcon = (actionState) => {
|
||||
}
|
||||
};
|
||||
|
||||
export default ({ row, column, onSubmit, value }) => {
|
||||
const classes = useStyles();
|
||||
export interface IActionFabProps extends Partial<FabProps> {
|
||||
row: any;
|
||||
column: any;
|
||||
onSubmit: (value: any) => void;
|
||||
value: any;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export default function ActionFab({
|
||||
row,
|
||||
column,
|
||||
onSubmit,
|
||||
value,
|
||||
disabled,
|
||||
...props
|
||||
}: IActionFabProps) {
|
||||
const { requestConfirmation } = useConfirmation();
|
||||
const { requestParams } = useActionParams();
|
||||
const { tableState } = useFiretableContext();
|
||||
@@ -57,23 +60,20 @@ export default ({ row, column, onSubmit, value }) => {
|
||||
? "redo"
|
||||
: "";
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const disabled = column.editable === false;
|
||||
const snack = useContext(SnackContext);
|
||||
|
||||
const callableName =
|
||||
const callableName: string =
|
||||
(column as any).callableName ?? config.callableName ?? "actionScript";
|
||||
const handleRun = (actionParams = null) => {
|
||||
setIsRunning(true);
|
||||
|
||||
const data = {
|
||||
ref: { path: ref.path, id: ref.id, tablePath: window.location.pathname },
|
||||
row: sanitiseRowData(Object.assign({}, docData)),
|
||||
column,
|
||||
column: { ...column, editor: undefined },
|
||||
action,
|
||||
schemaDocPath: formatPath(tableState?.tablePath ?? ""),
|
||||
actionParams,
|
||||
};
|
||||
console.log({ data });
|
||||
cloudFunction(
|
||||
callableName,
|
||||
data,
|
||||
@@ -106,9 +106,6 @@ export default ({ row, column, onSubmit, value }) => {
|
||||
typeof config.confirmation === "string" && config.confirmation !== "";
|
||||
return (
|
||||
<Fab
|
||||
size="small"
|
||||
color="secondary"
|
||||
className={classes.fab}
|
||||
onClick={
|
||||
needsParams
|
||||
? () =>
|
||||
@@ -139,6 +136,7 @@ export default ({ row, column, onSubmit, value }) => {
|
||||
) ||
|
||||
disabled
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{isRunning ? (
|
||||
<CircularProgress color="secondary" size={16} thickness={5.6} />
|
||||
@@ -147,4 +145,4 @@ export default ({ row, column, onSubmit, value }) => {
|
||||
)}
|
||||
</Fab>
|
||||
);
|
||||
};
|
||||
}
|
||||
6
www/src/components/fields/Action/BasicCell.tsx
Normal file
6
www/src/components/fields/Action/BasicCell.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from "react";
|
||||
import { IBasicCellProps } from "../types";
|
||||
|
||||
export default function Action({ name, value }: IBasicCellProps) {
|
||||
return <>{value ? value.status : name}</>;
|
||||
}
|
||||
179
www/src/components/fields/Action/Settings.tsx
Normal file
179
www/src/components/fields/Action/Settings.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React, { useState, lazy, Suspense } from "react";
|
||||
import {
|
||||
Typography,
|
||||
IconButton,
|
||||
TextField,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Divider,
|
||||
} from "@material-ui/core";
|
||||
import MultiSelect from "@antlerengineering/multiselect";
|
||||
import FieldSkeleton from "components/SideDrawer/Form/FieldSkeleton";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
|
||||
const CodeEditor = lazy(
|
||||
() =>
|
||||
import(
|
||||
"components/Table/editors/CodeEditor" /* webpackChunkName: "CodeEditor" */
|
||||
)
|
||||
);
|
||||
|
||||
const Settings = ({ config, handleChange }) => {
|
||||
const { tableState, roles } = useFiretableContext();
|
||||
const columnOptions = Object.values(tableState?.columns ?? {}).map((c) => ({
|
||||
label: c.name,
|
||||
value: c.key,
|
||||
}));
|
||||
return (
|
||||
<>
|
||||
<Typography variant="overline">Allowed roles</Typography>
|
||||
<Typography variant="body2">
|
||||
Authenticated user must have at least one of these to run the script
|
||||
</Typography>
|
||||
<MultiSelect
|
||||
label={"Allowed Roles"}
|
||||
options={roles ?? []}
|
||||
value={config.requiredRoles ?? []}
|
||||
onChange={handleChange("requiredRoles")}
|
||||
/>
|
||||
|
||||
<Typography variant="overline">Required fields</Typography>
|
||||
<Typography variant="body2">
|
||||
All of the selected fields must have a value for the script to run
|
||||
</Typography>
|
||||
|
||||
<MultiSelect
|
||||
label={"Required fields"}
|
||||
options={columnOptions}
|
||||
value={config.requiredFields ?? []}
|
||||
onChange={handleChange("requiredFields")}
|
||||
/>
|
||||
<Divider />
|
||||
<Typography variant="overline">Confirmation Template</Typography>
|
||||
<Typography variant="body2">
|
||||
The action button will not ask for confirmation if this is left empty
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label="Confirmation Template"
|
||||
placeholder="Are sure you want to invest {{stockName}}?"
|
||||
value={config.confirmation}
|
||||
onChange={(e) => {
|
||||
handleChange("confirmation")(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config.isActionScript}
|
||||
onChange={() =>
|
||||
handleChange("isActionScript")(!Boolean(config.isActionScript))
|
||||
}
|
||||
name="actionScript"
|
||||
/>
|
||||
}
|
||||
label="Set as an action script"
|
||||
/>
|
||||
{!Boolean(config.isActionScript) ? (
|
||||
<TextField
|
||||
label="callable name"
|
||||
name="callableName"
|
||||
value={config.callableName}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
handleChange("callableName")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="overline">action script</Typography>
|
||||
<Suspense fallback={<FieldSkeleton height={300} />}>
|
||||
<CodeEditor
|
||||
height={300}
|
||||
script={config.script}
|
||||
extraLibs={[
|
||||
[
|
||||
"declare class ref {",
|
||||
" /**",
|
||||
" * Reference object of the row running the action script",
|
||||
" */",
|
||||
"static id:string",
|
||||
"static path:string",
|
||||
"static parentId:string",
|
||||
"static tablePath:string",
|
||||
"}",
|
||||
].join("\n"),
|
||||
[
|
||||
"declare class actionParams {",
|
||||
" /**",
|
||||
" * actionParams are provided by dialog popup form",
|
||||
" */",
|
||||
(config.params ?? []).map((param) => {
|
||||
const validationKeys = Object.keys(param.validation);
|
||||
if (validationKeys.includes("string")) {
|
||||
return `static ${param.name}:string`;
|
||||
} else if (validationKeys.includes("array")) {
|
||||
return `static ${param.name}:any[]`;
|
||||
} else return `static ${param.name}:any`;
|
||||
}),
|
||||
"}",
|
||||
],
|
||||
]}
|
||||
handleChange={handleChange("script")}
|
||||
/>
|
||||
</Suspense>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config["redo.enabled"]}
|
||||
onChange={() =>
|
||||
handleChange("redo.enabled")(!Boolean(config["redo.enabled"]))
|
||||
}
|
||||
name="redo toggle"
|
||||
/>
|
||||
}
|
||||
label="enable redo(reruns the same script)"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config["undo.enabled"]}
|
||||
onChange={() =>
|
||||
handleChange("undo.enabled")(!Boolean(config["undo.enabled"]))
|
||||
}
|
||||
name="undo toggle"
|
||||
/>
|
||||
}
|
||||
label="enable undo"
|
||||
/>
|
||||
{config["undo.enabled"] && (
|
||||
<>
|
||||
<Typography variant="overline">
|
||||
Undo Confirmation Template
|
||||
</Typography>
|
||||
<TextField
|
||||
label="template"
|
||||
placeholder="are you sure you want to sell your stocks in {{stockName}}"
|
||||
value={config["undo.confirmation"]}
|
||||
onChange={(e) => {
|
||||
handleChange("undo.confirmation")(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<Typography variant="overline">Undo Action script</Typography>
|
||||
<Suspense fallback={<FieldSkeleton height={300} />}>
|
||||
<CodeEditor
|
||||
height={300}
|
||||
script={config["undo.script"]}
|
||||
handleChange={handleChange("undo.script")}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Settings;
|
||||
84
www/src/components/fields/Action/SideDrawerField.tsx
Normal file
84
www/src/components/fields/Action/SideDrawerField.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
import { Controller, useWatch } from "react-hook-form";
|
||||
import { ISideDrawerFieldProps } from "../types";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
Grid,
|
||||
Typography,
|
||||
Link,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import ActionFab from "./ActionFab";
|
||||
import { useFieldStyles } from "components/SideDrawer/Form/utils";
|
||||
import { sanitiseCallableName, isUrl } from "utils/fns";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
labelGridItem: { width: `calc(100% - 56px - ${theme.spacing(2)}px)` },
|
||||
label: {
|
||||
whiteSpace: "normal",
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function Action({
|
||||
column,
|
||||
control,
|
||||
docRef,
|
||||
disabled,
|
||||
}: ISideDrawerFieldProps) {
|
||||
const classes = useStyles();
|
||||
const fieldClasses = useFieldStyles();
|
||||
|
||||
const row = useWatch({ control });
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={column.key}
|
||||
render={({ onChange, value }) => {
|
||||
const hasRan = value && value.status;
|
||||
|
||||
return (
|
||||
<Grid container alignItems="center" wrap="nowrap" spacing={2}>
|
||||
<Grid item xs className={classes.labelGridItem}>
|
||||
<div className={fieldClasses.root}>
|
||||
<Typography variant="body1" className={classes.label}>
|
||||
{hasRan && isUrl(value.status) ? (
|
||||
<Link
|
||||
href={value.status}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="body2"
|
||||
underline="always"
|
||||
>
|
||||
{value.status}
|
||||
</Link>
|
||||
) : hasRan ? (
|
||||
value.status
|
||||
) : (
|
||||
sanitiseCallableName(column.key)
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<ActionFab
|
||||
row={{ ...row, ref: docRef }}
|
||||
column={{ config: column.config, key: column.key }}
|
||||
onSubmit={onChange}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from "react";
|
||||
import { CustomCellProps } from "../withCustomCell";
|
||||
import { IHeavyCellProps } from "../types";
|
||||
import clsx from "clsx";
|
||||
|
||||
import _get from "lodash/get";
|
||||
|
||||
import { createStyles, makeStyles, Grid } from "@material-ui/core";
|
||||
|
||||
import { sanitiseCallableName, isUrl } from "utils/fns";
|
||||
import ActionFab from "./ActionFab";
|
||||
import { sanitiseCallableName, isUrl } from "utils/fns";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
@@ -20,10 +20,12 @@ export default function Action({
|
||||
row,
|
||||
value,
|
||||
onSubmit,
|
||||
}: CustomCellProps) {
|
||||
disabled,
|
||||
}: IHeavyCellProps) {
|
||||
const classes = useStyles();
|
||||
const { name } = column as any;
|
||||
|
||||
const hasRan = value && value.status;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
@@ -39,7 +41,7 @@ export default function Action({
|
||||
) : hasRan ? (
|
||||
value.status
|
||||
) : (
|
||||
sanitiseCallableName(name)
|
||||
sanitiseCallableName(column.key)
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
@@ -49,6 +51,10 @@ export default function Action({
|
||||
column={column}
|
||||
onSubmit={onSubmit}
|
||||
value={value}
|
||||
size="small"
|
||||
color="secondary"
|
||||
style={{ width: 36, height: 36 }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
32
www/src/components/fields/Action/index.tsx
Normal file
32
www/src/components/fields/Action/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React, { lazy } from "react";
|
||||
import { IFieldConfig, FieldType } from "components/fields/types";
|
||||
import withHeavyCell from "../_withTableCell/withHeavyCell";
|
||||
|
||||
import ActionIcon from "assets/icons/Action";
|
||||
import BasicCell from "./BasicCell";
|
||||
import NullEditor from "components/Table/editors/NullEditor";
|
||||
|
||||
const TableCell = lazy(
|
||||
() => import("./TableCell" /* webpackChunkName: "TableCell-Action" */)
|
||||
);
|
||||
const SideDrawerField = lazy(
|
||||
() =>
|
||||
import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Action" */)
|
||||
);
|
||||
const Settings = lazy(
|
||||
() => import("./Settings" /* webpackChunkName: "Settings-Action" */)
|
||||
);
|
||||
export const config: IFieldConfig = {
|
||||
type: FieldType.action,
|
||||
name: "Action",
|
||||
dataType: "any",
|
||||
initialValue: {},
|
||||
icon: <ActionIcon />,
|
||||
description:
|
||||
"A button with a pre-defined action. Triggers a Cloud Function. 3 different states: Disabled, Enabled, Active (Clicked). Supports Undo and Redo.",
|
||||
TableCell: withHeavyCell(BasicCell, TableCell),
|
||||
TableEditor: NullEditor,
|
||||
SideDrawerField,
|
||||
settings: Settings,
|
||||
};
|
||||
export default config;
|
||||
97
www/src/components/fields/Aggregate/Settings.tsx
Normal file
97
www/src/components/fields/Aggregate/Settings.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { useState, lazy, Suspense } from "react";
|
||||
import {
|
||||
Typography,
|
||||
IconButton,
|
||||
TextField,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Divider,
|
||||
} from "@material-ui/core";
|
||||
import MultiSelect from "@antlerengineering/multiselect";
|
||||
import FieldSkeleton from "components/SideDrawer/Form/FieldSkeleton";
|
||||
import { FieldType } from "constants/fields";
|
||||
import FieldsDropdown from "components/Table/ColumnMenu/FieldsDropdown";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
const CodeEditor = lazy(
|
||||
() =>
|
||||
import(
|
||||
"components/Table/editors/CodeEditor" /* webpackChunkName: "CodeEditor" */
|
||||
)
|
||||
);
|
||||
|
||||
const Settings = ({ config, handleChange }) => {
|
||||
const { tableState } = useFiretableContext();
|
||||
|
||||
const columnOptions = Object.values(tableState?.columns ?? {})
|
||||
.filter((column) => column.type === FieldType.subTable)
|
||||
.map((c) => ({ label: c.name, value: c.key }));
|
||||
return (
|
||||
<>
|
||||
<MultiSelect
|
||||
label={"Sub Tables"}
|
||||
options={columnOptions}
|
||||
value={config.requiredFields ?? []}
|
||||
onChange={handleChange("subtables")}
|
||||
/>
|
||||
<Typography variant="overline">Aggergate script</Typography>
|
||||
<Suspense fallback={<FieldSkeleton height={200} />}>
|
||||
<CodeEditor
|
||||
script={
|
||||
config.script ??
|
||||
`//triggerType: create | update | delete\n//aggregateState: the subtable accumenlator stored in the cell of this column\n//snapshot: the triggered document snapshot of the the subcollection\n//incrementor: short for firebase.firestore.FieldValue.increment(n);\n//This script needs to return the new aggregateState cell value.
|
||||
switch (triggerType){
|
||||
case "create":return {
|
||||
count:incrementor(1)
|
||||
}
|
||||
case "update":return {}
|
||||
case "delete":
|
||||
return {
|
||||
count:incrementor(-1)
|
||||
}
|
||||
}`
|
||||
}
|
||||
extraLibs={[
|
||||
` /**
|
||||
* increaments firestore field value
|
||||
*/",
|
||||
function incrementor(value:number):number {
|
||||
|
||||
}`,
|
||||
]}
|
||||
handleChange={handleChange("script")}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<Typography variant="overline">Field type of the output</Typography>
|
||||
<FieldsDropdown
|
||||
value={config.renderFieldType}
|
||||
options={Object.values(FieldType).filter(
|
||||
(f) =>
|
||||
![
|
||||
FieldType.derivative,
|
||||
FieldType.aggregate,
|
||||
FieldType.subTable,
|
||||
FieldType.action,
|
||||
].includes(f)
|
||||
)}
|
||||
onChange={(newType: any) => {
|
||||
handleChange("renderFieldType")(newType.target.value);
|
||||
}}
|
||||
/>
|
||||
{config.renderFieldType && (
|
||||
<>
|
||||
<Typography variant="overline">Rendered field config</Typography>
|
||||
{/* <ConfigFields
|
||||
fieldType={config.renderFieldType}
|
||||
config={config}
|
||||
handleChange={handleChange}
|
||||
tables={tables}
|
||||
columns={columns}
|
||||
roles={roles}
|
||||
/> */}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Settings;
|
||||
22
www/src/components/fields/Aggregate/index.tsx
Normal file
22
www/src/components/fields/Aggregate/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { IFieldConfig, FieldType } from "components/fields/types";
|
||||
import withBasicCell from "../_withTableCell/withBasicCell";
|
||||
|
||||
import AggregateIcon from "@material-ui/icons/Layers";
|
||||
import BasicCell from "../_BasicCell/BasicCellNull";
|
||||
import NullEditor from "components/Table/editors/NullEditor";
|
||||
|
||||
export const config: IFieldConfig = {
|
||||
type: FieldType.aggregate,
|
||||
name: "Aggregate",
|
||||
dataType: "string",
|
||||
initialValue: "",
|
||||
initializable: false,
|
||||
icon: <AggregateIcon />,
|
||||
description:
|
||||
"Value aggregated from a specified sub-table of the row. Displayed using any other field type. Requires Cloud Function setup.",
|
||||
TableCell: withBasicCell(BasicCell),
|
||||
TableEditor: NullEditor,
|
||||
SideDrawerField: BasicCell as any,
|
||||
};
|
||||
export default config;
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { IFieldProps } from "../utils";
|
||||
import { ISideDrawerFieldProps } from "../types";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
@@ -8,28 +9,21 @@ import {
|
||||
ButtonBase,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
SwitchProps as MuiSwitchProps,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import { useFieldStyles } from "components/SideDrawer/Form/utils";
|
||||
import { useSwitchStyles } from "./styles";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
buttonBase: {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor:
|
||||
theme.palette.type === "light"
|
||||
? "rgba(0, 0, 0, 0.09)"
|
||||
: "rgba(255, 255, 255, 0.09)",
|
||||
padding: theme.spacing(9 / 8, 1, 9 / 8, 1.5),
|
||||
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
textAlign: "left",
|
||||
},
|
||||
root: { padding: 0 },
|
||||
|
||||
formControlLabel: {
|
||||
margin: 0,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
|
||||
padding: theme.spacing(9 / 8, 1, 9 / 8, 1.5),
|
||||
},
|
||||
|
||||
label: {
|
||||
@@ -39,48 +33,40 @@ const useStyles = makeStyles((theme) =>
|
||||
})
|
||||
);
|
||||
|
||||
export interface ICheckboxProps
|
||||
extends IFieldProps,
|
||||
Omit<MuiSwitchProps, "name"> {
|
||||
label?: React.ReactNode;
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
export default function Checkbox({
|
||||
column,
|
||||
control,
|
||||
docRef,
|
||||
label,
|
||||
name,
|
||||
editable,
|
||||
...props
|
||||
}: ICheckboxProps) {
|
||||
disabled,
|
||||
}: ISideDrawerFieldProps) {
|
||||
const classes = useStyles();
|
||||
const fieldClasses = useFieldStyles();
|
||||
const switchClasses = useSwitchStyles();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
name={column.key}
|
||||
render={({ onChange, onBlur, value }) => {
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(event.target.checked);
|
||||
};
|
||||
|
||||
const handleClick = () => onChange(!value);
|
||||
|
||||
return (
|
||||
<ButtonBase className={classes.buttonBase} onClick={handleClick}>
|
||||
<ButtonBase
|
||||
className={clsx(fieldClasses.root, classes.root)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
color="secondary"
|
||||
{...props}
|
||||
checked={value}
|
||||
onChange={handleChange}
|
||||
onBlur={onBlur}
|
||||
disabled={editable === false}
|
||||
disabled={disabled}
|
||||
classes={switchClasses}
|
||||
/>
|
||||
}
|
||||
label={label}
|
||||
label={column.name}
|
||||
labelPlacement="start"
|
||||
classes={{ root: classes.formControlLabel, label: classes.label }}
|
||||
/>
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
import { IHeavyCellProps } from "../types";
|
||||
import _get from "lodash/get";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
} from "@material-ui/core";
|
||||
|
||||
import Confirmation from "components/Confirmation";
|
||||
import { useSwitchStyles } from "./styles";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
@@ -22,13 +24,6 @@ const useStyles = makeStyles((theme) =>
|
||||
width: "calc(100% - 58px)",
|
||||
overflowX: "hidden",
|
||||
},
|
||||
|
||||
// switchBase: {
|
||||
// "&$switchChecked": { color: green["A700"] },
|
||||
// "&$switchChecked + $switchTrack": { backgroundColor: green["A700"] },
|
||||
// },
|
||||
// switchChecked: {},
|
||||
// switchTrack: {},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -43,28 +38,26 @@ export default function Checkbox({
|
||||
column,
|
||||
value,
|
||||
onSubmit,
|
||||
}: CustomCellProps) {
|
||||
disabled,
|
||||
}: IHeavyCellProps) {
|
||||
const classes = useStyles();
|
||||
const switchClasses = useSwitchStyles();
|
||||
|
||||
let component = (
|
||||
<Switch
|
||||
checked={!!value}
|
||||
onChange={() => onSubmit(!value)}
|
||||
disabled={!column.editable}
|
||||
classes={
|
||||
{
|
||||
// checked: classes.switchChecked,
|
||||
// track: classes.switchTrack,
|
||||
}
|
||||
}
|
||||
disabled={disabled}
|
||||
classes={switchClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
if ((column as any)?.config?.confirmation)
|
||||
if (column?.config?.confirmation)
|
||||
component = (
|
||||
<Confirmation
|
||||
message={{
|
||||
title: (column as any).config.confirmation.title,
|
||||
body: (column as any).config.confirmation.body.replace(
|
||||
title: column.config.confirmation.title,
|
||||
body: column.config.confirmation.body.replace(
|
||||
/\{\{(.*?)\}\}/g,
|
||||
replacer(row)
|
||||
),
|
||||
@@ -81,7 +74,7 @@ export default function Checkbox({
|
||||
label={column.name}
|
||||
labelPlacement="start"
|
||||
className="cell-collapse-padding"
|
||||
classes={classes}
|
||||
classes={{ root: classes.root, label: classes.label }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
36
www/src/components/fields/Checkbox/index.tsx
Normal file
36
www/src/components/fields/Checkbox/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React, { lazy } from "react";
|
||||
import { IFieldConfig, FieldType } from "components/fields/types";
|
||||
import withHeavyCell from "../_withTableCell/withHeavyCell";
|
||||
|
||||
import CheckboxIcon from "@material-ui/icons/CheckBox";
|
||||
import BasicCell from "../_BasicCell/BasicCellName";
|
||||
import NullEditor from "components/Table/editors/NullEditor";
|
||||
|
||||
const TableCell = lazy(
|
||||
() => import("./TableCell" /* webpackChunkName: "TableCell-Checkbox" */)
|
||||
);
|
||||
const SideDrawerField = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./SideDrawerField" /* webpackChunkName: "SideDrawerField-Checkbox" */
|
||||
)
|
||||
);
|
||||
|
||||
export const config: IFieldConfig = {
|
||||
type: FieldType.checkbox,
|
||||
name: "Checkbox",
|
||||
dataType: "boolean",
|
||||
initialValue: false,
|
||||
initializable: true,
|
||||
icon: <CheckboxIcon />,
|
||||
description: "Either checked or unchecked. Unchecked by default.",
|
||||
TableCell: withHeavyCell(BasicCell, TableCell),
|
||||
TableEditor: NullEditor,
|
||||
csvImportParser: (value: string) => {
|
||||
if (["YES", "TRUE", "1"].includes(value.toUpperCase())) return true;
|
||||
else if (["NO", "FALSE", "0"].includes(value.toUpperCase())) return false;
|
||||
else return null;
|
||||
},
|
||||
SideDrawerField,
|
||||
};
|
||||
export default config;
|
||||
13
www/src/components/fields/Checkbox/styles.ts
Normal file
13
www/src/components/fields/Checkbox/styles.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { makeStyles, createStyles } from "@material-ui/core";
|
||||
import { green } from "@material-ui/core/colors";
|
||||
|
||||
export const useSwitchStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
switchBase: {
|
||||
"&$checked": { color: green["A700"] },
|
||||
"&$checked + $track": { backgroundColor: green["A700"] },
|
||||
},
|
||||
checked: {},
|
||||
track: {},
|
||||
})
|
||||
);
|
||||
25
www/src/components/fields/Code/BasicCell.tsx
Normal file
25
www/src/components/fields/Code/BasicCell.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
import { IBasicCellProps } from "../types";
|
||||
|
||||
import { useTheme } from "@material-ui/core";
|
||||
|
||||
export default function Code({ value }: IBasicCellProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
maxHeight: "100%",
|
||||
padding: theme.spacing(0.5, 0),
|
||||
|
||||
whiteSpace: "pre-wrap",
|
||||
lineHeight: theme.typography.body2.lineHeight,
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
49
www/src/components/fields/Code/SideDrawerField.tsx
Normal file
49
www/src/components/fields/Code/SideDrawerField.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { ISideDrawerFieldProps } from "../types";
|
||||
|
||||
import CodeEditor from "components/CodeEditor";
|
||||
import { makeStyles, createStyles } from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
wrapper: {
|
||||
border: "1px solid",
|
||||
borderColor:
|
||||
theme.palette.type === "light"
|
||||
? "rgba(0, 0, 0, 0.09)"
|
||||
: "rgba(255, 255, 255, 0.09)",
|
||||
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
overflow: "hidden",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function Code({
|
||||
control,
|
||||
column,
|
||||
disabled,
|
||||
}: ISideDrawerFieldProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={column.key}
|
||||
render={({ onChange, value }) => (
|
||||
<CodeEditor
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
wrapperProps={{ className: classes.wrapper }}
|
||||
editorOptions={{
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
www/src/components/fields/Code/index.tsx
Normal file
26
www/src/components/fields/Code/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { lazy } from "react";
|
||||
import { IFieldConfig, FieldType } from "components/fields/types";
|
||||
import withBasicCell from "../_withTableCell/withBasicCell";
|
||||
|
||||
import CodeIcon from "@material-ui/icons/Code";
|
||||
import BasicCell from "./BasicCell";
|
||||
import withSideDrawerEditor from "components/Table/editors/withSideDrawerEditor";
|
||||
|
||||
const SideDrawerField = lazy(
|
||||
() =>
|
||||
import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Code" */)
|
||||
);
|
||||
|
||||
export const config: IFieldConfig = {
|
||||
type: FieldType.code,
|
||||
name: "Code",
|
||||
dataType: "string",
|
||||
initialValue: "",
|
||||
initializable: true,
|
||||
icon: <CodeIcon />,
|
||||
description: "Raw code editable with Monaco Editor.",
|
||||
TableCell: withBasicCell(BasicCell),
|
||||
TableEditor: withSideDrawerEditor(BasicCell),
|
||||
SideDrawerField,
|
||||
};
|
||||
export default config;
|
||||
58
www/src/components/fields/Color/InlineCell.tsx
Normal file
58
www/src/components/fields/Color/InlineCell.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import { IPopoverInlineCellProps } from "../types";
|
||||
|
||||
import { makeStyles, createStyles, Grid, ButtonBase } from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
font: "inherit",
|
||||
color: "inherit !important",
|
||||
letterSpacing: "inherit",
|
||||
textAlign: "inherit",
|
||||
|
||||
padding: theme.spacing(0, 1),
|
||||
},
|
||||
|
||||
colorIndicator: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
|
||||
boxShadow: `0 0 0 1px ${theme.palette.text.disabled} inset`,
|
||||
borderRadius: theme.shape.borderRadius / 2,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export const Color = React.forwardRef(function Color(
|
||||
{ value, showPopoverCell, disabled }: IPopoverInlineCellProps,
|
||||
ref: React.Ref<any>
|
||||
) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
className={clsx("cell-collapse-padding", classes.root)}
|
||||
component={ButtonBase}
|
||||
onClick={() => showPopoverCell(true)}
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Grid item>
|
||||
<div
|
||||
className={classes.colorIndicator}
|
||||
style={{ backgroundColor: value?.hex }}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs>
|
||||
{value?.hex}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
export default Color;
|
||||
12
www/src/components/fields/Color/PopoverCell.tsx
Normal file
12
www/src/components/fields/Color/PopoverCell.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import { IPopoverCellProps } from "../types";
|
||||
import { ChromePicker } from "react-color";
|
||||
import _get from "lodash/get";
|
||||
|
||||
export default function Color({ value, onSubmit }: IPopoverCellProps) {
|
||||
const handleChangeComplete = (color) => onSubmit(color);
|
||||
|
||||
return (
|
||||
<ChromePicker color={value?.rgb} onChangeComplete={handleChangeComplete} />
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user