passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
1,162 lines (1,065 loc) • 41 kB
JavaScript
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) Passbolt SARL (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) Passbolt SARL (https://www.passbolt.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.passbolt.com Passbolt(tm)
* @since 2.11.0
*/
import PropTypes from "prop-types";
import React from "react";
import { withAppContext } from "../../../../shared/context/AppContext/AppContext";
import { ResourceWorkspaceFilterTypes, withResourceWorkspace } from "../../../contexts/ResourceWorkspaceContext";
import debounce from "debounce-promise";
import { withActionFeedback } from "../../../contexts/ActionFeedbackContext";
import { withRouter } from "react-router-dom";
import DisplayResourcesListContextualMenu from "./DisplayResourcesListContextualMenu";
import { withContextualMenu } from "../../../contexts/ContextualMenuContext";
import { Trans, withTranslation } from "react-i18next";
import { withDrag } from "../../../contexts/DragContext";
import DisplayDragResource from "./DisplayDragResource";
import { withRbac } from "../../../../shared/context/Rbac/RbacContext";
import { uiActions } from "../../../../shared/services/rbacs/uiActionEnumeration";
import GridTable from "../../../../shared/components/Table/GridTable";
import CellFavorite from "../../../../shared/components/Table/CellFavorite";
import CellUris from "../../../../shared/components/Table/CellUris";
import CellPassword from "../../../../shared/components/Table/CellPassword";
import CellButton from "../../../../shared/components/Table/CellButton";
import CellHeaderCheckbox from "../../../../shared/components/Table/CellHeaderCheckbox";
import CellCheckbox from "../../../../shared/components/Table/CellChecbox";
import ColumnCheckboxModel from "../../../../shared/models/column/ColumnCheckboxModel";
import ColumnFavoriteModel from "../../../../shared/models/column/ColumnFavoriteModel";
import ColumnNameModel from "../../../../shared/models/column/ColumnNameModel";
import ColumnUsernameModel from "../../../../shared/models/column/ColumnUsernameModel";
import ColumnPasswordModel from "../../../../shared/models/column/ColumnPasswordModel";
import ColumnUriModel from "../../../../shared/models/column/ColumnUriModel";
import ColumnModifiedModel from "../../../../shared/models/column/ColumnModifiedModel";
import ColumnModel, { ColumnModelTypes } from "../../../../shared/models/column/ColumnModel";
import { withProgress } from "../../../contexts/ProgressContext";
import CellTotp from "../../../../shared/components/Table/CellTotp";
import ColumnTotpModel from "../../../../shared/models/column/ColumnTotpModel";
import { TotpCodeGeneratorService } from "../../../../shared/services/otp/TotpCodeGeneratorService";
import ColumnExpiredModel from "../../../../shared/models/column/ColumnExpiredModel";
import { withPasswordExpiry } from "../../../contexts/PasswordExpirySettingsContext";
import CellDate from "../../../../shared/components/Table/CellDate";
import CellExpiryDate from "../../../../shared/components/Table/CellExpiryDate";
import CellHeaderDefault from "../../../../shared/components/Table/CellHeaderDefault";
import ColumnLocationModel from "../../../../shared/models/column/ColumnLocationModel";
import ColumnTagsModel from "../../../../shared/models/column/ColumnTagsModel";
import CellLocation from "../../../../shared/components/Table/CellLocation";
import CellTag from "../../../../shared/components/Table/CellTag";
import ResourceTypesCollection from "../../../../shared/models/entity/resourceType/resourceTypesCollection";
import { withResourceTypesLocalStorage } from "../../../../shared/context/ResourceTypesLocalStorageContext/ResourceTypesLocalStorageContext";
import FavoriteSVG from "../../../../img/svg/favorite.svg";
import CellName from "../../../../shared/components/Table/CellName";
import CircleOffSVG from "../../../../img/svg/circle_off.svg";
import memoize from "memoize-one";
import { withClipboard } from "../../../contexts/Clipboard/ManagedClipboardServiceProvider";
import FavoriteServiceWorkerService from "./FavoriteServiceWorkerService";
import Logger from "../../../../shared/utils/logger";
/**
* This component allows to display the filtered resources into a grid
*/
class DisplayResourcesList extends React.Component {
/**
* The grid columns
* @type {array}
*/
defaultColumns = [];
/**
* Default constructor
* @param props Component props
*/
constructor(props) {
super(props);
this.state = this.getDefaultState();
this.initEventHandlers();
this.handleFavoriteClickDebounced = debounce(this.handleFavoriteUpdate, 200);
this.createRefs();
this.favoriteServiceWorkerService = new FavoriteServiceWorkerService(props.context.port);
}
/**
* Returns the component default state
*/
getDefaultState() {
return {
columns: [], // The current list of columns to display.
previewedCellule: {
columnId: null, // The previewed cellule column id
resourceId: null, // The previewed cellule resource id
},
plaintextSecretDto: null, // The plain text secret dto.
};
}
/**
* Initialize the component event handlers
*/
initEventHandlers() {
this.handleSelectAllChange = this.handleSelectAllChange.bind(this);
this.handleResourceSelected = this.handleResourceSelected.bind(this);
this.handleResourceRightClick = this.handleResourceRightClick.bind(this);
this.handleResourceDragStartEvent = this.handleResourceDragStartEvent.bind(this);
this.handleDragEndEvent = this.handleDragEndEvent.bind(this);
this.handleCheckboxWrapperClick = this.handleCheckboxWrapperClick.bind(this);
this.handleCopyPasswordClick = this.handleCopyPasswordClick.bind(this);
this.handleCopyUsernameClick = this.handleCopyUsernameClick.bind(this);
this.hasIconVisible = this.hasIconVisible.bind(this);
this.handleFavoriteClick = this.handleFavoriteClick.bind(this);
this.handleSortByColumnClick = this.handleSortByColumnClick.bind(this);
this.handleChangeColumnsSettings = this.handleChangeColumnsSettings.bind(this);
this.handleGoToResourceUriClick = this.handleGoToResourceUriClick.bind(this);
this.handlePreviewPasswordButtonClick = this.handlePreviewPasswordButtonClick.bind(this);
this.handleCopyTotpClick = this.handleCopyTotpClick.bind(this);
this.handlePreviewTotpButtonClick = this.handlePreviewTotpButtonClick.bind(this);
this.getPreviewPassword = this.getPreviewPassword.bind(this);
this.getPreviewTotp = this.getPreviewTotp.bind(this);
this.isPasswordResources = this.isPasswordResources.bind(this);
this.isTotpResources = this.isTotpResources.bind(this);
this.handleLocationClick = this.handleLocationClick.bind(this);
this.handleTagClick = this.handleTagClick.bind(this);
}
/**
* Init the grid columns.
*/
initColumns() {
this.defaultColumns.push(
new ColumnCheckboxModel({
cellRenderer: { component: CellCheckbox, props: { onClick: this.handleCheckboxWrapperClick } },
headerCellRenderer: { component: CellHeaderCheckbox, props: { onChange: this.handleSelectAllChange } },
}),
);
this.defaultColumns.push(
new ColumnFavoriteModel({
cellRenderer: { component: CellFavorite, props: { onClick: this.handleFavoriteClick } },
headerCellRenderer: { component: FavoriteSVG },
}),
);
this.defaultColumns.push(
new ColumnNameModel({
cellRenderer: {
component: CellName,
props: {
hasAttentionRequiredFeature: this.hasAttentionRequiredFeature,
hasIconVisibleCallback: this.hasIconVisible,
},
},
headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("Name") } },
}),
);
if (this.props.passwordExpiryContext.isFeatureEnabled()) {
this.defaultColumns.push(
new ColumnExpiredModel({
cellRenderer: { component: CellExpiryDate, props: { locale: this.props.context.locale, t: this.props.t } },
headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("Expiry") } },
}),
);
}
this.defaultColumns.push(
new ColumnUsernameModel({
cellRenderer: { component: CellButton, props: { onClick: this.handleCopyUsernameClick } },
headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("Username") } },
}),
);
this.defaultColumns.push(
new ColumnPasswordModel({
cellRenderer: {
component: CellPassword,
props: {
title: this.translate("Click to copy"),
getPreviewPassword: this.getPreviewPassword,
canCopy: this.canCopySecret,
canPreview: this.canPreviewSecret,
onPasswordClick: this.handleCopyPasswordClick,
onPreviewPasswordClick: this.handlePreviewPasswordButtonClick,
hasPassword: this.isPasswordResources,
},
},
headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("Password") } },
}),
);
if (this.props.context.siteSettings.canIUse("totpResourceTypes")) {
this.defaultColumns.push(
new ColumnTotpModel({
cellRenderer: {
component: CellTotp,
props: {
title: this.translate("Click to copy"),
getPreviewTotp: this.getPreviewTotp,
canCopy: this.canCopySecret,
canPreview: this.canPreviewSecret,
onTotpClick: this.handleCopyTotpClick,
onPreviewTotpClick: this.handlePreviewTotpButtonClick,
hasTotp: this.isTotpResources,
},
},
headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("TOTP") } },
}),
);
}
this.defaultColumns.push(
new ColumnUriModel({
cellRenderer: { component: CellUris, props: { onClick: this.handleGoToResourceUriClick } },
headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("URI") } },
}),
);
if (this.canUseTags) {
this.defaultColumns.push(
new ColumnTagsModel({
cellRenderer: { component: CellTag, props: { onTagClick: this.handleTagClick } },
headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("Tags") } },
}),
);
}
this.defaultColumns.push(
new ColumnModifiedModel({
cellRenderer: { component: CellDate, props: { locale: this.props.context.locale, t: this.props.t } },
headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("Modified") } },
}),
);
if (this.canUseFolders) {
this.defaultColumns.push(
new ColumnLocationModel({
getValue: (resource) => this.props.context.getHierarchyFolderCache(resource.folder_parent_id),
cellRenderer: { component: CellLocation, props: { onClick: this.handleLocationClick, t: this.props.t } },
headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("Location") } },
}),
);
}
}
/**
* Component did mount
*/
async componentDidMount() {
await this.props.passwordExpiryContext.findSettings();
this.initColumns();
// If columns resource settings already loaded merge columns
if (this.columnsResourceSetting !== null) {
this.mergeAndSortColumns();
}
}
/**
* Merge and sort columns
*/
mergeAndSortColumns() {
// Get the column with id as a key from the column to merge
const columnsResourceSetting = this.columnsResourceSetting.toHashTable();
// Merge the column values
const columns = this.defaultColumns.map((column) =>
Object.assign(new ColumnModel(column), columnsResourceSetting[column.id]),
);
// Sort the position of the column, the column with no position will be at the beginning
columns.sort((columnA, columnB) => ((columnA.position || 0) < (columnB.position || 0) ? -1 : 1));
this.setState({ columns });
}
/**
* Returns true if the icon should be visible
*/
hasIconVisible() {
return this.memoizedHasIconVisible(this.columnsResourceSetting);
}
/**
* Memoized hasIconVisible function.
* @param {array} columnsResourceSetting
* @returns {boolean}
*/
memoizedHasIconVisible = memoize((columns) => columns.getFirst("id", ColumnModelTypes.ICON).show);
/**
* Whenever the component has been updated
*
* @param prevProps The previous props
*/
componentDidUpdate(prevProps) {
this.handleResourceScroll();
// Column resource settings have changed
const hasColumnsResourceViewChange = this.columnsResourceSetting?.hasDifferentShowValue(
prevProps.resourceWorkspaceContext.columnsResourceSetting,
);
const hasColumnsSettingsChanged =
prevProps.resourceWorkspaceContext.columnsResourceSetting !==
this.props.resourceWorkspaceContext.columnsResourceSetting;
if (hasColumnsSettingsChanged || hasColumnsResourceViewChange) {
this.mergeAndSortColumns();
}
}
/**
* Returns true if the component should be re-rendered
*/
shouldComponentUpdate(nextProps, nextState) {
const { filteredResources, selectedResources, sorter, scrollTo, columnsResourceSetting } =
nextProps.resourceWorkspaceContext;
const hasFilteredResourcesChanged = this.props.resourceWorkspaceContext.filteredResources !== filteredResources;
const hasBothSingleSelection =
selectedResources.length === 1 && this.props.resourceWorkspaceContext.selectedResources.length === 1;
const hasSingleSelectedResourceChanged =
hasBothSingleSelection && selectedResources[0].id !== this.props.resourceWorkspaceContext.selectedResources[0].id;
const hasSelectedResourcesLengthChanged =
this.props.resourceWorkspaceContext.selectedResources.length !== selectedResources.length;
const hasSorterChanged = sorter !== this.props.resourceWorkspaceContext.sorter;
const hasResourceToScrollChange = Boolean(scrollTo.resource && scrollTo.resource.id);
const hasResourcePreviewSecretChange = nextState.previewedCellule !== this.state.previewedCellule;
const hasResourceColumnsChange = nextState.columns !== this.state.columns;
const hasColumnOrderChanged =
nextProps.resourceWorkspaceContext.columnsResourceSetting !==
this.props.resourceWorkspaceContext.columnsResourceSetting;
const hasColumnsResourceViewChange = columnsResourceSetting?.hasDifferentShowValue(
this.props.resourceWorkspaceContext.columnsResourceSetting,
);
const hasRowsSettingChanged =
nextProps.resourceWorkspaceContext.rowsSetting?.height !==
this.props.resourceWorkspaceContext.rowsSetting?.height;
const mustHidePreviewPassword =
hasFilteredResourcesChanged ||
hasSingleSelectedResourceChanged ||
hasSelectedResourcesLengthChanged ||
hasSorterChanged;
if (mustHidePreviewPassword) {
this.hidePreviewedCellule();
}
return (
hasFilteredResourcesChanged ||
hasSelectedResourcesLengthChanged ||
hasSingleSelectedResourceChanged ||
hasSorterChanged ||
hasResourceToScrollChange ||
hasResourceColumnsChange ||
hasColumnsResourceViewChange ||
hasResourcePreviewSecretChange ||
hasColumnOrderChanged ||
hasRowsSettingChanged
);
}
/**
* Create DOM nodes or React elements references in order to be able to access them programmatically.
*/
createRefs() {
this.tableviewRef = React.createRef();
this.listRef = React.createRef();
}
/**
* 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)
);
}
/**
* Check if the user can use tags.
* @returns {boolean}
*/
get canUseTags() {
return this.props.context.siteSettings.canIUse("tags") && this.props.rbacContext.canIUseAction(uiActions.TAGS_USE);
}
/**
* Handle the All resources selection
* @param event The DOM event
*/
handleSelectAllChange(event) {
const checked = event.target.checked;
const operationName = checked ? "all" : "none";
this.props.resourceWorkspaceContext.onResourceSelected[operationName]();
}
/**
* Handle the resource selection
* @param event The DOM event
* @param resource The selected resource
*/
async handleResourceSelected(event, resource) {
await this.selectResource(resource, event);
}
/**
* Handle when the user selects an element in the grid.
* @param {array} resources The selected resources
*/
handleSelectResources(resources) {
const selectedFolders = [];
const selectedResources = resources;
this.setState({ selectedFolders, selectedResources }, () => {
if (resources.length === 1) {
this.props.history.push(`/app/passwords/view/${resources[0].id}`);
}
});
}
/**
* Handle the right click on a resource
* @param event
* @param resource
*/
handleResourceRightClick(event, resource) {
this.handleSelectResources([resource]);
const left = event.pageX;
const top = event.pageY;
const contextualMenuProps = { left, top, resource };
this.props.contextualMenuContext.show(DisplayResourcesListContextualMenu, contextualMenuProps);
}
async handleCheckboxWrapperClick(event, resource) {
const isRangeSelection = event && event.shiftKey;
if (isRangeSelection) {
await this.props.resourceWorkspaceContext.onResourceSelected.range(resource);
} else {
await this.props.resourceWorkspaceContext.onResourceSelected.multiple(resource);
}
}
/**
* Handles the initial resource scroll ( with a specific manual resource url /password/view/:id )
*/
handleResourceScroll() {
const resourceToScroll = this.props.resourceWorkspaceContext.scrollTo.resource;
const hasNotEmptyRange = this.listRef.current?.getVisibleRange().some((value) => value);
if (resourceToScroll && hasNotEmptyRange) {
this.scrollTo(resourceToScroll.id);
this.props.resourceWorkspaceContext.onResourceScrolled();
}
}
/**
* Returns the current list of filtered resources to display
*/
get resources() {
return this.props.resourceWorkspaceContext.filteredResources;
}
/**
* Returns the current list of selected resources
*/
get selectedResources() {
return this.props.resourceWorkspaceContext.selectedResources;
}
/**
* Get selected resource ids
* @return {*}
*/
get selectedResourcesIds() {
const getIds = (resource) => resource.id;
return this.selectedResources.map(getIds);
}
/**
* get columns resource setting
* @return {ColumnsResourceSettingCollection}
*/
get columnsResourceSetting() {
return this.props.resourceWorkspaceContext.columnsResourceSetting;
}
/**
* Get the columns to display
* @return {[]}
*/
get columnsFiltered() {
const filteredByColumnToDisplay = (column) => column.id === "checkbox" || column.show;
return this.state.columns.filter(filteredByColumnToDisplay);
}
/**
* Get the previewed password
* @param {object} resource The resource
* @return {string|undefined}
*/
getPreviewPassword(resource) {
return this.isCellulePreviewed("password", resource.id) ? this.state.plaintextSecretDto?.password : undefined;
}
/**
* Get preview totp
* @param {object} resource The resource
* @return {object|undefined}
*/
getPreviewTotp(resource) {
return this.isCellulePreviewed("totp", resource.id) ? this.state.plaintextSecretDto?.totp : undefined;
}
/**
* Handle copy username click
* @param username
* @return {Promise<void>}
*/
async handleCopyUsernameClick(username) {
await this.props.clipboardContext.copy(username, this.translate("The username has been copied to clipboard."));
}
/**
* Handle copy password button click.
* @param {object} resource The resource
*/
async handleCopyPasswordClick(resource) {
await this.copyPasswordToClipboard(resource.id);
}
/**
* Handle preview password button click.
* @param {object} resource The resource to preview the password for
*/
async handlePreviewPasswordButtonClick(resource) {
await this.togglePreviewPassword(resource.id);
}
/**
* Handle copy totp button click.
* @param {object} resource The resource
*/
async handleCopyTotpClick(resource) {
await this.copyTotpToClipboard(resource.id);
}
/**
* Handle preview totp button click.
* @param {object} resource The resource to preview the totp for
*/
async handlePreviewTotpButtonClick(resource) {
await this.togglePreviewTotp(resource.id);
}
/**
* Handle copy totp
* @param resource The resource
* @return {Promise<void>}
*/
async copyTotpToClipboard(resourceId) {
let plaintextSecretDto, code;
const isTotpPreviewed = this.isCellulePreviewed("totp", resourceId);
if (isTotpPreviewed) {
plaintextSecretDto = this.state.plaintextSecretDto;
} else {
this.props.progressContext.open(this.props.t("Decrypting secret"));
try {
plaintextSecretDto = await this.decryptResourceSecret(resourceId);
} catch (error) {
if (error.name !== "UserAbortsOperationError") {
this.props.actionFeedbackContext.displayError(error.message);
}
}
this.props.progressContext.close();
}
if (!plaintextSecretDto) {
return;
}
if (!plaintextSecretDto.totp) {
await this.props.actionFeedbackContext.displayError(
this.translate("The TOTP is empty and cannot be copied to clipboard."),
);
return;
}
try {
code = TotpCodeGeneratorService.generate(plaintextSecretDto.totp);
} catch (error) {
Logger.error(error);
await this.props.actionFeedbackContext.displayError(this.translate("Unable to copy the TOTP"));
return;
}
await this.props.clipboardContext.copyTemporarily(code, this.translate("The TOTP has been copied to clipboard."));
await this.props.resourceWorkspaceContext.onResourceCopied();
}
/**
* Copy a resource secret to clipboard.
* @param {string} resourceId The target resource id
* @returns {Promise<void>}
*/
async copyPasswordToClipboard(resourceId) {
let plaintextSecretDto;
if (this.isCellulePreviewed("password", resourceId)) {
plaintextSecretDto = this.state.plaintextSecretDto;
} else {
this.props.progressContext.open(this.props.t("Decrypting secret"));
try {
plaintextSecretDto = await this.decryptResourceSecret(resourceId);
} catch (error) {
if (error.name !== "UserAbortsOperationError") {
this.props.actionFeedbackContext.displayError(error.message);
}
}
this.props.progressContext.close();
}
if (!plaintextSecretDto) {
return;
}
if (!plaintextSecretDto?.password?.length) {
await this.props.actionFeedbackContext.displayWarning(
this.translate("The password is empty and cannot be copied to clipboard."),
);
return;
}
await this.props.clipboardContext.copyTemporarily(
plaintextSecretDto.password,
this.translate("The password has been copied to clipboard."),
);
await this.props.resourceWorkspaceContext.onResourceCopied();
}
/**
* Toggle preview password for a given resource
* @param {string} resourceId The resource id to preview the password for
* @returns {Promise<void>}
*/
async togglePreviewPassword(resourceId) {
const isPasswordPreviewedPreviewed = this.isCellulePreviewed("password", resourceId);
this.hidePreviewedCellule();
if (!isPasswordPreviewedPreviewed) {
await this.previewPassword(resourceId);
await this.props.resourceWorkspaceContext.onResourcePreviewed();
}
}
/**
* Hide the previewed cellule.
*/
hidePreviewedCellule() {
const previewedCellule = null;
const plaintextSecretDto = null;
this.setState({ previewedCellule, plaintextSecretDto });
}
/**
* Preview password for a given resource
* @param {string} resourceId The resource id to preview the password for
* @returns {Promise<void>}
*/
async previewPassword(resourceId) {
let plaintextSecretDto;
this.props.progressContext.open(this.props.t("Decrypting secret"));
try {
plaintextSecretDto = await this.decryptResourceSecret(resourceId);
} catch (error) {
if (error.name !== "UserAbortsOperationError") {
this.props.actionFeedbackContext.displayError(error.message);
}
}
this.props.progressContext.close();
if (!plaintextSecretDto) {
return;
}
if (!plaintextSecretDto?.password?.length) {
plaintextSecretDto.password = "";
}
const columnId = "password";
const previewedCellule = { resourceId, columnId };
this.setState({ previewedCellule, plaintextSecretDto });
}
/**
* Toggle preview totp for a given resource
* @param {string} resourceId The resource id to preview the password for
* @returns {Promise<void>}
*/
async togglePreviewTotp(resourceId) {
const isTotpPreviewedPreviewed = this.isCellulePreviewed("totp", resourceId);
this.hidePreviewedCellule();
if (!isTotpPreviewedPreviewed) {
await this.previewTotp(resourceId);
await this.props.resourceWorkspaceContext.onResourcePreviewed();
}
}
/**
* Preview totp
* @param {string} resourceId The resource id to preview
*/
async previewTotp(resourceId) {
let plaintextSecretDto;
this.props.progressContext.open(this.props.t("Decrypting secret"));
try {
plaintextSecretDto = await this.decryptResourceSecret(resourceId);
} catch (error) {
if (error.name !== "UserAbortsOperationError") {
this.props.actionFeedbackContext.displayError(error.message);
}
}
this.props.progressContext.close();
if (!plaintextSecretDto) {
return;
}
if (!plaintextSecretDto.totp) {
await this.props.actionFeedbackContext.displayError(this.translate("The TOTP is empty and cannot be previewed."));
return;
}
const columnId = "totp";
const previewedCellule = { resourceId, columnId };
this.setState({ previewedCellule, plaintextSecretDto });
}
/**
* Decrypt the resource secret
* @param {string} resourceId The target resource id
* @returns {Promise<object>} The secret in plaintext format
* @throw UserAbortsOperationError If the user cancel the operation
*/
decryptResourceSecret(resourceId) {
return this.props.context.port.request("passbolt.secret.find-by-resource-id", resourceId);
}
async handleFavoriteClick(resource) {
await this.handleFavoriteClickDebounced(resource);
}
handleFavoriteUpdate(resource) {
if (resource.favorite === null) {
this.favoriteResource(resource);
} else {
this.unfavoriteResource(resource);
}
}
/**
* Handle the resource sorter change
* @param sortProperty The resource property to sort on
*/
async handleSortByColumnClick(sortProperty) {
this.props.resourceWorkspaceContext.onSorterChanged(sortProperty);
}
/**
* Handle change columns settings
* @param columns
*/
handleChangeColumnsSettings(columns) {
// remove first column (checkbox is fixed)
columns.shift();
this.props.resourceWorkspaceContext.onChangeColumnsSettings(columns);
}
/**
* Handle the drag start on the selected resource
* @param event The DOM event
* @param resource The selected resource
* @param isSelected is resource selected
* @returns {Promise<void>}
*/
async handleResourceDragStartEvent(event, resource, isSelected) {
if (!isSelected) {
await this.props.resourceWorkspaceContext.onResourceSelected.single(resource);
}
const draggedItems = { resources: this.props.resourceWorkspaceContext.selectedResources, folders: [] };
this.props.dragContext.onDragStart(event, DisplayDragResource, draggedItems);
}
/**
* Handle when the user stop dragging content.
*/
handleDragEndEvent() {
this.props.dragContext.onDragEnd();
}
/**
* Select the resource given the selection event.
* If no event is provided, the selection is considered as multiple
* @param resource
* @param event
*/
async selectResource(resource, event) {
const isMultipleSelection = event && this.isMacOS ? event.metaKey : event.ctrlKey;
const isRangeSelection = event && event.shiftKey;
const hasNoEvent = !event;
if (hasNoEvent || isMultipleSelection) {
await this.props.resourceWorkspaceContext.onResourceSelected.multiple(resource);
} else if (isRangeSelection) {
await this.props.resourceWorkspaceContext.onResourceSelected.range(resource);
} else {
await this.props.resourceWorkspaceContext.onResourceSelected.single(resource);
}
}
/**
* Is mac os system
* @returns {boolean}
*/
get isMacOS() {
// userAgentData only available on chromium
const platform = navigator.userAgentData ? navigator.userAgentData.platform : navigator.userAgent;
return /mac/i.test(platform);
}
async favoriteResource(resource) {
try {
await this.favoriteServiceWorkerService.addToFavorites(resource.id);
this.displaySuccessNotification(this.translate("The password has been added to favorites"));
} catch (error) {
this.displayErrorNotification(error.message);
}
}
async unfavoriteResource(resource) {
try {
await this.favoriteServiceWorkerService.removeFromFavorites(resource.id);
this.displaySuccessNotification(this.translate("The password has been removed from favorites"));
} catch (error) {
this.displayErrorNotification(error.message);
}
}
/**
* Handle the user click on location folder from the grid.
*/
handleLocationClick(folderId) {
if (folderId) {
const filterIsDifferent = this.props.resourceWorkspaceContext.filter.payload?.folder?.id !== folderId;
if (filterIsDifferent) {
this.props.history.push(`/app/folders/view/${folderId}`);
}
} else {
// Case of root folder
const filter = { type: ResourceWorkspaceFilterTypes.ROOT_FOLDER };
this.props.history.push(`/app/passwords`, { filter });
}
}
/**
* Handle the user click on a tag from the grid.
* @param {object} tag The clicked tag
*/
handleTagClick(tag) {
const filter = { type: ResourceWorkspaceFilterTypes.TAG, payload: { tag } };
this.props.history.push({ pathname: "/app/passwords", state: { filter } });
}
/**
* Display success notification (toaster)
* @param message
*/
displaySuccessNotification(message) {
this.props.actionFeedbackContext.displaySuccess(message);
}
/**
* Display error notification (toaster)
* @param message
*/
displayErrorNotification(message) {
this.props.actionFeedbackContext.displayError(message);
}
/**
* Triggers a scroll of the grid to a resource given its id, if the resource is not visible yet.
* @param {string} resourceId
*/
scrollTo(resourceId) {
const resourceIndex = this.resources.findIndex((resource) => resource.id === resourceId);
const [visibleStartIndex, visibleEndIndex] = this.listRef.current.getVisibleRange();
const isInvisible = resourceIndex < visibleStartIndex || resourceIndex > visibleEndIndex;
if (isInvisible) {
// Important to have the -1 to show the selected column behind the header with sticky position
this.listRef.current.scrollTo(resourceIndex - 1);
}
}
/**
* Whenever the user wants to follow a resource uri.
* @param {string} uri The uri
*/
handleGoToResourceUriClick(uri) {
this.props.resourceWorkspaceContext.onGoToResourceUriRequested(uri);
}
/**
* Check if the password of the given resource is previewed.
* @param {string} columnId The column id.
* @param {string} resourceId The resource id.
* @returns {boolean}
*/
isCellulePreviewed(columnId, resourceId) {
return this.state.previewedCellule?.columnId === columnId && this.state.previewedCellule?.resourceId === resourceId;
}
/**
* Is password resource
* @param resource
* @return {boolean}
*/
isPasswordResources(resource) {
// TODO: How to handle if resource type is not enabled or not loaded yet ?
return this.props.resourceTypes?.getFirstById(resource.resource_type_id)?.hasPassword();
}
/**
* Is TOTP resource
* @param resource
* @return {boolean}
*/
isTotpResources(resource) {
return this.props.resourceTypes?.getFirstById(resource.resource_type_id)?.hasTotp();
}
/**
* Returns true if the "attention required" column feature is available.
* @returns {boolean}
*/
get hasAttentionRequiredFeature() {
return this.props.passwordExpiryContext.isFeatureEnabled();
}
/**
* Can preview secret
* @return {boolean}
*/
get canPreviewSecret() {
return (
this.props.context.siteSettings.canIUse("previewPassword") &&
this.props.rbacContext.canIUseAction(uiActions.SECRETS_PREVIEW)
);
}
/**
* Can copy secret
* @return {boolean}
*/
get canCopySecret() {
return this.props.rbacContext.canIUseAction(uiActions.SECRETS_COPY);
}
/**
* Is ready
* @return {boolean}
*/
get isReady() {
return this.resources !== null;
}
/**
* Is grid ready and not empty
* @return {boolean}
*/
get isGridReady() {
return this.isReady && this.resources.length !== 0 && this.columnsFiltered.length !== 0;
}
/**
* Get the translate function
* @returns {function(...[*]=)}
*/
get translate() {
return this.props.t;
}
render() {
const isEmpty = this.isReady && this.resources.length === 0;
const filterType = this.props.resourceWorkspaceContext.filter.type;
return (
<>
{isEmpty && (
<div className="tableview empty">
{filterType === ResourceWorkspaceFilterTypes.TEXT && (
<div className="empty-content">
<CircleOffSVG />
<div className="message">
<h1>
<Trans>None of your passwords matched this search.</Trans>
</h1>
<p>
<Trans>Try another search or use the left panel to navigate into your passwords.</Trans>
</p>
</div>
</div>
)}
{filterType === ResourceWorkspaceFilterTypes.FAVORITE && (
<div className="empty-content">
<CircleOffSVG />
<div className="message">
<h1>
<Trans>None of your passwords are yet marked as favorite.</Trans>
</h1>
<p>
<Trans>Add stars to passwords you want to easily find later.</Trans>
</p>
</div>
</div>
)}
{filterType === ResourceWorkspaceFilterTypes.GROUP && (
<div className="empty-content">
<CircleOffSVG />
<div className="message">
<h1>
<Trans>No passwords are shared with this group yet.</Trans>
</h1>
<p>
<Trans>
Share a password with this group or wait for a team member to share one with this group.
</Trans>
</p>
</div>
</div>
)}
{(filterType === ResourceWorkspaceFilterTypes.FOLDER ||
filterType === ResourceWorkspaceFilterTypes.ROOT_FOLDER) && (
<div className="empty-content">
<CircleOffSVG />
<div className="message">
<h1>
<Trans>No passwords in this folder yet.</Trans>
</h1>
<p>
<Trans>It does feel a bit empty here.</Trans>
</p>
</div>
</div>
)}
{filterType === ResourceWorkspaceFilterTypes.SHARED_WITH_ME && (
<div className="empty-content">
<CircleOffSVG />
<div className="message">
<h1>
<Trans>No passwords are shared with you yet.</Trans>
</h1>
<p>
<Trans>It does feel a bit empty here.</Trans>
<Trans>Wait for a team member to share a password with you.</Trans>
</p>
</div>
</div>
)}
{filterType === ResourceWorkspaceFilterTypes.EXPIRED && (
<div className="empty-content">
<CircleOffSVG />
<div className="message">
<h1>
<Trans>No passwords have expired yet.</Trans>
</h1>
<p>
<Trans>It does feel a bit empty here.</Trans>
<Trans>Wait for a password to expire.</Trans>
</p>
</div>
</div>
)}
{(filterType === ResourceWorkspaceFilterTypes.ITEMS_I_OWN ||
filterType === ResourceWorkspaceFilterTypes.ALL) && (
<div className="empty-content">
<CircleOffSVG />
<div className="message">
<h1>
<Trans>Welcome to passbolt!</Trans>
</h1>
<p>
<Trans>It does feel a bit empty here.</Trans>
<Trans>Create your first password or wait for a team member to share one with you.</Trans>
</p>
</div>
</div>
)}
{filterType === ResourceWorkspaceFilterTypes.PRIVATE && (
<div className="empty-content">
<CircleOffSVG />
<div className="message">
<h1>
<Trans>Welcome to passbolt!</Trans>
</h1>
<p>
<Trans>It does feel a bit empty here.</Trans>
<Trans>Create your first password.</Trans>
</p>
</div>
</div>
)}
</div>
)}
{this.isGridReady && (
<GridTable
columns={this.columnsFiltered}
rows={this.resources}
rowsSetting={this.props.resourceWorkspaceContext.rowsSetting}
sorter={this.props.resourceWorkspaceContext.sorter}
onSortChange={this.handleSortByColumnClick}
onChange={this.handleChangeColumnsSettings}
onRowClick={this.handleResourceSelected}
onRowContextMenu={this.handleResourceRightClick}
onRowDragStart={this.handleResourceDragStartEvent}
onRowDragEnd={this.handleDragEndEvent}
selectedRowsIds={this.selectedResourcesIds}
rowsRef={this.listRef}
></GridTable>
)}
</>
);
}
}
DisplayResourcesList.propTypes = {
context: PropTypes.any, // The app context
rbacContext: PropTypes.any, // The role based access control context
resourceWorkspaceContext: PropTypes.any,
resourceTypes: PropTypes.instanceOf(ResourceTypesCollection), // The resource types collection
actionFeedbackContext: PropTypes.any, // The action feedback context
contextualMenuContext: PropTypes.any, // The contextual menu context
passwordExpiryContext: PropTypes.object, // the password expiry context
progressContext: PropTypes.any, // The progress context
history: PropTypes.any,
dragContext: PropTypes.any,
clipboardContext: PropTypes.object, // the clipboard service provider
t: PropTypes.func, // The translation function
};
export default withAppContext(
withClipboard(
withRouter(
withRbac(
withActionFeedback(
withContextualMenu(
withResourceWorkspace(
withResourceTypesLocalStorage(
withPasswordExpiry(withDrag(withProgress(withTranslation("common")(DisplayResourcesList)))),
),
),
),
),
),
),
),
);