diff --git a/src/front/Components/DesignSystem/Toasts/ToastsContainer/ToastElement/index.tsx b/src/front/Components/DesignSystem/Toasts/ToastsContainer/ToastElement/index.tsx index d10cc2e2..896f0894 100644 --- a/src/front/Components/DesignSystem/Toasts/ToastsContainer/ToastElement/index.tsx +++ b/src/front/Components/DesignSystem/Toasts/ToastsContainer/ToastElement/index.tsx @@ -51,7 +51,7 @@ class ToastElementClass extends React.Component { data-clickable={toast.redirectUrl ? true : false} onClick={this.handleClick}> {toast.time !== 0 &&
} -
+
{toast.icon && toast.icon}
diff --git a/src/front/Components/DesignSystem/Typography/index.tsx b/src/front/Components/DesignSystem/Typography/index.tsx index 30a5551e..8c5e87b1 100644 --- a/src/front/Components/DesignSystem/Typography/index.tsx +++ b/src/front/Components/DesignSystem/Typography/index.tsx @@ -149,6 +149,10 @@ export enum ETypoColor { INPUT_ERROR = "--input-error", TEXT_ACCENT = "--text-accent", + + CONTRAST_DEFAULT = "--contrast-default", + CONTRAST_HOVERED = "--contrast-hovered", + ERROR_WEAK_CONTRAST = "--error-weak-contrast", } export default function Typography(props: IProps) { diff --git a/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/classes.module.scss b/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/classes.module.scss new file mode 100644 index 00000000..3231922d --- /dev/null +++ b/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/classes.module.scss @@ -0,0 +1,23 @@ +.menu-item-wrapper { + width: 100%; + .menu-item { + display: flex; + padding: var(--spacing-md, 16px); + justify-content: flex-start; + align-items: center; + gap: var(--spacing-lg, 24px); + cursor: pointer; + + > svg { + width: 24px; + height: 24px; + transition: all ease-in-out 0.1s; + } + } + + .separator { + width: 100%; + height: 1px; + background-color: var(--separator-stroke-light, #d7dce0); + } +} diff --git a/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/index.tsx b/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/index.tsx new file mode 100644 index 00000000..81071a54 --- /dev/null +++ b/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/index.tsx @@ -0,0 +1,50 @@ +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import classes from "./classes.module.scss"; +import { useRouter } from "next/router"; +import React, { useCallback } from "react"; +import useHoverable from "@Front/Hooks/useHoverable"; +import { ISubElement } from ".."; + +type IProps = { + element: ISubElement; + closeMenuCb: () => void; +}; +export default function SubMenuItem(props: IProps) { + const { element, closeMenuCb } = props; + const router = useRouter(); + const handleClickElement = useCallback( + (e: React.MouseEvent) => { + closeMenuCb(); + const link = e.currentTarget.getAttribute("data-link"); + if (link) router.push(link); + if (element.onClick) element.onClick(); + }, + [closeMenuCb, element, router], + ); + + const { handleMouseEnter, handleMouseLeave, isHovered } = useHoverable(); + + // The element has a link or an onClick but not both, if it has a link, show it has a link, if it has an onClick, show it has an onClick + const getColor = useCallback(() => { + if (isHovered && element.color !== ETypoColor.ERROR_WEAK_CONTRAST) return ETypoColor.CONTRAST_HOVERED; + if (element.color) return element.color; + return ETypoColor.CONTRAST_DEFAULT; + }, [element.color, isHovered]); + + return ( +
+
+ {React.cloneElement(element.icon, { color: `var(${getColor()})` })} + + {element.text} + +
+ {element.hasSeparator &&
} +
+ ); +} diff --git a/src/front/Components/Elements/ButtonWithSubMenu/classes.module.scss b/src/front/Components/Elements/ButtonWithSubMenu/classes.module.scss new file mode 100644 index 00000000..28ddf217 --- /dev/null +++ b/src/front/Components/Elements/ButtonWithSubMenu/classes.module.scss @@ -0,0 +1,29 @@ +.root { + position: relative; + width: fit-content; + + .sub-menu { + position: absolute; + top: 48px; + display: inline-flex; + padding: var(--spacing-05, 4px) var(--spacing-2, 16px); + flex-direction: column; + align-items: flex-start; + border-radius: var(--menu-radius, 0px); + border: 1px solid var(--menu-border, #d7dce0); + background: var(--color-generic-white, #fff); + text-wrap: nowrap; + /* shadow/sm */ + box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.1); + z-index: 2; + &[data-opening-side="left"] { + left: auto; + right: 0px; + } + + &[data-opening-side="right"] { + left: 0px; + right: auto; + } + } +} diff --git a/src/front/Components/Elements/ButtonWithSubMenu/index.tsx b/src/front/Components/Elements/ButtonWithSubMenu/index.tsx new file mode 100644 index 00000000..1ef1a471 --- /dev/null +++ b/src/front/Components/Elements/ButtonWithSubMenu/index.tsx @@ -0,0 +1,78 @@ +import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton"; +import classes from "./classes.module.scss"; +import { ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import React, { useEffect, useRef, useState } from "react"; +import SubMenuItem from "./SubMenuItem"; + +type ISubElementBase = { + icon: JSX.Element; + text: string; + hasSeparator?: boolean; + color?: ETypoColor; +}; + +type ISubElementWithLink = ISubElementBase & { + link: string; + onClick?: never; +}; + +type ISubElementWithOnClick = ISubElementBase & { + onClick: () => void; + link?: never; +}; + +export type ISubElement = ISubElementWithLink | ISubElementWithOnClick; + +type IProps = { + icon: React.ReactNode; + text?: string; + subElements: ISubElement[]; + openingSide?: "left" | "right"; +}; + +export default function ButtonWithSubMenu(props: IProps) { + const { openingSide = "left" } = props; + + const [isSubMenuOpened, setIsSubMenuOpened] = useState(false); + + const subMenuRef = useRef(null); + const iconRef = useRef(null); + + const handleClick = () => { + setIsSubMenuOpened((prev) => !prev); + }; + + const closeMenu = () => { + setIsSubMenuOpened(false); + }; + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if ( + subMenuRef.current && + !subMenuRef.current.contains(e.target as Node) && + iconRef.current && + !iconRef.current.contains(e.target as Node) + ) { + setIsSubMenuOpened(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + return ( +
+
+ +
+ + {isSubMenuOpened && ( +
+ {props.subElements.map((element, index) => { + return ; + })} +
+ )} +
+ ); +} diff --git a/src/front/Components/Layouts/DesignSystem/index.tsx b/src/front/Components/Layouts/DesignSystem/index.tsx index 0e475bdf..4152498b 100644 --- a/src/front/Components/Layouts/DesignSystem/index.tsx +++ b/src/front/Components/Layouts/DesignSystem/index.tsx @@ -9,15 +9,24 @@ import Modal from "@Front/Components/DesignSystem/Modal"; import Newsletter from "@Front/Components/DesignSystem/Newsletter"; import Table from "@Front/Components/DesignSystem/Table"; import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag"; -import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; import NumberPicker from "@Front/Components/Elements/NumberPicker"; import Tabs from "@Front/Components/Elements/Tabs"; import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate"; import useOpenable from "@Front/Hooks/useOpenable"; -import { ArrowLongLeftIcon, ArrowLongRightIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { + ArchiveBoxIcon, + ArrowLongLeftIcon, + ArrowLongRightIcon, + EllipsisHorizontalIcon, + PencilSquareIcon, + UsersIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; import { useCallback, useMemo, useState } from "react"; import classes from "./classes.module.scss"; +import ButtonWithSubMenu from "@Front/Components/Elements/ButtonWithSubMenu"; export default function DesignSystem() { const { isOpen, open, close } = useOpenable(); @@ -69,6 +78,30 @@ export default function DesignSystem() {
+ Button icon with menu + } + subElements={[ + { + icon: , + text: "Modifier les collaborateurs", + onClick: () => alert("yo"), + hasSeparator: true, + }, + { + icon: , + text: "Modifier les informations du dossier", + link: "/", + hasSeparator: true, + }, + { + icon: , + text: "Archiver le dossier", + link: "/", + color: ETypoColor.ERROR_WEAK_CONTRAST, + }, + ]} + /> Inputs Number picker avec min à 1 et max à 10 {}} min={1} max={10} /> diff --git a/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx index 39a6e2c8..4e60f216 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx @@ -1,14 +1,14 @@ import CircleProgress from "@Front/Components/DesignSystem/CircleProgress"; -import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton"; import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag"; import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; import Module from "@Front/Config/Module"; -import { ArchiveBoxIcon, PencilSquareIcon, UserGroupIcon } from "@heroicons/react/24/outline"; +import { ArchiveBoxIcon, EllipsisHorizontalIcon, PencilSquareIcon, UsersIcon } from "@heroicons/react/24/outline"; import { OfficeFolder } from "le-coffre-resources/dist/Notary"; -import Link from "next/link"; import classes from "./classes.module.scss"; import { AnchorStatus } from ".."; +import ButtonWithSubMenu, { ISubElement } from "@Front/Components/Elements/ButtonWithSubMenu"; +import { useCallback } from "react"; type IProps = { folder: OfficeFolder | null; @@ -21,6 +21,49 @@ type IProps = { export default function InformationSection(props: IProps) { const { folder, progress, onArchive, anchorStatus, isArchived } = props; + const getSubMenuElement = useCallback(() => { + let elements: ISubElement[] = []; + + // Creating the three elements and adding them conditionnally + const modifyCollaboratorsElement = { + icon: , + text: "Modifier les collaborateurs", + link: Module.getInstance() + .get() + .modules.pages.Folder.pages.EditCollaborators.props.path.replace("[folderUid]", folder?.uid ?? ""), + hasSeparator: true, + }; + const modifyInformationsElement = { + icon: , + text: "Modifier les informations du dossier", + link: Module.getInstance() + .get() + .modules.pages.Folder.pages.EditInformations.props.path.replace("[folderUid]", folder?.uid ?? ""), + hasSeparator: true, + }; + + const archiveElement = { + icon: , + text: "Archiver le dossier", + onClick: onArchive, + color: ETypoColor.ERROR_WEAK_CONTRAST, + }; + + // If the folder is not anchored, we can modify the collaborators and the informations + if (anchorStatus === AnchorStatus.NOT_ANCHORED) { + elements.push(modifyCollaboratorsElement); + // Remove the separator if it's the last item (if the folder is not archived) + if (isArchived) modifyInformationsElement.hasSeparator = false; + + elements.push(modifyInformationsElement); + } + + // If the folder is not archived, we can archive it + if (!isArchived) { + elements.push(archiveElement); + } + return elements; + }, [anchorStatus, folder?.uid, isArchived, onArchive]); return (
@@ -43,37 +86,7 @@ export default function InformationSection(props: IProps) {
- {anchorStatus === AnchorStatus.NOT_ANCHORED && ( - <> - - } - variant={EIconButtonVariant.NEUTRAL} - /> - - - } - variant={EIconButtonVariant.NEUTRAL} - /> - - - )} - {!isArchived && ( - } - variant={EIconButtonVariant.ERROR} - /> - )} + } subElements={getSubMenuElement()} />