mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
update extensions UI
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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 I’m doing",
|
||||
cancel: "No, I’ll fix the errors",
|
||||
handleConfirm: handleAddOrUpdate,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user