passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
909 lines (823 loc) • 32.2 kB
JavaScript
/**
* 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 * as React from "react";
import PropTypes from "prop-types";
import { withRouter } from "react-router-dom";
import { withAppContext } from "../../shared/context/AppContext/AppContext";
import { withLoading } from "./LoadingContext";
import { withActionFeedback } from "./ActionFeedbackContext";
import EditUserGroup from "../components/UserGroup/EditUserGroup/EditUserGroup";
import { withDialog } from "./DialogContext";
import { DateTime } from "luxon";
import { withTranslation } from "react-i18next";
import { isUserSuspended, isAccountRecoveryRequested, isMissingMetadataKey } from "../../shared/utils/userUtils";
import { withRbac } from "../../shared/context/Rbac/RbacContext";
import { uiActions } from "../../shared/services/rbacs/uiActionEnumeration";
import { withRoles } from "./RoleContext";
import RolesCollection from "../../shared/models/entity/role/rolesCollection";
/**
* Context related to users ( filter, current selections, etc.)
*/
export const UserWorkspaceContext = React.createContext({
filter: {
type: null, // Filter type
payload: null, // Filter payload
},
sorter: {
propertyName: "user", // The name of the property to sort on
asc: false, // True if the sort must be descendant
},
filteredUsers: [], // The current list of filtered users
selectedUsers: [], // The current list of selected users
details: {
user: null, // The user to focus details on
group: null, // The user group to focus details on
locked: true, // The details display is locked
},
scrollTo: {
user: null, // The user to scroll to
},
groupToEdit: null, // The group to edit
isAccessAllowed: () => {}, // is the current user allowed to access the user workspace
onUserScrolled: () => {}, // Whenever one scrolled to a user
onDetailsLocked: () => {}, // Lock or unlock detail (hide or display the group or user details)
onSorterChanged: () => {}, // Whenever the sorter changed
onUserSelected: {
single: () => {}, // Whenever a single user has been selected
none: () => {}, // Whenever none users have been selected
},
onRefreshSelectedUsers: () => {}, // Whenever a component request to refresh the selected users
onGroupToEdit: () => {}, // Whenever a group will be edited
shouldDisplaySuspendedUsersFilter: () => {}, // returns true if the 'Suspended user' filter should be displayed in the UI
});
/**
* The related context provider
*/
class UserWorkspaceContextProvider extends React.Component {
/**
* Default constructor
* @param props The component props
*/
constructor(props) {
super(props);
this.state = this.defaultState;
this.initializeProperties();
}
/**
* Returns the default component state
*/
get defaultState() {
return {
filter: { type: UserWorkspaceFilterTypes.NONE }, // The current user search filter
sorter: {
propertyName: "modified", // The name of the property to sort on
asc: false, // True if the sort must be descendant
},
filteredUsers: [], // The current list of filtered users
selectedUsers: [], // The current list of selected users
details: {
user: null, // The user to focus details on
group: null, // The group to focus details on
locked: true, // The details display is locked
},
scrollTo: {
user: null, // The user to scroll to
},
groupToEdit: null, // The group to edit
isAccessAllowed: this.isAccessAllowed.bind(this), // is the current user allowed to access the user workspace
getTranslatedRoleName: this.getTranslatedRoleName.bind(this), // Tools to retrieve a user translated role name
onUserScrolled: this.handleUserScrolled.bind(this), // Whenever one scrolled to a user
onDetailsLocked: this.handleDetailsLocked.bind(this), // Lock or unlock detail (hide or display the group or user details)
onSorterChanged: this.handleSorterChange.bind(this), // Whenever the sorter changed
onUserSelected: {
single: this.handleUserSelected.bind(this), // Whenever a single user has been selected
none: this.handleNoneUsersSelected.bind(this), // Whenever none users have been selected
},
onGroupToEdit: this.handleGroupToEdit.bind(this), // Whenever a group will be edited
onRefreshSelectedUsers: this.handleRefreshSelectedUsers.bind(this), // Whenever a component request to refresh the selected users
isAttentionRequired: this.isAttentionRequired.bind(this), // Whenever a user needs attention
shouldDisplaySuspendedUsersFilter: this.shouldDisplaySuspendedUsersFilter.bind(this), // returns true if the 'Suspended user' filter should be displayed in the UI
};
}
/**
* Initialize class properties out of the state ( for performance purpose )
* @returns {void}
*/
initializeProperties() {
this.users = null; // A cache of the last known list of users from the App context
this.groups = []; // A cache of the last known list of groups from the App context
this.routeLocationKey = null; // The current route location key being resolved, it will be used to avoid double execution of the route change handler.
}
/**
* Whenever the component is mounted
*/
componentDidMount() {
if (this.isAccessAllowed()) {
this.populate();
this.handleUsersWaitedFor();
}
}
/**
* Returns true if the current user allowed to access the user workspace
* @returns {boolean}
*/
isAccessAllowed() {
return this.props.rbacContext.canIUseAction(uiActions.USERS_VIEW_WORKSPACE);
}
/**
* Whenever the component has updated in terms of props or state
* @param prevProps
*/
async componentDidUpdate(prevProps, prevState) {
await this.handleFilterChange(prevState.filter);
this.handleUsersLoaded();
await this.handleUsersChange();
await this.handleGroupsChange();
await this.handleRouteChange(prevProps.location);
}
/**
* Handles the user search filter change
* @param {Object} previousFilter
*/
async handleFilterChange(previousFilter) {
const hasFilterChanged = previousFilter !== this.state.filter;
if (hasFilterChanged) {
// Avoid a side-effect whenever one inputs a specific user url (it unselect the user otherwise )
const isNotNonePreviousFilter = previousFilter.type !== UserWorkspaceFilterTypes.NONE;
if (isNotNonePreviousFilter) {
this.populate();
await this.unselectAll();
}
}
}
/**
* Handle the users changes
* @returns {void}
*/
async handleUsersChange() {
const hasUsersChanged = this.props.context.users && this.props.context.users !== this.users;
if (hasUsersChanged) {
this.users = this.props.context.users;
const filteredUsers = await this.search(this.state.filter);
if (filteredUsers) {
await this.updateDetails(filteredUsers);
const selectedUsers = this.updateSelectedUsersFromUsersChange(filteredUsers);
this.setState({ selectedUsers });
this.redirectAfterSelection(selectedUsers);
}
}
}
/**
* Handle the groups change
* @returns {void}
*/
async handleGroupsChange() {
const hasGroupsChanged = this.props.context.groups && this.props.context.groups !== this.groups;
if (hasGroupsChanged) {
this.groups = this.props.context.groups;
const filteredUsers = await this.refreshSearchFilter();
await this.updateDetails(filteredUsers);
}
}
/**
* Handle the refresh of selected users, to retrieve updated users during action
* @returns {void}
*/
handleRefreshSelectedUsers() {
const selectedUserIds = new Set(this.state.selectedUsers.map((user) => user.id));
const selectedUsers = this.props.context.users.filter((user) => selectedUserIds.has(user.id));
this.setState({ selectedUsers });
}
/**
* Handle the route location change
* @param previousLocation Previous router location
*/
async handleRouteChange(previousLocation) {
const hasLocationChanged = this.props.location.key !== previousLocation.key; // Did the route change as per react-dom-router
/*
* Multiple componentDidUpdate can be triggered with the same route change.
* Avoid this by storing and checking the last route key change resolved by the component.
* @deprecated to removed with react-router-dom v6.1.0 @see https://github.com/supasate/connected-react-router/issues/129#issuecomment-446212160
*/
const hasLocationChangedRouter5 = this.props.location.key !== this.routeLocationKey;
this.routeLocationKey = this.props.location.key;
const isAppFirstLoad = this.state.filter.type === UserWorkspaceFilterTypes.NONE;
if ((hasLocationChanged && hasLocationChangedRouter5) || isAppFirstLoad) {
await this.handleGroupRouteChange();
await this.handleUserRouteChange();
}
}
/**
* Handle the group view route change
* E.g. /groups/view/:selectedGroupId
*/
async handleGroupRouteChange() {
const hasUsersAndGroups = this.users !== null && this.groups !== null;
if (hasUsersAndGroups) {
const groupId = this.props.match.params.selectedGroupId;
if (groupId && this.props.context.groups) {
const group = this.props.context.groups.find((group) => group.id === groupId);
if (group) {
// Known group
await this.search({ type: UserWorkspaceFilterTypes.GROUP, payload: { group } });
await this.detailGroup(group);
// Case of edit path
const isEditRoute = this.props.location.pathname.includes("edit");
if (isEditRoute) {
await this.updateGroupToEdit(group);
this.props.dialogContext.open(EditUserGroup);
}
} else {
// Unknown group
this.handleUnknownGroup();
}
}
}
}
/**
* Handle the user view route change
*/
async handleUserRouteChange() {
const isUserLocation =
this.props.location.pathname.includes("users") ||
this.props.location.pathname.includes("account-recovery-requests/review");
if (isUserLocation) {
const userId = this.props.match.params.selectedUserId;
if (userId) {
// Case of user view
await this.handleSingleUserRouteChange(userId);
} else {
// Case of all and applied filters
await this.handleAllUserRouteChange();
}
}
}
/**
* Handle the user view route change with a user id
* E.g. /users/view/:userId
*/
async handleSingleUserRouteChange(userId) {
const hasUsers = this.users !== null;
if (hasUsers) {
const user = this.users.find((user) => user.id === userId);
const hasNoneFilter = this.state.filter.type === UserWorkspaceFilterTypes.NONE;
if (hasNoneFilter) {
// Case of user view by url bar inputting
await this.search({ type: UserWorkspaceFilterTypes.ALL });
}
// If the user does not exist, it should display an error
if (user) {
await this.selectFromRoute(user);
await this.scrollTo(user);
await this.detailUser(user);
} else {
this.handleUnknownUser();
}
}
}
/**
* Handle the user view route change without a user id in the path
* E.g. /password
*/
async handleAllUserRouteChange() {
const hasUsers = this.users !== null;
if (hasUsers) {
const filter = this.props.location.state?.filter || { type: UserWorkspaceFilterTypes.ALL };
await this.search(filter);
await this.detailNothing();
}
}
/**
* Handle the lock detail display
*/
async handleDetailsLocked() {
await this.lockDetails();
}
/**
* Handle an unknown user ( passe by route parameter user identifier )
*/
handleUnknownUser() {
this.props.actionFeedbackContext.displayError("The user does not exist");
this.props.history.push({ pathname: "/app/users" });
}
/**
* Handle an unknown user ( passe by route parameter user identifier )
*/
handleUnknownGroup() {
this.props.actionFeedbackContext.displayError("The group does not exist");
this.props.history.push({ pathname: "/app/users" });
}
/**
* Handle the scrolling of a user
*/
async handleUserScrolled() {
await this.scrollNothing();
}
/**
* Handle the change of sorter ( on property or direction )
* @param {string} propertyName The name of the property to sort on
*/
async handleSorterChange(propertyName) {
await this.updateSorter(propertyName);
}
/**
* Handle the single user selection
* @param {User} user The selected user
*/
async handleUserSelected(user) {
const selectedUsers = await this.select(user);
this.redirectAfterSelection(selectedUsers);
}
/**
* Handle the single user selection
*/
async handleNoneUsersSelected() {
await this.unselectAll();
await this.detailNothing();
}
/**
* Handle the will to edit a group
* @param group
*/
async handleGroupToEdit(group) {
await this.updateGroupToEdit(group);
}
/**
* Handle the wait for the initial user to be loaded
*/
handleUsersWaitedFor() {
this.props.loadingContext.add();
}
/**
* Handle the intial loading of the users
*/
handleUsersLoaded() {
const hasUsersBeenInitialized = this.users === null && this.props.context.users;
if (hasUsersBeenInitialized) {
this.props.loadingContext.remove();
this.handleUsersLoaded = () => {};
}
}
/**
* Returns true if the given user requires attention from an admin.
* @param {User} user
* @returns {Boolean}
*/
isAttentionRequired(user) {
return Boolean(user.pending_account_recovery_request) || user.missing_metadata_key_ids?.length > 0;
}
/**
* Populate the context with initial data such as users and groups
*/
populate() {
this.props.context.port.request("passbolt.users.update-local-storage");
this.props.context.port.request("passbolt.groups.update-local-storage");
}
/** USER SEARCH **/
/**
* Search for the users which matches the given filter and sort them
* @param {Object} filter
* @returns {Array} The filtered users array
*/
async search(filter) {
const isRecentlyModifiedFilter = filter.type === UserWorkspaceFilterTypes.RECENTLY_MODIFIED;
const searchOperations = {
[UserWorkspaceFilterTypes.GROUP]: this.searchByGroup.bind(this),
[UserWorkspaceFilterTypes.TEXT]: this.searchByText.bind(this),
[UserWorkspaceFilterTypes.RECENTLY_MODIFIED]: this.searchByRecentlyModified.bind(this),
[UserWorkspaceFilterTypes.SUSPENDED_USER]: this.searchBySuspendedUsers.bind(this),
[UserWorkspaceFilterTypes.ALL]: this.searchAll.bind(this),
[UserWorkspaceFilterTypes.NONE]: () => {
/* No search */
},
[UserWorkspaceFilterTypes.ACCOUNT_RECOVERY_REQUEST]: this.searchByAccountRecoveryRequestUsers.bind(this),
[UserWorkspaceFilterTypes.MISSING_METADATA_KEY]: this.searchByMissingMetadataKeyUsers.bind(this),
};
let filteredUsers = searchOperations[filter.type](filter);
if (!isRecentlyModifiedFilter) {
filteredUsers = this.sort(filteredUsers);
} else {
await this.resetSorter();
}
this.setState({ filter, filteredUsers });
return filteredUsers;
}
/**
* All filter ( no filter at all )
* @param {string} filter The All filter
* @returns {Array} All users
*/
searchAll() {
return this.users;
}
/**
* Filter the users which belongs to the given group
* @param {string} filter The group filter
* @returns {Array} Filtered users
*/
searchByGroup(filter) {
const group = filter.payload.group;
const usersGroupIds = group.groups_users.map((groupUser) => groupUser.user_id);
return this.users.filter((user) => usersGroupIds.some((userId) => userId === user.id));
}
/**
* Filter the users which textual properties matched some user text words
* @param {string} filter A textual filter
* @returns {Array} Filtered users
*/
searchByText(filter) {
const text = filter.payload;
const words = (text && text.split(/\s+/)) || [""];
// Test match of some escaped test words against the name / username
const escapeWord = (word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const wordToRegex = (word) => new RegExp(escapeWord(word), "i");
const matchWord = (word, value) => wordToRegex(word).test(value);
const matchUsernameProperty = (word, user) => matchWord(word, user.username);
const matchNameProperty = (word, user) =>
matchWord(word, user.profile.first_name) || matchWord(word, user.profile.last_name);
const matchUser = (word, user) => matchUsernameProperty(word, user) || matchNameProperty(word, user);
const matchText = (user) => words.every((word) => matchUser(word, user));
return this.users.filter(matchText);
}
/**
* Keep the most recently modified users ( current state: just sort everything with the most recent modified users )
* @param {string} filter A recently modified filter
* @returns {Array} Sorted users (same reference as this.users)
*/
searchByRecentlyModified() {
const recentlyModifiedSorter = (user1, user2) =>
DateTime.fromISO(user2.modified) < DateTime.fromISO(user1.modified) ? -1 : 1;
return this.users.sort(recentlyModifiedSorter);
}
/**
* Keep only the currently suspended users
* @param {string} filter A suspended users filter
* @returns {Array} Filtered users
*/
searchBySuspendedUsers() {
return this.users.filter((u) => isUserSuspended(u));
}
/**
* Keep only the users who have account recovery requests
* @param {string} filter A account recovery request users filter
* @returns {Array} Filtered users
*/
searchByAccountRecoveryRequestUsers() {
return this.users.filter((user) => isAccountRecoveryRequested(user));
}
/**
* Keep only the users who have missing metadata keys
* @param {string} filter A missing metadata key users filter
* @returns {Array} Filtered users
*/
searchByMissingMetadataKeyUsers() {
return this.users.filter((user) => isMissingMetadataKey(user));
}
/**
* Refresh the filter in case of its payload is outdated due to the updated list of users
* @returns {Promise<Array|null>} The filtered users array if group filter was refreshed,
* null if group does not exist
*/
async refreshSearchFilter() {
const hasGroupFilter = this.state.filter.type === UserWorkspaceFilterTypes.GROUP;
if (hasGroupFilter) {
const isGroupStillExist = this.groups.some((group) => group.id === this.state.filter.payload.group.id);
if (isGroupStillExist) {
// Case of group exists but may have somme applied changes on it
const updatedGroup = this.groups.find((group) => group.id === this.state.filter.payload.group.id);
const filter = Object.assign(this.state.filter, { payload: { group: updatedGroup } });
const filteredUsers = this.search(filter);
return filteredUsers;
} else {
// Case of filter group deleted
const filter = { type: UserWorkspaceFilterTypes.ALL };
this.props.history.push({ pathname: "/app/users", state: { filter } });
return null;
}
}
}
/** USER SELECTION */
/**
* Select the given user as the single selected users if not already selected as single. Otherwise unselect it
* @param {User} user The user to select
* @returns {Array} The new selectedUsers array
*/
async select(user) {
const mustUnselect = this.state.selectedUsers.length === 1 && this.state.selectedUsers[0].id === user.id;
const selectedUsers = mustUnselect ? [] : [user];
this.setState({ selectedUsers });
return selectedUsers;
}
/**
* Selects the given user when one comes from the navigation route
* @param {User} user An user
*/
async selectFromRoute(user) {
const selectedUsers = [user];
this.setState({ selectedUsers });
}
/**
* Unselect all the users
*/
async unselectAll() {
const hasSelectedUsers = this.state.selectedUsers.length !== 0;
if (hasSelectedUsers) {
this.setState({ selectedUsers: [] });
}
}
/**
* Remove from the selected users those which are not present in regard of the current displayed list
* @param {Array} filteredUsers The filtered users array
*/
async unselectUsersNotFiltered(filteredUsers) {
const selectedUsers = this.updateSelectedUsersFromUsersChange(filteredUsers);
this.setState({ selectedUsers });
}
/**
* Return the selected users that are still present in the given filtered users list
* @param {Array} filteredUsers The filtered users array
* @returns {Array} The selected users still present in the filtered list
*/
updateSelectedUsersFromUsersChange(filteredUsers) {
const matchId = (selectedUser) => (user) => user.id === selectedUser.id;
const matchSelectedUser = (selectedUser) => filteredUsers.some(matchId(selectedUser));
return this.state.selectedUsers.filter(matchSelectedUser);
}
/**
* Navigate to the appropriate url after some users selection operation
*/
redirectAfterSelection(selectedUsers) {
const hasUsersAndGroups = this.users !== null && this.groups !== null;
if (hasUsersAndGroups) {
const hasUserSelected = selectedUsers.length === 1;
if (hasUserSelected) {
// Case of selected user
this.props.history.push(`/app/users/view/${selectedUsers[0].id}`);
} else {
const { filter } = this.state;
const isGroupFilter = filter.type === UserWorkspaceFilterTypes.GROUP;
if (isGroupFilter) {
const mustRedirect =
this.props.location.pathname !== `/app/groups/view/${this.state.filter.payload.group.id}`;
if (mustRedirect) {
this.props.history.push({ pathname: `/app/groups/view/${this.state.filter.payload.group.id}` });
}
} else {
const mustRedirect = this.props.location.pathname !== "/app/users";
if (mustRedirect) {
this.props.history.push({ pathname: `/app/users`, state: { filter } });
}
}
}
}
}
/** USER SORTER **/
/**
* Update the users sorter given a property name
* @param propertyName
*/
async updateSorter(propertyName) {
const hasSortPropertyChanged = this.state.sorter.propertyName !== propertyName;
const asc = hasSortPropertyChanged || !this.state.sorter.asc;
const sorter = { propertyName, asc };
this.setState({ sorter }, () => this.sort());
}
/**
* Reset the user sorter
*/
async resetSorter() {
const sorter = { propertyName: "modified", asc: false };
this.setState({ sorter });
}
/**
* Sort the users given the current sorter
* @param {Array} users Optional users array to sort. If not provided, sorts and updates state.filteredUsers
* @returns {Array} Sorted users array
*/
sort(users = null) {
const usersToSort = users ?? this.state.filteredUsers;
const reverseSorter = (sorter) => (s1, s2) => -sorter(s1, s2);
const baseSorter = (sorter) => (this.state.sorter.asc ? sorter : reverseSorter(sorter));
const keySorter = (key, sorter) => baseSorter((s1, s2) => sorter(s1[key], s2[key]));
const plainObjectSorter = (sorter) => baseSorter(sorter);
const dateSorter = (d1, d2) => (!d1 ? -1 : !d2 ? 1 : DateTime.fromISO(d1) < DateTime.fromISO(d2) ? -1 : 1);
const stringSorter = (s1, s2) => (s1 || "").localeCompare(s2 || "");
const mfaSorter = (u1, u2) => (u2.is_mfa_enabled === u1.is_mfa_enabled ? 0 : u2.is_mfa_enabled ? -1 : 1);
const accountRecoveryUserSettingStatusSorter = (u1, u2) =>
u2?.account_recovery_user_setting?.status === u1?.account_recovery_user_setting?.status
? 0
: u2?.account_recovery_user_setting?.status
? -1
: 1;
const getUserFullName = (user) => `${user.profile.first_name} ${user.profile.last_name}`;
const nameSorter = (u1, u2) => getUserFullName(u1).localeCompare(getUserFullName(u2));
const roleNameSorter = (roleIdU1, roleIdU2) =>
this.getTranslatedRoleName(roleIdU1).localeCompare(this.getTranslatedRoleName(roleIdU2));
const suspendedSorter = (u1, u2) =>
isUserSuspended(u1) === isUserSuspended(u2) ? 0 : isUserSuspended(u2) ? -1 : 1;
const dateOrStringSorter = ["modified", "last_logged_in"].includes(this.state.sorter.propertyName)
? dateSorter
: stringSorter;
const isNameProperty = this.state.sorter.propertyName === "profile";
const isMfaProperty = this.state.sorter.propertyName === "is_mfa_enabled";
const isRoleNameProperty = this.state.sorter.propertyName === "role_id";
const isSuspendedProperty = this.state.sorter.propertyName === "disabled";
const isAccountRecoveryUserSettingStatusProperty =
this.state.sorter.propertyName === "account_recovery_user_setting.status";
let propertySorter;
if (isNameProperty) {
propertySorter = plainObjectSorter(nameSorter);
} else if (isMfaProperty) {
propertySorter = plainObjectSorter(mfaSorter);
} else if (isRoleNameProperty) {
propertySorter = keySorter("role_id", roleNameSorter);
} else if (isAccountRecoveryUserSettingStatusProperty) {
propertySorter = plainObjectSorter(accountRecoveryUserSettingStatusSorter);
} else if (isSuspendedProperty) {
propertySorter = plainObjectSorter(suspendedSorter);
} else {
propertySorter = keySorter(this.state.sorter.propertyName, dateOrStringSorter);
}
const sortedUsers = usersToSort.sort(propertySorter);
// If no users parameter provided, update state (ex: called from handleSorterChange, user clicks column header)
if (!users) {
this.setState({ filteredUsers: sortedUsers });
}
return sortedUsers;
}
/** USER DETAILS **/
/**
* Set the details focus on the given group
* @param group The group to focus on
*/
async detailGroup(group) {
const locked = this.state.details.locked;
this.setState({ details: { group, user: null, locked } });
}
/**
* Set the details focus on the given user
* @param user The user to focus on
*/
async detailUser(user) {
const locked = this.state.details.locked;
this.setState({ details: { group: null, user, locked } });
}
/**
* Remove the details on something
*/
async detailNothing() {
const hasDetails = this.state.details.user || this.state.details.group;
if (hasDetails) {
const locked = this.state.details.locked;
this.setState({ details: { group: null, user: null, locked } });
}
}
/**
* Lock the group or user details display ( hide or show )
* @returns {Promise<void>}
*/
async lockDetails() {
const details = this.state.details;
const locked = this.state.details.locked;
this.setState({ details: Object.assign({}, details, { locked: !locked }) });
}
/**
* Update the current details with the current list of users or groups
* Note: The user details will be reset whenever the user is not part of the filtered list.
* @param {Array} filteredUsers Optional filtered users array (to avoid React 18 batching issues)
*/
async updateDetails(filteredUsers = null) {
const hasDetails = this.state.details.user || this.state.details.group;
if (hasDetails) {
const hasUserDetails = this.state.details.user;
const locked = this.state.details.locked;
if (hasUserDetails) {
// Case of user details
const updatedUserDetails = filteredUsers.find((user) => user.id === this.state.details.user.id) || null;
this.setState({ details: { user: updatedUserDetails, group: null, locked } });
} else {
// Case of group details
const updatedGroupDetails = this.groups.find((group) => group.id === this.state.details.group.id);
this.setState({ details: { group: updatedGroupDetails, user: null, locked } });
}
}
}
/** USER SCROLLING **/
/**
* Set the user to scroll to
* @param user A user
*/
async scrollTo(user) {
this.setState({ scrollTo: { user } });
}
/**
* Unset the user to scroll to
*/
async scrollNothing() {
this.setState({ scrollTo: {} });
}
/** GROUP EDIT **/
/**
* Updates the group to edit
* @param groupToEdit The group to edit
*/
async updateGroupToEdit(groupToEdit) {
this.setState({ groupToEdit });
}
/** Common roles getters **/
/**
* Get the translated role name by role id
* @param {string} id The role id
* @return {string}
*/
getTranslatedRoleName(id) {
const role = this.props.roleContext.getRole(id);
if (!role) {
return "";
}
/*
* The i18n parser can't find the translation for default role names.
* To fix that we can use it in comment
* this.translate("admin")
* this.translate("user")
*/
return role.isAReservedRole() ? this.props.t(role.name) : role.name; //it is a custom role, we do not handle translation
}
/**
* Returns true if the 'Suspended user' filter should be displayed in the UI
* @returns {boolean}
*/
shouldDisplaySuspendedUsersFilter() {
return (
this.props.context.siteSettings.canIUse("disableUser") && this.props.context.loggedInUser.role.name === "admin"
);
}
/**
* Render the component
* @returns {JSX}
*/
render() {
return <UserWorkspaceContext.Provider value={this.state}>{this.props.children}</UserWorkspaceContext.Provider>;
}
}
UserWorkspaceContextProvider.displayName = "UserWorkspaceContextProvider";
UserWorkspaceContextProvider.propTypes = {
context: PropTypes.any, // The application context
children: PropTypes.any, // The component children
location: PropTypes.object, // The router location
match: PropTypes.object, // The router match helper
history: PropTypes.object, // The router history
actionFeedbackContext: PropTypes.object, // The action feedback context
loadingContext: PropTypes.object, // The loading context
dialogContext: PropTypes.any, // The dialog context
rbacContext: PropTypes.object, // The Rbac context
roleContext: PropTypes.object, // The role context
roles: PropTypes.instanceOf(RolesCollection), // The roles collection
t: PropTypes.func, // The translation function
};
export default withAppContext(
withRouter(
withRbac(
withDialog(withActionFeedback(withLoading(withRoles(withTranslation("common")(UserWorkspaceContextProvider))))),
),
),
);
/**
* User Workspace Context Consumer HOC
* @param WrappedComponent
*/
export function withUserWorkspace(WrappedComponent) {
return class WithUserWorkspace extends React.Component {
render() {
return (
<UserWorkspaceContext.Consumer>
{(UserWorkspaceContext) => <WrappedComponent userWorkspaceContext={UserWorkspaceContext} {...this.props} />}
</UserWorkspaceContext.Consumer>
);
}
};
}
/**
* The list of user workspace search filter types
*/
export const UserWorkspaceFilterTypes = {
NONE: "NONE", // Initial filter at page load
ALL: "ALL", // All users
GROUP: "FILTER-BY-GROUP", // Users for a given group
TEXT: "FILTER-BY-TEXT-SEARCH", // Users matching some text words
RECENTLY_MODIFIED: "FILTER-BY-RECENTLY-MODIFIED", // Keep recently modified users
SUSPENDED_USER: "FILTER-BY-SUSPENDED-USER", // Keep only suspended users
ACCOUNT_RECOVERY_REQUEST: "ACCOUNT_RECOVERY_REQUEST",
MISSING_METADATA_KEY: "MISSING_METADATA_KEY",
};