UNPKG

passbolt-styleguide

Version:

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

426 lines (396 loc) 16.2 kB
import PropTypes from "prop-types"; import React from "react"; import { withRouter, Link } from "react-router-dom"; import { Trans, withTranslation } from "react-i18next"; import SpinnerSVG from "../../../img/svg/spinner.svg"; import { withAppContext } from "../../../shared/context/AppContext/AppContext"; import { sortResourcesAlphabetically } from "../../../shared/utils/sortUtils"; import { escapeRegExp, filterResourcesBySearch } from "../../../shared/utils/filterUtils"; import memoize from "memoize-one"; import { withResourcesLocalStorage } from "../../contexts/ResourceLocalStorageContext"; import { withMetadataTypesSettingsLocalStorage } from "../../../shared/context/MetadataTypesSettingsLocalStorageContext/MetadataTypesSettingsLocalStorageContext"; import { withResourceTypesLocalStorage } from "../../../shared/context/ResourceTypesLocalStorageContext/ResourceTypesLocalStorageContext"; import ResourceTypesCollection from "../../../shared/models/entity/resourceType/resourceTypesCollection"; import MetadataTypesSettingsEntity from "../../../shared/models/entity/metadata/metadataTypesSettingsEntity"; import { RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION_SLUG, RESOURCE_TYPE_V5_DEFAULT_SLUG, } from "../../../shared/models/entity/resourceType/resourceTypeSchemasDefinition"; import DisplayResourceUrisBadge from "../../../react-extension/components/Resource/DisplayResourceUrisBadge/DisplayResourceUrisBadge"; import CaretLeftSVG from "../../../img/svg/caret_left.svg"; import CloseSVG from "../../../img/svg/close.svg"; import { withMetadataKeysSettingsLocalStorage } from "../../../shared/context/MetadataKeysSettingsLocalStorageContext/MetadataKeysSettingsLocalStorageContext"; import MetadataKeysSettingsEntity from "../../../shared/models/entity/metadata/metadataKeysSettingsEntity"; import GroupServiceWorkerService from "../../../shared/services/serviceWorker/group/groupServiceWorkerService"; const BROWSED_RESOURCES_LIMIT = 500; const BROWSED_GROUPS_LIMIT = 500; class FilterResourcesByGroupPage extends React.Component { /** * @inheritDoc */ constructor(props) { super(props); this.state = this.defaultState; this.initEventHandlers(); this.groupServiceWorkerService = new GroupServiceWorkerService(props.context.port); } /** * ComponentDidMount hook. * Invoked immediately after component is inserted into the tree */ componentDidMount() { this.props.context.focusSearch(); if (this.props.context.searchHistory[this.props.location.pathname]) { this.props.context.updateSearch(this.props.context.searchHistory[this.props.location.pathname]); } if (this.props.location?.state?.selectedGroup) { this.findAndLoadGroupResourceIds(); } else { this.findAndLoadGroups(); } } /** * Returns the component default state * @return {object} */ get defaultState() { return { groups: null, groupResourceIds: null, }; } /** * Initializes event handlers */ initEventHandlers() { this.handleGoBackClick = this.handleGoBackClick.bind(this); this.handleSelectGroupClick = this.handleSelectGroupClick.bind(this); this.handleSelectResourceClick = this.handleSelectResourceClick.bind(this); } /** * Get the translate function * @returns {function(...[*]=)} */ get translate() { return this.props.t; } /** * Handles the click event on the "Go back" button. * @param {Event} ev */ handleGoBackClick(ev) { ev.preventDefault(); // Clean the search and remove the search history related to this page. this.props.context.updateSearch(""); delete this.props.context.searchHistory[this.props.location.pathname]; this.props.history.goBack(); } /** * Handles the click event on a group from the list. * @param {Event} ev * @param {Object} selectedGroup */ handleSelectGroupClick(ev, selectedGroup) { ev.preventDefault(); this.props.context.searchHistory[this.props.location.pathname] = this.props.context.search; this.props.context.updateSearch(""); this.props.history.push(`/webAccessibleResources/quickaccess/resources/group/${selectedGroup.id}`, { selectedGroup, }); } /** * Handles the click event on a resource from the list. * @param {Event} ev * @param {string} resourceId */ handleSelectResourceClick(ev, resourceId) { ev.preventDefault(); /* * Add a search history for the current page. * It will allow the page to restore the search when the user will come back after clicking goBack (caveat, the workflow is not this one). * By instance when you select a group that you have filtered you expect the page to be filtered as when you left it. */ this.props.context.searchHistory[this.props.location.pathname] = this.props.context.search; this.props.context.updateSearch(""); this.props.history.push(`/webAccessibleResources/quickaccess/resources/view/${resourceId}`); } /** * Find and load groups. * @returns {Promise<void>} */ async findAndLoadGroups() { const groups = await this.groupServiceWorkerService.findMyGroups(); this.sortGroupsAlphabetically(groups); this.setState({ groups }); } /** * Find and load group resource ids. * @returns {Promise<void>} */ async findAndLoadGroupResourceIds() { const groupResourceIds = await this.props.context.port.request( "passbolt.resources.find-all-ids-by-is-shared-with-group", this.props.location.state.selectedGroup.id, ); this.setState({ groupResourceIds }); } /** * Find resources by ids. * @param {array} resources The list of resources to filter. * @param {array} ids The list of ids to filter on. * @return {Array<Object>} The list of resources filtered. */ filterResourcesByIds = memoize((resources, ids) => { const groupResources = resources.filter((resource) => ids.includes(resource.id)); sortResourcesAlphabetically(groupResources); return groupResources; }); /** * Sort an array of groups alphabetically * @param {Array} groups The array of group to filter. */ sortGroupsAlphabetically(groups) { groups.sort((group1, group2) => group1.name.localeCompare(group2.name, undefined, { sensitivity: "base" })); } /** * Filter groups by keywords. * Search on the name * @param {array} groups The list of groups to filter. * @param {string} needle The needle to search. * @param {number} [limit = Number.MAX_SAFE_INTEGER] the count limit of results. * @return {array} The filtered groups. */ filterGroupsBySearch = memoize((groups, needle, limit = Number.MAX_SAFE_INTEGER) => { // Split the search by words const needles = needle.split(/\s+/); // Prepare the regexes for each word contained in the search. const regexes = needles.map((needle) => new RegExp(escapeRegExp(needle), "i")); let filterCount = 0; return groups.filter((group) => { if (filterCount >= limit) { return false; } let match = true; for (const i in regexes) { // To match a resource would have to match all the words of the search. match &= regexes[i].test(group.name); } filterCount++; return match; }); }); /** * Get the resources to display * @param {Array<Object>} resources the resource list to filter from * @param {Array} groupResourceIds The list of resources shared with the group * @param {string} search the keyword to search for in the list if any * @return {Array<Object>} The list of resources. */ filterSearchedResources = memoize((resources, groupResourceIds, search) => { const groupResources = this.filterResourcesByIds(resources, groupResourceIds); return search ? filterResourcesBySearch(groupResources, search, BROWSED_RESOURCES_LIMIT) : groupResources.slice(0, BROWSED_RESOURCES_LIMIT); }); /** * Get the groups to display * @param {Array<Object>} groups the group list to filter from * @param {string} search the keyword to search for in the list if any * @return {Array<Object>} The list of resources. */ filterSearchedGroups = memoize((groups, search) => search ? this.filterGroupsBySearch(groups, search, BROWSED_GROUPS_LIMIT) : groups.slice(0, BROWSED_GROUPS_LIMIT), ); /** * Has metadata types settings * @returns {boolean} */ hasMetadataTypesSettings() { return Boolean(this.props.metadataTypeSettings); } /** * Can create password * @returns {boolean} */ canCreatePassword() { if (this.props.metadataTypeSettings.isDefaultResourceTypeV5) { return this.props.resourceTypes?.hasOneWithSlug(RESOURCE_TYPE_V5_DEFAULT_SLUG); } else if (this.props.metadataTypeSettings.isDefaultResourceTypeV4) { return this.props.resourceTypes?.hasOneWithSlug(RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION_SLUG); } else { return false; } } /** * User has missing keys * @return {boolean} */ get userHasMissingKeys() { return this.props.context.loggedInUser.missing_metadata_key_ids?.length > 0; } /** * Should display action aborted missing metadata keys * @return {boolean} */ get shouldDisplayActionAbortedMissingMetadataKeys() { return ( this.props.metadataTypeSettings.isDefaultResourceTypeV5 && this.userHasMissingKeys && !this.props.metadataKeysSettings?.allowUsageOfPersonalKeys ); } render() { const isSearching = this.props.context.search.length > 0; const listGroupsOnly = !this.props.location?.state?.selectedGroup; let isReady, browsedGroups, browsedResources; if (listGroupsOnly) { isReady = this.state.groups !== null; if (isReady) { browsedGroups = this.filterSearchedGroups(this.state.groups, this.props.context.search); } } else { isReady = this.props.resources !== null && this.state.groupResourceIds !== null; if (isReady) { browsedResources = this.filterSearchedResources( this.props.resources, this.state.groupResourceIds, this.props.context.search, ); } } return ( <div className="index-list"> <div className="back-link"> <a href="#" className="primary-action" onClick={this.handleGoBackClick} title={this.translate("Go back")}> <CaretLeftSVG /> <span className="primary-action-title"> {(this.state.selectedGroup && this.state.selectedGroup.name) || <Trans>Groups</Trans>} </span> </a> <Link to="/webAccessibleResources/quickaccess/home" className="secondary-action button-transparent button" title={this.translate("Cancel")} > <CloseSVG className="close" /> <span className="visually-hidden"> <Trans>Cancel</Trans> </span> </Link> </div> <div className="list-container"> <ul className="list-items"> {!isReady && ( <li className="empty-entry"> <SpinnerSVG /> <p className="processing-text"> {listGroupsOnly ? <Trans>Retrieving your groups</Trans> : <Trans>Retrieving your passwords</Trans>} </p> </li> )} {isReady && ( <React.Fragment> {listGroupsOnly && ( <React.Fragment> {!browsedGroups.length && ( <li className="empty-entry"> <p> {isSearching && <Trans>No result match your search. Try with another search term.</Trans>} {!isSearching && ( <Trans> You are not member of any group. Wait for a group manager to add you in a group. </Trans> )} </p> </li> )} {browsedGroups.length > 0 && browsedGroups.map((group) => ( <li key={group.id} className="filter-entry"> <a href="#" onClick={(ev) => this.handleSelectGroupClick(ev, group)}> <span className="filter">{group.name}</span> </a> </li> ))} </React.Fragment> )} {!listGroupsOnly && ( <React.Fragment> {!browsedResources.length && ( <li className="empty-entry"> <p> {isSearching && <Trans>No result match your search. Try with another search term.</Trans>} {!isSearching && ( <Trans> No passwords are shared with this group yet. Share a password with this group or wait for a team member to share one with this group. </Trans> )} </p> </li> )} {browsedResources?.length > 0 && browsedResources.map((resource) => ( <li className="browse-resource-entry" key={resource.id}> <a href="#" onClick={(ev) => this.handleSelectResourceClick(ev, resource.id)}> <div className="inline-resource-entry"> <div className="inline-resource-name"> <span className="title">{resource.metadata.name}</span> <span className="username"> {" "} {resource.metadata.username ? `(${resource.metadata.username})` : ""} </span> </div> <div className="uris"> <span className="url">{resource.metadata.uris?.[0]}</span> {resource.metadata.uris?.length > 1 && ( <DisplayResourceUrisBadge additionalUris={resource.metadata.uris?.slice(1)} /> )} </div> </div> </a> </li> ))} </React.Fragment> )} </React.Fragment> )} </ul> </div> {this.hasMetadataTypesSettings() && this.canCreatePassword() && ( <div className="submit-wrapper"> <Link to={`/webAccessibleResources/quickaccess/resources/${this.shouldDisplayActionAbortedMissingMetadataKeys ? "action-aborted-missing-metadata-keys" : "create"}`} id="popupAction" className="button primary big full-width" role="button" > <Trans>Create new</Trans> </Link> </div> )} </div> ); } } FilterResourcesByGroupPage.propTypes = { context: PropTypes.any, // The application context resourceTypes: PropTypes.instanceOf(ResourceTypesCollection), // The resource types collection metadataTypeSettings: PropTypes.instanceOf(MetadataTypesSettingsEntity), // The metadata type settings metadataKeysSettings: PropTypes.instanceOf(MetadataKeysSettingsEntity), // The metadata key settings location: PropTypes.object, history: PropTypes.object, resources: PropTypes.array, t: PropTypes.func, // The translation function }; export default withAppContext( withRouter( withResourceTypesLocalStorage( withResourcesLocalStorage( withMetadataTypesSettingsLocalStorage( withMetadataKeysSettingsLocalStorage(withTranslation("common")(FilterResourcesByGroupPage)), ), ), ), ), );