passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
370 lines (339 loc) • 13.5 kB
JavaScript
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) 2021 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) 2021 Passbolt SA (https://www.passbolt.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.passbolt.com Passbolt(tm)
* @since 3.3.0
*
*/
import React from "react";
import PropTypes from "prop-types";
import { withRouter } from "react-router-dom";
import { Trans, withTranslation } from "react-i18next";
import SpinnerSVG from "../../../img/svg/spinner.svg";
import Password from "../../../shared/components/Password/Password";
import { withAppContext } from "../../../shared/context/AppContext/AppContext";
import { withPasswordPolicies } from "../../../shared/context/PasswordPoliciesContext/PasswordPoliciesContext";
import { withPasswordExpiry } from "../../../react-extension/contexts/PasswordExpirySettingsContext";
import EntityValidationError from "../../../shared/models/entity/abstract/entityValidationError";
import ResourceViewModel from "../../../shared/models/resource/ResourceViewModel";
import { withResourceTypesLocalStorage } from "../../../shared/context/ResourceTypesLocalStorageContext/ResourceTypesLocalStorageContext";
import ResourceTypesCollection from "../../../shared/models/entity/resourceType/resourceTypesCollection";
import { withMetadataTypesSettingsLocalStorage } from "../../../shared/context/MetadataTypesSettingsLocalStorageContext/MetadataTypesSettingsLocalStorageContext";
import MetadataTypesSettingsEntity from "../../../shared/models/entity/metadata/metadataTypesSettingsEntity";
import ResourceViewModelFactory from "../../../shared/models/resource/ResourceViewModelFactory";
import {
RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION_SLUG,
RESOURCE_TYPE_V5_DEFAULT_SLUG,
} from "../../../shared/models/entity/resourceType/resourceTypeSchemasDefinition";
class SaveResource extends React.Component {
/**
* @constructor
* @param {object} props
*/
constructor(props) {
super(props);
this.state = this.defaultState;
this.initEventHandlers();
}
/**
* Get the default state
* @returns {object}
*/
get defaultState() {
return {
resourceViewModel: null,
errors: null, //the validation errors set
unexpectedErrorMessage: "",
hasAlreadyBeenValidated: false, // True if the form has already been submitted once
loaded: false,
};
}
/**
* when the component is mounted
* @returns {Promise<void>}
*/
async componentDidMount() {
await this.props.passwordExpiryContext.findSettings();
this.loadPasswordMetaFromTabForm();
await this.props.passwordPoliciesContext.loadPolicies();
}
/**
* initialize event handlers
*/
initEventHandlers() {
this.handleClose = this.handleClose.bind(this);
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
}
/**
* Loads the resource data from the information of the autosave preparation
* @returns {Promise<void>}
*/
async loadPasswordMetaFromTabForm() {
const resourceDto = await this.props.context.port.request("passbolt.quickaccess.prepare-autosave");
resourceDto.uri = resourceDto.uris && resourceDto.uris.length > 0 ? resourceDto.uris[0] : "";
resourceDto.password = resourceDto.secret_clear;
let resourceType;
if (this.props.metadataTypeSettings.isDefaultResourceTypeV5) {
resourceType = this.props.resourceTypes.getFirstBySlug(RESOURCE_TYPE_V5_DEFAULT_SLUG);
} else if (this.props.metadataTypeSettings.isDefaultResourceTypeV4) {
resourceType = this.props.resourceTypes.getFirstBySlug(RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION_SLUG);
}
resourceDto.resource_type_id = resourceType.id;
const resourceViewModel = ResourceViewModelFactory.createFromResourceTypeAndResourceViewModelDto(
resourceType,
resourceDto,
);
this.setState({ loaded: true, resourceViewModel: resourceViewModel });
}
/**
* Handles the "close" event
*/
async handleClose() {
await this.props.context.closeWindow();
}
/**
* Validate the form data and returns true if it's valid
* @param {ResourcePasswordDescriptionViewModel} resourceViewModel
* @returns {EntityValidationError}
*/
validate(resourceViewModel) {
const errors = resourceViewModel.validate(ResourceViewModel.CREATE_MODE);
this.setState({ errors });
return errors;
}
/**
* Handles the form submission
* @param {React.Event} event
* @returns {Promise<void>}
*/
async handleFormSubmit(event) {
event.preventDefault();
this.setState({ processing: true, hasAlreadyBeenValidated: true });
const expired = this.props.passwordExpiryContext.getDefaultExpirationDate();
const resourceViewModel = this.state.resourceViewModel.cloneWithMutation("expired", expired);
const validationErrors = this.validate(resourceViewModel);
if (validationErrors.hasErrors()) {
this.setState({ processing: false });
return;
}
const resourceDto = resourceViewModel.toResourceDto();
const secretDto = resourceViewModel.toSecretDto();
try {
await this.props.context.port.request("passbolt.resources.create", resourceDto, secretDto);
await this.handleClose();
} catch (error) {
this.handleSubmitError(error);
}
}
/**
* Handles error during form submission
* @param {Error} error
*/
handleSubmitError(error) {
const newState = {
processing: false,
};
const isBadRequestError =
error.name === "PassboltApiFetchError" &&
error.data.code === 400 &&
(error.data.body?.name || error.data.body?.username || error.data.body?.uri);
if (isBadRequestError) {
newState.errors = this.formatApiErrors(error.data.body);
} else {
newState.unexpectedErrorMessage = error.message;
}
this.setState(newState);
}
/**
* Format the given BadRequest error invalid field information.
* @param {object} errorBody
* @returns {EntityValidationError}
*/
formatApiErrors(errorBody) {
const errors = new EntityValidationError();
const fieldsInError = Object.keys(errorBody);
for (let i = 0; i < fieldsInError.length; i++) {
const prop = fieldsInError[i];
const errorMessages = errorBody[prop].join(", ");
errors.addError(prop, "api-validation", errorMessages);
}
return errors;
}
/**
* Handles form input changed
* @param {React.Event} event
*/
handleInputChange(event) {
const { name, value } = event.target;
const newState = {
resourceViewModel: this.state.resourceViewModel.cloneWithMutation(name, value),
};
if (this.state.hasAlreadyBeenValidated) {
newState.errors = newState.resourceViewModel.validate(ResourceViewModel.CREATE_MODE);
}
this.setState(newState);
}
/**
* Get the translate function
* @returns {function(...[*]=)}
*/
get translate() {
return this.props.t;
}
/**
* Render the component
* @returns {JSX}
*/
render() {
return (
<div className="resource-auto-save">
<h1 className="title">
<Trans>Would you like to save this credential ?</Trans>
</h1>
<form onSubmit={this.handleFormSubmit}>
<div className="resource-auto-save-form">
<div className="form-container">
<div className={`input text required ${this.state.errors?.hasError("name") ? "error" : ""}`}>
<label htmlFor="name">
<Trans>Name</Trans>
</label>
<input
name="name"
value={this.state.resourceViewModel?.name || ""}
onChange={this.handleInputChange}
disabled={this.state.processing}
className="required fluid"
maxLength="255"
type="text"
id="name"
autoComplete="off"
/>
{this.state.errors?.hasError("name", "required") && (
<div className="error-message">
<Trans>A name is required.</Trans>
</div>
)}
{this.state.errors?.hasError("name", "api-validation") && (
<div className="error-message">{this.state.errors.getError("name", "api-validation")}</div>
)}
</div>
<div className={`input text ${this.state.errors?.hasError("uri") ? "error" : ""}`}>
<label htmlFor="uri">
<Trans>URL</Trans>
</label>
<input
name="uri"
value={this.state.resourceViewModel?.uri || ""}
onChange={this.handleInputChange}
disabled={this.state.processing}
className="fluid"
maxLength="1024"
type="text"
id="uri"
autoComplete="off"
/>
{this.state.errors?.hasError("uri", "maxLength") && (
<div className="error-message">
<Trans>The URI cannot exceed 1024 characters.</Trans>
</div>
)}
{this.state.errors?.hasError("uri", "api-validation") && (
<div className="error-message">{this.state.errors.getError("uri", "api-validation")}</div>
)}
</div>
<div className={`input text ${this.state.errors?.hasError("username") ? "error" : ""}`}>
<label htmlFor="username">
<Trans>Username</Trans>
</label>
<input
name="username"
value={this.state.resourceViewModel?.username || ""}
onChange={this.handleInputChange}
disabled={this.state.processing}
className="fluid"
maxLength="255"
type="text"
id="username"
autoComplete="off"
/>
{this.state.errors?.hasError("username", "api-validation") && (
<div className="error-message">{this.state.errors.getError("username", "api-validation")}</div>
)}
</div>
<div
className={`input-password-wrapper input required ${this.state.errors?.hasError("password") ? "error" : ""}`}
>
<label htmlFor="password">
<Trans>Password</Trans>
</label>
<div className="password-button-inline">
<Password
name="password"
value={this.state.resourceViewModel?.password || ""}
preview={true}
onChange={this.handleInputChange}
disabled={this.state.processing}
placeholder={this.translate("Password")}
id="password"
autoComplete="new-password"
/>
</div>
{this.state.errors?.hasError("password", "required") && (
<div className="error-message">
<Trans>A password is required.</Trans>
</div>
)}
{this.state.errors?.hasError("password", "api-validation") && (
<div className="error-message">{this.state.errors.getError("password", "api-validation")}</div>
)}
</div>
{this.state.unexpectedErrorMessage && (
<div className="error-message">{this.state.unexpectedErrorMessage}</div>
)}
</div>
</div>
<div className="submit-wrapper input flex-row-end">
<a className="cancel" role="button" onClick={this.handleClose}>
{this.translate("no, thanks")}
</a>
<button
type="submit"
className={`button primary big ${this.state.processing ? "processing" : ""}`}
role="button"
disabled={this.state.processing}
>
<Trans>Save</Trans>
{this.state.processing && <SpinnerSVG />}
</button>
</div>
</form>
</div>
);
}
}
SaveResource.propTypes = {
context: PropTypes.any, // The application context
history: PropTypes.object,
t: PropTypes.func, // The translation function
resourceTypes: PropTypes.instanceOf(ResourceTypesCollection), // The resource types collection
metadataTypeSettings: PropTypes.instanceOf(MetadataTypesSettingsEntity), // The metadata type settings
passwordPoliciesContext: PropTypes.object, // The password policy context
passwordExpiryContext: PropTypes.object, // The password expiry context
};
export default withAppContext(
withRouter(
withResourceTypesLocalStorage(
withMetadataTypesSettingsLocalStorage(
withPasswordPolicies(withPasswordExpiry(withTranslation("common")(SaveResource))),
),
),
),
);