UNPKG

passbolt-styleguide

Version:

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

348 lines (312 loc) 12.9 kB
import PropTypes from "prop-types"; import React from "react"; import {withRouter} from "react-router-dom"; import {Link} from "react-router-dom"; import {withAppContext} from "../../contexts/AppContext"; import {Trans, withTranslation} from "react-i18next"; import Icon from "../../../shared/components/Icons/Icon"; const BROWSED_RESOURCES_LIMIT = 500; const BROWSED_GROUPS_LIMIT = 500; class FilterResourcesByGroupPage extends React.Component { constructor(props) { super(props); this.state = this.initState(); this.initEventHandlers(); } 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 a group is selected, the component aims to display the resources shared with this group. * Load the resources */ if (this.props.match.params.id) { this.findAndLoadResources(); } else { // Otherwise list the groups the user is member of. this.findAndLoadGroups(); } } initEventHandlers() { this.handleGoBackClick = this.handleGoBackClick.bind(this); this.handleSelectGroupClick = this.handleSelectGroupClick.bind(this); this.handleSelectResourceClick = this.handleSelectResourceClick.bind(this); } initState() { let selectedGroup = null; // The selected group to use to filter the resources shared on is passed via the history.push state option. if (this.props.location.state && this.props.location.state.selectedGroup) { selectedGroup = this.props.location.state.selectedGroup; } return { selectedGroup: selectedGroup, groups: null, resources: null }; } /** * Get the translate function * @returns {function(...[*]=)} */ get translate() { return this.props.t; } 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(); } handleSelectGroupClick(ev, groupId) { ev.preventDefault(); this.props.context.searchHistory[this.props.location.pathname] = this.props.context.search; this.props.context.updateSearch(""); // Push the group as state of the component. const selectedGroup = this.state.groups.find(group => group.id === groupId); this.props.history.push(`/webAccessibleResources/quickaccess/resources/group/${groupId}`, {selectedGroup}); } 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}`); } async findAndLoadGroups() { const filters = {'has-users': [this.props.context.userSettings.id]}; const groups = await this.props.context.port.request("passbolt.groups.find-all", {filters}); this.sortGroupsAlphabetically(groups); this.setState({groups}); } async findAndLoadResources() { const filters = {'is-shared-with-group': this.props.match.params.id}; const resources = await this.props.context.port.request('passbolt.resources.find-all', {filters}); this.sortResourcesAlphabetically(resources); this.setState({resources}); } sortGroupsAlphabetically(groups) { groups.sort((group1, group2) => { const group1Name = group1.name.toUpperCase(); const group2Name = group2.name.toUpperCase(); if (group1Name > group2Name) { return 1; } else if (group2Name > group1Name) { return -1; } return 0; }); } sortResourcesAlphabetically(resources) { resources.sort((resource1, resource2) => { const resource1Name = resource1.name.toUpperCase(); const resource2Name = resource2.name.toUpperCase(); if (resource1Name > resource2Name) { return 1; } else if (resource2Name > resource1Name) { return -1; } return 0; }); } /** * Get the groups to display * @return {array} The list of groups. */ getBrowsedGroups() { let groups = this.state.groups; if (this.props.context.search.length) { /* * @todo optimization. Memoize result to avoid filtering each time the component is rendered. * @see reactjs doc https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#what-about-memoization */ groups = this.filterGroupsBySearch(groups, this.props.context.search); } return groups.slice(0, BROWSED_GROUPS_LIMIT); } /** * Filter groups by keywords. * Search on the name * @param {array} groups The list of groups to filter. * @param {string} needle The needle to search. * @return {array} The filtered groups. */ filterGroupsBySearch(groups, needle) { // 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(this.escapeRegExp(needle), 'i')); return groups.filter(group => { 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); } return match; }); } /** * Get the resources to display * @return {array} The list of resources. */ getBrowsedResources() { let resources = this.state.resources; if (this.props.context.search.length) { /* * @todo optimization. Memoize result to avoid filtering each time the component is rendered. * @see reactjs doc https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#what-about-memoization */ resources = this.filterResourcesBySearch(resources, this.props.context.search); } return resources.slice(0, BROWSED_RESOURCES_LIMIT); } /** * Filter resources by keywords. * Search on the name, the username, the uri and the description of the resources. * @param {array} resources The list of resources to filter. * @param {string} needle The needle to search. * @return {array} The filtered resources. */ filterResourcesBySearch(resources, needle) { // 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(this.escapeRegExp(needle), 'i')); return resources.filter(resource => { 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(resource.name) || regexes[i].test(resource.username) || regexes[i].test(resource.uri) || regexes[i].test(resource.description)); } return match; }); } /** * Escape a string that is to be treated as a literal string within a regular expression. * Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Using_special_characters * @param {string} value The string to escape */ escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } isReady() { return this.state.groups !== null || this.state.resources !== null; } render() { const isReady = this.isReady(); const isSearching = this.props.context.search.length > 0; const listGroupsOnly = this.state.selectedGroup === null; let browsedGroups, browsedResources; if (isReady) { if (listGroupsOnly) { browsedGroups = this.getBrowsedGroups(); } else { browsedResources = this.getBrowsedResources(); } } return ( <div className="index-list"> <div className="back-link"> <a href="#" className="primary-action" onClick={this.handleGoBackClick} title={this.translate("Go back")}> <Icon name="chevron-left"/> <span className="primary-action-title"> {this.state.selectedGroup && this.state.selectedGroup.name || <Trans>Groups</Trans>} </span> </a> <Link to="/webAccessibleResources/quickaccess.html" className="secondary-action button-transparent button" title={this.translate("Cancel")}> <Icon name="close"/> <span className="visually-hidden"><Trans>Cancel</Trans></span> </Link> </div> <div className="list-container"> <ul className="list-items"> {!isReady && <li className="empty-entry"> <Icon name="spinner"/> <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.id)}> <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.name}</span> <span className="username"> {resource.username ? `(${resource.username})` : ""}</span> </div> <span className="url">{resource.uri}</span> </div> </a> </li> )} </React.Fragment> } </React.Fragment> } </ul> </div> <div className="submit-wrapper"> <Link to="/webAccessibleResources/quickaccess/resources/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 // Match, location and history props are injected by the withRouter decoration call. match: PropTypes.object, location: PropTypes.object, history: PropTypes.object, t: PropTypes.func, // The translation function }; export default withAppContext(withRouter(withTranslation('common')(FilterResourcesByGroupPage)));