Merge branch 'dev' into staging
This commit is contained in:
commit
24fe70703a
@ -51,7 +51,7 @@ class ToastElementClass extends React.Component<IPropsClass, IState> {
|
||||
data-clickable={toast.redirectUrl ? true : false}
|
||||
onClick={this.handleClick}>
|
||||
{toast.time !== 0 && <div className={classes["loadbar"]} style={style} />}
|
||||
<div className={classes["header"]} onClick={this.onClose}>
|
||||
<div className={classes["header"]}>
|
||||
<div className={classes["text-icon_row"]}>
|
||||
{toast.icon && toast.icon}
|
||||
<div className={classes["text-container"]}>
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<HTMLDivElement>) => {
|
||||
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 (
|
||||
<div
|
||||
className={classes["menu-item-wrapper"]}
|
||||
onClick={handleClickElement}
|
||||
data-link={element.link}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}>
|
||||
<div className={classes["menu-item"]}>
|
||||
{React.cloneElement(element.icon, { color: `var(${getColor()})` })}
|
||||
<Typography typo={ETypo.TEXT_LG_REGULAR} color={getColor()}>
|
||||
{element.text}
|
||||
</Typography>
|
||||
</div>
|
||||
{element.hasSeparator && <div className={classes["separator"]} />}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
78
src/front/Components/Elements/ButtonWithSubMenu/index.tsx
Normal file
78
src/front/Components/Elements/ButtonWithSubMenu/index.tsx
Normal file
@ -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<HTMLDivElement>(null);
|
||||
const iconRef = useRef<HTMLDivElement>(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 (
|
||||
<div className={classes["root"]}>
|
||||
<div className={classes["main"]} onClick={handleClick} ref={iconRef}>
|
||||
<IconButton icon={props.icon} variant={EIconButtonVariant.NEUTRAL} />
|
||||
</div>
|
||||
|
||||
{isSubMenuOpened && (
|
||||
<div className={classes["sub-menu"]} ref={subMenuRef} data-opening-side={openingSide}>
|
||||
{props.subElements.map((element, index) => {
|
||||
return <SubMenuItem element={element} key={index} closeMenuCb={closeMenu} />;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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() {
|
||||
<Newsletter isOpen />
|
||||
<div className={classes["root"]}>
|
||||
<div className={classes["components"]}>
|
||||
<Typography typo={ETypo.TEXT_LG_BOLD}>Button icon with menu</Typography>
|
||||
<ButtonWithSubMenu
|
||||
icon={<EllipsisHorizontalIcon />}
|
||||
subElements={[
|
||||
{
|
||||
icon: <UsersIcon />,
|
||||
text: "Modifier les collaborateurs",
|
||||
onClick: () => alert("yo"),
|
||||
hasSeparator: true,
|
||||
},
|
||||
{
|
||||
icon: <PencilSquareIcon />,
|
||||
text: "Modifier les informations du dossier",
|
||||
link: "/",
|
||||
hasSeparator: true,
|
||||
},
|
||||
{
|
||||
icon: <ArchiveBoxIcon />,
|
||||
text: "Archiver le dossier",
|
||||
link: "/",
|
||||
color: ETypoColor.ERROR_WEAK_CONTRAST,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Typography typo={ETypo.TEXT_LG_BOLD}>Inputs</Typography>
|
||||
<Typography typo={ETypo.TEXT_SM_REGULAR}>Number picker avec min à 1 et max à 10</Typography>
|
||||
<NumberPicker defaultValue={1} onChange={() => {}} min={1} max={10} />
|
||||
|
@ -122,7 +122,7 @@ export default function DocumentTables(props: IProps) {
|
||||
label={tradDocumentStatus[document.document_status].toUpperCase()}
|
||||
/>
|
||||
),
|
||||
created_at: document.created_at,
|
||||
created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_",
|
||||
actions: (
|
||||
<Link
|
||||
href={Module.getInstance()
|
||||
@ -153,7 +153,7 @@ export default function DocumentTables(props: IProps) {
|
||||
label={tradDocumentStatus[document.document_status].toUpperCase()}
|
||||
/>
|
||||
),
|
||||
created_at: document.created_at,
|
||||
created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_",
|
||||
actions: (
|
||||
<div className={classes["actions"]}>
|
||||
<Link
|
||||
@ -187,7 +187,7 @@ export default function DocumentTables(props: IProps) {
|
||||
label={tradDocumentStatus[document.document_status].toUpperCase()}
|
||||
/>
|
||||
),
|
||||
created_at: document.created_at,
|
||||
created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_",
|
||||
actions: "",
|
||||
};
|
||||
})
|
||||
|
@ -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: <UsersIcon />,
|
||||
text: "Modifier les collaborateurs",
|
||||
link: Module.getInstance()
|
||||
.get()
|
||||
.modules.pages.Folder.pages.EditCollaborators.props.path.replace("[folderUid]", folder?.uid ?? ""),
|
||||
hasSeparator: true,
|
||||
};
|
||||
const modifyInformationsElement = {
|
||||
icon: <PencilSquareIcon />,
|
||||
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: <ArchiveBoxIcon />,
|
||||
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 (
|
||||
<section className={classes["root"]}>
|
||||
<div className={classes["info-box1"]}>
|
||||
@ -43,37 +86,7 @@ export default function InformationSection(props: IProps) {
|
||||
<div className={classes["progress-container"]}>
|
||||
<CircleProgress percentage={progress} />
|
||||
<div className={classes["icon-container"]}>
|
||||
{anchorStatus === AnchorStatus.NOT_ANCHORED && (
|
||||
<>
|
||||
<Link
|
||||
href={Module.getInstance()
|
||||
.get()
|
||||
.modules.pages.Folder.pages.EditCollaborators.props.path.replace("[folderUid]", folder?.uid ?? "")}
|
||||
title="Modifier les collaborateurs">
|
||||
<IconButton
|
||||
icon={<UserGroupIcon title="Modifier les collaborateurs" />}
|
||||
variant={EIconButtonVariant.NEUTRAL}
|
||||
/>
|
||||
</Link>
|
||||
<Link
|
||||
href={Module.getInstance()
|
||||
.get()
|
||||
.modules.pages.Folder.pages.EditInformations.props.path.replace("[folderUid]", folder?.uid ?? "")}
|
||||
title="Modifier les informations du dossiers">
|
||||
<IconButton
|
||||
icon={<PencilSquareIcon title="Modifier les informations du dossiers" />}
|
||||
variant={EIconButtonVariant.NEUTRAL}
|
||||
/>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{!isArchived && (
|
||||
<IconButton
|
||||
onClick={onArchive}
|
||||
icon={<ArchiveBoxIcon title="Archiver le dossier" />}
|
||||
variant={EIconButtonVariant.ERROR}
|
||||
/>
|
||||
)}
|
||||
<ButtonWithSubMenu icon={<EllipsisHorizontalIcon />} subElements={getSubMenuElement()} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes["description-container"]}>
|
||||
|
@ -11,7 +11,7 @@ type IProps = {
|
||||
isOpen: boolean;
|
||||
onClose?: () => void;
|
||||
folderUid: string;
|
||||
onAnchorSuccess?: () => void;
|
||||
onAnchorSuccess: () => void;
|
||||
};
|
||||
|
||||
export default function AnchoringModal(props: IProps) {
|
||||
@ -19,12 +19,22 @@ export default function AnchoringModal(props: IProps) {
|
||||
const [isAnchoring, setIsAnchoring] = useState(false);
|
||||
|
||||
const anchor = useCallback(() => {
|
||||
const timeoutDelay = 9800;
|
||||
const timeoutPromise = new Promise((resolve) => {
|
||||
setTimeout(resolve, timeoutDelay);
|
||||
});
|
||||
setIsAnchoring(true);
|
||||
OfficeFolderAnchors.getInstance()
|
||||
return OfficeFolderAnchors.getInstance()
|
||||
.post(folderUid)
|
||||
.then(() => timeoutPromise)
|
||||
.then(() => setIsAnchoring(false))
|
||||
.then(onAnchorSuccess)
|
||||
.catch((e) => console.warn(e))
|
||||
}, [folderUid, onAnchorSuccess]);
|
||||
.then(onClose)
|
||||
.catch((e) => {
|
||||
console.warn(e);
|
||||
setIsAnchoring(false);
|
||||
});
|
||||
}, [folderUid, onAnchorSuccess, onClose]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
@ -0,0 +1,12 @@
|
||||
import Alert, { EAlertVariant } from "@Front/Components/DesignSystem/Alert";
|
||||
|
||||
export default function AnchoringProcessingNeutral() {
|
||||
return (
|
||||
<Alert
|
||||
title="Ancrage en cours..."
|
||||
description="Vos documents sont en train d'être ancrés dans la blockchain. Cela peut prendre quelques instants. Merci de votre patience."
|
||||
variant={EAlertVariant.NEUTRAL}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
}
|
@ -13,6 +13,7 @@ import ClientView from "./ClientView";
|
||||
import AnchoringAlertInfo from "./elements/AnchoringAlertInfo";
|
||||
import AnchoringAlertSuccess from "./elements/AnchoringAlertSuccess";
|
||||
import AnchoringModal from "./elements/AnchoringModal";
|
||||
import AnchoringProcessingNeutral from "./elements/AnchoringProcessingNeutral";
|
||||
import ArchiveAlertWarning from "./elements/ArchiveAlertWarning";
|
||||
import ArchiveModal from "./elements/ArchiveModal";
|
||||
import DownloadAnchoringProofModal from "./elements/DownloadAnchoringProofModal";
|
||||
@ -66,6 +67,9 @@ export default function FolderInformation(props: IProps) {
|
||||
include: {
|
||||
contact: true,
|
||||
documents: {
|
||||
where: {
|
||||
folder_uid: folderUid,
|
||||
},
|
||||
include: {
|
||||
folder: true,
|
||||
document_type: true,
|
||||
@ -118,8 +122,6 @@ export default function FolderInformation(props: IProps) {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const onAnchorSuccess = useCallback(() => fetchData().then(downloadAnchoringProofModal.open), [fetchData, downloadAnchoringProofModal]);
|
||||
|
||||
const onArchive = useCallback(() => {
|
||||
if (anchorStatus === AnchorStatus.NOT_ANCHORED) return requireAnchoringModal.open();
|
||||
archiveModal.open();
|
||||
@ -146,17 +148,18 @@ export default function FolderInformation(props: IProps) {
|
||||
isArchived={isArchived}
|
||||
/>
|
||||
)}
|
||||
{!isArchived && anchorStatus === AnchorStatus.ANCHORING && <AnchoringProcessingNeutral />}
|
||||
{isArchived && folderUid && (
|
||||
<ArchiveAlertWarning folderUid={folderUid} onDownloadAnchoringProof={downloadAnchoringProofModal.open} />
|
||||
)}
|
||||
{folder && !doesFolderHaveClient && <NoClientView folder={folder} anchorStatus={anchorStatus} />}
|
||||
{folder && doesFolderHaveClient && <ClientView folder={folder} anchorStatus={anchorStatus} />}
|
||||
{folderUid && anchorStatus === AnchorStatus.NOT_ANCHORED && (
|
||||
{folderUid && (
|
||||
<AnchoringModal
|
||||
isOpen={anchoringModal.isOpen}
|
||||
onClose={anchoringModal.close}
|
||||
folderUid={folderUid}
|
||||
onAnchorSuccess={onAnchorSuccess}
|
||||
onAnchorSuccess={fetchData}
|
||||
/>
|
||||
)}
|
||||
{folder && anchorStatus === AnchorStatus.VERIFIED_ON_CHAIN && (
|
||||
|
@ -84,14 +84,14 @@ class UpdateClientClass extends BasePage<IPropsClass, IState> {
|
||||
<div className={classes["content"]}>
|
||||
<TextField
|
||||
name="first_name"
|
||||
placeholder="Nom"
|
||||
placeholder="Prénom"
|
||||
onChange={this.onChangeNameInput}
|
||||
defaultValue={this.state.customer?.contact?.first_name}
|
||||
validationError={this.state.validationError.find((error) => error.property === "first_name")}
|
||||
/>
|
||||
<TextField
|
||||
name="last_name"
|
||||
placeholder="Prénom"
|
||||
placeholder="Nom"
|
||||
onChange={this.onChangeFirstNameInput}
|
||||
defaultValue={this.state.customer?.contact?.last_name}
|
||||
validationError={this.state.validationError.find((error) => error.property === "last_name")}
|
||||
|
Loading…
x
Reference in New Issue
Block a user