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
8 changes: 8 additions & 0 deletions app/frontend/entrypoints/ui_v2.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
2 changes: 1 addition & 1 deletion app/frontend/src/app/routes/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const SettingsIndexRoute = () => {
<Col md={7}>
<UserForm user={user.data} isEditingSelf={true} />
</Col>
<Col md={{ span: 4, offset: 1 }} style={{ borderLeft: '1px solid #ddd', paddingLeft: '20px' }}>
<Col md={{ span: 4, offset: 1 }} className="border-start-md pt-4 pt-md-0 ps-md-4">
<UserAPIKeyGenerationForm userUid={user.data.uid} apiKeyDigest={user.data.apiKeyDigest} />
</Col>
</Row>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' }),
Expand All @@ -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(<UserProjectsRoute />, {
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(<UserProjectsRoute />, {
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(<UserProjectsRoute />, {
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(<UserProjectsRoute />, {
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();
});
});
});
});
10 changes: 6 additions & 4 deletions app/frontend/src/components/ui/table-builder/table-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ interface TableHeaderProps<T> {

function TableHeader<T>({ headerGroup }: TableHeaderProps<T>) {
const renderSortingIcon = (sortDirection: 'asc' | 'desc' | null) => {
const sharedClassNames = 'ms-2 flex-shrink-0 mt-1'

if (sortDirection === 'asc') {
return <ArrowUp className="ms-2" size={14} />
return <ArrowUp className={sharedClassNames} size={14} />
}
if (sortDirection === 'desc') {
return <ArrowDown className="ms-2" size={14} />
return <ArrowDown className={sharedClassNames} size={14} />
}
return <ArrowDownUp className="ms-2" style={{ color: '#b5b5b5ff' }} size={14} />
return <ArrowDownUp className={sharedClassNames} style={{ color: '#b5b5b5ff' }} size={14} />
}

const createColumnHeader = (header: HeaderGroup<T>) => {
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()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,9 +18,7 @@ export const CopyOtherPermissionsDisplay = ({
}: CopyOtherPermissionsDisplayProps) => {
return (
<div className="mb-4" style={{ borderBottom: '1px solid #dee2e6', paddingBottom: '1.5rem' }}>
<p>You can copy another user&apos;s project permissions by selecting their name from a dropdown. The permissions will be merged with any existing permissions.
<br />
Don&apos;t worry, you can still make individual adjustments before saving.
<p>You can copy project permissions from another user to this one by selecting the other user&apos;s name from the dropdown list below.
</p>
<Row className="g-2 align-items-center">
<Col md={4}>
Expand Down
2 changes: 1 addition & 1 deletion app/frontend/src/features/users/components/user-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const UserEdit = ({ userUid }: { userUid: string }) => {
<Col md={7}>
<UserForm user={user} />
</Col>
<Col md={{ span: 4, offset: 1 }} className="border-start ps-4">
<Col md={{ span: 4, offset: 1 }} className="border-start-md pt-4 pt-md-0 ps-md-4">
<UserAPIKeyGenerationForm userUid={user.uid} apiKeyDigest={user.apiKeyDigest} />
</Col>
</Row>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const UserProjectPermissionsForm = ({ userUid }: { userUid: string }) =>
addPermission,
updatePermission,
removePermission,
removeAllPermissions,
hasChanges,
handleSave,
isError,
Expand Down Expand Up @@ -86,6 +87,16 @@ export const UserProjectPermissionsForm = ({ userUid }: { userUid: string }) =>
mergeUserPermissions={mergePermissions}
/>

<div className="d-flex justify-content-end pt-2">
<Button
variant="btn btn-sm btn-outline-secondary"
onClick={removeAllPermissions}
disabled={data.length === 0 || mutation.isPending}
>
Remove All Project Permissions
</Button>
</div>

<ProjectPermissionsTable
data={data}
unassignedProjects={unassignedProjects}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export const useProjectPermissionsForm = ({ userUid }: UseProjectPermissionsForm
setData((old) => old.filter((_, index) => index !== rowIndex));
}, []);

const removeAllPermissions = useCallback(() => {
setData([]);
}, []);

const mergePermissions = useCallback(() => {
if (!selectedUserUid || !selectedUserPermissionsQuery.data) return;

Expand Down Expand Up @@ -98,6 +102,7 @@ export const useProjectPermissionsForm = ({ userUid }: UseProjectPermissionsForm
addPermission,
updatePermission,
removePermission,
removeAllPermissions,
handleSave,
mutation: updatePermissionsMutation,
// Copy permissions from another user
Expand Down
Loading