Skip to content

Commit

Permalink
feat(app): improved target branch selection (#407)
Browse files Browse the repository at this point in the history
* feat(app): improved target branch selection

* design tweaks

* feat(app): simplify target branch description

---------

Co-authored-by: Ondřej Pešička <77627332+OPesicka@users.noreply.github.com>
Co-authored-by: OPesicka <ondrejpesicka@gmail.com>
  • Loading branch information
3 people committed Jun 22, 2024
1 parent 3e5d9cb commit dada486
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
import type { FlowSteps, WaitStepOptions } from "@flows/js";
import type {
FlowModalStep,
FlowStep,
FlowSteps,
FlowTooltipStep,
FlowWaitStep,
FooterActionItem,
WaitStepOptions,
} from "@flows/js";
import { type FlowDetail, type UpdateFlow } from "lib/api";
import { useFormContext } from "react-hook-form";

import { type MatchGroup } from "./targeting";

export type FooterActionPlacement = "left" | "center" | "right";

export type WaitOptions = Omit<WaitStepOptions, "targetBranch"> & { targetBranch?: null | number };
type FooterItem = Omit<FooterActionItem, "targetBranch"> & { targetBranch?: null | number };
type IFooterActions = { left?: FooterItem[]; center?: FooterItem[]; right?: FooterItem[] };
type TooltipStep = Omit<FlowTooltipStep, "wait" | "footerActions"> & {
wait?: WaitOptions | WaitOptions[];
footerActions?: IFooterActions;
};
type ModalStep = Omit<FlowModalStep, "wait" | "footerActions"> & {
wait?: WaitOptions | WaitOptions[];
footerActions?: IFooterActions;
};
type IStep = TooltipStep | ModalStep | FlowWaitStep;
type Step = IStep | IStep[][];

export type IFlowEditForm = Pick<UpdateFlow, "frequency"> & {
steps: FlowSteps;
steps: Step[];

userProperties: MatchGroup[];
start: WaitStepOptions[];
};
Expand All @@ -31,13 +56,55 @@ export type SelectedItem =
| "frequency";

export const formToRequest = (data: IFlowEditForm): UpdateFlow => {
const fixedUserProperties = data.userProperties
.map((group) => group.filter((matcher) => !!matcher.key))
.filter((group) => !!group.length);
return {
...data,
start: data.start as unknown as UpdateFlow["start"],
steps: data.steps as unknown as UpdateFlow["steps"],
userProperties: fixedUserProperties,
};
};

export const fixFormData = (data: IFlowEditForm): IFlowEditForm => {
const fixedUserProperties = data.userProperties
.map((group) => group.filter((matcher) => !!matcher.key))
.filter((group) => !!group.length);

const fixedSteps = data.steps.map((step, i, arr) => {
if (Array.isArray(step)) return step;
const nextStep = arr.at(i + 1);
const nextStepBranchCount = Array.isArray(nextStep) ? nextStep.length : null;
const fixTargetBranch = (targetBranch: number | null | undefined): number | undefined => {
if (targetBranch === undefined || targetBranch === null) return;
if (nextStepBranchCount === null) return;
if (targetBranch >= nextStepBranchCount) return;
return targetBranch;
};

const waitArray = step.wait ? (Array.isArray(step.wait) ? step.wait : [step.wait]) : undefined;
const wait = waitArray?.map((w) => ({
...w,
targetBranch: fixTargetBranch(w.targetBranch),
}));

const footerActions =
"footerActions" in step && step.footerActions
? Object.entries(step.footerActions).reduce(
(acc, [key, actions]: [string, FooterActionItem[]]) => {
acc[key] = actions.map((action) => ({
...action,
targetBranch: fixTargetBranch(action.targetBranch),
}));
return acc;
},
{},
)
: undefined;

return {
...step,
wait,
footerActions,
} as FlowStep;
});

return { ...data, userProperties: fixedUserProperties, steps: fixedSteps };
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { FlowPublishChangesDialog } from "../(detail)/flow-publish-changes-dialo
import { Autosave } from "./autosave";
import {
createDefaultValues,
fixFormData,
formToRequest,
type IFlowEditForm,
type SelectedItem,
Expand Down Expand Up @@ -52,9 +53,10 @@ export const FlowEditForm: FC<Props> = ({ flow, organizationId }) => {
const { send } = useSend();
const onSubmit: SubmitHandler<IFlowEditForm> = useCallback(
async (data) => {
const res = await send(api["PATCH /flows/:flowId"](flow.id, formToRequest(data)), {
errorMessage: t.toasts.saveFlowFailed,
});
const res = await send(
api["PATCH /flows/:flowId"](flow.id, formToRequest(fixFormData(data))),
{ errorMessage: t.toasts.saveFlowFailed },
);
if (res.error) return;
reset(data, { keepValues: true });
router.refresh();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ import { Controller, useFieldArray } from "react-hook-form";
import { t } from "translations";
import { Button, Checkbox, Icon, Input, Label, Select, Text } from "ui";

import { useFlowEditForm } from "../edit-constants";

type Placement = "left" | "center" | "right";
import { type FooterActionPlacement, useFlowEditForm } from "../edit-constants";
import { TargetBranchInput } from "./target-branch-input";

type Props = {
index: number | `${number}.${number}.${number}`;
placement: Placement;
placement: FooterActionPlacement;
};

export const StepFooterActions: FC<Props> = ({ index, placement }) => {
Expand Down Expand Up @@ -51,12 +50,14 @@ export const StepFooterActions: FC<Props> = ({ index, placement }) => {

type OptionProps = {
fieldName:
| `steps.${number}.footerActions.${Placement}.${number}`
| `steps.${number}.${number}.${number}.footerActions.${Placement}.${number}`;
| `steps.${number}.footerActions.${FooterActionPlacement}.${number}`
| `steps.${number}.${number}.${number}.footerActions.${FooterActionPlacement}.${number}`;
onRemove: () => void;
index: number;
};

type OptionKey = "href" | "targetBranch" | "prev" | "next" | "cancel";

const Option: FC<OptionProps> = ({ fieldName, onRemove, index }) => {
const { control, setValue, watch } = useFlowEditForm();
const value = watch(fieldName);
Expand All @@ -79,6 +80,15 @@ const Option: FC<OptionProps> = ({ fieldName, onRemove, index }) => {
setValue(fieldName, newValue, { shouldDirty: true });
};

const targetBranchIsEnabled = fieldName.split(".footerActions.").at(0)?.split(".").length === 2;
const options: OptionKey[] = [
"href",
...(targetBranchIsEnabled ? (["targetBranch"] as const) : []),
"prev",
"next",
"cancel",
];

return (
<Box borBottom="1px" padding="space12">
<Flex alignItems="center" gap="space8" justifyContent="space-between" mb="space8">
Expand Down Expand Up @@ -111,7 +121,7 @@ const Option: FC<OptionProps> = ({ fieldName, onRemove, index }) => {
/>

<Flex cardWrap="-" mb="space16" overflowX="auto">
{(["href", "targetBranch", "prev", "next", "cancel"] as const).map((variant) => (
{options.map((variant) => (
<Button
className={css({ flex: 1, fontWeight: "normal" })}
key={variant}
Expand Down Expand Up @@ -149,22 +159,7 @@ const Option: FC<OptionProps> = ({ fieldName, onRemove, index }) => {
</Box>
)}

{currentVariant === "targetBranch" && (
<Controller
control={control}
name={`${fieldName}.targetBranch`}
render={({ field }) => (
<Input
description={t.steps.targetBranchDescription}
label={t.steps.targetBranchLabel}
onChange={(e) => field.onChange(Number(e.target.value))}
placeholder="0"
type="number"
value={field.value}
/>
)}
/>
)}
{currentVariant === "targetBranch" && <TargetBranchInput fieldName={fieldName} />}
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import type { WaitStepOptions } from "@flows/js";
import { css } from "@flows/styled-system/css";
import { Box, Flex } from "@flows/styled-system/jsx";
import { Close16 } from "icons";
import type { FC } from "react";
import { Controller } from "react-hook-form";
import { t } from "translations";
import { Button, Icon, Input, Text } from "ui";

import { useFlowEditForm } from "../edit-constants";
import { useFlowEditForm, type WaitOptions } from "../edit-constants";
import { StepWaitChange } from "./step-wait-change";
import { StepWaitForm } from "./step-wait-submit";
import { TargetBranchInput } from "./target-branch-input";

type FieldName =
| `steps.${number}.wait.${number}`
| `steps.${number}.${number}.${number}.wait.${number}`
| `start.${number}`;

type Props = {
fieldName:
| `steps.${number}.wait.${number}`
| `steps.${number}.${number}.${number}.wait.${number}`
| `start.${number}`;
fieldName: FieldName;
onRemove: () => void;
index: number;
};

export const StepWaitOption: FC<Props> = ({ fieldName, index, onRemove }) => {
const { setValue, register, watch, control } = useFlowEditForm();
const { setValue, register, watch } = useFlowEditForm();
const value = watch(fieldName);

const isStart = fieldName.startsWith("start.");
Expand All @@ -34,7 +35,7 @@ export const StepWaitOption: FC<Props> = ({ fieldName, index, onRemove }) => {
return "empty";
})();
const handleVariantChange = (variant: typeof currentVariant): void => {
let newValue: WaitStepOptions | null = null;
let newValue: WaitOptions | null = null;
if (variant === "change")
newValue = {
location: value.location,
Expand Down Expand Up @@ -123,24 +124,17 @@ export const StepWaitOption: FC<Props> = ({ fieldName, index, onRemove }) => {
{currentVariant === "change" && <StepWaitChange fieldName={fieldName} />}
{currentVariant === "submit" && <StepWaitForm fieldName={fieldName} />}

{!isStart && (
<Controller
control={control}
name={`${fieldName}.targetBranch`}
render={({ field }) => (
<Input
className={css({ mt: "space16" })}
description={t.steps.targetBranchDescription}
label={t.steps.targetBranchLabel}
onChange={(e) => field.onChange(Number(e.target.value))}
placeholder="0"
type="number"
value={field.value}
/>
)}
/>
)}
{isNotStart(fieldName) && <TargetBranchInput fieldName={fieldName} />}

<hr className={css({ borderColor: "border", my: "space16", mx: "-space16" })} />
</Box>
);
};

function isNotStart(
fieldName: FieldName,
): fieldName is
| `steps.${number}.wait.${number}`
| `steps.${number}.${number}.${number}.wait.${number}` {
return fieldName.startsWith("steps.");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Flex } from "@flows/styled-system/jsx";
import { type FC } from "react";
import { Controller } from "react-hook-form";
import { t } from "translations";
import { Button, Text } from "ui";

import { type FooterActionPlacement, useFlowEditForm } from "../edit-constants";

type Props = {
fieldName:
| `steps.${number}.wait.${number}`
| `steps.${number}.${number}.${number}.wait.${number}`
| `steps.${number}.footerActions.${FooterActionPlacement}.${number}`
| `steps.${number}.${number}.${number}.footerActions.${FooterActionPlacement}.${number}`;
};

export const TargetBranchInput: FC<Props> = ({ fieldName }) => {
const { watch, control } = useFlowEditForm();

const nextStepBranchCount = (() => {
const isForkStep = fieldName.includes(".wait")
? fieldName.split(".").length === 6
: fieldName.split(".").length === 7;
if (isForkStep) return null;
const stepNumberString = fieldName.split(".").at(1);
if (stepNumberString === undefined) return null;
const nextStepNumber = Number(stepNumberString) + 1;
const nextStep = watch(`steps.${nextStepNumber}`);
if (!Array.isArray(nextStep)) return null;
return nextStep.length;
})();

if (nextStepBranchCount === null) return null;

const targetBranchFieldName = `${fieldName}.targetBranch` as const;

return (
<Controller
control={control}
name={targetBranchFieldName}
render={({ field }) => {
const options = Array(nextStepBranchCount)
.fill(null)
.map((_, i) => i);

return (
<>
<Text mt="space16">{t.steps.targetBranchLabel}</Text>
<Text color="subtle" variant="bodyXs" mb="space8">
{t.steps.targetBranchDescription}
</Text>

<Flex cardWrap="-" overflowX="auto" display="inline-flex">
<Button
variant={field.value === null ? "black" : "ghost"}
onClick={() => field.onChange(null)}
size="small"
>
None
</Button>
{options.map((opt) => {
const active = field.value === opt;
return (
<Button
key={opt}
size="small"
variant={active ? "black" : "ghost"}
onClick={() => field.onChange(opt)}
>
{opt}
</Button>
);
})}
</Flex>
</>
);
}}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const Fork: FC<Props> = ({ index, onSelectStep, selectedStep, onRemove })
size="smallIcon"
variant="secondary"
className={css({ opacity: "0" })}
onClick={() => append(STEP_DEFAULT.fork as never[])}
onClick={() => append([[STEP_DEFAULT.tooltip]] as never[])}
>
<Icon icon={Fork16} />
</Button>
Expand Down
3 changes: 1 addition & 2 deletions apps/app/src/translations/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,7 @@ export const t = {
stepIdLabel: "Step ID",
stepIdDescription: "Unique ID of the step. Useful for programmatic control of the flow.",
targetBranchLabel: "Target branch",
targetBranchDescription:
"Which branch to take. Leave empty is there is no fork step after this step.",
targetBranchDescription: "Which branch to take.",
footer: {
buttonAlignment: {
left: "Left",
Expand Down

0 comments on commit dada486

Please sign in to comment.