add new step-based extensions UI

This commit is contained in:
Sidney Alcantara
2021-10-14 23:50:00 +11:00
parent 713ebe6e89
commit 0a09d6bbed
10 changed files with 504 additions and 498 deletions

View File

@@ -1,5 +1,6 @@
import { Stack, Typography, Grid, Tooltip, Chip, Button } from "@mui/material";
import { Stack, Typography, Grid, Tooltip, Button } from "@mui/material";
import InlineOpenInNewIcon from "components/InlineOpenInNewIcon";
export interface ICodeEditorHelperProps {
docLink: string;
additionalVariables?: {
@@ -42,24 +43,19 @@ export default function CodeEditorHelper({
return (
<Stack
direction="row"
spacing={0.25}
alignItems="baseline"
justifyContent="space-between"
sx={{ mb: 1 }}
sx={{ my: 1 }}
>
<Typography
variant="body2"
color="textSecondary"
style={{ flexShrink: 0 }}
>
<Typography variant="body2" color="textSecondary">
You can access:
</Typography>
<Grid container spacing={0.5}>
<Grid container spacing={1}>
{availableVariables.concat(additionalVariables ?? []).map((v) => (
<Grid item key={v.key}>
<Tooltip title={v.description}>
<Chip label={v.key} size="small" />
<code>{v.key}</code>
</Tooltip>
</Grid>
))}

View File

@@ -18,16 +18,22 @@ import {
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import ExtensionIcon from "assets/icons/Extension";
import DuplicateIcon from "@mui/icons-material/ContentCopy";
import EditIcon from "@mui/icons-material/Edit";
import DeleteIcon from "@mui/icons-material/DeleteForever";
import DuplicateIcon from "assets/icons/Copy";
import EditIcon from "@mui/icons-material/EditOutlined";
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import EmptyState from "components/EmptyState";
import { extensionTypes, IExtension, IExtensionType } from "./utils";
import {
extensionTypes,
extensionNames,
IExtension,
ExtensionType,
} from "./utils";
import { DATE_TIME_FORMAT } from "constants/dates";
export interface IExtensionListProps {
extensions: IExtension[];
handleAddExtension: (type: IExtensionType) => void;
handleAddExtension: (type: ExtensionType) => void;
handleUpdateActive: (index: number, active: boolean) => void;
handleDuplicate: (index: number) => void;
handleEdit: (index: number) => void;
@@ -53,7 +59,7 @@ export default function ExtensionList({
setAnchorEl(addButtonRef.current);
};
const handleChooseAddType = (type: IExtensionType) => {
const handleChooseAddType = (type: ExtensionType) => {
handleClose();
handleAddExtension(type);
};
@@ -95,12 +101,8 @@ export default function ExtensionList({
transformOrigin={{ vertical: "top", horizontal: "right" }}
>
{extensionTypes.map((type) => (
<MenuItem
onClick={() => {
handleChooseAddType(type);
}}
>
{type}
<MenuItem onClick={() => handleChooseAddType(type)}>
{extensionNames[type]}
</MenuItem>
))}
</Menu>
@@ -132,7 +134,14 @@ export default function ExtensionList({
children={
<ListItemText
primary={extensionObject.name}
secondary={extensionObject.type}
secondary={extensionNames[extensionObject.type]}
primaryTypographyProps={{
style: {
minHeight: 40,
display: "flex",
alignItems: "center",
},
}}
/>
}
secondaryAction={
@@ -182,13 +191,15 @@ export default function ExtensionList({
<Tooltip
title={
<>
Last updated by {extensionObject.lastEditor.displayName}
Last updated
<br />
on{" "}
{format(extensionObject.lastEditor.lastUpdate, "PPPP")}
by {extensionObject.lastEditor.displayName}
<br />
at{" "}
{format(extensionObject.lastEditor.lastUpdate, "pppp")}
{format(
extensionObject.lastEditor.lastUpdate,
DATE_TIME_FORMAT
)}
</>
}
>

View File

@@ -1,73 +1,42 @@
import { useState } from "react";
import _isEqual from "lodash/isEqual";
import _upperFirst from "lodash/upperFirst";
import useStateRef from "react-usestateref";
import {
styled,
Button,
Checkbox,
Divider,
FormControl,
FormControlLabel,
FormGroup,
FormLabel,
Grid,
IconButton,
Switch,
Stack,
Tab,
TextField,
FormControlLabel,
Switch,
Stepper,
Step,
StepButton,
StepContent,
Typography,
Link,
} from "@mui/material";
import TabContext from "@mui/lab/TabContext";
import TabList from "@mui/lab/TabList";
import TabPanel from "@mui/lab/TabPanel";
import AddIcon from "@mui/icons-material/AddBox";
import DeleteIcon from "@mui/icons-material/RemoveCircle";
import ExpandIcon from "@mui/icons-material/KeyboardArrowDown";
import InlineOpenInNewIcon from "components/InlineOpenInNewIcon";
import Modal, { IModalProps } from "components/Modal";
import CodeEditor from "../../editors/CodeEditor";
import CodeEditorHelper from "components/CodeEditorHelper";
import Step1Triggers from "./Step1Triggers";
import Step2RequiredFields from "./Step2RequiredFields";
import Step3Conditions from "./Step3Conditions";
import Step4Body from "./Step4Body";
import { useConfirmation } from "components/ConfirmationDialog";
import { useProjectContext } from "contexts/ProjectContext";
import { IExtension, triggerTypes } from "./utils";
import { extensionNames, IExtension } from "./utils";
import { WIKI_LINKS } from "constants/externalLinks";
const additionalVariables = [
{
key: "change",
description:
"you can pass in field name to change.before.get() or change.after.get() to get changes",
},
{
key: "triggerType",
description: "triggerType indicates the type of the extension invocation",
},
{
key: "fieldTypes",
description:
"fieldTypes is a map of all fields and its corresponding field type",
},
{
key: "extensionConfig",
description: "the configuration object of this extension",
},
];
const StyledTabPanel = styled(TabPanel)({
flexGrow: 1,
overflowY: "auto",
margin: "0 calc(var(--dialog-spacing) * -1) 0 !important",
padding: "var(--dialog-spacing) var(--dialog-spacing) 0",
"&[hidden]": { display: "none" },
display: "flex",
flexDirection: "column",
});
type StepValidation = Record<"condition" | "extensionBody", boolean>;
export interface IExtensionModalStepProps {
extensionObject: IExtension;
setExtensionObject: React.Dispatch<React.SetStateAction<IExtension>>;
validation: StepValidation;
setValidation: React.Dispatch<React.SetStateAction<StepValidation>>;
validationRef: React.RefObject<StepValidation>;
}
export interface IExtensionModalProps {
handleClose: IModalProps["onClose"];
@@ -85,46 +54,45 @@ export default function ExtensionModal({
extensionObject: initialObject,
}: IExtensionModalProps) {
const { requestConfirmation } = useConfirmation();
const [extensionObject, setExtensionObject] =
useState<IExtension>(initialObject);
const [tab, setTab] = useState("triggersRequirements");
const [validation, setValidation, validationRef] = useStateRef({
condition: true,
extensionBody: true,
});
const [, setConditionEditorActive, conditionEditorActiveRef] =
useStateRef(false);
const [, setBodyEditorActive, bodyEditorActiveRef] = useStateRef(false);
const { tableState } = useProjectContext();
const columns = Object.keys(tableState?.columns ?? {});
const [activeStep, setActiveStep] = useState(0);
const [validation, setValidation, validationRef] =
useStateRef<StepValidation>({ condition: true, extensionBody: true });
const edited = !_isEqual(initialObject, extensionObject);
const handleAddOrUpdate = () => {
switch (mode) {
case "add":
handleAdd(extensionObject);
return;
case "update":
handleUpdate(extensionObject);
return;
}
if (mode === "add") handleAdd(extensionObject);
if (mode === "update") handleUpdate(extensionObject);
};
const stepProps = {
extensionObject,
setExtensionObject,
validation,
setValidation,
validationRef,
};
return (
<Modal
onClose={handleClose}
maxWidth="md"
disableBackdropClick
disableEscapeKeyDown
fullWidth
fullHeight
title={`${mode === "add" ? "Add" : "Update"} Extension: ${
extensionNames[extensionObject.type]
}`}
sx={{
"& .MuiDialogContent-root": {
display: "flex",
flexDirection: "column",
"& .MuiPaper-root": {
maxWidth: 742 + 20,
height: 980,
},
}}
title={`${mode === "add" ? "Add" : "Update"} Extension`}
children={
<>
<Grid
@@ -133,7 +101,7 @@ export default function ExtensionModal({
justifyContent="center"
alignItems="center"
>
<Grid item xs={4}>
<Grid item xs={6}>
<TextField
size="small"
required
@@ -155,16 +123,16 @@ export default function ExtensionModal({
/>
</Grid>
<Grid item xs={4}>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
checked={extensionObject.active}
onChange={(e) =>
setExtensionObject({
setExtensionObject((extensionObject) => ({
...extensionObject,
active: e.target.checked,
})
}))
}
size="medium"
/>
@@ -174,337 +142,102 @@ export default function ExtensionModal({
}activated`}
/>
</Grid>
<Grid item xs={4}>
<TextField
size="small"
label="Extension type"
value={extensionObject.type}
variant="filled"
fullWidth
disabled
helperText="Cannot be changed once created"
/>
</Grid>
</Grid>
<TabContext value={tab}>
<TabList
aria-label="Extension settings tabs"
onChange={(_, val) => setTab(val)}
variant="fullWidth"
centered
style={{
marginTop: 0,
marginLeft: "calc(var(--dialog-spacing) * -1)",
marginRight: "calc(var(--dialog-spacing) * -1)",
}}
>
<Tab
value="triggersRequirements"
label="Triggers & requirements"
/>
<Tab value="parameters" label="Parameters" />
</TabList>
<Divider
style={{
marginTop: -1,
marginLeft: "calc(var(--dialog-spacing) * -1)",
marginRight: "calc(var(--dialog-spacing) * -1)",
}}
/>
<Stepper
nonLinear
activeStep={activeStep}
orientation="vertical"
sx={{
mt: 0,
<StyledTabPanel value="triggersRequirements">
<Grid
container
spacing={3}
justifyContent="space-between"
alignItems="flex-start"
>
<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>
<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 isTableColumn = columns.includes(trigger);
if (isTableColumn) {
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.875 }}
>
<Button
variant="text"
color="secondary"
startIcon={<AddIcon />}
onClick={() => {
setExtensionObject({
...extensionObject,
requiredFields: [
...extensionObject.requiredFields,
"",
],
});
}}
>
Add Firestore field
</Button>
</Stack>
</FormGroup>
</FormControl>
</Grid>
</Grid>
<div style={{ flexGrow: 1 }}>
<Typography variant="subtitle2" gutterBottom>
Conditions
"& .MuiStepLabel-root": { width: "100%" },
"& .MuiStepLabel-label": {
display: "flex",
width: "100%",
typography: "subtitle2",
"&.Mui-active": { typography: "subtitle2" },
},
"& .MuiStepLabel-label svg": {
display: "block",
marginLeft: "auto",
my: ((24 - 18) / 2 / 8) * -1,
transition: (theme) => theme.transitions.create("transform"),
},
"& .Mui-active svg": {
transform: "rotate(180deg)",
},
}}
>
<Step>
<StepButton onClick={() => setActiveStep(0)}>
Trigger events
<ExpandIcon />
</StepButton>
<StepContent>
<Typography gutterBottom>
Select which events trigger this extension
</Typography>
<Step1Triggers {...stepProps} />
</StepContent>
</Step>
<CodeEditor
script={extensionObject.conditions}
height="100%"
handleChange={(newValue) => {
setExtensionObject({
...extensionObject,
conditions: newValue,
});
}}
onValideStatusUpdate={({ isValid }) => {
if (!conditionEditorActiveRef.current) {
return;
}
setValidation({
...validationRef.current,
condition: isValid,
});
console.log(validationRef.current);
}}
diagnosticsOptions={{
noSemanticValidation: false,
noSyntaxValidation: false,
noSuggestionDiagnostics: true,
}}
onMount={() => {
setConditionEditorActive(true);
}}
onUnmount={() => {
setConditionEditorActive(false);
}}
/>
</div>
<CodeEditorHelper
docLink={WIKI_LINKS.extensions}
additionalVariables={additionalVariables}
/>
</StyledTabPanel>
<StyledTabPanel value="parameters">
<div style={{ flexGrow: 1 }}>
<Typography variant="subtitle2" gutterBottom>
Extension body
<Step>
<StepButton onClick={() => setActiveStep(1)}>
Required fields (optional)
<ExpandIcon />
</StepButton>
<StepContent>
<Typography gutterBottom>
Optionally, select fields that must have a value set for the
extension to be triggered for that row
</Typography>
<Step2RequiredFields {...stepProps} />
</StepContent>
</Step>
<CodeEditor
script={extensionObject.extensionBody}
height="100%"
handleChange={(newValue) => {
setExtensionObject({
...extensionObject,
extensionBody: newValue,
});
}}
onValidStatusUpdate={({ isValid }) => {
if (!bodyEditorActiveRef.current) {
return;
<Step>
<StepButton onClick={() => setActiveStep(2)}>
Trigger conditions (optional)
<ExpandIcon />
</StepButton>
<StepContent>
<Typography gutterBottom>
Optionally, write a function that determines if the extension
should be triggered for a given row. Leave the function to
always return <code>true</code> if you do not want to write
additional logic.
</Typography>
<Step3Conditions {...stepProps} />
</StepContent>
</Step>
<Step>
<StepButton onClick={() => setActiveStep(3)}>
Extension body
<ExpandIcon />
</StepButton>
<StepContent>
<Typography gutterBottom>
Write the extension body function. Make sure you have set all
the required parameters.{" "}
<Link
href={
WIKI_LINKS[
`extensions${_upperFirst(extensionObject.type)}`
] || WIKI_LINKS.extensions
}
setValidation({
...validationRef.current,
extensionBody: isValid,
});
console.log(validationRef.current);
}}
diagnosticsOptions={{
noSemanticValidation: false,
noSyntaxValidation: false,
noSuggestionDiagnostics: true,
}}
onMount={() => {
setBodyEditorActive(true);
}}
onUnmount={() => {
setBodyEditorActive(false);
}}
/>
</div>
<CodeEditorHelper
docLink={WIKI_LINKS.extensions}
additionalVariables={additionalVariables}
/>
</StyledTabPanel>
</TabContext>
target="_blank"
rel="noopener noreferrer"
>
Docs
<InlineOpenInNewIcon />
</Link>
</Typography>
<Step4Body {...stepProps} />
</StepContent>
</Step>
</Stepper>
</>
}
actions={{

View File

@@ -0,0 +1,56 @@
import { IExtensionModalStepProps } from "./ExtensionModal";
import {
FormControl,
FormLabel,
FormGroup,
FormControlLabel,
Checkbox,
} from "@mui/material";
import { triggerTypes } from "./utils";
export default function Step1Triggers({
extensionObject,
setExtensionObject,
}: IExtensionModalStepProps) {
return (
<FormControl component="fieldset" required>
<FormLabel component="legend" className="visually-hidden">
Triggers
</FormLabel>
<FormGroup>
{triggerTypes.map((trigger) => (
<FormControlLabel
key={trigger}
label={trigger}
control={
<Checkbox
checked={extensionObject.triggers.includes(trigger)}
name={trigger}
onChange={() => {
setExtensionObject((extensionObject) => {
if (extensionObject.triggers.includes(trigger)) {
return {
...extensionObject,
triggers: extensionObject.triggers.filter(
(t) => t !== trigger
),
};
} else {
return {
...extensionObject,
triggers: [...extensionObject.triggers, trigger],
};
}
});
}}
/>
}
/>
))}
</FormGroup>
</FormControl>
);
}

View File

@@ -0,0 +1,59 @@
import { IExtensionModalStepProps } from "./ExtensionModal";
import _sortBy from "lodash/sortBy";
import MultiSelect from "@rowy/multiselect";
import { ListItemIcon } from "@mui/material";
import { useProjectContext } from "contexts/ProjectContext";
import { FieldType } from "constants/fields";
import { getFieldProp } from "components/fields";
export default function Step2RequiredFields({
extensionObject,
setExtensionObject,
}: IExtensionModalStepProps) {
const { tableState } = useProjectContext();
return (
<MultiSelect
aria-label="Required fields"
multiple
value={extensionObject.requiredFields}
disabled={!tableState?.columns}
options={
tableState?.columns
? _sortBy(Object.values(tableState!.columns), "index")
.filter((c) => c.type !== FieldType.id)
.map((c) => ({
value: c.key,
label: c.name,
type: c.type,
}))
: []
}
onChange={(requiredFields) =>
setExtensionObject((e) => ({ ...e, requiredFields }))
}
TextFieldProps={{ autoFocus: true }}
freeText
AddButtonProps={{ children: "Add other field" }}
AddDialogProps={{
title: "Add other field",
textFieldLabel: "Field key",
}}
itemRenderer={(option: {
value: string;
label: string;
type?: FieldType;
}) => (
<>
<ListItemIcon style={{ minWidth: 40 }}>
{option.type && getFieldProp("icon", option.type)}
</ListItemIcon>
{option.label}
<code style={{ marginLeft: "auto" }}>{option.value}</code>
</>
)}
/>
);
}

View File

@@ -0,0 +1,71 @@
import { IExtensionModalStepProps } from "./ExtensionModal";
import useStateRef from "react-usestateref";
import CodeEditor from "components/Table/editors/CodeEditor";
import CodeEditorHelper from "components/CodeEditorHelper";
import { WIKI_LINKS } from "constants/externalLinks";
const additionalVariables = [
{
key: "change",
description:
"you can pass in field name to change.before.get() or change.after.get() to get changes",
},
{
key: "triggerType",
description: "triggerType indicates the type of the extension invocation",
},
{
key: "fieldTypes",
description:
"fieldTypes is a map of all fields and its corresponding field type",
},
{
key: "extensionConfig",
description: "the configuration object of this extension",
},
];
export default function Step3Conditions({
extensionObject,
setExtensionObject,
setValidation,
validationRef,
}: IExtensionModalStepProps) {
const [, setConditionEditorActive, conditionEditorActiveRef] =
useStateRef(false);
return (
<>
<div>
<CodeEditor
script={extensionObject.conditions}
height={200}
handleChange={(newValue) => {
setExtensionObject({
...extensionObject,
conditions: newValue,
});
}}
onValidStatusUpdate={({ isValid }) => {
if (!conditionEditorActiveRef.current) return;
setValidation({ ...validationRef.current!, condition: isValid });
}}
diagnosticsOptions={{
noSemanticValidation: false,
noSyntaxValidation: false,
noSuggestionDiagnostics: true,
}}
onMount={() => setConditionEditorActive(true)}
onUnmount={() => setConditionEditorActive(false)}
/>
</div>
<CodeEditorHelper
docLink={WIKI_LINKS.extensions}
additionalVariables={additionalVariables}
/>
</>
);
}

View File

@@ -0,0 +1,77 @@
import { IExtensionModalStepProps } from "./ExtensionModal";
import _upperFirst from "lodash/upperFirst";
import useStateRef from "react-usestateref";
import CodeEditor from "components/Table/editors/CodeEditor";
import CodeEditorHelper from "components/CodeEditorHelper";
import { WIKI_LINKS } from "constants/externalLinks";
const additionalVariables = [
{
key: "change",
description:
"you can pass in field name to change.before.get() or change.after.get() to get changes",
},
{
key: "triggerType",
description: "triggerType indicates the type of the extension invocation",
},
{
key: "fieldTypes",
description:
"fieldTypes is a map of all fields and its corresponding field type",
},
{
key: "extensionConfig",
description: "the configuration object of this extension",
},
];
export default function Step4Body({
extensionObject,
setExtensionObject,
setValidation,
validationRef,
}: IExtensionModalStepProps) {
const [, setBodyEditorActive, bodyEditorActiveRef] = useStateRef(false);
return (
<>
<div>
<CodeEditor
script={extensionObject.extensionBody}
height={400}
handleChange={(newValue) => {
setExtensionObject({
...extensionObject,
extensionBody: newValue,
});
}}
onValidStatusUpdate={({ isValid }) => {
if (!bodyEditorActiveRef.current) return;
setValidation({
...validationRef.current!,
extensionBody: isValid,
});
}}
diagnosticsOptions={{
noSemanticValidation: false,
noSyntaxValidation: false,
noSuggestionDiagnostics: true,
}}
onMount={() => setBodyEditorActive(true)}
onUnmount={() => setBodyEditorActive(false)}
/>
</div>
<CodeEditorHelper
docLink={
WIKI_LINKS[`extensions${_upperFirst(extensionObject.type)}`] ||
WIKI_LINKS.extensions
}
additionalVariables={additionalVariables}
/>
</>
);
}

View File

@@ -1,9 +1,7 @@
import { useState } from "react";
import _isEqual from "lodash/isEqual";
import { db } from "../../../../firebase";
import { useSnackbar } from "notistack";
import { Breadcrumbs, Typography, Button } from "@mui/material";
import { Breadcrumbs } from "@mui/material";
import TableHeaderButton from "../TableHeaderButton";
import ExtensionIcon from "assets/icons/Extension";
@@ -17,20 +15,19 @@ import { useAppContext } from "contexts/AppContext";
import { useConfirmation } from "components/ConfirmationDialog";
import { useSnackLogContext } from "contexts/SnackLogContext";
import { emptyExtensionObject, IExtension, IExtensionType } from "./utils";
import { name } from "@root/package.json";
import { emptyExtensionObject, IExtension, ExtensionType } from "./utils";
import { runRoutes } from "constants/runRoutes";
import { analytics } from "@src/analytics";
export default function ExtensionsEditor() {
const { enqueueSnackbar } = useSnackbar();
export default function Extensions() {
const { tableState, tableActions, rowyRun } = useProjectContext();
const appContext = useAppContext();
const { requestConfirmation } = useConfirmation();
const currentextensionObjects = (tableState?.config.extensionObjects ??
const currentExtensionObjects = (tableState?.config.extensionObjects ??
[]) as IExtension[];
const [localExtensionsObjects, setLocalExtensionsObjects] = useState(
currentextensionObjects
currentExtensionObjects
);
const [openExtensionList, setOpenExtensionList] = useState(false);
const [openMigrationGuide, setOpenMigrationGuide] = useState(false);
@@ -39,8 +36,9 @@ export default function ExtensionsEditor() {
extensionObject: IExtension;
index?: number;
} | null>(null);
const snackLogContext = useSnackLogContext();
const edited = !_isEqual(currentextensionObjects, localExtensionsObjects);
const edited = !_isEqual(currentExtensionObjects, localExtensionsObjects);
const tablePathTokens =
tableState?.tablePath?.split("/").filter(function (_, i) {
@@ -65,7 +63,7 @@ export default function ExtensionsEditor() {
body: "You will lose changes you have made to extensions",
confirm: "Discard",
handleConfirm: () => {
setLocalExtensionsObjects(currentextensionObjects);
setLocalExtensionsObjects(currentExtensionObjects);
setOpenExtensionList(false);
},
});
@@ -199,13 +197,13 @@ export default function ExtensionsEditor() {
children={
<>
<Breadcrumbs aria-label="breadcrumb">
{tablePathTokens.map((pathToken) => {
return <Typography>{pathToken}</Typography>;
})}
{tablePathTokens.map((pathToken) => (
<code>{pathToken}</code>
))}
</Breadcrumbs>
<ExtensionList
extensions={localExtensionsObjects}
handleAddExtension={(type: IExtensionType) => {
handleAddExtension={(type: ExtensionType) => {
setExtensionModal({
mode: "add",
extensionObject: emptyExtensionObject(

View File

@@ -1,40 +1,4 @@
type IExtensionType =
| "task"
| "docSync"
| "historySnapshot"
| "algoliaIndex"
| "meiliIndex"
| "bigqueryIndex"
| "slackMessage"
| "sendgridEmail"
| "apiCall"
| "twilioMessage";
type IExtensionTrigger = "create" | "update" | "delete";
interface IExtensionEditor {
displayName: string;
photoURL: string;
lastUpdate: number;
}
interface IExtension {
// rowy meta fields
name: string;
active: boolean;
lastEditor: IExtensionEditor;
// ft build fields
triggers: IExtensionTrigger[];
type: IExtensionType;
requiredFields: string[];
extensionBody: string;
conditions: string;
}
const triggerTypes: IExtensionTrigger[] = ["create", "update", "delete"];
const extensionTypes: IExtensionType[] = [
export const extensionTypes = [
"task",
"docSync",
"historySnapshot",
@@ -45,7 +9,46 @@ const extensionTypes: IExtensionType[] = [
"sendgridEmail",
"apiCall",
"twilioMessage",
];
] as const;
export type ExtensionType = typeof extensionTypes[number];
export const extensionNames: Record<ExtensionType, string> = {
task: "Task",
docSync: "Doc Sync",
historySnapshot: "History Snapshot",
algoliaIndex: "Algolia Index",
meiliIndex: "MeiliSearch Index",
bigqueryIndex: "Big Query Index",
slackMessage: "Slack Message",
sendgridEmail: "SendGrid Email",
apiCall: "API Call",
twilioMessage: "Twilio Message",
};
export type ExtensionTrigger = "create" | "update" | "delete";
export interface IExtensionEditor {
displayName: string;
photoURL: string;
lastUpdate: number;
}
export interface IExtension {
// rowy meta fields
name: string;
active: boolean;
lastEditor: IExtensionEditor;
// ft build fields
triggers: ExtensionTrigger[];
type: ExtensionType;
requiredFields: string[];
extensionBody: string;
conditions: string;
}
export const triggerTypes: ExtensionTrigger[] = ["create", "update", "delete"];
const extensionBodyTemplate = {
task: `const extensionBody: TaskBody = async({row, db, change, ref}) => {
@@ -158,8 +161,8 @@ const extensionBodyTemplate = {
}`,
};
function emptyExtensionObject(
type: IExtensionType,
export function emptyExtensionObject(
type: ExtensionType,
user: IExtensionEditor
): IExtension {
return {
@@ -176,7 +179,7 @@ function emptyExtensionObject(
lastEditor: user,
};
}
function sparkToExtensionObjects(
export function sparkToExtensionObjects(
sparkConfig: string,
user: IExtensionEditor
): IExtension[] {
@@ -225,8 +228,8 @@ function sparkToExtensionObjects(
lastEditor: user,
// ft build fields
triggers: (spark.triggers ?? []) as IExtensionTrigger[],
type: spark.type as IExtensionType,
triggers: (spark.triggers ?? []) as ExtensionTrigger[],
type: spark.type as ExtensionType,
requiredFields: spark.requiredFields ?? [],
extensionBody: spark.sparkBody,
conditions: spark.shouldRun ?? "",
@@ -234,11 +237,3 @@ function sparkToExtensionObjects(
});
return extensionObjects ?? [];
}
export {
extensionTypes,
triggerTypes,
emptyExtensionObject,
sparkToExtensionObjects,
};
export type { IExtension, IExtensionType, IExtensionEditor };

View File

@@ -91,6 +91,16 @@ export const components = (theme: Theme): ThemeOptions => {
"& input, & label": theme.typography.body2,
},
".visually-hidden": {
position: "absolute",
clip: "rect(1px, 1px, 1px, 1px)",
overflow: "hidden",
height: 1,
width: 1,
padding: 0,
border: 0,
},
},
},