UNPKG

passbolt-styleguide

Version:

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

393 lines (355 loc) 11.8 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 "../../../contexts/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", }, actions: { 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.actions.processing; } /** * Returns true if actions can be performed */ get areActionsAllowed() { return !this.isProcessing; } /** * True if the edit has validation errors */ get hasErrors() { return Object.values(this.state.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(); 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; await this.setState({profile: {first_name, last_name, locale}}); } /** * Saves the change on the user profile */ async save() { await this.setState({hasAlreadyBeenValidated: true}); await this.toggleProcessing(); await this.validate(); if (this.hasErrors) { await this.toggleProcessing(); 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"); this.props.context.setContext({loggedInUser}); this.props.context.onUpdateLocaleRequested(); this.props.onClose(); } /** * Whenever the save has failed * @param error The error */ async onSaveError(error) { await this.toggleProcessing(); const errorDialogProps = { error: error }; this.props.dialogContext.open(NotifyError, errorDialogProps); } /** * Close the component */ close() { this.props.onClose(); } /** * Validates the edit data */ async validate() { const isEmpty = s => s.trim().length === 0; const errors = { isFirstnameEmpty: isEmpty(this.state.profile.first_name), isLastnameEmpty: isEmpty(this.state.profile.last_name) }; await this.setState({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(); } } /** * Toggle processing state * @returns {Promise<void>} */ async toggleProcessing() { const prev = this.state.actions.processing; return this.setState({actions: Object.assign(this.state.actions, {processing: !prev})}); } /** * 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} 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)))));