UNPKG

passbolt-styleguide

Version:

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

444 lines (417 loc) 18.4 kB
/** * Passbolt ~ Open source password manager for teams * Copyright (c) 2020 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) 2020 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, { Component } from "react"; import DisplayUserBadgeMenu from "../../User/DisplayUserBadgeMenu/DisplayUserBadgeMenu"; import FilterResourcesByFolders from "../FilterResourcesByFolders/FilterResourcesByFolders"; import DisplayResourceFolderDetails from "../../ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetails"; import DisplayResourceDetails from "../../ResourceDetails/DisplayResourceDetails/DisplayResourceDetails"; import { withAppContext } from "../../../../shared/context/AppContext/AppContext"; import { withResourceWorkspace } from "../../../contexts/ResourceWorkspaceContext"; import FilterResourcesByTags from "../FilterResourcesByTags/FilterResourcesByTags"; import PropTypes from "prop-types"; import FilterResourcesByText from "../FilterResourcesByText/FilterResourcesByText"; import FilterResourcesByShortcuts from "../FilterResourcesByShortcuts/FilterResourcesByShortcuts"; import FilterResourcesByBreadcrumb from "../FilterResourcesByBreadcrumb/FilterResourcesByBreadcrumb"; import DisplayResourcesWorkspaceMenu from "./DisplayResourcesWorkspaceMenu"; import Logo from "../../Common/Navigation/Header/Logo"; import DisplayResourcesWorkspaceMainMenu from "./DisplayResourcesWorkspaceMainMenu"; import { Trans, withTranslation } from "react-i18next"; import FilterResourcesByGroups from "../FilterResourcesByGroups/FilterResourcesByGroups"; import DisplayResourcesList from "../DisplayResourcesList/DisplayResourcesList"; import { withRbac } from "../../../../shared/context/Rbac/RbacContext"; import { uiActions } from "../../../../shared/services/rbacs/uiActionEnumeration"; import Dropdown from "../../Common/Dropdown/Dropdown"; import DropdownButton from "../../Common/Dropdown/DropdownButton"; import DropdownMenu from "../../Common/Dropdown/DropdownMenu"; import DropdownMenuItem from "../../Common/Dropdown/DropdownMenuItem"; import ColumnsSVG from "../../../../img/svg/columns.svg"; import CaretDownSVG from "../../../../img/svg/caret_down.svg"; import InfoSVG from "../../../../img/svg/info.svg"; import WorkspaceSwitcher, { WORKSPACE_ENUM } from "../../Common/Navigation/WorkspaceSwitcher/WorkspaceSwitcher"; import RoleEntity from "../../../../shared/models/entity/role/roleEntity"; import DisplayResourcesWorkspaceFilters from "./DisplayResourcesWorkspaceFilters"; import Footer from "../../Common/Footer/Footer"; import DisplayEmptyDetails from "../../ResourceFolderDetails/DisplayResourceFolderDetails/DisplayEmptyDetails"; import DisplayResourcesListDetails from "../../ResourceDetails/DisplayResourceDetails/DisplayResourcesListDetails"; import debounce from "debounce-promise"; import RevertSVG from "../../../../img/svg/revert.svg"; import { ColumnModelTypes } from "../../../../shared/models/column/ColumnModel"; import { ROW_SETTING_HEIGHT_COMFORTABLE, ROW_SETTING_HEIGHT_COMPACT, } from "../../../../shared/models/entity/rowsSetting/rowsSettingEntity"; import ResizableSidebar from "../../ResizableSidebar/ResizableSidebar"; import { withResizableSidebar } from "../../../contexts/ResizeSidebar/ResizeSidebarContext"; import ManageAnnouncements from "../../Announcement/ManageAnnouncements/ManageAnnouncements"; const GAP_AND_PADDING_BUTTONS = 22; class Workspace extends Component { /** * Constructor * @param {Object} props */ constructor(props) { super(props); this.bindCallbacks(); this.createRefs(); window.addEventListener("resize", this.handleWindowResizeEventDebounced); } /** * Bind callbacks methods */ bindCallbacks() { this.handleOnChangeColumnView = this.handleOnChangeColumnView.bind(this); this.handleViewDetailClickEvent = this.handleViewDetailClickEvent.bind(this); this.handleWindowResizeEventDebounced = debounce(this.handleWindowResizeEvent.bind(this), 50); this.handleOnResetColumnsSettings = this.handleOnResetColumnsSettings.bind(this); this.handleOnChangeRowsSetting = this.handleOnChangeRowsSetting.bind(this); } /** * Create DOM nodes or React elements references in order to be able to access them programmatically. */ createRefs() { this.actionsBar = React.createRef(); this.actionsFilter = React.createRef(); this.actionsButton = React.createRef(); this.actionsSecondary = React.createRef(); } /** * ComponentDidMount */ componentDidMount() { this.displayOnlyButtonIconsIfButtonsOverlay(); } /** * ComponentDidUpdate */ componentDidUpdate() { this.displayOnlyButtonIconsIfButtonsOverlay(); } /** * Handle window resize event */ handleWindowResizeEvent() { this.displayOnlyButtonIconsIfButtonsOverlay(); } /** * calculate the width pf buttons, * if overlay the container, add classname to display only icons, else do nothing */ displayOnlyButtonIconsIfButtonsOverlay() { // Remove the class name to calculate with default width (text) this.actionsBar.current?.classList.remove("icon-only"); // Get elements width const offsetWidthActionBar = this.actionsBar.current?.offsetWidth; const offsetActionsButton = this.actionsFilter.current ? this.actionsFilter.current.offsetWidth : this.actionsButton.current?.offsetWidth; const offsetActionsSecondary = this.actionsSecondary.current?.offsetWidth; // Check if the default width overlay the container to show only icons if (offsetActionsButton + offsetActionsSecondary + GAP_AND_PADDING_BUTTONS > offsetWidthActionBar) { this.actionsBar.current.classList.add("icon-only"); } } /** * Has lock for the detail display * @returns {boolean} */ hasLockDetail() { return this.props.resourceWorkspaceContext.lockDisplayDetail; } /** * Get the columns list of resource * @return {[Object]} */ get columnsResourceSetting() { return this.props.resourceWorkspaceContext.columnsResourceSetting?.items; } /** * Handle the event on when the column view is changing. * @param {React.Event} event */ handleOnChangeColumnView(event) { const target = event.target; this.props.resourceWorkspaceContext.onChangeColumnView(target.id, target.checked); } /** * Handle the event on when the rows setings is changing. * @param {React.Event} event */ handleOnChangeRowsSetting(event) { const target = event.target; this.props.resourceWorkspaceContext.onChangeRowSettingsHeight(target.value); } /** * Handle the event when users resets the columns settings. * @return {Promise} */ async handleOnResetColumnsSettings() { await this.props.resourceWorkspaceContext.resetGridColumnsSettings(); } /** * handle view detail click event */ handleViewDetailClickEvent() { // lock or unlock the detail resource or folder this.props.resourceWorkspaceContext.onLockDetail(); } /** * Returns true if the current user is an admin. * @returns {boolean} */ get isUserAdmin() { const loggedInUser = this.props.context.loggedInUser; return ( loggedInUser?.role?.name === RoleEntity.ROLE_ADMIN && this.props.rbacContext.canIUseAction(uiActions.ADMINSTRATION_VIEW_WORKSPACE) ); } /** * Returns true if the current user is an admin. * @returns {boolean} */ get isUserWorkspaceVisible() { return this.props.rbacContext.canIUseAction(uiActions.USERS_VIEW_WORKSPACE); } /** * Returns true if there is currently if nothing is selected in the workspace. * @returns {boolean} */ get shouldDisplayEmptyDetails() { return ( !this.props.resourceWorkspaceContext.details.folder && !this.props.resourceWorkspaceContext.details.resource && this.props.resourceWorkspaceContext.selectedResources.length === 0 ); } /** * Returns true if multiple resources are currently selected. * @returns {boolean} */ get shouldDisplayListDetails() { return this.props.resourceWorkspaceContext.selectedResources.length > 1; } /** * Returns true if a separator should be displayed: * - last item of the list * - URI item if there is no expired item * - expired item * @param {columnsResourceSetting} column * @param {number} index * @returns {boolean} */ hasSeparator(column, index) { const nextColumn = this.props.resourceWorkspaceContext.columnsResourceSetting.items[index + 1]; return ( index === this.columnsResourceSetting.length - 1 || (column.id === ColumnModelTypes.URI && nextColumn?.id !== ColumnModelTypes.EXPIRED) || column.id === ColumnModelTypes.EXPIRED ); } /** * Render the component * @return {JSX} */ render() { const canUseFolders = this.props.context.siteSettings.canIUse("folders") && this.props.rbacContext.canIUseAction(uiActions.FOLDERS_USE); const canUseTags = this.props.context.siteSettings.canIUse("tags") && this.props.rbacContext.canIUseAction(uiActions.TAGS_USE); const rowsSetting = this.props.resourceWorkspaceContext.rowsSetting; /** Calculate the width of the header-right to align with the right sidebar resizing */ const { right, containerRef } = this.props.sidebarContext || {}; const containerWidth = containerRef?.current?.offsetWidth || 1; const rightHeaderWidth = (right?.width / containerWidth) * 100 < 25 ? `25%` : `${(right?.width / containerWidth) * 100}%`; // set width of right header return ( <> <ManageAnnouncements /> <div className="panel main"> <ResizableSidebar resizable gutterLeft={false} minWidth={"18%"} maxWidth={"23.4%"} classNames={"resource-workspace left-side-bar"} > <div className="panel left resource-filter"> <div className="sidebar-content"> <Logo /> <div className="main-action-wrapper"> <DisplayResourcesWorkspaceMainMenu /> </div> <div className="sidebar-content-left"> <FilterResourcesByShortcuts /> {canUseFolders && <FilterResourcesByFolders />} <FilterResourcesByGroups /> {canUseTags && <FilterResourcesByTags />} </div> </div> </div> </ResizableSidebar> <div className="panel middle"> <div className="header"> <div className="header-left"> <FilterResourcesByText placeholder={this.props.t("Search resource")} /> </div> <div className="header-right" style={{ width: rightHeaderWidth }}> <WorkspaceSwitcher isUserAdmin={this.isUserAdmin} isUserWorkspaceVisible={this.isUserWorkspaceVisible} currentWorkspace={WORKSPACE_ENUM.RESOURCE} /> <DisplayUserBadgeMenu baseUrl={this.props.context.userSettings.getTrustedDomain()} user={this.props.context.loggedInUser} /> </div> </div> <div className="middle-right"> <div className="breadcrumbs-and-grid"> <div className="top-bar"> <FilterResourcesByBreadcrumb /> <div className="action-bar" ref={this.actionsBar}> {this.props.resourceWorkspaceContext.selectedResources?.length > 0 ? ( <DisplayResourcesWorkspaceMenu actionsButtonRef={this.actionsButton} /> ) : ( <DisplayResourcesWorkspaceFilters actionsFilterRef={this.actionsFilter} /> )} <div className="actions-secondary" ref={this.actionsSecondary}> <Dropdown> <DropdownButton> <ColumnsSVG /> <span> <Trans>Columns</Trans> </span> <CaretDownSVG /> </DropdownButton> <DropdownMenu direction="left"> <DropdownMenuItem keepOpenOnClick={true}> <div className="radiolist"> <div className="input radio"> <input type="radio" checked={!rowsSetting?.height || rowsSetting?.height === ROW_SETTING_HEIGHT_COMPACT} value={ROW_SETTING_HEIGHT_COMPACT} id="rows_setting.height.compact" name="rows_setting.height" onChange={this.handleOnChangeRowsSetting} /> <label htmlFor="rows_setting.height.compact"> <Trans>Compact</Trans> </label> </div> </div> </DropdownMenuItem> <DropdownMenuItem keepOpenOnClick={true} separator={true}> <div className="radiolist"> <div className="input radio"> <input type="radio" checked={rowsSetting?.height === ROW_SETTING_HEIGHT_COMFORTABLE} value={ROW_SETTING_HEIGHT_COMFORTABLE} id="rows_setting.height.comfortable" name="rows_setting.height" onChange={this.handleOnChangeRowsSetting} /> <label htmlFor="rows_setting.height.comfortable"> <Trans>Comfortable</Trans> </label> </div> </div> </DropdownMenuItem> {this.columnsResourceSetting?.map((column, index) => ( <DropdownMenuItem keepOpenOnClick={true} key={column.id} separator={this.hasSeparator(column, index)} > <div className="input checkbox"> <input type="checkbox" checked={column.show} id={column.id} name={column.id} onChange={this.handleOnChangeColumnView} /> <label htmlFor={column.id}> <Trans>{column.label}</Trans> </label> </div> </DropdownMenuItem> ))} <DropdownMenuItem> <div className="action-button"> <button id="reset-columns-settings" type="button" onClick={this.handleOnResetColumnsSettings} > <RevertSVG /> <span> <Trans>Reset columns</Trans> </span> </button> </div> </DropdownMenuItem> </DropdownMenu> </Dropdown> <button type="button" className={`button-toggle button-action button-action-icon info ${this.hasLockDetail() ? "active" : ""}`} onClick={this.handleViewDetailClickEvent} > <InfoSVG /> <span className="visuallyhidden"> <Trans>View detail</Trans> </span> </button> </div> </div> </div> <DisplayResourcesList /> </div> {this.hasLockDetail() && ( <ResizableSidebar resizable gutterLeft={true} minWidth={"25%"} maxWidth={"35%"} classNames={"resource-workspace right-side-bar"} > <div className="panel aside"> {this.shouldDisplayListDetails && <DisplayResourcesListDetails />} {this.shouldDisplayEmptyDetails && <DisplayEmptyDetails />} {this.props.resourceWorkspaceContext.details.folder && <DisplayResourceFolderDetails />} {this.props.resourceWorkspaceContext.details.resource && <DisplayResourceDetails />} <Footer /> </div> </ResizableSidebar> )} </div> </div> </div> </> ); } } Workspace.propTypes = { context: PropTypes.any, // The application context rbacContext: PropTypes.any, // The rbac context resourceWorkspaceContext: PropTypes.any, t: PropTypes.func, // The translation function sidebarContext: PropTypes.any, }; export default withAppContext( withRbac(withResourceWorkspace(withResizableSidebar(withTranslation("common")(Workspace)))), );