From def82e5b7a1e39614a8d1e36523aff4fe2a79d82 Mon Sep 17 00:00:00 2001 From: MalinSvenberg <56155987+MalinSvenberg@users.noreply.github.com> Date: Tue, 7 Mar 2023 14:07:24 +0100 Subject: [PATCH] feat: adds read-only mode to sharing menu * fix: removes invite user field for non-admin users * fix: delete user button is hidden for non-admin users * fix: removes actions header for non admin users * fix: removes document visibility selection for non-admin * fix: removes selection and makes permission selection to text * fix: hides actions, invitations and make selection read only in pending inv * refactor: cleans up the code for read-only authorised table * fix: adds missing import statement * test: adds test for viewing authorised members read-only mode * test: adds test for email, permission and delete button invitation table read-only mode * refactor: removes code duplication * refactor: removes usage of current_user in membership test * docs: adds todo comment for test of no invitation button * test: adds test for permission in pending invitations table * test: adds tests to check if resend invitation button exists or not * docs: removes todo comment from test --- cypress/e2e/invitations/viewInvitation.cy.js | 58 ++++++++++++++++++- cypress/e2e/memberships/viewMemberships.cy.js | 46 ++++++++++++++- cypress/fixtures/invitations.js | 46 +++++++++++++++ cypress/fixtures/memberships.js | 35 +++++++++++ .../item/sharing/InvitationsTable.tsx | 33 ++++++++--- .../item/sharing/ItemMembershipsTable.tsx | 32 ++++++---- .../item/sharing/ItemSharingTab.tsx | 21 +++++-- .../item/sharing/ResendInvitationRenderer.tsx | 8 ++- .../sharing/TableRowPermissionRenderer.tsx | 8 ++- src/config/selectors.ts | 1 + 10 files changed, 258 insertions(+), 30 deletions(-) diff --git a/cypress/e2e/invitations/viewInvitation.cy.js b/cypress/e2e/invitations/viewInvitation.cy.js index 37dbe791a..15f01faeb 100644 --- a/cypress/e2e/invitations/viewInvitation.cy.js +++ b/cypress/e2e/invitations/viewInvitation.cy.js @@ -1,10 +1,15 @@ import { buildItemPath } from '../../../src/config/paths'; import { + ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS, + ITEM_RESEND_INVITATION_BUTTON_CLASS, buildInvitationTableRowSelector, buildItemInvitationRowDeleteButtonId, buildShareButtonId, } from '../../../src/config/selectors'; -import { ITEMS_WITH_INVITATIONS } from '../../fixtures/invitations'; +import { + ITEMS_WITH_INVITATIONS, + ITEM_WITH_INVITATIONS_WRITE_ACCESS, +} from '../../fixtures/invitations'; describe('View Invitations', () => { beforeEach(() => { @@ -17,7 +22,7 @@ describe('View Invitations', () => { cy.visit(buildItemPath(item.id)); cy.get(`#${buildShareButtonId(item.id)}`).click(); - invitations.forEach(({ itemPath, id, email }) => { + invitations.forEach(({ itemPath, id, email, permission }) => { cy.get(buildInvitationTableRowSelector(id)).should('contain', email); if (itemPath !== item.path) { @@ -25,8 +30,55 @@ describe('View Invitations', () => { 'be.disabled', ); } + cy.get( + `${buildInvitationTableRowSelector( + id, + )} .${ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS} input`, + ).should('have.value', permission); + + cy.get( + `${buildInvitationTableRowSelector( + id, + )} .${ITEM_RESEND_INVITATION_BUTTON_CLASS}`, + ).should('exist'); }); + }); +}); - // todo: check permission +describe('View Invitations Read-Only Mode', () => { + beforeEach(() => { + cy.setUpApi({ ...ITEM_WITH_INVITATIONS_WRITE_ACCESS }); + }); + + it('view invitation in share item modal read-only mode', () => { + const item = ITEM_WITH_INVITATIONS_WRITE_ACCESS.items[0]; + const { invitations } = item; + cy.visit(buildItemPath(item.id)); + cy.get(`#${buildShareButtonId(item.id)}`).click(); + + invitations.forEach(({ id, email, permission }) => { + cy.get(buildInvitationTableRowSelector(id)) + .should('contain', email) + .should('contain', permission); + + // delete invitation button should not exist + cy.get(`#${buildItemInvitationRowDeleteButtonId(id)}`).should( + 'not.exist', + ); + + // check no permission select component exists + cy.get( + `${buildInvitationTableRowSelector( + id, + )} .${ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS} input`, + ).should('not.exist'); + + // resend invitation button should not exist + cy.get( + `${buildInvitationTableRowSelector( + id, + )} .${ITEM_RESEND_INVITATION_BUTTON_CLASS}`, + ).should('not.exist'); + }); }); }); diff --git a/cypress/e2e/memberships/viewMemberships.cy.js b/cypress/e2e/memberships/viewMemberships.cy.js index 89eb14d9b..fce1969ec 100644 --- a/cypress/e2e/memberships/viewMemberships.cy.js +++ b/cypress/e2e/memberships/viewMemberships.cy.js @@ -1,13 +1,17 @@ import { buildItemPath } from '../../../src/config/paths'; import { ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS, + buildItemMembershipRowDeleteButtonId, buildItemMembershipRowSelector, buildMemberAvatarClass, buildShareButtonId, } from '../../../src/config/selectors'; import { membershipsWithoutUser } from '../../../src/utils/membership'; import { CURRENT_USER, MEMBERS } from '../../fixtures/members'; -import { ITEMS_WITH_MEMBERSHIPS } from '../../fixtures/memberships'; +import { + ITEMS_WITH_MEMBERSHIPS, + ITEM_WITH_WRITE_ACCESS, +} from '../../fixtures/memberships'; describe('View Memberships', () => { beforeEach(() => { @@ -62,8 +66,48 @@ describe('View Memberships', () => { id, )} .${ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS} input`, ).should('have.value', permission); + + // check delete button exists + cy.get(`#${buildItemMembershipRowDeleteButtonId(id)}`).should('exist'); } // todo: check permission level }); }); + +describe('View Memberships Read-Only Mode', () => { + beforeEach(() => { + cy.setUpApi({ ...ITEM_WITH_WRITE_ACCESS }); + }); + + it('view membership in settings read-only mode', () => { + const [item] = ITEM_WITH_WRITE_ACCESS.items; + const { memberships } = item; + cy.visit(buildItemPath(item.id)); + cy.get(`#${buildShareButtonId(item.id)}`).click(); + + // check contains member avatar + for (const { permission, memberId, id } of memberships) { + const { name, email } = Object.values(MEMBERS).find( + ({ id: mId }) => mId === memberId, + ); + // check name, mail and permission + cy.get(buildItemMembershipRowSelector(id)) + .should('contain', name) + .should('contain', email) + .should('contain', permission); + + // check no permission select component exists + cy.get( + `${buildItemMembershipRowSelector( + id, + )} .${ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS} input`, + ).should('not.exist'); + + // check no delete button exists + cy.get(`#${buildItemMembershipRowDeleteButtonId(id)}`).should( + 'not.exist', + ); + } + }); +}); diff --git a/cypress/fixtures/invitations.js b/cypress/fixtures/invitations.js index 8f9ef9781..3b791d8bd 100644 --- a/cypress/fixtures/invitations.js +++ b/cypress/fixtures/invitations.js @@ -72,3 +72,49 @@ export const ITEMS_WITH_INVITATIONS = { ], members: [MEMBERS.FANNY, MEMBERS.ANNA, MEMBERS.EVAN], }; + +export const ITEM_WITH_INVITATIONS_WRITE_ACCESS = { + items: [ + { + ...DEFAULT_FOLDER_ITEM, + id: 'ecafbd2a-5688-11eb-ae93-0242ac130002', + name: 'own_item_name1', + creator: MEMBERS.BOB.id, + path: 'bcafbd2a_5688_11eb_ae93_0242ac130002.ecafbd2a_5688_11eb_ae93_0242ac130002', + extra: { + image: 'someimageurl', + }, + // for tests only + memberships: [ + { + id: 'ecafbd2a-5688-11eb-be93-0242ac130002', + itemPath: 'bcafbd2a_5688_11eb_ae93_0242ac130002', + permission: PERMISSION_LEVELS.WRITE, + memberId: MEMBERS.ANNA.id, + }, + { + id: 'ecafbd2a-5688-11eb-be93-0242ac130004', + itemPath: 'bcafbd2a_5688_11eb_ae93_0242ac130002', + permission: PERMISSION_LEVELS.ADMIN, + email: MEMBERS.BOB.email, + }, + ], + invitations: [ + { + id: 'ecafbd2a-5688-11eb-be92-0242ac130005', + itemPath: 'bcafbd2a_5688_11eb_ae93_0242ac130002', + permission: PERMISSION_LEVELS.WRITE, + email: MEMBERS.CEDRIC.email, + }, + { + id: 'ecafbd1a-5688-11eb-be93-0242ac130006', + itemPath: + 'bcafbd2a_5688_11eb_ae93_0242ac130002.ecafbd2a_5688_11eb_ae93_0242ac130002', + permission: PERMISSION_LEVELS.READ, + email: MEMBERS.DAVID.email, + }, + ], + }, + ], + members: [MEMBERS.ANNA, MEMBERS.BOB], +}; diff --git a/cypress/fixtures/memberships.js b/cypress/fixtures/memberships.js index e2e668205..f6c6b49a5 100644 --- a/cypress/fixtures/memberships.js +++ b/cypress/fixtures/memberships.js @@ -74,3 +74,38 @@ export const ITEMS_WITH_MEMBERSHIPS = { }, ], }; + +export const ITEM_WITH_WRITE_ACCESS = { + items: [ + { + ...DEFAULT_FOLDER_ITEM, + id: 'ecafbd2a-5688-11eb-ae93-0242ac130002', + creator: MEMBERS.BOB.id, + name: 'own_item_name1', + path: 'ecafbd2a_5688_11eb_ae93_0242ac130002', + extra: { + image: 'someimageurl', + }, + memberships: [ + { + id: 'ecafbd2a-5688-11eb-be93-0242ac130002', + itemPath: 'ecafbd2a_5688_11eb_ae93_0242ac130002', + permission: PERMISSION_LEVELS.ADMIN, + memberId: MEMBERS.BOB.id, + }, + { + id: 'ecafbd2a-5688-11eb-be92-0242ac130002', + itemPath: 'ecafbd2a_5688_11eb_ae93_0242ac130002', + permission: PERMISSION_LEVELS.WRITE, + memberId: MEMBERS.ANNA.id, + }, + { + id: 'ecafbd1a-5688-11eb-be93-0242ac130002', + itemPath: 'ecafbd2a_5688_11eb_ae93_0242ac130002', + permission: PERMISSION_LEVELS.READ, + memberId: MEMBERS.CEDRIC.id, + }, + ], + }, + ], +}; diff --git a/src/components/item/sharing/InvitationsTable.tsx b/src/components/item/sharing/InvitationsTable.tsx index d71b0e387..3214d4add 100644 --- a/src/components/item/sharing/InvitationsTable.tsx +++ b/src/components/item/sharing/InvitationsTable.tsx @@ -31,12 +31,14 @@ type Props = { item: ItemRecord; invitations: List; emptyMessage?: string; + readOnly?: boolean; }; const InvitationsTable = ({ invitations, item, emptyMessage, + readOnly = false, }: Props): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); const { mutate: editInvitation } = useMutation< @@ -92,6 +94,7 @@ const InvitationsTable = ({ ], }); }, + readOnly, }); const InvitationRenderer = ResendInvitationRenderer(item.id); @@ -100,8 +103,8 @@ const InvitationsTable = ({ const columnDefs = useMemo( () => [ { - headerCheckboxSelection: true, - checkboxSelection: true, + headerCheckboxSelection: !readOnly, + checkboxSelection: !readOnly, comparator: GraaspTable.textComparator, headerName: translateBuilder(BUILDER.INVITATIONS_TABLE_EMAIL_HEADER), field: 'email', @@ -114,10 +117,10 @@ const InvitationsTable = ({ BUILDER.INVITATIONS_TABLE_INVITATION_HEADER, ), sortable: false, - cellRenderer: InvitationRenderer, + cellRenderer: readOnly ? null : InvitationRenderer, cellStyle: rowStyle, flex: 1, - field: 'email', + field: readOnly ? null : 'email', }, { headerName: translateBuilder( @@ -127,11 +130,19 @@ const InvitationsTable = ({ comparator: GraaspTable.textComparator, type: 'rightAligned', field: 'permission', + cellStyle: readOnly + ? { + display: 'flex', + justifyContent: 'right', + } + : null, }, { - field: 'actions', - cellRenderer: ActionRenderer, - headerName: translateBuilder(BUILDER.INVITATIONS_TABLE_ACTIONS_HEADER), + field: readOnly ? null : 'actions', + cellRenderer: readOnly ? null : ActionRenderer, + headerName: readOnly + ? null + : translateBuilder(BUILDER.INVITATIONS_TABLE_ACTIONS_HEADER), colId: 'actions', type: 'rightAligned', sortable: false, @@ -143,7 +154,13 @@ const InvitationsTable = ({ }, ], // eslint-disable-next-line react-hooks/exhaustive-deps - [translateBuilder, InvitationRenderer, PermissionRenderer, ActionRenderer], + [ + translateBuilder, + InvitationRenderer, + PermissionRenderer, + ActionRenderer, + readOnly, + ], ); const countTextFunction = (selected) => diff --git a/src/components/item/sharing/ItemMembershipsTable.tsx b/src/components/item/sharing/ItemMembershipsTable.tsx index be2e4dd49..f8dd0597c 100644 --- a/src/components/item/sharing/ItemMembershipsTable.tsx +++ b/src/components/item/sharing/ItemMembershipsTable.tsx @@ -64,6 +64,7 @@ type Props = { memberships: ItemMembership[]; emptyMessage?: string; showEmail?: boolean; + readOnly?: boolean; }; const ItemMembershipsTable = ({ @@ -71,6 +72,7 @@ const ItemMembershipsTable = ({ item, emptyMessage, showEmail = true, + readOnly = false, }: Props): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); const { data: users, isLoading } = hooks.useMembers( @@ -124,6 +126,7 @@ const ItemMembershipsTable = ({ permission: value, }); }, + readOnly, }); const NameCellRenderer = NameRenderer(users); @@ -131,8 +134,8 @@ const ItemMembershipsTable = ({ if (showEmail) { const EmailCellRenderer = EmailRenderer(users); columns.push({ - headerCheckboxSelection: true, - checkboxSelection: true, + headerCheckboxSelection: !readOnly, + checkboxSelection: !readOnly, headerName: translateBuilder( BUILDER.ITEM_MEMBERSHIPS_TABLE_EMAIL_HEADER, ), @@ -166,17 +169,22 @@ const ItemMembershipsTable = ({ type: 'rightAligned', field: 'permission', flex: 1, - cellStyle: { - overflow: 'visible', - textAlign: 'right', - }, + cellStyle: readOnly + ? { + display: 'flex', + justifyContent: 'right', + } + : { + overflow: 'visible', + textAlign: 'right', + }, }, { - field: 'actions', - cellRenderer: ActionRenderer, - headerName: translateBuilder( - BUILDER.ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER, - ), + field: readOnly ? null : 'actions', + cellRenderer: readOnly ? null : ActionRenderer, + headerName: readOnly + ? null + : translateBuilder(BUILDER.ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER), colId: 'actions', type: 'rightAligned', sortable: false, @@ -189,7 +197,7 @@ const ItemMembershipsTable = ({ }, ]); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [item, users, showEmail]); + }, [item, users, showEmail, readOnly]); if (isLoading) { return ; diff --git a/src/components/item/sharing/ItemSharingTab.tsx b/src/components/item/sharing/ItemSharingTab.tsx index 67c29eb95..50f23b558 100644 --- a/src/components/item/sharing/ItemSharingTab.tsx +++ b/src/components/item/sharing/ItemSharingTab.tsx @@ -19,7 +19,10 @@ import { Loader } from '@graasp/ui'; import { useBuilderTranslation } from '../../../config/i18n'; import { hooks } from '../../../config/queryClient'; import { getItemLoginSchema } from '../../../utils/itemExtra'; -import { isItemUpdateAllowedForUser } from '../../../utils/membership'; +import { + isItemUpdateAllowedForUser, + isSettingsEditionAllowedForUser, +} from '../../../utils/membership'; import { useCurrentUserContext } from '../../context/CurrentUserContext'; import CreateItemMembershipForm from './CreateItemMembershipForm'; import CsvInputParser from './CsvInputParser'; @@ -47,6 +50,11 @@ const ItemSharingTab = ({ item, memberships }: Props): JSX.Element => { memberId: currentMember?.id, }); + const canEditSettings = isSettingsEditionAllowedForUser({ + memberships, + memberId: currentMember?.id, + }); + if (isLoadingCurrentMember) { return ; } @@ -73,15 +81,18 @@ const ItemSharingTab = ({ item, memberships }: Props): JSX.Element => { {translateBuilder(BUILDER.SHARING_AUTHORIZED_MEMBERS_TITLE)} - {canEdit && } + {canEditSettings && } - {canEdit && } + {canEditSettings && ( + + )} {/* show authenticated members if login schema is defined @@ -101,6 +112,7 @@ const ItemSharingTab = ({ item, memberships }: Props): JSX.Element => { BUILDER.SHARING_AUTHENTICATED_MEMBERS_EMPTY_MESSAGE, )} showEmail={false} + readOnly={!canEditSettings} /> )} @@ -117,6 +129,7 @@ const ItemSharingTab = ({ item, memberships }: Props): JSX.Element => { emptyMessage={translateBuilder( BUILDER.SHARING_INVITATIONS_EMPTY_MESSAGE, )} + readOnly={!canEditSettings} /> @@ -134,7 +147,7 @@ const ItemSharingTab = ({ item, memberships }: Props): JSX.Element => { {translateBuilder(BUILDER.ITEM_SETTINGS_VISIBILITY_TITLE)} - + {renderMembershipSettings()} ); diff --git a/src/components/item/sharing/ResendInvitationRenderer.tsx b/src/components/item/sharing/ResendInvitationRenderer.tsx index f7a0e236e..073cb190d 100644 --- a/src/components/item/sharing/ResendInvitationRenderer.tsx +++ b/src/components/item/sharing/ResendInvitationRenderer.tsx @@ -6,6 +6,7 @@ import { Button } from '@graasp/ui'; import { useBuilderTranslation } from '../../../config/i18n'; import { useMutation } from '../../../config/queryClient'; +import { ITEM_RESEND_INVITATION_BUTTON_CLASS } from '../../../config/selectors'; type ChildProps = { data: Invitation; @@ -30,7 +31,12 @@ const ResendInvitationRenderer = ( }; return ( - ); diff --git a/src/components/item/sharing/TableRowPermissionRenderer.tsx b/src/components/item/sharing/TableRowPermissionRenderer.tsx index 6fd8aee51..d45eca52d 100644 --- a/src/components/item/sharing/TableRowPermissionRenderer.tsx +++ b/src/components/item/sharing/TableRowPermissionRenderer.tsx @@ -1,3 +1,5 @@ +import Typography from '@mui/material/Typography'; + import { Invitation, ItemMembership } from '@graasp/sdk'; import { ItemRecord } from '@graasp/sdk/frontend'; @@ -8,6 +10,7 @@ type Props = { item: ItemRecord; editFunction; createFunction; + readOnly?; }; type ChildProps = Invitation | ItemMembership; @@ -16,6 +19,7 @@ const TableRowPermissionRenderer = ({ item, editFunction, createFunction, + readOnly = false, }: Props): (({ data }: { data: ChildProps }) => JSX.Element) => { // todo: use typescript to precise data is one of Invitation or Membership const ChildComponent = ({ data: instance }: { data: ChildProps }) => { @@ -34,7 +38,9 @@ const TableRowPermissionRenderer = ({ } }; - return ( + return readOnly ? ( + {instance.permission} + ) : ( `invitationTableRow-${id}`; export const buildInvitationTableRowSelector = (id: string): string => `[row-id="${buildInvitationTableRowId(id)}"]`; +export const ITEM_RESEND_INVITATION_BUTTON_CLASS = 'itemResendInvitationButton'; export const CREATE_MEMBERSHIP_FORM_ID = 'createMembershipFormId'; export const NAVIGATION_ROOT_ID = 'navigationRoot'; export const HEADER_MEMBER_MENU_BUTTON_ID = 'headerMemberMenuButton';