passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
392 lines (349 loc) • 12.9 kB
JavaScript
/**
* 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 Icon from "../../../../shared/components/Icons/Icon";
import FilterResourcesByFoldersItem from "./FilterResourcesByFoldersItem";
import {ResourceWorkspaceFilterTypes, withResourceWorkspace} from "../../../contexts/ResourceWorkspaceContext";
import {withRouter} from "react-router-dom";
import {withAppContext} from "../../../contexts/AppContext";
import FilterResourcesByRootFolderContextualMenu from "./FilterResourcesByRootFolderContextualMenu";
import {withContextualMenu} from "../../../contexts/ContextualMenuContext";
import PropTypes from "prop-types";
import {withDialog} from "../../../contexts/DialogContext";
import ReactList from "react-list";
import {Trans, withTranslation} from "react-i18next";
import {withDrag} from "../../../contexts/DragContext";
// Root virtual folder identifier.
const ROOT = null;
class FilterResourcesByFolders 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 {
draggingOverTitle: false,
draggingOverTitleSince: null,
open: true,
moreMenuOpen: false
};
}
/**
* Returns true if the component should be re-rendered
*/
shouldComponentUpdate(prevProps, prevState) {
return prevState !== this.state
|| prevProps.context.folders !== this.props.context.folders
|| prevProps.resourceWorkspaceContext.filter.type !== this.props.resourceWorkspaceContext.filter.type;
}
/**
* Bind callbacks methods
*/
bindCallbacks() {
this.handleCloseMoreMenu = this.handleCloseMoreMenu.bind(this);
this.handleClickOnTitle = this.handleClickOnTitle.bind(this);
this.handleTitleContextualMenuEvent = this.handleTitleContextualMenuEvent.bind(this);
this.handleDragLeaveTitle = this.handleDragLeaveTitle.bind(this);
this.handleDragOverTitle = this.handleDragOverTitle.bind(this);
this.handleDropTitle = this.handleDropTitle.bind(this);
this.handleTitleMoreClickEvent = this.handleTitleMoreClickEvent.bind(this);
}
/**
* Close the create menu
*/
handleCloseMoreMenu() {
this.setState({moreMenuOpen: false});
}
/**
* Handle when the user clicks on the section title.
*/
handleClickOnTitle() {
const filter = {type: ResourceWorkspaceFilterTypes.ROOT_FOLDER};
this.props.history.push(`/app/passwords`, {filter});
}
/**
* Returns true if the All Items shortcut is currently selected
*/
get isSelected() {
return this.props.resourceWorkspaceContext.filter.type === ResourceWorkspaceFilterTypes.ROOT_FOLDER;
}
/**
* Handle when the user requests to display the contextual menu on the root folder.
* @param {ReactEvent} event The event
*/
handleTitleContextualMenuEvent(event) {
// Prevent the browser contextual menu to pop up.
event.preventDefault();
const top = event.pageY;
const left = event.pageX;
const contextualMenuProps = {left, top};
this.props.contextualMenuContext.show(FilterResourcesByRootFolderContextualMenu, contextualMenuProps);
}
/**
* Handle when the user requests to display the contextual menu on the root folder.
* @param {ReactEvent} event The event
*/
handleTitleMoreClickEvent(event) {
const moreMenuOpen = !this.state.moreMenuOpen;
this.setState({moreMenuOpen});
if (moreMenuOpen) {
const {left, top} = event.currentTarget.getBoundingClientRect();
const onBeforeHide = this.handleCloseMoreMenu;
const contextualMenuProps = {left, top: top + 18, className: "right", onBeforeHide};
this.props.contextualMenuContext.show(FilterResourcesByRootFolderContextualMenu, contextualMenuProps);
}
}
/**
* Handle fold/unfold root folder icon click
*/
handleSectionTitleClickCaretEvent() {
const open = !this.state.open;
this.setState({open});
}
/**
* Handle when the user is not dragging over the section title anymore.
*/
handleDragLeaveTitle() {
const draggingOverTitle = false;
const draggingOverTitleSince = null;
this.setState({draggingOverTitle, draggingOverTitleSince});
}
/**
* Handle when the user is dragging content over the section title.
* @param {ReactEvent} event The event
*/
handleDragOverTitle(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.draggingOverTitle) {
this.openTitleOnLongDragOver();
return;
}
const draggingOverTitle = true;
const draggingOverTitleSince = Date.now();
this.setState({draggingOverTitle, draggingOverTitleSince});
}
/**
* Open the root folder when the user is dragging for a defined period on it.
* @param {ReactEvent} event The event
*/
openTitleOnLongDragOver() {
const period = 2000;
// If already open, leave.
if (!this.state.open) {
const now = Date.now();
if (now - this.state.draggingOverTitleSince > period) {
this.handleSectionTitleClickCaretEvent();
}
}
}
/**
* Handle when the user is dropping the content on the title.
*/
handleDropTitle() {
const folders = this.props.dragContext.draggedItems.folders.map(folder => folder.id);
const resources = this.props.dragContext.draggedItems.resources.map(resource => resource.id);
const folderParentId = null;
this.props.context.port.request("passbolt.folders.open-move-confirmation-dialog", {folders, resources, folderParentId});
// The dragLeave event is not fired when a drop is happening. Cancel the state manually.
const draggingOverTitle = false;
this.setState({draggingOverTitle});
}
/**
* 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 = canDragItems && draggedResources.reduce((accumulator, folder) => accumulator && this.canDragItem(folder), true);
return canDragItems;
}
/**
* Check if the component is loading.
* @returns {boolean}
*/
isLoading() {
return this.props.context.folders === null;
}
/**
* Sort a list of folders alphabetically
* @param {array} folders The list of folders to sort
*/
sortFoldersAlphabetically(folders) {
folders.sort((folderA, folderB) => {
const folderAName = folderA.name.toLowerCase();
const folderBName = folderB.name.toLowerCase();
if (folderAName < folderBName) {
return -1;
} else if (folderAName > folderBName) {
return 1;
}
return 0;
});
}
/**
* Get the folders that are at the root of the user.
* @returns {array}
*/
getRootFolders() {
let folders = [];
if (!this.isLoading()) {
folders = this.props.context.folders.filter(folder => folder.folder_parent_id === ROOT);
}
this.sortFoldersAlphabetically(folders);
return folders;
}
/**
* Check if the user is currently dragging content.
* @returns {boolean}
*/
isDragging() {
return this.props.dragContext.dragging;
}
/**
* return dragged items
* @returns {*}
*/
get draggedItems() {
return this.props.dragContext.draggedItems;
}
/**
* Render the component
* @returns {JSX}
*/
render() {
const isLoading = this.isLoading();
const isOpen = this.state.open;
const rootFolders = this.getRootFolders();
const isDragging = this.isDragging();
let showDropFocus = false;
let disabled = false;
if (isDragging && this.state.draggingOverTitle) {
const canDragItems = this.canDragItems(this.draggedItems);
if (canDragItems) {
showDropFocus = true;
} else {
// Disabled the drop area in order to avoid the drop event to be fired when the user cannot drop content on the root.
disabled = true;
}
}
return (
<div className="navigation-secondary-tree navigation-secondary navigation-folders accordion">
<div className="accordion-header">
<div className={`${isOpen ? "open" : "close"} node root`}>
<div className={`row title ${this.isSelected ? "selected" : ""} ${showDropFocus ? "drop-focus" : ""} ${disabled ? "disabled" : ""} ${this.state.moreMenuOpen ? "highlight" : ""}`}>
<div className="main-cell-wrapper">
<div className="main-cell">
<h3>
<span className="folders-label">
<a role="button"
onDragOver={this.handleDragOverTitle}
onDragLeave={this.handleDragLeaveTitle}
onDrop={this.handleDropTitle}
onClick={this.handleClickOnTitle}
onContextMenu={this.handleTitleContextualMenuEvent}>
{!isLoading &&
<div className="toggle-folder" onClick={() => this.handleSectionTitleClickCaretEvent()}>
{isOpen &&
<Icon name="caret-down"/>
}
{!isOpen &&
<Icon name="caret-right"/>
}
</div>
}
<Trans>Folders</Trans>
</a>
</span>
</h3>
</div>
</div>
<div className="dropdown right-cell more-ctrl">
<a className={`button ${this.state.moreMenuOpen ? "open" : ""}`} onClick={this.handleTitleMoreClickEvent}>
<Icon name="3-dots-h"/>
</a>
</div>
</div>
</div>
</div>
<div className="accordion-content">
{!isLoading && isOpen && rootFolders.length === 0 &&
<em className="empty-content"><Trans>empty</Trans></em>
}
{!isLoading && isOpen && rootFolders.length > 0 &&
<ReactList
itemRenderer={(index, key) => <FilterResourcesByFoldersItem
key={key}
folder={rootFolders[index]}/>
}
itemsRenderer={(items, ref) => <ul ref={ref} className="folders-tree">{items}</ul>}
length={rootFolders.length}
pageSize={20}
minSize={20}
type="simple">
</ReactList>
}
</div>
</div>
);
}
}
FilterResourcesByFolders.propTypes = {
context: PropTypes.any, // The app context
contextualMenuContext: PropTypes.any, // The contextual menu context
history: PropTypes.object,
resourceWorkspaceContext: PropTypes.object,
dialogContext: PropTypes.any,
dragContext: PropTypes.any,
};
export default withRouter(withDialog(withContextualMenu(withResourceWorkspace(withAppContext(withDrag(withTranslation("common")(FilterResourcesByFolders)))))));