Skip to content

Commit

Permalink
Merge pull request #4951 from voxel51/patch/panel-tag-modal
Browse files Browse the repository at this point in the history
Cherry-pick ModalView and relevant changes from tag-modal branch
  • Loading branch information
Br2850 authored Oct 20, 2024
2 parents 66daf79 + d1260b7 commit af9f0f7
Show file tree
Hide file tree
Showing 9 changed files with 475 additions and 79 deletions.
80 changes: 80 additions & 0 deletions app/packages/components/src/components/ModalBase/DisplayTags.tsx
Original file line number Diff line number Diff line change
@@ -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<DisplayTagsProps> = ({ saveTags }) => {
const [chips, setChips] = useState<string[]>([]); // chips is an array of strings
const [inputValue, setInputValue] = useState<string>(""); // 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 (
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 2,
alignItems: "start",
paddingTop: 1,
paddingBottom: 1,
width: "100%", // Ensure the box takes full width
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
width: "100%", // Ensure the inner box takes full width
}}
>
<TextField
variant="outlined"
label="Enter tag"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
fullWidth // Make TextField take up the remaining width
/>
<IconButton onClick={handleAddChip} color="primary">
<AddIcon />
</IconButton>
</Box>

<Box
sx={{
display: "flex",
gap: 1,
flexWrap: "wrap",
}}
>
{chips.map((chip, index) => (
<Chip
key={index}
label={chip}
onDelete={() => handleDeleteChip(chip)}
/>
))}
</Box>
</Box>
);
};

export default DisplayTags;
253 changes: 253 additions & 0 deletions app/packages/components/src/components/ModalBase/ModalBase.tsx
Original file line number Diff line number Diff line change
@@ -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<ModalBaseProps> = ({
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 (
<>
<Box>
<ButtonView
onClick={handleOpen}
schema={{
view: modalButtonView,
}}
/>
</Box>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-title"
aria-describedby="modal-subtitle"
>
<Box sx={modalStyle}>
<Typography
id="modal-title"
variant="h5"
component="h5"
sx={{ textAlign: titleAlign }}
>
{modal?.icon && (
<MuiIconFont
variant={modal?.iconVariant || "filled"}
sx={{ verticalAlign: "middle" }}
name={modal.icon}
>
{modal.icon}
</MuiIconFont>
)}{" "}
{title}
</Typography>
<Typography
id="modal-subtitle"
variant="h6"
component="h6"
sx={{ mt: 4, textAlign: subtitleAlign }}
>
{subtitle}
</Typography>
<Typography id="modal-body" sx={{ my: 1, textAlign: bodyAlign }}>
{body}
</Typography>
{functionality === "tagging" && (
<DisplayTags saveTags={handleSaveTags} />
)}
<Box
sx={{
display: "flex",
justifyContent: "space-between",
mt: 2,
gap: 3,
}}
>
{secondaryButton && (
<Box sx={{ flexGrow: 1 }}>
<ButtonView
onClick={handleClose}
schema={{
view: secondaryButtonView,
}}
/>
</Box>
)}
{primaryButton && (
<Box sx={{ flexGrow: 1 }}>
<ButtonView
onClick={handleClose}
schema={{
view: primaryButtonView,
}}
/>
</Box>
)}
</Box>
</Box>
</Modal>
</>
);
};
export default ModalBase;
1 change: 1 addition & 0 deletions app/packages/components/src/components/ModalBase/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as ModalBase } from "./ModalBase";
28 changes: 26 additions & 2 deletions app/packages/components/src/components/MuiIconFont/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Icon {...iconProps}>{name}</Icon>;
const { name, variant, ...iconProps } = props;

let icon = <Icon {...iconProps}>{name}</Icon>;

if (variant === "outlined") {
icon = (
<Icon {...iconProps} className="material-icons-outlined">
{name}
</Icon>
);
} else if (variant === "rounded") {
icon = (
<Icon {...iconProps} className="material-icons-rounded">
{name}
</Icon>
);
} else if (variant === "sharp") {
icon = (
<Icon {...iconProps} className="material-icons-sharp">
{name}
</Icon>
);
}

return icon;
}

type MuiIconFontProps = IconProps & {
name: string;
variant?: "filled" | "outlined" | "rounded" | "sharp";
};
Loading

0 comments on commit af9f0f7

Please sign in to comment.