diff --git a/app/frontend/entrypoints/ui_v2.scss b/app/frontend/entrypoints/ui_v2.scss index 03986c0a6..c356be19d 100644 --- a/app/frontend/entrypoints/ui_v2.scss +++ b/app/frontend/entrypoints/ui_v2.scss @@ -5,3 +5,11 @@ .letter-spacing-wide { letter-spacing: 0.4px; } + +.border-start-md { + border-left: none !important; + + @media (min-width: 768px) { + border-left: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; + } +} diff --git a/app/frontend/src/app/routes/settings/index.tsx b/app/frontend/src/app/routes/settings/index.tsx index 8dbc71313..3acb16aa5 100644 --- a/app/frontend/src/app/routes/settings/index.tsx +++ b/app/frontend/src/app/routes/settings/index.tsx @@ -15,7 +15,7 @@ const SettingsIndexRoute = () => { - + diff --git a/app/frontend/src/app/routes/users/__tests__/project-permissions.test.tsx b/app/frontend/src/app/routes/users/__tests__/project-permissions.test.tsx index c04b065e2..a4089af8f 100644 --- a/app/frontend/src/app/routes/users/__tests__/project-permissions.test.tsx +++ b/app/frontend/src/app/routes/users/__tests__/project-permissions.test.tsx @@ -225,7 +225,7 @@ describe('User Project Permissions Route', () => { }); }); - describe('removing permissions', () => { + describe('removing individual permissions', () => { it('should remove a project row when clicking Delete', async () => { const permissions = [ buildProjectPermission({ projectId: 1, projectDisplayLabel: 'Project To Keep', projectStringKey: 'keep' }), @@ -249,5 +249,82 @@ describe('User Project Permissions Route', () => { expect(screen.getByText(/you have unsaved changes/i)).toBeInTheDocument(); }); }); + + describe('clearing all permissions', () => { + it('should disable the Remove All button when user has no permissions', async () => { + mockNonAdminPermissionsAPIs('regular-user'); + + await renderApp(, { + path: '/users/:userUid/project-permissions/edit', + url: '/users/regular-user/project-permissions/edit', + }); + + await screen.findByRole('button', { name: /save changes/i }); + + expect( + screen.getByRole('button', { name: /remove all project permissions/i }), + ).toBeDisabled(); + }); + + it('should enable the Remove All button when user has permissions', async () => { + const permissions = [buildProjectPermission()]; + + mockNonAdminPermissionsAPIs('regular-user', { permissions }); + + await renderApp(, { + path: '/users/:userUid/project-permissions/edit', + url: '/users/regular-user/project-permissions/edit', + }); + + await screen.findByText('Test Project Alpha'); + + expect( + screen.getByRole('button', { name: /remove all project permissions/i }), + ).toBeEnabled(); + }); + + it('should remove all project rows when clicking Remove All', async () => { + const permissions = [ + buildProjectPermission({ projectId: 1, projectDisplayLabel: 'Project One', projectStringKey: 'one' }), + buildProjectPermission({ projectId: 2, projectDisplayLabel: 'Project Two', projectStringKey: 'two' }), + ]; + + mockNonAdminPermissionsAPIs('regular-user', { permissions }); + + await renderApp(, { + path: '/users/:userUid/project-permissions/edit', + url: '/users/regular-user/project-permissions/edit', + }); + + await screen.findByText('Project One'); + expect(screen.getByText('Project Two')).toBeInTheDocument(); + + await userEvent.click( + screen.getByRole('button', { name: /remove all project permissions/i }), + ); + + expect(screen.queryByText('Project One')).not.toBeInTheDocument(); + expect(screen.queryByText('Project Two')).not.toBeInTheDocument(); + expect(screen.getByText(/no project permissions assigned/i)).toBeInTheDocument(); + }); + + it('should disable the Remove All button after clearing all permissions', async () => { + const permissions = [buildProjectPermission()]; + + mockNonAdminPermissionsAPIs('regular-user', { permissions }); + + await renderApp(, { + path: '/users/:userUid/project-permissions/edit', + url: '/users/regular-user/project-permissions/edit', + }); + + await screen.findByText('Test Project Alpha'); + + const clearAllButton = screen.getByRole('button', { name: /remove all project permissions/i }); + await userEvent.click(clearAllButton); + + expect(clearAllButton).toBeDisabled(); + }); + }); }); }); \ No newline at end of file diff --git a/app/frontend/src/components/ui/table-builder/table-header.tsx b/app/frontend/src/components/ui/table-builder/table-header.tsx index a1620dfed..e2212ceb6 100644 --- a/app/frontend/src/components/ui/table-builder/table-header.tsx +++ b/app/frontend/src/components/ui/table-builder/table-header.tsx @@ -7,19 +7,21 @@ interface TableHeaderProps { function TableHeader({ headerGroup }: TableHeaderProps) { const renderSortingIcon = (sortDirection: 'asc' | 'desc' | null) => { + const sharedClassNames = 'ms-2 flex-shrink-0 mt-1' + if (sortDirection === 'asc') { - return + return } if (sortDirection === 'desc') { - return + return } - return + return } const createColumnHeader = (header: HeaderGroup) => { if (header.isPlaceholder) return null - const sharedClassNames = 'fw-semibold d-flex justify-content-between m-0 p-0 align-items-center' + const sharedClassNames = 'fw-semibold d-inline-flex m-0 p-0 align-items-start text-start' const headerText = flexRender(header.column.columnDef.header, header.getContext()) if (header.column.getCanSort()) { diff --git a/app/frontend/src/features/users/components/copy-other-user-permissions-display.tsx b/app/frontend/src/features/users/components/copy-other-user-permissions-display.tsx index cd856faa1..ad1d14b24 100644 --- a/app/frontend/src/features/users/components/copy-other-user-permissions-display.tsx +++ b/app/frontend/src/features/users/components/copy-other-user-permissions-display.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Button, Row, Col } from 'react-bootstrap'; import { User } from '@/types/api'; import { AutocompleteSelect } from '@/components/ui/autocomplete-select'; @@ -19,9 +18,7 @@ export const CopyOtherPermissionsDisplay = ({ }: CopyOtherPermissionsDisplayProps) => { return (
-

You can copy another user's project permissions by selecting their name from a dropdown. The permissions will be merged with any existing permissions. -
- Don't worry, you can still make individual adjustments before saving. +

You can copy project permissions from another user to this one by selecting the other user's name from the dropdown list below.

diff --git a/app/frontend/src/features/users/components/user-edit.tsx b/app/frontend/src/features/users/components/user-edit.tsx index 2113e74ef..f7ee38dfe 100644 --- a/app/frontend/src/features/users/components/user-edit.tsx +++ b/app/frontend/src/features/users/components/user-edit.tsx @@ -22,7 +22,7 @@ export const UserEdit = ({ userUid }: { userUid: string }) => { - + diff --git a/app/frontend/src/features/users/components/user-project-permissions-form.tsx b/app/frontend/src/features/users/components/user-project-permissions-form.tsx index 6d12e3228..9bac4da2d 100644 --- a/app/frontend/src/features/users/components/user-project-permissions-form.tsx +++ b/app/frontend/src/features/users/components/user-project-permissions-form.tsx @@ -23,6 +23,7 @@ export const UserProjectPermissionsForm = ({ userUid }: { userUid: string }) => addPermission, updatePermission, removePermission, + removeAllPermissions, hasChanges, handleSave, isError, @@ -86,6 +87,16 @@ export const UserProjectPermissionsForm = ({ userUid }: { userUid: string }) => mergeUserPermissions={mergePermissions} /> +
+ +
+ old.filter((_, index) => index !== rowIndex)); }, []); + const removeAllPermissions = useCallback(() => { + setData([]); + }, []); + const mergePermissions = useCallback(() => { if (!selectedUserUid || !selectedUserPermissionsQuery.data) return; @@ -98,6 +102,7 @@ export const useProjectPermissionsForm = ({ userUid }: UseProjectPermissionsForm addPermission, updatePermission, removePermission, + removeAllPermissions, handleSave, mutation: updatePermissionsMutation, // Copy permissions from another user