Skip to content

Commit

Permalink
Merge pull request Sage-Bionetworks#1229 from nickgros/SWC-6776
Browse files Browse the repository at this point in the history
  • Loading branch information
nickgros authored Sep 23, 2024
2 parents 652ce90 + eeec8e1 commit d77fc4c
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,24 @@ function verifyHasLocalSharingSettingsMessage() {
)
}

function verifyHasProjectMessage() {
screen.getByText(/The sharing settings shown below apply to this project/)
}

function verifyInheritsSharingSettingsFromBenefactorMessage() {
screen.getByText(
/The sharing settings shown below are currently being inherited/,
{ exact: false },
)
}

function verifyInheritsSharingSettingsPostUploadMessage() {
screen.getByText(
/Currently, the sharing settings are inherited from the project named/,
{ exact: false },
)
}

describe('EntityAclEditor', () => {
beforeEach(() => jest.clearAllMocks())
beforeAll(() => server.listen())
Expand Down Expand Up @@ -281,7 +292,7 @@ describe('EntityAclEditor', () => {
},
mockProject.bundle.benefactorAcl.resourceAccess.length,
)
verifyHasLocalSharingSettingsMessage()
verifyHasProjectMessage()

expect(
screen.queryByRole('button', {
Expand Down Expand Up @@ -507,58 +518,6 @@ describe('EntityAclEditor', () => {
).toBeInTheDocument()
})

it('displays an error on mutate failure', async () => {
const errorReason = 'Something was invalid'
server.use(
rest.put(
`${getEndpoint(BackendDestinationEnum.REPO_ENDPOINT)}${ENTITY_ID(
':entityId',
)}/acl`,
async (req, res, ctx) => {
const status = 400
let response: SynapseApiResponse<AccessControlList> = {
reason: errorReason,
}

return res(ctx.status(status), ctx.json(response))
},
),
)

const { ref, user } = await setUp(
{
entityId: mockFileEntityWithLocalSharingSettingsData.id,
onCanSaveChange,
onUpdateSuccess,
},
mockFileEntityWithLocalSharingSettingsData.bundle!.benefactorAcl
.resourceAccess.length,
)

// Enable sending a message, so we can verify that it is not sent when the update fails
await checkNotifyUsers(user)

// Add a user to the ACL
const newUserRow = await addUserToAcl(user, MOCK_USER_NAME_2)
confirmItem(newUserRow, MOCK_USER_NAME_2, 'Can download')

await waitFor(() => expect(onCanSaveChange).toHaveBeenLastCalledWith(true))

act(() => {
ref.current!.save()
})

const alert = await screen.findByRole('alert')
within(alert).getByText(errorReason)

await waitFor(() => {
expect(updateAclSpy).toHaveBeenCalled()
// Verify callback and sendMessage were not called
expect(onUpdateSuccess).not.toHaveBeenCalled()
expect(sendMessageSpy).not.toHaveBeenCalled()
})
})

it('current user cannot remove themselves', async () => {
const { itemRows } = await setUp(
{
Expand Down Expand Up @@ -626,4 +585,97 @@ describe('EntityAclEditor', () => {
'You do not have sufficient privileges to modify the sharing settings.',
)
})

it('shows appropriate UI in the post-upload scenario', async () => {
const { ref, user } = await setUp(
{
entityId: mockProject.id,
onCanSaveChange,
onUpdateSuccess,
isAfterUpload: true,
},
mockProjectEntityData.bundle.accessControlList!.resourceAccess.length,
)
verifyInheritsSharingSettingsPostUploadMessage()

// Verify no alert appears (yet)
expect(screen.queryByRole('alert')).not.toBeInTheDocument()

// Add a user to the ACL
const newUserRow = await addUserToAcl(user, MOCK_USER_NAME_2)
confirmItem(newUserRow, MOCK_USER_NAME_2, 'Can download')
await waitFor(() => expect(onCanSaveChange).toHaveBeenLastCalledWith(true))

// Verify that an alert appears that indicates that the entire project will be updated
const alert = await screen.findByRole('alert')
within(alert).getByText(/Edits will affect settings of entire project/)

// Save the project
act(() => {
ref.current!.save()
})

await waitFor(() => {
expect(updateAclSpy).toHaveBeenCalledWith(
{
...mockProject.bundle.accessControlList,
resourceAccess: expect.any(Array),
},
MOCK_ACCESS_TOKEN,
)
expect(onUpdateSuccess).toHaveBeenCalled()
expect(sendMessageSpy).not.toHaveBeenCalled()
})
})
it('displays an error on mutate failure', async () => {
const errorReason = 'Something was invalid'
server.use(
rest.put(
`${getEndpoint(BackendDestinationEnum.REPO_ENDPOINT)}${ENTITY_ID(
':entityId',
)}/acl`,
async (req, res, ctx) => {
const status = 400
let response: SynapseApiResponse<AccessControlList> = {
reason: errorReason,
}

return res(ctx.status(status), ctx.json(response))
},
),
)

const { ref, user } = await setUp(
{
entityId: mockFileEntityWithLocalSharingSettingsData.id,
onCanSaveChange,
onUpdateSuccess,
},
mockFileEntityWithLocalSharingSettingsData.bundle!.benefactorAcl
.resourceAccess.length,
)

// Enable sending a message, so we can verify that it is not sent when the update fails
await checkNotifyUsers(user)

// Add a user to the ACL
const newUserRow = await addUserToAcl(user, MOCK_USER_NAME_2)
confirmItem(newUserRow, MOCK_USER_NAME_2, 'Can download')

await waitFor(() => expect(onCanSaveChange).toHaveBeenLastCalledWith(true))

act(() => {
ref.current!.save()
})

const alert = await screen.findByRole('alert')
within(alert).getByText(errorReason)

await waitFor(() => {
expect(updateAclSpy).toHaveBeenCalled()
// Verify callback and sendMessage were not called
expect(onUpdateSuccess).not.toHaveBeenCalled()
expect(sendMessageSpy).not.toHaveBeenCalled()
})
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useEffect, useImperativeHandle, useMemo, useState } from 'react'
import { entityTypeToFriendlyName } from '../../utils/functions/EntityTypeUtils'
import OpenData from './OpenData'
import { AclEditor, AclEditorProps } from '../AclEditor/AclEditor'
import useUpdateAcl from '../AclEditor/useUpdateAcl'
Expand Down Expand Up @@ -26,7 +27,7 @@ import {
useSuspenseGetEntityBundle,
useUpdateEntityACL,
} from '../../synapse-queries'
import { Alert, Stack } from '@mui/material'
import { Alert, Link, Stack } from '@mui/material'
import { InheritanceMessage } from './InheritanceMessage'
import { CreateOrDeleteLocalSharingSettingsButton } from './CreateOrDeleteLocalSharingSettingsButton'
import { resourceAccessListIsEqual } from '../../utils/functions/AccessControlListUtils'
Expand All @@ -35,6 +36,7 @@ import { BackendDestinationEnum, getEndpoint } from '../../utils/functions'
import { getDisplayNameFromProfile } from '../../utils/functions/DisplayUtils'
import { AclEditorSkeleton } from '../AclEditor/AclEditorSkeleton'
import { SynapseErrorBoundary } from '../error'
import FullWidthAlert from '../FullWidthAlert'

const availablePermissionLevels: PermissionLevel[] = [
'CAN_VIEW',
Expand All @@ -45,9 +47,16 @@ const availablePermissionLevels: PermissionLevel[] = [
]

export type EntityAclEditorProps = {
/** The ID of the entity on which to view/edit the ACL. Note that the ACL may actually belong to an entity ancestor (benefactor). */
entityId: string
/** Invoked when the user can/cannot save the pending changes. */
onCanSaveChange: (canSaveChanges: boolean) => void
/** Invoked when changes are successfully made. */
onUpdateSuccess: () => void
/** Special case to show specific copy and allow changes to an inherited ACL immediately after a file is uploaded.
* Note that if this is true, the entityId should be the ID of the folder or project that is the benefactor of the newly uploaded file(s)
* @defaultValue false */
isAfterUpload?: boolean
}

export type EntityAclEditorHandle = {
Expand Down Expand Up @@ -122,7 +131,12 @@ const EntityAclEditor = React.forwardRef(function EntityAclEditor(
props: EntityAclEditorProps,
ref: React.ForwardedRef<EntityAclEditorHandle>,
) {
const { entityId, onCanSaveChange, onUpdateSuccess } = props
const {
entityId,
onCanSaveChange,
onUpdateSuccess,
isAfterUpload = false,
} = props

const { data: ownProfile } = useSuspenseGetCurrentUserProfile()
const { data: entityBundle } = useSuspenseGetEntityBundle(
Expand Down Expand Up @@ -260,6 +274,8 @@ const EntityAclEditor = React.forwardRef(function EntityAclEditor(
const canSave =
hasAclChanged || isLoadingSendMessageToNewACLUsers || isPending

const showInheritedAclUpdateWarning = hasAclChanged && isAfterUpload

useEffect(() => {
onCanSaveChange(canSave)
}, [onCanSaveChange, canSave])
Expand Down Expand Up @@ -318,7 +334,41 @@ const EntityAclEditor = React.forwardRef(function EntityAclEditor(
isProject={isProject}
isInherited={updatedIsInherited}
benefactorId={updatedIsInherited ? parentAcl?.id : entityId}
isAfterUpload={isAfterUpload}
/>
{showInheritedAclUpdateWarning && (
<FullWidthAlert
isGlobal={false}
variant={'warning'}
title={`Edits will affect settings of entire ${entityTypeToFriendlyName(
entityBundle?.entityType,
).toLowerCase()}.`}
description={
<>
<p>
Editing the settings here will impact the sharing settings for
all files and folders within this{' '}
{entityTypeToFriendlyName(
entityBundle?.entityType,
).toLowerCase()}
, not just the ones you&apos;ve recently uploaded.
</p>
<p>
View the instructions above for setting your{' '}
<Link
href={
'https://help.synapse.org/docs/Sharing-Settings,-Permissions,-and-Conditions-for-Use.2024276030.html'
}
target={'_blank'}
>
Local Sharing Settings
</Link>
.
</p>
</>
}
/>
)}
<AclEditor
canEdit={getCanEditResourceAccess(
canEdit,
Expand Down Expand Up @@ -357,12 +407,14 @@ const EntityAclEditor = React.forwardRef(function EntityAclEditor(
showAddRemovePublicButton={true}
/>
{/* Create / delete local sharing settings button */}
{!isProject && entityBundle.permissions.canEnableInheritance && (
<CreateOrDeleteLocalSharingSettingsButton
isInherited={updatedIsInherited}
setIsInherited={setUpdatedIsInherited}
/>
)}
{!isAfterUpload &&
!isProject &&
entityBundle.permissions.canEnableInheritance && (
<CreateOrDeleteLocalSharingSettingsButton
isInherited={updatedIsInherited}
setIsInherited={setUpdatedIsInherited}
/>
)}
{error && <Alert severity="error">{error.message}</Alert>}
</Stack>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ export type EntityAclEditorModalProps = {
open: boolean
onUpdateSuccess?: () => void
onClose: () => void
isAfterUpload?: boolean
}

export default function EntityAclEditorModal(props: EntityAclEditorModalProps) {
const { entityId, open, onUpdateSuccess = noop, onClose } = props
const {
entityId,
open,
onUpdateSuccess = noop,
onClose,
isAfterUpload = false,
} = props
const [isDirty, setIsDirty] = useState(false)
const entityAclEditorRef = useRef<EntityAclEditorHandle>(null)

Expand Down Expand Up @@ -52,6 +59,7 @@ export default function EntityAclEditorModal(props: EntityAclEditorModalProps) {
onUpdateSuccess()
onClose()
}}
isAfterUpload={isAfterUpload}
/>
}
onConfirm={() => {
Expand Down
Loading

0 comments on commit d77fc4c

Please sign in to comment.