diff --git a/cypress/e2e/invitations/editInvitation.cy.ts b/cypress/e2e/invitations/editInvitation.cy.ts index 4cfd2993a..c52a8e2a7 100644 --- a/cypress/e2e/invitations/editInvitation.cy.ts +++ b/cypress/e2e/invitations/editInvitation.cy.ts @@ -3,7 +3,7 @@ import { PermissionLevel } from '@graasp/sdk'; import { buildItemPath } from '../../../src/config/paths'; import { ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS, - buildInvitationTableRowSelector, + buildInvitationTableRowId, buildPermissionOptionId, buildShareButtonId, } from '../../../src/config/selectors'; @@ -19,13 +19,11 @@ const editInvitation = ({ permission: PermissionLevel; }) => { cy.get(`#${buildShareButtonId(itemId)}`).click(); - const select = cy.get( - `${buildInvitationTableRowSelector( - id, - )} .${ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS}`, - ); + cy.get(`#${buildInvitationTableRowId(id)} [aria-label="Edit"]`).click(); + const select = cy.get(`.${ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS}`); select.click(); select.get(`#${buildPermissionOptionId(permission)}`).click(); + cy.get('button[type="submit"]').click(); }; describe('Edit Invitation', () => { diff --git a/cypress/e2e/invitations/viewInvitation.cy.ts b/cypress/e2e/invitations/viewInvitation.cy.ts index 5ff7d45f9..2aae84434 100644 --- a/cypress/e2e/invitations/viewInvitation.cy.ts +++ b/cypress/e2e/invitations/viewInvitation.cy.ts @@ -1,15 +1,17 @@ -import { buildItemPath } from '../../../src/config/paths'; +import { PackedFolderItemFactory, PermissionLevel } from '@graasp/sdk'; +import { namespaces } from '@graasp/translations'; + +import i18n from '@/config/i18n'; + +import { buildItemPath, buildItemSharePath } from '../../../src/config/paths'; import { - ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS, ITEM_RESEND_INVITATION_BUTTON_CLASS, - buildInvitationTableRowSelector, + buildInvitationTableRowId, buildItemInvitationRowDeleteButtonId, buildShareButtonId, } from '../../../src/config/selectors'; -import { - ITEMS_WITH_INVITATIONS, - ITEM_WITH_INVITATIONS_WRITE_ACCESS, -} from '../../fixtures/invitations'; +import { ITEMS_WITH_INVITATIONS } from '../../fixtures/invitations'; +import { CURRENT_USER, MEMBERS } from '../../fixtures/members'; describe('View Invitations', () => { beforeEach(() => { @@ -17,70 +19,66 @@ describe('View Invitations', () => { }); it('view invitation in share item modal', () => { + i18n.changeLanguage(CURRENT_USER.extra.lang); const item = ITEMS_WITH_INVITATIONS.items[1]; const { invitations } = item; - cy.visit(buildItemPath(item.id)); - cy.get(`#${buildShareButtonId(item.id)}`).click(); + cy.visit(buildItemSharePath(item.id)); invitations.forEach( ({ item: { path: itemPath }, id, email, permission }) => { - cy.get(buildInvitationTableRowSelector(id)).should('contain', email); - if (itemPath !== item.path) { cy.get(`#${buildItemInvitationRowDeleteButtonId(id)}`).should( 'be.disabled', ); } - cy.get( - `${buildInvitationTableRowSelector( - id, - )} .${ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS} input`, - ).should('have.value', permission); + cy.get(`#${buildInvitationTableRowId(id)}`) + .should('contain', email) + .should('contain', i18n.t(permission, { ns: namespaces.enums })); cy.get( - `${buildInvitationTableRowSelector( - id, - )} .${ITEM_RESEND_INVITATION_BUTTON_CLASS}`, + `#${buildInvitationTableRowId(id)} .${ITEM_RESEND_INVITATION_BUTTON_CLASS}`, ).should('exist'); }, ); }); }); -describe('View Invitations Read-Only Mode', () => { - beforeEach(() => { - cy.setUpApi({ ...ITEM_WITH_INVITATIONS_WRITE_ACCESS }); - }); +describe('Cannot view Invitations for writers and readers', () => { + it('view invitation in share item modal write-only mode', () => { + const item = PackedFolderItemFactory( + {}, + { permission: PermissionLevel.Write }, + ); + const invitations = [ + { + id: 'ecafbd2a-5688-11eb-be92-0242ac130005', + item, + permission: PermissionLevel.Write, + email: MEMBERS.CEDRIC.email, + createdAt: '2021-08-11T12:56:36.834Z', + updatedAt: '2021-08-11T12:56:36.834Z', + creator: MEMBERS.ANNA, + }, + { + id: 'ecafbd1a-5688-11eb-be93-0242ac130006', + item, + permission: PermissionLevel.Read, + email: MEMBERS.DAVID.email, + createdAt: '2021-08-11T12:56:36.834Z', + updatedAt: '2021-08-11T12:56:36.834Z', + creator: MEMBERS.ANNA, + }, + ]; + cy.setUpApi({ items: [{ ...item, invitations }] }); - 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'); + // should not contain given invitations + cy.get('tr').then((c) => { + invitations.forEach((inv) => { + expect(c[0]).not.to.contain(inv.email); + }); }); }); }); diff --git a/cypress/e2e/item/authorization/forbidden.cy.ts b/cypress/e2e/item/authorization/forbidden.cy.ts new file mode 100644 index 000000000..a747c3637 --- /dev/null +++ b/cypress/e2e/item/authorization/forbidden.cy.ts @@ -0,0 +1,16 @@ +import { PackedFolderItemFactory } from '@graasp/sdk'; + +import { buildItemPath } from '@/config/paths'; +import { ITEM_LOGIN_SCREEN_FORBIDDEN_ID } from '@/config/selectors'; + +it('User is logged out and item is private', () => { + const item = PackedFolderItemFactory({}, { permission: null }); + cy.setUpApi({ + items: [item], + currentMember: null, + }); + + cy.visit(buildItemPath(item.id)); + + cy.get(`#${ITEM_LOGIN_SCREEN_FORBIDDEN_ID}`).should('be.visible'); +}); diff --git a/cypress/e2e/item/authorization/itemLogin/itemLogin.cy.ts b/cypress/e2e/item/authorization/itemLogin/itemLogin.cy.ts new file mode 100644 index 000000000..84bd55a5b --- /dev/null +++ b/cypress/e2e/item/authorization/itemLogin/itemLogin.cy.ts @@ -0,0 +1,122 @@ +import { ItemLoginSchemaType, PackedFolderItemFactory } from '@graasp/sdk'; + +import { SETTINGS_ITEM_LOGIN_DEFAULT } from '../../../../../src/config/constants'; +import { buildItemPath } from '../../../../../src/config/paths'; +import { + ENROLL_BUTTON_SELECTOR, + ITEM_LOGIN_SIGN_IN_BUTTON_ID, + ITEM_LOGIN_SIGN_IN_PASSWORD_ID, + ITEM_LOGIN_SIGN_IN_USERNAME_ID, + buildDataCyWrapper, +} from '../../../../../src/config/selectors'; +import { MEMBERS, SIGNED_OUT_MEMBER } from '../../../../fixtures/members'; +import { ITEM_LOGIN_PAUSE } from '../../../../support/constants'; +import { addItemLoginSchema } from './utils'; + +const checkItemLoginScreenLayout = ( + itemLoginSchema: + | ItemLoginSchemaType + | `${ItemLoginSchemaType}` = SETTINGS_ITEM_LOGIN_DEFAULT, +) => { + cy.get(`#${ITEM_LOGIN_SIGN_IN_USERNAME_ID}`).should('exist'); + if (itemLoginSchema === ItemLoginSchemaType.UsernameAndPassword) { + cy.get(`#${ITEM_LOGIN_SIGN_IN_PASSWORD_ID}`).should('exist'); + } + cy.get(`#${ITEM_LOGIN_SIGN_IN_BUTTON_ID}`).should('exist'); +}; + +const fillItemLoginScreenLayout = ({ + username, + password, +}: { + username?: string; + password?: string; +}) => { + cy.get(`#${ITEM_LOGIN_SIGN_IN_USERNAME_ID}`).clear().type(username); + + if (password) { + cy.get(`#${ITEM_LOGIN_SIGN_IN_PASSWORD_ID}`).clear().type(password); + } + cy.get(`#${ITEM_LOGIN_SIGN_IN_BUTTON_ID}`).click(); +}; + +describe('User is signed out', () => { + describe('Display Item Login Screen', () => { + it('username', () => { + const item = addItemLoginSchema( + PackedFolderItemFactory({}, { permission: null }), + ItemLoginSchemaType.Username, + ); + cy.setUpApi({ items: [item], currentMember: SIGNED_OUT_MEMBER }); + + cy.visit(buildItemPath(item.id)); + checkItemLoginScreenLayout(item.itemLoginSchema.type); + fillItemLoginScreenLayout({ + username: 'username', + }); + cy.wait('@postItemLogin'); + }); + it('username and password', () => { + const item = addItemLoginSchema( + PackedFolderItemFactory({}, { permission: null }), + ItemLoginSchemaType.UsernameAndPassword, + ); + cy.setUpApi({ items: [item], currentMember: SIGNED_OUT_MEMBER }); + + cy.visit(buildItemPath(item.id)); + checkItemLoginScreenLayout(item.itemLoginSchema.type); + fillItemLoginScreenLayout({ + username: 'username', + password: 'password', + }); + cy.wait('@postItemLogin'); + }); + }); + + describe('Error handling', () => { + it('error while signing in', () => { + const item = addItemLoginSchema( + PackedFolderItemFactory({}, { permission: null }), + ItemLoginSchemaType.UsernameAndPassword, + ); + cy.setUpApi({ + items: [item], + postItemLoginError: true, + currentMember: SIGNED_OUT_MEMBER, + }); + + // go to children item + cy.visit(buildItemPath(item.id)); + + fillItemLoginScreenLayout({ + username: 'username', + password: 'password', + }); + + cy.wait(ITEM_LOGIN_PAUSE); + + cy.get(`#${ITEM_LOGIN_SIGN_IN_USERNAME_ID}`).should('exist'); + }); + }); +}); + +describe('User is signed in as normal user', () => { + it('Enroll to item automatically', () => { + const item = addItemLoginSchema( + PackedFolderItemFactory({}, { permission: null }), + ItemLoginSchemaType.UsernameAndPassword, + ); + cy.setUpApi({ items: [item], currentMember: MEMBERS.BOB }); + cy.visit(buildItemPath(item.id)); + + // avoid to detect intermediate screens because of loading + // to remove when requests loading time is properly managed + cy.wait(ITEM_LOGIN_PAUSE); + + // enroll + cy.get(buildDataCyWrapper(ENROLL_BUTTON_SELECTOR)).click(); + cy.wait('@enroll').then(({ request }) => { + expect(request.url).to.contain(item.id); + }); + }); +}); diff --git a/cypress/e2e/item/authorization/itemLogin/itemLoginSetting.cy.ts b/cypress/e2e/item/authorization/itemLogin/itemLoginSetting.cy.ts new file mode 100644 index 000000000..0357cf7c8 --- /dev/null +++ b/cypress/e2e/item/authorization/itemLogin/itemLoginSetting.cy.ts @@ -0,0 +1,100 @@ +import { + ItemLoginSchemaType, + PackedFolderItemFactory, + PermissionLevel, +} from '@graasp/sdk'; + +import { buildItemPath } from '../../../../../src/config/paths'; +import { + REQUEST_MEMBERSHIP_BUTTON_ID, + SHARE_ITEM_PSEUDONYMIZED_SCHEMA_ID, + buildShareButtonId, +} from '../../../../../src/config/selectors'; +import { MEMBERS } from '../../../../fixtures/members'; +import { ITEM_LOGIN_PAUSE } from '../../../../support/constants'; +import { addItemLoginSchema } from './utils'; + +const checkItemLoginSetting = ({ + mode, + disabled = false, +}: { + mode: string; + disabled?: boolean; +}) => { + if (!disabled) { + cy.get(`#${SHARE_ITEM_PSEUDONYMIZED_SCHEMA_ID} + input`).should( + 'have.value', + mode, + ); + } else { + cy.get(`#${SHARE_ITEM_PSEUDONYMIZED_SCHEMA_ID}`).then((el) => { + // test classnames are 'disabled' + expect(el.parent().html()).to.contain('disabled'); + }); + } +}; + +const editItemLoginSetting = (mode: string) => { + cy.get(`#${SHARE_ITEM_PSEUDONYMIZED_SCHEMA_ID}`).click(); + cy.get(`li[data-value="${mode}"]`).click(); + cy.wait('@putItemLoginSchema').then(({ request: { body } }) => { + expect(body?.type).to.equal(mode); + }); +}; + +describe('Item Login', () => { + it('Item Login not allowed', () => { + const item = PackedFolderItemFactory({}, { permission: null }); + cy.setUpApi({ + items: [item], + currentMember: MEMBERS.BOB, + }); + cy.visit(buildItemPath(item.id)); + cy.wait(ITEM_LOGIN_PAUSE); + cy.get(`#${REQUEST_MEMBERSHIP_BUTTON_ID}`).should('exist'); + }); + + describe('Display Item Login Setting', () => { + it('edit item login setting', () => { + const item = addItemLoginSchema( + PackedFolderItemFactory(), + ItemLoginSchemaType.Username, + ); + const child = { + ...PackedFolderItemFactory({ parentItem: item }), + // inherited schema + itemLoginSchema: item.itemLoginSchema, + }; + cy.setUpApi({ items: [item, child] }); + // check item with item login enabled + cy.visit(buildItemPath(item.id)); + cy.get(`#${buildShareButtonId(item.id)}`).click(); + + checkItemLoginSetting({ + mode: ItemLoginSchemaType.Username, + }); + editItemLoginSetting(ItemLoginSchemaType.UsernameAndPassword); + + // disabled at child level + cy.visit(buildItemPath(child.id)); + cy.get(`#${buildShareButtonId(child.id)}`).click(); + checkItemLoginSetting({ + mode: ItemLoginSchemaType.UsernameAndPassword, + disabled: true, + }); + }); + + it('read permission', () => { + const item = addItemLoginSchema( + PackedFolderItemFactory({}, { permission: PermissionLevel.Read }), + ItemLoginSchemaType.UsernameAndPassword, + ); + cy.setUpApi({ + items: [item], + currentMember: MEMBERS.BOB, + }); + cy.visit(buildItemPath(item.id)); + cy.wait(ITEM_LOGIN_PAUSE); + }); + }); +}); diff --git a/cypress/e2e/item/authorization/itemLogin/utils.ts b/cypress/e2e/item/authorization/itemLogin/utils.ts new file mode 100644 index 000000000..fd6862d0b --- /dev/null +++ b/cypress/e2e/item/authorization/itemLogin/utils.ts @@ -0,0 +1,17 @@ +import { ItemLoginSchema, ItemLoginSchemaType, PackedItem } from '@graasp/sdk'; + +import { v4 } from 'uuid'; + +export const addItemLoginSchema = ( + item: PackedItem, + itemLoginSchemaType: ItemLoginSchemaType, +): PackedItem & { itemLoginSchema: ItemLoginSchema } => ({ + ...item, + itemLoginSchema: { + item, + type: itemLoginSchemaType, + id: v4(), + createdAt: '2021-08-11T12:56:36.834Z', + updatedAt: '2021-08-11T12:56:36.834Z', + }, +}); diff --git a/cypress/e2e/item/authorization/membershipRequest/membershipRequest.cy.ts b/cypress/e2e/item/authorization/membershipRequest/membershipRequest.cy.ts new file mode 100644 index 000000000..ffabafe0b --- /dev/null +++ b/cypress/e2e/item/authorization/membershipRequest/membershipRequest.cy.ts @@ -0,0 +1,47 @@ +import { FolderItemFactory, MembershipRequestStatus } from '@graasp/sdk'; + +import { buildItemPath } from '@/config/paths'; +import { + MEMBERSHIP_REQUEST_PENDING_SCREEN_SELECTOR, + REQUEST_MEMBERSHIP_BUTTON_ID, + buildDataCyWrapper, +} from '@/config/selectors'; + +import { CURRENT_USER } from '../../../../fixtures/members'; + +it('Request membership when signed in', () => { + const item = FolderItemFactory(); + cy.setUpApi({ + items: [item], + }); + + cy.visit(buildItemPath(item.id)); + + // click on request button + cy.get(`#${REQUEST_MEMBERSHIP_BUTTON_ID}`).click(); + + // check endpoint + cy.wait('@requestMembership').then(({ request }) => { + expect(request.url).to.contain(item.id); + }); + + // button is disabled + cy.get(`#${REQUEST_MEMBERSHIP_BUTTON_ID}`).should('be.disabled'); +}); + +it('Membership request is already sent', () => { + const item = FolderItemFactory(); + cy.setUpApi({ + items: [item], + membershipRequests: [ + { item, member: CURRENT_USER, status: MembershipRequestStatus.Pending }, + ], + }); + + cy.visit(buildItemPath(item.id)); + + // request pending screen + cy.get(buildDataCyWrapper(MEMBERSHIP_REQUEST_PENDING_SCREEN_SELECTOR)).should( + 'be.visible', + ); +}); diff --git a/cypress/e2e/item/share/itemLogin.cy.ts b/cypress/e2e/item/share/itemLogin.cy.ts deleted file mode 100644 index 47cc74b5d..000000000 --- a/cypress/e2e/item/share/itemLogin.cy.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { - ItemLoginSchemaType, - PackedFolderItemFactory, - PackedItem, - PermissionLevel, -} from '@graasp/sdk'; - -import { v4 } from 'uuid'; - -import { SETTINGS_ITEM_LOGIN_DEFAULT } from '../../../../src/config/constants'; -import { buildItemPath } from '../../../../src/config/paths'; -import { - ITEM_LOGIN_SCREEN_FORBIDDEN_ID, - ITEM_LOGIN_SIGN_IN_BUTTON_ID, - ITEM_LOGIN_SIGN_IN_PASSWORD_ID, - ITEM_LOGIN_SIGN_IN_USERNAME_ID, - SHARE_ITEM_PSEUDONYMIZED_SCHEMA_ID, - buildShareButtonId, -} from '../../../../src/config/selectors'; -import { MEMBERS, SIGNED_OUT_MEMBER } from '../../../fixtures/members'; -import { ITEM_LOGIN_PAUSE } from '../../../support/constants'; - -const addItemLoginSchema = ( - item: PackedItem, - itemLoginSchemaType: ItemLoginSchemaType, -) => ({ - ...item, - itemLoginSchema: { - item, - type: itemLoginSchemaType, - id: v4(), - createdAt: '2021-08-11T12:56:36.834Z', - updatedAt: '2021-08-11T12:56:36.834Z', - }, -}); - -const checkItemLoginScreenLayout = ( - itemLoginSchema: - | ItemLoginSchemaType - | `${ItemLoginSchemaType}` = SETTINGS_ITEM_LOGIN_DEFAULT, -) => { - cy.get(`#${ITEM_LOGIN_SIGN_IN_USERNAME_ID}`).should('exist'); - if (itemLoginSchema === ItemLoginSchemaType.UsernameAndPassword) { - cy.get(`#${ITEM_LOGIN_SIGN_IN_PASSWORD_ID}`).should('exist'); - } - cy.get(`#${ITEM_LOGIN_SIGN_IN_BUTTON_ID}`).should('exist'); -}; - -const fillItemLoginScreenLayout = ({ - username, - password, -}: { - username?: string; - password?: string; -}) => { - cy.get(`#${ITEM_LOGIN_SIGN_IN_USERNAME_ID}`).clear().type(username); - - if (password) { - cy.get(`#${ITEM_LOGIN_SIGN_IN_PASSWORD_ID}`).clear().type(password); - } - cy.get(`#${ITEM_LOGIN_SIGN_IN_BUTTON_ID}`).click(); -}; - -const checkItemLoginSetting = ({ - isEnabled, - mode, - disabled = false, -}: { - isEnabled: boolean; - mode: string; - disabled?: boolean; -}) => { - if (isEnabled && !disabled) { - cy.get(`#${SHARE_ITEM_PSEUDONYMIZED_SCHEMA_ID} + input`).should( - 'have.value', - mode, - ); - } - if (disabled) { - cy.get(`#${SHARE_ITEM_PSEUDONYMIZED_SCHEMA_ID}`).then((el) => { - // test classnames are 'disabled' - expect(el.parent().html()).to.contain('disabled'); - }); - } -}; - -const editItemLoginSetting = (mode: string) => { - cy.get(`#${SHARE_ITEM_PSEUDONYMIZED_SCHEMA_ID}`).click(); - cy.get(`li[data-value="${mode}"]`).click(); - cy.wait('@putItemLoginSchema').then(({ request: { body } }) => { - expect(body?.type).to.equal(ItemLoginSchemaType.UsernameAndPassword); - }); -}; - -describe('Item Login', () => { - it('Item Login not allowed', () => { - const item = PackedFolderItemFactory({}, { permission: null }); - cy.setUpApi({ - items: [item], - currentMember: MEMBERS.BOB, - }); - cy.visit(buildItemPath(item.id)); - cy.wait(ITEM_LOGIN_PAUSE); - cy.get(`#${ITEM_LOGIN_SCREEN_FORBIDDEN_ID}`).should('exist'); - }); - - describe('User is signed out', () => { - describe('Display Item Login Screen', () => { - it('username', () => { - const item = addItemLoginSchema( - PackedFolderItemFactory({}, { permission: null }), - ItemLoginSchemaType.Username, - ); - cy.setUpApi({ items: [item], currentMember: SIGNED_OUT_MEMBER }); - - cy.visit(buildItemPath(item.id)); - checkItemLoginScreenLayout(item.itemLoginSchema.type); - fillItemLoginScreenLayout({ - username: 'username', - }); - cy.wait('@postItemLogin'); - - // use username to check no member id is incorrectly sent - fillItemLoginScreenLayout({ - username: 'username', - }); - cy.wait('@postItemLogin'); - }); - it('username and password', () => { - const item = addItemLoginSchema( - PackedFolderItemFactory({}, { permission: null }), - ItemLoginSchemaType.UsernameAndPassword, - ); - cy.setUpApi({ items: [item], currentMember: SIGNED_OUT_MEMBER }); - - cy.visit(buildItemPath(item.id)); - checkItemLoginScreenLayout(item.itemLoginSchema.type); - fillItemLoginScreenLayout({ - username: 'username', - password: 'password', - }); - cy.wait('@postItemLogin'); - - // use username to check no member id is incorrectly sent - fillItemLoginScreenLayout({ - username: 'username', - password: 'password', - }); - cy.wait('@postItemLogin'); - }); - }); - }); - - describe('User is signed in as normal user', () => { - it('Should not be able to access the item', () => { - const item = addItemLoginSchema( - PackedFolderItemFactory({}, { permission: null }), - ItemLoginSchemaType.UsernameAndPassword, - ); - cy.setUpApi({ items: [item], currentMember: MEMBERS.BOB }); - cy.visit(buildItemPath(item.id)); - - // avoid to detect intermediate screens because of loading - // to remove when requests loading time is properly managed - cy.wait(ITEM_LOGIN_PAUSE); - - cy.get(`#${ITEM_LOGIN_SCREEN_FORBIDDEN_ID}`).should('exist'); - }); - }); - - describe('Display Item Login Setting', () => { - it('edit item login setting', () => { - const item = addItemLoginSchema( - PackedFolderItemFactory(), - ItemLoginSchemaType.Username, - ); - const child = { - ...PackedFolderItemFactory({ parentItem: item }), - // inherited schema - itemLoginSchema: item.itemLoginSchema, - }; - cy.setUpApi({ items: [item, child] }); - // check item with item login enabled - cy.visit(buildItemPath(item.id)); - cy.get(`#${buildShareButtonId(item.id)}`).click(); - - checkItemLoginSetting({ - isEnabled: true, - mode: ItemLoginSchemaType.Username, - }); - editItemLoginSetting(ItemLoginSchemaType.UsernameAndPassword); - - // disabled at child level - cy.visit(buildItemPath(child.id)); - cy.get(`#${buildShareButtonId(child.id)}`).click(); - checkItemLoginSetting({ - isEnabled: true, - mode: ItemLoginSchemaType.UsernameAndPassword, - disabled: true, - }); - }); - - it('read permission', () => { - const item = addItemLoginSchema( - PackedFolderItemFactory({}, { permission: PermissionLevel.Read }), - ItemLoginSchemaType.UsernameAndPassword, - ); - cy.setUpApi({ - items: [item], - currentMember: MEMBERS.BOB, - }); - cy.visit(buildItemPath(item.id)); - cy.wait(ITEM_LOGIN_PAUSE); - }); - }); - - describe('Error handling', () => { - it('error while signing in', () => { - const item = addItemLoginSchema( - PackedFolderItemFactory({}, { permission: null }), - ItemLoginSchemaType.UsernameAndPassword, - ); - cy.setUpApi({ - items: [item], - postItemLoginError: true, - currentMember: SIGNED_OUT_MEMBER, - }); - - // go to children item - cy.visit(buildItemPath(item.id)); - - fillItemLoginScreenLayout({ - username: 'username', - password: 'password', - }); - - cy.wait(ITEM_LOGIN_PAUSE); - - cy.get(`#${ITEM_LOGIN_SIGN_IN_USERNAME_ID}`).should('exist'); - }); - }); -}); diff --git a/cypress/e2e/item/share/publicItems.cy.ts b/cypress/e2e/item/share/publicItems.cy.ts index cb6e41e04..9637dbf95 100644 --- a/cypress/e2e/item/share/publicItems.cy.ts +++ b/cypress/e2e/item/share/publicItems.cy.ts @@ -3,7 +3,10 @@ import { PackedFolderItemFactory } from '@graasp/sdk'; import { StatusCodes } from 'http-status-codes'; import { buildItemPath } from '../../../../src/config/paths'; -import { ITEM_LOGIN_SCREEN_FORBIDDEN_ID } from '../../../../src/config/selectors'; +import { + ITEM_LOGIN_SCREEN_FORBIDDEN_ID, + REQUEST_MEMBERSHIP_BUTTON_ID, +} from '../../../../src/config/selectors'; import { SAMPLE_PUBLIC_ITEMS } from '../../../fixtures/items'; import { MEMBERS, SIGNED_OUT_MEMBER } from '../../../fixtures/members'; import { expectFolderViewScreenLayout } from '../../../support/viewUtils'; @@ -68,7 +71,7 @@ describe('Public Items', () => { cy.get(`#${ITEM_LOGIN_SCREEN_FORBIDDEN_ID}`).should('exist'); }); - it('User without a membership cannot access a private item', () => { + it('User without a membership can request access a private item', () => { const currentMember = MEMBERS.BOB; cy.setUpApi({ items: [item], @@ -78,10 +81,13 @@ describe('Public Items', () => { cy.wait('@getItem').then(({ response: { statusCode } }) => { expect(statusCode).to.equal(StatusCodes.UNAUTHORIZED); }); - cy.get(`#${ITEM_LOGIN_SCREEN_FORBIDDEN_ID}`).should('exist'); + cy.get(`#${REQUEST_MEMBERSHIP_BUTTON_ID}`).click(); + cy.wait('@requestMembership').then(({ request }) => { + expect(request.url).to.contain(item.id); + }); }); - it('User without a membership cannot access a child of a private item', () => { + it('User without a membership can request access to a child of a private item', () => { const currentMember = MEMBERS.BOB; cy.setUpApi({ items: [item], @@ -91,7 +97,10 @@ describe('Public Items', () => { cy.wait('@getItem').then(({ response: { statusCode } }) => { expect(statusCode).to.equal(StatusCodes.UNAUTHORIZED); }); - cy.get(`#${ITEM_LOGIN_SCREEN_FORBIDDEN_ID}`).should('exist'); + cy.get(`#${REQUEST_MEMBERSHIP_BUTTON_ID}`).click(); + cy.wait('@requestMembership').then(({ request }) => { + expect(request.url).to.contain(item.id); + }); }); }); }); diff --git a/cypress/e2e/item/share/shareItemFromCsv.cy.ts b/cypress/e2e/item/share/shareItemFromCsv.cy.ts index 7bb000577..c3840b35c 100644 --- a/cypress/e2e/item/share/shareItemFromCsv.cy.ts +++ b/cypress/e2e/item/share/shareItemFromCsv.cy.ts @@ -3,6 +3,7 @@ import { PackedFolderItemFactory } from '@graasp/sdk'; import { buildItemSharePath } from '../../../../src/config/paths'; import { CSV_FILE_SELECTION_DELETE_BUTTON_ID, + SHARE_BUTTON_MORE_ID, SHARE_CSV_TEMPLATE_SELECTION_BUTTON_ID, SHARE_CSV_TEMPLATE_SELECTION_DELETE_BUTTON_ID, SHARE_CSV_TEMPLATE_SUMMARY_CONTAINER_ID, @@ -18,6 +19,7 @@ import { import { MEMBERS } from '../../../fixtures/members'; const shareItem = ({ fixture }: { id: string; fixture: string }) => { + cy.get(`#${SHARE_BUTTON_MORE_ID}`).click(); cy.get(`#${SHARE_ITEM_CSV_PARSER_BUTTON_ID}`).click(); cy.attachFile( cy.get(`#${SHARE_ITEM_CSV_PARSER_INPUT_BUTTON_SELECTOR}`), diff --git a/cypress/e2e/memberships/deleteItemMembership.cy.ts b/cypress/e2e/memberships/deleteItemMembership.cy.ts index 7b5dc87b7..adb80fe69 100644 --- a/cypress/e2e/memberships/deleteItemMembership.cy.ts +++ b/cypress/e2e/memberships/deleteItemMembership.cy.ts @@ -4,10 +4,13 @@ import { PermissionLevel, } from '@graasp/sdk'; +import { v4 } from 'uuid'; + import { buildItemPath } from '../../../src/config/paths'; import { CONFIRM_MEMBERSHIP_DELETE_BUTTON_ID, buildItemMembershipRowDeleteButtonId, + buildItemMembershipRowEditButtonId, buildShareButtonId, } from '../../../src/config/selectors'; import { CURRENT_USER, MEMBERS } from '../../fixtures/members'; @@ -54,9 +57,8 @@ describe('Delete Membership', () => { cy.get(`#${buildShareButtonId(id)}`).click(); const { id: mId } = memberships[1]; - cy.get(`#${buildItemMembershipRowDeleteButtonId(mId)}`).should( - 'be.disabled', - ); + cy.get(`#${buildItemMembershipRowEditButtonId(mId)}`).should('exist'); + cy.get(`#${buildItemMembershipRowDeleteButtonId(mId)}`).should('not.exist'); }); it('cannot delete if there is only one admin item membership', () => { @@ -66,11 +68,13 @@ describe('Delete Membership', () => { ...item, memberships: [ { + id: v4(), permission: PermissionLevel.Admin, account: CURRENT_USER, item, } as unknown as ItemMembership, { + id: v4(), permission: PermissionLevel.Read, account: MEMBERS.BOB, item, @@ -85,9 +89,10 @@ describe('Delete Membership', () => { cy.visit(buildItemPath(id)); cy.get(`#${buildShareButtonId(id)}`).click(); - const { id: mId } = memberships[1]; - cy.get(`#${buildItemMembershipRowDeleteButtonId(mId)}`).should( - 'be.disabled', + const [m1, m2] = memberships; + cy.get(`#${buildItemMembershipRowDeleteButtonId(m1.id)}`).should( + 'not.exist', ); + cy.get(`#${buildItemMembershipRowDeleteButtonId(m2.id)}`).should('exist'); }); }); diff --git a/cypress/e2e/memberships/editItemMembership.cy.ts b/cypress/e2e/memberships/editItemMembership.cy.ts index c6855a0fd..c98b1654d 100644 --- a/cypress/e2e/memberships/editItemMembership.cy.ts +++ b/cypress/e2e/memberships/editItemMembership.cy.ts @@ -1,9 +1,13 @@ -import { PackedFolderItemFactory, PermissionLevel } from '@graasp/sdk'; +import { + ItemMembership, + PackedFolderItemFactory, + PermissionLevel, +} from '@graasp/sdk'; import { buildItemPath, buildItemSharePath } from '../../../src/config/paths'; import { ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS, - buildItemMembershipRowSelector, + buildItemMembershipRowEditButtonId, buildPermissionOptionId, buildShareButtonId, } from '../../../src/config/selectors'; @@ -11,14 +15,17 @@ import { CURRENT_USER, MEMBERS } from '../../fixtures/members'; import { ITEMS_WITH_MEMBERSHIPS } from '../../fixtures/memberships'; import { ItemForTest } from '../../support/types'; -const openPermissionSelect = (id: string) => { - const select = cy.get( - `${buildItemMembershipRowSelector( - id, - )} .${ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS}`, - ); +const openPermissionSelect = ({ + id, + permission, +}: { + id: ItemMembership['id']; + permission: PermissionLevel; +}) => { + cy.get(`#${buildItemMembershipRowEditButtonId(id)}`).click(); + const select = cy.get(`.${ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS}`); select.click(); - return select; + select.get(`#${buildPermissionOptionId(permission)}`).click(); }; const editItemMembership = ({ @@ -31,8 +38,8 @@ const editItemMembership = ({ permission: PermissionLevel; }) => { cy.get(`#${buildShareButtonId(itemId)}`).click(); - const select = openPermissionSelect(id); - select.get(`#${buildPermissionOptionId(permission)}`).click(); + openPermissionSelect({ id, permission }); + cy.get('button[type="submit"]').click(); }; describe('Edit Membership', () => { @@ -101,7 +108,7 @@ describe('Edit Membership', () => { cy.visit(buildItemSharePath(child.id)); const m = memberships[1]; - openPermissionSelect(m.id); + openPermissionSelect(m); // should not show read cy.get(`#${buildPermissionOptionId(PermissionLevel.Read)}`).should( diff --git a/cypress/e2e/memberships/membershipRequestTable.cy.ts b/cypress/e2e/memberships/membershipRequestTable.cy.ts new file mode 100644 index 000000000..7a082b804 --- /dev/null +++ b/cypress/e2e/memberships/membershipRequestTable.cy.ts @@ -0,0 +1,102 @@ +import { + MemberFactory, + PackedFolderItemFactory, + PermissionLevel, +} from '@graasp/sdk'; + +import i18n, { BUILDER_NAMESPACE } from '@/config/i18n'; +import { buildItemSharePath } from '@/config/paths'; +import { + MEMBERSHIPS_TAB_SELECTOR, + MEMBERSHIP_REQUESTS_EMPTY_SELECTOR, + MEMBERSHIP_REQUESTS_TAB_SELECTOR, + MEMBERSHIP_REQUEST_ACCEPT_BUTTON_SELECTOR, + MEMBERSHIP_REQUEST_REJECT_BUTTON_SELECTOR, + buildDataCyWrapper, + buildMembershipRequestRowSelector, +} from '@/config/selectors'; +import { BUILDER } from '@/langs/constants'; + +import { CURRENT_USER } from '../../fixtures/members'; + +const itemWithRequests = PackedFolderItemFactory(); +const membershipRequests = [ + { item: itemWithRequests, member: MemberFactory() }, + { item: itemWithRequests, member: MemberFactory() }, + { item: itemWithRequests, member: MemberFactory() }, +]; + +describe('Membership requests table', () => { + it('Writers cannot see', () => { + const itemWithWrite = PackedFolderItemFactory( + { creator: CURRENT_USER }, + { permission: PermissionLevel.Write }, + ); + cy.setUpApi({ items: [itemWithWrite], membershipRequests }); + cy.visit(buildItemSharePath(itemWithWrite.id)); + cy.get(buildDataCyWrapper(MEMBERSHIPS_TAB_SELECTOR)).should('exist'); + cy.get(buildDataCyWrapper(MEMBERSHIP_REQUESTS_TAB_SELECTOR)).should( + 'not.exist', + ); + }); + + it('empty membership requests', () => { + cy.setUpApi({ items: [itemWithRequests] }); + cy.visit(buildItemSharePath(itemWithRequests.id)); + cy.get(buildDataCyWrapper(MEMBERSHIP_REQUESTS_TAB_SELECTOR)).click(); + cy.get(buildDataCyWrapper(MEMBERSHIP_REQUESTS_EMPTY_SELECTOR)).should( + 'be.visible', + ); + }); + + describe('Filled Membership Requests', () => { + beforeEach(() => { + cy.setUpApi({ items: [itemWithRequests], membershipRequests }); + cy.visit(buildItemSharePath(itemWithRequests.id)); + cy.get(buildDataCyWrapper(MEMBERSHIP_REQUESTS_TAB_SELECTOR)).click(); + i18n.changeLanguage(CURRENT_USER.extra.lang); + }); + it('view membership requests', () => { + for (const mr of membershipRequests) { + cy.get( + buildDataCyWrapper(buildMembershipRequestRowSelector(mr.member.id)), + ) + .should('contain', mr.member.name) + .should('contain', mr.member.email) + .should( + 'contain', + i18n.t(BUILDER.MEMBERSHIP_REQUEST_ACCEPT_BUTTON, { + ns: BUILDER_NAMESPACE, + }), + ) + .should( + 'contain', + i18n.t(BUILDER.MEMBERSHIP_REQUEST_REJECT_BUTTON, { + ns: BUILDER_NAMESPACE, + }), + ); + } + }); + it('accept membership requests', () => { + const { member } = membershipRequests[0]; + + cy.get( + `${buildDataCyWrapper(buildMembershipRequestRowSelector(member.id))} ${buildDataCyWrapper(MEMBERSHIP_REQUEST_ACCEPT_BUTTON_SELECTOR)}`, + ).click(); + + cy.wait('@postItemMembership').then(({ request: { body } }) => { + expect(body.accountId).to.equal(member.id); + expect(body.permission).to.equal(PermissionLevel.Read); + }); + }); + it('reject membership requests', () => { + const { member } = membershipRequests[0]; + + cy.get( + `${buildDataCyWrapper(buildMembershipRequestRowSelector(member.id))} ${buildDataCyWrapper(MEMBERSHIP_REQUEST_REJECT_BUTTON_SELECTOR)}`, + ).click(); + + cy.wait('@rejectMembershipRequest'); + }); + }); +}); diff --git a/cypress/e2e/memberships/viewMemberships.cy.ts b/cypress/e2e/memberships/viewMemberships.cy.ts index d87cb0685..41164f5a4 100644 --- a/cypress/e2e/memberships/viewMemberships.cy.ts +++ b/cypress/e2e/memberships/viewMemberships.cy.ts @@ -1,92 +1,144 @@ +import { Member, PackedFolderItemFactory, PermissionLevel } from '@graasp/sdk'; +import { namespaces } from '@graasp/translations'; + +import i18n from '@/config/i18n'; + import { buildItemPath } from '../../../src/config/paths'; import { - ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS, + buildDataCyWrapper, buildItemMembershipRowDeleteButtonId, + buildItemMembershipRowEditButtonId, + buildItemMembershipRowId, buildItemMembershipRowSelector, buildShareButtonId, } from '../../../src/config/selectors'; -import { membershipsWithoutUser } from '../../../src/utils/membership'; import { CURRENT_USER, MEMBERS } from '../../fixtures/members'; -import { - ITEMS_WITH_MEMBERSHIPS, - ITEM_WITH_WRITE_ACCESS, -} from '../../fixtures/memberships'; +import { buildItemMembership } from '../../fixtures/memberships'; + +const itemWithAdmin = { ...PackedFolderItemFactory() }; +const adminMembership = buildItemMembership({ + item: itemWithAdmin, + permission: PermissionLevel.Admin, + account: MEMBERS.ANNA, + creator: MEMBERS.ANNA, +}); +const membershipsWithoutAdmin = [ + buildItemMembership({ + item: itemWithAdmin, + permission: PermissionLevel.Write, + account: MEMBERS.BOB, + creator: MEMBERS.ANNA, + }), + buildItemMembership({ + item: itemWithAdmin, + permission: PermissionLevel.Write, + account: MEMBERS.CEDRIC, + creator: MEMBERS.ANNA, + }), + buildItemMembership({ + item: itemWithAdmin, + permission: PermissionLevel.Read, + account: MEMBERS.DAVID, + creator: MEMBERS.ANNA, + }), +]; describe('View Memberships', () => { beforeEach(() => { - cy.setUpApi(ITEMS_WITH_MEMBERSHIPS); + cy.setUpApi({ + items: [ + { + ...itemWithAdmin, + memberships: [adminMembership, ...membershipsWithoutAdmin], + }, + ], + }); }); it('view membership in settings', () => { - const [item] = ITEMS_WITH_MEMBERSHIPS.items; - const { memberships } = item; + const item = itemWithAdmin; cy.visit(buildItemPath(item.id)); cy.get(`#${buildShareButtonId(item.id)}`).click(); - const filteredMemberships = membershipsWithoutUser( - memberships, - CURRENT_USER.id, - ); + i18n.changeLanguage(CURRENT_USER.extra.lang); + + // only admin - cannot edit, delete + cy.get(buildDataCyWrapper(buildItemMembershipRowId(adminMembership.id))) + .should('contain', adminMembership.account.name) + .should('contain', (adminMembership.account as Member).email); - // panel only contains 2 avatars: one user, one +x - // check contains member avatar - for (const { permission, account, id } of filteredMemberships) { + // editable rows + for (const { permission, account, id } of membershipsWithoutAdmin) { const { name, email } = Object.values(MEMBERS).find( ({ id: mId }) => mId === account.id, ); + // check name and mail - cy.get(buildItemMembershipRowSelector(id)) + cy.get(buildDataCyWrapper(buildItemMembershipRowId(id))) .should('contain', name) - .should('contain', email); - - // check permission select - cy.get( - `${buildItemMembershipRowSelector( - id, - )} .${ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS} input`, - ).should('have.value', permission); + .should('contain', email) + .should('contain', i18n.t(permission, { ns: namespaces.enums })); // 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; + const item = PackedFolderItemFactory( + {}, + { permission: PermissionLevel.Write }, + ); + const ownMembership = buildItemMembership({ + item, + permission: PermissionLevel.Write, + account: MEMBERS.ANNA, + creator: MEMBERS.ANNA, + }); + const memberships = [ + buildItemMembership({ + item, + permission: PermissionLevel.Admin, + account: MEMBERS.BOB, + creator: MEMBERS.ANNA, + }), + buildItemMembership({ + item, + permission: PermissionLevel.Read, + account: MEMBERS.CEDRIC, + creator: MEMBERS.ANNA, + }), + ]; + + cy.setUpApi({ + items: [{ ...item, memberships: [...memberships, ownMembership] }], + }); cy.visit(buildItemPath(item.id)); cy.get(`#${buildShareButtonId(item.id)}`).click(); - // check contains member avatar - for (const { permission, account, id } of memberships) { - const { name, email } = Object.values(MEMBERS).find( - ({ id: mId }) => mId === account.id, - ); - // 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', + i18n.changeLanguage(CURRENT_USER.extra.lang); + + // can only see own permission - can edit, delete + cy.get(buildItemMembershipRowSelector(ownMembership.id)) + .should('contain', CURRENT_USER.email) + .should( + 'contain', + i18n.t(ownMembership.permission, { ns: namespaces.enums }), ); + + cy.get(`#${buildItemMembershipRowEditButtonId(ownMembership.id)}`).should( + 'be.visible', + ); + + cy.get(`#${buildItemMembershipRowDeleteButtonId(ownMembership.id)}`).should( + 'be.visible', + ); + + // cannot see others + for (const { id } of memberships) { + cy.get(buildItemMembershipRowSelector(id)).should('not.exist'); } }); }); diff --git a/cypress/fixtures/memberships.ts b/cypress/fixtures/memberships.ts index 79d0c408f..08c0d1ec3 100644 --- a/cypress/fixtures/memberships.ts +++ b/cypress/fixtures/memberships.ts @@ -11,7 +11,6 @@ import { import { v4 } from 'uuid'; import { ApiConfig } from '../support/types'; -import { DEFAULT_FOLDER_ITEM } from './items'; import { MEMBERS } from './members'; export const buildItemMembership = (args: { @@ -117,53 +116,3 @@ export const ITEMS_WITH_MEMBERSHIPS: ApiConfig = { sampleItems[2], ], }; - -const sampleItemsWithWriteAccess = [ - PackedFolderItemFactory( - { - ...DEFAULT_FOLDER_ITEM, - id: 'ecafbd2a-5688-11eb-ae93-0242ac130002', - creator: MEMBERS.BOB, - name: 'own_item_name1', - path: 'ecafbd2a_5688_11eb_ae93_0242ac130002', - }, - { permission: PermissionLevel.Write }, - ), -]; - -export const ITEM_WITH_WRITE_ACCESS: ApiConfig = { - items: [ - { - ...sampleItemsWithWriteAccess[0], - memberships: [ - { - id: 'ecafbd2a-5688-11eb-be93-0242ac130002', - item: sampleItemsWithWriteAccess[0], - permission: PermissionLevel.Admin, - account: MEMBERS.BOB, - creator: MEMBERS.ANNA, - updatedAt: '2021-08-11T12:56:36.834Z', - createdAt: '2021-08-11T12:56:36.834Z', - }, - { - id: 'ecafbd2a-5688-11eb-be92-0242ac130002', - item: sampleItemsWithWriteAccess[0], - permission: PermissionLevel.Write, - account: MEMBERS.ANNA, - creator: MEMBERS.ANNA, - updatedAt: '2021-08-11T12:56:36.834Z', - createdAt: '2021-08-11T12:56:36.834Z', - }, - { - id: 'ecafbd1a-5688-11eb-be93-0242ac130002', - item: sampleItemsWithWriteAccess[0], - permission: PermissionLevel.Read, - account: MEMBERS.CEDRIC, - creator: MEMBERS.ANNA, - updatedAt: '2021-08-11T12:56:36.834Z', - createdAt: '2021-08-11T12:56:36.834Z', - }, - ], - }, - ], -}; diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index b2a309c67..1f5e85b13 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -32,6 +32,7 @@ import { mockEditItem, mockEditItemMembershipForItem, mockEditMember, + mockEnroll, mockGetAccessibleItems, mockGetAppData, mockGetAppLink, @@ -60,7 +61,9 @@ import { mockGetMemberMentions, mockGetMembers, mockGetMembersBy, + mockGetMembershipRequestsForItem, mockGetOwnItems, + mockGetOwnMembershipRequests, mockGetParents, mockGetPublicationStatus, mockGetPublishItemInformations, @@ -91,6 +94,8 @@ import { mockPublishItem, mockPutItemLoginSchema, mockRecycleItems, + mockRejectMembershipRequest, + mockRequestMembership, mockRestoreItems, mockSignInRedirection, mockSignOut, @@ -113,6 +118,7 @@ Cypress.Commands.add( categories = SAMPLE_CATEGORIES, itemValidationGroups = [], itemPublicationStatus = PublicationStatus.Unpublished, + membershipRequests = [], deleteItemsError = false, postItemError = false, moveItemsError = false, @@ -354,6 +360,16 @@ Cypress.Commands.add( mockImportH5p(importH5pError); mockGetPublishItemsForMember(publishedItemData, getPublishedItemsError); + + mockGetOwnMembershipRequests(currentMember, membershipRequests); + + mockRequestMembership(); + + mockGetMembershipRequestsForItem(membershipRequests); + + mockRejectMembershipRequest(); + + mockEnroll(); }, ); diff --git a/cypress/support/commands/item.ts b/cypress/support/commands/item.ts index a98291135..cebadbb60 100644 --- a/cypress/support/commands/item.ts +++ b/cypress/support/commands/item.ts @@ -17,9 +17,11 @@ import { ITEM_FORM_NAME_INPUT_ID, ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS, MY_GRAASP_ITEM_PATH, + SHARE_BUTTON_SELECTOR, SHARE_ITEM_EMAIL_INPUT_ID, SHARE_ITEM_SHARE_BUTTON_ID, TREE_MODAL_CONFIRM_BUTTON_ID, + buildDataCyWrapper, buildFolderItemCardThumbnail, buildItemFormAppOptionId, buildItemRowArrowId, @@ -37,6 +39,8 @@ import { Cypress.Commands.add( 'fillShareForm', ({ email, permission, submit = true, selector = '' }) => { + cy.get(buildDataCyWrapper(SHARE_BUTTON_SELECTOR)).click(); + // select permission cy.get(`${selector} .${ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS}`).click(); cy.get(`#${buildPermissionOptionId(permission)}`).click(); diff --git a/cypress/support/server.ts b/cypress/support/server.ts index cc6bc0d5c..5f85a3941 100644 --- a/cypress/support/server.ts +++ b/cypress/support/server.ts @@ -4,6 +4,7 @@ import { App, Category, ChatMention, + CompleteMembershipRequest, DiscriminatedItem, HttpMethod, Invitation, @@ -2283,3 +2284,79 @@ export const mockGetLinkMetadata = (): void => { }, ).as('getLinkMetadata'); }; + +export const mockGetOwnMembershipRequests = ( + currentMember: Member, + membershipRequests: CompleteMembershipRequest[], +): void => { + cy.intercept( + { + method: HttpMethod.Get, + url: new RegExp( + `${API_HOST}/items/${ID_FORMAT}/memberships/requests/own$`, + ), + }, + ({ reply, url }) => { + const urlParams = url.split('/'); + const itemId = urlParams[urlParams.length - 4]; + return reply( + membershipRequests.find( + ({ item, member }) => + item.id === itemId && member.id === currentMember.id, + ), + ); + }, + ).as('getOwnMembershipRequests'); +}; + +export const mockRequestMembership = (): void => { + cy.intercept( + { + method: HttpMethod.Post, + url: new RegExp(`${API_HOST}/items/${ID_FORMAT}/memberships/requests$`), + }, + ({ reply }) => reply({ statusCode: StatusCodes.OK }), + ).as('requestMembership'); +}; + +export const mockGetMembershipRequestsForItem = ( + membershipRequests: CompleteMembershipRequest[], +): void => { + cy.intercept( + { + method: HttpMethod.Get, + url: new RegExp(`${API_HOST}/items/${ID_FORMAT}/memberships/requests$`), + }, + ({ reply, url }) => { + const urlParams = url.split('/'); + const itemId = urlParams[urlParams.length - 3]; + return reply(membershipRequests.filter(({ item }) => item.id === itemId)); + }, + ).as('getMembershipRequestsForItem'); +}; + +export const mockRejectMembershipRequest = (): void => { + cy.intercept( + { + method: HttpMethod.Delete, + url: new RegExp( + `${API_HOST}/items/${ID_FORMAT}/memberships/requests/${ID_FORMAT}`, + ), + }, + ({ reply }) => { + reply({ statusCode: StatusCodes.OK }); + }, + ).as('rejectMembershipRequest'); +}; + +export const mockEnroll = (): void => { + cy.intercept( + { + method: HttpMethod.Post, + url: new RegExp(`${API_HOST}/items/${ID_FORMAT}/enroll`), + }, + ({ reply }) => { + reply({ statusCode: StatusCodes.OK }); + }, + ).as('enroll'); +}; diff --git a/cypress/support/types.ts b/cypress/support/types.ts index e4ae146e3..8ebd476f8 100644 --- a/cypress/support/types.ts +++ b/cypress/support/types.ts @@ -3,6 +3,7 @@ import { ChatMention, ChatMessage, CompleteMember, + CompleteMembershipRequest, DiscriminatedItem, Invitation, ItemBookmark, @@ -20,7 +21,6 @@ import { ShortLink, } from '@graasp/sdk'; -// TODO: not the best way, to change with mirage? export type ItemForTest = DiscriminatedItem & { categories?: ItemCategory[]; thumbnails?: string; @@ -35,7 +35,6 @@ export type ItemForTest = DiscriminatedItem & { public?: ItemTag; }; -// TODO: not ideal, to change? export type MemberForTest = CompleteMember & { thumbnails?: string }; export type LocalFileItemForTest = LocalFileItemType & { @@ -61,6 +60,7 @@ export type ApiConfig = { recycledItemData?: RecycledItemData[]; itemPublicationStatus?: PublicationStatus; publishedItemData?: ItemPublished[]; + membershipRequests?: CompleteMembershipRequest[]; // statuses = SAMPLE_STATUSES, itemValidationGroups?: ItemValidationGroup[]; deleteItemsError?: boolean; diff --git a/package.json b/package.json index 7deb6eda7..4764eab17 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,11 @@ "@emotion/styled": "11.13.0", "@graasp/chatbox": "3.3.0", "@graasp/map": "1.18.0", - "@graasp/query-client": "3.22.4", + "@graasp/query-client": "3.24.0", "@graasp/sdk": "4.29.1", "@graasp/stylis-plugin-rtl": "2.2.0", - "@graasp/translations": "1.37.0", - "@graasp/ui": "5.1.0", + "@graasp/translations": "1.38.0", + "@graasp/ui": "5.2.0", "@mui/icons-material": "5.16.4", "@mui/lab": "5.0.0-alpha.172", "@mui/material": "5.16.4", diff --git a/src/components/App.tsx b/src/components/App.tsx index 5ce466170..07ed0c3db 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -34,13 +34,12 @@ import MapItemsScreen from './pages/MapItemsScreen'; import PublishedItemsScreen from './pages/PublishedItemsScreen'; import RecycledItemsScreen from './pages/RecycledItemsScreen'; import HomeScreen from './pages/home/HomeScreen'; -import ItemLoginWrapper from './pages/item/ItemLoginWrapper'; import ItemPageLayout from './pages/item/ItemPageLayout'; import ItemScreen from './pages/item/ItemScreen'; -import ItemScreenLayout from './pages/item/ItemScreenLayout'; import ItemSettingsPage from './pages/item/ItemSettingsPage'; import ItemSharingPage from './pages/item/ItemSharingPage'; import LibrarySettingsPage from './pages/item/LibrarySettingsPage'; +import ItemAccessWrapper from './pages/item/accessWrapper/ItemAccessWrapper'; const { useItemFeedbackUpdates, useCurrentMember } = hooks; @@ -117,14 +116,7 @@ const App = (): JSX.Element => { {/* item pages - can be public */} - - - - } - > + }> } /> }> } /> diff --git a/src/components/Root.tsx b/src/components/Root.tsx index 6d8695271..c860872f2 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -10,12 +10,13 @@ import { ToastContainer } from 'react-toastify'; import { CssBaseline } from '@mui/material'; -import { AccountType } from '@graasp/sdk'; import { langs } from '@graasp/translations'; import { ThemeProvider } from '@graasp/ui'; import * as Sentry from '@sentry/react'; +import { getCurrentAccountLang } from '@/utils/member'; + import i18nConfig from '../config/i18n'; import { QueryClientProvider, @@ -37,11 +38,7 @@ const ThemeWrapper = () => { langs={langs} languageSelectSx={{ mb: 2, mr: 2 }} i18n={i18nConfig} - defaultDirection={i18nConfig.dir( - currentMember?.type === AccountType.Individual - ? currentMember?.extra?.lang - : 'ltr', - )} + defaultDirection={i18nConfig.dir(getCurrentAccountLang(currentMember))} > diff --git a/src/components/item/edit/EditButton.tsx b/src/components/item/edit/EditButton.tsx index 57cfbd594..b6fd62e94 100644 --- a/src/components/item/edit/EditButton.tsx +++ b/src/components/item/edit/EditButton.tsx @@ -1,3 +1,5 @@ +import { MouseEventHandler } from 'react'; + import { DiscriminatedItem } from '@graasp/sdk'; import { ActionButtonVariant, @@ -12,19 +14,19 @@ import { import { BUILDER } from '../../../langs/constants'; type Props = { - item: DiscriminatedItem; - type: ActionButtonVariant; - onClick?: () => void; + itemId: DiscriminatedItem['id']; + type?: ActionButtonVariant; + onClick?: MouseEventHandler; }; -const EditButton = ({ item, onClick, type = 'icon' }: Props): JSX.Element => { +const EditButton = ({ itemId, onClick, type = 'icon' }: Props): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); return ( { )} diff --git a/src/components/item/sharing/InvitationsTable.tsx b/src/components/item/sharing/InvitationsTable.tsx deleted file mode 100644 index 347d83e08..000000000 --- a/src/components/item/sharing/InvitationsTable.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Typography, -} from '@mui/material'; - -import { DiscriminatedItem, Invitation, PermissionLevel } from '@graasp/sdk'; - -import { useBuilderTranslation } from '@/config/i18n'; -import { mutations } from '@/config/queryClient'; -import { - buildInvitationTableRowId, - buildItemInvitationRowDeleteButtonId, -} from '@/config/selectors'; -import { BUILDER } from '@/langs/constants'; - -import ResendInvitation from './ResendInvitation'; -import TableRowDeleteButton from './TableRowDeleteButton'; -import TableRowPermission from './TableRowPermission'; - -type Props = { - item: DiscriminatedItem; - invitations?: Invitation[]; - emptyMessage?: string; - readOnly?: boolean; -}; - -const InvitationsTable = ({ - invitations, - item, - emptyMessage, - readOnly = false, -}: Props): JSX.Element => { - const { t: translateBuilder } = useBuilderTranslation(); - const { mutate: editInvitation } = mutations.usePatchInvitation(); - const { mutate: postInvitations } = mutations.usePostInvitations(); - const { mutate: deleteInvitation } = mutations.useDeleteInvitation(); - - const onDelete = (invitation: Invitation) => { - deleteInvitation({ itemId: item.id, id: invitation.id }); - }; - - if (!invitations?.length) { - return {emptyMessage ?? 'empty'}; - } - - const changePermission = - (invitation: Invitation) => (permission: PermissionLevel) => { - if (invitation.item.path === item.path) { - editInvitation({ - id: invitation.id, - permission, - itemId: item.id, - }); - } else { - postInvitations({ - itemId: item.id, - invitations: [ - { - email: invitation.email, - permission, - }, - ], - }); - } - }; - - return ( - - - - - - {translateBuilder(BUILDER.INVITATIONS_TABLE_EMAIL_HEADER)} - - - {translateBuilder(BUILDER.INVITATIONS_TABLE_PERMISSION_HEADER)} - - {!readOnly && ( - - {translateBuilder(BUILDER.INVITATIONS_TABLE_ACTIONS_HEADER)} - - )} - - - - {invitations?.map((row) => { - const isDisabled = item.path !== row.item.path; - return ( - - - {row.email} - - - - - {!readOnly && ( - - onDelete(row)} - id={buildItemInvitationRowDeleteButtonId(row.id)} - tooltip={translateBuilder( - BUILDER.INVITATIONS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP, - )} - disabled={isDisabled} - /> - - - )} - - ); - })} - -
-
- ); -}; - -export default InvitationsTable; diff --git a/src/components/item/sharing/ItemLoginMembershipsTable.tsx b/src/components/item/sharing/ItemLoginMembershipsTable.tsx deleted file mode 100644 index 004bab433..000000000 --- a/src/components/item/sharing/ItemLoginMembershipsTable.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useOutletContext } from 'react-router'; - -import { Box, Typography } from '@mui/material'; - -import { DiscriminatedItem, isPseudoMember } from '@graasp/sdk'; - -import { OutletType } from '@/components/pages/item/type'; -import { useBuilderTranslation } from '@/config/i18n'; -import { hooks } from '@/config/queryClient'; -import { BUILDER } from '@/langs/constants'; -import { selectHighestMemberships } from '@/utils/membership'; - -import ItemMembershipsTable from './ItemMembershipsTable'; - -type Props = { item: DiscriminatedItem }; - -const ItemLoginMembershipsTable = ({ item }: Props): JSX.Element | false => { - const { t: translateBuilder } = useBuilderTranslation(); - const { canAdmin } = useOutletContext(); - const { data: itemLoginSchema } = hooks.useItemLoginSchema({ - itemId: item.id, - }); - const { data: memberships } = hooks.useItemMemberships(item?.id); - - if (itemLoginSchema && memberships) { - // show authenticated members if login schema is defined - // todo: show only if item is pseudonymised - return ( - - - {translateBuilder(BUILDER.SHARING_AUTHENTICATED_MEMBERS_TITLE)} - - isPseudoMember(m.account)) - .sort((im1, im2) => (im1.account.name > im2.account.name ? 1 : -1))} - emptyMessage={translateBuilder( - BUILDER.SHARING_AUTHENTICATED_MEMBERS_EMPTY_MESSAGE, - )} - showEmail={false} - readOnly={!canAdmin} - /> - - ); - } - - return false; -}; - -export default ItemLoginMembershipsTable; diff --git a/src/components/item/sharing/ItemMembershipSelect.tsx b/src/components/item/sharing/ItemMembershipSelect.tsx index a3dbc9a5e..cdddf135e 100644 --- a/src/components/item/sharing/ItemMembershipSelect.tsx +++ b/src/components/item/sharing/ItemMembershipSelect.tsx @@ -30,7 +30,7 @@ const defaultDisabledMap: DisabledMap = { }; export type ItemMembershipSelectProps = { - value: PermissionLevel; + value?: PermissionLevel; onChange?: SelectProps['onChange']; color?: SelectProps['color']; showLabel?: boolean; @@ -41,6 +41,7 @@ export type ItemMembershipSelectProps = { */ disabled?: boolean | DisabledMap; allowDowngrade?: boolean; + size?: SelectProps['size']; }; const ItemMembershipSelect = ({ @@ -51,6 +52,7 @@ const ItemMembershipSelect = ({ displayEmpty = false, disabled = false, allowDowngrade = true, + size = 'small', }: ItemMembershipSelectProps): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); const { t: enumT } = useEnumsTranslation(); @@ -67,8 +69,8 @@ const ItemMembershipSelect = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [value]); - const values = Object.values(PermissionLevel).filter( - (p) => allowDowngrade || PermissionLevelCompare.gte(p, value), + const values = Object.values(PermissionLevel).filter((p) => + value ? allowDowngrade || PermissionLevelCompare.gte(p, value) : true, ); return ( @@ -87,7 +89,7 @@ const ItemMembershipSelect = ({ displayEmpty={displayEmpty} className={ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS} color={color} - size="small" + size={size} /> ); }; diff --git a/src/components/item/sharing/ItemMembershipsTable.tsx b/src/components/item/sharing/ItemMembershipsTable.tsx deleted file mode 100644 index 8d5684e0d..000000000 --- a/src/components/item/sharing/ItemMembershipsTable.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { useState } from 'react'; - -import { - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Typography, -} from '@mui/material'; - -import { - AccountType, - DiscriminatedItem, - ItemMembership, - PermissionLevel, -} from '@graasp/sdk'; - -import { useBuilderTranslation } from '../../../config/i18n'; -import { hooks, mutations } from '../../../config/queryClient'; -import { - buildItemMembershipRowDeleteButtonId, - buildItemMembershipRowId, -} from '../../../config/selectors'; -import { BUILDER } from '../../../langs/constants'; -import DeleteItemDialog from './ConfirmMembership'; -import TableRowDeleteButton from './TableRowDeleteButton'; -import TableRowPermission from './TableRowPermission'; - -type Props = { - item: DiscriminatedItem; - memberships: ItemMembership[]; - emptyMessage?: string; - showEmail?: boolean; - readOnly?: boolean; -}; - -const ItemMembershipsTable = ({ - memberships, - item, - emptyMessage, - showEmail = true, - readOnly = false, -}: Props): JSX.Element => { - const { t: translateBuilder } = useBuilderTranslation(); - - const { data: currentMember } = hooks.useCurrentMember(); - const { mutate: editItemMembership } = mutations.useEditItemMembership(); - const { mutate: postItemMembership } = mutations.usePostItemMembership(); - - const [open, setOpen] = useState(false); - const [membershipToDelete, setMembershipToDelete] = - useState(null); - - const handleClose = () => { - setOpen(false); - }; - const onDelete = (im: ItemMembership) => { - setMembershipToDelete(im); - setOpen(true); - }; - - const hasOnlyOneAdmin = - memberships.filter((per) => per.permission === PermissionLevel.Admin) - .length === 1; - - const changePermission = - (im: ItemMembership) => (permission: PermissionLevel) => { - if (im.item.path === item.path) { - editItemMembership({ - id: im.id, - permission, - itemId: item.id, - }); - } else if (im.account.type === AccountType.Individual) { - postItemMembership({ - id: item.id, - accountId: im.account.id, - permission, - }); - } - }; - - if (!memberships.length) { - return {emptyMessage ?? 'empty'}; - } - - return ( - - - - - {showEmail && ( - - {translateBuilder(BUILDER.ITEM_MEMBERSHIPS_TABLE_EMAIL_HEADER)} - - )} - - {translateBuilder(BUILDER.ITEM_MEMBERSHIPS_TABLE_NAME_HEADER)} - - - {translateBuilder( - BUILDER.ITEM_MEMBERSHIPS_TABLE_PERMISSION_HEADER, - )} - - {!readOnly && ( - - {translateBuilder( - BUILDER.ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER, - )} - - )} - - - - {memberships.map((row) => ( - - {showEmail && ( - - - {row.account.type === AccountType.Individual - ? row.account.email - : ''} - - - )} - - {row.account.name} - - - - - {!readOnly && ( - - onDelete(row)} - id={buildItemMembershipRowDeleteButtonId(row.id)} - tooltip={translateBuilder( - BUILDER.ITEM_MEMBERSHIPS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP, - )} - disabled={ - // cannot delete if not for current item - row.item.path !== item.path || - // cannot delete if is the only admin - (hasOnlyOneAdmin && - row.permission === PermissionLevel.Admin) - } - /> - - )} - - ))} - -
- {open && ( - per.permission === PermissionLevel.Admin, - ).length === 1 - } - /> - )} -
- ); -}; - -export default ItemMembershipsTable; diff --git a/src/components/item/sharing/ItemSharingTab.tsx b/src/components/item/sharing/ItemSharingTab.tsx index fbc9a1b00..55448eeee 100644 --- a/src/components/item/sharing/ItemSharingTab.tsx +++ b/src/components/item/sharing/ItemSharingTab.tsx @@ -1,87 +1,16 @@ import { useOutletContext } from 'react-router-dom'; -import { Box, Container, Stack, Typography } from '@mui/material'; - -import { AccountType } from '@graasp/sdk'; +import { Box, Container, Divider, Stack, Typography } from '@mui/material'; import { OutletType } from '@/components/pages/item/type'; -import { selectHighestMemberships } from '@/utils/membership'; import { useBuilderTranslation } from '../../../config/i18n'; import { hooks } from '../../../config/queryClient'; import { BUILDER } from '../../../langs/constants'; -import CreateItemMembershipForm from './CreateItemMembershipForm'; -import InvitationsTable from './InvitationsTable'; -import ItemLoginMembershipsTable from './ItemLoginMembershipsTable'; -import ItemMembershipsTable from './ItemMembershipsTable'; import VisibilitySelect from './VisibilitySelect'; -import ImportUsersWithCSVButton from './csvImport/ImportUsersWithCSVButton'; +import MembershipTabs from './membershipTable/MembershipTabs'; import ShortLinksRenderer from './shortLink/ShortLinksRenderer'; -const MembershipSettings = (): JSX.Element | null => { - const { t: translateBuilder } = useBuilderTranslation(); - const { item, canWrite, canAdmin } = useOutletContext(); - - const { data: rawMemberships } = hooks.useItemMemberships(item?.id); - - const { data: invitations } = hooks.useItemInvitations(item?.id); - - // do not display settings if cannot access memberships - if (!rawMemberships || !canWrite) { - return null; - } - - const memberships = selectHighestMemberships(rawMemberships) - .filter((m) => m.account.type === AccountType.Individual) - .sort((im1, im2) => { - if (im1.account.type !== AccountType.Individual) { - return 1; - } - if (im2.account.type !== AccountType.Individual) { - return -1; - } - return im1.account.email > im2.account.email ? 1 : -1; - }); - - return ( - - {canAdmin && ( - <> - - - - )} - - - {translateBuilder(BUILDER.SHARING_AUTHORIZED_MEMBERS_TITLE)} - - - - - - - {translateBuilder(BUILDER.SHARING_INVITATIONS_TITLE)} - - - - - ); -}; - const ItemSharingTab = (): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); const { item, canAdmin } = useOutletContext(); @@ -90,7 +19,7 @@ const ItemSharingTab = (): JSX.Element => { return ( - + {translateBuilder(BUILDER.SHARING_TITLE)} @@ -100,13 +29,15 @@ const ItemSharingTab = (): JSX.Element => { canAdminShortLink={Boolean(memberships && canAdmin)} /> + {translateBuilder(BUILDER.ITEM_SETTINGS_VISIBILITY_TITLE)} - + + ); diff --git a/src/components/item/sharing/TableRowPermission.tsx b/src/components/item/sharing/TableRowPermission.tsx deleted file mode 100644 index 2f6e1764f..000000000 --- a/src/components/item/sharing/TableRowPermission.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Typography } from '@mui/material'; - -import { PermissionLevel } from '@graasp/sdk'; - -import ItemMembershipSelect from './ItemMembershipSelect'; -import type { ItemMembershipSelectProps } from './ItemMembershipSelect'; - -type TableRowPermissionProps = { - readOnly?: boolean; - changePermission: (permission: PermissionLevel) => void; - permission: PermissionLevel; - allowDowngrade?: boolean; -}; - -const TableRowPermission = ({ - readOnly = false, - changePermission, - permission, - allowDowngrade = true, -}: TableRowPermissionProps): JSX.Element => { - const onChangePermission: ItemMembershipSelectProps['onChange'] = (e) => { - const value = e.target.value as PermissionLevel; - changePermission(value); - }; - - if (readOnly || (!allowDowngrade && permission === PermissionLevel.Admin)) { - return {permission}; - } - return ( - - ); -}; -export default TableRowPermission; diff --git a/src/components/item/sharing/csvImport/ImportUsersWithCSVButton.tsx b/src/components/item/sharing/csvImport/ImportUsersWithCSVButton.tsx deleted file mode 100644 index 7398c8f37..000000000 --- a/src/components/item/sharing/csvImport/ImportUsersWithCSVButton.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useState } from 'react'; - -import { Dialog } from '@mui/material'; - -import { DiscriminatedItem, ItemType } from '@graasp/sdk'; -import { Button } from '@graasp/ui'; - -import { useBuilderTranslation } from '@/config/i18n'; -import { SHARE_ITEM_CSV_PARSER_BUTTON_ID } from '@/config/selectors'; -import { BUILDER } from '@/langs/constants'; - -import ImportUsersDialogContent, { - DIALOG_ID_LABEL, -} from './ImportUsersDialogContent'; - -type ImportUsersWithCSVButtonProps = { - item: DiscriminatedItem; -}; - -const ImportUsersWithCSVButton = ({ - item, -}: ImportUsersWithCSVButtonProps): JSX.Element => { - const { t } = useBuilderTranslation(); - const [modalOpen, setModalOpen] = useState(false); - - const handleOpenModal = () => { - setModalOpen(true); - }; - const handleCloseModal = () => { - setModalOpen(false); - }; - - return ( - <> - - - - - - ); -}; -export default ImportUsersWithCSVButton; diff --git a/src/components/item/sharing/csvImport/ImportUsersWithCSVDialog.tsx b/src/components/item/sharing/csvImport/ImportUsersWithCSVDialog.tsx new file mode 100644 index 000000000..40a1cd1d6 --- /dev/null +++ b/src/components/item/sharing/csvImport/ImportUsersWithCSVDialog.tsx @@ -0,0 +1,33 @@ +import { Dialog } from '@mui/material'; + +import { DiscriminatedItem, ItemType } from '@graasp/sdk'; + +import ImportUsersDialogContent, { + DIALOG_ID_LABEL, +} from './ImportUsersDialogContent'; + +type ImportUsersWithCSVDialogProps = { + item: DiscriminatedItem; + handleCloseModal: () => void; + open: boolean; +}; + +const ImportUsersWithCSVDialog = ({ + item, + handleCloseModal, + open, +}: ImportUsersWithCSVDialogProps): JSX.Element => ( + + + +); +export default ImportUsersWithCSVDialog; diff --git a/src/components/item/sharing/membershipTable/DeleteItemMembershipButton.tsx b/src/components/item/sharing/membershipTable/DeleteItemMembershipButton.tsx new file mode 100644 index 000000000..435a5ef5c --- /dev/null +++ b/src/components/item/sharing/membershipTable/DeleteItemMembershipButton.tsx @@ -0,0 +1,40 @@ +import { DiscriminatedItem, ItemMembership } from '@graasp/sdk'; + +import useModalStatus from '@/components/hooks/useModalStatus'; +import { useBuilderTranslation } from '@/config/i18n'; +import { buildItemMembershipRowDeleteButtonId } from '@/config/selectors'; +import { BUILDER } from '@/langs/constants'; + +import DeleteItemMembershipDialog from './DeleteItemMembershipDialog'; +import TableRowDeleteButton from './TableRowDeleteButton'; + +const DeleteItemMembershipButton = ({ + data, + itemId, +}: { + data: ItemMembership; + itemId: DiscriminatedItem['id']; +}): JSX.Element => { + const { isOpen, closeModal, openModal } = useModalStatus(); + const { t: translateBuilder } = useBuilderTranslation(); + + return ( + <> + openModal()} + id={buildItemMembershipRowDeleteButtonId(data.id)} + tooltip={translateBuilder( + BUILDER.ITEM_MEMBERSHIPS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP, + )} + /> + + + ); +}; + +export default DeleteItemMembershipButton; diff --git a/src/components/item/sharing/ConfirmMembership.tsx b/src/components/item/sharing/membershipTable/DeleteItemMembershipDialog.tsx similarity index 83% rename from src/components/item/sharing/ConfirmMembership.tsx rename to src/components/item/sharing/membershipTable/DeleteItemMembershipDialog.tsx index a3cca9635..c0e6261ae 100644 --- a/src/components/item/sharing/ConfirmMembership.tsx +++ b/src/components/item/sharing/membershipTable/DeleteItemMembershipDialog.tsx @@ -16,11 +16,11 @@ import { } from '@graasp/sdk'; import { Button } from '@graasp/ui'; -import { useBuilderTranslation } from '../../../config/i18n'; -import { hooks, mutations } from '../../../config/queryClient'; -import { CONFIRM_MEMBERSHIP_DELETE_BUTTON_ID } from '../../../config/selectors'; -import { BUILDER } from '../../../langs/constants'; -import CancelButton from '../../common/CancelButton'; +import { useBuilderTranslation } from '../../../../config/i18n'; +import { hooks, mutations } from '../../../../config/queryClient'; +import { CONFIRM_MEMBERSHIP_DELETE_BUTTON_ID } from '../../../../config/selectors'; +import { BUILDER } from '../../../../langs/constants'; +import CancelButton from '../../../common/CancelButton'; const labelId = 'alert-dialog-title'; const descriptionId = 'alert-dialog-description'; @@ -28,13 +28,16 @@ const descriptionId = 'alert-dialog-description'; type Props = { open?: boolean; handleClose: () => void; - item: DiscriminatedItem; - membershipToDelete: ItemMembership | null; - hasOnlyOneAdmin: boolean; + itemId: DiscriminatedItem['id']; + membershipToDelete: Pick< + ItemMembership, + 'id' | 'account' | 'permission' + > | null; + hasOnlyOneAdmin?: boolean; }; -const DeleteItemDialog = ({ - item, +const DeleteItemMembershipDialog = ({ + itemId, open = false, handleClose, membershipToDelete, @@ -51,7 +54,7 @@ const DeleteItemDialog = ({ if (membershipToDelete?.id) { deleteItemMembership({ id: membershipToDelete.id, - itemId: item.id, + itemId, }).then(() => { // if current user deleted their own membership navigate them to the home if (membershipToDelete.account.id === member?.id) { @@ -122,4 +125,4 @@ const DeleteItemDialog = ({ ); }; -export default DeleteItemDialog; +export default DeleteItemMembershipDialog; diff --git a/src/components/item/sharing/membershipTable/EditPermissionButton.tsx b/src/components/item/sharing/membershipTable/EditPermissionButton.tsx new file mode 100644 index 000000000..2d40170d5 --- /dev/null +++ b/src/components/item/sharing/membershipTable/EditPermissionButton.tsx @@ -0,0 +1,118 @@ +import { useState } from 'react'; +import { Trans } from 'react-i18next'; + +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Stack, + Typography, +} from '@mui/material'; + +import { PermissionLevel } from '@graasp/sdk'; +import { COMMON } from '@graasp/translations'; +import { Button, EditButton } from '@graasp/ui'; + +import useModalStatus from '@/components/hooks/useModalStatus'; +import { BUILDER } from '@/langs/constants'; + +import { + useBuilderTranslation, + useCommonTranslation, +} from '../../../../config/i18n'; +import ItemMembershipSelect from '../ItemMembershipSelect'; + +type Props = { + email?: string; + name?: string; + allowDowngrade?: boolean; + permission: PermissionLevel; + handleUpdate: (p: PermissionLevel) => void; + id?: string; +}; + +const EditPermissionButton = ({ + email, + name, + permission, + allowDowngrade = true, + handleUpdate, + id, +}: Props): JSX.Element | null => { + const { isOpen, openModal, closeModal } = useModalStatus(); + + const [currentPermission, setCurrentPermission] = useState(permission); + + const { t: translateCommon } = useCommonTranslation(); + const { t: translateBuilder } = useBuilderTranslation(); + + if (!allowDowngrade && permission === PermissionLevel.Admin) { + return null; + } + + return ( + <> + openModal()} /> + + + }} + /> + + + + + {translateBuilder( + BUILDER.EDIT_PERMISSION_CANNOT_DOWNGRADE_FROM_PARENT, + )} + + +
+ + {name} + + + {email} + +
+ + setCurrentPermission(e.target.value as PermissionLevel) + } + size="medium" + allowDowngrade={allowDowngrade} + /> +
+
+
+ + + + +
+ + ); +}; + +export default EditPermissionButton; diff --git a/src/components/item/sharing/membershipTable/GuestItemMembershipTableRow.tsx b/src/components/item/sharing/membershipTable/GuestItemMembershipTableRow.tsx new file mode 100644 index 000000000..afa29306c --- /dev/null +++ b/src/components/item/sharing/membershipTable/GuestItemMembershipTableRow.tsx @@ -0,0 +1,37 @@ +import { TableCell, Typography } from '@mui/material'; + +import { DiscriminatedItem, ItemMembership } from '@graasp/sdk'; + +import { useEnumsTranslation } from '@/config/i18n'; + +import { buildItemMembershipRowId } from '../../../../config/selectors'; +import DeleteItemMembershipButton from './DeleteItemMembershipButton'; +import { StyledTableRow } from './StyledTableRow'; + +const GuestItemMembershipTableRow = ({ + data, + itemId, +}: { + data: ItemMembership; + itemId: DiscriminatedItem['id']; +}): JSX.Element => { + const { t: translateEnums } = useEnumsTranslation(); + + return ( + + + + {data.account.name} + + + + {translateEnums(data.permission)} + + + + + + ); +}; + +export default GuestItemMembershipTableRow; diff --git a/src/components/item/sharing/membershipTable/InvitationTableRow.tsx b/src/components/item/sharing/membershipTable/InvitationTableRow.tsx new file mode 100644 index 000000000..22899d367 --- /dev/null +++ b/src/components/item/sharing/membershipTable/InvitationTableRow.tsx @@ -0,0 +1,86 @@ +import { TableCell, Typography } from '@mui/material'; + +import { DiscriminatedItem, Invitation, PermissionLevel } from '@graasp/sdk'; + +import { useBuilderTranslation, useEnumsTranslation } from '@/config/i18n'; +import { mutations } from '@/config/queryClient'; +import { + buildInvitationTableRowId, + buildItemInvitationRowDeleteButtonId, +} from '@/config/selectors'; +import { BUILDER } from '@/langs/constants'; + +import EditPermissionButton from './EditPermissionButton'; +import ResendInvitation from './ResendInvitation'; +import { StyledTableRow } from './StyledTableRow'; +import TableRowDeleteButton from './TableRowDeleteButton'; + +const InvitationTableRow = ({ + data, + item, +}: { + item: DiscriminatedItem; + data: Invitation; +}): JSX.Element => { + const { t: translateEnums } = useEnumsTranslation(); + const { t: translateBuilder } = useBuilderTranslation(); + + const { mutate: editInvitation } = mutations.usePatchInvitation(); + const { mutate: postInvitations } = mutations.usePostInvitations(); + const { mutate: deleteInvitation } = mutations.useDeleteInvitation(); + + const changePermission = (permission: PermissionLevel) => { + if (data.item.path === item.path) { + editInvitation({ + id: data.id, + permission, + itemId: item.id, + }); + } else { + postInvitations({ + itemId: item.id, + invitations: [ + { + email: data.email, + permission, + }, + ], + }); + } + }; + + return ( + + + + ({data.name ?? translateBuilder(BUILDER.INVITATION_NOT_REGISTER_TEXT)} + ) + + + {data.email} + + + + {translateEnums(data.permission)} + + + + + deleteInvitation({ id: data.id, itemId: item.id })} + disabled={data.item.path !== item.path} + /> + + + ); +}; + +export default InvitationTableRow; diff --git a/src/components/item/sharing/membershipTable/ItemMembershipTableRow.tsx b/src/components/item/sharing/membershipTable/ItemMembershipTableRow.tsx new file mode 100644 index 000000000..ab3660fef --- /dev/null +++ b/src/components/item/sharing/membershipTable/ItemMembershipTableRow.tsx @@ -0,0 +1,89 @@ +import { TableCell, Typography } from '@mui/material'; + +import { + AccountType, + DiscriminatedItem, + ItemMembership, + PermissionLevel, +} from '@graasp/sdk'; + +import { useEnumsTranslation } from '@/config/i18n'; +import { mutations } from '@/config/queryClient'; + +import { + buildItemMembershipRowEditButtonId, + buildItemMembershipRowId, +} from '../../../../config/selectors'; +import DeleteItemMembershipButton from './DeleteItemMembershipButton'; +import EditPermissionButton from './EditPermissionButton'; +import { StyledTableRow } from './StyledTableRow'; + +const ItemMembershipTableRow = ({ + allowDowngrade = false, + isOnlyAdmin = false, + item, + data, +}: { + data: ItemMembership; + item: DiscriminatedItem; + allowDowngrade?: boolean; + isOnlyAdmin?: boolean; +}): JSX.Element => { + const { t: translateEnums } = useEnumsTranslation(); + + const { mutate: editItemMembership } = mutations.useEditItemMembership(); + const { mutate: shareItem } = mutations.usePostItemMembership(); + + const changePermission = (newPermission: PermissionLevel) => { + if (data.item.path === item.path) { + editItemMembership({ + id: data.id, + permission: newPermission, + itemId: item.id, + }); + } else { + shareItem({ + id: item.id, + accountId: data.account.id, + permission: newPermission, + }); + } + }; + + return ( + + + + {data.account.name} + + + {data.account.type === AccountType.Individual && data.account.email} + + + + {translateEnums(data.permission)} + + + {!isOnlyAdmin && ( + + )} + {!isOnlyAdmin && allowDowngrade && ( + + )} + + + ); +}; + +export default ItemMembershipTableRow; diff --git a/src/components/item/sharing/membershipTable/ItemMembershipsTable.tsx b/src/components/item/sharing/membershipTable/ItemMembershipsTable.tsx new file mode 100644 index 000000000..643414406 --- /dev/null +++ b/src/components/item/sharing/membershipTable/ItemMembershipsTable.tsx @@ -0,0 +1,163 @@ +import { useOutletContext } from 'react-router-dom'; + +import { + Skeleton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from '@mui/material'; + +import { AccountType, PermissionLevel } from '@graasp/sdk'; + +import groupby from 'lodash.groupby'; + +import ErrorAlert from '@/components/common/ErrorAlert'; +import { OutletType } from '@/components/pages/item/type'; + +import { useBuilderTranslation } from '../../../../config/i18n'; +import { hooks } from '../../../../config/queryClient'; +import { BUILDER } from '../../../../langs/constants'; +import GuestItemMembershipTableRow from './GuestItemMembershipTableRow'; +import InvitationTableRow from './InvitationTableRow'; +import ItemMembershipTableRow from './ItemMembershipTableRow'; +import { useHighestMemberships } from './useHighestMemberships'; + +type Props = { + showEmail?: boolean; +}; + +const EMPTY_NAME_VALUE = '-'; + +// sort by name, email +// empty names should be at the end +// sorting by permission is done in the splitting below +const sortByNameAndEmail = ( + d1: { name: string; email: string }, + d2: { name: string; email: string }, +) => { + if (d1.name === d2.name) { + return d1.email > d2.email ? 1 : -1; + } + if (d1.name === EMPTY_NAME_VALUE) { + return 1; + } + if (d2.name === EMPTY_NAME_VALUE) { + return -1; + } + return d1.name > d2.name ? 1 : -1; +}; + +const ItemMembershipsTable = ({ showEmail = true }: Props): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + + const { item, canAdmin } = useOutletContext(); + const { data: invitations, isLoading: isInvitationsLoading } = + hooks.useItemInvitations(item.id, { + enabled: canAdmin, + }); + const { + data: memberships, + hasOnlyOneAdmin, + isLoading: isMembershipsLoading, + } = useHighestMemberships({ canAdmin, item }); + + if (memberships) { + // map memberships to corresponding row layout and meaningful data to sort + const membershipsRows = memberships.map((im) => ({ + name: im.account.name, + email: + im.account.type === AccountType.Individual ? im.account.email : '-', + permission: im.permission, + component: + im.account.type === AccountType.Individual ? ( + + ) : ( + + ), + })); + + // map invitations to row layout and meaningful data to sort + const invitationsRows = + invitations?.map((r) => ({ + name: r.name ?? EMPTY_NAME_VALUE, + email: r.email, + permission: r.permission, + component: , + })) ?? []; + + // split per permission to add divider between sections + const groupedRows = groupby( + [...membershipsRows, ...invitationsRows], + ({ permission }) => permission, + ); + + const adminRows = + groupedRows[PermissionLevel.Admin]?.toSorted(sortByNameAndEmail); + const writeRows = + groupedRows[PermissionLevel.Write]?.toSorted(sortByNameAndEmail); + const readRows = + groupedRows[PermissionLevel.Read]?.toSorted(sortByNameAndEmail); + const allRows = [adminRows, writeRows, readRows] + .filter(Boolean) + .flatMap((rows) => [ + + + , + ...rows.map((r) => r.component), + ]) + .slice(1); + + if (allRows) { + return ( + + + + + {showEmail && ( + + {translateBuilder( + BUILDER.ITEM_MEMBERSHIPS_TABLE_MEMBER_HEADER, + )} + + )} + + {translateBuilder( + BUILDER.ITEM_MEMBERSHIPS_TABLE_PERMISSION_HEADER, + )} + + + {translateBuilder( + BUILDER.ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER, + )} + + + + {allRows.map((el) => el)} +
+
+ ); + } + } + + if (isMembershipsLoading || isInvitationsLoading) { + return ; + } + + return ; +}; + +export default ItemMembershipsTable; diff --git a/src/components/item/sharing/membershipTable/MembershipRequestTable.tsx b/src/components/item/sharing/membershipTable/MembershipRequestTable.tsx new file mode 100644 index 000000000..c11181459 --- /dev/null +++ b/src/components/item/sharing/membershipTable/MembershipRequestTable.tsx @@ -0,0 +1,149 @@ +import { useTranslation } from 'react-i18next'; +import { useOutletContext } from 'react-router-dom'; + +import { + Skeleton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from '@mui/material'; + +import { + CompleteMembershipRequest, + PermissionLevel, + formatDate, +} from '@graasp/sdk'; +import { Button } from '@graasp/ui'; + +import { Check } from 'lucide-react'; + +import ErrorAlert from '@/components/common/ErrorAlert'; +import { OutletType } from '@/components/pages/item/type'; +import { useBuilderTranslation } from '@/config/i18n'; +import { hooks, mutations } from '@/config/queryClient'; +import { BUILDER } from '@/langs/constants'; + +import { + MEMBERSHIP_REQUESTS_EMPTY_SELECTOR, + MEMBERSHIP_REQUEST_ACCEPT_BUTTON_SELECTOR, + MEMBERSHIP_REQUEST_REJECT_BUTTON_SELECTOR, + buildMembershipRequestRowSelector, +} from '../../../../config/selectors'; +import { StyledTableRow } from './StyledTableRow'; + +const MembershipRequestTable = (): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + const { i18n } = useTranslation(); + const { item, canAdmin } = useOutletContext(); + const { data: requests, isLoading } = hooks.useMembershipRequests(item.id, { + enabled: canAdmin, + }); + const { mutate: deleteRequest } = mutations.useDeleteMembershipRequest(); + const { mutate: shareItem } = mutations.usePostItemMembership(); + + const acceptRequest = (data: CompleteMembershipRequest) => { + shareItem({ + id: item.id, + accountId: data.member.id, + permission: PermissionLevel.Read, + }); + }; + + if (requests?.length) { + return ( + + + + + + {translateBuilder(BUILDER.ITEM_MEMBERSHIPS_TABLE_MEMBER_HEADER)} + + + {translateBuilder( + BUILDER.MEMBERSHIPS_REQUEST_TABLE_CREATED_AT_HEADER, + )} + + + {translateBuilder( + BUILDER.ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER, + )} + + + + + {requests?.map((r) => ( + + + + {r.member.name} + + + {r.member.email} + + + + + {formatDate(r.createdAt, { locale: i18n.language })} + + + + + + + + ))} + +
+
+ ); + } + + if (requests?.length === 0) { + return ( + + {translateBuilder(BUILDER.MEMBERSHIP_REQUESTS_TABLE_EMPTY)} + + ); + } + + if (isLoading) { + return ( + <> + + + + + ); + } + + return ; +}; + +export default MembershipRequestTable; diff --git a/src/components/item/sharing/membershipTable/MembershipTabs.tsx b/src/components/item/sharing/membershipTable/MembershipTabs.tsx new file mode 100644 index 000000000..be5109ef9 --- /dev/null +++ b/src/components/item/sharing/membershipTable/MembershipTabs.tsx @@ -0,0 +1,88 @@ +import { ReactNode, SyntheticEvent, useState } from 'react'; +import { useOutletContext } from 'react-router-dom'; + +import { Badge, Box, Stack, Tab, Tabs, Typography } from '@mui/material'; + +import { OutletType } from '@/components/pages/item/type'; +import { + MEMBERSHIPS_TAB_SELECTOR, + MEMBERSHIP_REQUESTS_TAB_SELECTOR, +} from '@/config/selectors'; + +import { useBuilderTranslation } from '../../../../config/i18n'; +import { hooks } from '../../../../config/queryClient'; +import { BUILDER } from '../../../../langs/constants'; +import ShareButton from '../shareButton/ShareButton'; +import ItemMembershipsTable from './ItemMembershipsTable'; +import MembershipRequestTable from './MembershipRequestTable'; + +type TabPanelProps = { + children?: ReactNode; + value: number; + selectedTabId: number; +}; +const CustomTabPanel = ({ children, value, selectedTabId }: TabPanelProps) => ( + +); + +const MembershipTabs = (): JSX.Element | null => { + const { t: translateBuilder } = useBuilderTranslation(); + const { item, canAdmin } = useOutletContext(); + const [selectedTabId, setSelectedTabId] = useState(0); + + const { data: requests } = hooks.useMembershipRequests(item.id); + + return ( + + + + {translateBuilder(BUILDER.ACCESS_MANAGEMENT_TITLE)} + + {canAdmin && } + + + { + setSelectedTabId(newValue); + }} + aria-label={translateBuilder(BUILDER.ACCESS_MANAGEMENT_TITLE)} + data-cy={MEMBERSHIPS_TAB_SELECTOR} + > + + {canAdmin && ( + + {translateBuilder(BUILDER.USER_MANAGEMENT_REQUESTS_TAB)} + + ) : ( + translateBuilder(BUILDER.USER_MANAGEMENT_REQUESTS_TAB) + ) + } + /> + )} + + + + + + {canAdmin && ( + + + + )} + + ); +}; + +export default MembershipTabs; diff --git a/src/components/item/sharing/ResendInvitation.tsx b/src/components/item/sharing/membershipTable/ResendInvitation.tsx similarity index 74% rename from src/components/item/sharing/ResendInvitation.tsx rename to src/components/item/sharing/membershipTable/ResendInvitation.tsx index 54359645b..070416f1e 100644 --- a/src/components/item/sharing/ResendInvitation.tsx +++ b/src/components/item/sharing/membershipTable/ResendInvitation.tsx @@ -2,10 +2,10 @@ import { useState } from 'react'; import { Button } from '@graasp/ui'; -import { useBuilderTranslation } from '../../../config/i18n'; -import { mutations } from '../../../config/queryClient'; -import { ITEM_RESEND_INVITATION_BUTTON_CLASS } from '../../../config/selectors'; -import { BUILDER } from '../../../langs/constants'; +import { useBuilderTranslation } from '../../../../config/i18n'; +import { mutations } from '../../../../config/queryClient'; +import { ITEM_RESEND_INVITATION_BUTTON_CLASS } from '../../../../config/selectors'; +import { BUILDER } from '../../../../langs/constants'; type Props = { invitationId: string; @@ -30,7 +30,7 @@ const ResendInvitation = ({ return ( - - - - - - + + Share item + + + {translateBuilder(BUILDER.SHARE_ITEM_FORM_INVITATION_TOOLTIP)} + + + + + + + + + + + ); }; diff --git a/src/components/item/sharing/shareButton/ShareButton.tsx b/src/components/item/sharing/shareButton/ShareButton.tsx new file mode 100644 index 000000000..ff2655d7a --- /dev/null +++ b/src/components/item/sharing/shareButton/ShareButton.tsx @@ -0,0 +1,127 @@ +import { useRef, useState } from 'react'; + +import Button from '@mui/material/Button'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import Grow from '@mui/material/Grow'; +import MenuItem from '@mui/material/MenuItem'; +import MenuList from '@mui/material/MenuList'; +import Paper from '@mui/material/Paper'; +import Popper from '@mui/material/Popper'; + +import { DiscriminatedItem } from '@graasp/sdk'; + +import { ChevronDown } from 'lucide-react'; + +import useModalStatus from '@/components/hooks/useModalStatus'; +import { useBuilderTranslation } from '@/config/i18n'; +import { + SHARE_BUTTON_MORE_ID, + SHARE_BUTTON_SELECTOR, + SHARE_ITEM_CSV_PARSER_BUTTON_ID, +} from '@/config/selectors'; +import { BUILDER } from '@/langs/constants'; + +import ImportUsersWithCSVDialog from '../csvImport/ImportUsersWithCSVDialog'; +import CreateItemMembershipForm from './CreateItemMembershipForm'; + +type Props = { + item: DiscriminatedItem; +}; + +const ShareButton = ({ item }: Props): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + + const [openMenu, setOpenMenu] = useState(false); + const anchorRef = useRef(null); + const { + isOpen: isOpenImportCsvModal, + closeModal: closeImportCsvModal, + openModal: openImportCsvModal, + } = useModalStatus(); + const { + isOpen: isOpenShareItemModal, + closeModal: closeShareItemModal, + openModal: openShareItemModal, + } = useModalStatus(); + + const handleToggle = () => { + setOpenMenu((prevOpen) => !prevOpen); + }; + + const handleClose = (event: Event) => { + if (anchorRef.current?.contains(event.target as HTMLElement)) { + return; + } + + setOpenMenu(false); + }; + + return ( + <> + + + + + + {({ TransitionProps }) => ( + // eslint-disable-next-line react/jsx-props-no-spreading + + + + + + {translateBuilder(BUILDER.SHARE_ITEM_BUTTON)} + + openImportCsvModal()} + > + {translateBuilder(BUILDER.SHARE_ITEM_CSV_IMPORT_BUTTON)} + + + + + + )} + + + + + ); +}; + +export default ShareButton; diff --git a/src/components/main/ItemMenuContent.tsx b/src/components/main/ItemMenuContent.tsx index afec4bf63..cdfdb0cba 100644 --- a/src/components/main/ItemMenuContent.tsx +++ b/src/components/main/ItemMenuContent.tsx @@ -92,7 +92,7 @@ const ItemMenuContent = ({ item }: Props): JSX.Element | null => { closeMenu(); }} key="edit" - item={item} + itemId={item.id} type={ActionButton.MENU_ITEM} /> ) : ( diff --git a/src/components/main/list/ItemForbiddenScreen.tsx b/src/components/main/list/ItemForbiddenScreen.tsx deleted file mode 100644 index 725289403..000000000 --- a/src/components/main/list/ItemForbiddenScreen.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import AccountCircleIcon from '@mui/icons-material/AccountCircle'; -import { Stack } from '@mui/material'; - -import { Button, ForbiddenContent } from '@graasp/ui'; - -import { hooks } from '@/config/queryClient'; - -import { useBuilderTranslation } from '../../../config/i18n'; -import { ITEM_LOGIN_SCREEN_FORBIDDEN_ID } from '../../../config/selectors'; -import { BUILDER } from '../../../langs/constants'; -import UserSwitchWrapper from '../../common/UserSwitchWrapper'; - -const ItemForbiddenScreen = (): JSX.Element => { - const { data: member } = hooks.useCurrentMember(); - const { t: translateBuilder } = useBuilderTranslation(); - - const ButtonContent = ( - - ); - - return ( - - - - - ); -}; - -export default ItemForbiddenScreen; diff --git a/src/components/pages/item/ItemLoginWrapper.tsx b/src/components/pages/item/ItemLoginWrapper.tsx deleted file mode 100644 index 285b4b8ed..000000000 --- a/src/components/pages/item/ItemLoginWrapper.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useParams } from 'react-router-dom'; - -import { ItemLoginAuthorization } from '@graasp/ui'; - -import { hooks, mutations } from '@/config/queryClient'; -import { - ITEM_LOGIN_SIGN_IN_BUTTON_ID, - ITEM_LOGIN_SIGN_IN_PASSWORD_ID, - ITEM_LOGIN_SIGN_IN_USERNAME_ID, -} from '@/config/selectors'; - -import ItemForbiddenScreen from '../../main/list/ItemForbiddenScreen'; - -const { useItem, useCurrentMember, useItemLoginSchemaType } = hooks; - -const ItemLoginWrapper = ({ - children, -}: { - children: JSX.Element; -}): JSX.Element => { - const { mutate: itemLoginSignIn } = mutations.usePostItemLogin(); - const { data: currentAccount, isLoading: isCurrentAccountLoading } = - useCurrentMember(); - const { itemId } = useParams(); - const { data: item, isLoading: isItemLoading } = useItem(itemId); - const { data: itemLoginSchemaType, isLoading: isItemLoginSchemaTypeLoading } = - useItemLoginSchemaType({ itemId }); - - const forbiddenContent = ; - - if (!itemId) { - return forbiddenContent; - } - - return ( - - {children} - - ); -}; - -export default ItemLoginWrapper; diff --git a/src/components/pages/item/ItemScreenLayout.tsx b/src/components/pages/item/ItemScreenLayout.tsx deleted file mode 100644 index e1fe58297..000000000 --- a/src/components/pages/item/ItemScreenLayout.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useEffect } from 'react'; -import { Outlet, useParams } from 'react-router-dom'; - -import { PermissionLevel, PermissionLevelCompare } from '@graasp/sdk'; -import { Loader } from '@graasp/ui'; - -import { hooks } from '../../../config/queryClient'; -import ErrorAlert from '../../common/ErrorAlert'; -import { useLayoutContext } from '../../context/LayoutContext'; - -const { useItem } = hooks; - -const ItemScreenLayout = (): JSX.Element => { - const { itemId } = useParams(); - - const { data: item, isLoading } = useItem(itemId); - const { setEditingItemId } = useLayoutContext(); - - useEffect(() => { - setEditingItemId(null); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [itemId]); - - const canWrite = item?.permission - ? PermissionLevelCompare.gte(item.permission, PermissionLevel.Write) - : false; - - const canAdmin = item?.permission - ? PermissionLevelCompare.gte(item.permission, PermissionLevel.Admin) - : false; - - if (item && itemId) { - return ( - - ); - } - - if (isLoading) { - return ; - } - return ; -}; - -export default ItemScreenLayout; diff --git a/src/components/pages/item/accessWrapper/EnrollContent.tsx b/src/components/pages/item/accessWrapper/EnrollContent.tsx new file mode 100644 index 000000000..676e1a12c --- /dev/null +++ b/src/components/pages/item/accessWrapper/EnrollContent.tsx @@ -0,0 +1,47 @@ +import { Stack, Typography } from '@mui/material'; + +import { DiscriminatedItem } from '@graasp/sdk'; +import { Button } from '@graasp/ui'; + +import { CircleUser } from 'lucide-react'; + +import { useBuilderTranslation } from '@/config/i18n'; +import { mutations } from '@/config/queryClient'; +import { ENROLL_BUTTON_SELECTOR } from '@/config/selectors'; +import { BUILDER } from '@/langs/constants'; + +type Props = { itemId: DiscriminatedItem['id'] }; + +const EnrollContent = ({ itemId }: Props): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + + const { mutate: enroll } = mutations.useEnroll(); + + return ( + + + + {translateBuilder(BUILDER.ENROLL_TITLE)} + + + {translateBuilder(BUILDER.ENROLL_DESCRIPTION)} + + + + ); +}; + +export default EnrollContent; diff --git a/src/components/pages/item/accessWrapper/ItemAccessWrapper.tsx b/src/components/pages/item/accessWrapper/ItemAccessWrapper.tsx new file mode 100644 index 000000000..bfcca17db --- /dev/null +++ b/src/components/pages/item/accessWrapper/ItemAccessWrapper.tsx @@ -0,0 +1,84 @@ +import { Outlet, useParams } from 'react-router-dom'; + +import { Stack } from '@mui/material'; + +import { + AccountType, + PermissionLevel, + PermissionLevelCompare, +} from '@graasp/sdk'; +import { ForbiddenContent, ItemLoginWrapper } from '@graasp/ui'; + +import Redirect from '@/components/main/Redirect'; +import EnrollContent from '@/components/pages/item/accessWrapper/EnrollContent'; +import RequestAccessContent from '@/components/pages/item/accessWrapper/RequestAccessContent'; +import { hooks, mutations } from '@/config/queryClient'; +import { + ITEM_LOGIN_SCREEN_FORBIDDEN_ID, + ITEM_LOGIN_SIGN_IN_BUTTON_ID, + ITEM_LOGIN_SIGN_IN_PASSWORD_ID, + ITEM_LOGIN_SIGN_IN_USERNAME_ID, +} from '@/config/selectors'; + +const ItemAccessWrapper = (): JSX.Element => { + const { itemId } = useParams(); + const { data: item, isLoading: itemIsLoading } = hooks.useItem(itemId); + const { data: currentMember, isLoading: currentMemberIsLoading } = + hooks.useCurrentMember(); + const { data: itemLoginSchemaType, isLoading: itemLoginSchemaTypeIsLoading } = + hooks.useItemLoginSchemaType({ itemId }); + + const { mutate: itemLoginSignIn } = mutations.usePostItemLogin(); + + const canWrite = item?.permission + ? PermissionLevelCompare.gte(item.permission, PermissionLevel.Write) + : false; + + const canAdmin = item?.permission + ? PermissionLevelCompare.gte(item.permission, PermissionLevel.Admin) + : false; + + if (!itemId) { + return ; + } + + return ( + } + signInButtonId={ITEM_LOGIN_SIGN_IN_BUTTON_ID} + usernameInputId={ITEM_LOGIN_SIGN_IN_USERNAME_ID} + passwordInputId={ITEM_LOGIN_SIGN_IN_PASSWORD_ID} + signIn={itemLoginSignIn} + itemLoginSchemaType={itemLoginSchemaType} + itemId={itemId} + isLoading={ + currentMemberIsLoading || itemLoginSchemaTypeIsLoading || itemIsLoading + } + requestAccessContent={ + currentMember?.type === AccountType.Individual ? ( + + ) : undefined + } + forbiddenContent={ + + + + } + > + + + ); +}; + +export default ItemAccessWrapper; diff --git a/src/components/pages/item/accessWrapper/RequestAccessContent.tsx b/src/components/pages/item/accessWrapper/RequestAccessContent.tsx new file mode 100644 index 000000000..1e22a9d59 --- /dev/null +++ b/src/components/pages/item/accessWrapper/RequestAccessContent.tsx @@ -0,0 +1,90 @@ +import { LoadingButton } from '@mui/lab'; +import { Stack, Typography } from '@mui/material'; + +import { + DiscriminatedItem, + Member, + MembershipRequestStatus, +} from '@graasp/sdk'; + +import { Check, Lock } from 'lucide-react'; + +import { useBuilderTranslation } from '@/config/i18n'; +import { hooks, mutations } from '@/config/queryClient'; +import { + MEMBERSHIP_REQUEST_PENDING_SCREEN_SELECTOR, + REQUEST_MEMBERSHIP_BUTTON_ID, +} from '@/config/selectors'; +import { BUILDER } from '@/langs/constants'; + +type Props = { + member: Member; + itemId: DiscriminatedItem['id']; +}; + +const RequestAccessContent = ({ member, itemId }: Props): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + const { + mutateAsync: requestMembership, + isSuccess, + isLoading, + } = mutations.useRequestMembership(); + const { data: request } = hooks.useOwnMembershipRequest(itemId); + + if (request?.status === MembershipRequestStatus.Pending) { + return ( + + + + {translateBuilder(BUILDER.REQUEST_ACCESS_PENDING_TITLE)} + + + {translateBuilder(BUILDER.REQUEST_ACCESS_PENDING_DESCRIPTION)} + + + ); + } + + return ( + + + + {translateBuilder(BUILDER.REQUEST_ACCESS_TITLE)} + + : null} + onClick={async () => { + await requestMembership({ id: itemId }); + }} + > + {isSuccess + ? translateBuilder(BUILDER.REQUEST_ACCESS_SENT_BUTTON) + : translateBuilder(BUILDER.REQUEST_ACCESS_BUTTON)} + + + {translateBuilder(BUILDER.ITEM_LOGIN_HELPER_SIGN_OUT, { + email: member.email, + })} + + + ); +}; + +export default RequestAccessContent; diff --git a/src/config/notifier.ts b/src/config/notifier.ts index caee1950a..ecc8b2157 100644 --- a/src/config/notifier.ts +++ b/src/config/notifier.ts @@ -79,7 +79,10 @@ const { createShortLinkRoutine, deleteShortLinkRoutine, patchShortLinkRoutine, + patchInvitationRoutine, + deleteInvitationRoutine, deleteItemThumbnailRoutine, + deleteMembershipRequestRoutine, } = routines; const notify = ({ @@ -184,6 +187,8 @@ const notifier: Notifier = ( case createShortLinkRoutine.FAILURE: case deleteShortLinkRoutine.FAILURE: case patchShortLinkRoutine.FAILURE: + case deleteInvitationRoutine.FAILURE: + case patchInvitationRoutine.FAILURE: case deleteItemThumbnailRoutine.FAILURE: { message = getErrorMessageFromPayload(payload); break; @@ -215,6 +220,10 @@ const notifier: Notifier = ( case createShortLinkRoutine.SUCCESS: case deleteShortLinkRoutine.SUCCESS: case patchShortLinkRoutine.SUCCESS: + case deleteInvitationRoutine.SUCCESS: + case patchInvitationRoutine.SUCCESS: + case postItemMembershipRoutine.SUCCESS: + case deleteMembershipRequestRoutine.SUCCESS: case deleteItemThumbnailRoutine.SUCCESS: { message = getSuccessMessageFromPayload(payload); break; diff --git a/src/config/selectors.ts b/src/config/selectors.ts index d9eaf3cf8..6938a0d77 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -51,6 +51,8 @@ export const SHARE_ITEM_EMAIL_INPUT_ID = 'shareItemModalEmailInput'; export const buildPermissionOptionId = (id: string): string => `permission-${id}`; export const SHARE_ITEM_SHARE_BUTTON_ID = 'shareItemModalShareButton'; +export const SHARE_BUTTON_SELECTOR = 'shareItem'; +export const SHARE_BUTTON_MORE_ID = 'shareItemMore'; export const PUBLISHED_ITEMS_ID = 'publishedItems'; export const BOOKMARKED_ITEMS_ID = 'bookmarkedItems'; @@ -116,6 +118,8 @@ export const ITEM_MEMBERSHIP_PERMISSION_SELECT_CLASS = 'itemMembershipPermissionSelect'; export const buildItemMembershipRowDeleteButtonId = (id: string): string => `itemMembershipRowDeleteButtonId-${id}`; +export const buildItemMembershipRowEditButtonId = (id: string): string => + `itemMembershipRowEditButtonId-${id}`; export const ITEM_INFORMATION_ICON_IS_OPEN_CLASS = 'itemInformationIconIsOpen'; export const ITEM_SEARCH_INPUT_ID = 'itemSearchInput'; export const ITEMS_GRID_NO_SEARCH_RESULT_ID = 'itemsGridNoSearchResult'; @@ -227,8 +231,6 @@ export const buildInvitationEmailTableRowId = (id: string): string => `invitationEmailTableRow-${id}`; export const buildInvitationTableRowId = (id: string): string => `invitationTableRow-${id}`; -export const buildInvitationTableRowSelector = (id: string): string => - `[data-cy="${buildInvitationTableRowId(id)}"]`; export const ITEM_RESEND_INVITATION_BUTTON_CLASS = 'itemResendInvitationButton'; export const CREATE_MEMBERSHIP_FORM_ID = 'createMembershipFormId'; export const NAVIGATION_ROOT_ID = 'navigationRoot'; @@ -435,3 +437,18 @@ export const COPY_MANY_ITEMS_BUTTON_SELECTOR = `.lucide-copy`; export const MOVE_MANY_ITEMS_BUTTON_SELECTOR = `.lucide-move`; export const DELETE_SINGLE_ITEM_BUTTON_SELECTOR = `.lucide-trash`; export const PREVENT_GUEST_MESSAGE_ID = 'preventGuestMessage'; +export const REQUEST_MEMBERSHIP_BUTTON_ID = 'requestMembershipButton'; +export const MEMBERSHIP_REQUEST_PENDING_SCREEN_SELECTOR = + 'membershipRequestPendingScreen'; +export const MEMBERSHIPS_TAB_SELECTOR = `membershipsTab`; +export const MEMBERSHIP_REQUESTS_TAB_SELECTOR = `membershipRequestsTab`; + +export const MEMBERSHIP_REQUESTS_EMPTY_SELECTOR = 'membershipRequestsEmpty'; +export const buildMembershipRequestRowSelector = (memberId: string): string => + `membershipRequestRow-${memberId}`; +export const MEMBERSHIP_REQUEST_ACCEPT_BUTTON_SELECTOR = + 'membershipRequestAcceptButton'; + +export const MEMBERSHIP_REQUEST_REJECT_BUTTON_SELECTOR = + 'membershipRequestRejectButton'; +export const ENROLL_BUTTON_SELECTOR = 'enrollButton'; diff --git a/src/langs/ar.json b/src/langs/ar.json index ef41c7402..545597907 100644 --- a/src/langs/ar.json +++ b/src/langs/ar.json @@ -90,11 +90,7 @@ "IMPORT_ZIP_LIMITATIONS_TEXT": "يمكنك تحميل ما يصل إلى H5P واحد بحجم {{maxSize}} في المرّة الواحدة. عند حدوث خطأ ، حاول تحميل ملف ZIP أصغر حجمًا.", "IMPORT_ZIP_TITLE": "استيراد أرشيف", "IMPORT_ZIP_WARNING": "بمجرد أن تمّ قبول ملفك ، سيستغرق الأمر عدة دقائق حتى يصبح متاحًا.", - "INVITATIONS_TABLE_ACTIONS_HEADER": "إجراءات", "INVITATIONS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP": "تم تحديد هذه الدعوة في العنصر الأصلي ولا يمكن حذفها هنا.", - "INVITATIONS_TABLE_EMAIL_HEADER": "البريد إلكتروني", - "INVITATIONS_TABLE_INVITATION_HEADER": "دعوة", - "INVITATIONS_TABLE_PERMISSION_HEADER": "صلاحية الوصول", "ITEM_CATEGORIES_CONTAINER_TITLE": "الفئة", "ITEM_CATEGORIES_CONTAINER_MISSING_WARNING": "أضف فئة واحدة على الأقل لتحديد الموضوع العالمي للمحتوى الخاص بك بشكل أفضل", "ITEM_CATEGORIES_CONTAINER_EMPTY_BUTTON": "أضف فئة", @@ -307,7 +303,6 @@ "SHARING_AUTHENTICATED_MEMBERS_EMPTY_MESSAGE": "لم يوثق أي مستخدم لهذا العنصر حتى الآن.", "SHARING_AUTHENTICATED_MEMBERS_TITLE": "أعضاء مصدَّق عليهم", "SHARING_AUTHORIZED_MEMBERS_EMPTY_MESSAGE": "لا يوجد مستخدم لديه حقّ الوصول إلى هذا العنصر.", - "SHARING_AUTHORIZED_MEMBERS_TITLE": "الأعضاء المصرَّح لهم", "SHARING_INVITATIONS_EMPTY_MESSAGE": "لا توجد دعوة لهذا العنصر حتى الآن.", "SHARING_INVITATIONS_RESEND_BUTTON": "اعادة ارسال الدعوة", "SHARING_INVITATIONS_TITLE": "الدعوات المعلّقة", diff --git a/src/langs/constants.ts b/src/langs/constants.ts index fdd1aa198..0a8c0bcdd 100644 --- a/src/langs/constants.ts +++ b/src/langs/constants.ts @@ -213,9 +213,6 @@ export const BUILDER = { 'SHARE_ITEM_FORM_INVITATION_EMPTY_EMAIL_MESSAGE', INVITATIONS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP: 'INVITATIONS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP', - INVITATIONS_TABLE_EMAIL_HEADER: 'INVITATIONS_TABLE_EMAIL_HEADER', - INVITATIONS_TABLE_INVITATION_HEADER: 'INVITATIONS_TABLE_INVITATION_HEADER', - INVITATIONS_TABLE_PERMISSION_HEADER: 'INVITATIONS_TABLE_PERMISSION_HEADER', ITEM_CATEGORIES_CONTAINER_TITLE: 'ITEM_CATEGORIES_CONTAINER_TITLE', ITEM_CATEGORIES_CONTAINER_MISSING_WARNING: 'ITEM_CATEGORIES_CONTAINER_MISSING_WARNING', @@ -236,8 +233,8 @@ export const BUILDER = { 'ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER', ITEM_MEMBERSHIPS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP: 'ITEM_MEMBERSHIPS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP', - ITEM_MEMBERSHIPS_TABLE_EMAIL_HEADER: 'ITEM_MEMBERSHIPS_TABLE_EMAIL_HEADER', - ITEM_MEMBERSHIPS_TABLE_NAME_HEADER: 'ITEM_MEMBERSHIPS_TABLE_NAME_HEADER', + ITEM_MEMBERSHIPS_TABLE_MEMBER_HEADER: 'ITEM_MEMBERSHIPS_TABLE_MEMBER_HEADER', + ITEM_MEMBERSHIPS_TABLE_STATUS_HEADER: 'ITEM_MEMBERSHIPS_TABLE_STATUS_HEADER', ITEM_MEMBERSHIPS_TABLE_PERMISSION_HEADER: 'ITEM_MEMBERSHIPS_TABLE_PERMISSION_HEADER', ITEM_MENU_CREATE_SHORTCUT_MENU_ITEM: 'ITEM_MENU_CREATE_SHORTCUT_MENU_ITEM', @@ -392,7 +389,6 @@ export const BUILDER = { SHARING_AUTHENTICATED_MEMBERS_TITLE: 'SHARING_AUTHENTICATED_MEMBERS_TITLE', SHARING_AUTHORIZED_MEMBERS_EMPTY_MESSAGE: 'SHARING_AUTHORIZED_MEMBERS_EMPTY_MESSAGE', - SHARING_AUTHORIZED_MEMBERS_TITLE: 'SHARING_AUTHORIZED_MEMBERS_TITLE', SHARING_INVITATIONS_EMPTY_MESSAGE: 'SHARING_INVITATIONS_EMPTY_MESSAGE', SHARING_INVITATIONS_RESEND_BUTTON: 'SHARING_INVITATIONS_RESEND_BUTTON', SHARING_INVITATIONS_TITLE: 'SHARING_INVITATIONS_TITLE', @@ -574,4 +570,27 @@ export const BUILDER = { GUEST_SIGN_OUT_BUTTON: 'GUEST_SIGN_OUT_BUTTON', REDIRECTION_TEXT: 'REDIRECTION_TEXT', REDIRECTION_BUTTON: 'REDIRECTION_BUTTON', + ITEM_LOGIN_HELPER_SIGN_OUT: 'ITEM_LOGIN_HELPER_SIGN_OUT', + INVITATION_NOT_REGISTER_TEXT: 'INVITATION_NOT_REGISTER_TEXT', + MEMBERSHIPS_REQUEST_TABLE_CREATED_AT_HEADER: + 'MEMBERSHIPS_REQUEST_TABLE_CREATED_AT_HEADER', + USER_MANAGEMENT_MEMBERS_TAB: 'USER_MANAGEMENT_MEMBERS_TAB', + USER_MANAGEMENT_REQUESTS_TAB: 'USER_MANAGEMENT_REQUESTS_TAB', + EDIT_PERMISSION_DIALOG_TITLE: 'EDIT_PERMISSION_DIALOG_TITLE', + EDIT_PERMISSION_DIALOG_SUBMIT_BUTTON: 'EDIT_PERMISSION_DIALOG_SUBMIT_BUTTON', + EDIT_PERMISSION_CANNOT_DOWNGRADE_FROM_PARENT: + 'EDIT_PERMISSION_CANNOT_DOWNGRADE_FROM_PARENT', + INVITATION_DELETE_TOOLTIP: 'INVITATION_DELETE_TOOLTIP', + MEMBERSHIP_REQUEST_ACCEPT_BUTTON: 'MEMBERSHIP_REQUEST_ACCEPT_BUTTON', + MEMBERSHIP_REQUEST_REJECT_BUTTON: 'MEMBERSHIP_REQUEST_REJECT_BUTTON', + MEMBERSHIP_REQUESTS_TABLE_EMPTY: 'MEMBERSHIP_REQUESTS_TABLE_EMPTY', + ENROLL_TITLE: 'ENROLL_TITLE', + ENROLL_BUTTON: 'ENROLL_BUTTON', + ENROLL_DESCRIPTION: 'ENROLL_DESCRIPTION', + REQUEST_ACCESS_PENDING_TITLE: 'REQUEST_ACCESS_PENDING_TITLE', + REQUEST_ACCESS_PENDING_DESCRIPTION: 'REQUEST_ACCESS_PENDING_DESCRIPTION', + REQUEST_ACCESS_TITLE: 'REQUEST_ACCESS_TITLE', + REQUEST_ACCESS_BUTTON: 'REQUEST_ACCESS_BUTTON', + REQUEST_ACCESS_SENT_BUTTON: 'REQUEST_ACCESS_SENT_BUTTON', + ACCESS_MANAGEMENT_TITLE: 'ACCESS_MANAGEMENT_TITLE', }; diff --git a/src/langs/de.json b/src/langs/de.json index 26eb55add..3f735ebb5 100644 --- a/src/langs/de.json +++ b/src/langs/de.json @@ -92,11 +92,7 @@ "IMPORT_ZIP_LIMITATIONS_TEXT": "Sie können eine ZIP-Datei von bis zu {{maxSize}} gleichzeitig hochladen. Im Falle eines Fehlers versuchen Sie bitte eine kleinere ZIP-Datei hochzuladen.", "IMPORT_ZIP_TITLE": "Importieren Sie ein Archiv", "IMPORT_ZIP_WARNING": "Sobald Ihre Datei akzeptiert wurde, dauert es einige Minuten, bis sie verfügbar ist.", - "INVITATIONS_TABLE_ACTIONS_HEADER": "Aktionen", "INVITATIONS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP": "Diese Einladung ist im übergeordneten Element definiert und kann hier nicht gelöscht werden.", - "INVITATIONS_TABLE_EMAIL_HEADER": "Email", - "INVITATIONS_TABLE_INVITATION_HEADER": "Einladung", - "INVITATIONS_TABLE_PERMISSION_HEADER": "Genehmigung", "ITEM_CATEGORIES_CONTAINER_TITLE": "Kategorie", "ITEM_CATEGORIES_CONTAINER_MISSING_WARNING": "Fügen Sie mindestens eine Kategorie hinzu, um das globale Thema Ihres Inhalts besser zu definieren", "ITEM_CATEGORIES_CONTAINER_EMPTY_BUTTON": "Kategorie hinzufügen", @@ -312,7 +308,6 @@ "SHARING_AUTHENTICATED_MEMBERS_EMPTY_MESSAGE": "Für dieses Element hat sich noch kein Benutzer authentifiziert.", "SHARING_AUTHENTICATED_MEMBERS_TITLE": "Authentifizierte Mitglieder", "SHARING_AUTHORIZED_MEMBERS_EMPTY_MESSAGE": "Kein Benutzer hat Zugriff auf dieses Element.", - "SHARING_AUTHORIZED_MEMBERS_TITLE": "Autorisierte Mitglieder", "SHARING_INVITATIONS_EMPTY_MESSAGE": "Noch keine Einladung für dieses Element.", "SHARING_INVITATIONS_RESEND_BUTTON": "Einladung erneut versenden", "SHARING_INVITATIONS_TITLE": "Ausstehende Einladungen", diff --git a/src/langs/en.json b/src/langs/en.json index 37a19c0b8..87343b612 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -93,11 +93,7 @@ "IMPORT_ZIP_LIMITATIONS_TEXT": "You can upload up to one ZIP of {{maxSize}} at a time. On error, try to upload a smaller zip.", "IMPORT_ZIP_TITLE": "Import an Archive", "IMPORT_ZIP_WARNING": "Once your file is accepted, it will take several minutes for it to be available.", - "INVITATIONS_TABLE_ACTIONS_HEADER": "Actions", "INVITATIONS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP": "This invitation is defined in the parent item and cannot be deleted here.", - "INVITATIONS_TABLE_EMAIL_HEADER": "Email", - "INVITATIONS_TABLE_INVITATION_HEADER": "Invitation", - "INVITATIONS_TABLE_PERMISSION_HEADER": "Permission", "ITEM_CATEGORIES_CONTAINER_TITLE": "Category", "ITEM_CATEGORIES_CONTAINER_MISSING_WARNING": "Add at least one category to better define the global theme of your content", "ITEM_CATEGORIES_CONTAINER_EMPTY_BUTTON": "Add a category", @@ -111,9 +107,9 @@ "ITEM_MEMBERSHIP_PERMISSION_LABEL": "Permission", "ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER": "Actions", "ITEM_MEMBERSHIPS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP": "This membership is defined in the parent item and cannot be deleted here.", - "ITEM_MEMBERSHIPS_TABLE_EMAIL_HEADER": "Email", - "ITEM_MEMBERSHIPS_TABLE_NAME_HEADER": "Name", "ITEM_MEMBERSHIPS_TABLE_PERMISSION_HEADER": "Permission", + "ITEM_MEMBERSHIPS_TABLE_MEMBER_HEADER": "Member", + "ITEM_MEMBERSHIPS_TABLE_STATUS_HEADER": "Status", "ITEM_MENU_CREATE_SHORTCUT_MENU_ITEM": "Create Shortcut", "ITEM_MENU_FLAG_MENU_ITEM": "Flag", "ITEM_METADATA_CREATED_AT_TITLE": "Creation", @@ -313,7 +309,6 @@ "SHARING_AUTHENTICATED_MEMBERS_EMPTY_MESSAGE": "No user has authenticated to this item yet.", "SHARING_AUTHENTICATED_MEMBERS_TITLE": "Authenticated Members", "SHARING_AUTHORIZED_MEMBERS_EMPTY_MESSAGE": "You are the only user with access to this item.", - "SHARING_AUTHORIZED_MEMBERS_TITLE": "Authorized Members", "SHARING_INVITATIONS_EMPTY_MESSAGE": "No pending invitations for this item.", "SHARING_INVITATIONS_RESEND_BUTTON": "Resend Invitation", "SHARING_INVITATIONS_TITLE": "Pending Invitations", @@ -475,5 +470,26 @@ "MEMBER_VALIDATION_DESCRIPTION": "In order to use all features of Graasp you need to validate your account email. You will find more information in the link below.", "MEMBER_VALIDATION_LINK_TEXT": "Learn more", "GUEST_LIMITATION_TEXT": "You are currently using Graasp with the guest account <1>{{name}}. In order to use all features of Graasp, you have to log out and create a Graasp account.", - "GUEST_SIGN_OUT_BUTTON": " Log out and Create a Graasp account" + "GUEST_SIGN_OUT_BUTTON": " Log out and Create a Graasp account", + "ITEM_LOGIN_HELPER_SIGN_OUT": "You're logged in as {{email}}", + "INVITATION_NOT_REGISTER_TEXT": "invitation pending", + "MEMBERSHIPS_REQUEST_TABLE_CREATED_AT_HEADER": "Sent on", + "USER_MANAGEMENT_MEMBERS_TAB": "Members", + "USER_MANAGEMENT_REQUESTS_TAB": "Requests", + "EDIT_PERMISSION_DIALOG_TITLE": "Update <1>{{name}}'s permission", + "EDIT_PERMISSION_DIALOG_SUBMIT_BUTTON": "Update", + "EDIT_PERMISSION_CANNOT_DOWNGRADE_FROM_PARENT": "Cannot downgrade permission if defined above.", + "INVITATION_DELETE_TOOLTIP": "Delete invitation", + "MEMBERSHIP_REQUEST_ACCEPT_BUTTON": "Accept", + "MEMBERSHIP_REQUEST_REJECT_BUTTON": "Reject", + "MEMBERSHIP_REQUESTS_TABLE_EMPTY": "No pending request.", + "ENROLL_TITLE": "Enroll to this item", + "ENROLL_BUTTON": "Enroll", + "ENROLL_DESCRIPTION": "Click on the button below to enroll and access this item.", + "REQUEST_ACCESS_PENDING_TITLE": "Your access request is pending", + "REQUEST_ACCESS_PENDING_DESCRIPTION": "An admin needs to approve your request for you to access this item.", + "REQUEST_ACCESS_TITLE": "Request access to this item", + "REQUEST_ACCESS_BUTTON": "Request access", + "REQUEST_ACCESS_SENT_BUTTON": "Request sent", + "ACCESS_MANAGEMENT_TITLE": "Access Management" } diff --git a/src/langs/es.json b/src/langs/es.json index 5b79a16f9..833316b34 100644 --- a/src/langs/es.json +++ b/src/langs/es.json @@ -91,11 +91,7 @@ "IMPORT_ZIP_LIMITATIONS_TEXT": "Puedes cargar hasta un ZIP de {{maxSize}} a la vez. En caso de error, intente cargar un zip más pequeño.", "IMPORT_ZIP_TITLE": "Importar un archivo", "IMPORT_ZIP_WARNING": "Una vez que su archivo sea aceptado, tardará varios minutos en estar disponible.", - "INVITATIONS_TABLE_ACTIONS_HEADER": "Comportamiento", "INVITATIONS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP": "Esta invitación se define en el elemento principal y no se puede eliminar aquí.", - "INVITATIONS_TABLE_EMAIL_HEADER": "Correo electrónico", - "INVITATIONS_TABLE_INVITATION_HEADER": "Invitación", - "INVITATIONS_TABLE_PERMISSION_HEADER": "Permiso", "ITEM_CATEGORIES_CONTAINER_TITLE": "Categoría", "ITEM_CATEGORIES_CONTAINER_MISSING_WARNING": "Añade al menos una categoría para definir mejor el tema global de tu contenido", "ITEM_CATEGORIES_CONTAINER_EMPTY_BUTTON": "Añadir una categoría", @@ -308,7 +304,6 @@ "SHARING_AUTHENTICATED_MEMBERS_EMPTY_MESSAGE": "Ningún usuario se ha autenticado en este elemento todavía.", "SHARING_AUTHENTICATED_MEMBERS_TITLE": "Miembros autenticados", "SHARING_AUTHORIZED_MEMBERS_EMPTY_MESSAGE": "Ningún usuario tiene acceso a este elemento.", - "SHARING_AUTHORIZED_MEMBERS_TITLE": "Miembros autorizados", "SHARING_INVITATIONS_EMPTY_MESSAGE": "Aún no hay invitación para este artículo.", "SHARING_INVITATIONS_RESEND_BUTTON": "Reenviar invitacíon", "SHARING_INVITATIONS_TITLE": "Invitaciones pendientes", diff --git a/src/langs/fr.json b/src/langs/fr.json index f77130976..3509fd134 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -92,11 +92,7 @@ "IMPORT_ZIP_LIMITATIONS_TEXT": "Vous pouvez télécharger un seul ZIP de {{maxSize}} à la fois. En cas d'erreur, essayez de télécharger un zip plus petit.", "IMPORT_ZIP_TITLE": "Importer une archive ZIP", "IMPORT_ZIP_WARNING": "Une fois que le fichier est accepté, cela prendra quelques minutes avant qu'il ne soit disponible.", - "INVITATIONS_TABLE_ACTIONS_HEADER": "Actions", "INVITATIONS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP": "Cette invitation est définie dans l'élément parent, et ne peut pas être supprimée ici.", - "INVITATIONS_TABLE_EMAIL_HEADER": "Email", - "INVITATIONS_TABLE_INVITATION_HEADER": "Invitation", - "INVITATIONS_TABLE_PERMISSION_HEADER": "Permission", "ITEM_CATEGORIES_CONTAINER_TITLE": "Catégories", "ITEM_CATEGORIES_CONTAINER_MISSING_WARNING": "Ajoutez au moins une catégorie pour mieux définir la thématique globale de votre contenu", "ITEM_CATEGORIES_CONTAINER_EMPTY_BUTTON": "Ajouter une catégorie", @@ -110,8 +106,8 @@ "ITEM_MEMBERSHIP_PERMISSION_LABEL": "Permission", "ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER": "Actions", "ITEM_MEMBERSHIPS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP": "Cette permission est définie dans l'élément parent, et ne peut pas être supprimée ici.", - "ITEM_MEMBERSHIPS_TABLE_EMAIL_HEADER": "Email", - "ITEM_MEMBERSHIPS_TABLE_NAME_HEADER": "Nom", + "ITEM_MEMBERSHIPS_TABLE_MEMBER_HEADER": "Membre", + "ITEM_MEMBERSHIPS_TABLE_STATUS_HEADER": "Statut", "ITEM_MEMBERSHIPS_TABLE_PERMISSION_HEADER": "Permission", "ITEM_MENU_CREATE_SHORTCUT_MENU_ITEM": "Créer un Raccourci", "ITEM_MENU_FLAG_MENU_ITEM": "Signaler", @@ -309,7 +305,6 @@ "SHARING_AUTHENTICATED_MEMBERS_EMPTY_MESSAGE": "Aucun utilisateur ne s'est encore connecté à cet élément.", "SHARING_AUTHENTICATED_MEMBERS_TITLE": "Membres Connectés", "SHARING_AUTHORIZED_MEMBERS_EMPTY_MESSAGE": "Aucun utilisateur ne peut accéder à cet élément.", - "SHARING_AUTHORIZED_MEMBERS_TITLE": "Membres Autorisés", "SHARING_INVITATIONS_EMPTY_MESSAGE": "Aucune invitation en attente pour cet élément.", "SHARING_INVITATIONS_RESEND_BUTTON": "Renvoyer l'Invitation", "SHARING_INVITATIONS_TITLE": "Invitations En Cours", @@ -469,5 +464,25 @@ "item.order": "Ordre", "MEMBER_VALIDATION_TITLE": "Votre adresse e-mail doit être vérifiée", "MEMBER_VALIDATION_DESCRIPTION": "Afin d'utiliser toutes les fonctionnalités de Graasp, vous devez valider l'e-mail de votre compte. Vous trouverez plus d’informations dans le lien ci-dessous.", - "MEMBER_VALIDATION_LINK_TEXT": "Plus de détails" + "MEMBER_VALIDATION_LINK_TEXT": "Plus de détails", + "INVITATION_NOT_REGISTER_TEXT": "invitation envoyée", + "MEMBERSHIPS_REQUEST_TABLE_CREATED_AT_HEADER": "Envoyé le", + "USER_MANAGEMENT_MEMBERS_TAB": "Membres", + "USER_MANAGEMENT_REQUESTS_TAB": "Requêtes", + "EDIT_PERMISSION_DIALOG_TITLE": "Modifier la permission de <1>{{name}}", + "EDIT_PERMISSION_DIALOG_SUBMIT_BUTTON": "Modifier", + "EDIT_PERMISSION_CANNOT_DOWNGRADE_FROM_PARENT": "Impossible de restreindre la permission si elle est définie plus haut.", + "INVITATION_DELETE_TOOLTIP": "Supprimer l'invitation", + "MEMBERSHIP_REQUEST_ACCEPT_BUTTON": "Accepter", + "MEMBERSHIP_REQUEST_REJECT_BUTTON": "Refuser", + "MEMBERSHIP_REQUESTS_TABLE_EMPTY": "Aucune requête en attente.", + "ENROLL_TITLE": "Rejoindre cet élément", + "ENROLL_BUTTON": "Rejoindre", + "ENROLL_DESCRIPTION": "Cliquez sur le bouton ci-dessous pour rejoindre et accéder à cet élément.", + "REQUEST_ACCESS_PENDING_TITLE": "Votre demande d'accès est en cours", + "REQUEST_ACCESS_PENDING_DESCRIPTION": "Un administrateur doit approuver votre requête pour que vous puissiez y accéder.", + "REQUEST_ACCESS_TITLE": "Demander l'accès à cet élément", + "REQUEST_ACCESS_BUTTON": "Demander l'accès", + "REQUEST_ACCESS_SENT_BUTTON": "Demande envoyée", + "ACCESS_MANAGEMENT_TITLE": "Gestion des accès" } diff --git a/src/langs/it.json b/src/langs/it.json index 1d6fb4e93..3f0eb458a 100644 --- a/src/langs/it.json +++ b/src/langs/it.json @@ -91,11 +91,7 @@ "IMPORT_ZIP_LIMITATIONS_TEXT": "Puoi caricare fino a un totale ZIP di {{maxSize}} alla volta. In caso di errore, prova a caricare un file zip più piccolo.", "IMPORT_ZIP_TITLE": "Importa un archivio", "IMPORT_ZIP_WARNING": "Una volta accettato il file, saranno necessari alcuni minuti prima che sia disponibile.", - "INVITATIONS_TABLE_ACTIONS_HEADER": "Azioni", "INVITATIONS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP": "Questo invito è definito nell'elemento principale e non può essere eliminato qui", - "INVITATIONS_TABLE_EMAIL_HEADER": "E-mail", - "INVITATIONS_TABLE_INVITATION_HEADER": "Invito", - "INVITATIONS_TABLE_PERMISSION_HEADER": "Autorizzazione", "ITEM_CATEGORIES_CONTAINER_TITLE": "Categoria", "ITEM_CATEGORIES_CONTAINER_MISSING_WARNING": "Aggiungi almeno una categoria per definire meglio il tema globale dei tuoi contenuti", "ITEM_CATEGORIES_CONTAINER_EMPTY_BUTTON": "Aggiungi una categoria", @@ -308,7 +304,6 @@ "SHARING_AUTHENTICATED_MEMBERS_EMPTY_MESSAGE": "Nessun utente si è ancora autenticato per questo elemento.", "SHARING_AUTHENTICATED_MEMBERS_TITLE": "Membri autenticati", "SHARING_AUTHORIZED_MEMBERS_EMPTY_MESSAGE": "Nessun utente ha accesso a questo elemento.", - "SHARING_AUTHORIZED_MEMBERS_TITLE": "Membri autorizzati", "SHARING_INVITATIONS_EMPTY_MESSAGE": "Ancora nessun invito per questo elemento.", "SHARING_INVITATIONS_RESEND_BUTTON": "Invia nuovamente l'invito", "SHARING_INVITATIONS_TITLE": "Inviti in sospeso", diff --git a/src/utils/member.ts b/src/utils/member.ts index b74f3f555..6c5bd04b8 100644 --- a/src/utils/member.ts +++ b/src/utils/member.ts @@ -1,6 +1,17 @@ -import { Member } from '@graasp/sdk'; +import { AccountType, CurrentAccount, Member } from '@graasp/sdk'; +import { DEFAULT_LANG } from '@graasp/translations'; export const getMemberById = ( members: Member[], id: string, ): Member | undefined => members.find(({ id: thisId }) => id === thisId); + +export const getCurrentAccountLang = ( + account: CurrentAccount | null | undefined, + // eslint-disable-next-line arrow-body-style +): string | undefined => { + if (account?.type === AccountType.Individual) { + return account.extra.lang; + } + return DEFAULT_LANG; +}; diff --git a/yarn.lock b/yarn.lock index 215e7734d..054f0f179 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41,7 +41,17 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.24.1, @babel/code-frame@npm:^7.24.2": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/code-frame@npm:7.24.7" + dependencies: + "@babel/highlight": "npm:^7.24.7" + picocolors: "npm:^1.0.0" + checksum: 10/4812e94885ba7e3213d49583a155fdffb05292330f0a9b2c41b49288da70cf3c746a3fda0bf1074041a6d741c33f8d7be24be5e96f41ef77395eeddc5c9ff624 + languageName: node + linkType: hard + +"@babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.24.1, @babel/code-frame@npm:^7.24.2": version: 7.24.2 resolution: "@babel/code-frame@npm:7.24.2" dependencies: @@ -162,7 +172,19 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.23.0, @babel/generator@npm:^7.24.1": +"@babel/generator@npm:^7.23.0, @babel/generator@npm:^7.25.6": + version: 7.25.6 + resolution: "@babel/generator@npm:7.25.6" + dependencies: + "@babel/types": "npm:^7.25.6" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^2.5.1" + checksum: 10/541e4fbb6ea7806f44232d70f25bf09dee9a57fe43d559e375536870ca5261ebb4647fec3af40dcbb3325ea2a49aff040e12a4e6f88609eaa88f10c4e27e31f8 + languageName: node + linkType: hard + +"@babel/generator@npm:^7.24.1": version: 7.24.4 resolution: "@babel/generator@npm:7.24.4" dependencies: @@ -277,7 +299,17 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15": +"@babel/helper-module-imports@npm:^7.16.7": + version: 7.24.7 + resolution: "@babel/helper-module-imports@npm:7.24.7" + dependencies: + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10/df8bfb2bb18413aa151ecd63b7d5deb0eec102f924f9de6bc08022ced7ed8ca7fed914562d2f6fa5b59b74a5d6e255dc35612b2bc3b8abf361e13f61b3704770 + languageName: node + linkType: hard + +"@babel/helper-module-imports@npm:^7.22.15": version: 7.24.3 resolution: "@babel/helper-module-imports@npm:7.24.3" dependencies: @@ -404,7 +436,21 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.16.7, @babel/helper-validator-identifier@npm:^7.22.20": +"@babel/helper-string-parser@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-string-parser@npm:7.24.8" + checksum: 10/6d1bf8f27dd725ce02bdc6dffca3c95fb9ab8a06adc2edbd9c1c9d68500274230d1a609025833ed81981eff560045b6b38f7b4c6fb1ab19fc90e5004e3932535 + languageName: node + linkType: hard + +"@babel/helper-validator-identifier@npm:^7.16.7, @babel/helper-validator-identifier@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-validator-identifier@npm:7.24.7" + checksum: 10/86875063f57361471b531dbc2ea10bbf5406e12b06d249b03827d361db4cad2388c6f00936bcd9dc86479f7e2c69ea21412c2228d4b3672588b754b70a449d4b + languageName: node + linkType: hard + +"@babel/helper-validator-identifier@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-validator-identifier@npm:7.22.20" checksum: 10/df882d2675101df2d507b95b195ca2f86a3ef28cb711c84f37e79ca23178e13b9f0d8b522774211f51e40168bf5142be4c1c9776a150cddb61a0d5bf3e95750b @@ -495,12 +541,26 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.5, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.1": - version: 7.24.1 - resolution: "@babel/parser@npm:7.24.1" +"@babel/highlight@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/highlight@npm:7.24.7" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.24.7" + chalk: "npm:^2.4.2" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.0.0" + checksum: 10/69b73f38cdd4f881b09b939a711e76646da34f4834f4ce141d7a49a6bb1926eab1c594148970a8aa9360398dff800f63aade4e81fafdd7c8d8a8489ea93bfec1 + languageName: node + linkType: hard + +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.5, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.25.0, @babel/parser@npm:^7.25.6": + version: 7.25.6 + resolution: "@babel/parser@npm:7.25.6" + dependencies: + "@babel/types": "npm:^7.25.6" bin: parser: ./bin/babel-parser.js - checksum: 10/561d9454091e07ecfec3828ce79204c0fc9d24e17763f36181c6984392be4ca6b79c8225f2224fdb7b1b3b70940e243368c8f83ac77ec2dc20f46d3d06bd6795 + checksum: 10/830aab72116aa14eb8d61bfa8f9d69fc8f3a43d909ce993cb4350ae14d3af1a2f740a54410a22d821c48a253263643dfecbc094f9608e6a70ce9ff3c0bbfe91a languageName: node linkType: hard @@ -513,6 +573,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/parser@npm:7.24.1" + bin: + parser: ./bin/babel-parser.js + checksum: 10/561d9454091e07ecfec3828ce79204c0fc9d24e17763f36181c6984392be4ca6b79c8225f2224fdb7b1b3b70940e243368c8f83ac77ec2dc20f46d3d06bd6795 + languageName: node + linkType: hard + "@babel/parser@npm:^7.24.6": version: 7.24.6 resolution: "@babel/parser@npm:7.24.6" @@ -602,6 +671,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.25.0": + version: 7.25.0 + resolution: "@babel/template@npm:7.25.0" + dependencies: + "@babel/code-frame": "npm:^7.24.7" + "@babel/parser": "npm:^7.25.0" + "@babel/types": "npm:^7.25.0" + checksum: 10/07ebecf6db8b28244b7397628e09c99e7a317b959b926d90455c7253c88df3677a5a32d1501d9749fe292a263ff51a4b6b5385bcabd5dadd3a48036f4d4949e0 + languageName: node + linkType: hard + "@babel/traverse@npm:7.23.2": version: 7.23.2 resolution: "@babel/traverse@npm:7.23.2" @@ -656,6 +736,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.24.7": + version: 7.25.6 + resolution: "@babel/traverse@npm:7.25.6" + dependencies: + "@babel/code-frame": "npm:^7.24.7" + "@babel/generator": "npm:^7.25.6" + "@babel/parser": "npm:^7.25.6" + "@babel/template": "npm:^7.25.0" + "@babel/types": "npm:^7.25.6" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10/de75a918299bc27a44ec973e3f2fa8c7902bbd67bd5d39a0be656f3c1127f33ebc79c12696fbc8170a0b0e1072a966d4a2126578d7ea2e241b0aeb5d16edc738 + languageName: node + linkType: hard + "@babel/types@npm:7.17.0": version: 7.17.0 resolution: "@babel/types@npm:7.17.0" @@ -699,6 +794,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.24.7, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.6": + version: 7.25.6 + resolution: "@babel/types@npm:7.25.6" + dependencies: + "@babel/helper-string-parser": "npm:^7.24.8" + "@babel/helper-validator-identifier": "npm:^7.24.7" + to-fast-properties: "npm:^2.0.0" + checksum: 10/7b54665e1b51f525fe0f451efdd9fe7a4a6dfba3fd4956c3530bc77336b66ffe3d78c093796ed044119b5d213176af7cf326f317a2057c538d575c6cefcb3562 + languageName: node + linkType: hard + "@colors/colors@npm:1.5.0": version: 1.5.0 resolution: "@colors/colors@npm:1.5.0" @@ -1672,9 +1778,9 @@ __metadata: languageName: node linkType: hard -"@graasp/query-client@npm:3.22.4": - version: 3.22.4 - resolution: "@graasp/query-client@npm:3.22.4" +"@graasp/query-client@npm:3.24.0": + version: 3.24.0 + resolution: "@graasp/query-client@npm:3.24.0" dependencies: "@tanstack/react-query": "npm:4.36.1" "@tanstack/react-query-devtools": "npm:4.36.1" @@ -1684,7 +1790,7 @@ __metadata: "@graasp/sdk": ^4.0.0 "@graasp/translations": "*" react: ^18.0.0 - checksum: 10/e4c5958141a55aa41f5a68fc39b8cc3ac2bf123a47f8b7a4c8c05836274f962082227353ed1ad93c10ec1b25a10691eb29bcd72c31f38f7243bae2cdfd7fd250 + checksum: 10/06feb022dfd7094f4e9e680acc828b1daf0d1fbf6efe8b3f1ed99e13f28ac0a17abe255bd317cb00efd22c66cea80e02a56307e82574f53df8939c53c2e10d33 languageName: node linkType: hard @@ -1714,18 +1820,18 @@ __metadata: languageName: node linkType: hard -"@graasp/translations@npm:1.37.0": - version: 1.37.0 - resolution: "@graasp/translations@npm:1.37.0" +"@graasp/translations@npm:1.38.0": + version: 1.38.0 + resolution: "@graasp/translations@npm:1.38.0" peerDependencies: i18next: ^23.8.1 - checksum: 10/a1026d4098cc1a84ced835352fd0672a305cdebe69562d2b885d5ac9bee0cbcfe2894b943855e65b74d464d031e72feaab5296d604e182abe1adf1edc731f791 + checksum: 10/4ba3ce113c2df86d1fe7a8c379a23ae015d6ada7f5bcafa96b53212e1016af6ba92679235fbaafcb0b074ddf7f535510573a3a7c944af89eb5d46eb9bfe64c08 languageName: node linkType: hard -"@graasp/ui@npm:5.1.0": - version: 5.1.0 - resolution: "@graasp/ui@npm:5.1.0" +"@graasp/ui@npm:5.2.0": + version: 5.2.0 + resolution: "@graasp/ui@npm:5.2.0" dependencies: http-status-codes: "npm:2.3.0" interweave: "npm:13.1.0" @@ -1754,7 +1860,7 @@ __metadata: react-i18next: ^13.0.0 || ^14.0.0 || ^15.0.0 react-router-dom: ^6.11.0 stylis: ^4.1.3 - checksum: 10/fcab9f3d021848e81f8779be9508a6e6efcae249a364611e4adf888410658d83dd1ecfe8b0fbcda26c3bd3f840b44e6efe73cd2e3f9af53917c4510499a3b37a + checksum: 10/b98fc515f4f2bc7d2d1b800404cbfb480485e178db8e79c5681d10c5d152669bbffcbd0cdcf044d04b7b39856c64a10d0478f40d072213a23eb8d1fef0120853 languageName: node linkType: hard @@ -4239,14 +4345,7 @@ __metadata: languageName: node linkType: hard -"clsx@npm:^2.0.0, clsx@npm:^2.1.0": - version: 2.1.0 - resolution: "clsx@npm:2.1.0" - checksum: 10/2e0ce7c3b6803d74fc8147c408f88e79245583202ac14abd9691e2aebb9f312de44270b79154320d10bb7804a9197869635d1291741084826cff20820f31542b - languageName: node - linkType: hard - -"clsx@npm:^2.1.1": +"clsx@npm:^2.0.0, clsx@npm:^2.1.0, clsx@npm:^2.1.1": version: 2.1.1 resolution: "clsx@npm:2.1.1" checksum: 10/cdfb57fa6c7649bbff98d9028c2f0de2f91c86f551179541cf784b1cfdc1562dcb951955f46d54d930a3879931a980e32a46b598acaea274728dbe068deca919 @@ -5449,9 +5548,9 @@ __metadata: linkType: hard "escalade@npm:^3.1.1": - version: 3.1.2 - resolution: "escalade@npm:3.1.2" - checksum: 10/a1e07fea2f15663c30e40b9193d658397846ffe28ce0a3e4da0d8e485fedfeca228ab846aee101a05015829adf39f9934ff45b2a3fca47bed37a29646bd05cd3 + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 10/9d7169e3965b2f9ae46971afa392f6e5a25545ea30f2e2dd99c9b0a95a3f52b5653681a84f5b2911a413ddad2d7a93d3514165072f349b5ffc59c75a899970d6 languageName: node linkType: hard @@ -6497,11 +6596,11 @@ __metadata: "@emotion/styled": "npm:11.13.0" "@graasp/chatbox": "npm:3.3.0" "@graasp/map": "npm:1.18.0" - "@graasp/query-client": "npm:3.22.4" + "@graasp/query-client": "npm:3.24.0" "@graasp/sdk": "npm:4.29.1" "@graasp/stylis-plugin-rtl": "npm:2.2.0" - "@graasp/translations": "npm:1.37.0" - "@graasp/ui": "npm:5.1.0" + "@graasp/translations": "npm:1.38.0" + "@graasp/ui": "npm:5.2.0" "@mui/icons-material": "npm:5.16.4" "@mui/lab": "npm:5.0.0-alpha.172" "@mui/material": "npm:5.16.4" @@ -8833,7 +8932,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.1, minimatch@npm:^9.0.4": +"minimatch@npm:^9.0.1": version: 9.0.4 resolution: "minimatch@npm:9.0.4" dependencies: @@ -8842,6 +8941,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^9.0.4": + version: 9.0.5 + resolution: "minimatch@npm:9.0.5" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10/dd6a8927b063aca6d910b119e1f2df6d2ce7d36eab91de83167dd136bb85e1ebff97b0d3de1cb08bd1f7e018ca170b4962479fefab5b2a69e2ae12cb2edc8348 + languageName: node + linkType: hard + "minimist@npm:^1.2.0, minimist@npm:^1.2.6, minimist@npm:^1.2.8": version: 1.2.8 resolution: "minimist@npm:1.2.8" @@ -9924,20 +10032,20 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^18.0.0, react-is@npm:^18.2.0": - version: 18.2.0 - resolution: "react-is@npm:18.2.0" - checksum: 10/200cd65bf2e0be7ba6055f647091b725a45dd2a6abef03bf2380ce701fd5edccee40b49b9d15edab7ac08a762bf83cb4081e31ec2673a5bfb549a36ba21570df - languageName: node - linkType: hard - -"react-is@npm:^18.3.1": +"react-is@npm:^18.0.0, react-is@npm:^18.3.1": version: 18.3.1 resolution: "react-is@npm:18.3.1" checksum: 10/d5f60c87d285af24b1e1e7eaeb123ec256c3c8bdea7061ab3932e3e14685708221bf234ec50b21e10dd07f008f1b966a2730a0ce4ff67905b3872ff2042aec22 languageName: node linkType: hard +"react-is@npm:^18.2.0": + version: 18.2.0 + resolution: "react-is@npm:18.2.0" + checksum: 10/200cd65bf2e0be7ba6055f647091b725a45dd2a6abef03bf2380ce701fd5edccee40b49b9d15edab7ac08a762bf83cb4081e31ec2673a5bfb549a36ba21570df + languageName: node + linkType: hard + "react-leaflet-cluster@npm:2.1.0": version: 2.1.0 resolution: "react-leaflet-cluster@npm:2.1.0"