UNPKG

passbolt-styleguide

Version:

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

406 lines (374 loc) 12.2 kB
/** * Passbolt ~ Open source password manager for teams * Copyright (c) 2020 Passbolt SA (https://www.passbolt.com) * * Licensed under GNU Affero General Public License version 3 of the or any later version. * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * * @copyright Copyright (c) 2020 Passbolt SA (https://www.passbolt.com) * @license https://opensource.org/licenses/AGPL-3.0 AGPL License * @link https://www.passbolt.com Passbolt(tm) * @since 2.13.0 */ import React, { Component } from "react"; import PropTypes from "prop-types"; import { withAppContext } from "../../../../shared/context/AppContext/AppContext"; import DialogWrapper from "../../Common/Dialog/DialogWrapper/DialogWrapper"; import FormSubmitButton from "../../Common/Inputs/FormSubmitButton/FormSubmitButton"; import FormCancelButton from "../../Common/Inputs/FormSubmitButton/FormCancelButton"; import NotifyError from "../../Common/Error/NotifyError/NotifyError"; import { withDialog } from "../../../contexts/DialogContext"; import { withActionFeedback } from "../../../contexts/ActionFeedbackContext"; import { Trans, withTranslation } from "react-i18next"; import { withUserSettings } from "../../../contexts/UserSettingsContext"; import Select from "../../Common/Select/Select"; class EditUserProfile extends Component { /** * Constructor * @param {Object} props */ constructor(props) { super(props); this.state = this.defaultState; this.bindHandlers(); this.createReferences(); } /** * Whenever the component is mounted */ async componentDidMount() { await this.populate(); } /** * Returns the component default state */ get defaultState() { return { profile: { // The editing user profile data first_name: "", last_name: "", username: "", locale: "en-UK", }, processing: false, // True if one is processing the edit errors: { isFirstnameEmpty: false, // True if the firstname is empty isLastnameEmpty: false, // True if the lastname is empty }, hasAlreadyBeenValidated: false, // True if the data have already been validated once }; } /** * Return trus if the export is processing */ get isProcessing() { return this.state.processing; } /** * Returns true if actions can be performed */ get areActionsAllowed() { return !this.isProcessing; } /** * True if the edit has validation errors * @param {object} errors * @return boolean */ hasErrors(errors) { return Object.values(errors).some((value) => value); } /** * Bind handlers */ bindHandlers() { this.handleInputChange = this.handleInputChange.bind(this); this.handleClose = this.handleClose.bind(this); this.handleSave = this.handleSave.bind(this); } /** * Create references */ createReferences() { this.firstnameRef = React.createRef(); this.lastnameRef = React.createRef(); } /** * Handle form input changes. * @params {ReactEvent} The react event * @returns {void} */ async handleInputChange(event) { const target = event.target; const value = target.value; const name = target.name; this.setState({ profile: Object.assign(this.state.profile, { [name]: value }) }); if (this.state.hasAlreadyBeenValidated) { await this.validate(); } } /** * Whenever the user wants to close */ handleClose() { this.close(); } /** * Whenever the user wants to save the changes on his profile * @param event A DOM event */ async handleSave(event) { // Avoid the form to be submitted. event.preventDefault(); if (this.isProcessing) { return; } await this.save(); } /** * Populates the component with data */ async populate() { const { first_name, last_name } = this.props.context.loggedInUser.profile; const locale = this.props.context.locale; this.setState({ profile: { first_name, last_name, locale } }); } /** * Saves the change on the user profile */ async save() { this.setState({ hasAlreadyBeenValidated: true, processing: true }); const errors = this.validate(); if (this.hasErrors(errors)) { this.setState({ processing: false }); this.focusFirstFieldError(); return; } await this.updateUserProfile().then(this.onSaveSuccess.bind(this)).catch(this.onSaveError.bind(this)); } /** * Update the user profile. * @returns {Promise<void>} */ async updateUserProfile() { const userToUpdateDto = this.buildUserToUpdateDto(); await this.props.context.port.request("passbolt.users.update", userToUpdateDto); if (this.canIUseLocale) { const localeToUpdateDto = this.buildLocaleToUpdateDto(); await this.props.userSettingsContext.onUpdateLocaleUserRequested(localeToUpdateDto); } } /** * Build the user to update DTO * @returns {object} */ buildUserToUpdateDto() { const { id, username } = this.props.context.loggedInUser; const profile = { first_name: this.state.profile.first_name, last_name: this.state.profile.last_name, }; return { id, username, profile }; } /** * Build the locale to update DTO * @returns {object} */ buildLocaleToUpdateDto() { return { locale: this.state.profile.locale }; } /** * Whenever the save has been successful */ async onSaveSuccess() { await this.props.actionFeedbackContext.displaySuccess(this.translate("The user has been updated successfully")); const loggedInUser = await this.props.context.port.request("passbolt.users.find-logged-in-user", true); this.props.context.setContext({ loggedInUser }); this.props.context.onUpdateLocaleRequested(); this.props.onClose(); } /** * Whenever the save has failed * @param error The error */ async onSaveError(error) { this.setState({ processing: false }); const errorDialogProps = { error: error, }; this.props.dialogContext.open(NotifyError, errorDialogProps); } /** * Close the component */ close() { this.props.onClose(); } /** * Validates the edit data * @return {object} errors */ validate() { const isEmpty = (s) => s.trim().length === 0; const errors = { isFirstnameEmpty: isEmpty(this.state.profile.first_name), isLastnameEmpty: isEmpty(this.state.profile.last_name), }; this.setState({ errors }); return errors; } /** * Focus the first field of the form which is in error state. */ focusFirstFieldError() { if (this.state.errors.isFirstnameEmpty) { this.firstnameRef.current.focus(); } else if (this.state.errors.isLastnameEmpty) { this.lastnameRef.current.focus(); } } /** * Get the translate function * @returns {function(...[*]=)} */ get translate() { return this.props.t; } /** * Get the supported locales * @returns {array} */ get supportedLocales() { if (this.props.context.siteSettings.supportedLocales) { return this.props.context.siteSettings.supportedLocales.map((supportedLocale) => ({ value: supportedLocale.locale, label: supportedLocale.label, })); } return []; } /** * Can I use the locale plugin. * @type {boolean} */ get canIUseLocale() { return this.props.context.siteSettings.canIUse("locale"); } /** * Render the component */ render() { const firstnameErrorSelector = this.state.errors.isFirstnameEmpty ? "error" : ""; const lastnameErrorSelector = this.state.errors.isLastnameEmpty ? "error" : ""; return ( <DialogWrapper title={this.translate("Edit profile")} onClose={this.handleClose} disabled={!this.areActionsAllowed} > <form className="user-edit-form" onSubmit={this.handleSave} noValidate> <div className="form-content"> <div className={`input text required ${firstnameErrorSelector} ${!this.areActionsAllowed ? "disabled" : ""}`} > <label htmlFor="user-profile-firstname-input"> <Trans>First name</Trans> </label> <input id="user-profile-firstname-input" name="first_name" type="text" placeholder={this.translate("First name")} required="required" autoComplete="off" autoFocus={true} ref={this.firstnameRef} value={this.state.profile.first_name} onChange={this.handleInputChange} disabled={!this.areActionsAllowed} /> {this.state.errors.isFirstnameEmpty && ( <div className="first_name error-message"> <Trans>A first name is required.</Trans> </div> )} </div> <div className={`input text required ${lastnameErrorSelector} ${!this.areActionsAllowed ? "disabled" : ""}`} > <label htmlFor="user-profile-lastname-input"> <Trans>Last name</Trans> </label> <input id="user-profile-lastname-input" name="last_name" type="text" placeholder={this.translate("Last name")} required="required" autoComplete="off" ref={this.lastnameRef} value={this.state.profile.last_name} onChange={this.handleInputChange} disabled={!this.areActionsAllowed} /> {this.state.errors.isLastnameEmpty && ( <div className="last_name error-message"> <Trans>A last name is required.</Trans> </div> )} </div> <div className="input text required disabled"> <label htmlFor="user-profile-username-input"> <Trans>Username / Email</Trans> </label> <input id="user-profile-username-input" name="username" type="text" disabled={true} aria-required={true} value={this.props.context.loggedInUser.username} /> </div> {this.canIUseLocale && ( <div className={`select-wrapper input required ${!this.areActionsAllowed ? "disabled" : ""}`}> <label htmlFor="user-profile-locale-input"> <Trans>Language</Trans> </label> <Select id="user-profile-locale-input" name="locale" value={this.state.profile.locale} items={this.supportedLocales} disabled={!this.areActionsAllowed} onChange={this.handleInputChange} /> </div> )} </div> <div className="submit-wrapper clearfix"> <FormCancelButton disabled={!this.areActionsAllowed} onClick={this.handleClose} /> <FormSubmitButton disabled={!this.areActionsAllowed} processing={this.isProcessing} value={this.translate("Save")} /> </div> </form> </DialogWrapper> ); } } EditUserProfile.propTypes = { context: PropTypes.any, // The application context onClose: PropTypes.func, // Whenever the dialog must be closed dialogContext: PropTypes.object, // The dialog context userSettingsContext: PropTypes.object, // The user settings context actionFeedbackContext: PropTypes.object, // The action feedback context t: PropTypes.func, // The translation function }; export default withAppContext( withActionFeedback(withDialog(withUserSettings(withTranslation("common")(EditUserProfile)))), );