passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
435 lines (395 loc) • 14.4 kB
JavaScript
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) 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) 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 React from "react";
import PropTypes from "prop-types";
import {withAppContext} from "../../../contexts/AppContext";
import {withActionFeedback} from "../../../contexts/ActionFeedbackContext";
import {withLoading} from "../../../contexts/LoadingContext";
import Tooltip from "../../Common/Tooltip/Tooltip";
import {withResourceWorkspace} from "../../../contexts/ResourceWorkspaceContext";
import {Trans, withTranslation} from "react-i18next";
import Icon from "../../../../shared/components/Icons/Icon";
/**
* This component allows the current user to edit the description of a resource
*/
class EditResourceDescription extends React.Component {
/**
* Constructor
* @param {Object} props
*/
constructor(props) {
super(props);
this.state = this.getDefaultState();
this.bindCallbacks();
this.createInputRef();
}
/**
* Get default state
* @returns {*}
*/
getDefaultState() {
return {
encryptDescription: false,
description: undefined, // description of the resource
plaintextDto: undefined, // description of the resource
loading: true, // component loading
processing: false, // component processing
error: "" // error to display
};
}
/**
* Bind callbacks methods
*/
bindCallbacks() {
this.handleEditorClickEvent = this.handleEditorClickEvent.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleDescriptionToggle = this.handleDescriptionToggle.bind(this);
}
/**
* Create DOM nodes or React elements references in order to be able to access them programmatically.
*/
createInputRef() {
this.elementRef = React.createRef();
this.textareaRef = React.createRef();
}
componentDidMount() {
this.handleOutsideEditorClickEvent();
const state = {
loading: false,
plaintextDto: this.props.plaintextDto,
description: this.props.description,
encryptDescription: this.mustEncryptDescription()
};
this.setState(state, this.setFocusOnDescriptionEditor.bind(this));
}
componentWillUnmount() {
this.removeOutsideEditorClickEvent();
}
/*
* =============================================================
* Resource type helpers
* =============================================================
*/
/**
* Must the description be kept encrypted?
* @returns {boolean}
*/
mustEncryptDescription() {
return this.props.context.resourceTypesSettings.mustEncryptDescription(this.props.resource.resource_type_id);
}
/*
* =============================================================
* Getter helpers
* =============================================================
*/
/**
* @returns {ResourceTypesSettings}
*/
get resourceTypesSettings() {
return this.props.context.resourceTypesSettings;
}
/*
* =============================================================
* Resource type helpers
* =============================================================
*/
isEncryptedDescriptionEnabled() {
return this.resourceTypesSettings.isEncryptedDescriptionEnabled();
}
areResourceTypesEnabled() {
return this.resourceTypesSettings.areResourceTypesEnabled();
}
/**
* @returns {string}
*/
get description() {
return this.state.description;
}
/**
* @returns {}
*/
get plaintextDto() {
return this.state.plaintextDto;
}
/*
* =============================================================
* Save the description
* =============================================================
*/
/**
* Save the changes.
*/
async save() {
this.setState({processing: true});
try {
await this.updateResource();
await this.props.actionFeedbackContext.displaySuccess(this.translate("The description has been updated successfully"));
await this.props.resourceWorkspaceContext.onResourceDescriptionEdited();
this.close(this.state.description);
} catch (error) {
// It can happen when the user has closed the passphrase entry dialog by instance.
if (error.name === "UserAbortsOperationError") {
this.setState({processing: false});
} else {
// Unexpected error occurred.
console.error(error);
this.setState({
error: error.message,
processing: false
});
}
}
}
/**
* Update the resource
* @returns {Promise<Object>} updated resource
*/
async updateResource() {
// Resource types enabled but legacy type requested
if (!this.state.encryptDescription) {
return this.updateCleartextDescription();
}
return this.updateWithEncryptedDescription();
}
/**
* Update the resource (LEGACY)
* @returns {Promise<Object>} updated resource
* @deprecated will be removed when v2 support is dropped
*/
async updateCleartextDescription() {
const resourceDto = {...this.props.resource};
resourceDto.description = this.description;
return this.props.context.port.request("passbolt.resources.update", resourceDto, null);
}
/**
* Update the resource with encrypted description content type
* @returns {Promise<Object>} updated resource
*/
async updateWithEncryptedDescription() {
const resourceDto = {...this.props.resource};
resourceDto.description = '';
resourceDto.resource_type_id = this.props.context.resourceTypesSettings.findResourceTypeIdBySlug(
this.props.context.resourceTypesSettings.DEFAULT_RESOURCE_TYPES_SLUGS.PASSWORD_AND_DESCRIPTION
);
let plaintextDto = {};
if (this.plaintextDto === undefined) {
const password = await this.props.context.port.request("passbolt.secret.decrypt", resourceDto.id, {showProgress: false});
plaintextDto.password = password;
} else {
plaintextDto = {...this.plaintextDto};
}
plaintextDto.description = this.description;
await this.setState({plaintextDto});
return this.props.context.port.request("passbolt.resources.update", resourceDto, plaintextDto);
}
/*
* =============================================================
* Out of widget actions
* =============================================================
*/
/**
* Toggle the editor back to display mode
* @param description The description to display
* @returns {string} Send back the updated description and plaintextDto to avoid potential unnecessary decrypt round
*/
close(description) {
return this.props.onClose(description, this.plaintextDto);
}
/**
* Remove listener for outside description editor clicks that aims to closes the editor
*/
removeOutsideEditorClickEvent() {
document.removeEventListener('click', this.handleEditorClickEvent, {capture: true});
}
/**
* handle a click outside of the editor
*/
handleOutsideEditorClickEvent() {
document.addEventListener('click', this.handleEditorClickEvent, {capture: true});
}
/*
* =============================================================
* Widget related events
* =============================================================
*/
/**
* set the focus at the end of the description editor
*/
setFocusOnDescriptionEditor() {
const descriptionLength = this.description ? this.description.length : 0;
this.textareaRef.current.selectionStart = descriptionLength;
this.textareaRef.current.selectionEnd = descriptionLength;
this.textareaRef.current.focus();
}
/**
* Handle click events on editor. Hide the component if the click occurred outside of the component.
* @param {ReactEvent} event The event
*/
handleEditorClickEvent(event) {
// Prevent stop editing when the user click on an element of the editor or is in processing state and click to enter his passphrase
if (this.elementRef.current.contains(event.target) || this.state.processing) {
return;
}
this.close(this.props.description);
}
/**
* Should input be disabled? True if state is loading or processing
* @returns {boolean}
*/
hasAllInputDisabled() {
return this.state.processing || this.state.loading;
}
/**
* Handle key down on the component.
* @params {ReactEvent} The react event
*/
handleKeyDown(event) {
// Close the editor when the user presses the "ESC" key.
if (event.keyCode === 27) {
// Stop the event propagation in order to avoid a parent component to react to this ESC event.
event.stopPropagation();
this.close(this.props.description);
}
}
/**
* On cancel button click
*/
handleCancel() {
this.close(this.props.description);
}
/**
* Handle form input change.
* @params {ReactEvent} The react event.
*/
handleInputChange(event) {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({
[name]: value
});
}
/**
* Handle form submit event.
* @params {ReactEvent} The react event
* @return {Promise}
*/
async handleFormSubmit(event) {
event.preventDefault();
if (!this.state.processing) {
this.props.loadingContext.add();
await this.save();
this.props.loadingContext.remove();
}
}
/**
* Switch to toggle description field encryption
*/
async handleDescriptionToggle(event) {
/*
* When click on the lock button
* the click is detected out of the element and the editor close.
* To fix that an immediate stop propagation enable to avoid the editor close.
* Need absolutely an immediate propagation to stop other listeners.
*/
event.nativeEvent.stopImmediatePropagation();
// Description is not encrypted and encrypted description type is not supported => leave it alone
if (!this.isEncryptedDescriptionEnabled() && !this.state.encryptDescription) {
return;
}
// No obligation to keep description encrypted, allow toggle
if (!this.mustEncryptDescription()) {
const encrypt = !this.state.encryptDescription;
this.setState({encryptDescription: encrypt});
}
}
/**
* Get the translate function
* @returns {function(...[*]=)}
*/
get translate() {
return this.props.t;
}
/*
* =============================================================
* Render
* =============================================================
*/
/**
* Render the component
* @returns {JSX}
*/
render() {
return (
<form onKeyDown={this.handleKeyDown} noValidate className="description-editor">
<div className="form-content" ref={this.elementRef}>
<div className="input textarea required">
<textarea name="description" className="fluid" ref={this.textareaRef}
maxLength="10000" placeholder={this.translate("Enter a description")} value={this.description}
onChange={this.handleInputChange}
disabled={this.hasAllInputDisabled()} autoComplete="off"/>
</div>
{this.state.error &&
<div className="feedbacks error-message">{this.state.error}</div>
}
<div className="actions">
<div className="description-lock">
{!this.areResourceTypesEnabled() &&
<Tooltip message={this.translate("Do not store sensitive data. Unlike the password, this data is not encrypted. Upgrade to version 3 to encrypt this information.")}>
<Icon name="info-circle"/>
</Tooltip>
}
{this.areResourceTypesEnabled() && !this.state.encryptDescription &&
<a role="button" onClick={event => this.handleDescriptionToggle(event)} className="lock-toggle">
<Tooltip message={this.translate("Do not store sensitive data or click here to enable encryption for the description field.")}>
<Icon name="lock-open"/>
</Tooltip>
</a>
}
{this.areResourceTypesEnabled() && this.state.encryptDescription &&
<a role="button" onClick={event => this.handleDescriptionToggle(event)} className="lock-toggle">
<Tooltip message={this.translate("The description content will be encrypted.")} icon="">
<Icon name="lock"/>
</Tooltip>
</a>
}
</div>
<a className={`cancel button ${this.hasAllInputDisabled() ? "disabled" : ""}`} role="button"
onClick={this.handleCancel}><Trans>Cancel</Trans>
</a>
<a className={`button primary description-editor-submit ${this.hasAllInputDisabled() ? "processing disabled" : ""}`}
onClick={this.handleFormSubmit} role="button">
<span><Trans>Save</Trans></span>
</a>
</div>
</div>
</form>
);
}
}
EditResourceDescription.propTypes = {
context: PropTypes.any, // The application context
description: PropTypes.string, // the description
resource: PropTypes.any, // the resource to update the description for
plaintextDto: PropTypes.any, // the plaintext secret to update if description is encrypted
onClose: PropTypes.func, // toggle to display or not the editor
resourceWorkspaceContext: PropTypes.any, // The resource workspace context
actionFeedbackContext: PropTypes.any, // The action feedback context
loadingContext: PropTypes.any, // The loading context
t: PropTypes.func, // The translation function
};
export default withAppContext(withResourceWorkspace(withLoading(withActionFeedback(withTranslation('common')(EditResourceDescription)))));