Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
277 changes: 222 additions & 55 deletions admin-ui/src/modules/accounts/components/UserList.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,240 @@
import React from 'react';
import { useIntl } from 'react-intl';
import { IRoleAction } from '../../../gql/types';
import useAuth from '../../Auth/useAuth';
import Table from '../../common/components/Table';
import BulkActionsToolbar from '../../common/components/BulkActionsToolbar';
import BulkTagForm from '../../common/components/BulkTagForm';
import useBulkSelection from '../../common/hooks/useBulkSelection';
import useBulkResultHandler from '../../common/hooks/useBulkResultHandler';
import useBulkUserOperations from '../hooks/useBulkUserOperations';
import useModal from '../../modal/hooks/useModal';
import DangerMessage from '../../modal/components/DangerMessage';
import UserListItem from './UserListItem';

const UserList = ({ users }) => {
const { formatMessage } = useIntl();
const { hasRole } = useAuth();
const { setModal } = useModal();

return (
<Table className="min-w-full">
{users?.map((user) => (
<Table.Row key={user._id} header>
<Table.Cell>
{formatMessage({
id: 'name',
defaultMessage: 'Name',
})}
</Table.Cell>

<Table.Cell>
{formatMessage({
id: 'email',
defaultMessage: 'Email',
})}
</Table.Cell>
const {
selectedIds,
selectedCount,
toggle,
clearAll,
isSelected,
toggleAll,
} = useBulkSelection();

<Table.Cell>
{formatMessage({
id: 'status',
defaultMessage: 'Status',
})}
</Table.Cell>
const { bulkUpdateUserTags, bulkRemoveUsers, bulkSetUserRoles } =
useBulkUserOperations();
const handleBulkResult = useBulkResultHandler();

<Table.Cell>
{formatMessage({
id: 'last_login',
defaultMessage: 'Last Login:',
})}
</Table.Cell>
const allIds = users?.map((u) => u._id) || [];
const canManage = hasRole(IRoleAction.RemoveUser);

<Table.Cell>
{formatMessage({
id: 'tags',
defaultMessage: 'Tags',
})}
</Table.Cell>
const bulkActions = canManage
? [
{
key: 'update-tags',
label: formatMessage({
id: 'bulk_update_tags',
defaultMessage: 'Update Tags',
}),
renderForm: ({ onCancel }) => (
<BulkTagForm
onSubmit={async ({ add, remove }) => {
await handleBulkResult(
() => bulkUpdateUserTags(selectedIds, add, remove),
'bulkUpdateUserTags',
);
clearAll();
}}
onCancel={onCancel}
/>
),
},
{
key: 'set-roles',
label: formatMessage({
id: 'bulk_set_roles',
defaultMessage: 'Set Roles',
}),
renderForm: ({ onCancel }) => (
<BulkRolesForm
onSubmit={async (roles) => {
await handleBulkResult(
() => bulkSetUserRoles(selectedIds, roles),
'bulkSetUserRoles',
);
clearAll();
}}
onCancel={onCancel}
/>
),
},
{
key: 'delete',
label: formatMessage({
id: 'bulk_delete',
defaultMessage: 'Delete',
}),
variant: 'danger' as const,
onAction: async (ids: string[]) => {
await new Promise<void>((resolve) => {
setModal(
<DangerMessage
onCancelClick={async () => {
setModal('');
resolve();
}}
message={formatMessage(
{
id: 'bulk_delete_users_warning',
defaultMessage:
'This will permanently delete {count} users. Are you sure?',
},
{ count: ids.length },
)}
onOkClick={async () => {
setModal('');
await handleBulkResult(
() => bulkRemoveUsers(ids),
'bulkRemoveUsers',
);
resolve();
}}
okText={formatMessage({
id: 'delete_users',
defaultMessage: 'Delete Users',
})}
/>,
);
});
},
},
]
: [];

<Table.Cell>
{formatMessage({
id: 'cart',
defaultMessage: 'Cart',
})}
</Table.Cell>

<Table.Cell>
{formatMessage({
id: 'orders',
defaultMessage: 'Orders',
})}
</Table.Cell>
return (
<>
<BulkActionsToolbar
selectedCount={selectedCount}
selectedIds={selectedIds}
onClear={clearAll}
actions={bulkActions}
/>
<Table className="min-w-full">
{users?.map((user) => (
<Table.Row key={user._id} header>
{canManage && (
<Table.Cell>
<input
type="checkbox"
checked={allIds.length > 0 && selectedCount === allIds.length}
onChange={() => toggleAll(allIds)}
className="h-4 w-4 rounded border-slate-300 text-slate-800 focus:ring-slate-800 cursor-pointer"
onClick={(e) => e.stopPropagation()}
/>
</Table.Cell>
)}
<Table.Cell>
{formatMessage({ id: 'name', defaultMessage: 'Name' })}
</Table.Cell>
<Table.Cell>
{formatMessage({ id: 'email', defaultMessage: 'Email' })}
</Table.Cell>
<Table.Cell>
{formatMessage({ id: 'status', defaultMessage: 'Status' })}
</Table.Cell>
<Table.Cell>
{formatMessage({
id: 'last_login',
defaultMessage: 'Last Login:',
})}
</Table.Cell>
<Table.Cell>
{formatMessage({ id: 'tags', defaultMessage: 'Tags' })}
</Table.Cell>
<Table.Cell>
{formatMessage({ id: 'cart', defaultMessage: 'Cart' })}
</Table.Cell>
<Table.Cell>
{formatMessage({ id: 'orders', defaultMessage: 'Orders' })}
</Table.Cell>
<Table.Cell className="text-right">&nbsp;</Table.Cell>
</Table.Row>
))}

<Table.Cell className="text-right">&nbsp;</Table.Cell>
</Table.Row>
))}
{users?.map((user) => (
<UserListItem
key={`${user?._id}-body`}
user={user}
isSelected={isSelected(user._id)}
onToggleSelect={() => toggle(user._id)}
showCheckbox={canManage}
/>
))}
</Table>
</>
);
};

{users?.map((user) => (
<UserListItem key={`${user?._id}-body`} user={user} />
))}
</Table>
const BulkRolesForm = ({
onSubmit,
onCancel,
}: {
onSubmit: (roles: string[]) => void;
onCancel: () => void;
}) => {
const { formatMessage } = useIntl();
const [roles, setRoles] = React.useState('');
return (
<form
onSubmit={(e) => {
e.preventDefault();
const parsed = roles
.split(',')
.map((r) => r.trim())
.filter(Boolean);
if (parsed.length) onSubmit(parsed);
}}
className="flex flex-wrap items-end gap-3"
>
<div className="flex flex-col gap-1">
<label
htmlFor="bulk-roles"
className="text-xs text-slate-500 dark:text-slate-300"
>
{formatMessage({
id: 'bulk_roles',
defaultMessage: 'Roles (comma-separated)',
})}
</label>
<input
id="bulk-roles"
type="text"
value={roles}
onChange={(e) => setRoles(e.target.value)}
placeholder="admin, editor"
className="text-sm px-2 py-1 rounded bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-500 text-slate-900 dark:text-white placeholder:text-slate-400 w-48"
/>
</div>
<button
type="submit"
disabled={!roles.trim()}
className="text-xs font-medium px-3 py-1.5 rounded-md bg-slate-800 hover:bg-slate-900 text-white dark:bg-white/10 dark:hover:bg-white/20 disabled:opacity-50"
>
{formatMessage({ id: 'apply', defaultMessage: 'Apply' })}
</button>
<button
type="button"
onClick={onCancel}
className="text-xs font-medium px-3 py-1.5 rounded-md hover:bg-slate-100 dark:hover:bg-white/10 text-slate-500 dark:text-slate-300"
>
{formatMessage({ id: 'cancel', defaultMessage: 'Cancel' })}
</button>
</form>
);
};

Expand Down
18 changes: 17 additions & 1 deletion admin-ui/src/modules/accounts/components/UserListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ const UserLastLogin = ({ lastLogin }) => {
);
};

const UserListItem = ({ user }) => {
const UserListItem = ({
user,
isSelected = false,
onToggleSelect = undefined,
showCheckbox = false,
}) => {
const { formatMessage, locale } = useIntl();
const router = useRouter();
const { currentUser, loading } = useCurrentUser();
Expand Down Expand Up @@ -74,6 +79,17 @@ const UserListItem = ({ user }) => {

return (
<Table.Row className="group">
{showCheckbox && (
<Table.Cell>
<input
type="checkbox"
checked={isSelected}
onChange={onToggleSelect}
className="h-4 w-4 rounded border-slate-300 text-slate-800 focus:ring-slate-800 cursor-pointer"
onClick={(e) => e.stopPropagation()}
/>
</Table.Cell>
)}
<Table.Cell>
<Link
href={`/users?userId=${user._id}`}
Expand Down
70 changes: 70 additions & 0 deletions admin-ui/src/modules/accounts/hooks/useBulkUserOperations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { gql } from '@apollo/client';
import { useMutation } from '@apollo/client/react';

const BulkUpdateUserTagsMutation = gql`
mutation BulkUpdateUserTags(
$userIds: [ID!]!
$add: [LowerCaseString!]
$remove: [LowerCaseString!]
) {
bulkUpdateUserTags(userIds: $userIds, add: $add, remove: $remove) {
successCount
failedCount
failedIds
}
}
`;

const BulkRemoveUsersMutation = gql`
mutation BulkRemoveUsers($userIds: [ID!]!) {
bulkRemoveUsers(userIds: $userIds) {
successCount
failedCount
failedIds
}
}
`;

const BulkSetUserRolesMutation = gql`
mutation BulkSetUserRoles($userIds: [ID!]!, $roles: [String!]!) {
bulkSetUserRoles(userIds: $userIds, roles: $roles) {
successCount
failedCount
failedIds
}
}
`;

const refetchQueries = ['Users', 'UsersCount'];

const useBulkUserOperations = () => {
const [bulkUpdateTagsMutation] = useMutation(BulkUpdateUserTagsMutation);
const [bulkRemoveMutation] = useMutation(BulkRemoveUsersMutation);
const [bulkSetRolesMutation] = useMutation(BulkSetUserRolesMutation);

return {
bulkUpdateUserTags: (
userIds: string[],
add?: string[],
remove?: string[],
) =>
bulkUpdateTagsMutation({
variables: { userIds, add, remove },
refetchQueries,
}),

bulkRemoveUsers: (userIds: string[]) =>
bulkRemoveMutation({
variables: { userIds },
refetchQueries,
}),

bulkSetUserRoles: (userIds: string[], roles: string[]) =>
bulkSetRolesMutation({
variables: { userIds, roles },
refetchQueries,
}),
};
};

export default useBulkUserOperations;
Loading