UNPKG

passbolt-styleguide

Version:

Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.

619 lines (557 loc) 20.4 kB
/** * Passbolt ~ Open source password manager for teams * Copyright (c) Passbolt SA (https://www.passbolt.com) * * Licensed under GNU Affero General Public License version 3 of the or any later version. * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) * @license https://opensource.org/licenses/AGPL-3.0 AGPL License * @link https://www.passbolt.com Passbolt(tm) * @since 2.13.0 */ import React from "react"; import PropTypes from "prop-types"; import { withContextualMenu } from "../../../contexts/ContextualMenuContext"; import FilterResourcesByFoldersItemContextualMenu from "./FilterResourcesByFoldersItemContextualMenu"; import { withAppContext } from "../../../../shared/context/AppContext/AppContext"; import { ResourceWorkspaceFilterTypes, withResourceWorkspace } from "../../../contexts/ResourceWorkspaceContext"; import { withDrag } from "../../../contexts/DragContext"; import DisplayDragFolderItem from "./DisplayDragFolderItem"; import { withRouter } from "react-router-dom"; import CarretDownSVG from "../../../../img/svg/caret_down.svg"; import CarretRightSVG from "../../../../img/svg/caret_right.svg"; import ShareFolderSVG from "../../../../img/svg/share_folder.svg"; import FolderSVG from "../../../../img/svg/folder.svg"; import MoreHorizontalSVG from "../../../../img/svg/more_horizontal.svg"; import NotifyError from "../../Common/Error/NotifyError/NotifyError"; import { withDialog } from "../../../contexts/DialogContext"; import { withResourceTypesLocalStorage } from "../../../../shared/context/ResourceTypesLocalStorageContext/ResourceTypesLocalStorageContext"; import ResourceTypesCollection from "../../../../shared/models/entity/resourceType/resourceTypesCollection"; import ActionAbortedMissingMetadataKeys from "../../Metadata/ActionAbortedMissingMetadataKeys/ActionAbortedMissingMetadataKeys"; class FilterResourcesByFoldersItem extends React.Component { /** * Constructor * Initialize state and bind methods */ constructor(props) { super(props); this.state = this.getDefaultState(); this.bindCallbacks(); } /** * Return default state * @returns {Object} default state */ getDefaultState() { return { draggingOver: false, draggingOverSince: null, open: this.props.isOpen, moreMenuOpen: false, }; } bindCallbacks() { this.handleCloseMoreMenu = this.handleCloseMoreMenu.bind(this); this.handleToggleOpenFolder = this.handleToggleOpenFolder.bind(this); this.handleContextualMenuEvent = this.handleContextualMenuEvent.bind(this); this.handleDragEndEvent = this.handleDragEndEvent.bind(this); this.handleDragLeaveEvent = this.handleDragLeaveEvent.bind(this); this.handleDragOverEvent = this.handleDragOverEvent.bind(this); this.handleDragStartEvent = this.handleDragStartEvent.bind(this); this.handleDropEvent = this.handleDropEvent.bind(this); this.handleMoreClickEvent = this.handleMoreClickEvent.bind(this); this.handleSelectEvent = this.handleSelectEvent.bind(this); } /** * Component did mount */ componentDidMount() { if (this.props.match.params.filterByFolderId) { // Expand folder tree until the selected folder this.openFolderParentTree(this.props.match.params.filterByFolderId); } } /** * Component did update * @param prevProps The previous props */ componentDidUpdate(prevProps) { const hasFolderRouteChange = this.props.match.params.filterByFolderId !== prevProps.match.params.filterByFolderId; if (hasFolderRouteChange && this.props.match.params.filterByFolderId) { // Expand folder tree until the selected folder this.openFolderParentTree(this.props.match.params.filterByFolderId); } } /** * Close the create menu */ handleCloseMoreMenu() { this.setState({ moreMenuOpen: false }); } /** * Handle when the user start dragging the folder. * @param {ReactEvent} event The event */ handleDragStartEvent(event) { event.persist(); const draggedItems = { folders: [this.props.folder], resources: [], }; this.props.dragContext.onDragStart(event, DisplayDragFolderItem, draggedItems); } /** * Handle when the user click on the folder left caret. * Fold/Unfold the folder. * @param {ReactEvent} event The event */ handleToggleOpenFolder(event) { /* * Prevent the component to select the folder. * @todo This default behavior should not be allowed as it will break other behavior, such as closing a contextual menu closing. */ event.stopPropagation(); const open = !this.state.open; this.setState({ open }); if (open) { this.props.toggleOpenFolder(this.props.folder.id); } else { this.props.toggleCloseFolder(this.props.folder.id); } } /** * Open the tree until a given folder * @param {object} selectedFolderId The folder id to scroll to */ openFolderParentTree(selectedFolderId) { const selectedFolder = this.props.context.folders.find((folder) => folder.id === selectedFolderId); // If the selected folder has a parent. Open it if not yet open. if (selectedFolder?.folder_parent_id) { if (selectedFolder.folder_parent_id === this.props.folder.id) { this.setState({ open: true }); this.props.toggleOpenFolder(this.props.folder.id); } else { this.openFolderParentTree(selectedFolder.folder_parent_id); } } } /** * Check if the current folder is open. * @return {boolean} */ isOpen() { return this.state.open; } /** * Handle when the user right clicks on a folder name. * @param {ReactEvent} event The event */ handleContextualMenuEvent(event) { // Prevent the browser contextual menu to pop up. event.preventDefault(); const top = event.pageY; const left = event.pageX; const folder = this.props.folder; const contextualMenuProps = { folder, left, top }; this.props.contextualMenuContext.show(FilterResourcesByFoldersItemContextualMenu, contextualMenuProps); } /** * Handle when the user clicks on the more button. * @param {ReactEvent} event The event */ handleMoreClickEvent(event) { const moreMenuOpen = !this.state.moreMenuOpen; this.setState({ moreMenuOpen }); if (moreMenuOpen) { const { left, top } = event.currentTarget.getBoundingClientRect(); const folder = this.props.folder; const onBeforeHide = this.handleCloseMoreMenu; const contextualMenuProps = { folder, left, top: top + 19, className: "right", onBeforeHide }; this.props.contextualMenuContext.show(FilterResourcesByFoldersItemContextualMenu, contextualMenuProps); } } /** * Handle when the user stop dragging content. * @param {ReactEvent} event The event */ handleDragEndEvent() { this.props.dragContext.onDragEnd(); } /** * Handle when the user is not dragging over this component anymore. */ handleDragLeaveEvent() { const draggingOver = false; const draggingOverSince = null; this.setState({ draggingOver, draggingOverSince }); } /** * Handle when the user is dragging over this component. * @param {ReactEvent} event The event */ handleDragOverEvent(event) { /* * If you want to allow a drop, you must prevent the default handling by cancelling both the dragenter and dragover events. * see: https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#droptargets */ event.preventDefault(); if (this.state.draggingOver) { this.openOnLongDragOver(event); return; } const draggingOver = true; const draggingOverSince = Date.now(); this.setState({ draggingOver, draggingOverSince }); } /** * Open the folder when the user is dragging for a defined period on a folder. * @param {ReactEvent} event The event */ openOnLongDragOver(event) { const period = 2000; const open = this.isOpen(); // If already open, leave. if (!open) { const now = Date.now(); if (now - this.state.draggingOverSince > period) { this.handleToggleOpenFolder(event); } } } /** * Handle when the user drop content on this component. * @param {ReactEvent} event The event */ async handleDropEvent() { // The user cannot drop the dragged content on a dragged item. const folderParentId = this.props.folder.id; const isDroppingOnDraggedItem = this.draggedItems.folders.some((item) => item.id === folderParentId); if (!isDroppingOnDraggedItem) { const folders = this.draggedItems.folders; const resources = this.draggedItems.resources; try { if (folders?.length > 0) { await this.moveFolder(folders); } else if (resources?.length > 0) { await this.moveResource(resources); } } catch (error) { this.handleError(error); } } // The dragLeave event is not fired when a drop is happening. Cancel the state manually. const draggingOver = false; this.setState({ draggingOver }); } /** * Move the folders or display action aborted if is not possible to move * @param {Array<Object>} folders * @return {Promise<void>} */ async moveFolder(folders) { // Folders to move const hasSomeSharedFolder = folders.some((folder) => !folder.personal); const isPersonalFolder = this.props.folder.personal; // Zero knowledge requires the user to have access to shared metadata key prior to move personal folders into shared folder or shared folders into personal folder. if ((!isPersonalFolder || (hasSomeSharedFolder && isPersonalFolder)) && this.userHasMissingKeys) { this.props.dialogContext.open(ActionAbortedMissingMetadataKeys); } else { await this.props.context.port.request("passbolt.folders.move-by-id", folders[0].id, this.props.folder.id); } } /** * Move the resources or display action aborted if is not possible to move * @param {Array<Object>} resources * @return {Promise<void>} */ async moveResource(resources) { const hasSomePersonalResourceV5 = resources.some( (resource) => resource.personal && this.props.resourceTypes.getFirstById(resource.resource_type_id)?.isV5(), ); // Zero knowledge requires the user to have access to shared metadata key prior to move personal resources into shared folder. if (hasSomePersonalResourceV5 && !this.props.folder.personal && this.userHasMissingKeys) { this.props.dialogContext.open(ActionAbortedMissingMetadataKeys); } else { // Resource ids to move const resourceIds = resources.map((resource) => resource.id); await this.props.context.port.request("passbolt.resources.move-by-ids", resourceIds, this.props.folder.id); } } /** * User has missing keys * @return {boolean} */ get userHasMissingKeys() { return this.props.context.loggedInUser.missing_metadata_key_ids?.length > 0; } /** * handle error and display error dialog * @param error */ handleError(error) { const errorDialogProps = { error: error, }; this.props.dialogContext.open(NotifyError, errorDialogProps); } /** * Handle the user selects a folder from the list. */ handleSelectEvent() { this.props.history.push(`/app/folders/view/${this.props.folder.id}`); } /** * Check if the folder associated to this component is selected. * @returns {boolean} */ isSelected() { const filter = this.props.resourceWorkspaceContext.filter; const hasSelectedFolder = filter.type === ResourceWorkspaceFilterTypes.FOLDER && filter.payload.folder; if (!hasSelectedFolder) { return false; } return filter.payload.folder.id === this.props.folder.id; } /** * Check if a folder is a child of any of the given folders. * @param {object} folder The target folder to check if it is a child * @param {array} folders The folders to check for */ isChildOfAny(folder, folders) { for (const i in folders) { if (folder.folder_parent_id === folders[i].id) { return true; } } if (folder.folder_parent_id !== null) { const folderParent = this.props.context.folders.find((item) => item.id === folder.folder_parent_id); return this.isChildOfAny(folderParent, folders); } return false; } /** * Check if the user can drag an item. * @param {object} item The target item */ canDragItem(item) { // The user can always drag an element located at their root. if (item.folder_parent_id === null) { return true; } const folderParent = this.props.context.folders.find((folder) => folder.id === item.folder_parent_id); // The user can always drag content from a personal folder. if (folderParent.personal) { return true; } // The user cannot drag an element if the parent folder is in READ. if (folderParent.permission.type < 7) { return false; } // The user cannot move folder in READ ONLY from a shared folder. if (item.permission.type < 7) { return false; } return true; } /** * Check if the user can drag all the items they are currently dragging. * @param {array} draggedItems The list of dragged items. * @returns {boolean} */ canDragItems(draggedItems) { const draggedFolders = draggedItems.folders; let canDragItems = draggedFolders?.reduce((accumulator, folder) => accumulator && this.canDragItem(folder), true); const draggedResources = draggedItems.resources; canDragItems &= draggedResources?.reduce((accumulator, folder) => accumulator && this.canDragItem(folder), true); return canDragItems; } /** * Get the lowest permissions among a list of items * @param {array} items The list of items to look into * @returns {int} */ getItemsListLowestPermission(items) { return items?.reduce((accumulator, draggedItem) => { if (draggedItem.permission.type < accumulator) { accumulator = draggedItem.permission.type; } return accumulator; }, 15); } /** * Get the lowest permissions among all the dragged items. * @returns {int} */ getDraggedItemsLowestPermission() { const draggedFoldersLowestPermission = this.getItemsListLowestPermission(this.draggedItems.folders); const draggedResourcesLowestPermission = this.getItemsListLowestPermission(this.draggedItems.resources); return draggedFoldersLowestPermission < draggedResourcesLowestPermission ? draggedFoldersLowestPermission : draggedResourcesLowestPermission; } /** * Check if the user can drop the content they are dragging into the folder associated to this component. * @returns {boolean} */ canDropInto() { if (!this.isDragging()) { return false; } // Cannot move content into a folder for with the user has insufficient permission (<UPDATE) if (this.props.folder.permission.type < 7) { return false; } // Cannot move a content in READ ONLY into a shared folder. if (!this.props.folder.personal) { const draggedItemsLowestPermission = this.getDraggedItemsLowestPermission(); if (draggedItemsLowestPermission < 7) { return false; } } // Cannot move a folder into one of its own children. if (this.isChildOfAny(this.props.folder, this.draggedItems.folders)) { return false; } // Cannot move a folder into itself. for (const i in this.draggedItems.folders) { if (this.props.folder.id === this.draggedItems.folders[i].id) { return false; } } return true; } /** * Check if the component is dragged. * @returns {boolean} */ isDragged() { if (this.isDragging()) { return this.draggedItems.folders?.some((folder) => folder.id === this.props.folder.id); } return false; } /** * Check if the component is disabled. * @returns {boolean} */ isDisabled() { /* * If the user is dragging content, disable the component if: * - The user is not allowed to drag any of dragged items; * - The user is not allowed to drop content in the folder associated to this component. */ if (this.isDragging()) { const canDragItems = this.canDragItems(this.draggedItems); if (!canDragItems) { return true; } const canDropInto = this.canDropInto(); if (!canDropInto) { return true; } } return false; } /** * Has the children folder. * @returns {boolean} */ get hasChildrenFolders() { return this.props.context.folders.filter((folder) => folder.folder_parent_id === this.props.folder.id).length > 0; } /** * return dragged items * @returns {*} */ get draggedItems() { return this.props.dragContext.draggedItems; } /** * Check if the user is currently dragging content. * @returns {boolean} */ isDragging() { return this.props.dragContext.dragging; } /** * Render the component * @returns {JSX} */ render() { const hasChildren = this.hasChildrenFolders; const isSelected = this.isSelected(); const isDisabled = this.isDisabled(); const isDragged = this.isDragged(); const isOpen = this.isOpen(); const canDropInto = this.canDropInto(); const showDropFocus = this.state.draggingOver && canDropInto; const depth = this.props.context.getHierarchyFolderCache(this.props.folder.folder_parent_id).length; return ( <li className="folder-item"> <div className={`row ${isSelected ? "selected" : ""} ${isDisabled ? "disabled" : ""} ${isDragged ? "is-dragged" : ""} ${showDropFocus ? "drop-focus" : ""} ${hasChildren ? "" : "no-child"} ${this.state.moreMenuOpen ? "highlight" : ""}`} draggable="true" onDrop={this.handleDropEvent} onDragOver={this.handleDragOverEvent} onDragEnd={this.handleDragEndEvent} onDragLeave={this.handleDragLeaveEvent} onDragStart={this.handleDragStartEvent} style={{ "--folder-depth": depth }} > <div className="main-cell-wrapper"> <div className="main-cell" onClick={this.handleSelectEvent} onContextMenu={this.handleContextualMenuEvent}> <button className="link no-border" type="button"> {hasChildren && ( <div className="toggle-folder" onClick={this.handleToggleOpenFolder} role="button"> {isOpen ? <CarretDownSVG /> : <CarretRightSVG />} </div> )} {this.props.folder.personal ? <FolderSVG /> : <ShareFolderSVG />} <span title={this.props.folder.name} className="folder-name"> {this.props.folder.name} </span> </button> </div> </div> {!isDragged && ( <div className="dropdown right-cell more-ctrl"> <button type="button" className={`button-transparent inline-menu-horizontal ${this.state.moreMenuOpen ? "open" : ""}`} onClick={this.handleMoreClickEvent} > <MoreHorizontalSVG /> </button> </div> )} </div> </li> ); } } FilterResourcesByFoldersItem.defaultProps = { isOpen: false, }; FilterResourcesByFoldersItem.propTypes = { context: PropTypes.any, // The app context contextualMenuContext: PropTypes.any, // The contextual menu context history: PropTypes.object, match: PropTypes.object, folder: PropTypes.object, isOpen: PropTypes.bool.isRequired, resourceWorkspaceContext: PropTypes.any, resourceTypes: PropTypes.instanceOf(ResourceTypesCollection), // The resource types collection dragContext: PropTypes.any, dialogContext: PropTypes.object, // The dialog context toggleOpenFolder: PropTypes.func, toggleCloseFolder: PropTypes.func, }; export default withRouter( withAppContext( withResourceTypesLocalStorage( withContextualMenu(withDialog(withResourceWorkspace(withDrag(FilterResourcesByFoldersItem)))), ), ), );