passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
1,322 lines (1,208 loc) • 55 kB
JavaScript
/**
* 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 "../../shared/context/AppContext/AppContext";
import { withRouter } from "react-router-dom";
import { withActionFeedback } from "./ActionFeedbackContext";
import { withLoading } from "./LoadingContext";
import sanitizeUrl, { urlProtocols } from "../lib/Sanitize/sanitizeUrl";
import SorterEntity from "../../shared/models/entity/sorter/sorterEntity";
import GridUserSettingEntity from "../../shared/models/entity/gridUserSetting/gridUserSettingEntity";
import GridResourceUserSettingServiceWorkerService from "../../shared/services/serviceWorker/gridResourceUserSetting/GridResourceUserSettingServiceWorkerService";
import ColumnsResourceSettingCollection from "../../shared/models/entity/resource/columnsResourceSettingCollection";
import { withPasswordExpiry } from "./PasswordExpirySettingsContext";
import { withRbac } from "../../shared/context/Rbac/RbacContext";
import { uiActions } from "../../shared/services/rbacs/uiActionEnumeration";
import { ColumnModelTypes } from "../../shared/models/column/ColumnModel";
import getPropValue from "../lib/Object/getPropValue";
import { withTranslation } from "react-i18next";
import RowsSettingEntity from "../../shared/models/entity/rowsSetting/rowsSettingEntity";
import ResourcesServiceWorkerService from "../../shared/services/serviceWorker/resources/resourcesServiceWorkerService";
import TabsServiceWorkerService from "../../shared/services/serviceWorker/tabs/tabsServiceWorkerService";
/**
* 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
columnsResourceSetting: [], // The settings of columns for resources
rowsSetting: null, // The setting for the display of the rows
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
folder: null, // The folder 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
onResourcePreviewed: () => {}, // Whenever a resource has been previewed
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
onChangeColumnView: () => {}, // Whenever the users wants to show or hide a column
onChangeColumnsSettings: () => {}, // Whenever the user change the columns configuration
resetGridColumnsSettings: () => {}, // Whenever the user resets the columns configuration
onChangeRowSettingsHeight: () => {}, // Whenever the user change the row settings
getHierarchyFolderCache: () => {}, // Whenever the need to get folder hierarchy
});
/**
* The related context provider
*/
export class ResourceWorkspaceContextProvider extends React.Component {
/**
* Default constructor
* @param props The component props
*/
constructor(props) {
super(props);
this.rowsSetting = RowsSettingEntity.createFromDefault();
this.state = this.defaultState;
this.gridResourceUserSetting = new GridResourceUserSettingServiceWorkerService(props.context.port);
this.resourcesServiceWorkerService = new ResourcesServiceWorkerService(props.context.port);
this.tabsServiceWorkerService = new TabsServiceWorkerService(props.context.port);
}
/**
* Get default sorter
* @return {object}
*/
get defaultSorter() {
return new SorterEntity({
propertyName: "modified", // The name of the property to sort on
asc: false, // True if the sort must be descendant
});
}
/**
* Returns the default component state
*/
get defaultState() {
return {
filter: { type: ResourceWorkspaceFilterTypes.NONE }, // The current resource search filter
sorter: this.defaultSorter, // The default sorter
filteredResources: null, // The current list of filtered resources
selectedResources: [], // The current list of selected resources
columnsResourceSetting: null, // The settings of columns for resources
rowsSetting: this.rowsSetting.toDto(), // The setting for the display of the rows
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
folder: null, // The folder 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)
onFolderScrolled: this.handleFolderScrolled.bind(this), // Whenever one scrolled to a resource
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.handleResourceDescriptionDecrypted.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
onResourcePreviewed: this.handleResourcePreviewed.bind(this), // Whenever a resource (password) has been copied
onResourceActivitiesRefreshed: this.handleResourceActivitiesRefreshed.bind(this), // Whenever the resource activities have been refreshed
onSorterChanged: this.handleUpdatedSorterChange.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
onChangeColumnView: this.handleChangeColumnView.bind(this), // Whenever the users wants to show or hide a column
onChangeColumnsSettings: this.handleChangeColumnsSettings.bind(this), // Whenever the user change the columns configuration
resetGridColumnsSettings: this.resetGridColumnsSettings.bind(this), // Whenever the user resets the columns configuration
onChangeRowSettingsHeight: this.onChangeRowSettingsHeight.bind(this), // Whenever the user change the rows setting
};
}
/**
* Get the folders
* @return {*}
*/
get resources() {
return this.props.context.resources;
}
/**
* Get the folders
* @return {*}
*/
get folders() {
return this.props.context.folders;
}
/**
* Whenever the component is mounted
*/
async componentDidMount() {
await this.props.passwordExpiryContext.findSettings();
this.loadGridResourceSetting();
this.populate();
this.handleResourcesWaitedFor();
}
/**
* Check if the component is ready for update processing
* @returns {boolean}
*/
isContentLoaded() {
return this.resources !== null && (!this.canUseFolders || this.folders !== null);
}
/**
* Check if the route location has changed
* @param {object} prevProps
* @returns {boolean}
*/
hasLocationChanged(prevProps) {
const hasPathChanged = this.props.location.pathname !== prevProps.location.pathname;
const hasStateChanged = this.props.location.state !== prevProps.location?.state;
return hasPathChanged || hasStateChanged;
}
/**
* Check if this is the first app load
* @returns {boolean}
*/
get isAppFirstLoad() {
return this.state.filter.type === ResourceWorkspaceFilterTypes.NONE;
}
/**
* Get the change from previous props and state
* @param prevProps The previous props
* @param prevState The previous state
* @returns {object} Change flags
*/
getChangesFromPreviousPropsAndState(prevProps, prevState) {
return {
route: this.hasLocationChanged(prevProps) || this.isAppFirstLoad,
folders: prevProps.context.folders !== this.folders,
resources: prevProps.context.resources !== this.resources,
selection: prevState.selectedResources !== this.state.selectedResources,
filter: !this.isFilterEqual(prevState.filter, this.state.filter),
details: prevState.details !== this.state.details,
sorter: prevState.sorter !== this.state.sorter,
};
}
/**
* Whenever the component has updated in terms of props or state
* @param prevProps The previous props
* @param prevState The previous state
*/
async componentDidUpdate(prevProps, prevState) {
if (!this.isContentLoaded()) {
return;
}
this.handleResourcesLoaded();
// Get the data from the state that could be updated
const nextState = {
filter: this.state.filter,
details: this.state.details,
selectedResources: this.state.selectedResources,
};
const changes = this.getChangesFromPreviousPropsAndState(prevProps, prevState);
// Update next state according to the changes detected
this.processDataChanges(nextState, changes);
const hasFilterChanged = !this.isFilterEqual(nextState.filter, this.state.filter);
if (hasFilterChanged) {
this.handleFilterChange(nextState);
}
// Update details according to the selection and redirect if needed
this.processSelectionChanges(nextState, changes, hasFilterChanged);
// Apply set state if change detected based on next state
this.applyStateUpdatesAndSearch(nextState, changes, hasFilterChanged);
}
/**
* Process data changes and update next state accordingly
* @param {object} nextState - Mutable state object to update
* @param {object} changes - Change flags from detectChanges
*/
processDataChanges(nextState, changes) {
// Route changed or App first load when resource and folder are loaded or has previous selection changed
if (changes.route) {
this.handleRouteChange(nextState);
}
// Folder changed
if (changes.folders) {
this.handleFoldersChange(nextState);
}
// Resource changed
if (changes.resources) {
this.handleResourceChange(nextState);
}
// Has previous sorter changed
if (changes.sorter) {
this.handleSorterChange();
}
}
/**
* Process selection and navigation changes
* @param {object} nextState - Mutable state object to update
* @param {object} changes - Change flags
* @param {boolean} hasFilterChanged - Whether filter changed
*/
processSelectionChanges(nextState, changes, hasFilterChanged) {
// Has previous select resources changed and previous filter and details unchanged
const shouldUpdateDetailsFromSelection = changes.selection && !changes.filter && !changes.details;
// Example of a use case:
// Given resources filter by folder with the route
// And I select all resources (Selection has changed, route and details are unchanged and details should change to display a list of resources)
// Then I should see a list of resources in details
// And I unselect all (Selection has changed, route and details are unchanged and details should change to display the folder)
// Then I should see the folder details
if (shouldUpdateDetailsFromSelection) {
// Update details from filter
nextState.details = this.getDetailsFromFilter(nextState.filter, nextState.selectedResources);
}
// Has previous select resources changed (Should redirect when one or several resources has been selected)
// or filter changed (Should redirect and unselect all resources and display folder details if previously selected)
// or route changed (When one resource is selected and same filter is applied and the route should remain on the selected resource)
const shouldRedirectAfterSelection = changes.selection || hasFilterChanged || changes.route;
if (shouldRedirectAfterSelection) {
// Redirect according to the selected resources or filter if needed
this.redirectAfterSelection(nextState.selectedResources, nextState.filter);
}
}
/**
* Apply state updates and trigger search if needed
* @param {object} nextState - State to apply
* @param {object} changes - Change flags
* @param {boolean} hasFilterChanged - Whether filter changed
*/
applyStateUpdatesAndSearch(nextState, changes, hasFilterChanged) {
this.handleSelectedResourcesChange(nextState.selectedResources);
this.handleDetailsChange(nextState.details);
const needsSearch = hasFilterChanged || changes.folders || changes.resources;
if (needsSearch) {
// The search will set the filter state
this.search(nextState.filter);
}
}
/**
* Handle route change.
* Mutates nextState to set filter, details, and selectedResources based on the current route.
* @param {object} nextState - The next state object to mutate
* @param {object} [nextState.filter] - The filter configuration
* @param {object} [nextState.details] - The details panel state (resource or folder)
* @param {Array} [nextState.selectedResources] - The list of selected resources
*/
handleRouteChange(nextState) {
// Get filter from route
nextState.filter = this.getFilterFromRoute(nextState.filter);
// Get details from filter
nextState.details = this.getDetailsFromFilter(nextState.filter, nextState.selectedResources);
// Select resource if route match and details resource exist
if (this.props.match.params.selectedResourceId) {
// Edge case when resource does not exist anymore and should be selected by the route
if (nextState.details.resource === null) {
nextState.selectedResources = [];
// Display notification message to inform the resource does not exist
this.handleUnknownResource();
} else {
// Select the resource
nextState.selectedResources = [nextState.details.resource];
}
}
}
/**
* Get the filter according to the route.
* Determines the appropriate filter based on URL parameters (folder ID, filter type, selected resource).
* @param {object} filter - The current filter state
* @param {string} filter.type - The current filter type
* @param {object} [filter.payload] - Optional filter payload
* @returns {object} The resolved filter object
* @returns {string} return.type - The filter type (e.g., FOLDER, ALL, EXPIRED)
* @returns {object} [return.payload] - Optional payload containing filter-specific data
* @returns {object} [return.payload.folder] - The folder when filter type is FOLDER
*/
getFilterFromRoute(filter) {
if (this.folders !== null && this.props.match.params.filterByFolderId) {
const folder = this.folders.find((folder) => folder.id === this.props.match.params.filterByFolderId);
if (folder) {
if (this.canUseFolders) {
this.populateFolders();
}
this.resourcesServiceWorkerService.updateResourceLocalStorageForParentFolderId(folder.id);
return { type: ResourceWorkspaceFilterTypes.FOLDER, payload: { folder: folder } };
} else {
this.handleUnknownFolder();
// Return ALL if folder is unknown
return { type: ResourceWorkspaceFilterTypes.ALL };
}
} else if (this.resources !== null && this.props.location.pathname.includes("passwords")) {
const isExpiredResourceLocation = this.props.match.params?.filterType === ResourceWorkspaceFilterTypes.EXPIRED;
if (isExpiredResourceLocation) {
return { type: ResourceWorkspaceFilterTypes.EXPIRED };
} else if (this.props.match.params.selectedResourceId) {
// Return ALL if the actual filter is none or the actual filter (fix edge case on first load)
return filter.type === ResourceWorkspaceFilterTypes.NONE ? { type: ResourceWorkspaceFilterTypes.ALL } : filter;
}
}
// Return ALL if the actual filter is none or the location filter or ALL (fix edge case on filter group tag or home)
return filter.type === ResourceWorkspaceFilterTypes.NONE
? { type: ResourceWorkspaceFilterTypes.ALL }
: this.props.location.state?.filter || { type: ResourceWorkspaceFilterTypes.ALL };
}
/**
* Get the details panel state based on the current filter and selection.
* Determines which resource or folder should be displayed in the details panel.
* Only one of folder or resource can be set at a time.
* @param {object} filter - The current filter
* @param {string} filter.type - The filter type
* @param {object} [filter.payload] - Optional filter payload
* @param {object} [filter.payload.folder] - The folder when filter type is FOLDER
* @param {Array} selectedResources - The currently selected resources
* @returns {object} The details panel state
* @returns {object|null} return.folder - The folder to display, or null
* @returns {object|null} return.resource - The resource to display, or null
*/
getDetailsFromFilter(filter, selectedResources) {
if (this.props.match.params.selectedResourceId) {
// Find the resource with the route
const resource = this.resources.find((resource) => resource.id === this.props.match.params.selectedResourceId);
if (resource) {
return { folder: null, resource: resource };
}
} else if (selectedResources.length > 1) {
// Multiple resources selected (do not display folder or resource details)
return { folder: null, resource: null };
} else if (filter.type === ResourceWorkspaceFilterTypes.FOLDER) {
// If filter is folder and there is no or one resource selected display folder details
return { folder: filter.payload.folder, resource: null };
}
return { folder: null, resource: null };
}
/**
* Handle folders change
* @param {object} nextState The next state
*/
handleFoldersChange(nextState) {
const hasFolderFilter = nextState.filter.type === ResourceWorkspaceFilterTypes.FOLDER;
if (hasFolderFilter) {
// Update the filter
nextState.filter = this.updateFilterFromFoldersChange(nextState.filter);
// Update the folder details
nextState.details = this.getDetailsFromFilter(nextState.filter, nextState.selectedResources);
}
}
/**
* Handle resources change
* @param {object} nextState The next state
*/
handleResourceChange(nextState) {
if (nextState.details.resource !== null) {
nextState.details = this.updateResourceDetails(nextState.details.resource);
}
nextState.selectedResources = this.updateSelectedResourcesFromResourcesChange(nextState.selectedResources);
}
/**
* Handle details change
* @param {object} details
*/
handleDetailsChange(details) {
const hasDetailsChanges =
details.folder !== this.state.details.folder || details.resource !== this.state.details.resource;
if (hasDetailsChanges) {
this.setState({ details });
if (details.resource) {
this.scrollToResource(details.resource);
} else if (details.folder) {
this.scrollToFolder(details.folder);
}
}
}
/**
* Handle previous sorter change
*/
handleSorterChange() {
if (this.state.filteredResources !== null) {
const filteredResources = [...this.state.filteredResources];
this.sort(filteredResources);
this.setState({ filteredResources });
}
}
/**
* Handle selected resources change
* @param {Array} selectedResources
*/
handleSelectedResourcesChange(selectedResources) {
const hasSelectedResourcesChanged = selectedResources !== this.state.selectedResources;
if (hasSelectedResourcesChanged) {
this.setState({ selectedResources });
}
}
/**
* 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
* @param {object} nextState The next state
* @return {void}
*/
handleFilterChange(nextState) {
// Avoid a side-effect whenever one inputs a specific resource url (it unselect the resource otherwise )
if (!this.isAppFirstLoad) {
// Unselect if filter changed and is not the app first load
nextState.selectedResources = [];
}
if (
nextState.filter.type !== ResourceWorkspaceFilterTypes.GROUP &&
nextState.filter.type !== ResourceWorkspaceFilterTypes.FOLDER
) {
this.populate();
}
}
/**
* Whenever the folders change update the filter
* @param {object} filter - The current filter state
* @param {string} filter.type - The current filter type
* @param {object} [filter.payload] - Optional filter payload
* @returns {object} The resolved filter object
* @returns {string} return.type - The filter type (e.g., FOLDER, ALL)
* @returns {object} [return.payload] - Optional payload containing filter-specific data
* @returns {object} [return.payload.folder] - The folder when filter type is FOLDER
*/
updateFilterFromFoldersChange(filter) {
const folder = this.folders.find((folder) => folder.id === filter.payload.folder.id);
if (folder) {
return { type: ResourceWorkspaceFilterTypes.FOLDER, payload: { folder: folder } };
} else {
// If folder does not exist go back to ALL filter
return { type: ResourceWorkspaceFilterTypes.ALL };
}
}
/**
* Update resource details
* @param {object} outdatedResource The outdated resource
* @return {{folder: null, resource: *}|{folder: null, resource: null}}
*/
updateResourceDetails(outdatedResource) {
// Case of resource details
const resource = this.resources.find((resource) => resource.id === outdatedResource.id);
if (resource) {
return { folder: null, resource: resource };
}
// If resource does not exist go back to ALL filter
return { folder: null, resource: null };
}
/**
* Remove from the selected resources those which are not known resources in regard of the current resources list
* @param {Array<*>} selectedResources
* @return {Array<*>}
*/
updateSelectedResourcesFromResourcesChange(selectedResources) {
const matchId = (selectedResource) => (resource) => resource.id === selectedResource.id;
const matchSelectedResource = (selectedResource) => selectedResources.some(matchId(selectedResource));
return this.resources.filter(matchSelectedResource);
}
/**
* Handle the lock detail to display it or not
* @returns {Promise<void>}
*/
async handleLockDetail() {
const lockDisplayDetail = !this.state.lockDisplayDetail;
return this.setState({ lockDisplayDetail });
}
/**
* Handle an unknown resource (display an error notification)
*/
handleUnknownResource() {
this.props.actionFeedbackContext.displayError("The resource does not exist");
}
/**
* Handle an unknown folder (display an error notification)
*/
handleUnknownFolder() {
this.props.actionFeedbackContext.displayError("The folder does not exist");
}
/**
* Handle the scrolling of a resource
*/
handleResourceScrolled() {
this.scrollNothing();
}
/**
* Handle the scrolling of a folder
*/
handleFolderScrolled() {
this.scrollNothing();
}
/**
* Handle the edited resource
*/
async handleResourceEdited() {
this.refreshSelectedResourceActivities();
}
/**
* Handle the edited resource description
*/
async handleResourceDescriptionEdited() {
this.refreshSelectedResourceActivities();
}
/**
* Handle the decrypted resource description
*/
async handleResourceDescriptionDecrypted() {
this.refreshSelectedResourceActivities();
}
/**
* Handle the shared resource
*/
async handleResourceShared() {
this.refreshSelectedResourceActivities();
this.refreshSelectedResourcePermissions();
}
/**
* Handle the refresh of the resource permission
*/
handleResourcePermissionsRefreshed() {
this.setResourcesPermissionsAsRefreshed();
}
/**
* Handle the copied resource
*/
handleResourceCopied() {
this.refreshSelectedResourceActivities();
}
/**
* Handle the previewed resource
*/
handleResourcePreviewed() {
this.refreshSelectedResourceActivities();
}
/**
* Handle the refresh of the resource activitie
* @returns {Promise<void>}
*/
handleResourceActivitiesRefreshed() {
this.setResourceActivitiesAsRefreshed();
}
/**
* Handle the change of sorter ( on property or direction )
* @param propertyName The name of the property to sort on
*/
handleUpdatedSorterChange(propertyName) {
this.updateSorter(propertyName);
}
/**
* Handle the all resource selection
*/
handleAllResourcesSelected() {
this.selectAll();
}
/**
* Handle the none resource selection
*/
handleNoneResourcesSelected() {
this.unselectAll();
}
/**
* Handle the resource selection in a multiple mode
* @param resource The selected resource
*/
handleMultipleResourcesSelected(resource) {
this.selectMultiple(resource);
}
/**
* Handle the resource selection in a range mode
* @param resource The selected resource
*/
handleResourceRangeSelected(resource) {
this.selectRange(resource);
}
/**
* Handle the single resource selection
* @param resource The selected resource
*/
handleResourceSelected(resource) {
this.select(resource);
}
/**
* Handle the wait for the initial resources to be loaded
*/
handleResourcesWaitedFor() {
this.props.loadingContext.add();
}
/**
* Handle the initial loading of the resources
*/
handleResourcesLoaded() {
const hasResourcesBeenInitialized = this.resources !== null;
if (hasResourcesBeenInitialized) {
this.props.loadingContext.remove();
this.handleResourcesLoaded = () => {};
}
}
/**
* Handle the will to import a resource file
*/
handleResourceFileToImport(resourceFile) {
this.import(resourceFile);
}
/**
* Handle the resource file import result
* @param result The import result
*/
handleResourceFileImportResult(result) {
this.updateImportResult(result);
}
/**
* Whenever the resources / folders to export change
* @param resourcesIds The resources ids to export
* @param foldersIds The folders ids to export
*/
handleResourcesToExportChange({ resourcesIds, foldersIds }) {
this.updateResourcesToExport({ resourcesIds, foldersIds });
}
/**
* Whenever the users wants to follow a resource uri
* @param {string} uri The uri to follow
*/
onGoToResourceUriRequested(uri) {
const safeUri = sanitizeUrl(uri, {
whiteListedProtocols: resourceLinkAuthorizedProtocols,
defaultProtocol: urlProtocols.HTTPS,
});
if (safeUri) {
this.tabsServiceWorkerService.openResourceUriInNewTab(safeUri);
}
}
/**
* Check if the user can use folders.
* @returns {boolean}
*/
get canUseFolders() {
return (
this.props.context.siteSettings.canIUse("folders") && this.props.rbacContext.canIUseAction(uiActions.FOLDERS_USE)
);
}
/**
* Populate the context with initial data such as resources and folders
*/
populate() {
if (this.canUseFolders) {
this.populateFolders();
}
this.populateResources();
}
/**
* Populate the resources local storage
* @returns {Promise<void>}
*/
async populateResources() {
try {
await this.props.context.port.request("passbolt.resources.update-local-storage");
} catch (error) {
console.error(error);
const message =
this.props.t("Unable to load/refresh the resources.") + (error?.message ? ` ${error?.message}` : "");
await this.props.actionFeedbackContext.displayError(message);
}
}
/**
* Populate the folders local storage
* @returns {Promise<void>}
*/
async populateFolders() {
try {
await this.props.context.port.request("passbolt.folders.update-local-storage");
} catch (error) {
console.error(error);
const message = this.props.t("Unable to load/refresh the folders.");
Number(error?.message ? ` ${error?.message}` : "");
await this.props.actionFeedbackContext.displayError(message);
}
}
/** RESOURCE SEARCH **/
/**
* Search for the resources which matches the given filter and sort them
* @param filter
*/
search(filter) {
// To prevent the filtered resources to be loaded before the columns
if (this.state.columnsResourceSetting === null) {
this.setState({ filter });
return;
}
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.PRIVATE]: this.searchByPrivate.bind(this),
[ResourceWorkspaceFilterTypes.FAVORITE]: this.searchByFavorite.bind(this),
[ResourceWorkspaceFilterTypes.SHARED_WITH_ME]: this.searchBySharedWithMe.bind(this),
[ResourceWorkspaceFilterTypes.EXPIRED]: this.searchByExpired.bind(this),
[ResourceWorkspaceFilterTypes.ALL]: this.searchAll.bind(this),
[ResourceWorkspaceFilterTypes.NONE]: () => {
/* No search */
},
};
searchOperations[filter.type](filter);
}
/**
* All filter ( no filter at all )
* @param {object} filter The All filter
*/
searchAll(filter) {
this.sort(this.resources);
this.setState({ filter, filteredResources: this.resources });
}
/**
* Filter the resources which belongs to the filter root folder
* @param {object} filter The filter
*/
searchByRootFolder(filter) {
const folderResources = this.resources.filter((resource) => !resource.folder_parent_id);
this.sort(folderResources);
this.setState({ filter, filteredResources: folderResources });
}
/**
* Filter the resources which belongs to the filter folder
* @param {object} filter The filter
*/
searchByFolder(filter) {
const folderId = filter.payload.folder.id;
const folderResources = this.resources.filter((resource) => resource.folder_parent_id === folderId);
this.sort(folderResources);
this.setState({ filter, filteredResources: folderResources });
}
/**
* Filter the resources which belongs to the filter tag
* @param {object} filter The filter
* @return {Array} The resources filter by tag
*/
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,
);
this.sort(tagResources);
this.setState({ filter, filteredResources: tagResources });
}
/**
* Filter the resources which textual properties matched some user text words
* @param {object} filter A textual filter
*/
searchByText(filter) {
const text = filter.payload;
const words = (text && text.split(/\s+/)) || [""];
const canUseTags = this.props.context.siteSettings.canIUse("tags");
const foldersMatchCache = {};
// 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 getFolderById = (id) => this.props.context.foldersMapById[id];
const matchFolderNameProperty = (word, folder) => matchWord(word, folder?.name);
const matchFolder = (word, folder) =>
matchFolderNameProperty(word, folder) ||
(folder?.folder_parent_id && matchFolderCache(word, folder.folder_parent_id));
const matchFolderCache = (word, id) => {
const key = word + id;
if (typeof foldersMatchCache[key] === "undefined") {
foldersMatchCache[key] = matchFolder(word, getFolderById(id));
}
return foldersMatchCache[key];
};
const matchTagProperty = (word, resource) => resource.tags?.some((tag) => matchWord(word, tag.slug));
const matchUrisProperty = (word, resource) => resource.metadata?.uris?.some((uri) => matchWord(word, uri));
const matchCustomFieldsProperty = (word, resource) =>
resource.metadata?.custom_fields?.some((customField) => matchWord(word, customField.metadata_key));
const matchStringProperty = (word, resource) =>
["name", "username", "description"].some((key) => matchWord(word, resource.metadata?.[key]));
const matchResource = (word, resource) =>
matchStringProperty(word, resource) ||
matchUrisProperty(word, resource) ||
(canUseTags && matchTagProperty(word, resource)) ||
matchCustomFieldsProperty(word, resource) ||
(resource?.folder_parent_id && matchFolderCache(word, resource.folder_parent_id));
const matchText = (resource) => words.every((word) => matchResource(word, resource));
const filteredResources = this.resources.filter(matchText);
this.sort(filteredResources);
this.setState({ filter, filteredResources });
}
/**
* Filter the resources which belongs to the filter group
* @param {object} filter The filter
*/
searchByGroup(filter) {
if (this.isFilterEqual(this.state.filter, filter) && Boolean(this.state.filteredResources)) {
return;
}
this.props.loadingContext.add();
/*
* Resources filtering is applied after having set the configuration of the filter in the state.
* It allows Firefox not to loop infinitely because of the local storage update induced by the call of the background page.
*
* What seem to happen on Firefox is that the state is not updated on the right time but the local storage onChanged callback is executed.
* This creates a confusion in the filtering system where we expect resources to be filtered by group but the filter is still on "ALL".
* The execution order of the callback from the local storage and the state change (that calls componentDidUpdate)
* produces an infinite loop in Firefox.
*/
this.setState({ filter, selectedResources: [] }, async () => {
const resourceIds =
(await this.props.context.port.request(
"passbolt.resources.find-all-ids-by-is-shared-with-group",
filter.payload.group.id,
)) || [];
// keep only the resource with the group
const groupResources = this.resources.filter((resource) => resourceIds.includes(resource.id));
this.sort(groupResources);
this.setState({ filteredResources: groupResources });
this.props.loadingContext.remove();
});
}
/**
* Search for resources the current user owned
* @param {object} filter The filter
*/
searchByItemsIOwn(filter) {
const filteredResources = this.resources.filter((resource) => resource.permission.type === 15);
this.sort(filteredResources);
this.setState({ filter, filteredResources });
}
/**
* Search for user private resources
* @param {object} filter The filter
*/
searchByPrivate(filter) {
const filteredResources = this.resources.filter((resource) => Boolean(resource.personal));
this.sort(filteredResources);
this.setState({ filter, filteredResources });
}
/**
* Filter the resources which are the current user favorites one
* @param {object} filter The filter
*/
searchByFavorite(filter) {
const filteredResources = this.resources.filter((resource) => resource.favorite !== null);
this.sort(filteredResources);
this.setState({ filter, filteredResources });
}
/**
* Filter the resources which are shared wit the current user
* @param {object} filter The filter
*/
searchBySharedWithMe(filter) {
const filteredResources = this.resources.filter((resource) => resource.permission.type < 15);
this.sort(filteredResources);
this.setState({ filter, filteredResources });
}
/**
* Keep the expired resources
* @param filter A "expired" filter
*/
searchByExpired(filter) {
const filteredResources = this.resources.filter(
(resource) => resource.expired && new Date(resource.expired) <= new Date(),
);
this.sort(filteredResources);
this.setState({ filter, filteredResources });
}
/** 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
*/
select(resource) {
const mustUnselect =
this.state.selectedResources.length === 1 && this.state.selectedResources[0].id === resource.id;
this.setState({ selectedResources: mustUnselect ? [] : [resource] });
}
/**
* Select the given resource in a multiple selection mode
* @param resource
*/
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];
this.setState({ selectedResources });
}
/**
* Select the given resource in a range selection mode
* @param resource
* @returns {void}
*/
selectRange(resource) {
const hasNoSelection = this.state.selectedResources.length === 0;
if (hasNoSelection) {
this.setState({ selectedResources: [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);
}
return this.setState({ selectedResources });
}
}
/**
* Select all the resources
*/
selectAll() {
this.setState({ selectedResources: [...this.state.filteredResources] });
}
/**
* Unselect all the resources
*/
unselectAll() {
this.setState({ selectedResources: [] });
}
/**
* Navigate to the appropriate url after some resources selection operation
*/
redirectAfterSelection(selectedResources, filter) {
// Case of single selected resource
const hasSingleSelectionNow = selectedResources.length === 1;
if (hasSingleSelectionNow) {
const mustRedirect = this.props.location.pathname !== `/app/passwords/view/${selectedResources[0].id}`;
if (mustRedirect) {
this.props.history.push(`/app/passwords/view/${selectedResources[0].id}`);
}
return;
}
// Case of multiple selected resources
const isFolderFilter = filter.type === ResourceWorkspaceFilterTypes.FOLDER;
if (isFolderFilter) {
// Case of folder
const mustRedirect = this.props.location.pathname !== `/app/folders/view/${filter.payload.folder.id}`;
if (mustRedirect) {
this.props.history.push({ pathname: `/app/folders/view/${filter.payload.folder.id}` });
}
return;
}
// Case of resources filtered by expired
const isExpiredFilter = filter.type === ResourceWorkspaceFilterTypes.EXPIRED;
if (isExpiredFilter) {
const mustRedirect = this.props.location.pathname !== `/app/passwords/filter/expired`;
if (mustRedirect) {
this.props.history.push({ pathname: `/app/passwords/filter/expired` });
}
return;
}
// 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 resources sorter given a property name
* @param propertyName
*/
updateSorter(propertyName) {
const hasSortPropertyChanged = this.state.sorter.propertyName !== propertyName;
const asc = hasSortPropertyChanged || !this.state.sorter.asc;
const sorter = new SorterEntity({ propertyName, asc });
this.setState({ sorter }, () => this.updateGridSetting());
}
/**
* Sort the resources given the current sorter
* @param {Array} resources The resources
*/
sort(resources) {
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(getPropValue(s1, key), getPropValue(s2, key)));
const stringSorter = (s1, s2) => (s1 || "").localeCompare(s2 || "");
const arrayStringSorter = (s1, s2) => (s1?.[0] || "").localeCompare(s2?.[0] || "");
const booleanSorter = (s1, s2) => (s1 === s2 ? 0 : s1 ? -1 : 1);
const mapSorter = { favorite: booleanSorter, "metadata.uris": arrayStringSorter };
const sorter = mapSorter[this.state.sorter.propertyName] ?? stringSorter;
const propertySorter = keySorter(this.state.sorter.propertyName, sorter);
if (resources !== null) {
resources.sort(propertySorter);
}
}
/** Resource scrolling **/
/**
* Set the resource to scroll to
* @param resource A resource
*/
scrollToResource(resource) {
this.setState({ scrollTo: { resource } });
}
/**
* Set the folder to scroll to
* @param folder A folder
*/
scrollToFolder(folder) {
this.setState({ scrollTo: { folder } });
}
/**
* Unset the resource to scroll to
*/
scrollNothing() {
this.setState({ scrollTo: {} });
}
/** RESOURCE ACTIVITIES */
/**
* Refresh the activities of the current selected resource
*/
refreshSelectedResourceActivities() {
this.setState((currentState) => ({
refresh: { ...currentState.refresh, activities: true },
}));
}
/**
* Set the resources activitie as refreshed
*/
setResourceActivitiesAsRefreshed() {
this.setState((currentState) => ({
refresh: { ...currentState.refresh, activities: false },
}));
}
/** RESOURCE PERMISSION */
/**
* Refresh the permissions of the current selected resources
*/
refreshSelectedResourcePermissions() {
this.setState((currentState) => ({
refresh: { ...currentState.refresh, permissions: true },
}));
}
/**
* Set the resources permissions as refreshed
*/
setResourcesPermissionsAsRefreshed() {
this.setState((currentState) => ({
refresh: { ...currentState.refresh, permissions: false },
}));
}
/** RESOURCE IMPORT */
/**
* Import the given resource file
* @param resourceFile A resource file to import
*/
import(resourceFile) {
this.setState({ resourceFileToImport: resourceFile });
}
/**
* Update the resource file import result
* @param result The import result
*/
updateImportResult(result) {
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
*/
updateResourcesToExport({ resourcesIds, foldersIds }) {
this.setState({ resourcesToExport: { resourcesIds, foldersIds } });
}
/**
* Handle the columns resources configuration
*
* @return {Promise<void>}
*/
async loadGridResourceSetting() {
const gridUserSettingEntity = await this.gridResourceUserSetting.getSetting();
// Merge the columns setting collection by ID
const columnsResourceSetting = ColumnsResourceSettingCollection.createFromDefault(
gridUserSettingEntity?.columnsSetting,
{ keepUnknownValue: false },
);
if (!this.props.context.siteSettings.canIUse("totpResourceTypes")) {
columnsResourceSetting.removeById(ColumnModelTypes.TOTP);
}
if (!this.props.passwordExpiryContext.isFeatureEnabled()) {
columnsResourceSetting.removeById(ColumnModelTypes.EXPIRED);
}
if (!this.canUseFolders) {
columnsResourceSetting.removeById(ColumnModelTypes.LOCATION);
}
const sorter = gridUserSettingEntity?.sorter || this.defaultSorter;
const rowsSet