Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: LEAP-1454: Custom action buttons in LSF #6411

Merged
merged 11 commits into from
Sep 26, 2024
3 changes: 3 additions & 0 deletions web/libs/editor/src/common/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export interface ButtonProps extends HTMLButtonProps {
tooltip?: string;
tooltipTheme?: "light" | "dark";
nopadding?: boolean;
// Block props
// @todo can be imported/infered from Block
mod?: Record<string, any>;
}

export interface ButtonGroupProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,56 @@
* Only this component should get interface updates, other versions should be removed.
*/

import { inject, observer } from "mobx-react";
import { observer } from "mobx-react";
import type { Instance } from "mobx-state-tree";
import type React from "react";
import { useCallback, useState } from "react";

import { IconBan, LsChevron } from "../../assets/icons";
import { Button } from "../../common/Button/Button";
import { Tooltip } from "../../common/Tooltip/Tooltip";
import { Dropdown } from "../../common/Dropdown/Dropdown";
import type { CustomButton } from "../../stores/CustomButton";
import { Block, cn, Elem } from "../../utils/bem";
import { isDefined } from "../../utils/utilities";
import { IconBan } from "../../assets/icons";
import { FF_REVIEWER_FLOW, isFF } from "../../utils/feature-flags";
import "./Controls.scss";
import { useCallback, useMemo, useState } from "react";
import { LsChevron } from "../../assets/icons";
import { Dropdown } from "../../common/Dropdown/DropdownComponent";
import { isDefined } from "../../utils/utilities";
import { AcceptButton, ButtonTooltip, controlsInjector, RejectButton, SkipButton, UnskipButton } from "./buttons";

const TOOLTIP_DELAY = 0.8;
import "./Controls.scss";

const ButtonTooltip = inject("store")(
observer(({ store, title, children }) => {
return (
<Tooltip title={title} enabled={store.settings.enableTooltips} mouseEnterDelay={TOOLTIP_DELAY}>
{children}
</Tooltip>
);
}),
);
type CustomControlProps = {
button: Instance<typeof CustomButton>;
disabled: boolean;
onClick?: (name: string) => void;
};

const controlsInjector = inject(({ store }) => {
return {
store,
history: store?.annotationStore?.selected?.history,
};
/**
* Custom action button component, rendering buttons from store.customButtons
*/
const CustomControl = observer(({ button, disabled, onClick }: CustomControlProps) => {
const look = button.disabled || disabled ? "disabled" : button.look;
const [waiting, setWaiting] = useState(false);
const clickHandler = useCallback(async () => {
if (!onClick) return;
setWaiting(true);
await onClick?.(button.name);
setWaiting(false);
}, []);
return (
<ButtonTooltip title={button.tooltip ?? ""}>
<Button
aria-label={button.ariaLabel}
disabled={button.disabled || disabled || waiting}
look={look}
onClick={clickHandler}
waiting={waiting}
>
{button.title}
</Button>
</ButtonTooltip>
);
});

export const Controls = controlsInjector(
export const Controls = controlsInjector<{ annotation: MSTAnnotation }>(
observer(({ store, history, annotation }) => {
const isReview = store.hasInterface("review") || annotation.canBeReviewed;
const isNotQuickView = store.hasInterface("topbar:prevnext");
Expand All @@ -49,7 +67,7 @@ export const Controls = controlsInjector(
const submitDisabled = store.hasInterface("annotations:deny-empty") && results.length === 0;

const buttonHandler = useCallback(
async (e, callback, tooltipMessage) => {
async (e: React.MouseEvent, callback: () => any, tooltipMessage: string) => {
const { addedCommentThisSession, currentComment, commentFormSubmit } = store.commentStore;

if (isInProgress) return;
Expand Down Expand Up @@ -80,110 +98,56 @@ export const Controls = controlsInjector(
],
);

const RejectButton = useMemo(() => {
return (
<ButtonTooltip key="reject" title="Reject annotation: [ Ctrl+Space ]">
<Button
aria-label="reject-annotation"
disabled={disabled}
onClick={async (e) => {
if (store.hasInterface("comments:reject") ?? true) {
buttonHandler(e, () => store.rejectAnnotation({}), "Please enter a comment before rejecting");
} else {
const selected = store.annotationStore?.selected;

selected?.submissionInProgress();
await store.commentStore.commentFormSubmit();
store.rejectAnnotation({});
}
}}
>
Reject
</Button>
</ButtonTooltip>
);
}, [disabled, store]);

if (isReview) {
buttons.push(RejectButton);

buttons.push(
<ButtonTooltip key="accept" title="Accept annotation: [ Ctrl+Enter ]">
<Button
aria-label="accept-annotation"
disabled={disabled}
look="primary"
onClick={async () => {
const selected = store.annotationStore?.selected;
// custom buttons replace all the internal buttons, but they can be reused if `name` is one of the internal buttons
if (store.customButtons?.length) {
for (const customButton of store.customButtons ?? []) {
// @todo make a list of all internal buttons and use them here to mix custom buttons with internal ones
if (customButton.name === "accept") {
buttons.push(<AcceptButton disabled={disabled} history={history} store={store} />);
} else {
buttons.push(
<CustomControl
key={customButton.name}
disabled={disabled}
button={customButton}
onClick={store.handleCustomButton}
/>,
);
}
}
} else if (isReview) {
const onRejectWithComment = (e: React.MouseEvent, action: () => any) => {
buttonHandler(e, action, "Please enter a comment before rejecting");
};

selected?.submissionInProgress();
await store.commentStore.commentFormSubmit();
store.acceptAnnotation();
}}
>
{history.canUndo ? "Fix + Accept" : "Accept"}
</Button>
</ButtonTooltip>,
);
buttons.push(<RejectButton disabled={disabled} store={store} onRejectWithComment={onRejectWithComment} />);
buttons.push(<AcceptButton disabled={disabled} history={history} store={store} />);
} else if (annotation.skipped) {
buttons.push(
<Elem name="skipped-info" key="skipped">
<IconBan color="#d00" /> Was skipped
</Elem>,
);
buttons.push(
<ButtonTooltip key="cancel-skip" title="Cancel skip: []">
<Button
aria-label="cancel-skip"
disabled={disabled}
look="primary"
onClick={async () => {
const selected = store.annotationStore?.selected;

selected?.submissionInProgress();
await store.commentStore.commentFormSubmit();
store.unskipTask();
}}
>
Cancel skip
</Button>
</ButtonTooltip>,
);
buttons.push(<UnskipButton disabled={disabled} store={store} />);
} else {
if (store.hasInterface("skip")) {
buttons.push(
<ButtonTooltip key="skip" title="Cancel (skip) task: [ Ctrl+Space ]">
<Button
aria-label="skip-task"
disabled={disabled}
onClick={async (e) => {
if (store.hasInterface("comments:skip") ?? true) {
buttonHandler(e, () => store.skipTask({}), "Please enter a comment before skipping");
} else {
const selected = store.annotationStore?.selected;
const onSkipWithComment = (e: React.MouseEvent, action: () => any) => {
buttonHandler(e, action, "Please enter a comment before skipping");
};

selected?.submissionInProgress();
await store.commentStore.commentFormSubmit();
store.skipTask({});
}
}}
>
Skip
</Button>
</ButtonTooltip>,
);
buttons.push(<SkipButton disabled={disabled} store={store} onSkipWithComment={onSkipWithComment} />);
}

const isDisabled = disabled || submitDisabled;
const look = isDisabled ? "disabled" : "primary";

const useExitOption = !isDisabled && isNotQuickView;

const SubmitOption = ({ isUpdate, onClickMethod }) => {
const SubmitOption = ({ isUpdate, onClickMethod }: { isUpdate: boolean; onClickMethod: () => any }) => {
return (
<Button
name="submit-option"
look="secondary"
look="primary"
onClick={async (event) => {
event.preventDefault();

Expand Down Expand Up @@ -222,15 +186,15 @@ export const Controls = controlsInjector(
look={look}
mod={{ has_icon: useExitOption, disabled: isDisabled }}
onClick={async (event) => {
if (event.target.classList.contains(dropdownTrigger)) return;
if ((event.target as HTMLButtonElement).classList.contains(dropdownTrigger)) return;
const selected = store.annotationStore?.selected;

selected?.submissionInProgress();
await store.commentStore.commentFormSubmit();
store.submitAnnotation();
}}
icon={
useExitOption && (
useExitOption ? (
<Dropdown.Trigger
alignment="top-right"
content={<SubmitOption onClickMethod={store.submitAnnotation} isUpdate={false} />}
Expand All @@ -239,7 +203,7 @@ export const Controls = controlsInjector(
<LsChevron />
</div>
</Dropdown.Trigger>
)
) : undefined
}
>
Submit
Expand All @@ -250,7 +214,7 @@ export const Controls = controlsInjector(
}

if ((userGenerate && sentUserGenerate) || (!userGenerate && store.hasInterface("update"))) {
const isUpdate = isFF(FF_REVIEWER_FLOW) || sentUserGenerate || versions.result;
const isUpdate = Boolean(isFF(FF_REVIEWER_FLOW) || sentUserGenerate || versions.result);
// no changes were made over previously submitted version — no drafts, no pending changes
const noChanges = isFF(FF_REVIEWER_FLOW) && !history.canUndo && !annotation.draftId;
const isUpdateDisabled = isDisabled || noChanges;
Expand All @@ -263,15 +227,15 @@ export const Controls = controlsInjector(
look={look}
mod={{ has_icon: useExitOption, disabled: isUpdateDisabled }}
onClick={async (event) => {
if (event.target.classList.contains(dropdownTrigger)) return;
if ((event.target as HTMLButtonElement).classList.contains(dropdownTrigger)) return;
const selected = store.annotationStore?.selected;

selected?.submissionInProgress();
await store.commentStore.commentFormSubmit();
store.updateAnnotation();
}}
icon={
useExitOption && (
useExitOption ? (
<Dropdown.Trigger
alignment="top-right"
content={<SubmitOption onClickMethod={store.updateAnnotation} isUpdate={isUpdate} />}
Expand All @@ -280,7 +244,7 @@ export const Controls = controlsInjector(
<LsChevron />
</div>
</Dropdown.Trigger>
)
) : undefined
}
>
{isUpdate ? "Update" : "Submit"}
Expand Down
Loading
Loading