From 17614fb43751c33de98dc29fa99d9222cd021882 Mon Sep 17 00:00:00 2001 From: Lanny W Date: Tue, 22 Oct 2024 17:23:51 -0500 Subject: [PATCH] Update plugin components (#4927) --- .../src/components/Loading/LoadingSpinner.tsx | 29 ++ .../src/components/ModalBase/DisplayTags.tsx | 88 ++++++ .../src/components/ModalBase/ModalBase.tsx | 270 +++++++++++++++++ .../src/components/ModalBase/index.ts | 1 + .../src/components/MuiIconFont/index.tsx | 30 +- .../src/components/PillBadge/PillBadge.tsx | 175 +++++++++++ .../src/components/PillBadge/index.ts | 1 + .../SchemaIO/components/ButtonView.tsx | 37 ++- .../SchemaIO/components/DropdownView.tsx | 98 +++++- .../plugins/SchemaIO/components/GridView.tsx | 17 +- .../plugins/SchemaIO/components/Header.tsx | 8 +- .../SchemaIO/components/LoadingView.tsx | 9 +- .../plugins/SchemaIO/components/ModalView.tsx | 30 ++ .../SchemaIO/components/PillBadgeView.tsx | 22 ++ .../SchemaIO/components/SliderView.tsx | 280 ++++++++++++++++-- .../plugins/SchemaIO/components/TextView.tsx | 59 ++++ .../src/plugins/SchemaIO/components/index.ts | 5 +- .../core/src/plugins/SchemaIO/utils/style.ts | 4 + app/packages/operators/src/types.ts | 45 +++ fiftyone/operators/types.py | 76 ++++- 20 files changed, 1221 insertions(+), 63 deletions(-) create mode 100644 app/packages/components/src/components/Loading/LoadingSpinner.tsx 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/components/src/components/PillBadge/PillBadge.tsx create mode 100644 app/packages/components/src/components/PillBadge/index.ts create mode 100644 app/packages/core/src/plugins/SchemaIO/components/ModalView.tsx create mode 100644 app/packages/core/src/plugins/SchemaIO/components/PillBadgeView.tsx create mode 100644 app/packages/core/src/plugins/SchemaIO/components/TextView.tsx diff --git a/app/packages/components/src/components/Loading/LoadingSpinner.tsx b/app/packages/components/src/components/Loading/LoadingSpinner.tsx new file mode 100644 index 00000000000..b14e99daf37 --- /dev/null +++ b/app/packages/components/src/components/Loading/LoadingSpinner.tsx @@ -0,0 +1,29 @@ +import React from "react"; + +import { CircularProgress } from "@mui/material"; + +const LoadingSpinner = ({ + color = "base", + size = "medium", +}: { + color?: string; + size?: string; +}) => { + const COLORS: { [key: string]: string } = { + base: "#FFC59B", + primary: "primary", + secondary: "secondary", + error: "error", + warning: "warning", + info: "info", + success: "success", + }; + const SIZES: { [key: string]: string } = { + small: "1rem", + medium: "2rem", + large: "3rem", + }; + return ; +}; + +export default LoadingSpinner; 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 00000000000..935b23e18b6 --- /dev/null +++ b/app/packages/components/src/components/ModalBase/DisplayTags.tsx @@ -0,0 +1,88 @@ +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 handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + event.preventDefault(); + handleAddChip(); + } + }; + + 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)} + onKeyDown={handleKeyPress} + 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 00000000000..ea2857f3366 --- /dev/null +++ b/app/packages/components/src/components/ModalBase/ModalBase.tsx @@ -0,0 +1,270 @@ +import React, { useCallback, useEffect, 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; + disabled?: boolean; + primaryText: string; + primaryColor: string; + }; + secondaryButton?: { + href?: any; + prompt?: any; + params?: any; + operator?: any; + align?: string; + width?: string; + onClick?: any; + disabled?: boolean; + 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, + disabled: primaryButton?.disabled, + 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, + disabled: secondaryButton?.disabled, + componentsProps: { + button: { + sx: { + width: primaryButton?.width || "100%", + justifyContent: primaryButton?.align || "center", + ...secondaryButton, + }, + }, + }, + }); + + // State options for functionality based on user input + + { + /* TAGGING FUNCTIONALITY */ + } + useEffect(() => { + if ( + (functionality === "tagging" || functionality === "Tagging") && + (!primaryButtonView.params || + !primaryButtonView.params.tags || + primaryButtonView.params.tags.length === 0) + ) { + setPrimaryButtonView({ ...primaryButtonView, disabled: true }); + } else { + setPrimaryButtonView({ ...primaryButtonView, disabled: false }); + } + }, [primaryButtonView.params]); + + 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" || 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 00000000000..71cee8fb7d6 --- /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 50cbcf175f4..3f3e83467a7 100644 --- a/app/packages/components/src/components/MuiIconFont/index.tsx +++ b/app/packages/components/src/components/MuiIconFont/index.tsx @@ -2,12 +2,30 @@ import { Icon, IconProps } from "@mui/material"; import "material-icons/iconfont/material-icons.css"; 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}; -} - type MuiIconFontProps = IconProps & { name: string; + variant?: "filled" | "outlined" | "rounded" | "sharp"; }; + +const defaultProps = { variant: "filled" as const }; + +// Available Icons: https://github.com/marella/material-icons?tab=readme-ov-file#available-icons +const MuiIconFont = React.memo(function MuiIconFont(props: MuiIconFontProps) { + const { name, variant = defaultProps.variant, ...iconProps } = props; + + const variantClassMap = { + filled: "", + outlined: "material-icons-outlined", + rounded: "material-icons-rounded", + sharp: "material-icons-sharp", + }; + + const className = variant ? variantClassMap[variant] : ""; + + return ( + + {name} + + ); +}); +export default MuiIconFont; diff --git a/app/packages/components/src/components/PillBadge/PillBadge.tsx b/app/packages/components/src/components/PillBadge/PillBadge.tsx new file mode 100644 index 00000000000..d9fc4f4b328 --- /dev/null +++ b/app/packages/components/src/components/PillBadge/PillBadge.tsx @@ -0,0 +1,175 @@ +import React, { useState } from "react"; +import CircleIcon from "@mui/icons-material/Circle"; +import { Chip, FormControl, MenuItem, Select } from "@mui/material"; +import { usePanelEvent } from "@fiftyone/operators"; +import { usePanelId } from "@fiftyone/spaces"; + +const PillBadge = ({ + text, + color = "default", + variant = "filled", + showIcon = true, + operator, +}: { + text: string | string[] | [string, string][]; + color?: string; + variant?: "filled" | "outlined"; + showIcon?: boolean; + operator?: () => void; +}) => { + const getInitialChipSelection = ( + text: string | string[] | [string, string][] + ) => { + if (typeof text === "string") return text; + if (Array.isArray(text)) { + if (text.length === 0) return ""; + if (Array.isArray(text[0])) return text[0][0]; + return text[0]; + } + return ""; + }; + + const getInitialChipColor = ( + text: string | string[] | [string, string][], + color?: string + ) => { + if (typeof text === "string") return color; + if (Array.isArray(text)) { + if (text.length === 0) return "default"; + if (Array.isArray(text[0])) return text[0][1]; + return color || "default"; + } + return "default"; + }; + + const [chipSelection, setChipSelection] = useState( + getInitialChipSelection(text) + ); + const [chipColor, setChipColor] = useState(getInitialChipColor(text, color)); + + const COLORS: { [key: string]: string } = { + default: "#999999", + primary: "#FFB682", + error: "error", + warning: "warning", + info: "info", + success: "#8BC18D", + }; + + const chipStyle: { [key: string]: string | number } = { + color: COLORS[chipColor || "default"] || COLORS.default, + fontSize: 14, + fontWeight: 500, + paddingLeft: 1, + }; + + const panelId = usePanelId(); + const handleClick = usePanelEvent(); + + return ( + + {typeof text === "string" ? ( + + ) : undefined + } + label={text} + sx={{ + ...chipStyle, + "& .MuiChip-icon": { + marginRight: "-7px", + }, + "& .MuiChip-label": { + marginBottom: "1px", + }, + }} + variant={variant as "filled" | "outlined" | undefined} + /> + ) : ( + + + ) : undefined + } + label={ + Array.isArray(text) && + text.length > 0 && + Array.isArray(text[0]) ? ( + + ) : ( + + ) + } + sx={{ + ...chipStyle, + "& .MuiChip-icon": { + marginRight: "-7px", + }, + "& .MuiChip-label": { + marginBottom: "1px", + }, + "& .MuiInput-input:focus": { + backgroundColor: "inherit", + }, + }} + variant={variant as "filled" | "outlined" | undefined} + > + + )} + + ); +}; + +export default PillBadge; diff --git a/app/packages/components/src/components/PillBadge/index.ts b/app/packages/components/src/components/PillBadge/index.ts new file mode 100644 index 00000000000..45ed630ea28 --- /dev/null +++ b/app/packages/components/src/components/PillBadge/index.ts @@ -0,0 +1 @@ +export { default as PillBadge } from "./PillBadge"; diff --git a/app/packages/core/src/plugins/SchemaIO/components/ButtonView.tsx b/app/packages/core/src/plugins/SchemaIO/components/ButtonView.tsx index ae4b7b2becc..5778fa96ac1 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/ButtonView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/ButtonView.tsx @@ -1,10 +1,10 @@ +import React from "react"; import { MuiIconFont } from "@fiftyone/components"; import { usePanelEvent } from "@fiftyone/operators"; import { usePanelId } from "@fiftyone/spaces"; import { isNullish } from "@fiftyone/utilities"; import { Box, ButtonProps, Typography } from "@mui/material"; -import React from "react"; -import { getColorByCode, getComponentProps } from "../utils"; +import { getColorByCode, getComponentProps, getDisabledColors } from "../utils"; import { ViewPropsType } from "../utils/types"; import Button from "./Button"; import TooltipProvider from "./TooltipProvider"; @@ -22,6 +22,7 @@ export default function ButtonView(props: ViewPropsType) { params = {}, prompt, title, + disabled = false, } = view; const panelId = usePanelId(); const handleClick = usePanelEvent(); @@ -37,8 +38,12 @@ export default function ButtonView(props: ViewPropsType) { return ( - +