Merge branch 'develop' into FT_function

This commit is contained in:
AntlerEngineering
2021-02-23 02:45:15 +11:00
committed by GitHub
213 changed files with 6557 additions and 4962 deletions

3
.gitignore vendored
View File

@@ -46,5 +46,4 @@ node_modules/
cloud_functions/functions/src/functionConfig.json
*.iml
.idea
*-firebase.json
*-firebase.json

View File

@@ -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);
}

View File

@@ -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…

View File

@@ -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",

View File

@@ -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"

View File

@@ -333,6 +333,7 @@ export const defaultOverrides = (theme: Theme): ThemeOptions => ({
valueLabel: {
top: -22,
left: "calc(-25%)",
...theme.typography.caption,
color: theme.palette.primary.main,

View 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>
);
}

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -1 +0,0 @@
export { useDatePicker } from "./Context";

View File

@@ -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: () => {},
};

View File

@@ -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>
</>
);

View 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"),
},
];

View 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}
/>
</>
);
}

View File

@@ -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}

View File

@@ -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 doesnt 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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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} />}
/>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}}
/>
);
}

View File

@@ -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>
)}
/>
);
}

View File

@@ -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} />
)}
/>
);
}

View File

@@ -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>
)}
</>
)}
/>
);
}

View File

@@ -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>
);
}}
/>
);
}

View File

@@ -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>
);
}

View File

@@ -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}
/>
);
}}
/>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
)}
/>
);
}

View 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 forms values and errors when the Firestore docs 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 hasnt 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;
}

View File

@@ -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 wont 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"

View File

@@ -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,
},
})
);

View File

@@ -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>

View 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} />;
});

View 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>
);
}

View File

@@ -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"

View File

@@ -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 rows 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 tables 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>
)}
</>
);
}

View File

@@ -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;
}

View 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",
},
}}
/>
);
}

View File

@@ -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>

View File

@@ -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",
},
}}
/>
);
}

View File

@@ -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",
},
}}
/>
);
}

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View 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,
}}
/>
);
}

View File

@@ -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",
},
}}
/>
);
}

View File

@@ -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();
}}
/>
</>
)}

View File

@@ -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()

View File

@@ -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"

View File

@@ -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-grids 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;

View File

@@ -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;

View File

@@ -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-grids 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;
}
};

View 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-grids 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;
};
}

View File

@@ -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>
);
}

View File

@@ -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>
</>
);
}

View File

@@ -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
)}
/>
);
}

View File

@@ -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
)}
/>
);
}

View File

@@ -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 <></>;
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;
}

View File

@@ -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 }}
/>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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 />;
}
};

View File

@@ -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-grids 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;

View File

@@ -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>
) : (

View File

@@ -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",

View File

@@ -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}>

View File

@@ -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,

View File

@@ -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

View File

@@ -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}
/>

View File

@@ -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}

View File

@@ -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>
);
};
}

View 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}</>;
}

View 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;

View 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>
);
}}
/>
);
}

View File

@@ -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>

View 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;

View 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;

View 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;

View File

@@ -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 }}
/>

View File

@@ -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 }}
/>
);
}

View 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;

View 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: {},
})
);

View 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>
);
}

View 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,
},
}}
/>
)}
/>
);
}

View 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;

View 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;

View 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