ToolJet/frontend/src/modules/common/components/UsersTable/UsersTable.jsx
Midhun G S 0c5ab3484c
Platform LTS Final fixes (#13221)
* Cloud Blocker bugfixes (#13160)

* fix

* minor email fixes

* settings menu fix

* fixes

* Bugfixes/whitelabelling apis (#13180)

* white-labelling apis

* removed consoles logs

* reverts

* fixes for white-labelling

* fixes

* reverted breadcrumb changes (#13194)

* fixes for getting public sso configurations

* fix for enable signup on cloud

* Cloud Trial and Banners (#13182)

* Cloud Blocker bugfixes (#13160)

* fix

* minor email fixes

* settings menu fix

* fixes

* Cloud Trial and Banners

* revert

* initial commit

* Added website onboarding APIs

* moved ai onboarding controller to auth module

* ee banners

* fix

---------

Co-authored-by: Rohan Lahori <64496391+rohanlahori@users.noreply.github.com>
Co-authored-by: gsmithun4 <gsmithun4@gmail.com>

* Bugfixes/minor UI fixes-CLoud (#13203)

* Bugfixes/UI bugs platform 1 (#13205)

* cleanup

* Audit logs fix

* gitignore changes

* postgrest configs removed

* removed unused import

* improvements

* fix

* improved startup logs

* Platform cypress fix (#13192)

* Cloud Blocker bugfixes (#13160)

* fix

* minor email fixes

* settings menu fix

* fixes

* Bugfixes/whitelabelling apis (#13180)

* white-labelling apis

* removed consoles logs

* reverts

* fixes for white-labelling

* fixes

* Cypress fix

* reverted breadcrumb changes (#13194)

* cypress fix

* title fix

* fixes for getting public sso configurations

---------

Co-authored-by: Rohan Lahori <64496391+rohanlahori@users.noreply.github.com>
Co-authored-by: gsmithun4 <gsmithun4@gmail.com>

* deployment fix

* added interfaces and permissions

* Bugfixes/lts 3.6 branch 1 platform (#13238)

* fix

* Licensing Banners Fixes Cloud and EE (#13241)

* design: Adds license buttons to header

* Refactor header actions

* Cloud Blocker bugfixes (#13160)

* fix

* minor email fixes

* settings menu fix

* fixes

* subscription page

* fix banners

---------

Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Rohan Lahori <64496391+rohanlahori@users.noreply.github.com>

* fix for public apps

* fix

* CE Instance Signup bug (#13254)

* CE Instance Signup bug

* improvement

* fix

* Add WEBSITE_SIGNUP_URL to deployment environment variables

* Add WEBSITE_SIGNUP_URL to environment variables for deployment

* Super admin banner fix (#13262)

* Git Sync Fixes  (#13249)

* git-sync module changes

* git sync fixes

* added app resource guard

* git-sync fixes

* removed require feature

* fix

* review comment changes

* ypress fix

* App logo fix inside app builder

* fix for subpath cache

* fix (#13274)

* platform-cypress-fix (#13271)

* git sync fixes (#13277)

* fix

* Add data-cy for new components (#13289)

---------

Co-authored-by: Rohan Lahori <64496391+rohanlahori@users.noreply.github.com>
Co-authored-by: Rudhra Deep Biswas <98055396+rudeUltra@users.noreply.github.com>
Co-authored-by: Ajith KV <ajith.jaban@gmail.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: rohanlahori <rohanlahori99@gmail.com>
Co-authored-by: Adish M <adish.madhu@gmail.com>
Co-authored-by: Rudra deep Biswas <rudra21ultra@gmail.com>
2025-07-09 22:36:41 +05:30

375 lines
15 KiB
JavaScript

import React, { useEffect, useRef, useState } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import Avatar from '@/_ui/Avatar';
import cx from 'classnames';
import { Pagination } from '@/_components';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { Tooltip } from 'react-tooltip';
import UsersActionMenu from './components/UsersActionMenu';
import { humanizeifDefaultGroupName, decodeEntities } from '@/_helpers/utils';
import { ResetPasswordModal } from '@/_components/ResetPasswordModal';
import OverflowTooltip from '@/_components/OverflowTooltip';
import { NoActiveWorkspaceModal } from './components/NoActiveWorkspaceModal';
import Spinner from 'react-bootstrap/Spinner';
import { ToolTip } from '@/_components/ToolTip';
import { fetchEdition } from '../../helpers/utils';
const UsersTable = ({
isLoading,
users,
archivingUser,
unarchivingUser,
generateInvitationURL,
invitationLinkCopyHandler,
unarchiveOrgUser,
archiveOrgUser,
meta,
pageChanged,
darkMode,
translator,
isLoadingAllUsers,
openOrganizationModal,
openEditModal,
customStyles,
toggleEditUserDrawer,
resetPassword = false,
wsSettings = false,
}) => {
const [isResetPasswordModalVisible, setIsResetPasswordModalVisible] = useState(false);
const [selectedUser, setSelectedUser] = useState(null);
const [showNoActiveWorkspaceModal, setShowNoActiveWorkspaceModal] = useState(false);
const hideAccountSetupLink = window.public_config?.HIDE_ACCOUNT_SETUP_LINK == 'true';
// Check if user has metadata
const shouldShowMetadataColumn = wsSettings && Array.isArray(users) && users.some((user) => user.user_metadata);
function showMetadataIcon(metadata) {
if (!metadata) return false;
for (const [key, value] of Object.entries(metadata)) {
// Check if both key and value are not empty
if (key.trim() !== '' && value.trim() !== '') {
return true;
}
}
return false;
}
const handleResetPasswordClick = (user) => {
setSelectedUser(user);
setIsResetPasswordModalVisible(true);
};
const edition = fetchEdition();
return (
<div className="workspace-settings-table-wrap mb-4">
<NoActiveWorkspaceModal
show={showNoActiveWorkspaceModal}
handleClose={() => {
setShowNoActiveWorkspaceModal(false);
}}
darkMode={darkMode}
/>
<div style={customStyles} className="tj-user-table-wrapper">
<div className="card-table fixedHeader table-responsive">
<table data-testid="usersTable" className="users-table table table-vcenter h-100 mx-0">
<thead>
<tr>
<th data-cy="users-table-name-column-header" data-name="name-header">
{translator('header.organization.menus.manageUsers.name', 'Name')}
</th>
{shouldShowMetadataColumn && (
<th data-cy="users-table-metadata-column-header" data-name="meta-header">
Metadata
</th>
)}
{!isLoadingAllUsers && (
<th data-cy="users-table-roles-column-header" data-name="role-header">
User role
</th>
)}
{isLoadingAllUsers && (
<th data-cy="users-table-type-column-header">
{translator('header.organization.menus.manageUsers.userType', 'Type')}
</th>
)}
{!isLoadingAllUsers && (
<th data-cy="users-table-groups-column-header" data-name="custom-header">
Custom groups
</th>
)}
{users && users[0]?.status ? (
<th data-cy="users-table-status-column-header" data-name={wsSettings ? 'status-header' : ''}>
{translator('header.organization.menus.manageUsers.status', 'Status')}
</th>
) : (
<th className="w-1"></th>
)}
{isLoadingAllUsers && (
<th data-cy="users-table-workspaces-column-header">
{translator('header.organization.menus.manageUsers.workspaces', 'Workspaces')}
</th>
)}
<th className="w-1 !tw-w-16 !tw-max-w-16 !tw-min-w-16"></th>
</tr>
</thead>
{isLoading ? (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: 'calc(100vh - 270px)',
}}
>
<Spinner variant="primary" />
</div>
) : (
<tbody>
{Array.isArray(users) &&
users.length > 0 &&
users.map((user) => (
<tr key={user.id} data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-row`}>
<td data-name="name-header">
<Avatar
avatarId={user.avatar_id}
text={`${user.first_name ? user.first_name[0] : ''}${
user.last_name ? user.last_name[0] : ''
}`}
/>
<div className="user-detail">
<span
className="mx-3 tj-text tj-text-sm"
data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-name`}
>
<OverflowTooltip>{decodeEntities(user.name)}</OverflowTooltip>
</span>
<span
style={{ color: '#687076' }}
className="user-email mx-3 tj-text-xsm"
data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-email`}
>
<OverflowTooltip>{user.email}</OverflowTooltip>
</span>
</div>
</td>
{shouldShowMetadataColumn && (
<td data-name="meta-header">
<span className="text-muted user-type">
<div className={`metadata ${showMetadataIcon(user?.user_metadata) ? '' : 'empty'}`}>
{showMetadataIcon(user?.user_metadata) ? '{..}' : '-'}
</div>
</span>
</td>
)}
{isLoadingAllUsers && (
<td className="text-muted !tw-w-[230px] tw-max-w-[230px]">
<span
className="text-muted user-type"
data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-type`}
>
{user.user_type}
</span>
</td>
)}
{!isLoadingAllUsers && (
<GroupChipTD groups={user.role_group.map((group) => group.name)} isRole={true} />
)}
{!isLoadingAllUsers && <GroupChipTD groups={user.groups.map((group) => group.name)} />}
{user.status && (
<td
className="text-muted !tw-w-[230px] tw-max-w-[230px]"
data-name={wsSettings ? 'status-header' : ''}
style={{ marginRight: wsSettings ? '6px' : '0px' }}
>
<span
className={cx('badge', {
'tj-invited': user.status === 'invited',
'tj-archive': user.status === 'archived',
'tj-active': user.status === 'active',
})}
data-cy="status-badge"
></span>
<small
className="workspace-user-status tj-text-sm text-capitalize"
data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-status`}
>
{user.status}
</small>
{user.status === 'invited' &&
!hideAccountSetupLink &&
user?.invitation_token &&
edition != 'cloud' ? (
<div className="workspace-clipboard-wrap">
<CopyToClipboard text={generateInvitationURL(user)} onCopy={invitationLinkCopyHandler}>
<span>
<SolidIcon
data-tooltip-id="tooltip-for-copy-invitation-link"
data-tooltip-content="Copy invitation link"
width="10"
fill="#889096"
name="copy"
/>
<p
className="tj-text-xsm"
data-cy={`${user.name
.toLowerCase()
.replace(/\s+/g, '-')}-user-copy-invitation-link`}
>
Copy link
</p>
</span>
</CopyToClipboard>
<Tooltip id="tooltip-for-copy-invitation-link" className="tooltip" />
</div>
) : (
''
)}
</td>
)}
{isLoadingAllUsers && (
<td className="text-muted !tw-w-[230px] tw-max-w-[230px]">
<a
className="px-2 text-muted workspaces"
onClick={
user.total_organizations > 0
? () => openOrganizationModal(user)
: () => {
setShowNoActiveWorkspaceModal(true);
}
}
data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-view-button`}
>
View ({user.total_organizations})
</a>
</td>
)}
<td className="user-actions-button tw-w-16 tw-max-w-16">
<UsersActionMenu
archivingUser={archivingUser}
user={user}
unarchivingUser={unarchivingUser}
unarchiveOrgUser={unarchiveOrgUser}
archiveOrgUser={archiveOrgUser}
toggleEditUserDrawer={() => toggleEditUserDrawer(user)}
onResetPasswordClick={() => handleResetPasswordClick(user)}
resetPassword={resetPassword}
/>
</td>
</tr>
))}
</tbody>
)}
</table>
</div>
{meta?.total_count > 10 && (
<Pagination
currentPage={meta.current_page}
count={meta.total_count}
pageChanged={pageChanged}
itemsPerPage={10}
darkMode={darkMode}
/>
)}
</div>
{isResetPasswordModalVisible && (
<ResetPasswordModal
show={isResetPasswordModalVisible}
closeModal={() => {
setIsResetPasswordModalVisible(false);
setSelectedUser(null);
}}
user={selectedUser}
/>
)}
</div>
);
};
export default UsersTable;
const GroupChipTD = ({ groups = [], isRole = false }) => {
const [showAllGroups, setShowAllGroups] = useState(false);
const groupsListRef = useRef();
useEffect(() => {
const onCloseHandler = (e) => {
if (groupsListRef.current && !groupsListRef.current.contains(e.target)) {
setShowAllGroups(false);
}
};
window.addEventListener('click', onCloseHandler);
return () => {
window.removeEventListener('click', onCloseHandler);
};
}, [showAllGroups]);
function moveValuesToLast(arr, valuesToMove) {
const validValuesToMove = valuesToMove.filter((value) => arr.includes(value));
validValuesToMove.forEach((value) => {
const index = arr.indexOf(value);
if (index !== -1) {
const removedItem = arr.splice(index, 1);
arr.push(removedItem[0]);
}
});
return arr;
}
const orderedArray = groups;
const toggleAllGroupsList = (e) => {
setShowAllGroups(!showAllGroups);
};
const renderGroupChip = (group, index) => (
<ToolTip message={group}>
<span className="group-chip" key={index} data-cy="group-chip">
{humanizeifDefaultGroupName(group)}
</span>
</ToolTip>
);
return (
<td
data-name={isRole ? 'role-header' : ''}
data-active={showAllGroups}
ref={groupsListRef}
onClick={(e) => {
orderedArray.length > 2 && toggleAllGroupsList(e);
}}
className={cx('text-muted groups-name-cell !tw-w-[230px] tw-max-w-[230px]', {
'groups-hover': orderedArray.length > 2,
})}
>
<div className="groups-name-container tj-text-sm font-weight-500">
{orderedArray.length === 0 ? (
<div className="empty-text">-</div>
) : (
orderedArray.slice(0, 2).map((group, index) => {
if (orderedArray.length <= 2) {
return renderGroupChip(group, index);
}
if (orderedArray.length > 2 && index === 1) {
return (
<React.Fragment key={index}>
{renderGroupChip(group, index)}
<span className="group-chip">+{orderedArray.length - 2} more</span>
{showAllGroups && (
<div className="all-groups-list">
{orderedArray.slice(2).map((group, index) => renderGroupChip(group, index))}
</div>
)}
</React.Fragment>
);
}
return renderGroupChip(group, index);
})
)}
</div>
</td>
);
};