add collapsible SteppedAccordion

This commit is contained in:
Sidney Alcantara
2021-11-26 11:35:17 +11:00
parent 86f6ad281d
commit ccff7500bc
9 changed files with 689 additions and 766 deletions

View File

@@ -0,0 +1,92 @@
import { useState } from "react";
import {
Stepper,
StepperProps,
Step,
StepProps,
StepButton,
StepButtonProps,
Typography,
StepContent,
StepContentProps,
} from "@mui/material";
import ExpandIcon from "@mui/icons-material/KeyboardArrowDown";
export interface ISteppedAccordionProps extends Partial<StepperProps> {
steps: {
id: string;
title: React.ReactNode;
optional?: boolean;
content: React.ReactNode;
stepProps?: Partial<StepProps>;
titleProps?: Partial<StepButtonProps>;
contentProps?: Partial<StepContentProps>;
}[];
}
export default function SteppedAccordion({
steps,
...props
}: ISteppedAccordionProps) {
const [activeStep, setActiveStep] = useState(steps[0].id);
return (
<Stepper
nonLinear
activeStep={steps.findIndex((x) => x.id === activeStep)}
orientation="vertical"
{...props}
sx={{
mt: 0,
"& .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)",
},
...props.sx,
}}
>
{steps.map(
({
id,
title,
optional,
content,
stepProps,
titleProps,
contentProps,
}) => (
<Step key={id} {...stepProps}>
<StepButton
onClick={() => setActiveStep((s) => (s === id ? "" : id))}
optional={
optional && <Typography variant="caption">Optional</Typography>
}
{...titleProps}
>
{title}
<ExpandIcon />
</StepButton>
<StepContent {...contentProps}>{content}</StepContent>
</Step>
)
)}
</Stepper>
);
}

View File

@@ -1,33 +1,18 @@
import { useState } from "react";
import _isEqual from "lodash/isEqual";
import _upperFirst from "lodash/upperFirst";
import useStateRef from "react-usestateref";
import {
Grid,
TextField,
FormControlLabel,
Switch,
Stepper,
Step,
StepButton,
StepContent,
Typography,
Link,
} from "@mui/material";
import ExpandIcon from "@mui/icons-material/KeyboardArrowDown";
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
import { Grid, TextField, FormControlLabel, Switch } from "@mui/material";
import Modal, { IModalProps } from "@src/components/Modal";
import SteppedAccordion from "@src/components/SteppedAccordion";
import Step1Triggers from "./Step1Triggers";
import Step2RequiredFields from "./Step2RequiredFields";
import Step3Conditions from "./Step3Conditions";
import Step4Body from "./Step4Body";
import { useConfirmation } from "@src/components/ConfirmationDialog";
import { extensionNames, IExtension } from "./utils";
import { WIKI_LINKS } from "@src/constants/externalLinks";
type StepValidation = Record<"condition" | "extensionBody", boolean>;
export interface IExtensionModalStepProps {
@@ -58,8 +43,6 @@ export default function ExtensionModal({
const [extensionObject, setExtensionObject] =
useState<IExtension>(initialObject);
const [activeStep, setActiveStep] = useState(0);
const [validation, setValidation, validationRef] =
useStateRef<StepValidation>({ condition: true, extensionBody: true });
@@ -144,100 +127,32 @@ export default function ExtensionModal({
</Grid>
</Grid>
<Stepper
nonLinear
activeStep={activeStep}
orientation="vertical"
sx={{
mt: 0,
"& .MuiStepLabel-root": { width: "100%" },
"& .MuiStepLabel-label": {
display: "flex",
width: "100%",
typography: "subtitle2",
"&.Mui-active": { typography: "subtitle2" },
<SteppedAccordion
steps={[
{
id: "triggers",
title: "Trigger events",
content: <Step1Triggers {...stepProps} />,
},
"& .MuiStepLabel-label svg": {
display: "block",
marginLeft: "auto",
my: ((24 - 18) / 2 / 8) * -1,
transition: (theme) => theme.transitions.create("transform"),
{
id: "requiredFields",
title: "Required fields",
optional: true,
content: <Step2RequiredFields {...stepProps} />,
},
"& .Mui-active svg": {
transform: "rotate(180deg)",
{
id: "conditions",
title: "Trigger conditions",
optional: true,
content: <Step3Conditions {...stepProps} />,
},
}}
>
<Step>
<StepButton onClick={() => setActiveStep(0)}>
Trigger events
<ExpandIcon />
</StepButton>
<StepContent>
<Typography gutterBottom>
Select which events trigger this extension
</Typography>
<Step1Triggers {...stepProps} />
</StepContent>
</Step>
<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>
<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
}
target="_blank"
rel="noopener noreferrer"
>
Docs
<InlineOpenInNewIcon />
</Link>
</Typography>
<Step4Body {...stepProps} />
</StepContent>
</Step>
</Stepper>
{
id: "body",
title: "Extension body",
content: <Step4Body {...stepProps} />,
},
]}
/>
</>
}
actions={{

View File

@@ -1,6 +1,7 @@
import { IExtensionModalStepProps } from "./ExtensionModal";
import {
Typography,
FormControl,
FormLabel,
FormGroup,
@@ -15,42 +16,48 @@ export default function Step1Triggers({
setExtensionObject,
}: IExtensionModalStepProps) {
return (
<FormControl component="fieldset" required>
<FormLabel component="legend" className="visually-hidden">
Triggers
</FormLabel>
<>
<Typography gutterBottom>
Select which events trigger this extension
</Typography>
<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>
<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

@@ -1,8 +1,8 @@
import { IExtensionModalStepProps } from "./ExtensionModal";
import _sortBy from "lodash/sortBy";
import { Typography, ListItemIcon } from "@mui/material";
import MultiSelect from "@rowy/multiselect";
import { ListItemIcon } from "@mui/material";
import { useProjectContext } from "@src/contexts/ProjectContext";
import { FieldType } from "@src/constants/fields";
@@ -15,45 +15,52 @@ export default function Step2RequiredFields({
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>
</>
)}
/>
<>
<Typography gutterBottom>
Optionally, select fields that must have a value set for the extension
to be triggered for that row
</Typography>
<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

@@ -2,6 +2,7 @@ import { lazy, Suspense } from "react";
import { IExtensionModalStepProps } from "./ExtensionModal";
import useStateRef from "react-usestateref";
import { Typography } from "@mui/material";
import FieldSkeleton from "@src/components/SideDrawer/Form/FieldSkeleton";
import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
import { WIKI_LINKS } from "@src/constants/externalLinks";
@@ -49,6 +50,12 @@ export default function Step3Conditions({
return (
<>
<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>
<Suspense fallback={<FieldSkeleton height={200} />}>
<CodeEditor
value={extensionObject.conditions}

View File

@@ -1,6 +1,7 @@
import { IWebhookModalStepProps } from "./WebhookModal";
import useStateRef from "react-usestateref";
import { Typography } from "@mui/material";
import CodeEditor from "@src/components/CodeEditor";
import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
@@ -24,6 +25,12 @@ export default function Step3Conditions({
return (
<>
<Typography gutterBottom>
Optionally, write a function that determines if the webhook call should
be processed. Leave the function to always return <code>true</code> if
you do not want to write additional logic.
</Typography>
<div>
<CodeEditor
value={webhookObject.conditions}

View File

@@ -2,11 +2,14 @@ import { IWebhookModalStepProps } from "./WebhookModal";
import _upperFirst from "lodash/upperFirst";
import useStateRef from "react-usestateref";
import { Typography, Link } from "@mui/material";
import CodeEditor from "@src/components/CodeEditor";
import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
import { WIKI_LINKS } from "@src/constants/externalLinks";
import { parserExtraLibs } from "./utils";
const additionalVariables = [
{
key: "req",
@@ -30,6 +33,22 @@ export default function Step4Body({
return (
<>
<Typography gutterBottom>
Write the webhook parsed function. The returned object of the parser
will be added as new row{" "}
<Link
href={
WIKI_LINKS[`webhooks${_upperFirst(webhookObject.type)}`] ||
WIKI_LINKS.webhooks
}
target="_blank"
rel="noopener noreferrer"
>
Docs
<InlineOpenInNewIcon />
</Link>
</Typography>
<div>
<CodeEditor
value={webhookObject.parser}

View File

@@ -1,32 +1,17 @@
import { useState } from "react";
import _isEqual from "lodash/isEqual";
import _upperFirst from "lodash/upperFirst";
import useStateRef from "react-usestateref";
import {
Grid,
TextField,
FormControlLabel,
Switch,
Stepper,
Step,
StepButton,
StepContent,
Typography,
Link,
} from "@mui/material";
import ExpandIcon from "@mui/icons-material/KeyboardArrowDown";
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
import { Grid, TextField, FormControlLabel, Switch } from "@mui/material";
import Modal, { IModalProps } from "@src/components/Modal";
import SteppedAccordion from "@src/components/SteppedAccordion";
import Step1Auth from "./Step1Auth";
import Step2Conditions from "./Step2Conditions";
import Step3Body from "./Step3Parser";
import { useConfirmation } from "@src/components/ConfirmationDialog";
import { webhookNames, IWebhook } from "./utils";
import { WIKI_LINKS } from "@src/constants/externalLinks";
type StepValidation = Record<"condition" | "parser", boolean>;
export interface IWebhookModalStepProps {
@@ -56,8 +41,6 @@ export default function WebhookModal({
const [webhookObject, setWebhookObject] = useState<IWebhook>(initialObject);
const [activeStep, setActiveStep] = useState(0);
const [validation, setValidation, validationRef] =
useStateRef<StepValidation>({ condition: true, parser: true });
@@ -141,83 +124,27 @@ export default function WebhookModal({
</Grid>
</Grid>
<Stepper
nonLinear
activeStep={activeStep}
orientation="vertical"
sx={{
mt: 0,
"& .MuiStepLabel-root": { width: "100%" },
"& .MuiStepLabel-label": {
display: "flex",
width: "100%",
typography: "subtitle2",
"&.Mui-active": { typography: "subtitle2" },
<SteppedAccordion
steps={[
{
id: "verification",
title: "Verification",
optional: true,
content: <Step1Auth {...stepProps} />,
},
"& .MuiStepLabel-label svg": {
display: "block",
marginLeft: "auto",
my: ((24 - 18) / 2 / 8) * -1,
transition: (theme) => theme.transitions.create("transform"),
{
id: "conditions",
title: "Conditions",
optional: true,
content: <Step2Conditions {...stepProps} />,
},
"& .Mui-active svg": {
transform: "rotate(180deg)",
{
id: "parser",
title: "Parser",
content: <Step3Body {...stepProps} />,
},
}}
>
<Step>
<StepButton onClick={() => setActiveStep(0)}>
Verification
<ExpandIcon />
</StepButton>
<StepContent>
<Step1Auth {...stepProps} />
</StepContent>
</Step>
<Step>
<StepButton onClick={() => setActiveStep(1)}>
Conditions (optional)
<ExpandIcon />
</StepButton>
<StepContent>
<Typography gutterBottom>
Optionally, write a function that determines if the webhook
call should be processed. Leave the function to always return{" "}
<code>true</code> if you do not want to write additional
logic.
</Typography>
<Step2Conditions {...stepProps} />
</StepContent>
</Step>
<Step>
<StepButton onClick={() => setActiveStep(2)}>
Parser
<ExpandIcon />
</StepButton>
<StepContent>
<Typography gutterBottom>
Write the webhook parsed function. The returned object of the
parser will be added as new row{" "}
<Link
href={
WIKI_LINKS[
`webhooks${_upperFirst(webhookObject.type)}`
] || WIKI_LINKS.webhooks
}
target="_blank"
rel="noopener noreferrer"
>
Docs
<InlineOpenInNewIcon />
</Link>
</Typography>
<Step3Body {...stepProps} />
</StepContent>
</Step>
</Stepper>
]}
/>
</>
}
actions={{

View File

@@ -27,6 +27,7 @@ import RunIcon from "@mui/icons-material/PlayArrow";
import RedoIcon from "@mui/icons-material/Refresh";
import UndoIcon from "@mui/icons-material/Undo";
import SteppedAccordion from "@src/components/SteppedAccordion";
import MultiSelect from "@rowy/multiselect";
import FieldSkeleton from "@src/components/SideDrawer/Form/FieldSkeleton";
import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
@@ -101,287 +102,368 @@ const Settings = ({ config, onChange }) => {
config.confirmation !== "");
return (
<Stepper
nonLinear
activeStep={steps.indexOf(activeStep)}
orientation="vertical"
sx={{
mt: 0,
"& .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("requirements")}>
Requirements
<ExpandIcon />
</StepButton>
<StepContent>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<MultiSelect
label="Required roles"
options={roles ?? []}
value={config.requiredRoles ?? []}
onChange={onChange("requiredRoles")}
TextFieldProps={{
id: "requiredRoles",
helperText:
"The user must have at least one of these roles to run the script",
}}
/>
</Grid>
<Grid item xs={12} sm={6}>
<MultiSelect
label="Required fields"
options={columnOptions}
value={config.requiredFields ?? []}
onChange={onChange("requiredFields")}
TextFieldProps={{
id: "requiredFields",
helperText:
"All the selected fields must have a value for the script to run",
}}
/>
</Grid>
</Grid>
</StepContent>
</Step>
<Step>
<StepButton onClick={() => setActiveStep("friction")}>
Confirmation
<ExpandIcon />
</StepButton>
<StepContent>
<Stack spacing={3}>
<FormControl component="fieldset">
<FormLabel component="legend">
Clicking the action button will:
</FormLabel>
<RadioGroup
aria-label="Action button friction"
name="friction"
defaultValue={
typeof config.confirmation === "string" &&
config.confirmation !== ""
? "confirmation"
: "none"
}
value={config.friction}
onChange={(e) => onChange("friction")(e.target.value)}
>
<FormControlLabel
value="none"
control={<Radio />}
label="Run the action immediately"
<SteppedAccordion
steps={[
{
id: "requirements",
title: "Requirements",
content: (
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<MultiSelect
label="Required roles"
options={roles ?? []}
value={config.requiredRoles ?? []}
onChange={onChange("requiredRoles")}
TextFieldProps={{
id: "requiredRoles",
helperText:
"The user must have at least one of these roles to run the script",
}}
/>
<FormControlLabel
value="confirmation"
control={<Radio />}
label="Ask the user for confirmation"
</Grid>
<Grid item xs={12} sm={6}>
<MultiSelect
label="Required fields"
options={columnOptions}
value={config.requiredFields ?? []}
onChange={onChange("requiredFields")}
TextFieldProps={{
id: "requiredFields",
helperText:
"All the selected fields must have a value for the script to run",
}}
/>
<FormControlLabel
value="params"
control={<Radio />}
label={
<>
<Typography variant="inherit">
Ask the user for input in a form (Alpha)
</Typography>
</Grid>
</Grid>
),
},
{
id: "confirmation",
title: "Confirmation",
content: (
<Stack spacing={3}>
<FormControl component="fieldset">
<FormLabel component="legend">
Clicking the action button will:
</FormLabel>
<Typography variant="caption" color="text.secondary">
This feature is currently undocumented and is subject to
change in future minor versions
</Typography>
</>
<RadioGroup
aria-label="Action button friction"
name="friction"
defaultValue={
typeof config.confirmation === "string" &&
config.confirmation !== ""
? "confirmation"
: "none"
}
/>
</RadioGroup>
</FormControl>
{showConfirmationField && (
<TextField
id="confirmation"
label="Confirmation template"
placeholder="Are sure you want to invest {{stockName}}?"
value={config.confirmation}
onChange={(e) => onChange("confirmation")(e.target.value)}
fullWidth
helperText="The action button will not ask for confirmation if this is left empty"
/>
)}
{config.friction === "params" && (
<FormControl>
<Grid container spacing={1} sx={{ mb: 0.5 }}>
<Grid item xs>
<InputLabel variant="filled">Form fields</InputLabel>
</Grid>
<Grid item>
<FormFieldSnippets />
</Grid>
</Grid>
<Suspense fallback={<FieldSkeleton height={300} />}>
<CodeEditor
minHeight={200}
defaultLanguage="json"
value={formattedParamsJson}
onChange={(v) => {
try {
if (v) {
const parsed = JSON.parse(v);
onChange("params")(parsed);
}
} catch (e) {
console.log(`Failed to parse JSON: ${e}`);
setCodeValid(false);
}
}}
onValidStatusUpdate={({ isValid }) => setCodeValid(isValid)}
error={!codeValid}
value={config.friction}
onChange={(e) => onChange("friction")(e.target.value)}
>
<FormControlLabel
value="none"
control={<Radio />}
label="Run the action immediately"
/>
</Suspense>
<FormControlLabel
value="confirmation"
control={<Radio />}
label="Ask the user for confirmation"
/>
<FormControlLabel
value="params"
control={<Radio />}
label={
<>
<Typography variant="inherit">
Ask the user for input in a form (Alpha)
</Typography>
{!codeValid && (
<FormHelperText error variant="filled">
Invalid JSON
</FormHelperText>
)}
<Typography variant="caption" color="text.secondary">
This feature is currently undocumented and is subject
to change in future minor versions
</Typography>
</>
}
/>
</RadioGroup>
</FormControl>
)}
</Stack>
</StepContent>
</Step>
<Step>
<StepButton onClick={() => setActiveStep("action")}>
Action
<ExpandIcon />
</StepButton>
<StepContent>
<Stack spacing={3}>
<FormControl component="fieldset">
<FormLabel component="legend">
Clicking the action button will run a:
</FormLabel>
<RadioGroup
aria-label="Action will run"
name="isActionScript"
value={config.isActionScript ? "actionScript" : "cloudFunction"}
onChange={(e) =>
onChange("isActionScript")(e.target.value === "actionScript")
}
>
<FormControlLabel
value="actionScript"
control={<Radio />}
label={
<>
<Typography variant="inherit">Script</Typography>
<Typography variant="caption" color="textSecondary">
Write JavaScript code below that will be executed by
Rowy Run.{" "}
<Link
href={WIKI_LINKS.rowyRun}
target="_blank"
rel="noopener noreferrer"
>
Requires Rowy Run setup
<InlineOpenInNewIcon />
</Link>
</Typography>
</>
}
{showConfirmationField && (
<TextField
id="confirmation"
label="Confirmation template"
placeholder="Are sure you want to invest {{stockName}}?"
value={config.confirmation}
onChange={(e) => onChange("confirmation")(e.target.value)}
fullWidth
helperText="The action button will not ask for confirmation if this is left empty"
/>
<FormControlLabel
value="cloudFunction"
control={<Radio />}
label={
<>
<Typography variant="inherit">Callable</Typography>
<Typography variant="caption" color="textSecondary">
A{" "}
<Link
href="https://firebase.google.com/docs/functions/callable"
target="_blank"
rel="noopener noreferrer"
>
callable function
<InlineOpenInNewIcon />
</Link>{" "}
youve deployed on your Firestore or Google Cloud
project
</Typography>
</>
}
/>
</RadioGroup>
</FormControl>
)}
{!config.isActionScript ? (
<TextField
id="callableName"
label="Callable name"
name="callableName"
value={config.callableName}
fullWidth
onChange={(e) => onChange("callableName")(e.target.value)}
helperText={
<>
Write the name of the callable function youve deployed to
your project.{" "}
<Link
href={`https://console.firebase.google.com/project/rowyio/functions/list`}
target="_blank"
rel="noopener noreferrer"
>
View your callable functions
<InlineOpenInNewIcon />
</Link>
<br />
Your callable function must be compatible with Rowy Action
columns.{" "}
<Link
href={WIKI_LINKS.fieldTypesAction + "#callable"}
target="_blank"
rel="noopener noreferrer"
>
View requirements
<InlineOpenInNewIcon />
</Link>
</>
}
/>
) : (
<>
{config.friction === "params" && (
<FormControl>
<InputLabel variant="filled">Action script</InputLabel>
<Grid container spacing={1} sx={{ mb: 0.5 }}>
<Grid item xs>
<InputLabel variant="filled">Form fields</InputLabel>
</Grid>
<Grid item>
<FormFieldSnippets />
</Grid>
</Grid>
<Suspense fallback={<FieldSkeleton height={300} />}>
<CodeEditor
minHeight={200}
value={config.script}
onChange={onChange("script")}
defaultLanguage="json"
value={formattedParamsJson}
onChange={(v) => {
try {
if (v) {
const parsed = JSON.parse(v);
onChange("params")(parsed);
}
} catch (e) {
console.log(`Failed to parse JSON: ${e}`);
setCodeValid(false);
}
}}
onValidStatusUpdate={({ isValid }) =>
setCodeValid(isValid)
}
error={!codeValid}
/>
</Suspense>
{!codeValid && (
<FormHelperText error variant="filled">
Invalid JSON
</FormHelperText>
)}
</FormControl>
)}
</Stack>
),
},
{
id: "action",
title: "Action",
content: (
<Stack spacing={3}>
<FormControl component="fieldset">
<FormLabel component="legend">
Clicking the action button will run a:
</FormLabel>
<RadioGroup
aria-label="Action will run"
name="isActionScript"
value={
config.isActionScript ? "actionScript" : "cloudFunction"
}
onChange={(e) =>
onChange("isActionScript")(
e.target.value === "actionScript"
)
}
>
<FormControlLabel
value="actionScript"
control={<Radio />}
label={
<>
<Typography variant="inherit">Script</Typography>
<Typography variant="caption" color="textSecondary">
Write JavaScript code below that will be executed by
Rowy Run.{" "}
<Link
href={WIKI_LINKS.rowyRun}
target="_blank"
rel="noopener noreferrer"
>
Requires Rowy Run setup
<InlineOpenInNewIcon />
</Link>
</Typography>
</>
}
/>
<FormControlLabel
value="cloudFunction"
control={<Radio />}
label={
<>
<Typography variant="inherit">Callable</Typography>
<Typography variant="caption" color="textSecondary">
A{" "}
<Link
href="https://firebase.google.com/docs/functions/callable"
target="_blank"
rel="noopener noreferrer"
>
callable function
<InlineOpenInNewIcon />
</Link>{" "}
youve deployed on your Firestore or Google Cloud
project
</Typography>
</>
}
/>
</RadioGroup>
</FormControl>
{!config.isActionScript ? (
<TextField
id="callableName"
label="Callable name"
name="callableName"
value={config.callableName}
fullWidth
onChange={(e) => onChange("callableName")(e.target.value)}
helperText={
<>
Write the name of the callable function youve deployed to
your project.{" "}
<Link
href={`https://console.firebase.google.com/project/rowyio/functions/list`}
target="_blank"
rel="noopener noreferrer"
>
View your callable functions
<InlineOpenInNewIcon />
</Link>
<br />
Your callable function must be compatible with Rowy Action
columns.{" "}
<Link
href={WIKI_LINKS.fieldTypesAction + "#callable"}
target="_blank"
rel="noopener noreferrer"
>
View requirements
<InlineOpenInNewIcon />
</Link>
</>
}
/>
) : (
<>
<FormControl>
<InputLabel variant="filled">Action script</InputLabel>
<Suspense fallback={<FieldSkeleton height={300} />}>
<CodeEditor
minHeight={200}
value={config.script}
onChange={onChange("script")}
extraLibs={scriptExtraLibs}
/>
</Suspense>
<CodeEditorHelper
docLink={WIKI_LINKS.fieldTypesAction + "#script"}
additionalVariables={[]}
/>
</FormControl>
<Grid container>
<Grid item xs={12} sm={6}>
<FormControlLabel
control={
<Checkbox
checked={config.redo?.enabled}
onChange={() =>
onChange("redo.enabled")(
!Boolean(config.redo?.enabled)
)
}
name="redo"
/>
}
label={
<>
<Typography variant="inherit">
User can redo
</Typography>
<Typography
variant="caption"
color="text.secondary"
>
Re-runs the script above
</Typography>
</>
}
style={{ marginLeft: -11 }}
/>
</Grid>
<Grid item xs={12} sm={6}>
<FormControlLabel
control={
<Checkbox
checked={config.undo?.enabled}
onChange={() =>
onChange("undo.enabled")(
!Boolean(config.undo?.enabled)
)
}
name="undo"
/>
}
label={
<>
<Typography variant="inherit">
User can undo
</Typography>
<Typography
variant="caption"
color="text.secondary"
>
Runs a new script
</Typography>
</>
}
style={{ marginLeft: -11 }}
/>
</Grid>
</Grid>
</>
)}
</Stack>
),
},
config.isActionScript &&
_get(config, "undo.enabled") && {
id: "undo",
title: "Undo action",
content: (
<Stack spacing={3}>
{(showConfirmationField ||
!config.friction ||
config.friction === "none") && (
<TextField
id="undo.confirmation"
label="Undo confirmation template"
placeholder="Are you sure you want to sell your stocks in {{stockName}}?"
value={_get(config, "undo.confirmation")}
onChange={(e) => {
onChange("undo.confirmation")(e.target.value);
}}
fullWidth
helperText={
<>
{showConfirmationField &&
"Override the confirmation message above. "}
The action button will not ask for confirmation if this
is left empty{showConfirmationField && "."}
</>
}
/>
)}
<FormControl>
<InputLabel variant="filled">Undo script</InputLabel>
<Suspense fallback={<FieldSkeleton height={300} />}>
<CodeEditor
value={_get(config, "undo.script")}
onChange={onChange("undo.script")}
extraLibs={scriptExtraLibs}
/>
</Suspense>
@@ -390,230 +472,90 @@ const Settings = ({ config, onChange }) => {
additionalVariables={[]}
/>
</FormControl>
</Stack>
),
},
{
id: "customization",
title: "Customization",
content: (
<>
<FormControlLabel
control={
<Checkbox
checked={config.customIcons?.enabled}
onChange={(e) =>
onChange("customIcons.enabled")(e.target.checked)
}
name="customIcons.enabled"
/>
}
label="Customize button icons with emoji"
style={{ marginLeft: -11 }}
/>
<Grid container>
<Grid item xs={12} sm={6}>
<FormControlLabel
control={
<Checkbox
checked={config.redo?.enabled}
onChange={() =>
onChange("redo.enabled")(
!Boolean(config.redo?.enabled)
)
}
name="redo"
/>
}
label={
<>
<Typography variant="inherit">
User can redo
</Typography>
<Typography variant="caption" color="text.secondary">
Re-runs the script above
</Typography>
</>
}
style={{ marginLeft: -11 }}
/>
{config.customIcons?.enabled && (
<Grid container spacing={2} sx={{ mt: { xs: 0, sm: -1 } }}>
<Grid item xs={12} sm={true}>
<Stack direction="row" spacing={1}>
<TextField
id="customIcons.run"
value={_get(config, "customIcons.run")}
onChange={(e) =>
onChange("customIcons.run")(e.target.value)
}
label="Run:"
className="labelHorizontal"
inputProps={{ style: { width: "3ch" } }}
/>
<Fab size="small" aria-label="Preview of run button">
{_get(config, "customIcons.run") || <RunIcon />}
</Fab>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<FormControlLabel
control={
<Checkbox
checked={config.undo?.enabled}
onChange={() =>
onChange("undo.enabled")(
!Boolean(config.undo?.enabled)
)
}
name="undo"
/>
}
label={
<>
<Typography variant="inherit">
User can undo
</Typography>
<Typography variant="caption" color="text.secondary">
Runs a new script
</Typography>
</>
}
style={{ marginLeft: -11 }}
/>
<Grid item xs={12} sm={true}>
<Stack direction="row" spacing={1}>
<TextField
id="customIcons.redo"
value={_get(config, "customIcons.redo")}
onChange={(e) =>
onChange("customIcons.redo")(e.target.value)
}
label="Redo:"
className="labelHorizontal"
inputProps={{ style: { width: "3ch" } }}
/>
<Fab size="small" aria-label="Preview of redo button">
{_get(config, "customIcons.redo") || <RedoIcon />}
</Fab>
</Stack>
</Grid>
<Grid item xs={12} sm={true}>
<Stack direction="row" spacing={1}>
<TextField
id="customIcons.undo"
value={_get(config, "customIcons.undo")}
onChange={(e) =>
onChange("customIcons.undo")(e.target.value)
}
label="Undo:"
className="labelHorizontal"
inputProps={{ style: { width: "3ch" } }}
/>
<Fab size="small" aria-label="Preview of undo button">
{_get(config, "customIcons.undo") || <UndoIcon />}
</Fab>
</Stack>
</Grid>
</Grid>
</>
)}
</Stack>
</StepContent>
</Step>
{config.isActionScript && _get(config, "undo.enabled") && (
<Step>
<StepButton onClick={() => setActiveStep("undo")}>
Undo action
<ExpandIcon />
</StepButton>
<StepContent>
<Stack spacing={3}>
{(showConfirmationField ||
!config.friction ||
config.friction === "none") && (
<TextField
id="undo.confirmation"
label="Undo confirmation template"
placeholder="Are you sure you want to sell your stocks in {{stockName}}?"
value={_get(config, "undo.confirmation")}
onChange={(e) => {
onChange("undo.confirmation")(e.target.value);
}}
fullWidth
helperText={
<>
{showConfirmationField &&
"Override the confirmation message above. "}
The action button will not ask for confirmation if this is
left empty{showConfirmationField && "."}
</>
}
/>
)}
<FormControl>
<InputLabel variant="filled">Undo script</InputLabel>
<Suspense fallback={<FieldSkeleton height={300} />}>
<CodeEditor
value={_get(config, "undo.script")}
onChange={onChange("undo.script")}
extraLibs={scriptExtraLibs}
/>
</Suspense>
<CodeEditorHelper
docLink={WIKI_LINKS.fieldTypesAction + "#script"}
additionalVariables={[]}
/>
</FormControl>
</Stack>
</StepContent>
</Step>
)}
<Step>
<StepButton onClick={() => setActiveStep("customization")}>
Customization
<ExpandIcon />
</StepButton>
<StepContent>
{/* <Stack spacing={3}> */}
<FormControlLabel
control={
<Checkbox
checked={config.customIcons?.enabled}
onChange={(e) =>
onChange("customIcons.enabled")(e.target.checked)
}
name="customIcons.enabled"
/>
}
label="Customize button icons with emoji"
style={{ marginLeft: -11 }}
/>
{config.customIcons?.enabled && (
<Grid container spacing={2} sx={{ mt: { xs: 0, sm: -1 } }}>
<Grid item xs={12} sm={true}>
<Stack direction="row" spacing={1}>
<TextField
id="customIcons.run"
value={_get(config, "customIcons.run")}
onChange={(e) =>
onChange("customIcons.run")(e.target.value)
}
label="Run:"
className="labelHorizontal"
inputProps={{ style: { width: "3ch" } }}
/>
<Fab size="small" aria-label="Preview of run button">
{_get(config, "customIcons.run") || <RunIcon />}
</Fab>
</Stack>
</Grid>
<Grid item xs={12} sm={true}>
<Stack direction="row" spacing={1}>
<TextField
id="customIcons.redo"
value={_get(config, "customIcons.redo")}
onChange={(e) =>
onChange("customIcons.redo")(e.target.value)
}
label="Redo:"
className="labelHorizontal"
inputProps={{ style: { width: "3ch" } }}
/>
<Fab size="small" aria-label="Preview of redo button">
{_get(config, "customIcons.redo") || <RedoIcon />}
</Fab>
</Stack>
</Grid>
<Grid item xs={12} sm={true}>
<Stack direction="row" spacing={1}>
<TextField
id="customIcons.undo"
value={_get(config, "customIcons.undo")}
onChange={(e) =>
onChange("customIcons.undo")(e.target.value)
}
label="Undo:"
className="labelHorizontal"
inputProps={{ style: { width: "3ch" } }}
/>
<Fab size="small" aria-label="Preview of undo button">
{_get(config, "customIcons.undo") || <UndoIcon />}
</Fab>
</Stack>
</Grid>
</Grid>
)}
{/* </Stack> */}
{/* <Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<MultiSelect
label="Required roles"
options={roles ?? []}
value={config.requiredRoles ?? []}
onChange={onChange("requiredRoles")}
TextFieldProps={{
id: "requiredRoles",
helperText:
"The user must have at least one of these roles to run the script",
}}
/>
</Grid>
<Grid item xs={12} sm={6}>
<MultiSelect
label="Required fields"
options={columnOptions}
value={config.requiredFields ?? []}
onChange={onChange("requiredFields")}
TextFieldProps={{
id: "requiredFields",
helperText:
"All the selected fields must have a value for the script to run",
}}
/>
</Grid>
</Grid> */}
</StepContent>
</Step>
</Stepper>
</>
),
},
].filter(Boolean)}
/>
);
};
export default Settings;