UNPKG

passbolt-styleguide

Version:

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

1,078 lines (973 loc) 38.9 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 * as React from "react"; import PropTypes from "prop-types"; import {withAppContext} from "./AppContext"; import {withRouter} from "react-router-dom"; import {withActionFeedback} from "./ActionFeedbackContext"; import {withLoading} from "./LoadingContext"; import sanitizeUrl, {urlProtocols} from "../lib/Sanitize/sanitizeUrl"; import {DateTime} from "luxon"; import debounce from "debounce-promise"; /** * Context related to resources ( filter, current selections, etc.) */ export const ResourceWorkspaceContext = React.createContext({ filter: { type: null, // Filter type payload: null // Filter payload }, sorter: { propertyName: 'modified', // The name of the property to sort on asc: false // True if the sort must be descendant }, filteredResources: [], // The current list of filtered resources selectedResources: [], // The current list of selected resources details: { resource: null, // The resource to focus details on folder: null, // The folder to focus details on }, scrollTo: { resource: null // The resource to scroll to }, refresh: { permissions: false // Flag to force the refresh of the permissions }, resourceFileToImport: null, // The resource file to import resourceFileImportResult: null, // The resource file import result lockDisplayDetail: true, // lock the detail to display the folder or password sidebar resourcesToExport: { resourcesIds: null, // The resources ids to export foldersIds: null // The folders ids to export }, onLockDetail: () => {}, // Lock or unlock detail (hide or display the folder or password sidebar) onResourceScrolled: () => {}, // Whenever one scrolled to a resource onResourceEdited: () => {}, // Whenever a resource descript has been edited onResourceDescriptionEdited: () => {}, // Whenever a resource description has been edited onResourceDescriptionDecrypted: () => {}, // Whenever a resource description area has been descripted onResourceShared: () => {}, // Whenever a resource is shared onResourcePermissionsRefreshed: () => {}, // Whenever the resource permissions have been refreshed onResourceCopied: () => {}, // Whenever a resource (password) has been copied onResourceActivitiesRefreshed: () => {}, // Whenever the resource activities have been refreshed onSorterChanged: () => {}, // Whenever the sorter changed onResourceSelected: { all: () => {}, // Whenever all the resources have been selected none: () => {}, // Whenever none resources have been selected multiple: () => {}, // Whenever a resource has been selected in a multiple mode range: () => {}, // Whenever a resource has been selected in a multiple mode single: () => {}// Whenever a single resource has been selected }, onResourceFileToImport: () => {}, // Whenever a resource file will be imported onResourceFileImportResult: () => {}, // Whenever the import result has been provided onResourcesToExport: () => {}, // Whenever resources and/or folder will be exported onGoToResourceUriRequested: () => {}, // Whenever the users wants to follow a resource uri }); /** * The related context provider */ export class ResourceWorkspaceContextProvider extends React.Component { /** * Default constructor * @param props The component props */ constructor(props) { super(props); this.state = this.defaultState; this.initializeProperties(); /* * Execute first request to refresh users, groups, etc. then wait 10sec to trigger next one * E.g. Only perform two populate max per 10 sec */ this.populateDebounced = debounce(this.populate, 10000, {leading: true, accumulate: false}); } /** * Returns the default component state */ get defaultState() { return { filter: {type: ResourceWorkspaceFilterTypes.NONE}, // The current resource search filter sorter: { propertyName: 'modified', // The name of the property to sort on asc: false // True if the sort must be descendant }, filteredResources: [], // The current list of filtered resources selectedResources: [], // The current list of selected resources details: { resource: null, // The resource to focus details on folder: null, // The folder to focus details on }, scrollTo: { resource: null // The resource to scroll to }, refresh: { activities: false, // Flag to force the refresh of the activities permissions: false // Flag to force the refresh of the permissions }, resourceFileToImport: null, // The resource file to import resourceFileImportResult: null, // The resource file import result lockDisplayDetail: true, // lock the detail to display the folder or password sidebar resourcesToExport: null, // The resources / folders to export onLockDetail: this.handleLockDetail.bind(this), // Lock or unlock detail (hide or display the folder or password sidebar) onResourceScrolled: this.handleResourceScrolled.bind(this), // Whenever one scrolled to a resource onResourceEdited: this.handleResourceEdited.bind(this), // Whenever a resource descript has been edited onResourceDescriptionEdited: this.handleResourceDescriptionEdited.bind(this), // Whenever a resource description has been edited onResourceDescriptionDecrypted: this.handleResourceDescriptionDecryted.bind(this), // Whenever a resource description has been decrypted onResourceShared: this.handleResourceShared.bind(this), // Whenever a resource is shared onResourcePermissionsRefreshed: this.handleResourcePermissionsRefreshed.bind(this), // Whenever the resource permissions have been refreshed onResourceCopied: this.handleResourceCopied.bind(this), // Whenever a resource (password) has been copied onResourceActivitiesRefreshed: this.handleResourceActivitiesRefreshed.bind(this), // Whenever the resource activities have been refreshed onSorterChanged: this.handleSorterChange.bind(this), // Whenever the sorter changed onResourceSelected: { all: this.handleAllResourcesSelected.bind(this), // Whenever all the resources have been selected none: this.handleNoneResourcesSelected.bind(this), // Whenever none resources have been selected multiple: this.handleMultipleResourcesSelected.bind(this), // Whenever a resource has been selected in a multiple mode range: this.handleResourceRangeSelected.bind(this), // Whenever a resource has been selected in a multiple mode single: this.handleResourceSelected.bind(this)// Whenever a single resource has been selected }, onResourceFileToImport: this.handleResourceFileToImport.bind(this), // Whenever a resource file will be imported onResourceFileImportResult: this.handleResourceFileImportResult.bind(this), // Whenever the import result has been provided onResourcesToExport: this.handleResourcesToExportChange.bind(this), // Whenever resources and/or folder have to be exported onGoToResourceUriRequested: this.onGoToResourceUriRequested.bind(this), // Whenever the users wants to follow a resource uri }; } /** * Initialize class properties out of the state ( for performance purpose ) */ initializeProperties() { this.resources = null; // A cache of the last known list of resources from the App context this.folders = null; // A cache of the last known list of folders from the App context } /** * Whenever the component is mounted */ async componentDidMount() { this.populateDebounced(); this.handleResourcesWaitedFor(); } /** * Whenever the component has updated in terms of props or state * @param prevProps */ async componentDidUpdate(prevProps, prevState) { await this.handleResourcesLoaded(); await this.handleFoldersChange(); await this.handleResourcesChange(); await this.handleRouteChange(prevProps.location); await this.handleFilterChange(prevState.filter); await this.redirectAfterSelection(); } /** * Is filter equal * @param {Object} filter1 The first filter to compare. * @param {Object} filter2 The second filter to compare. * @returns {boolean} */ isFilterEqual(filter1, filter2) { if (filter1.type !== filter2.type) { return false; } const type = filter1.type; switch (type) { case ResourceWorkspaceFilterTypes.GROUP: return filter1?.payload?.group?.id === filter2?.payload?.group?.id; case ResourceWorkspaceFilterTypes.FOLDER: return filter1?.payload?.folder?.id === filter2?.payload?.folder?.id; case ResourceWorkspaceFilterTypes.TAG: return filter1?.payload?.tag?.id === filter2?.payload?.tag?.id; case ResourceWorkspaceFilterTypes.TEXT: return filter1?.payload === filter2?.payload; default: return true; } } /** * Handles the resource search filter change * @return {Promise<void>} */ async handleFilterChange(previousFilter) { const hasFilterChanged = !this.isFilterEqual(previousFilter, this.state.filter); if (hasFilterChanged) { // Avoid a side-effect whenever one inputs a specific resource url (it unselect the resource otherwise ) const isNotNonePreviousFilter = previousFilter.type !== ResourceWorkspaceFilterTypes.NONE; if (isNotNonePreviousFilter) { await this.unselectAll(); await this.populateDebounced(); } } } /** * Handles the resource search text filter change * @param text The filter text */ async handleTextFilterChange(text) { await this.search({type: ResourceWorkspaceFilterTypes.TEXT, payload: text}); await this.detailNothing(); } /** * Whenever the folders change */ async handleFoldersChange() { const hasFoldersChanged = this.props.context.folders !== this.folders; if (hasFoldersChanged) { this.folders = this.props.context.folders; await this.refreshSearchFilter(); await this.updateDetails(); } } /** * Handle the resources changes */ async handleResourcesChange() { const hasResourcesChanged = this.props.context.resources && this.props.context.resources !== this.resources; if (hasResourcesChanged) { this.resources = this.props.context.resources; await this.search(this.state.filter); await this.updateDetails(); await this.unselectUnknownResources(); } } /** * Handle the route location change * @param previousLocation Previous router location */ async handleRouteChange(previousLocation) { const hasLocationChanged = this.props.location.key !== previousLocation.key; const isBrowserClosing = !this.props.location.key; // The key property is undefined when the browser history is initialized or destroyed const isAppFirstLoad = this.state.filter.type === ResourceWorkspaceFilterTypes.NONE; if ((hasLocationChanged || isAppFirstLoad) && !isBrowserClosing) { await this.handleFolderRouteChange(); await this.handleResourceRouteChange(); } } /** * Handle the folder view route change * E.g. /folder/view.:filterByFolderId */ async handleFolderRouteChange() { if (this.props.context.folders !== null) { const folderId = this.props.match.params.filterByFolderId; if (folderId) { const folder = this.props.context.folders.find(folder => folder.id === folderId); if (folder) { // Known folder await this.search({type: ResourceWorkspaceFilterTypes.FOLDER, payload: {folder}}); await this.detailFolder(folder); } else { // Unknown folder this.handleUnknownFolder(); } } } } /** * Handle the resource view route change */ async handleResourceRouteChange() { const isResourceLocation = this.props.location.pathname.includes('passwords'); const resourceId = this.props.match.params.selectedResourceId; if (isResourceLocation) { if (resourceId) { // Case of password view this.handleSingleResourceRouteChange(resourceId); } else { // Case of all and applied filters this.handleAllResourceRouteChange(); } } } /** * Handle the resource view route change with a resource id * E.g. /passwords/view/:resourceId */ async handleSingleResourceRouteChange(resourceId) { const hasResources = this.resources !== null; if (hasResources) { const resource = this.resources.find(resource => resource.id === resourceId); const hasNoneFilter = this.state.filter.type === ResourceWorkspaceFilterTypes.NONE; if (hasNoneFilter) { // Case of password view by url bar inputting await this.search({type: ResourceWorkspaceFilterTypes.ALL}); } // If the resource does not exist , it should display an error if (resource) { await this.selectFromRoute(resource); await this.scrollTo(resource); await this.detailResource(resource); } else { this.handleUnknownResource(); } } } /** * Handle the resource view route change without a resource id in the path * E.g. /password */ async handleAllResourceRouteChange() { const hasResources = this.resources !== null; if (hasResources) { const filter = (this.props.location.state && this.props.location.state.filter) || {type: ResourceWorkspaceFilterTypes.ALL}; const isSameFilter = this.state.filter === filter; await this.detailNothing(); if (!isSameFilter) { await this.search(filter); } } } /** * Handle the lock detail to display it or not * @returns {Promise<void>} */ async handleLockDetail() { const lockDisplayDetail = !this.state.lockDisplayDetail; this.setState({lockDisplayDetail}); } /** * Handle an unknown resource ( passe by route parameter resource identifier ) */ handleUnknownResource() { this.props.actionFeedbackContext.displayError("The resource does not exist"); this.props.history.push({pathname: `/app/passwords`}); } /** * Handle an unknown folder (passed by route parameter folder identifier) */ handleUnknownFolder() { this.props.actionFeedbackContext.displayError("The folder does not exist"); this.props.history.push({pathname: `/app/passwords`}); } /** * Handle the scrolling of a resource */ async handleResourceScrolled() { await this.scrollNothing(); } /** * Handle the edited resource */ async handleResourceEdited() { await this.refreshSelectedResourceActivities(); } /** * Handle the edited resource description */ async handleResourceDescriptionEdited() { await this.refreshSelectedResourceActivities(); } /** * Handle the decrypted resource description */ async handleResourceDescriptionDecryted() { await this.refreshSelectedResourceActivities(); } /** * Handle the shared resource */ async handleResourceShared() { await this.refreshSelectedResourceActivities(); await this.refreshSelectedResourcePermissions(); } /** * Handle the refresh of the resource permission */ async handleResourcePermissionsRefreshed() { await this.setResourcesPermissionsAsRefreshed(); } /** * Handle the copied resource */ async handleResourceCopied() { await this.refreshSelectedResourceActivities(); } /** * Handle the refresh of the resource activitie * @returns {Promise<void>} */ async handleResourceActivitiesRefreshed() { await this.setResourceActivitiesAsRefreshed(); } /** * Handle the change of sorter ( on property or direction ) * @param propertyName The name of the property to sort on */ async handleSorterChange(propertyName) { await this.updateSorter(propertyName); await this.sort(); } /** * Handle the all resource selection */ async handleAllResourcesSelected() { await this.selectAll(); await this.detailNothing(); } /** * Handle the none resource selection */ async handleNoneResourcesSelected() { await this.unselectAll(); await this.detailNothing(); } /** * Handle the resource selection in a multiple mode * @param resource The selected resource */ async handleMultipleResourcesSelected(resource) { await this.selectMultiple(resource); await this.detailsResourceIfSingleSelection(); } /** * Handle the resource selection in a range mode * @param resource The selected resource */ async handleResourceRangeSelected(resource) { await this.selectRange(resource); } /** * Handle the single resource selection * @param resource The selected resource */ async handleResourceSelected(resource) { await this.select(resource); } /** * Handle the toggle sidebar to display it or not */ handleToggleSidebar() { const mustDisplaySidebar = !this.state.mustDisplaySidebar; this.setState({mustDisplaySidebar}); } /** * Handle the wait for the initial resources to be loaded */ handleResourcesWaitedFor() { this.props.loadingContext.add(); } /** * Handle the intial loading of the resources */ handleResourcesLoaded() { const hasResourcesBeenInitialized = this.resources === null && this.props.context.resources; if (hasResourcesBeenInitialized) { this.props.loadingContext.remove(); this.handleResourcesLoaded = () => {}; } } /** * Handle the will to import a resource file */ async handleResourceFileToImport(resourceFile) { await this.import(resourceFile); } /** * Handle the resource file import result * @param The import result */ async handleResourceFileImportResult(result) { await this.updateImportResult(result); } /** * Whenever the resources / folders to export change * @param resourcesIds The resources ids to export * @param foldersIds The folders ids to export */ async handleResourcesToExportChange({resourcesIds, foldersIds}) { await this.updateResourcesToExport({resourcesIds, foldersIds}); } /** * Whenever the users wants to follow a resource uri * @param {object} resource The resource to follow the uri */ onGoToResourceUriRequested(resource) { const safeUri = sanitizeUrl(resource.uri, { whiteListedProtocols: resourceLinkAuthorizedProtocols, defaultProtocol: urlProtocols.HTTPS }); if (safeUri) { window.open(safeUri, '_blank', 'noopener,noreferrer'); } } /** * Populate the context with initial data such as resources and folders */ populate() { if (this.props.context.siteSettings.canIUse("folders")) { this.props.context.port.request("passbolt.folders.update-local-storage"); } this.props.context.port.request("passbolt.resources.update-local-storage"); this.props.context.port.request("passbolt.groups.update-local-storage"); this.props.context.port.request("passbolt.users.update-local-storage"); } /** RESOURCE SEARCH **/ /** * Search for the resources which matches the given filter and sort them * @param filter */ async search(filter) { const isRecentlyModifiedFilter = filter.type === ResourceWorkspaceFilterTypes.RECENTLY_MODIFIED; const searchOperations = { [ResourceWorkspaceFilterTypes.ROOT_FOLDER]: this.searchByRootFolder.bind(this), [ResourceWorkspaceFilterTypes.FOLDER]: this.searchByFolder.bind(this), [ResourceWorkspaceFilterTypes.TAG]: this.searchByTag.bind(this), [ResourceWorkspaceFilterTypes.GROUP]: this.searchByGroup.bind(this), [ResourceWorkspaceFilterTypes.TEXT]: this.searchByText.bind(this), [ResourceWorkspaceFilterTypes.ITEMS_I_OWN]: this.searchByItemsIOwn.bind(this), [ResourceWorkspaceFilterTypes.FAVORITE]: this.searchByFavorite.bind(this), [ResourceWorkspaceFilterTypes.SHARED_WITH_ME]: this.seachBySharedWithMe.bind(this), [ResourceWorkspaceFilterTypes.RECENTLY_MODIFIED]: this.searchByRecentlyModified.bind(this), [ResourceWorkspaceFilterTypes.ALL]: this.searchAll.bind(this), [ResourceWorkspaceFilterTypes.NONE]: () => { /* No search */ } }; await searchOperations[filter.type](filter); if (!isRecentlyModifiedFilter) { await this.sort(); } else { await this.resetSorter(); } } /** * All filter ( no filter at all ) * @param filter The All filter */ async searchAll(filter) { await this.setState({filter, filteredResources: this.resources}); } /** * Filter the resources which belongs to the filter root folder */ async searchByRootFolder(filter) { const folderResources = this.resources.filter(resource => ! resource.folder_parent_id); await this.setState({filter, filteredResources: folderResources}); } /** * Filter the resources which belongs to the filter folder */ async searchByFolder(filter) { const folderId = filter.payload.folder.id; const folderResources = this.resources.filter(resource => resource.folder_parent_id === folderId); await this.setState({filter, filteredResources: folderResources}); } /** * Filter the resources which belongs to the filter tag */ async searchByTag(filter) { const tagId = filter.payload.tag.id; const tagResources = this.resources.filter(resource => resource.tags && resource.tags.length > 0 && resource.tags.filter(tag => tag.id === tagId).length > 0); await this.setState({filter, filteredResources: tagResources}); } /** * Filter the resources which textual properties matched some user text words * @param filter A textual filter */ async searchByText(filter) { const text = filter.payload; const words = (text && text.split(/\s+/)) || ['']; const canUseTags = this.props.context.siteSettings.canIUse("tags"); // Test match of some escaped test words against the name / username / uri / description /tags resource properties const escapeWord = word => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const wordToRegex = word => new RegExp(escapeWord(word), 'i'); const matchWord = (word, value) => wordToRegex(word).test(value); const matchTagProperty = (word, resource) => resource.tags.some(tag => matchWord(word, tag.slug)); const matchStringProperty = (word, resource) => ['name', 'username', 'uri', 'description'].some(key => matchWord(word, resource[key])); const matchResource = (word, resource) => matchStringProperty(word, resource) || (canUseTags && matchTagProperty(word, resource)); const matchText = resource => words.every(word => matchResource(word, resource)); const filteredResources = this.resources.filter(matchText); await this.setState({filter, filteredResources}); } /** * Filter the resources which belongs to the filter group */ async searchByGroup(filter) { const groupId = filter.payload.group.id; const filters = {'is-shared-with-group': groupId}; // get the resources with the group this.props.loadingContext.add(); const resourcesFilteredByGroup = await this.props.context.port.request('passbolt.resources.find-all', {filters}) || []; const resourceIds = resourcesFilteredByGroup.map(resource => resource.id); // keep only the resource with the group const groupResources = this.resources.filter(resource => resourceIds.includes(resource.id)); await this.setState({filter, filteredResources: groupResources, selectedResources: []}); this.props.loadingContext.remove(); } /** * Search for current user personal resources * @param filter The filter */ async searchByItemsIOwn(filter) { const filteredResources = this.resources.filter(resource => resource.permission.type === 15); await this.setState({filter, filteredResources}); } /** * Filter the resources which are the current user favorites one */ async searchByFavorite(filter) { const filteredResources = this.resources.filter(resource => resource.favorite !== null); await this.setState({filter, filteredResources}); } /** * Filter the resources which are shared wit the current user */ async seachBySharedWithMe(filter) { const filteredResources = this.resources.filter(resource => resource.permission.type < 15); await this.setState({filter, filteredResources}); } /** * Keep the most recently modified resources ( current state: just sort everything with the most recent modified resource ) * @param filter A recently modified filter */ async searchByRecentlyModified(filter) { const recentlyModifiedSorter = (resource1, resource2) => DateTime.fromISO(resource2.modified) < DateTime.fromISO(resource1.modified) ? -1 : 1; const filteredResources = this.resources.sort(recentlyModifiedSorter); await this.setState({filter, filteredResources}); } /** * Refresh the filter in case of its payload is outdated due to the updated list of resources */ async refreshSearchFilter() { const hasFolderFilter = this.state.filter.type === ResourceWorkspaceFilterTypes.FOLDER; if (hasFolderFilter) { const isFolderStillExist = this.folders.some(folder => folder.id === this.state.filter.payload.folder.id); if (isFolderStillExist) { // Case of folder exists but may have somme applied changes on it const updatedFolder = this.folders.find(folder => folder.id === this.state.filter.payload.folder.id); const filter = Object.assign(this.state.filter, {payload: {folder: updatedFolder}}); await this.setState({filter}); } else { // Case of filter folder deleted const filter = {type: ResourceWorkspaceFilterTypes.ALL}; this.props.history.push({pathname: '/app/passwords', state: {filter}}); } } } /** RESOURCE SELECTION */ /** * Select the given resource as the single selected resources if not already selected as single. Otherwise unselect it * @param resource The resource to select */ async select(resource) { const mustUnselect = this.state.selectedResources.length === 1 && this.state.selectedResources[0].id === resource.id; await this.setState({selectedResources: mustUnselect ? [] : [resource]}); } /** * Selects the given resource when one comes from the navigation route * @param resource * @returns {Promise<void>} */ async selectFromRoute(resource) { const isAlreadySelected = this.state.selectedResources.length === 1 && this.state.selectedResources[0].id === resource.id; if (!isAlreadySelected) { const selectedResources = [resource]; await this.setState({selectedResources}); } } /** * Select the given resource in a multiple selection mode * @param resource * @returns {Promise<void>} */ async selectMultiple(resource) { const hasNotSameId = selectedResource => selectedResource.id !== resource.id; const selectionWithoutResource = this.state.selectedResources.filter(hasNotSameId); const mustUnselect = this.state.selectedResources.length !== selectionWithoutResource.length; const selectedResources = mustUnselect ? selectionWithoutResource : [...this.state.selectedResources, resource]; await this.setState({selectedResources}); } /** * Select the given resource in a range selection mode * @param resource * @returns {Promise<void>} */ async selectRange(resource) { const hasNoSelection = this.state.selectedResources.length === 0; if (hasNoSelection) { await this.select(resource); } else { const hasSameId = resource => selectedResource => selectedResource.id === resource.id; const findIndex = resource => this.state.filteredResources.findIndex(hasSameId(resource)); const startRangeIndex = findIndex(this.state.selectedResources[0]); const endRangeIndex = findIndex(resource); let selectedResources; if (startRangeIndex > endRangeIndex) { // Down range selection selectedResources = this.state.filteredResources.slice(endRangeIndex, startRangeIndex + 1).reverse(); } else { // Up range selection selectedResources = this.state.filteredResources.slice(startRangeIndex, endRangeIndex + 1); } await this.setState({selectedResources}); } } /** * Select all the resources */ async selectAll() { await this.setState({selectedResources: [...this.state.filteredResources]}); } /** * Unselect all the resources */ async unselectAll() { const hasSelectedResources = this.state.selectedResources.length !== 0; if (hasSelectedResources) { await this.setState({selectedResources: []}); } } /** * Remove from the selected resources those which are not known resources in regard of the current resources list */ async unselectUnknownResources() { const matchId = selectedResource => resource => resource.id === selectedResource.id; const matchSelectedResource = selectedResource => this.state.selectedResources.some(matchId(selectedResource)); const selectedResources = this.resources.filter(matchSelectedResource); await this.setState({selectedResources}); } /** * Navigate to the appropriate url after some resources selection operation */ redirectAfterSelection() { const canUseFolders = this.props.context.siteSettings.canIUse('folders'); const contentLoaded = this.resources !== null && (!canUseFolders || this.folders !== null); if (contentLoaded) { const hasSingleSelectionNow = this.state.selectedResources.length === 1; if (hasSingleSelectionNow) { // Case of single selected resource const mustRedirect = this.props.location.pathname !== `/app/passwords/view/${this.state.selectedResources[0].id}`; if (mustRedirect) { this.props.history.push(`/app/passwords/view/${this.state.selectedResources[0].id}`); } } else { // Case of multiple selected resources const {filter} = this.state; const isFolderFilter = filter.type === ResourceWorkspaceFilterTypes.FOLDER; if (isFolderFilter) { // Case of folder const mustRedirect = this.props.location.pathname !== `/app/folders/view/${this.state.filter.payload.folder.id}`; if (mustRedirect) { this.props.history.push({pathname: `/app/folders/view/${this.state.filter.payload.folder.id}`}); } } else { // Case of resources const mustRedirect = this.props.location.pathname !== '/app/passwords'; if (mustRedirect) { this.props.history.push({pathname: `/app/passwords`, state: {filter}}); } } } } } /** Resource Sorter **/ /** * Update the resourcces sorter given a property name * @param propertyName */ async updateSorter(propertyName) { const hasSortPropertyChanged = this.state.sorter.propertyName !== propertyName; const asc = hasSortPropertyChanged || !this.state.sorter.asc; const sorter = {propertyName, asc}; await this.setState({sorter}); } /** * Reset the user sorter */ async resetSorter() { const sorter = {propertyName: 'modified', asc: false}; this.setState({sorter}); } /** * Sort the resources given the current sorter */ async sort() { const reverseSorter = sorter => (s1, s2) => -sorter(s1, s2); const baseSorter = sorter => this.state.sorter.asc ? sorter : reverseSorter(sorter); const keySorter = (key, sorter) => baseSorter((s1, s2) => sorter(s1[key], s2[key])); const stringSorter = (s1, s2) => (s1 || "").localeCompare(s2 || ""); const booleanSorter = (s1, s2) => s1 === s2 ? 0 : s1 ? -1 : 1; const sorter = this.state.sorter.propertyName === "favorite" ? booleanSorter : stringSorter; const propertySorter = keySorter(this.state.sorter.propertyName, sorter); if (this.state.filteredResources !== null) { await this.setState({filteredResources: [...this.state.filteredResources.sort(propertySorter)]}); } } /** RESOURCE DETAILS **/ /** * Set the details focus on the given folder * @param folder The folder to focus on */ async detailFolder(folder) { await this.setState({details: {folder: folder, resource: null}}); } /** * Set the details focus on the given resource * @param resource The resource to focus on */ async detailResource(resource) { await this.setState({details: {folder: null, resource: resource}}); } /** * Remove the details on something */ async detailNothing() { const hasDetails = this.state.details.resource || this.state.details.folder; if (hasDetails) { await this.setState({details: {folder: null, resource: null}}); } } /** * Set the details focus on the selected resource if it's the only one selected * @returns {Promise<void>} */ async detailsResourceIfSingleSelection() { const hasSingleSelection = this.state.selectedResources.length == 1; if (hasSingleSelection) { await this.detailResource(this.state.selectedResources[0]); } else { await this.detailNothing(); } } /** * Update the current details with the current list of resources or folders */ async updateDetails() { const hasDetails = this.state.details.resource || this.state.details.folder; if (hasDetails) { const hasResourceDetails = this.state.details.resource; if (hasResourceDetails) { // Case of resource details const updatedResourceDetails = this.resources.find(resource => resource.id === this.state.details.resource.id); await this.setState({details: {resource: updatedResourceDetails}}); } else { // Case of folder details const updatedFolderDetails = this.folders.find(folder => folder.id === this.state.details.folder.id); await this.setState({details: {folder: updatedFolderDetails}}); } } } /** Resource scrolling **/ /** * Set the resource to scroll to * @param resource A resource */ async scrollTo(resource) { await this.setState({scrollTo: {resource}}); } /** * Unset the resource to scroll to */ async scrollNothing() { await this.setState({scrollTo: {}}); } /** RESOURCE ACTIVITIES */ /** * Refresh the activities of the current selected resource */ async refreshSelectedResourceActivities() { const refresh = Object.assign({}, this.state.refresh, {activities: true}); await this.setState({refresh}); } /** * Set the resources activitie as refreshed */ async setResourceActivitiesAsRefreshed() { const refresh = Object.assign({}, this.state.refresh, {activities: false}); await this.setState({refresh}); } /** RESOURCE PERMISSION */ /** * Refresh the permissions of the current selected resources */ async refreshSelectedResourcePermissions() { const refresh = Object.assign({}, this.state.refresh, {permissions: true}); await this.setState({refresh}); } /** * Set the resources permissions as refreshed */ async setResourcesPermissionsAsRefreshed() { const refresh = Object.assign({}, this.state.refresh, {permissions: false}); await this.setState({refresh}); } /** RESOURCE IMPORT */ /** * Import the given resource file * @param resourceFile A resource file to import */ async import(resourceFile) { await this.setState({resourceFileToImport: resourceFile}); } /** * Update the resource file import result * @param The import result */ async updateImportResult(result) { await this.setState({resourceFileImportResult: result}); } /** Resource export */ /** * Update the resources / folders to export * @param resourcesIds The resources ids to export * @param foldersIds The folders ids to export */ async updateResourcesToExport({resourcesIds, foldersIds}) { await this.setState({resourcesToExport: {resourcesIds, foldersIds}}); } /** * Render the component * @returns {JSX} */ render() { return ( <ResourceWorkspaceContext.Provider value={this.state}> {this.props.children} </ResourceWorkspaceContext.Provider> ); } } ResourceWorkspaceContextProvider.displayName = 'ResourceWorkspaceContextProvider'; ResourceWorkspaceContextProvider.propTypes = { context: PropTypes.any, // The application context children: PropTypes.any, location: PropTypes.object, match: PropTypes.object, history: PropTypes.object, actionFeedbackContext: PropTypes.object, loadingContext: PropTypes.object // The loading context }; export default withAppContext(withLoading(withActionFeedback(withRouter(ResourceWorkspaceContextProvider)))); /** * Resource Workspace Context Consumer HOC * @param WrappedComponent */ export function withResourceWorkspace(WrappedComponent) { return class WithResourceWorkspace extends React.Component { render() { return ( <ResourceWorkspaceContext.Consumer> { ResourceWorkspaceContext => <WrappedComponent resourceWorkspaceContext={ResourceWorkspaceContext} {...this.props} /> } </ResourceWorkspaceContext.Consumer> ); } }; } /** * The list of resource workspace search filter types */ export const ResourceWorkspaceFilterTypes = { NONE: 'NONE', // Initial filter at page load ALL: 'ALL', // All resources FOLDER: 'FILTER-BY-FOLDER', // Resources in a given folder ROOT_FOLDER: 'FILTER-BY-ROOT-FOLDER', // Resources at the root folder TAG: 'FILTER-BY-TAG', // Resources marked with a given tag GROUP: 'FILTER-BY-GROUP', // Resources shared with a given group TEXT: 'FILTER-BY-TEXT-SEARCH', // Resources matching some text words ITEMS_I_OWN: 'FILTER-BY-ITEMS-I-OWN', // Resources the users is owner of FAVORITE: 'FILTER-BY-FAVORITE', // Favorite resources SHARED_WITH_ME: 'FILTER-BY-SHARED-WITH-ME', // Resources shared with the current user (who is not the owner) RECENTLY_MODIFIED: 'FILTER-BY-RECENTLY-MODIFIED', // Resources recently modified }; /** * The list of resource link authorized protocols */ export const resourceLinkAuthorizedProtocols = [ urlProtocols.FTP, urlProtocols.FTPS, urlProtocols.HTTPS, urlProtocols.HTTP, urlProtocols.SSH ];