From 5d2c0b0111c06cb4ab65169ed9d44c394558541b Mon Sep 17 00:00:00 2001 From: Kim Lan Phan Hoang Date: Fri, 27 Sep 2024 09:36:59 +0200 Subject: [PATCH] feat: disable guest row in membership table (#1479) * feat: disable guest row in membership table * refactor: update put item login schema instead of delete * refactor: update sdk * refactor: update query client * refactor: fix tests * refactor: apply PR requested changes --- .../e2e/item/authorization/itemLogin/utils.ts | 16 +- cypress/e2e/item/share/changeVisibility.cy.ts | 16 +- cypress/e2e/memberships/viewMemberships.cy.ts | 174 +++++++++++++++++- cypress/support/commands.ts | 3 - cypress/support/server.ts | 20 -- cypress/support/types.ts | 4 +- package.json | 4 +- src/components/hooks/useVisibility.tsx | 19 +- .../item/sharing/ItemSharingTab.tsx | 21 ++- .../GuestItemMembershipTableRow.tsx | 72 ++++++-- .../membershipTable/ItemMembershipsTable.tsx | 10 +- src/langs/constants.ts | 2 + src/langs/en.json | 3 +- vite.config.ts | 7 + yarn.lock | 20 +- 15 files changed, 310 insertions(+), 81 deletions(-) diff --git a/cypress/e2e/item/authorization/itemLogin/utils.ts b/cypress/e2e/item/authorization/itemLogin/utils.ts index fd6862d0b..e6c55747b 100644 --- a/cypress/e2e/item/authorization/itemLogin/utils.ts +++ b/cypress/e2e/item/authorization/itemLogin/utils.ts @@ -1,17 +1,17 @@ -import { ItemLoginSchema, ItemLoginSchemaType, PackedItem } from '@graasp/sdk'; - -import { v4 } from 'uuid'; +import { + ItemLoginSchema, + ItemLoginSchemaFactory, + ItemLoginSchemaType, + PackedItem, +} from '@graasp/sdk'; export const addItemLoginSchema = ( item: PackedItem, itemLoginSchemaType: ItemLoginSchemaType, ): PackedItem & { itemLoginSchema: ItemLoginSchema } => ({ ...item, - itemLoginSchema: { + itemLoginSchema: ItemLoginSchemaFactory({ 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/share/changeVisibility.cy.ts b/cypress/e2e/item/share/changeVisibility.cy.ts index 57a891919..bad7ef205 100644 --- a/cypress/e2e/item/share/changeVisibility.cy.ts +++ b/cypress/e2e/item/share/changeVisibility.cy.ts @@ -1,4 +1,5 @@ import { + ItemLoginSchemaStatus, ItemLoginSchemaType, ItemTagType, PackedFolderItemFactory, @@ -91,6 +92,12 @@ describe('Visibility of an Item', () => { } = data[0]; expect(url).to.contain(item.id); expect(url).to.contain(ItemTagType.Public); // originally item login + + const { + request: { body, url: itemLoginUrl }, + } = data[1]; + expect(itemLoginUrl).to.contain(item.id); + expect(body.status).to.contain(ItemLoginSchemaStatus.Active); }); }); @@ -125,8 +132,9 @@ describe('Visibility of an Item', () => { // change pseudonymized -> private changeVisibility(SETTINGS.ITEM_PRIVATE.name); - cy.wait(`@deleteItemLoginSchema`).then(({ request: { url } }) => { + cy.wait(`@putItemLoginSchema`).then(({ request: { url, body } }) => { expect(url).to.include(item.id); + expect(body.status).to.eq(ItemLoginSchemaStatus.Disabled); }); }); @@ -193,6 +201,12 @@ describe('Visibility of an Item', () => { } = data[0]; expect(url).to.contain(item.id); expect(url).to.contain(ItemTagType.Public); // originally item login + + const { + request: { url: itemLoginUrl, body }, + } = data[1]; + expect(itemLoginUrl).to.contain(item.id); // originally item login + expect(body.status).to.eq(ItemLoginSchemaStatus.Active); }); }); }); diff --git a/cypress/e2e/memberships/viewMemberships.cy.ts b/cypress/e2e/memberships/viewMemberships.cy.ts index 41164f5a4..b750eae9c 100644 --- a/cypress/e2e/memberships/viewMemberships.cy.ts +++ b/cypress/e2e/memberships/viewMemberships.cy.ts @@ -1,9 +1,17 @@ -import { Member, PackedFolderItemFactory, PermissionLevel } from '@graasp/sdk'; +import { + GuestFactory, + ItemLoginSchemaFactory, + ItemLoginSchemaStatus, + ItemLoginSchemaType, + Member, + PackedFolderItemFactory, + PermissionLevel, +} from '@graasp/sdk'; import { namespaces } from '@graasp/translations'; import i18n from '@/config/i18n'; -import { buildItemPath } from '../../../src/config/paths'; +import { buildItemPath, buildItemSharePath } from '../../../src/config/paths'; import { buildDataCyWrapper, buildItemMembershipRowDeleteButtonId, @@ -43,7 +51,7 @@ const membershipsWithoutAdmin = [ }), ]; -describe('View Memberships', () => { +describe('View Memberships - Individual', () => { beforeEach(() => { cy.setUpApi({ items: [ @@ -85,6 +93,166 @@ describe('View Memberships', () => { }); }); +describe('View Memberships - Guest', () => { + it('view guest membership', () => { + const itemLoginSchema = ItemLoginSchemaFactory({ + type: ItemLoginSchemaType.Username, + item: itemWithAdmin, + }); + const guestMemberships = [ + buildItemMembership({ + item: itemWithAdmin, + permission: PermissionLevel.Read, + account: GuestFactory({ + itemLoginSchema, + }), + creator: MEMBERS.ANNA, + }), + buildItemMembership({ + item: itemWithAdmin, + permission: PermissionLevel.Read, + account: GuestFactory({ + itemLoginSchema, + }), + creator: MEMBERS.ANNA, + }), + ]; + const item = itemWithAdmin; + cy.setUpApi({ + items: [ + { + ...itemWithAdmin, + itemLoginSchema, + memberships: [adminMembership, ...guestMemberships], + }, + ], + }); + i18n.changeLanguage(CURRENT_USER.extra.lang); + cy.visit(buildItemSharePath(item.id)); + // editable rows + for (const { permission, account, id } of guestMemberships) { + const { name } = Object.values( + guestMemberships.map((m) => m.account), + ).find(({ id: mId }) => mId === account.id); + + // check name and disabled permission + cy.get(buildDataCyWrapper(buildItemMembershipRowId(id))) + .should('contain', name) + .should('contain', i18n.t(permission, { ns: namespaces.enums })); + + // check delete button exists + cy.get(`#${buildItemMembershipRowDeleteButtonId(id)}`).should('exist'); + } + }); + it('view frozen guest membership', () => { + const itemLoginSchema = ItemLoginSchemaFactory({ + type: ItemLoginSchemaType.Username, + item: itemWithAdmin, + status: ItemLoginSchemaStatus.Freeze, + }); + const guestMemberships = [ + buildItemMembership({ + item: itemWithAdmin, + permission: PermissionLevel.Read, + account: GuestFactory({ + itemLoginSchema, + }), + creator: MEMBERS.ANNA, + }), + buildItemMembership({ + item: itemWithAdmin, + permission: PermissionLevel.Read, + account: GuestFactory({ + itemLoginSchema, + }), + creator: MEMBERS.ANNA, + }), + ]; + const item = itemWithAdmin; + cy.setUpApi({ + items: [ + { + ...itemWithAdmin, + itemLoginSchema, + memberships: [adminMembership, ...guestMemberships], + }, + ], + }); + i18n.changeLanguage(CURRENT_USER.extra.lang); + cy.visit(buildItemSharePath(item.id)); + // editable rows + for (const { permission, account, id } of guestMemberships) { + const { name } = Object.values( + guestMemberships.map((m) => m.account), + ).find(({ id: mId }) => mId === account.id); + + // check name and disabled permission + cy.get(buildDataCyWrapper(buildItemMembershipRowId(id))) + .should('contain', name) + .should('contain', i18n.t(permission, { ns: namespaces.enums })); + + // check delete button exists + cy.get(`#${buildItemMembershipRowDeleteButtonId(id)}`).should('exist'); + } + }); + + it('view disabled guest membership', () => { + const itemLoginSchema = ItemLoginSchemaFactory({ + type: ItemLoginSchemaType.Username, + item: itemWithAdmin, + status: ItemLoginSchemaStatus.Disabled, + }); + const guestMemberships = [ + buildItemMembership({ + item: itemWithAdmin, + permission: PermissionLevel.Read, + account: GuestFactory({ + itemLoginSchema, + }), + creator: MEMBERS.ANNA, + }), + buildItemMembership({ + item: itemWithAdmin, + permission: PermissionLevel.Read, + account: GuestFactory({ + itemLoginSchema, + }), + creator: MEMBERS.ANNA, + }), + ]; + const item = itemWithAdmin; + cy.setUpApi({ + items: [ + { + ...itemWithAdmin, + itemLoginSchema, + memberships: [adminMembership, ...guestMemberships], + }, + ], + }); + i18n.changeLanguage(CURRENT_USER.extra.lang); + cy.visit(buildItemSharePath(item.id)); + // editable rows + for (const { permission, account, id } of guestMemberships) { + const { name } = Object.values( + guestMemberships.map((m) => m.account), + ).find(({ id: mId }) => mId === account.id); + + // check name and disabled permission + cy.get(buildDataCyWrapper(buildItemMembershipRowId(id))) + .should('contain', name) + .should('not.contain', permission) + .should( + 'contain', + i18n.t(ItemLoginSchemaStatus.Disabled, { ns: namespaces.enums }), + ); + + // check delete button exists + cy.get(`#${buildItemMembershipRowDeleteButtonId(id)}`).should('exist'); + } + }); +}); + describe('View Memberships Read-Only Mode', () => { it('view membership in settings read-only mode', () => { const item = PackedFolderItemFactory( diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 1f5e85b13..312ad0071 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -22,7 +22,6 @@ import { mockDeleteFavorite, mockDeleteInvitation, mockDeleteItemCategory, - mockDeleteItemLoginSchemaRoute, mockDeleteItemMembershipForItem, mockDeleteItemTag, mockDeleteItemThumbnail, @@ -337,8 +336,6 @@ Cypress.Commands.add( mockUpdatePassword(members, updatePasswordError); - mockDeleteItemLoginSchemaRoute(items); - mockGetItemFavorites(bookmarkedItems, getFavoriteError); mockAddFavorite(cachedItems, addFavoriteError); diff --git a/cypress/support/server.ts b/cypress/support/server.ts index 5f85a3941..917676621 100644 --- a/cypress/support/server.ts +++ b/cypress/support/server.ts @@ -948,26 +948,6 @@ export const mockPostItemLogin = ( ).as('postItemLogin'); }; -export const mockDeleteItemLoginSchemaRoute = (items: ItemForTest[]): void => { - cy.intercept( - { - method: HttpMethod.Delete, - // TODO: use build url - url: new RegExp(`${API_HOST}/items/${ID_FORMAT}/login-schema$`), - }, - ({ reply, url }) => { - // check query match item login schema - const id = url.slice(API_HOST.length).split('/')[2]; - const item: ItemForTest = getItemById(items, id); - - // TODO: item login is not in extra anymore - item.itemLoginSchema = null; - - reply(item); - }, - ).as('deleteItemLoginSchema'); -}; - export const mockPutItemLoginSchema = ( items: ItemForTest[], shouldThrowError: boolean, diff --git a/cypress/support/types.ts b/cypress/support/types.ts index 8ebd476f8..2eb25bc14 100644 --- a/cypress/support/types.ts +++ b/cypress/support/types.ts @@ -14,6 +14,7 @@ import { ItemTag, ItemValidationGroup, LocalFileItemType, + PackedItem, PermissionLevel, PublicationStatus, RecycledItemData, @@ -23,7 +24,8 @@ import { export type ItemForTest = DiscriminatedItem & { categories?: ItemCategory[]; - thumbnails?: string; + // TODO: INCORRECT! Fix in coming + thumbnails?: PackedItem['thumbnails']; tags?: ItemTag[]; itemLoginSchema?: ItemLoginSchema; readFilepath?: string; diff --git a/package.json b/package.json index 57ae03d69..6eaaf72f5 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "@emotion/styled": "11.13.0", "@graasp/chatbox": "3.3.0", "@graasp/map": "1.18.0", - "@graasp/query-client": "3.24.0", - "@graasp/sdk": "4.29.1", + "@graasp/query-client": "3.25.0", + "@graasp/sdk": "4.31.0", "@graasp/stylis-plugin-rtl": "2.2.0", "@graasp/translations": "1.38.0", "@graasp/ui": "5.2.0", diff --git a/src/components/hooks/useVisibility.tsx b/src/components/hooks/useVisibility.tsx index 8331d4e85..8366ea67c 100644 --- a/src/components/hooks/useVisibility.tsx +++ b/src/components/hooks/useVisibility.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react'; import { ItemLoginSchema, + ItemLoginSchemaStatus, ItemLoginSchemaType, ItemPublished, ItemTag, @@ -17,7 +18,6 @@ const { useDeleteItemTag, usePostItemTag, useUnpublishItem, - useDeleteItemLoginSchema, usePutItemLoginSchema, } = mutations; @@ -47,7 +47,6 @@ export const useVisibility = (item: PackedItem): UseVisibility => { // item login tag and item extra value const { data: itemLoginSchema, isLoading: isItemLoginLoading } = useItemLoginSchema({ itemId: item.id }); - const { mutate: deleteItemLoginSchema } = useDeleteItemLoginSchema(); const { mutate: putItemLoginSchema } = usePutItemLoginSchema(); // is disabled @@ -79,7 +78,10 @@ export const useVisibility = (item: PackedItem): UseVisibility => { setVisibility(SETTINGS.ITEM_PUBLIC.name); break; } - case Boolean(itemLoginSchema?.id): { + case Boolean( + itemLoginSchema?.id && + itemLoginSchema.status !== ItemLoginSchemaStatus.Disabled, + ): { setVisibility(SETTINGS.ITEM_LOGIN.name); break; } @@ -101,10 +103,11 @@ export const useVisibility = (item: PackedItem): UseVisibility => { } }; - const deleteLoginSchema = () => { + const disableLoginSchema = () => { if (itemLoginSchema) { - deleteItemLoginSchema({ + putItemLoginSchema({ itemId: item.id, + status: ItemLoginSchemaStatus.Disabled, }); } }; @@ -112,7 +115,7 @@ export const useVisibility = (item: PackedItem): UseVisibility => { switch (newTag) { case SETTINGS.ITEM_PRIVATE.name: { deletePublishedAndPublic(); - deleteLoginSchema(); + disableLoginSchema(); break; } case SETTINGS.ITEM_LOGIN.name: { @@ -120,6 +123,7 @@ export const useVisibility = (item: PackedItem): UseVisibility => { putItemLoginSchema({ itemId: item.id, type: ItemLoginSchemaType.Username, + status: ItemLoginSchemaStatus.Active, }); break; } @@ -128,7 +132,7 @@ export const useVisibility = (item: PackedItem): UseVisibility => { itemId: item.id, type: ItemTagType.Public, }); - deleteLoginSchema(); + disableLoginSchema(); break; } default: @@ -136,7 +140,6 @@ export const useVisibility = (item: PackedItem): UseVisibility => { } }, [ - deleteItemLoginSchema, deleteItemTag, item.id, itemLoginSchema, diff --git a/src/components/item/sharing/ItemSharingTab.tsx b/src/components/item/sharing/ItemSharingTab.tsx index 55448eeee..088c46fc7 100644 --- a/src/components/item/sharing/ItemSharingTab.tsx +++ b/src/components/item/sharing/ItemSharingTab.tsx @@ -2,6 +2,8 @@ import { useOutletContext } from 'react-router-dom'; import { Box, Container, Divider, Stack, Typography } from '@mui/material'; +import { AccountType } from '@graasp/sdk'; + import { OutletType } from '@/components/pages/item/type'; import { useBuilderTranslation } from '../../../config/i18n'; @@ -15,6 +17,7 @@ const ItemSharingTab = (): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); const { item, canAdmin } = useOutletContext(); + const { data: currentAccount } = hooks.useCurrentMember(); const { data: memberships } = hooks.useItemMemberships(item?.id); return ( @@ -30,13 +33,17 @@ const ItemSharingTab = (): JSX.Element => { /> - - - {translateBuilder(BUILDER.ITEM_SETTINGS_VISIBILITY_TITLE)} - - - - + {currentAccount?.type === AccountType.Individual ? ( + <> + + + {translateBuilder(BUILDER.ITEM_SETTINGS_VISIBILITY_TITLE)} + + + + + + ) : null} diff --git a/src/components/item/sharing/membershipTable/GuestItemMembershipTableRow.tsx b/src/components/item/sharing/membershipTable/GuestItemMembershipTableRow.tsx index afa29306c..f869ac24b 100644 --- a/src/components/item/sharing/membershipTable/GuestItemMembershipTableRow.tsx +++ b/src/components/item/sharing/membershipTable/GuestItemMembershipTableRow.tsx @@ -1,36 +1,76 @@ -import { TableCell, Typography } from '@mui/material'; +import { TableCell, Tooltip, Typography } from '@mui/material'; -import { DiscriminatedItem, ItemMembership } from '@graasp/sdk'; +import { + DiscriminatedItem, + ItemLoginSchema, + ItemLoginSchemaStatus, + ItemMembership, +} from '@graasp/sdk'; -import { useEnumsTranslation } from '@/config/i18n'; +import { useBuilderTranslation, useEnumsTranslation } from '@/config/i18n'; +import { hooks } from '@/config/queryClient'; +import { BUILDER } from '@/langs/constants'; import { buildItemMembershipRowId } from '../../../../config/selectors'; import DeleteItemMembershipButton from './DeleteItemMembershipButton'; import { StyledTableRow } from './StyledTableRow'; +const DISABLED_COLOR = '#a5a5a5'; + const GuestItemMembershipTableRow = ({ data, itemId, + itemLoginSchema, }: { data: ItemMembership; itemId: DiscriminatedItem['id']; + itemLoginSchema?: ItemLoginSchema; }): JSX.Element => { const { t: translateEnums } = useEnumsTranslation(); + const { t: translateBuilder } = useBuilderTranslation(); + const { data: currentAccount } = hooks.useCurrentMember(); + + const itemLoginSchemaIsDisabled = + !itemLoginSchema || + itemLoginSchema.status === ItemLoginSchemaStatus.Disabled; + + const isDisabled = + currentAccount?.id !== data.account.id && itemLoginSchemaIsDisabled; return ( - - - - {data.account.name} - - - - {translateEnums(data.permission)} - - - - - + + + + + {data.account.name} + + + + {isDisabled ? ( + + {translateEnums(ItemLoginSchemaStatus.Disabled)} + + ) : ( + {translateEnums(data.permission)} + )} + + + + + + ); }; diff --git a/src/components/item/sharing/membershipTable/ItemMembershipsTable.tsx b/src/components/item/sharing/membershipTable/ItemMembershipsTable.tsx index 643414406..165ce1b00 100644 --- a/src/components/item/sharing/membershipTable/ItemMembershipsTable.tsx +++ b/src/components/item/sharing/membershipTable/ItemMembershipsTable.tsx @@ -63,6 +63,9 @@ const ItemMembershipsTable = ({ showEmail = true }: Props): JSX.Element => { hasOnlyOneAdmin, isLoading: isMembershipsLoading, } = useHighestMemberships({ canAdmin, item }); + const { data: itemLoginSchema } = hooks.useItemLoginSchema({ + itemId: item.id, + }); if (memberships) { // map memberships to corresponding row layout and meaningful data to sort @@ -86,7 +89,12 @@ const ItemMembershipsTable = ({ showEmail = true }: Props): JSX.Element => { } /> ) : ( - + ), })); diff --git a/src/langs/constants.ts b/src/langs/constants.ts index 0a8c0bcdd..d99571052 100644 --- a/src/langs/constants.ts +++ b/src/langs/constants.ts @@ -593,4 +593,6 @@ export const BUILDER = { REQUEST_ACCESS_BUTTON: 'REQUEST_ACCESS_BUTTON', REQUEST_ACCESS_SENT_BUTTON: 'REQUEST_ACCESS_SENT_BUTTON', ACCESS_MANAGEMENT_TITLE: 'ACCESS_MANAGEMENT_TITLE', + ITEM_LOGIN_SCHEMA_DISABLED_GUEST_ACCESS_MESSAGE: + 'ITEM_LOGIN_SCHEMA_DISABLED_GUEST_ACCESS_MESSAGE', }; diff --git a/src/langs/en.json b/src/langs/en.json index 87343b612..c49f7f6b4 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -491,5 +491,6 @@ "REQUEST_ACCESS_TITLE": "Request access to this item", "REQUEST_ACCESS_BUTTON": "Request access", "REQUEST_ACCESS_SENT_BUTTON": "Request sent", - "ACCESS_MANAGEMENT_TITLE": "Access Management" + "ACCESS_MANAGEMENT_TITLE": "Access Management", + "ITEM_LOGIN_SCHEMA_DISABLED_GUEST_ACCESS_MESSAGE": "This guest cannot login because pseudonymized access is disabled." } diff --git a/vite.config.ts b/vite.config.ts index 4da77ce6e..21945dace 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -49,6 +49,13 @@ const config = ({ mode }: { mode: string }): UserConfigExport => { checkProd: true, }), ], + optimizeDeps: { + include: [ + // Solves "Uncaught TypeError: styled_default is not a function" + // This issue seems to be introduced in Graasp Builder 2.39.0 + '@mui/material/Tooltip', + ], + }, resolve: { alias: { '@': resolve(__dirname, 'src'), diff --git a/yarn.lock b/yarn.lock index 054f0f179..b0a3e1b34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1778,9 +1778,9 @@ __metadata: languageName: node linkType: hard -"@graasp/query-client@npm:3.24.0": - version: 3.24.0 - resolution: "@graasp/query-client@npm:3.24.0" +"@graasp/query-client@npm:3.25.0": + version: 3.25.0 + resolution: "@graasp/query-client@npm:3.25.0" dependencies: "@tanstack/react-query": "npm:4.36.1" "@tanstack/react-query-devtools": "npm:4.36.1" @@ -1790,13 +1790,13 @@ __metadata: "@graasp/sdk": ^4.0.0 "@graasp/translations": "*" react: ^18.0.0 - checksum: 10/06feb022dfd7094f4e9e680acc828b1daf0d1fbf6efe8b3f1ed99e13f28ac0a17abe255bd317cb00efd22c66cea80e02a56307e82574f53df8939c53c2e10d33 + checksum: 10/eaf22beda6f5a53b6c30b20debbffcf692ef39e9c80b878e24f529d89863ba6eb2babe4f80d4d57901d443abaa082465cd4def6e9136d6f443207048b6e69463 languageName: node linkType: hard -"@graasp/sdk@npm:4.29.1": - version: 4.29.1 - resolution: "@graasp/sdk@npm:4.29.1" +"@graasp/sdk@npm:4.31.0": + version: 4.31.0 + resolution: "@graasp/sdk@npm:4.31.0" dependencies: "@faker-js/faker": "npm:9.0.1" filesize: "npm:10.1.6" @@ -1805,7 +1805,7 @@ __metadata: peerDependencies: date-fns: ^3 || ^4.0.0 uuid: ^9 || ^10 - checksum: 10/f35a4c2dfbc7b9ac7d2112536dcded3da6d7fb32a3f4593921329340f96f21ccd558f68a77d723828f23055a6fb4d77493effbc746986ce7fcce913f3775a725 + checksum: 10/9b2bf85a51cc12b6f2bdefeb7bbc0c615db9ea3188ace6d460b14e61503763aeab13fd2aa1c4135cee602c2c58465895569b2845b91d942982f96f5594dfd1d4 languageName: node linkType: hard @@ -6596,8 +6596,8 @@ __metadata: "@emotion/styled": "npm:11.13.0" "@graasp/chatbox": "npm:3.3.0" "@graasp/map": "npm:1.18.0" - "@graasp/query-client": "npm:3.24.0" - "@graasp/sdk": "npm:4.29.1" + "@graasp/query-client": "npm:3.25.0" + "@graasp/sdk": "npm:4.31.0" "@graasp/stylis-plugin-rtl": "npm:2.2.0" "@graasp/translations": "npm:1.38.0" "@graasp/ui": "npm:5.2.0"