-
Notifications
You must be signed in to change notification settings - Fork 563
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5099 from voxel51/feat/operator-exec-ctx-menu
Feat/operator exec ctx menu
- Loading branch information
Showing
7 changed files
with
450 additions
and
4 deletions.
There are no files selected for viewing
161 changes: 161 additions & 0 deletions
161
app/packages/core/src/plugins/SchemaIO/components/OperatorExecutionButtonView.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import React from "react"; | ||
import { MuiIconFont } from "@fiftyone/components"; | ||
import { OperatorExecutionButton } from "@fiftyone/operators"; | ||
import { usePanelId } from "@fiftyone/spaces"; | ||
import { isNullish } from "@fiftyone/utilities"; | ||
import { Box, ButtonProps, Typography } from "@mui/material"; | ||
import { getColorByCode, getComponentProps, getDisabledColors } from "../utils"; | ||
import { ViewPropsType } from "../utils/types"; | ||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; | ||
import TooltipProvider from "./TooltipProvider"; | ||
|
||
export default function OperatorExecutionButtonView(props: ViewPropsType) { | ||
const { schema, path, onClick } = props; | ||
const { view = {} } = schema; | ||
const { | ||
description, | ||
icon, | ||
icon_position = "right", | ||
label, | ||
operator, | ||
params = {}, | ||
prompt, | ||
title, | ||
disabled = false, | ||
} = view; | ||
const panelId = usePanelId(); | ||
const variant = getVariant(props); | ||
const computedParams = { ...params, path, panel_id: panelId }; | ||
|
||
const Icon = icon ? ( | ||
<MuiIconFont | ||
name={icon} | ||
{...getComponentProps(props, "icon", getIconProps(props))} | ||
/> | ||
) : ( | ||
<ExpandMoreIcon /> | ||
); | ||
|
||
return ( | ||
<Box {...getComponentProps(props, "container")}> | ||
<TooltipProvider title={title} {...getComponentProps(props, "tooltip")}> | ||
<OperatorExecutionButton | ||
operatorUri={operator} | ||
executionParams={computedParams} | ||
variant={variant} | ||
onClick={(e) => onClick?.(e, computedParams, props)} | ||
color="primary" | ||
disabled={disabled} | ||
startIcon={icon_position === "left" ? Icon : undefined} | ||
endIcon={icon_position === "right" ? Icon : undefined} | ||
title={description} | ||
{...getComponentProps(props, "button", getButtonProps(props))} | ||
> | ||
<Typography>{label}</Typography> | ||
</OperatorExecutionButton> | ||
</TooltipProvider> | ||
</Box> | ||
); | ||
} | ||
|
||
function getButtonProps(props: ViewPropsType): ButtonProps { | ||
const { label, variant, color, disabled } = props.schema.view; | ||
const baseProps: ButtonProps = getCommonProps(props); | ||
if (isNullish(label)) { | ||
baseProps.sx["& .MuiButton-startIcon"] = { mr: 0, ml: 0 }; | ||
baseProps.sx.minWidth = "auto"; | ||
baseProps.sx.p = "6px"; | ||
} | ||
if (variant === "round") { | ||
baseProps.sx.borderRadius = "1rem"; | ||
baseProps.sx.p = "3.5px 10.5px"; | ||
} | ||
if (variant === "square") { | ||
baseProps.sx.borderRadius = "3px 3px 0 0"; | ||
baseProps.sx.backgroundColor = (theme) => theme.palette.background.field; | ||
baseProps.sx.borderBottom = "1px solid"; | ||
baseProps.sx.paddingBottom = "5px"; | ||
baseProps.sx.borderColor = (theme) => theme.palette.primary.main; | ||
} | ||
if (variant === "outlined") { | ||
baseProps.sx.p = "5px"; | ||
} | ||
if ((variant === "square" || variant === "outlined") && isNullish(color)) { | ||
const borderColor = | ||
"rgba(var(--fo-palette-common-onBackgroundChannel) / 0.23)"; | ||
baseProps.sx.borderColor = borderColor; | ||
baseProps.sx.borderBottomColor = borderColor; | ||
} | ||
if (isNullish(variant)) { | ||
baseProps.variant = "contained"; | ||
baseProps.color = "tertiary"; | ||
baseProps.sx["&:hover"] = { | ||
backgroundColor: (theme) => theme.palette.tertiary.hover, | ||
}; | ||
} | ||
|
||
if (disabled) { | ||
const [bgColor, textColor] = getDisabledColors(); | ||
baseProps.sx["&.Mui-disabled"] = { | ||
backgroundColor: variant === "outlined" ? "inherit" : bgColor, | ||
color: textColor, | ||
}; | ||
if (["square", "outlined"].includes(variant)) { | ||
baseProps.sx["&.Mui-disabled"].backgroundColor = (theme) => | ||
theme.palette.background.field; | ||
} | ||
} | ||
|
||
return baseProps; | ||
} | ||
|
||
function getIconProps(props: ViewPropsType): ButtonProps { | ||
return getCommonProps(props); | ||
} | ||
|
||
function getCommonProps(props: ViewPropsType): ButtonProps { | ||
const color = getColor(props); | ||
const disabled = props.schema.view?.disabled || false; | ||
|
||
return { | ||
sx: { | ||
color, | ||
fontSize: "1rem", | ||
fontWeight: "bold", | ||
borderColor: color, | ||
"&:hover": { | ||
borderColor: color, | ||
}, | ||
...(disabled | ||
? { | ||
opacity: 0.5, | ||
} | ||
: {}), | ||
}, | ||
}; | ||
} | ||
|
||
function getColor(props: ViewPropsType) { | ||
const { | ||
schema: { view = {} }, | ||
} = props; | ||
const { color } = view; | ||
if (color) { | ||
return getColorByCode(color); | ||
} | ||
const variant = getVariant(props); | ||
return (theme) => { | ||
return variant === "contained" | ||
? theme.palette.common.white | ||
: theme.palette.secondary.main; | ||
}; | ||
} | ||
|
||
const defaultVariant = ["contained", "outlined"]; | ||
|
||
function getVariant(pros: ViewPropsType) { | ||
const variant = pros.schema.view.variant; | ||
if (defaultVariant.includes(variant)) return variant; | ||
if (variant === "round") return "contained"; | ||
return "contained"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
app/packages/operators/src/components/OperatorExecutionButton/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { Button } from "@mui/material"; | ||
import { OperatorExecutionTrigger } from "../OperatorExecutionTrigger"; | ||
import React from "react"; | ||
import { | ||
ExecutionCallback, | ||
ExecutionErrorCallback, | ||
} from "../../types-internal"; | ||
import { OperatorExecutionOption } from "../../state"; | ||
|
||
/** | ||
* Button which acts as a trigger for opening an `OperatorExecutionMenu`. | ||
* | ||
* @param operatorUri Operator URI | ||
* @param onSuccess Callback for successful operator execution | ||
* @param onError Callback for operator execution error | ||
* @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 | ||
*/ | ||
export const OperatorExecutionButton = ({ | ||
operatorUri, | ||
onSuccess, | ||
onError, | ||
executionParams, | ||
onOptionSelected, | ||
disabled, | ||
children, | ||
...props | ||
}: { | ||
operatorUri: string; | ||
onSuccess?: ExecutionCallback; | ||
onError?: ExecutionErrorCallback; | ||
executionParams?: object; | ||
onOptionSelected?: (option: OperatorExecutionOption) => void; | ||
disabled?: boolean; | ||
children: React.ReactNode; | ||
}) => { | ||
return ( | ||
<OperatorExecutionTrigger | ||
operatorUri={operatorUri} | ||
onSuccess={onSuccess} | ||
onError={onError} | ||
executionParams={executionParams} | ||
onOptionSelected={onOptionSelected} | ||
disabled={disabled} | ||
> | ||
<Button disabled={disabled} {...props}> | ||
{children} | ||
</Button> | ||
</OperatorExecutionTrigger> | ||
); | ||
}; | ||
|
||
export default OperatorExecutionButton; |
51 changes: 51 additions & 0 deletions
51
app/packages/operators/src/components/OperatorExecutionMenu/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { Menu, MenuItem, Stack, Typography } from "@mui/material"; | ||
import React from "react"; | ||
import { OperatorExecutionOption } from "../../state"; | ||
|
||
/** | ||
* Component which provides a context menu for executing an operator using a | ||
* specified execution target. | ||
* | ||
* @param anchor Element to use as context menu anchor | ||
* @param open If true, context menu will be visible | ||
* @param onClose Callback for context menu close events | ||
* @param executionOptions List of operator execution options | ||
* @param onClick Callback for an option being clicked | ||
*/ | ||
export const OperatorExecutionMenu = ({ | ||
anchor, | ||
open, | ||
onClose, | ||
executionOptions, | ||
onOptionClick, | ||
}: { | ||
anchor?: Element | null; | ||
open: boolean; | ||
onClose: () => void; | ||
executionOptions: OperatorExecutionOption[]; | ||
onOptionClick?: (option: OperatorExecutionOption) => void; | ||
}) => { | ||
return ( | ||
<Menu anchorEl={anchor} open={open} onClose={onClose}> | ||
{executionOptions.map((target) => ( | ||
<MenuItem | ||
key={target.id} | ||
onClick={() => { | ||
onClose?.(); | ||
onOptionClick?.(target); | ||
target.onClick(); | ||
}} | ||
> | ||
<Stack direction="column" spacing={1}> | ||
<Typography fontWeight="bold"> | ||
{target.choiceLabel ?? target.label} | ||
</Typography> | ||
<Typography color="secondary">{target.description}</Typography> | ||
</Stack> | ||
</MenuItem> | ||
))} | ||
</Menu> | ||
); | ||
}; | ||
|
||
export default OperatorExecutionMenu; |
118 changes: 118 additions & 0 deletions
118
app/packages/operators/src/components/OperatorExecutionTrigger/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import React, { useCallback, useMemo, 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"; | ||
|
||
/** | ||
* Component which acts as a trigger for opening an `OperatorExecutionMenu`. | ||
* | ||
* This component is meant to act as a wrapper around the interactable | ||
* component. For example, if you wanted to add operator execution to a button, | ||
* | ||
* ```tsx | ||
* <OperatorExecutionTrigger {...props}> | ||
* <Button>Execute operator</Button> | ||
* </OperatorExecutionTrigger> | ||
* ``` | ||
* | ||
* | ||
* This component registers a click handler which will manage the | ||
* `OperatorExecutionMenu` lifecycle. | ||
* | ||
* @param operatorUri Operator URI | ||
* @param onClick Callback for click events | ||
* @param onSuccess Callback for successful operator execution | ||
* @param onError Callback for operator execution error | ||
* @param executionParams Parameters to provide to the operator's execute call | ||
* @param executorOptions Operator executor options | ||
* @param onOptionSelected Callback for execution option selection | ||
* @param disabled If true, context menu will never open | ||
*/ | ||
export const OperatorExecutionTrigger = ({ | ||
operatorUri, | ||
onClick, | ||
onSuccess, | ||
onError, | ||
executionParams, | ||
executorOptions, | ||
onOptionSelected, | ||
disabled, | ||
children, | ||
...props | ||
}: { | ||
operatorUri: string; | ||
children: React.ReactNode; | ||
onClick?: () => void; | ||
onSuccess?: ExecutionCallback; | ||
onError?: ExecutionErrorCallback; | ||
executionParams?: object; | ||
executorOptions?: OperatorExecutorOptions; | ||
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) { | ||
setIsMenuOpen(false); | ||
} else { | ||
onClick?.(); | ||
setIsMenuOpen(true); | ||
} | ||
}, [setIsMenuOpen, onClick, disabled]); | ||
|
||
return ( | ||
<> | ||
<Box ref={containerRef} onClick={clickHandler} {...props}> | ||
{children} | ||
</Box> | ||
|
||
<OperatorExecutionMenu | ||
anchor={containerRef.current} | ||
open={isMenuOpen && !disabled} | ||
onClose={() => setIsMenuOpen(false)} | ||
onOptionClick={onOptionSelected} | ||
executionOptions={executionOptions} | ||
/> | ||
</> | ||
); | ||
}; | ||
|
||
export default OperatorExecutionTrigger; |
Oops, something went wrong.