Skip to content

Commit

Permalink
Merge pull request #5348 from voxel51/prompt-on-cancel
Browse files Browse the repository at this point in the history
Add on_cancel callback for operator prompt
  • Loading branch information
ritch authored Jan 7, 2025
2 parents 55709fb + f6dfbe0 commit c5d3bf5
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import TooltipProvider from "./TooltipProvider";
import { OperatorExecutionOption } from "@fiftyone/operators/src/state";
import {
ExecutionCallback,
ExecutionCancelCallback,
ExecutionErrorCallback,
} from "@fiftyone/operators/src/types-internal";
import { OperatorResult } from "@fiftyone/operators/src/operators";
Expand All @@ -30,6 +31,8 @@ export default function OperatorExecutionButtonView(props: ViewPropsType) {
on_error,
on_success,
on_option_selected,
on_cancel,
prompt,
} = view;
const panelId = usePanelId();
const variant = getVariant(props);
Expand Down Expand Up @@ -75,6 +78,13 @@ export default function OperatorExecutionButtonView(props: ViewPropsType) {
});
}
};
const handleOnCancel: ExecutionCancelCallback = () => {
if (on_cancel) {
triggerEvent(panelId, {
operator: on_cancel,
});
}
};
const handleOnOptionSelected = (option: OperatorExecutionOption) => {
if (on_option_selected) {
triggerEvent(panelId, {
Expand All @@ -86,6 +96,13 @@ export default function OperatorExecutionButtonView(props: ViewPropsType) {
}
};

const iconProps = prompt
? {}
: {
startIcon: icon_position === "left" ? Icon : undefined,
endIcon: icon_position === "right" ? Icon : undefined,
};

return (
<Box {...getComponentProps(props, "container")}>
<TooltipProvider title={title} {...getComponentProps(props, "tooltip")}>
Expand All @@ -94,11 +111,12 @@ export default function OperatorExecutionButtonView(props: ViewPropsType) {
onSuccess={handleOnSuccess}
onError={handleOnError}
onOptionSelected={handleOnOptionSelected}
prompt={prompt}
onCancel={handleOnCancel}
executionParams={computedParams}
variant={variant}
disabled={disabled}
startIcon={icon_position === "left" ? Icon : undefined}
endIcon={icon_position === "right" ? Icon : undefined}
{...iconProps}
title={description}
{...getComponentProps(props, "button", getButtonProps(props))}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default function TooltipProvider(props: TooltipProps) {
if (!title) return children;
return (
<Tooltip title={title} {...tooltipProps}>
<Box>{children}</Box>
<Box component="span">{children}</Box>
</Tooltip>
);
}
12 changes: 11 additions & 1 deletion app/packages/operators/src/built-in-operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,7 @@ class PromptUserForOperation extends Operator {
inputs.obj("params", { label: "Params" });
inputs.str("on_success", { label: "On success" });
inputs.str("on_error", { label: "On error" });
inputs.str("on_cancel", { label: "On cancel" });
inputs.bool("skip_prompt", { label: "Skip prompt", default: false });
return new types.Property(inputs);
}
Expand All @@ -1045,7 +1046,8 @@ class PromptUserForOperation extends Operator {
return { triggerEvent };
}
async execute(ctx: ExecutionContext): Promise<void> {
const { params, operator_uri, on_success, on_error } = ctx.params;
const { params, operator_uri, on_success, on_error, on_cancel } =
ctx.params;
const { triggerEvent } = ctx.hooks;
const panelId = ctx.getCurrentPanelId();
const shouldPrompt = !ctx.params.skip_prompt;
Expand All @@ -1054,6 +1056,14 @@ class PromptUserForOperation extends Operator {
operator: operator_uri,
params,
prompt: shouldPrompt,
onCancel: () => {
if (on_cancel) {
triggerEvent(panelId, {
operator: on_cancel,
params: { operator_uri },
});
}
},
callback: (result: OperatorResult, opts: { ctx: ExecutionContext }) => {
const ctx = opts.ctx;
if (result.error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import TooltipProvider from "@fiftyone/core/src/plugins/SchemaIO/components/TooltipProvider";
import { Button } from "@mui/material";
import { OperatorExecutionTrigger } from "../OperatorExecutionTrigger";
import React from "react";
import React, { useCallback, useMemo } from "react";
import {
OperatorExecutionOption,
useOperatorExecutionOptions,
useOperatorExecutor,
usePromptOperatorInput,
} from "../../state";
import {
ExecutionCallback,
ExecutionErrorCallback,
OperatorExecutorOptions,
} from "../../types-internal";
import { OperatorExecutionOption } from "../../state";
import { OperatorExecutionTrigger } from "../OperatorExecutionTrigger";

/**
* Button which acts as a trigger for opening an `OperatorExecutionMenu`.
Expand All @@ -16,35 +23,105 @@ import { OperatorExecutionOption } from "../../state";
* @param executionParams Parameters to provide to the operator's execute call
* @param onOptionSelected Callback for execution option selection
* @param disabled If true, disables the button and context menu
* @param executorOptions Operator executor options
*/
export const OperatorExecutionButton = ({
operatorUri,
onSuccess,
onError,
onClick,
onCancel,
executionParams,
onOptionSelected,
prompt,
disabled,
children,
executorOptions,
...props
}: {
operatorUri: string;
onSuccess?: ExecutionCallback;
onError?: ExecutionErrorCallback;
onClick?: () => void;
onCancel?: () => void;
prompt?: boolean;
executionParams?: object;
onOptionSelected?: (option: OperatorExecutionOption) => void;
disabled?: boolean;
children: React.ReactNode;
executorOptions?: OperatorExecutorOptions;
}) => {
// Pass onSuccess and onError through to the operator executor.
// These will be invoked on operator completion.
const operatorHandlers = useMemo(() => {
return { onSuccess, onError };
}, [onSuccess, onError]);
const operator = useOperatorExecutor(operatorUri, operatorHandlers);
const promptForOperator = usePromptOperatorInput();

// This callback will be invoked when an execution target option is clicked
const onExecute = useCallback(
(options?: OperatorExecutorOptions) => {
const resolvedOptions = {
...executorOptions,
...options,
};

if (prompt) {
promptForOperator(operatorUri, executionParams, {
callback: (result, opts) => {
if (result?.error) {
onError?.(result, opts);
} else {
onSuccess?.(result, opts);
}
},
onCancel,
});
} else {
return operator.execute(executionParams ?? {}, resolvedOptions);
}
},
[executorOptions, operator, executionParams]
);

const { executionOptions, warningMessage, showWarning, isLoading } =
useOperatorExecutionOptions({
operatorUri,
onExecute,
});

if (isLoading) return null;

if (showWarning) {
return (
<TooltipProvider title={warningMessage}>
<Button disabled={true} variant={props.variant}>
{children}
</Button>
</TooltipProvider>
);
}

if (prompt) {
return (
<Button disabled={disabled} {...props} onClick={onExecute}>
{children}
</Button>
);
}

return (
<OperatorExecutionTrigger
operatorUri={operatorUri}
onClick={onClick}
onSuccess={onSuccess}
onError={onError}
onCancel={onCancel}
prompt={prompt}
executionParams={executionParams}
onOptionSelected={onOptionSelected}
executionOptions={executionOptions}
disabled={disabled}
>
<Button disabled={disabled} {...props}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import React, { useCallback, useMemo, useRef, useState } from "react";
import React, { useCallback, useRef, useState } from "react";
import { Box } from "@mui/material";
import { OperatorExecutionMenu } from "../OperatorExecutionMenu";
import {
ExecutionCallback,
ExecutionErrorCallback,
OperatorExecutorOptions,
} from "../../types-internal";
import {
OperatorExecutionOption,
useOperatorExecutionOptions,
useOperatorExecutor,
} from "../../state";
import { OperatorExecutionOption } from "../../state";

/**
* Component which acts as a trigger for opening an `OperatorExecutionMenu`.
Expand Down Expand Up @@ -38,12 +34,8 @@ import {
* @param disabled If true, context menu will never open
*/
export const OperatorExecutionTrigger = ({
operatorUri,
onClick,
onSuccess,
onError,
executionParams,
executorOptions,
executionOptions,
onOptionSelected,
disabled,
children,
Expand All @@ -54,40 +46,17 @@ export const OperatorExecutionTrigger = ({
onClick?: () => void;
onSuccess?: ExecutionCallback;
onError?: ExecutionErrorCallback;
prompt?: boolean;
executionParams?: object;
executorOptions?: OperatorExecutorOptions;
executionOptions?: OperatorExecutionOption[];
onOptionSelected?: (option: OperatorExecutionOption) => void;
disabled?: boolean;
}) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
// Anchor to use for context menu
const containerRef = useRef(null);

// Pass onSuccess and onError through to the operator executor.
// These will be invoked on operator completion.
const operatorHandlers = useMemo(() => {
return { onSuccess, onError };
}, [onSuccess, onError]);
const operator = useOperatorExecutor(operatorUri, operatorHandlers);

// This callback will be invoked when an execution target option is clicked
const onExecute = useCallback(
(options?: OperatorExecutorOptions) => {
const resolvedOptions = {
...executorOptions,
...options,
};

return operator.execute(executionParams ?? {}, resolvedOptions);
},
[executorOptions, operator, executionParams]
);

const { executionOptions } = useOperatorExecutionOptions({
operatorUri,
onExecute,
});

// Click handler controls the state of the context menu.
const clickHandler = useCallback(() => {
if (disabled) {
Expand Down
15 changes: 14 additions & 1 deletion app/packages/operators/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,10 @@ export const useOperatorExecutionOptions = ({
onExecute: (opts: OperatorExecutorOptions) => void;
}): {
executionOptions: OperatorExecutionOption[];
hasOptions: boolean;
warningMessage: React.ReactNode;
showWarning: boolean;
isLoading: boolean;
} => {
const ctx = useExecutionContext(operatorUri);
const { isRemote } = getLocalOrRemoteOperator(operatorUri);
Expand All @@ -434,6 +438,10 @@ export const useOperatorExecutionOptions = ({

return {
executionOptions: submitOptions.options,
hasOptions: submitOptions.hasOptions,
warningMessage: submitOptions.warningMessage,
showWarning: submitOptions.showWarning,
isLoading: execDetails.isLoading,
};
};

Expand Down Expand Up @@ -579,6 +587,11 @@ export const useOperatorPrompt = () => {
},
[operator, promptingOperator, cachedResolvedInput]
);
const onCancel = promptingOperator.options?.onCancel;
const cancel = () => {
if (onCancel) onCancel();
close();
};
const close = () => {
setPromptingOperator(null);
setInputFields(null);
Expand Down Expand Up @@ -656,7 +669,7 @@ export const useOperatorPrompt = () => {
isExecuting,
hasResultOrError,
close,
cancel: close,
cancel,
validationErrors,
validate,
validateThrottled,
Expand Down
1 change: 1 addition & 0 deletions app/packages/operators/src/types-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type ExecutionErrorCallback = (
error: OperatorResult,
options: ExecutionCallbackOptions
) => void;
export type ExecutionCancelCallback = () => void;

export type OperatorExecutorOptions = {
delegationTarget?: string;
Expand Down
8 changes: 6 additions & 2 deletions app/packages/operators/src/usePanelEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type HandlerOptions = {
prompt?: boolean;
panelId: string;
callback?: ExecutionCallback;
onCancel?: () => void;
currentPanelState?: any; // most current panel state
};

Expand All @@ -20,7 +21,7 @@ export default function usePanelEvent() {
const { increment, decrement } = useActivePanelEventsCount("");
return usePanelStateByIdCallback((panelId, panelState, args) => {
const options = args[0] as HandlerOptions;
const { params, operator, prompt, currentPanelState } = options;
const { params, operator, prompt, currentPanelState, onCancel } = options;

if (!operator) {
notify({
Expand Down Expand Up @@ -49,7 +50,10 @@ export default function usePanelEvent() {
};

if (prompt) {
promptForOperator(operator, actualParams, { callback: eventCallback });
promptForOperator(operator, actualParams, {
callback: eventCallback,
onCancel,
});
} else {
increment(panelId);
executeOperator(operator, actualParams, { callback: eventCallback });
Expand Down
Loading

0 comments on commit c5d3bf5

Please sign in to comment.