From d1260b7bdf5775d0993ba5bb5be8eb0f938f463b Mon Sep 17 00:00:00 2001 From: Br2850 Date: Sat, 19 Oct 2024 21:43:32 -0600 Subject: [PATCH] cherry-pick ModalView and relevant changes from tag-modal branch --- .../src/components/ModalBase/DisplayTags.tsx | 80 ++++++ .../src/components/ModalBase/ModalBase.tsx | 253 ++++++++++++++++++ .../src/components/ModalBase/index.ts | 1 + .../src/components/MuiIconFont/index.tsx | 28 +- .../plugins/SchemaIO/components/ModalView.tsx | 30 +++ .../SchemaIO/components/PillBadgeView.tsx | 1 - .../src/plugins/SchemaIO/components/index.ts | 6 +- app/packages/operators/src/types.ts | 45 ++-- fiftyone/operators/types.py | 110 ++++---- 9 files changed, 475 insertions(+), 79 deletions(-) create mode 100644 app/packages/components/src/components/ModalBase/DisplayTags.tsx create mode 100644 app/packages/components/src/components/ModalBase/ModalBase.tsx create mode 100644 app/packages/components/src/components/ModalBase/index.ts create mode 100644 app/packages/core/src/plugins/SchemaIO/components/ModalView.tsx diff --git a/app/packages/components/src/components/ModalBase/DisplayTags.tsx b/app/packages/components/src/components/ModalBase/DisplayTags.tsx new file mode 100644 index 0000000000..79c6132cee --- /dev/null +++ b/app/packages/components/src/components/ModalBase/DisplayTags.tsx @@ -0,0 +1,80 @@ +import React, { useState } from "react"; +import { Box, Chip, TextField, IconButton } from "@mui/material"; +import AddIcon from "@mui/icons-material/Add"; + +// Define the props interface for the DisplayTags component +interface DisplayTagsProps { + saveTags: (tags: string[]) => void; // saveTags is a function that accepts an array of strings and returns void +} + +const DisplayTags: React.FC = ({ saveTags }) => { + const [chips, setChips] = useState([]); // chips is an array of strings + const [inputValue, setInputValue] = useState(""); // inputValue is a string + + const handleAddChip = () => { + if (inputValue.trim() !== "") { + const updatedChips = [...chips, inputValue]; + setChips(updatedChips); + setInputValue(""); + saveTags(updatedChips); // Call the saveTags function to save the new list of chips + } + }; + + const handleDeleteChip = (chipToDelete: string) => { + const updatedChips = chips.filter((chip) => chip !== chipToDelete); + setChips(updatedChips); + saveTags(updatedChips); // Call the saveTags function to save the updated list of chips + }; + + return ( + + + setInputValue(e.target.value)} + fullWidth // Make TextField take up the remaining width + /> + + + + + + + {chips.map((chip, index) => ( + handleDeleteChip(chip)} + /> + ))} + + + ); +}; + +export default DisplayTags; diff --git a/app/packages/components/src/components/ModalBase/ModalBase.tsx b/app/packages/components/src/components/ModalBase/ModalBase.tsx new file mode 100644 index 0000000000..30321d3bc8 --- /dev/null +++ b/app/packages/components/src/components/ModalBase/ModalBase.tsx @@ -0,0 +1,253 @@ +import React, { useCallback, useState } from "react"; +import ButtonView from "@fiftyone/core/src/plugins/SchemaIO/components/ButtonView"; +import { Box, Modal, Typography } from "@mui/material"; +import DisplayTags from "./DisplayTags"; +import { MuiIconFont } from "../index"; + +interface ModalBaseProps { + modal: { + icon?: string; + iconVariant?: "outlined" | "filled" | "rounded" | "sharp" | undefined; + title: string; + subtitle: string; + body: string; + textAlign?: string | { [key: string]: string }; + }; + primaryButton?: { + href?: any; + prompt?: any; + params?: any; + operator?: any; + align?: string; + width?: string; + onClick?: any; + primaryText: string; + primaryColor: string; + }; + secondaryButton?: { + href?: any; + prompt?: any; + params?: any; + operator?: any; + align?: string; + width?: string; + onClick?: any; + secondaryText: string; + secondaryColor: string; + }; + functionality?: string; + primaryCallback?: () => void; + secondaryCallback?: () => void; + props: any; +} + +interface ModalButtonView { + variant: string; + label: string; + icon?: string; + iconPosition?: string; + componentsProps: any; +} + +const ModalBase: React.FC = ({ + modal, + primaryButton, + secondaryButton, + primaryCallback, + secondaryCallback, + functionality = "none", + props, +}) => { + const { title, subtitle, body } = modal; + + const defaultAlign = "left"; + + let titleAlign = defaultAlign; + let subtitleAlign = defaultAlign; + let bodyAlign = defaultAlign; + + const [open, setOpen] = useState(false); + const handleOpen = () => setOpen(true); + const handleClose = () => { + if (!secondaryCallback) { + setOpen(false); + } + }; + + if (typeof modal?.textAlign === "string") { + titleAlign = subtitleAlign = bodyAlign = modal.textAlign; + } else { + titleAlign = modal?.textAlign?.title ?? defaultAlign; + subtitleAlign = modal?.textAlign?.subtitle ?? defaultAlign; + bodyAlign = modal?.textAlign?.body ?? defaultAlign; + } + + const modalStyle = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: 600, + bgcolor: "background.paper", + border: "2px solid #000", + boxShadow: 24, + p: 6, // Padding for inner spacing + display: "flex", + flexDirection: "column", // Stack items vertically + justifyContent: "center", // Vertically center the content + }; + + const modalButtonView: ModalButtonView = { + variant: props?.variant || "outlined", + label: props?.label || "Open Modal", + componentsProps: { + button: { + sx: { + height: props?.height || "100%", + width: props?.width || "100%", + padding: 1, + }, + }, + }, + }; + + if (Object.keys(props).includes("icon")) { + modalButtonView["icon"] = props["icon"]; + modalButtonView["iconPosition"] = props?.iconPosition || "left"; + } + + const [primaryButtonView, setPrimaryButtonView] = useState({ + variant: "contained", + color: primaryButton?.primaryColor, + label: primaryButton?.primaryText, + onClick: primaryButton?.onClick, + operator: primaryCallback || primaryButton?.operator, + params: primaryButton?.params, + href: primaryButton?.href, + prompt: primaryButton?.prompt, + componentsProps: { + button: { + sx: { + width: primaryButton?.width || "100%", + justifyContent: primaryButton?.align || "center", + ...primaryButton, + }, + }, + }, + }); + + const [secondaryButtonView, setSecondaryButtonView] = useState({ + variant: "outlined", + color: secondaryButton?.secondaryColor, + label: secondaryButton?.secondaryText, + onClick: secondaryButton?.onClick, + operator: secondaryCallback || secondaryButton?.operator, + params: secondaryButton?.params, + href: secondaryButton?.href, + prompt: secondaryButton?.prompt, + componentsProps: { + button: { + sx: { + width: primaryButton?.width || "100%", + justifyContent: primaryButton?.align || "center", + ...secondaryButton, + }, + }, + }, + }); + + // State options for functionality based on user input + + { + /* TAGGING FUNCTIONALITY */ + } + const handleSaveTags = useCallback((tags: string[]) => { + setPrimaryButtonView((prevButtonView) => ({ + ...prevButtonView, + params: { ...prevButtonView.params, tags }, // Add tags to existing params + })); + }, []); + + return ( + <> + + + + + + + {modal?.icon && ( + + {modal.icon} + + )}{" "} + {title} + + + {subtitle} + + + {body} + + {functionality === "tagging" && ( + + )} + + {secondaryButton && ( + + + + )} + {primaryButton && ( + + + + )} + + + + + ); +}; +export default ModalBase; diff --git a/app/packages/components/src/components/ModalBase/index.ts b/app/packages/components/src/components/ModalBase/index.ts new file mode 100644 index 0000000000..71cee8fb7d --- /dev/null +++ b/app/packages/components/src/components/ModalBase/index.ts @@ -0,0 +1 @@ +export { default as ModalBase } from "./ModalBase"; diff --git a/app/packages/components/src/components/MuiIconFont/index.tsx b/app/packages/components/src/components/MuiIconFont/index.tsx index 50cbcf175f..b21f0f9f24 100644 --- a/app/packages/components/src/components/MuiIconFont/index.tsx +++ b/app/packages/components/src/components/MuiIconFont/index.tsx @@ -4,10 +4,34 @@ import React from "react"; // Available Icons: https://github.com/marella/material-icons?tab=readme-ov-file#available-icons export default function MuiIconFont(props: MuiIconFontProps) { - const { name, ...iconProps } = props; - return {name}; + const { name, variant, ...iconProps } = props; + + let icon = {name}; + + if (variant === "outlined") { + icon = ( + + {name} + + ); + } else if (variant === "rounded") { + icon = ( + + {name} + + ); + } else if (variant === "sharp") { + icon = ( + + {name} + + ); + } + + return icon; } type MuiIconFontProps = IconProps & { name: string; + variant?: "filled" | "outlined" | "rounded" | "sharp"; }; diff --git a/app/packages/core/src/plugins/SchemaIO/components/ModalView.tsx b/app/packages/core/src/plugins/SchemaIO/components/ModalView.tsx new file mode 100644 index 0000000000..7588db49c0 --- /dev/null +++ b/app/packages/core/src/plugins/SchemaIO/components/ModalView.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import ModalBase from "@fiftyone/components/src/components/ModalBase/ModalBase"; + +export default function ModalView(props) { + const { schema } = props; + const { view = {} } = schema; + const { + modal, + primaryButton, + secondaryButton, + functionality, + primaryCallback, + secondaryCallback, + ...remainingViewProps + } = view; + + return ( + + ); +} diff --git a/app/packages/core/src/plugins/SchemaIO/components/PillBadgeView.tsx b/app/packages/core/src/plugins/SchemaIO/components/PillBadgeView.tsx index 787fabbd7d..afcb4e5d8e 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/PillBadgeView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/PillBadgeView.tsx @@ -1,5 +1,4 @@ import { Box } from "@mui/material"; -import React from "react"; import { getComponentProps } from "../utils"; import PillBadge from "@fiftyone/components/src/components/PillBadge/PillBadge"; diff --git a/app/packages/core/src/plugins/SchemaIO/components/index.ts b/app/packages/core/src/plugins/SchemaIO/components/index.ts index 50bdcca53c..d8809bd604 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/index.ts +++ b/app/packages/core/src/plugins/SchemaIO/components/index.ts @@ -17,6 +17,7 @@ export { default as FieldWrapper } from "./FieldWrapper"; export { default as FileDrop } from "./FileDrop"; export { default as FileExplorerView } from "./FileExplorerView/FileExplorerView"; export { default as FileView } from "./FileView"; +export { default as FrameLoaderView } from "./FrameLoaderView"; export { default as GridView } from "./GridView"; export { default as Header } from "./Header"; export { default as HeaderView } from "./HeaderView"; @@ -32,9 +33,11 @@ export { default as ListView } from "./ListView"; export { default as LoadingView } from "./LoadingView"; export { default as MapView } from "./MapView"; export { default as MarkdownView } from "./MarkdownView"; +export { default as ModalView } from "./ModalView"; export { default as MediaPlayerView } from "./MediaPlayerView"; export { default as ObjectView } from "./ObjectView"; export { default as OneOfView } from "./OneOfView"; +export { default as PillBadgeView } from "./PillBadgeView"; export { default as PlotlyView } from "./PlotlyView"; export { default as PrimitiveView } from "./PrimitiveView"; export { default as ProgressView } from "./ProgressView"; @@ -45,9 +48,6 @@ export { default as SwitchView } from "./SwitchView"; export { default as TableView } from "./TableView"; export { default as TabsView } from "./TabsView"; export { default as TagsView } from "./TagsView"; -export { default as TextView } from "./TextView"; export { default as TextFieldView } from "./TextFieldView"; export { default as TupleView } from "./TupleView"; export { default as UnsupportedView } from "./UnsupportedView"; -export { default as FrameLoaderView } from "./FrameLoaderView"; -export { default as PillBadgeView } from "./PillBadgeView"; diff --git a/app/packages/operators/src/types.ts b/app/packages/operators/src/types.ts index fa12e4b54f..8a9353aa4b 100644 --- a/app/packages/operators/src/types.ts +++ b/app/packages/operators/src/types.ts @@ -1111,20 +1111,6 @@ export class FieldView extends View { } } -/** - * Operator class for describing a TextView {@link View} for an - * operator type. - */ -export class TextView extends View { - constructor(options: ViewProps) { - super(options); - this.name = "TextView"; - } - static fromJSON(json) { - return new TextView(json); - } -} - /** * Operator class for describing a TextFieldView {@link View} for an * operator type. @@ -1168,6 +1154,34 @@ export class IconButtonView extends Button { } } +/** + * Operator class for describing a PillBadgeView {@link View} for an + * operator type. + */ +export class PillBadgeView extends View { + constructor(options: ViewProps) { + super(options); + this.name = "PillBadgeView"; + } + static fromJSON(json) { + return new PillBadgeView(json); + } +} + +/** + * Operator class for describing a ModalView {@link Button} for an + * operator type. + */ +export class ModalView extends Button { + constructor(options: ViewProps) { + super(options); + this.name = "ModalView"; + } + static fromJSON(json) { + return new ModalView(json); + } +} + /** * Places where you can have your operator placement rendered. */ @@ -1234,9 +1248,10 @@ const VIEWS = { MediaPlayerView, PromptView, FieldView, - TextView, TextFieldView, LazyFieldView, + PillBadgeView, + ModalView, }; export function typeFromJSON({ name, ...rest }): ANY_TYPE { diff --git a/fiftyone/operators/types.py b/fiftyone/operators/types.py index 5725a8f910..a062bdbb70 100644 --- a/fiftyone/operators/types.py +++ b/fiftyone/operators/types.py @@ -1472,24 +1472,11 @@ def __init__(self, **kwargs): class LoadingView(ReadOnlyView): """Displays a loading indicator. - Examples:: - - schema_dots = { - text: "Loading dots only" - } - schema_spinner = { - variant: "spinner", - color: "primary", - size: "medium", - } - loading_dots = types.LoadingView(**schema_dots) - loading_spinner = types.LoadingView(**schema_spinner) - Args: - text (None): a label for the loading indicator - variant (None): the variant of the loading indicator - color (None): the color of the loading indicator - size (None): the size of the loading indicator + text ("Loading"): a label for the loading indicator + variant ("spinner"): the variant of the loading indicator + color ("primary"): the color of the loading indicator + size ("medium"): the size of the loading indicator """ def __init__(self, **kwargs): @@ -1499,36 +1486,11 @@ def __init__(self, **kwargs): class PillBadgeView(ReadOnlyView): """Displays a pill shaped badge. - Examples:: - - schema_options_single_color = { - text: ["Reviewed", "Not Reviewed"], - color: "primary", - variant: "outlined", - show_icon: True - } - - schema_options_multi_color = { - text: [["Not Started", "primary"], ["Reviewed", "success"], ["In Review", "warning"]], - color: "primary", - variant: "outlined", - show_icon: True - } - - schema_single_option= { - text: "Reviewed", - color: "primary", - variant: "outlined", - show_icon: True - } - badge = types.PillBadgeView(**schema_options_single_color) - - Args: - text: a label or set of label options with or without a color for the pill badge - color (None): the color of the pill - variant (None): the variant of the pill - show_icon (None): whether to display indicator icon + text ("Reviewed" | ["Reviewed", "Not Reviewed"] | [["Not Started", "primary"], ["Reviewed", "success"], ["In Review", "warning"]): a label or set of label options with or without a color for the pill badge + color ("primary"): the color of the pill + variant ("outlined"): the variant of the pill + show_icon (False | True): whether to display indicator icon """ def __init__(self, **kwargs): @@ -1892,18 +1854,6 @@ def __init__(self, **kwargs): super().__init__(**kwargs) -class TextView(View): - """Displays a text. - - .. note:: - - Must be used with :class:`String` properties. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - class TextFieldView(View): """Displays a text input. @@ -2373,6 +2323,50 @@ def __init__(self, **kwargs): super().__init__(**kwargs) +class ModalView(Button): + """Represents a button in a :class:`View` that opens up an interactive modal. + + Examples:: + + import fiftyone.operators.types as types + + schema = { + "modal": {"icon": "local_offer", "iconVariant": "outlined", "title": "Modal Title", "subtitle": "Modal Subtitle", "body": "Modal Body", textAlign: {title: "center", subtitle: "left", body: "right"}}, + "primaryButton": {"primaryText": "This is the primary button", "primaryColor": "primary", "params": {"foo": "bar", "multiple": True}}, + "secondaryButton": {"secondaryText": "This is the secondary button", "secondaryColor": "secondary"}, + "primaryCallback": self.do_something(), + "secondaryCallback": self.do_nothing(), + "functionality": "tagging", + } + modal = types.ModalView(**schema, label="This is a modal", variant="outlined", icon="local_offer") + + .. note:: + The primary callback is called when the primary button is clicked and the secondary callback is called when the secondary button is clicked. + Secondary callback defaults to a closure of the modal unless defined. + Buttons of ModalView inherit all functionality of ButtonView. + + inputs = types.Object() + inputs.view("modal_btn", modal) + + Args: + modal: the textual content of the modal + primaryButton (None): the properties of the primary button + secondaryButton (None): the properties of the secondary button + primaryCallback (None): the function to execute when the primary button is clicked + secondaryCallback (None): the function to execute when the secondary button is clicked + functionality (None): the name of the functionality to execute when the primary button is clicked. Available options are 'tagging' + """ + + def __init__(self, **kwargs): + if "primaryCallback" not in kwargs or not callable( + kwargs["primaryCallback"] + ): + raise ValueError( + "The 'primaryCallback' parameter is missing or must be function that is callable." + ) + super().__init__(**kwargs) + + class HStackView(GridView): """Displays properties of an object as a horizontal stack of components.