passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
488 lines (451 loc) • 15.6 kB
JavaScript
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) 2022 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) 2022 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.6.0
*/
import React from "react";
import PropTypes from "prop-types";
import UserAvatar from "../../Common/Avatar/UserAvatar";
import { withAppContext } from "../../../../shared/context/AppContext/AppContext";
import { withUserWorkspace } from "../../../contexts/UserWorkspaceContext";
import SpinnerSVG from "../../../../img/svg/spinner.svg";
import { Trans, withTranslation } from "react-i18next";
import { formatDateTimeAgo } from "../../../../shared/utils/dateUtils";
import CaretDownSVG from "../../../../img/svg/caret_down.svg";
import CaretRightSVG from "../../../../img/svg/caret_right.svg";
const LIMIT_ACTIVITIES_PER_PAGE = 5;
/**
* This component display activity section of a user
*/
class DisplayUserDetailsActivity extends React.Component {
/**
* Constructor
* @param {Object} props
*/
constructor(props) {
super(props);
this.state = this.getDefaultState();
this.bindCallbacks();
}
/**
* Get default state
* @returns {*}
*/
getDefaultState() {
return {
activities: null, // list of activities
activitiesPage: 1, // pagination for activity
loadingMore: false, // processing when the user want to see more activities
open: false,
loading: true,
};
}
/**
* Whenever the component has updated in terms of props
* @param prevProps
*/
async componentDidUpdate(prevProps) {
if (prevProps.userWorkspaceContext.details) {
await this.handleUserChange(prevProps.userWorkspaceContext.details.user);
}
if (prevProps.userWorkspaceContext.refresh) {
await this.handleUserActivitiesRefresh(prevProps.userWorkspaceContext.refresh.activities);
}
}
/**
* Bind callbacks methods
*/
bindCallbacks() {
this.handleTitleClickEvent = this.handleTitleClickEvent.bind(this);
this.handleMoreClickEvent = this.handleMoreClickEvent.bind(this);
}
/**
* Check if the user has changed and fetch
* @param previousUser
*/
async handleUserChange(previousUser) {
// do nothing if the section is closed.
if (!this.state.open) {
return;
}
// do nothing if the user doesn't change.
if (this.user.id === previousUser.id) {
return;
}
// Reset the component, and fetch activities for the new user.
const state = Object.assign({}, this.getDefaultState(), { open: true });
this.setState(state);
await this.fetch();
this.setState({ loading: false });
}
/**
* Handle the refresh of the activities
* @param hasPreviouslyRefreshed True if one previously refreshed the activities
*/
async handleUserActivitiesRefresh(hasPreviouslyRefreshed) {
const mustRefresh = !hasPreviouslyRefreshed && this.props.userWorkspaceContext.refresh.activities;
if (mustRefresh) {
await this.fetch();
await this.props.userWorkspaceContext.onUserActivitiesRefreshed();
}
}
/**
* handle when the users click on the section header.
* Open/Close it.
*/
async handleTitleClickEvent() {
// If the section is open, reset the component and close the section.
if (this.state.open) {
const defaultState = this.getDefaultState();
this.setState(defaultState);
} else {
this.setState({ loading: true, open: true });
await this.fetch();
this.setState({ loading: false });
}
}
/**
* handle when the users click on the more button.
* Open/Close it.
*/
async handleMoreClickEvent() {
const activitiesPage = this.state.activitiesPage + 1;
const loadingMore = true;
await this.setState({ activitiesPage, loadingMore });
await this.fetch();
this.setState({ loadingMore: false });
}
/**
* Fetch the user activities
* @returns {Promise<void>}
*/
async fetch() {
const limit = LIMIT_ACTIVITIES_PER_PAGE;
const page = this.state.activitiesPage;
const options = { limit, page };
const newActivities = await this.props.context.port.request(
"passbolt.actionlogs.find-all-for",
"User",
this.user.id,
options,
);
let activities;
// For the first page need to reset activities state
if (this.state.activitiesPage > 1) {
activities = [...(this.state.activities || []), ...newActivities];
} else {
activities = [...newActivities];
}
this.setState({ activities });
}
/**
* Get an activity creator full name
* @param {object} user The creator
* @return string
*/
getActivityCreatorFullName(user) {
return `${user.profile.first_name} ${user.profile.last_name}`;
}
/**
* Get the base url
* @return {string}
*/
get baseUrl() {
return this.props.context.userSettings.getTrustedDomain();
}
/**
* Render a requested account recovery activity.
* @param {object} activity The target activity
* @returns {JSX}
*/
renderAccountRecoveryRequested(activity) {
const activityCreatorName = this.getActivityCreatorFullName(activity.creator);
const activityFormattedDate = formatDateTimeAgo(activity.created, this.props.t, this.props.context.locale);
return (
<li key={activity.id} className="usercard-detailed-col-2">
<div className="content-wrapper">
<div className="content">
<div className="name">
<Trans>
<span className="creator">{{ activityCreatorName }}</span> requested an account recovery
</Trans>
</div>
<div className="subinfo third-level light" title={activity.created}>
{activityFormattedDate}
</div>
</div>
</div>
<UserAvatar user={activity.creator} baseUrl={this.baseUrl} />
</li>
);
}
/**
* Render an account recovery request rejection activity.
* @param {object} activity The target activity
* @returns {JSX}
*/
renderAccountRecoveryRequestRejected(activity) {
const activityCreatorName = this.getActivityCreatorFullName(activity.creator);
const userLink = `${this.baseUrl}/app/users/view/${activity.creator.id}`;
const activityFormattedDate = formatDateTimeAgo(activity.created, this.props.t, this.props.context.locale);
return (
<li key={activity.id} className="usercard-detailed-col-2">
<div className="content-wrapper">
<div className="content">
<div className="name">
<Trans>
<a rel="noopener noreferrer" href={userLink}>
<span className="creator">{{ activityCreatorName }}</span>
</a>{" "}
rejected the account recovery request
</Trans>
</div>
<div className="subinfo light" title={activity.created}>
{activityFormattedDate}
</div>
</div>
</div>
<UserAvatar user={activity.creator} baseUrl={this.baseUrl} />
</li>
);
}
/**
* Render an account recovery request acceptation activity.
* @param {object} activity The target activity
* @returns {JSX}
*/
renderAccountRecoveryRequestAccepted(activity) {
const activityCreatorName = this.getActivityCreatorFullName(activity.creator);
const userLink = `${this.baseUrl}/app/users/view/${activity.creator.id}`;
const activityFormattedDate = formatDateTimeAgo(activity.created, this.props.t, this.props.context.locale);
return (
<li key={activity.id} className="usercard-detailed-col-2">
<div className="content-wrapper">
<div className="content">
<div className="name">
<Trans>
<a rel="noopener noreferrer" href={userLink}>
<span className="creator">{{ activityCreatorName }}</span>
</a>{" "}
accepted the account recovery request
</Trans>
</div>
<div className="subinfo light" title={activity.created}>
{activityFormattedDate}
</div>
</div>
</div>
<UserAvatar user={activity.creator} baseUrl={this.baseUrl} />
</li>
);
}
/**
* Render an account recovery policy rejection activity.
* @param {object} activity The target activity
* @returns {JSX}
*/
renderAccountRecoveryPolicyRejected(activity) {
const activityCreatorName = this.getActivityCreatorFullName(activity.creator);
const activityFormattedDate = formatDateTimeAgo(activity.created, this.props.t, this.props.context.locale);
return (
<li key={activity.id} className="usercard-detailed-col-2">
<div className="content-wrapper">
<div className="content">
<div className="name">
<Trans>
<span className="creator">{{ activityCreatorName }}</span> rejected the account recovery policy
</Trans>
</div>
<div className="subinfo light" title={activity.created}>
{activityFormattedDate}
</div>
</div>
</div>
<UserAvatar user={activity.creator} baseUrl={this.baseUrl} />
</li>
);
}
/**
* Render an account recovery policy acceptation activity.
* @param {object} activity The target activity
* @returns {JSX}
*/
renderAccountRecoveryPolicyAccepted(activity) {
const activityCreatorName = this.getActivityCreatorFullName(activity.creator);
const activityFormattedDate = formatDateTimeAgo(activity.created, this.props.t, this.props.context.locale);
return (
<li key={activity.id} className="usercard-detailed-col-2">
<div className="content-wrapper">
<div className="content">
<div className="name">
<Trans>
<span className="creator">{{ activityCreatorName }}</span> accepted the account recovery policy
</Trans>
</div>
<div className="subinfo light" title={activity.created}>
{activityFormattedDate}
</div>
</div>
</div>
<UserAvatar user={activity.creator} baseUrl={this.baseUrl} />
</li>
);
}
/**
* Render an activity about the creation of a user.
* @param {object} activity The target activity
* @returns {JSX}
*/
renderUserCreated(activity) {
const activityCreatorName = this.getActivityCreatorFullName(activity.creator);
const userLink = `${this.baseUrl}/app/users/view/${activity.creator.id}`;
const activityFormattedDate = formatDateTimeAgo(activity.created, this.props.t, this.props.context.locale);
return (
<li key={activity.id} className="usercard-detailed-col-2">
<div className="content-wrapper">
<div className="content">
<div className="name">
<Trans>
<a rel="noopener noreferrer" href={userLink}>
<span className="creator">{{ activityCreatorName }}</span>
</a>{" "}
created the user account
</Trans>
</div>
<div className="subinfo light" title={activity.created}>
{activityFormattedDate}
</div>
</div>
</div>
<UserAvatar user={activity.creator} baseUrl={this.baseUrl} />
</li>
);
}
/**
* Render an unknown activity.
* @returns {JSX}
*/
renderUnknownActivity(activity) {
return (
<li key={activity.id} className="usercard-detailed-col-2">
<div className="content-wrapper">
<div className="content">
<Trans>Unknown activity, please contact your administrator.</Trans>
</div>
</div>
</li>
);
}
/**
* Render an activity
* @param {object} activity The activity to render
*/
renderActivity(activity) {
let render;
switch (activity.type) {
case "AccountRecovery.Requests.initiated": {
render = this.renderAccountRecoveryRequested(activity);
break;
}
case "AccountRecovery.Requests.accepted": {
render = this.renderAccountRecoveryRequestAccepted(activity);
break;
}
case "AccountRecovery.Requests.rejected": {
render = this.renderAccountRecoveryRequestRejected(activity);
break;
}
case "AccountRecovery.Policies.accepted": {
render = this.renderAccountRecoveryPolicyAccepted(activity);
break;
}
case "AccountRecovery.Policies.rejected": {
render = this.renderAccountRecoveryPolicyRejected(activity);
break;
}
case "Users.created": {
render = this.renderUserCreated(activity);
break;
}
default: {
render = this.renderUnknownActivity(activity);
break;
}
}
return render;
}
/**
* Check if the more button should be displayed.
* @return {boolean}
*/
mustDisplayMoreButton() {
return !this.state.activities.some((activity) => activity.type === "Users.created");
}
/**
* Returns the current detailed user
*/
get user() {
return this.props.userWorkspaceContext.details.user;
}
/**
* Render the component
* @returns {JSX}
*/
render() {
return (
<div className={`activity accordion sidebar-section ${this.state.open ? "" : "closed"}`}>
<div className="accordion-header">
<h4>
<button type="button" className="link no-border" onClick={this.handleTitleClickEvent}>
<Trans>Activity</Trans>
{this.state.open ? <CaretDownSVG className="caret-down" /> : <CaretRightSVG className="caret-right" />}
</button>
</h4>
</div>
<div className="accordion-content">
{this.state.loading && (
<div className="processing-wrapper">
<SpinnerSVG />
<span className="processing-text">
<Trans>Retrieving activities</Trans>
</span>
</div>
)}
{!this.state.loading && (
<React.Fragment>
<ul className="ready">{this.state.activities.map((activity) => this.renderActivity(activity))}</ul>
{this.mustDisplayMoreButton() && (
<div className="actions">
<button
type="button"
disabled={this.state.loadingMore}
onClick={this.handleMoreClickEvent}
className={`action-logs-load-more ${this.state.loadingMore ? "processing" : ""}`}
>
<span>
<Trans>More</Trans>
</span>
</button>
</div>
)}
</React.Fragment>
)}
</div>
</div>
);
}
}
DisplayUserDetailsActivity.propTypes = {
context: PropTypes.any, // The application context
userWorkspaceContext: PropTypes.any,
t: PropTypes.func, // The translation function
};
export default withAppContext(withUserWorkspace(withTranslation("common")(DisplayUserDetailsActivity)));