update extensions UI

This commit is contained in:
Sidney Alcantara
2021-08-30 16:33:23 +10:00
parent 3e34e0f2a1
commit 1585fcf3f0
15 changed files with 706 additions and 661 deletions

View File

@@ -103,6 +103,8 @@ export default function CodeEditor({
options={{
readOnly: disabled,
fontFamily: theme.typography.fontFamilyMono,
rulers: [80],
minimap: { enabled: false },
...editorOptions,
}}
onChange={onChange as any}

View File

@@ -1,21 +1,12 @@
import { useFiretableContext } from "contexts/FiretableContext";
import { Box, Tooltip, Button, Chip } from "@material-ui/core";
import {
Stack,
Typography,
Grid,
Tooltip,
Chip,
Button,
} from "@material-ui/core";
import OpenIcon from "@material-ui/icons/OpenInNew";
function AvailableValueTag({ label, details }) {
return (
<Tooltip
style={{
zIndex: 9999,
marginRight: 4,
}}
title={<>{details}</>}
>
<Chip label={label} size="small" />
</Tooltip>
);
}
export interface ICodeEditorHelperProps {
docLink: string;
additionalVariables?: {
@@ -28,7 +19,6 @@ export default function CodeEditorHelper({
docLink,
additionalVariables,
}: ICodeEditorHelperProps) {
const { tableState } = useFiretableContext();
const availableVariables = [
{
key: "row",
@@ -55,22 +45,42 @@ export default function CodeEditorHelper({
description: `utilFns provides a set of functions that are commonly used, such as easy access to GCP Secret Manager`,
},
];
return (
<Box marginBottom={1} display="flex" justifyContent="space-between">
<Box>
You have access to:{" "}
<Stack
direction="row"
spacing={0.25}
alignItems="baseline"
justifyContent="space-between"
sx={{ mb: 1 }}
>
<Typography
variant="body2"
color="textSecondary"
style={{ flexShrink: 0 }}
>
You can access:
</Typography>
<Grid container spacing={0.5}>
{availableVariables.concat(additionalVariables ?? []).map((v) => (
<AvailableValueTag label={v.key} details={v.description} />
<Grid item key={v.key}>
<Tooltip title={v.description}>
<Chip label={v.key} size="small" />
</Tooltip>
</Grid>
))}
</Box>
</Grid>
<Button
size="small"
endIcon={<OpenIcon />}
target="_blank"
href={docLink}
style={{ flexShrink: 0 }}
>
Examples & Docs
</Button>
</Box>
</Stack>
);
}

View File

@@ -64,9 +64,7 @@ export default function Confirmation({
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
{cancel ?? "Cancel"}
</Button>
<Button onClick={handleClose}>{cancel ?? "Cancel"}</Button>
<Button
onClick={() => {
handleConfirm();

View File

@@ -2,9 +2,15 @@ import clsx from "clsx";
import Div100vh from "react-div-100vh";
import { makeStyles, createStyles } from "@material-ui/styles";
import { Grid, GridProps, Typography, SvgIconTypeMap } from "@material-ui/core";
import {
Grid,
GridProps,
Stack,
Typography,
SvgIconTypeMap,
} from "@material-ui/core";
import { OverridableComponent } from "@material-ui/core/OverridableComponent";
import ErrorIcon from "@material-ui/icons/ErrorOutlined";
import ErrorIcon from "@material-ui/icons/ErrorOutline";
const useStyles = makeStyles((theme) =>
createStyles({
@@ -12,9 +18,13 @@ const useStyles = makeStyles((theme) =>
height: "100%",
width: "100%",
textAlign: "center",
...theme.typography.body2,
},
content: { maxWidth: "25em" },
content: {
"&&": { maxWidth: "25em" },
},
icon: {
color: theme.palette.action.active,
@@ -95,7 +105,11 @@ export default function EmptyState({
{message}
</Typography>
{description && <Typography variant="body2">{description}</Typography>}
{description && (
<Stack spacing={2} alignItems="center">
{description}
</Stack>
)}
</Grid>
</Grid>
);

View File

@@ -1,32 +1,15 @@
import React from "react";
import EmptyState, { IEmptyStateProps } from "./EmptyState";
import { Stack, Button } from "@material-ui/core";
import { Button } from "@material-ui/core";
import ReloadIcon from "@material-ui/icons/Refresh";
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
import meta from "../../package.json";
class ErrorBoundary extends React.Component<IEmptyStateProps> {
state = { hasError: false, errorMessage: "" };
static getDerivedStateFromError(error: Error) {
// Update state so the next render will show the fallback UI.
// Special error message for chunk loading failures:
if (error.message.startsWith("Loading chunk"))
return {
hasError: true,
errorMessage: (
<Stack spacing={2} alignItems="center">
<span>{error.message}</span>
<Button
variant="outlined"
color="secondary"
startIcon={<ReloadIcon />}
onClick={() => window.location.reload()}
>
Reload
</Button>
</Stack>
),
};
return { hasError: true, errorMessage: error.message };
}
@@ -42,7 +25,33 @@ class ErrorBoundary extends React.Component<IEmptyStateProps> {
return (
<EmptyState
message="Something Went Wrong"
description={this.state.errorMessage}
description={
<>
<span>{this.state.errorMessage}</span>
{this.state.errorMessage.startsWith("Loading chunk") ? (
<Button
variant="outlined"
color="secondary"
startIcon={<ReloadIcon />}
onClick={() => window.location.reload()}
>
Reload
</Button>
) : (
<Button
href={
meta.repository.url.replace(".git", "") +
"/issues/new/choose"
}
target="_blank"
rel="noopener noreferrer"
endIcon={<OpenInNewIcon />}
>
Report Issue
</Button>
)}
</>
}
fullScreen
{...this.props}
/>

View File

@@ -62,7 +62,7 @@ const useStyles = makeStyles((theme) =>
padding: "0 var(--spacing-modal)",
margin: "0 calc(var(--spacing-modal) * -1)",
...theme.typography.body1,
...theme.typography.body2,
backgroundRepeat: "no-repeat",
backgroundColor: "var(--bg-paper)",

View File

@@ -83,13 +83,7 @@ export default function FormDialog({
fullWidth
onChange={(e) => setFieldKey(e.target.value)}
disabled={type === FieldType.id && fieldKey === "id"}
helperText={
<>
Set the Firestore field key to link to this column.
<br />
It will display any existing data for this field key.
</>
}
helperText="Set the Firestore field key to link to this column. It will display any existing data for this field key."
FormHelperTextProps={{ classes: { root: classes.helperText } }}
/>
</section>

View File

@@ -1,11 +1,14 @@
import { useState } from "react";
import { useState, useRef } from "react";
import { format, formatRelative } from "date-fns";
import { makeStyles, createStyles } from "@material-ui/styles";
import {
Stack,
ButtonBase,
List,
ListItem,
ListItemText,
Avatar,
Box,
Button,
Divider,
IconButton,
Menu,
MenuItem,
@@ -13,46 +16,14 @@ import {
Tooltip,
Typography,
} from "@material-ui/core";
import moment from "moment";
import { extensionTypes, IExtension, IExtensionType } from "./utils";
import EmptyState from "components/EmptyState";
import AddIcon from "@material-ui/icons/Add";
import EmptyIcon from "@material-ui/icons/AddBox";
import DuplicateIcon from "@material-ui/icons/FileCopy";
import ExtensionIcon from "assets/icons/Extension";
import DuplicateIcon from "@material-ui/icons/ContentCopy";
import EditIcon from "@material-ui/icons/Edit";
import DeleteIcon from "@material-ui/icons/DeleteForever";
import { useRef } from "react";
const useStyles = makeStyles((theme) =>
createStyles({
hoverableEmptyState: {
borderRadius: theme.spacing(1),
cursor: "pointer",
padding: theme.spacing(2),
"&:hover": {
background: theme.palette.background.paper,
},
},
divider: {
margin: theme.spacing(1, 0),
},
extensionName: {
marginTop: theme.spacing(1),
},
extensionType: {
marginBottom: theme.spacing(1),
},
avatar: {
marginRight: theme.spacing(1),
width: theme.spacing(4),
height: theme.spacing(4),
},
extensionList: {
height: "50vh",
overflowY: "scroll",
},
})
);
import EmptyState from "components/EmptyState";
import { extensionTypes, IExtension, IExtensionType } from "./utils";
export interface IExtensionListProps {
extensions: IExtension[];
@@ -73,7 +44,6 @@ export default function ExtensionList({
}: IExtensionListProps) {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const addButtonRef = useRef(null);
const classes = useStyles();
const activeExtensionCount = extensions.filter(
(extension) => extension.active
@@ -94,27 +64,35 @@ export default function ExtensionList({
return (
<>
<Box
display="flex"
<Stack
direction="row"
spacing={2}
justifyContent="space-between"
alignItems="center"
marginTop={"0px !important"}
style={{ marginTop: 0 }}
>
<Typography variant="overline">
EXTENSION ({activeExtensionCount}/{extensions.length})
<Typography
variant="subtitle2"
component="h2"
style={{ fontFeatureSettings: "'case', 'tnum'" }}
>
Extensions ({activeExtensionCount} / {extensions.length})
</Typography>
<Button
color="primary"
startIcon={<AddIcon />}
onClick={handleAddButton}
ref={addButtonRef}
>
ADD EXTENTION
Add Extension
</Button>
<Menu
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
transformOrigin={{ vertical: "top", horizontal: "right" }}
>
{extensionTypes.map((type) => (
<MenuItem
@@ -126,121 +104,117 @@ export default function ExtensionList({
</MenuItem>
))}
</Menu>
</Box>
</Stack>
<Box className={classes.extensionList}>
{extensions.length === 0 && (
{extensions.length === 0 ? (
<ButtonBase
onClick={handleAddButton}
sx={{
width: "100%",
height: 72 * 3,
borderRadius: 1,
"&:hover": { bgcolor: "action.hover" },
}}
>
<EmptyState
message="Add your first extension"
description={
"When you add extentions, your extentions should be shown here."
}
Icon={EmptyIcon}
className={classes.hoverableEmptyState}
onClick={handleAddButton}
message="Add Your First Extension"
description="Your extensions will appear here."
Icon={ExtensionIcon}
/>
)}
{extensions.map((extensionObject, index) => {
return (
<>
<Box
display="flex"
flexDirection="row"
justifyContent="space-between"
>
<Box
display="flex"
flexDirection="column"
justifyContent="space-between"
>
<Typography variant="body2" className={classes.extensionName}>
{extensionObject.name}
</Typography>
<Typography
variant="overline"
className={classes.extensionType}
>
{extensionObject.type}
</Typography>
</Box>
<Box
display="flex"
flexDirection="column"
justifyContent="space-between"
alignItems="flex-end"
>
<Box display="flex" alignItems="center">
</ButtonBase>
) : (
<List style={{ paddingTop: 0, minHeight: 72 * 3 }}>
{extensions.map((extensionObject, index) => (
<ListItem
disableGutters
dense={false}
divider={index !== extensions.length - 1}
children={
<ListItemText
primary={extensionObject.name}
secondary={extensionObject.type}
/>
}
secondaryAction={
<Stack alignItems="flex-end">
<Stack direction="row" alignItems="center" spacing={1}>
<Tooltip
title={extensionObject.active ? "Deactivate" : "Activate"}
>
<Switch
color="primary"
checked={extensionObject.active}
onClick={() => {
handleUpdateActive(index, !extensionObject.active);
}}
onClick={() =>
handleUpdateActive(index, !extensionObject.active)
}
inputProps={{ "aria-label": "Activate" }}
sx={{ mr: 1 }}
/>
</Tooltip>
<Tooltip title={"Edit"}>
<Tooltip title="Duplicate">
<IconButton
color="secondary"
onClick={() => {
handleEdit(index);
}}
>
<EditIcon />
</IconButton>
</Tooltip>
<Tooltip title={"Duplicate"}>
<IconButton
color="secondary"
onClick={() => {
handleDuplicate(index);
}}
aria-label="Duplicate"
onClick={() => handleDuplicate(index)}
>
<DuplicateIcon />
</IconButton>
</Tooltip>
<Tooltip title={"Delete"}>
<Tooltip title="Edit">
<IconButton
color="primary"
onClick={() => {
handleDelete(index);
}}
aria-label="Edit"
onClick={() => handleEdit(index)}
>
<EditIcon />
</IconButton>
</Tooltip>
<Tooltip title="Delete">
<IconButton
aria-label="Delete"
color="error"
onClick={() => handleDelete(index)}
sx={{ "&&": { mr: -1.5 } }}
>
<DeleteIcon />
</IconButton>
</Tooltip>
</Box>
</Stack>
<Tooltip
title={`Last updated by ${
extensionObject.lastEditor.displayName
} on ${moment(extensionObject.lastEditor.lastUpdate).format(
"LLLL"
)}`}
title={
<>
Last updated by {extensionObject.lastEditor.displayName}
<br />
on{" "}
{format(extensionObject.lastEditor.lastUpdate, "PPPP")}
<br />
at{" "}
{format(extensionObject.lastEditor.lastUpdate, "pppp")}
</>
}
>
<Box display="flex" alignItems="center">
<Avatar
alt="profile"
src={extensionObject.lastEditor.photoURL}
className={classes.avatar}
/>
<Typography variant="caption" color="textSecondary">
{moment(
extensionObject.lastEditor.lastUpdate
).calendar()}
<Stack direction="row" spacing={1} alignItems="center">
<Typography
variant="body2"
sx={{ color: "text.disabled" }}
>
{formatRelative(
extensionObject.lastEditor.lastUpdate,
new Date()
)}
</Typography>
</Box>
<Avatar
alt={`${extensionObject.lastEditor.displayName}s profile photo`}
src={extensionObject.lastEditor.photoURL}
sx={{ width: 24, height: 24, "&&": { mr: -0.5 } }}
/>
</Stack>
</Tooltip>
</Box>
</Box>
{index + 1 !== extensions.length && (
<Divider light className={classes.divider} />
)}
</>
);
})}
</Box>
</Stack>
}
/>
))}
</List>
)}
</>
);
}

View File

@@ -1,23 +1,17 @@
import { useState } from "react";
import { makeStyles, createStyles } from "@material-ui/styles";
import { sparkToExtensionObjects } from "./utils";
import firebase from "firebase/app";
import { Button, Link, Typography } from "@material-ui/core";
import LoadingButton from "@material-ui/lab/LoadingButton";
import DownloadIcon from "@material-ui/icons/FileDownloadOutlined";
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
import GoIcon from "@material-ui/icons/ChevronRight";
import Modal from "components/Modal";
import { useFiretableContext } from "contexts/FiretableContext";
import { useAppContext } from "contexts/AppContext";
import firebase from "firebase/app";
import { Box, Button, CircularProgress, Typography } from "@material-ui/core";
const useStyles = makeStyles((theme) =>
createStyles({
modalRoot: {
height: `calc(100vh - 200px)`,
},
download: {
maxWidth: 320,
marginTop: theme.spacing(0.5),
},
})
);
import { sparkToExtensionObjects } from "./utils";
import WIKI_LINKS from "constants/wikiLinks";
export interface IExtensionMigrationProps {
handleClose: () => void;
@@ -28,9 +22,9 @@ export default function ExtensionMigration({
handleClose,
handleUpgradeComplete,
}: IExtensionMigrationProps) {
const classes = useStyles();
const { tableState, tableActions } = useFiretableContext();
const appContext = useAppContext();
const { tableState, tableActions } = useFiretableContext();
const [isSaved, setIsSaved] = useState(false);
const [isUpgrading, setIsUpgrading] = useState(false);
@@ -78,60 +72,84 @@ export default function ExtensionMigration({
return (
<Modal
onClose={handleClose}
maxWidth="lg"
maxWidth="xs"
fullWidth
title={"Extensions Migration Guide"}
disableBackdropClick
disableEscapeKeyDown
title="Welcome to Extensions"
children={
<Box
display="flex"
flexDirection="column"
className={classes.modalRoot}
>
<Typography variant="body2">
We have upgraded spark editor to extension editor with a better UI.
The old sparks are not compatible with this change, however you can
use this tool to upgrade your sparks.
</Typography>
<br />
<>
<div>
<Typography paragraph>
It looks like you have Sparks configured for this table.
</Typography>
<Typography>
Sparks have been revamped to Extensions, with a brand new UI. Your
existing Sparks are not compatible with this change, but you can
migrate your Sparks to Extensions.
</Typography>
</div>
<Typography variant="overline">
1. Save your sparks for backup
</Typography>
<Typography variant="body2">
You must save your sparks before upgrade.
</Typography>
<Button
className={classes.download}
variant="contained"
color={isSaved ? "secondary" : "primary"}
onClick={downloadSparkFile}
>
Save sparks
</Button>
<br />
<div>
<Typography variant="subtitle1" component="h3" gutterBottom>
1. Back Up Existing Sparks
</Typography>
<Typography paragraph>
Back up your existing Sparks to a .ts file.
</Typography>
<Button
variant={isSaved ? "outlined" : "contained"}
color={isSaved ? "secondary" : "primary"}
onClick={downloadSparkFile}
endIcon={<DownloadIcon />}
style={{ width: "100%" }}
>
Save Sparks
</Button>
</div>
<Typography variant="overline">
2. Upgrade sparks to extensions
</Typography>
{/* TODO add documentation link */}
<Typography variant="body2">
After the upgrade, your old sparks will be removed from database.
And you might need to do some manual change to the code. See this
documentation for more information.
</Typography>
<Button
className={classes.download}
variant="contained"
onClick={upgradeToExtensions}
disabled={!isSaved || isUpgrading}
startIcon={
isUpgrading && <CircularProgress size={20} thickness={5} />
}
>
{isUpgrading && "Upgrading..."}
{!isUpgrading && "Upgrade to extensions"}
</Button>
</Box>
<div>
<Typography variant="subtitle1" component="h3" gutterBottom>
2. Migrate Sparks to Extensions
</Typography>
<Typography gutterBottom>
After the upgrade, Sparks will be removed from this table. You may
need to make manual changes to your Extensions code.
</Typography>
<Link
href={WIKI_LINKS.extensions}
target="_blank"
rel="noopener noreferrer"
paragraph
display="block"
>
Read the Extensions documentation
<OpenInNewIcon
aria-label="Open in new tab"
sx={{
fontSize: "1rem",
ml: 0.5,
verticalAlign: "middle",
}}
/>
</Link>
<LoadingButton
variant="contained"
color="primary"
loading={isUpgrading}
loadingPosition="end"
onClick={upgradeToExtensions}
disabled={!isSaved || isUpgrading}
endIcon={<GoIcon />}
style={{ width: "100%" }}
>
Migrate to Extensions
</LoadingButton>
</div>
</>
}
/>
);

View File

@@ -1,30 +1,39 @@
import { useState } from "react";
import _isEqual from "lodash/isEqual";
import useStateRef from "react-usestateref";
import { IExtension, triggerTypes } from "./utils";
import Modal from "components/Modal";
import CodeEditorHelper from "components/CodeEditorHelper";
import { useConfirmation } from "components/ConfirmationDialog";
import CodeEditor from "../../editors/CodeEditor";
import { useFiretableContext } from "contexts/FiretableContext";
import BackIcon from "@material-ui/icons/ArrowBack";
import AddIcon from "@material-ui/icons/AddBox";
import DeleteIcon from "@material-ui/icons/RemoveCircle";
import { makeStyles, createStyles } from "@material-ui/styles";
import {
AppBar,
Box,
styled,
Button,
Checkbox,
Divider,
FormControl,
FormControlLabel,
FormGroup,
FormLabel,
Grid,
IconButton,
Switch,
Stack,
Tab,
Tabs,
TextField,
Tooltip,
Typography,
} from "@material-ui/core";
import TabContext from "@material-ui/lab/TabContext";
import TabList from "@material-ui/lab/TabList";
import TabPanel from "@material-ui/lab/TabPanel";
import AddIcon from "@material-ui/icons/AddBox";
import DeleteIcon from "@material-ui/icons/RemoveCircle";
import Modal, { IModalProps } from "components/Modal";
import CodeEditor from "../../editors/CodeEditor";
import CodeEditorHelper from "components/CodeEditorHelper";
import { useConfirmation } from "components/ConfirmationDialog";
import { useFiretableContext } from "contexts/FiretableContext";
import { IExtension, triggerTypes } from "./utils";
import WIKI_LINKS from "constants/wikiLinks";
const additionalVariables = [
{
@@ -34,7 +43,7 @@ const additionalVariables = [
},
{
key: "triggerType",
description: "triggerType indicates the type of the extention invocation",
description: "triggerType indicates the type of the extension invocation",
},
{
key: "fieldTypes",
@@ -47,83 +56,21 @@ const additionalVariables = [
},
];
const useStyles = makeStyles((theme) =>
createStyles({
modalRoot: {
height: `calc(100vh - 250px)`,
},
metaRoot: {
marginBottom: theme.spacing(2),
},
tabWrapper: {
backgroundColor: theme.palette.background.default,
},
tabRoot: {
backgroundColor: theme.palette.background.paper,
},
tabPanel: {
padding: 0,
},
label: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(1),
display: "block",
},
hoverable: {
borderRadius: theme.spacing(1),
cursor: "pointer",
padding: theme.spacing(1, 0),
"&:hover": {
background: theme.palette.background.paper,
},
},
requiredFields: {
maxHeight: `max(300px, 30vh)`,
overflowY: "scroll",
},
addField: {
paddingLeft: 13, // align icons to the left
},
removeField: {
marginLeft: -3, // align icons to the left
},
})
);
const StyledTabPanel = styled(TabPanel)({
flexGrow: 1,
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
overflowY: "auto",
margin: "0 calc(var(--spacing-modal) * -1) 0 !important",
padding: "var(--spacing-modal) var(--spacing-modal) 0",
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
"&[hidden]": { display: "none" },
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
style={{ height: "100%" }}
{...other}
>
{value === index && (
<Box
style={{ height: "100%" }}
p={3}
display="flex"
flexDirection="column"
>
{children}
</Box>
)}
</div>
);
}
display: "flex",
flexDirection: "column",
});
export interface IExtensionModalProps {
handleClose: () => void;
handleClose: IModalProps["onClose"];
handleAdd: (extensionObject: IExtension) => void;
handleUpdate: (extensionObject: IExtension) => void;
mode: "add" | "update";
@@ -141,30 +88,19 @@ export default function ExtensionModal({
const [extensionObject, setExtensionObject] = useState<IExtension>(
initialObject
);
const [tabIndex, setTabIndex] = useState(0);
const [tab, setTab] = useState("triggersRequirements");
const [validation, setValidation, validationRef] = useStateRef({
condition: true,
extensionBody: true,
});
const [
conditionEditorActive,
setConditionEditorActive,
conditionEditorActiveRef,
] = useStateRef(false);
const [
bodyEditorActive,
setBodyEditorActive,
bodyEditorActiveRef,
] = useStateRef(false);
const classes = useStyles();
const [, setConditionEditorActive, conditionEditorActiveRef] = useStateRef(
false
);
const [, setBodyEditorActive, bodyEditorActiveRef] = useStateRef(false);
const { tableState } = useFiretableContext();
const columns = Object.keys(tableState?.columns ?? {});
const edited = !_isEqual(initialObject, extensionObject);
const handleChange = (_, newValue: number) => {
setTabIndex(newValue);
};
const handleAddOrUpdate = () => {
switch (mode) {
case "add":
@@ -179,42 +115,39 @@ export default function ExtensionModal({
return (
<Modal
onClose={handleClose}
maxWidth="lg"
maxWidth="md"
disableBackdropClick
disableEscapeKeyDown
fullWidth
title={
<Button
color="secondary"
startIcon={<BackIcon />}
onClick={handleClose}
>
EXTENSIONS
</Button>
}
fullHeight
sx={{
"& .MuiDialogContent-root": {
display: "flex",
flexDirection: "column",
},
}}
title={`${mode === "add" ? "Add" : "Update"} Extension`}
children={
<Box
className={classes.modalRoot}
display="flex"
flexDirection="column"
>
<>
<Grid
container
spacing={3}
spacing={4}
justifyContent="center"
alignItems="center"
className={classes.metaRoot}
>
<Grid item xs={4}>
<TextField
size="small"
label={
edited && !extensionObject.name.length
? "Extension name (required)"
: "Extension name"
}
required
label="Extension Name"
variant="filled"
fullWidth
autoFocus
value={extensionObject.name}
error={edited && !extensionObject.name.length}
helperText={
edited && !extensionObject.name.length ? "Required" : " "
}
onChange={(event) => {
setExtensionObject({
...extensionObject,
@@ -223,204 +156,272 @@ export default function ExtensionModal({
}}
/>
</Grid>
<Grid item xs={4}>
<Box
display="flex"
alignItems="center"
className={classes.hoverable}
onClick={() => {
setExtensionObject({
...extensionObject,
active: !extensionObject.active,
});
}}
>
<Switch color="primary" checked={extensionObject.active} />
<Typography>
Extention is {!extensionObject.active && "de"}activated
</Typography>
</Box>
<FormControlLabel
control={
<Switch
checked={extensionObject.active}
onChange={(e) =>
setExtensionObject({
...extensionObject,
active: e.target.checked,
})
}
size="medium"
/>
}
label={`Extension is ${
!extensionObject.active ? "de" : ""
}activated`}
/>
</Grid>
<Grid item xs={4}>
<Tooltip title="Extension type cannot be changed once created.">
<TextField
size="small"
label="Extension Type"
value={extensionObject.type}
variant="filled"
fullWidth
disabled
/>
</Tooltip>
<TextField
size="small"
label="Extension Type"
value={extensionObject.type}
variant="filled"
fullWidth
disabled
helperText="Cannot be changed once created"
/>
</Grid>
</Grid>
<Box
className={classes.tabWrapper}
flexGrow={1}
display="flex"
flexDirection="column"
>
<AppBar position="static" className={classes.tabRoot} elevation={0}>
<Tabs
value={tabIndex}
onChange={handleChange}
variant="fullWidth"
centered
indicatorColor="primary"
textColor="primary"
>
<Tab label="Triggers & Requirements" />
<Tab label="Parameters" />
</Tabs>
</AppBar>
<TabPanel value={tabIndex} index={0}>
<TabContext value={tab}>
<TabList
aria-label="Extension settings tabs"
onChange={(_, val) => setTab(val)}
variant="fullWidth"
centered
style={{
marginTop: 0,
marginLeft: "calc(var(--spacing-modal) * -1)",
marginRight: "calc(var(--spacing-modal) * -1)",
}}
>
<Tab
value="triggersRequirements"
label="Triggers & Requirements"
/>
<Tab value="parameters" label="Parameters" />
</TabList>
<Divider
style={{
marginTop: -1,
marginLeft: "calc(var(--spacing-modal) * -1)",
marginRight: "calc(var(--spacing-modal) * -1)",
}}
/>
<StyledTabPanel value="triggersRequirements">
<Grid
container
spacing={3}
justifyContent="center"
justifyContent="space-between"
alignItems="flex-start"
>
<Grid item xs={6}>
<Typography variant="body2">
Select a trigger that runs your extension code. Selected
actions on any cells will trigger the extension.
</Typography>
<Box>
<Typography variant="overline" className={classes.label}>
<Grid item xs={12} sm={6}>
<FormControl component="fieldset" required>
<FormLabel
component="legend"
sx={{
typography: "subtitle2",
color: "text.primary",
mb: 1,
}}
>
Triggers
</FormLabel>
<Typography gutterBottom>
Select a trigger that runs your extension code. Selected
actions on any cells will trigger the extension.
</Typography>
</Box>
{triggerTypes.map((trigger) => (
<Box
display="flex"
alignItems="center"
className={classes.hoverable}
onClick={() => {
if (extensionObject.triggers.includes(trigger)) {
setExtensionObject({
...extensionObject,
triggers: extensionObject.triggers.filter(
(t) => t !== trigger
),
});
} else {
setExtensionObject({
...extensionObject,
triggers: [...extensionObject.triggers, trigger],
});
}
}}
>
<Checkbox
checked={extensionObject.triggers.includes(trigger)}
name={trigger}
/>
<Typography>{trigger}</Typography>
</Box>
))}
</Grid>
<Grid item xs={6} className={classes.requiredFields}>
<Typography variant="body2">
Optionally, select the fields that are required for the
extension to be triggered for a row.
</Typography>
<Box>
<Typography variant="overline" className={classes.label}>
Required Fields (Optional)
</Typography>
</Box>
{columns.sort().map((field) => (
<Box
display="flex"
alignItems="center"
className={classes.hoverable}
onClick={() => {
if (extensionObject.requiredFields.includes(field)) {
setExtensionObject({
...extensionObject,
requiredFields: extensionObject.requiredFields.filter(
(t) => t !== field
),
});
} else {
setExtensionObject({
...extensionObject,
requiredFields: [
...extensionObject.requiredFields,
field,
],
});
}
}}
>
<Checkbox
checked={extensionObject.requiredFields.includes(field)}
name={field}
/>
<Typography>{field}</Typography>
</Box>
))}
{extensionObject.requiredFields.map((trigger, index) => {
const isFiretableColumn = columns.includes(trigger);
if (isFiretableColumn) {
return null;
}
return (
<Box display="flex" alignItems="center">
<IconButton
<FormGroup>
{triggerTypes.map((trigger) => (
<FormControlLabel
label={trigger}
control={
<Checkbox
checked={extensionObject.triggers.includes(
trigger
)}
name={trigger}
onChange={() => {
if (
extensionObject.triggers.includes(trigger)
) {
setExtensionObject({
...extensionObject,
triggers: extensionObject.triggers.filter(
(t) => t !== trigger
),
});
} else {
setExtensionObject({
...extensionObject,
triggers: [
...extensionObject.triggers,
trigger,
],
});
}
}}
/>
}
/>
))}
</FormGroup>
</FormControl>
</Grid>
<Grid item xs={12} sm={6}>
<FormControl component="fieldset">
<FormLabel
component="legend"
sx={{
typography: "subtitle2",
color: "text.primary",
mb: 1,
}}
>
Required Fields (optional)
</FormLabel>
<Typography gutterBottom>
Optionally, select the fields that are required for the
extension to be triggered for a row.
</Typography>
<FormGroup
sx={{
maxHeight: 42 * 3.5,
overflowY: "auto",
flexWrap: "nowrap",
borderBottom: 1,
borderColor: "divider",
"& > *": { flexShrink: 0 },
}}
>
{columns.sort().map((field) => (
<FormControlLabel
label={field}
control={
<Checkbox
checked={extensionObject.requiredFields.includes(
field
)}
name={field}
onChange={() => {
if (
extensionObject.requiredFields.includes(field)
) {
setExtensionObject({
...extensionObject,
requiredFields: extensionObject.requiredFields.filter(
(t) => t !== field
),
});
} else {
setExtensionObject({
...extensionObject,
requiredFields: [
...extensionObject.requiredFields,
field,
],
});
}
}}
/>
}
/>
))}
{extensionObject.requiredFields.map((trigger, index) => {
const isFiretableColumn = columns.includes(trigger);
if (isFiretableColumn) {
return null;
}
return (
<Stack
direction="row"
alignItems="center"
sx={{ ml: -1.25, height: 42 }}
>
<IconButton
color="secondary"
component="span"
aria-label="Delete Firestore Field"
onClick={() => {
setExtensionObject({
...extensionObject,
requiredFields: extensionObject.requiredFields.filter(
(t) => t !== trigger
),
});
}}
>
<DeleteIcon />
</IconButton>
<TextField
id={`extensions-requiredFields-firestoreField-${index}`}
label="Firestore Field"
sx={{
flexDirection: "row",
alignItems: "baseline",
"& .MuiInputLabel-root": { pl: 0, pr: 1 },
}}
value={trigger}
onChange={(event) => {
setExtensionObject({
...extensionObject,
requiredFields: extensionObject.requiredFields.map(
(value, i) =>
i === index ? event.target.value : value
),
});
}}
/>
</Stack>
);
})}
<Stack
direction="row"
justifyContent="flex-start"
alignItems="center"
sx={{ height: 42, ml: -0.75 }}
>
<Button
variant="text"
color="secondary"
component="span"
className={classes.removeField}
startIcon={<AddIcon />}
onClick={() => {
setExtensionObject({
...extensionObject,
requiredFields: extensionObject.requiredFields.filter(
(t) => t !== trigger
),
requiredFields: [
...extensionObject.requiredFields,
"",
],
});
}}
>
<DeleteIcon />
</IconButton>
<TextField
label="Firestore field"
variant="outlined"
value={trigger}
size="small"
onChange={(event) => {
setExtensionObject({
...extensionObject,
requiredFields: extensionObject.requiredFields.map(
(value, i) =>
i === index ? event.target.value : value
),
});
}}
/>
</Box>
);
})}
<Button
variant="text"
color="secondary"
className={classes.addField}
startIcon={<AddIcon />}
onClick={() => {
setExtensionObject({
...extensionObject,
requiredFields: [...extensionObject.requiredFields, ""],
});
}}
>
Add a new Firestore field
</Button>
Add Firestore Field
</Button>
</Stack>
</FormGroup>
</FormControl>
</Grid>
</Grid>
<Box className={classes.tabPanel} flexGrow={1}>
<Typography variant="overline" className={classes.label}>
<div style={{ flexGrow: 1 }}>
<Typography variant="subtitle2" gutterBottom>
Conditions
</Typography>
<CodeEditor
script={extensionObject.conditions}
height="100%"
@@ -452,17 +453,19 @@ export default function ExtensionModal({
setConditionEditorActive(false);
}}
/>
</Box>
</div>
<CodeEditorHelper
docLink="https://github.com/FiretableProject/firetable/wiki/Extensions"
docLink={WIKI_LINKS.extensions}
additionalVariables={additionalVariables}
/>
</TabPanel>
<TabPanel value={tabIndex} index={1}>
<Box className={classes.tabPanel} flexGrow={1}>
<Typography variant="overline" className={classes.label}>
</StyledTabPanel>
<StyledTabPanel value="parameters">
<div style={{ flexGrow: 1 }}>
<Typography variant="subtitle2" gutterBottom>
Extension Body
</Typography>
<CodeEditor
script={extensionObject.extensionBody}
height="100%"
@@ -472,7 +475,7 @@ export default function ExtensionModal({
extensionBody: newValue,
});
}}
onValideStatusUpdate={({ isValid }) => {
onValidStatusUpdate={({ isValid }) => {
if (!bodyEditorActiveRef.current) {
return;
}
@@ -494,14 +497,14 @@ export default function ExtensionModal({
setBodyEditorActive(false);
}}
/>
</Box>
</div>
<CodeEditorHelper
docLink="https://github.com/FiretableProject/firetable/wiki/Extensions"
docLink={WIKI_LINKS.extensions}
additionalVariables={additionalVariables}
/>
</TabPanel>
</Box>
</Box>
</StyledTabPanel>
</TabContext>
</>
}
actions={{
primary: {
@@ -510,19 +513,19 @@ export default function ExtensionModal({
onClick: () => {
let warningMessage;
if (!validation.condition && !validation.extensionBody) {
warningMessage = "Condition and extention body are not valid";
warningMessage = "Condition and extension body are not valid";
} else if (!validation.condition) {
warningMessage = "Condition is not valid";
} else if (!validation.extensionBody) {
warningMessage = "Extention body is not valid";
warningMessage = "Extension body is not valid";
}
if (warningMessage) {
requestConfirmation({
title: "Validation failed",
body: `${warningMessage}, do you want to continue?`,
confirm: "Yes, I know what I am doing",
cancel: "No, I'll fix the errors",
title: "Validation Failed",
body: `${warningMessage}. Continue?`,
confirm: "Yes, I know what Im doing",
cancel: "No, Ill fix the errors",
handleConfirm: handleAddOrUpdate,
});
} else {

View File

@@ -1,20 +1,22 @@
import { useState } from "react";
import _isEqual from "lodash/isEqual";
import { useConfirmation } from "components/ConfirmationDialog";
import { useSnackContext } from "contexts/SnackContext";
import { db } from "../../../../firebase";
import { Breadcrumbs, Typography, Button } from "@material-ui/core";
import TableHeaderButton from "../TableHeaderButton";
import ExtensionIcon from "assets/icons/Extension";
import Modal from "components/Modal";
import { useFiretableContext } from "contexts/FiretableContext";
import { useAppContext } from "contexts/AppContext";
import { useSnackLogContext } from "contexts/SnackLogContext";
import ExtensionList from "./ExtensionList";
import ExtensionModal from "./ExtensionModal";
import ExtensionMigration from "./ExtensionMigration";
import { useFiretableContext } from "contexts/FiretableContext";
import { useAppContext } from "contexts/AppContext";
import { useConfirmation } from "components/ConfirmationDialog";
import { useSnackContext } from "contexts/SnackContext";
import { useSnackLogContext } from "contexts/SnackLogContext";
import {
serialiseExtension,
emptyExtensionObject,
@@ -217,11 +219,10 @@ export default function ExtensionsEditor() {
{openExtensionList && !!tableState && (
<Modal
open={openExtensionList}
onClose={handleClose}
maxWidth="sm"
fullWidth
title={<>Extensions</>}
title="Extensions"
children={
<>
<Breadcrumbs aria-label="breadcrumb">

View File

@@ -13,12 +13,9 @@ const useStyles = makeStyles((theme) =>
minWidth: 400,
minHeight: 100,
height: "calc(100% - 50px)",
},
resizeIcon: {
position: "absolute",
bottom: 0,
right: 0,
color: theme.palette.text.disabled,
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadius,
overflow: "hidden",
},
saveButton: {
@@ -386,6 +383,13 @@ export default function CodeEditor(props: any) {
value={initialEditorValue}
onChange={handleChange}
onValidate={handleEditorValidation}
options={{
// readOnly: disabled,
fontFamily: theme.typography.fontFamilyMono,
rulers: [80],
minimap: { enabled: false },
// ...editorOptions,
}}
className={classes.editor}
/>
</div>

View File

@@ -8,6 +8,7 @@ const WIKI_PATHS = {
FtFunctions: "/Firetable-Cloud-Functions",
securityRules: "/Role-Based-Security-Rules",
setUpAuth: "/Setting-Up-Firebase-Authentication",
extensions: "/Extensions",
};
const WIKI_LINK_ROOT = meta.repository.url.replace(".git", "/wiki");

View File

@@ -188,72 +188,81 @@ export default function TestView() {
<Stack spacing={1} direction="row" alignItems="center">
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
variant="text"
size="small"
>
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
variant="text"
size="medium"
>
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
variant="text"
size="large"
>
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
color="secondary"
variant="text"
size="small"
>
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
color="secondary"
variant="text"
size="medium"
>
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
color="secondary"
variant="text"
size="large"
>
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
disabled
variant="text"
size="small"
>
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
disabled
variant="text"
size="medium"
>
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
disabled
variant="text"
size="large"
>
Button
@@ -262,32 +271,32 @@ export default function TestView() {
<Stack spacing={1} direction="row" alignItems="center">
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
variant="outlined"
size="small"
>
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
variant="outlined"
size="medium"
>
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
variant="outlined"
size="large"
>
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
color="secondary"
variant="outlined"
size="small"
@@ -295,8 +304,8 @@ export default function TestView() {
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
color="secondary"
variant="outlined"
size="medium"
@@ -304,8 +313,8 @@ export default function TestView() {
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
color="secondary"
variant="outlined"
size="large"
@@ -314,8 +323,8 @@ export default function TestView() {
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
disabled
variant="outlined"
size="small"
@@ -323,8 +332,8 @@ export default function TestView() {
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
disabled
variant="outlined"
size="medium"
@@ -332,8 +341,8 @@ export default function TestView() {
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
disabled
variant="outlined"
size="large"
@@ -344,32 +353,32 @@ export default function TestView() {
<Stack spacing={1} direction="row" alignItems="center">
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
variant="contained"
size="small"
>
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
variant="contained"
size="medium"
>
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
variant="contained"
size="large"
>
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
color="secondary"
variant="contained"
size="small"
@@ -377,8 +386,8 @@ export default function TestView() {
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
color="secondary"
variant="contained"
size="medium"
@@ -386,8 +395,8 @@ export default function TestView() {
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
color="secondary"
variant="contained"
size="large"
@@ -396,8 +405,8 @@ export default function TestView() {
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
disabled
variant="contained"
size="small"
@@ -405,8 +414,8 @@ export default function TestView() {
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
disabled
variant="contained"
size="medium"
@@ -414,8 +423,8 @@ export default function TestView() {
Button
</Button>
<Button
startIcon={<SparkIcon />}
endIcon={<SparkIcon />}
// startIcon={<SparkIcon />}
// endIcon={<SparkIcon />}
disabled
variant="contained"
size="large"

View File

@@ -50,10 +50,14 @@ export const components = (theme: Theme): ThemeOptions => {
MuiDialog: {
styleOverrides: {
paper: { borderRadius: (theme.shape.borderRadius as number) * 2 },
paperWidthXs: { maxWidth: 360 },
},
},
MuiDialogContentText: {
defaultProps: { variant: "body2" },
defaultProps: {
variant: "body2",
color: "textPrimary",
},
},
MuiSnackbar: {
styleOverrides: {
@@ -178,14 +182,6 @@ export const components = (theme: Theme): ThemeOptions => {
},
},
MuiListItem: {
styleOverrides: {
root: {
width: `calc(100% - ${theme.spacing(2)})`,
margin: theme.spacing(0.5, 1),
padding: theme.spacing(0.5, 1),
borderRadius: theme.shape.borderRadius,
},
},
defaultProps: { dense: true },
},
MuiMenu: {
@@ -265,15 +261,27 @@ export const components = (theme: Theme): ThemeOptions => {
minHeight: 32,
paddingTop: theme.spacing(0.5),
paddingBottom: theme.spacing(0.5),
"&.MuiButton-outlined, &.MuiButton-contained": {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
},
"& .MuiButton-iconSizeMedium > *:nth-of-type(1)": { fontSize: 24 },
},
sizeSmall: {
minHeight: 28,
minHeight: 24,
paddingTop: theme.spacing(0.25),
paddingBottom: theme.spacing(0.25),
"&.MuiButton-outlined, &.MuiButton-contained": {
paddingLeft: theme.spacing(10 / 8),
paddingRight: theme.spacing(10 / 8),
},
},
sizeLarge: {
minHeight: 48,
"&.MuiButton-outlined, &.MuiButton-contained": {
paddingLeft: theme.spacing(22 / 8),
paddingRight: theme.spacing(22 / 8),
},
fontSize: "1rem",
borderRadius: (theme.shape.borderRadius as number) * (16 / 14),
"& .MuiButton-iconSizeLarge > *:nth-of-type(1)": { fontSize: 24 },