diff --git a/cypress/fixtures/items.js b/cypress/fixtures/items.js index 62845750b..76882f1b6 100644 --- a/cypress/fixtures/items.js +++ b/cypress/fixtures/items.js @@ -1,5 +1,11 @@ -import { ITEM_TYPES } from '../../src/config/constants'; -import { CURRENT_USER } from './members'; +import { + ITEM_TYPES, + PERMISSION_LEVELS, + SETTINGS, +} from '../../src/config/constants'; +import { buildItemLoginSchemaExtra } from '../../src/utils/itemExtra'; +import { CURRENT_USER, MEMBERS } from './members'; +import { DEFAULT_TAGS, ITEM_LOGIN_TAG } from './tags'; const DEFAULT_ITEM = { description: '', @@ -22,43 +28,193 @@ export const EDITED_FIELDS = { description: 'new description', }; -export const SAMPLE_ITEMS = [ - { - ...DEFAULT_ITEM, - id: 'ecafbd2a-5688-11eb-ae93-0242ac130002', - name: 'own_item_name1', - path: 'ecafbd2a_5688_11eb_ae93_0242ac130002', - extra: { - image: 'someimageurl', +export const SAMPLE_ITEMS = { + items: [ + { + ...DEFAULT_ITEM, + id: 'ecafbd2a-5688-11eb-ae93-0242ac130002', + name: 'own_item_name1', + path: 'ecafbd2a_5688_11eb_ae93_0242ac130002', + extra: { + image: 'someimageurl', + }, }, - }, - { - ...DEFAULT_ITEM, - id: 'fdf09f5a-5688-11eb-ae93-0242ac130002', - name: 'own_item_name2', - path: 'fdf09f5a_5688_11eb_ae93_0242ac130002', - extra: { - image: 'someimageurl', + { + ...DEFAULT_ITEM, + id: 'fdf09f5a-5688-11eb-ae93-0242ac130002', + name: 'own_item_name2', + path: 'fdf09f5a_5688_11eb_ae93_0242ac130002', + extra: { + image: 'someimageurl', + }, }, - }, - { - ...DEFAULT_ITEM, - id: 'fdf09f5a-5688-11eb-ae93-0242ac130003', - name: 'own_item_name3', - path: - 'ecafbd2a_5688_11eb_ae93_0242ac130002.fdf09f5a_5688_11eb_ae93_0242ac130003', - extra: { - image: 'someimageurl', + { + ...DEFAULT_ITEM, + id: 'fdf09f5a-5688-11eb-ae93-0242ac130003', + name: 'own_item_name3', + path: + 'ecafbd2a_5688_11eb_ae93_0242ac130002.fdf09f5a_5688_11eb_ae93_0242ac130003', + extra: { + image: 'someimageurl', + }, }, - }, - { - ...DEFAULT_ITEM, - id: 'fdf09f5a-5688-11eb-ae93-0242ac130004', - name: 'own_item_name4', - path: - 'ecafbd2a_5688_11eb_ae93_0242ac130002.fdf09f5a_5688_11eb_ae93_0242ac130004', - extra: { - image: 'someimageurl', + { + ...DEFAULT_ITEM, + id: 'fdf09f5a-5688-11eb-ae93-0242ac130004', + name: 'own_item_name4', + path: + 'ecafbd2a_5688_11eb_ae93_0242ac130002.fdf09f5a_5688_11eb_ae93_0242ac130004', + extra: { + image: 'someimageurl', + }, }, - }, -]; + ], + memberships: [], +}; + +export const ITEM_LOGIN_ITEMS = { + items: [ + { + ...DEFAULT_ITEM, + id: 'ecafbd2a-5688-11eb-ae93-0242ac130002', + name: 'item login with username', + path: 'ecafbd2a_5688_11eb_ae93_0242ac130002', + extra: { + image: 'someimageurl', + ...buildItemLoginSchemaExtra(SETTINGS.ITEM_LOGIN.OPTIONS.USERNAME), + }, + tags: [ + { + tagId: ITEM_LOGIN_TAG.id, + itemPath: 'ecafbd2a_5688_11eb_ae93_0242ac130002', + }, + ], + memberships: [ + { + itemPath: 'fdf09f5a_5688_11eb_ae93_0242ac130002', + permission: PERMISSION_LEVELS.ADMIN, + memberId: MEMBERS.ANNA.id, + }, + { + itemPath: 'fdf09f5a_5688_11eb_ae93_0242ac130002', + permission: PERMISSION_LEVELS.READ, + memberId: MEMBERS.BOB.id, + }, + ], + }, + { + ...DEFAULT_ITEM, + id: 'fdf09f5a-5688-11eb-ae93-0242ac130002', + name: 'no item login', + path: 'fdf09f5a_5688_11eb_ae93_0242ac130002', + extra: { + image: 'someimageurl', + }, + memberships: [ + { + itemPath: 'fdf09f5a_5688_11eb_ae93_0242ac130002', + permission: PERMISSION_LEVELS.ADMIN, + memberId: MEMBERS.ANNA.id, + }, + ], + }, + { + ...DEFAULT_ITEM, + id: 'fdf09f5a-5688-11eb-ae93-0242ac130003', + name: 'child of item login with username', + path: + 'ecafbd2a_5688_11eb_ae93_0242ac130002.fdf09f5a_5688_11eb_ae93_0242ac130003', + extra: { + image: 'someimageurl', + }, + memberships: [ + { + itemId: 'fdf09f5a-5688-11eb-ae93-0242ac130003', + itemPath: 'fdf09f5a_5688_11eb_ae93_0242ac130002', + permission: PERMISSION_LEVELS.ADMIN, + memberId: MEMBERS.ANNA.id, + }, + ], + }, + { + ...DEFAULT_ITEM, + id: 'fdf09f5a-5688-11eb-ae93-0242ac130004', + name: 'item login with username and password', + path: + 'ecafbd2a_5688_11eb_ae93_0242ac130002.fdf09f5a_5688_11eb_ae93_0242ac130004', + extra: { + image: 'someimageurl', + ...buildItemLoginSchemaExtra( + SETTINGS.ITEM_LOGIN.OPTIONS.USERNAME_AND_PASSWORD, + ), + }, + tags: [ + { + tagId: ITEM_LOGIN_TAG.id, + itemPath: + 'ecafbd2a_5688_11eb_ae93_0242ac130002.fdf09f5a_5688_11eb_ae93_0242ac130004', + }, + ], + memberships: [ + { + itemPath: 'fdf09f5a_5688_11eb_ae93_0242ac130002', + permission: PERMISSION_LEVELS.ADMIN, + memberId: MEMBERS.ANNA.id, + }, + { + itemPath: 'fdf09f5a_5688_11eb_ae93_0242ac130002', + permission: PERMISSION_LEVELS.READ, + memberId: MEMBERS.BOB.id, + }, + ], + }, + { + ...DEFAULT_ITEM, + id: 'egafbd2a-5688-11eb-ae93-0242ac130002', + name: 'item login with username and password', + path: 'egafbd2a_5688_11eb_ae93_0242ac130002', + extra: { + image: 'someimageurl', + ...buildItemLoginSchemaExtra( + SETTINGS.ITEM_LOGIN.OPTIONS.USERNAME_AND_PASSWORD, + ), + }, + tags: [ + { + tagId: ITEM_LOGIN_TAG.id, + itemPath: 'egafbd2a_5688_11eb_ae93_0242ac130002', + }, + ], + memberships: [ + { + itemPath: 'fdf09f5a_5688_11eb_ae93_0242ac130002', + permission: PERMISSION_LEVELS.ADMIN, + memberId: MEMBERS.ANNA.id, + }, + ], + }, + { + ...DEFAULT_ITEM, + id: 'bdf09f5a-5688-11eb-ae93-0242ac130004', + name: 'child of item login with username and password', + path: + 'ecafbd2a_5688_11eb_ae93_0242ac130002.fdf09f5a_5688_11eb_ae93_0242ac130004.bdf09f5a_5688_11eb_ae93_0242ac130004', + tags: [ + { + tagId: ITEM_LOGIN_TAG.id, + itemPath: + 'ecafbd2a_5688_11eb_ae93_0242ac130002.fdf09f5a_5688_11eb_ae93_0242ac130004', + }, + ], + memberships: [ + { + itemId: 'bdf09f5a-5688-11eb-ae93-0242ac130004', + itemPath: 'bdf09f5a_5688_11eb_ae93_0242ac130004', + permission: PERMISSION_LEVELS.ADMIN, + memberId: MEMBERS.ANNA.id, + }, + ], + }, + ], + tags: DEFAULT_TAGS, +}; diff --git a/cypress/fixtures/links.js b/cypress/fixtures/links.js index 7e6fecefa..236b31884 100644 --- a/cypress/fixtures/links.js +++ b/cypress/fixtures/links.js @@ -1,4 +1,5 @@ import { ITEM_TYPES } from '../../src/config/constants'; +import { CURRENT_USER } from './members'; import { buildEmbeddedLinkExtra } from '../../src/utils/itemExtra'; export const GRAASP_LINK_ITEM = { @@ -7,6 +8,7 @@ export const GRAASP_LINK_ITEM = { name: 'graasp link', description: 'a description for graasp link', path: 'ecafbd2a_5688_11eb_ae93_0242ac130002', + creator: CURRENT_USER.id, extra: buildEmbeddedLinkExtra({ url: 'https://graasp.eu', thumbnails: ['https://graasp.eu/img/epfl/logo-tile.png'], @@ -21,6 +23,7 @@ export const YOUTUBE_LINK_ITEM = { type: ITEM_TYPES.LINK, name: 'graasp youtube link', description: 'a description for graasp youtube link', + creator: CURRENT_USER.id, path: 'gcafbd2a_5688_11eb_ae93_0242ac130002', extra: buildEmbeddedLinkExtra({ url: 'https://www.youtube.com/watch?v=FmiEgBMTPLo', @@ -33,6 +36,7 @@ export const YOUTUBE_LINK_ITEM = { export const INVALID_LINK_ITEM = { type: ITEM_TYPES.LINK, + creator: CURRENT_USER.id, name: 'graasp youtube link', description: 'a description for graasp youtube link', extra: buildEmbeddedLinkExtra({ diff --git a/cypress/fixtures/tags.js b/cypress/fixtures/tags.js new file mode 100644 index 000000000..a55f40388 --- /dev/null +++ b/cypress/fixtures/tags.js @@ -0,0 +1,8 @@ +import { SETTINGS } from '../../src/config/constants'; + +export const ITEM_LOGIN_TAG = { + id: 'item-login-tag-id', + name: SETTINGS.ITEM_LOGIN.name, +}; + +export const DEFAULT_TAGS = [ITEM_LOGIN_TAG]; diff --git a/cypress/integration/authentication.spec.js b/cypress/integration/authentication.spec.js index bdfd58c8e..a41c8c9b2 100644 --- a/cypress/integration/authentication.spec.js +++ b/cypress/integration/authentication.spec.js @@ -6,6 +6,7 @@ import { import { HEADER_APP_BAR_ID, HEADER_USER_ID, + ITEM_MAIN_CLASS, USER_MENU_SIGN_OUT_OPTION_ID, } from '../../src/config/selectors'; import { SAMPLE_ITEMS } from '../fixtures/items'; @@ -19,7 +20,7 @@ import { describe('Authentication', () => { describe('Signed Off > Redirect to sign in route', () => { beforeEach(() => { - cy.setUpApi({ items: SAMPLE_ITEMS, getCurrentMemberError: true }); + cy.setUpApi({ ...SAMPLE_ITEMS, getCurrentMemberError: true }); }); it('Home', () => { cy.visit(HOME_PATH); @@ -31,16 +32,11 @@ describe('Authentication', () => { cy.wait(REQUEST_FAILURE_LOADING_TIME); cy.get('html').should('contain', REDIRECTION_CONTENT); }); - it('Item', () => { - cy.visit(buildItemPath(SAMPLE_ITEMS[0].id)); - cy.wait(REQUEST_FAILURE_LOADING_TIME); - cy.get('html').should('contain', REDIRECTION_CONTENT); - }); }); describe('Signed In', () => { beforeEach(() => { - cy.setUpApi({ items: SAMPLE_ITEMS }); + cy.setUpApi(SAMPLE_ITEMS); }); it('Signing Off redirect to sign in route', () => { @@ -64,9 +60,9 @@ describe('Authentication', () => { cy.get(`#${HEADER_APP_BAR_ID}`).should('exist'); }); it('Item', () => { - cy.visit(buildItemPath(SAMPLE_ITEMS[0].id)); - cy.wait(PAGE_LOAD_WAITING_PAUSE); + cy.visit(buildItemPath(SAMPLE_ITEMS.items[0].id)); cy.get(`#${HEADER_APP_BAR_ID}`).should('exist'); + cy.get(`.${ITEM_MAIN_CLASS}`).should('exist'); }); }); diff --git a/cypress/integration/item/copy/gridCopyItem.spec.js b/cypress/integration/item/copy/gridCopyItem.spec.js index b679e2315..9598a8279 100644 --- a/cypress/integration/item/copy/gridCopyItem.spec.js +++ b/cypress/integration/item/copy/gridCopyItem.spec.js @@ -17,13 +17,13 @@ const copyItem = (id, toItemId) => { describe('Copy Item in Grid', () => { it('copy item on Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); + cy.setUpApi({ ...SAMPLE_ITEMS }); cy.visit(HOME_PATH); cy.switchMode(ITEM_LAYOUT_MODES.GRID); // copy - const { id: copyItemId } = SAMPLE_ITEMS[0]; - const { id: toItem } = SAMPLE_ITEMS[1]; + const { id: copyItemId } = SAMPLE_ITEMS.items[0]; + const { id: toItem } = SAMPLE_ITEMS.items[1]; copyItem(copyItemId, toItem); cy.wait('@copyItem').then(({ response: { body } }) => { @@ -36,16 +36,16 @@ describe('Copy Item in Grid', () => { }); it('copy item in item', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); cy.switchMode(ITEM_LAYOUT_MODES.GRID); // move - const { id: copyItemId } = SAMPLE_ITEMS[2]; - const { id: toItem } = SAMPLE_ITEMS[3]; + const { id: copyItemId } = SAMPLE_ITEMS.items[2]; + const { id: toItem } = SAMPLE_ITEMS.items[3]; copyItem(copyItemId, toItem); cy.wait('@copyItem').then(({ response: { body } }) => { @@ -58,15 +58,15 @@ describe('Copy Item in Grid', () => { }); it('copy item to Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); cy.switchMode(ITEM_LAYOUT_MODES.GRID); // move - const { id: copyItemId } = SAMPLE_ITEMS[2]; + const { id: copyItemId } = SAMPLE_ITEMS.items[2]; const toItem = ROOT_ID; copyItem(copyItemId, toItem); @@ -79,18 +79,18 @@ describe('Copy Item in Grid', () => { }); }); - describe('Errors handling', () => { + describe('Error handling', () => { it('error while moving item does not create in interface', () => { - cy.setUpApi({ items: SAMPLE_ITEMS, copyItemError: true }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi({ ...SAMPLE_ITEMS, copyItemError: true }); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); cy.switchMode(ITEM_LAYOUT_MODES.GRID); // move - const { id: copyItemId } = SAMPLE_ITEMS[2]; - const { id: toItem } = SAMPLE_ITEMS[0]; + const { id: copyItemId } = SAMPLE_ITEMS.items[2]; + const { id: toItem } = SAMPLE_ITEMS.items[0]; copyItem(copyItemId, toItem); cy.wait('@copyItem').then(({ response: { body } }) => { diff --git a/cypress/integration/item/copy/listCopyItem.spec.js b/cypress/integration/item/copy/listCopyItem.spec.js index 763613e55..43b00ac9c 100644 --- a/cypress/integration/item/copy/listCopyItem.spec.js +++ b/cypress/integration/item/copy/listCopyItem.spec.js @@ -23,15 +23,15 @@ const copyItem = (id, toItemId) => { describe('Copy Item in List', () => { it('copy item on Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); + cy.setUpApi(SAMPLE_ITEMS); cy.visit(HOME_PATH); if (DEFAULT_ITEM_LAYOUT_MODE !== ITEM_LAYOUT_MODES.LIST) { cy.switchMode(ITEM_LAYOUT_MODES.LIST); } // copy - const { id: copyItemId } = SAMPLE_ITEMS[0]; - const { id: toItem } = SAMPLE_ITEMS[1]; + const { id: copyItemId } = SAMPLE_ITEMS.items[0]; + const { id: toItem } = SAMPLE_ITEMS.items[1]; copyItem(copyItemId, toItem); cy.wait('@copyItem').then(({ response: { body } }) => { @@ -44,8 +44,8 @@ describe('Copy Item in List', () => { }); it('copy item in item', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); @@ -54,8 +54,8 @@ describe('Copy Item in List', () => { } // copy - const { id: copyItemId } = SAMPLE_ITEMS[2]; - const { id: toItem } = SAMPLE_ITEMS[3]; + const { id: copyItemId } = SAMPLE_ITEMS.items[2]; + const { id: toItem } = SAMPLE_ITEMS.items[3]; copyItem(copyItemId, toItem); cy.wait('@copyItem').then(({ response: { body } }) => { @@ -68,8 +68,8 @@ describe('Copy Item in List', () => { }); it('copy item to Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); @@ -78,7 +78,7 @@ describe('Copy Item in List', () => { } // copy - const { id: copyItemId } = SAMPLE_ITEMS[2]; + const { id: copyItemId } = SAMPLE_ITEMS.items[2]; const toItem = ROOT_ID; copyItem(copyItemId, toItem); @@ -91,10 +91,10 @@ describe('Copy Item in List', () => { }); }); - describe('Errors handling', () => { + describe('Error handling', () => { it('error while moving item does not create in interface', () => { - cy.setUpApi({ items: SAMPLE_ITEMS, copyItemError: true }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi({ ...SAMPLE_ITEMS, copyItemError: true }); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); @@ -103,8 +103,8 @@ describe('Copy Item in List', () => { } // copy - const { id: copyItemId } = SAMPLE_ITEMS[2]; - const { id: toItem } = SAMPLE_ITEMS[0]; + const { id: copyItemId } = SAMPLE_ITEMS.items[2]; + const { id: toItem } = SAMPLE_ITEMS.items[0]; copyItem(copyItemId, toItem); cy.wait('@copyItem').then(({ response: { body } }) => { diff --git a/cypress/integration/item/create/createFile.spec.js b/cypress/integration/item/create/createFile.spec.js index f33bdb41f..3908809c7 100644 --- a/cypress/integration/item/create/createFile.spec.js +++ b/cypress/integration/item/create/createFile.spec.js @@ -29,8 +29,8 @@ describe('Create File', () => { }); it('create file in item', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); diff --git a/cypress/integration/item/create/createFolder.spec.js b/cypress/integration/item/create/createFolder.spec.js index a1897dcd2..0df803f05 100644 --- a/cypress/integration/item/create/createFolder.spec.js +++ b/cypress/integration/item/create/createFolder.spec.js @@ -25,8 +25,8 @@ describe('Create Folder', () => { }); it('create folder in item', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); @@ -63,8 +63,8 @@ describe('Create Folder', () => { }); it('create folder in item', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); @@ -80,10 +80,10 @@ describe('Create Folder', () => { }); }); - describe('Errors handling', () => { + describe('Error handling', () => { it('error while creating folder does not create in interface', () => { - cy.setUpApi({ items: SAMPLE_ITEMS, postItemError: true }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi({ ...SAMPLE_ITEMS, postItemError: true }); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); diff --git a/cypress/integration/item/create/createLink.spec.js b/cypress/integration/item/create/createLink.spec.js index 7d67af948..5e6e0ff64 100644 --- a/cypress/integration/item/create/createLink.spec.js +++ b/cypress/integration/item/create/createLink.spec.js @@ -31,8 +31,8 @@ describe('Create Link', () => { }); it('create folder in item', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); @@ -53,10 +53,10 @@ describe('Create Link', () => { }); }); - describe('Errors handling', () => { + describe('Error handling', () => { it('cannot add an invalid link', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); diff --git a/cypress/integration/item/delete/gridDeleteItem.spec.js b/cypress/integration/item/delete/gridDeleteItem.spec.js index 6b7fc2384..2bf50215b 100644 --- a/cypress/integration/item/delete/gridDeleteItem.spec.js +++ b/cypress/integration/item/delete/gridDeleteItem.spec.js @@ -12,11 +12,11 @@ const deleteItem = (id) => { describe('Delete Item in Grid', () => { it('delete item on Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); + cy.setUpApi(SAMPLE_ITEMS); cy.visit(HOME_PATH); cy.switchMode(ITEM_LAYOUT_MODES.GRID); - const { id } = SAMPLE_ITEMS[0]; + const { id } = SAMPLE_ITEMS.items[0]; // delete deleteItem(id); @@ -24,9 +24,9 @@ describe('Delete Item in Grid', () => { }); it('delete item inside parent', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; - const { id: idToDelete } = SAMPLE_ITEMS[2]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; + const { id: idToDelete } = SAMPLE_ITEMS.items[2]; // go to children item cy.visit(buildItemPath(id)); @@ -40,11 +40,11 @@ describe('Delete Item in Grid', () => { }); }); - describe('Errors handling', () => { + describe('Error handling', () => { it('error while deleting item does not delete in interface', () => { - cy.setUpApi({ items: SAMPLE_ITEMS, deleteItemError: true }); - const { id } = SAMPLE_ITEMS[0]; - const { id: idToDelete } = SAMPLE_ITEMS[2]; + cy.setUpApi({ ...SAMPLE_ITEMS, deleteItemError: true }); + const { id } = SAMPLE_ITEMS.items[0]; + const { id: idToDelete } = SAMPLE_ITEMS.items[2]; // go to children item cy.visit(buildItemPath(id)); diff --git a/cypress/integration/item/delete/listDeleteItem.spec.js b/cypress/integration/item/delete/listDeleteItem.spec.js index d7d39f7b2..afea6e331 100644 --- a/cypress/integration/item/delete/listDeleteItem.spec.js +++ b/cypress/integration/item/delete/listDeleteItem.spec.js @@ -15,14 +15,14 @@ const deleteItem = (id) => { describe('Delete Item in List', () => { it('delete item on Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); + cy.setUpApi(SAMPLE_ITEMS); cy.visit(HOME_PATH); if (DEFAULT_ITEM_LAYOUT_MODE !== ITEM_LAYOUT_MODES.LIST) { cy.switchMode(ITEM_LAYOUT_MODES.LIST); } - const { id } = SAMPLE_ITEMS[0]; + const { id } = SAMPLE_ITEMS.items[0]; // delete deleteItem(id); @@ -30,9 +30,9 @@ describe('Delete Item in List', () => { }); it('delete item inside parent', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; - const { id: idToDelete } = SAMPLE_ITEMS[2]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; + const { id: idToDelete } = SAMPLE_ITEMS.items[2]; // go to children item cy.visit(buildItemPath(id)); @@ -46,15 +46,17 @@ describe('Delete Item in List', () => { cy.wait('@deleteItem').then(() => { // check item is deleted, others are still displayed cy.get(`#${buildItemsTableRowId(idToDelete)}`).should('not.exist'); - cy.get(`#${buildItemsTableRowId(SAMPLE_ITEMS[3].id)}`).should('exist'); + cy.get(`#${buildItemsTableRowId(SAMPLE_ITEMS.items[3].id)}`).should( + 'exist', + ); }); }); - describe('Errors handling', () => { + describe('Error handling', () => { it('error while deleting item does not delete in interface', () => { - cy.setUpApi({ items: SAMPLE_ITEMS, deleteItemError: true }); - const { id } = SAMPLE_ITEMS[0]; - const { id: idToDelete } = SAMPLE_ITEMS[2]; + cy.setUpApi({ ...SAMPLE_ITEMS, deleteItemError: true }); + const { id } = SAMPLE_ITEMS.items[0]; + const { id: idToDelete } = SAMPLE_ITEMS.items[2]; // go to children item cy.visit(buildItemPath(id)); diff --git a/cypress/integration/item/delete/listDeleteItems.spec.js b/cypress/integration/item/delete/listDeleteItems.spec.js index eff3e1e55..fca152bc4 100644 --- a/cypress/integration/item/delete/listDeleteItems.spec.js +++ b/cypress/integration/item/delete/listDeleteItems.spec.js @@ -23,7 +23,7 @@ const deleteItems = (itemIds) => { describe('Delete Items in List', () => { it('delete 2 items in Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); + cy.setUpApi(SAMPLE_ITEMS); cy.visit(HOME_PATH); if (DEFAULT_ITEM_LAYOUT_MODE !== ITEM_LAYOUT_MODES.LIST) { @@ -31,30 +31,31 @@ describe('Delete Items in List', () => { } // delete - deleteItems([SAMPLE_ITEMS[0].id, SAMPLE_ITEMS[1].id]); + deleteItems([SAMPLE_ITEMS.items[0].id, SAMPLE_ITEMS.items[1].id]); cy.wait(['@deleteItems', '@getOwnItems']); }); it('delete 2 items in item', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; - cy.visit(buildItemPath(id)); + cy.setUpApi(SAMPLE_ITEMS); + cy.visit(buildItemPath(SAMPLE_ITEMS.items[0].id)); if (DEFAULT_ITEM_LAYOUT_MODE !== ITEM_LAYOUT_MODES.LIST) { cy.switchMode(ITEM_LAYOUT_MODES.LIST); } // delete - deleteItems([SAMPLE_ITEMS[2].id, SAMPLE_ITEMS[3].id]); + deleteItems([SAMPLE_ITEMS.items[2].id, SAMPLE_ITEMS.items[3].id]); cy.wait('@deleteItems').then(() => { // check item is deleted, others are still displayed - cy.wait('@getItem').its('response.url').should('contain', id); + cy.wait('@getItem') + .its('response.url') + .should('contain', SAMPLE_ITEMS.items[0].id); }); }); - describe('Errors handling', () => { + describe('Error handling', () => { it('does not delete items on error', () => { - cy.setUpApi({ items: SAMPLE_ITEMS, deleteItemsError: true }); + cy.setUpApi({ ...SAMPLE_ITEMS, deleteItemsError: true }); cy.visit(HOME_PATH); if (DEFAULT_ITEM_LAYOUT_MODE !== ITEM_LAYOUT_MODES.LIST) { @@ -62,11 +63,15 @@ describe('Delete Items in List', () => { } // delete - deleteItems([SAMPLE_ITEMS[0].id, SAMPLE_ITEMS[1].id]); + deleteItems([SAMPLE_ITEMS.items[0].id, SAMPLE_ITEMS.items[1].id]); cy.wait('@deleteItems').then(() => { // check item is deleted, others are still displayed - cy.get(`#${buildItemsTableRowId(SAMPLE_ITEMS[0].id)}`).should('exist'); - cy.get(`#${buildItemsTableRowId(SAMPLE_ITEMS[1].id)}`).should('exist'); + cy.get(`#${buildItemsTableRowId(SAMPLE_ITEMS.items[0].id)}`).should( + 'exist', + ); + cy.get(`#${buildItemsTableRowId(SAMPLE_ITEMS.items[1].id)}`).should( + 'exist', + ); }); }); }); diff --git a/cypress/integration/item/edit/editFolder.spec.js b/cypress/integration/item/edit/editFolder.spec.js index bda124f80..c26e270be 100644 --- a/cypress/integration/item/edit/editFolder.spec.js +++ b/cypress/integration/item/edit/editFolder.spec.js @@ -10,14 +10,14 @@ import { editItem } from './utils'; describe('Edit Folder', () => { describe('List', () => { it('edit folder on Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); + cy.setUpApi(SAMPLE_ITEMS); cy.visit(HOME_PATH); if (DEFAULT_ITEM_LAYOUT_MODE !== ITEM_LAYOUT_MODES.LIST) { cy.switchMode(ITEM_LAYOUT_MODES.LIST); } - const itemToEdit = SAMPLE_ITEMS[0]; + const itemToEdit = SAMPLE_ITEMS.items[0]; // edit editItem( @@ -45,15 +45,15 @@ describe('Edit Folder', () => { }); it('edit folder in item', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); + cy.setUpApi(SAMPLE_ITEMS); // go to children item - cy.visit(buildItemPath(SAMPLE_ITEMS[0].id)); + cy.visit(buildItemPath(SAMPLE_ITEMS.items[0].id)); if (DEFAULT_ITEM_LAYOUT_MODE !== ITEM_LAYOUT_MODES.LIST) { cy.switchMode(ITEM_LAYOUT_MODES.LIST); } - const itemToEdit = SAMPLE_ITEMS[2]; + const itemToEdit = SAMPLE_ITEMS.items[2]; // edit editItem( @@ -77,7 +77,7 @@ describe('Edit Folder', () => { expect(description).to.equal(EDITED_FIELDS.description); cy.get('@getItem') .its('response.url') - .should('contain', SAMPLE_ITEMS[0].id); + .should('contain', SAMPLE_ITEMS.items[0].id); }, ); }); @@ -85,11 +85,11 @@ describe('Edit Folder', () => { describe('Grid', () => { it('edit folder on Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); + cy.setUpApi(SAMPLE_ITEMS); cy.visit(HOME_PATH); cy.switchMode(ITEM_LAYOUT_MODES.GRID); - const itemToEdit = SAMPLE_ITEMS[0]; + const itemToEdit = SAMPLE_ITEMS.items[0]; // edit editItem( @@ -117,12 +117,12 @@ describe('Edit Folder', () => { }); it('edit folder in item', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); + cy.setUpApi(SAMPLE_ITEMS); // go to children item - cy.visit(buildItemPath(SAMPLE_ITEMS[0].id)); + cy.visit(buildItemPath(SAMPLE_ITEMS.items[0].id)); cy.switchMode(ITEM_LAYOUT_MODES.GRID); - const itemToEdit = SAMPLE_ITEMS[2]; + const itemToEdit = SAMPLE_ITEMS.items[2]; // edit editItem( @@ -146,7 +146,7 @@ describe('Edit Folder', () => { expect(description).to.equal(EDITED_FIELDS.description); cy.get('@getItem') .its('response.url') - .should('contain', SAMPLE_ITEMS[0].id); + .should('contain', SAMPLE_ITEMS.items[0].id); }, ); }); diff --git a/cypress/integration/item/itemLogin/itemLogin.spec.js b/cypress/integration/item/itemLogin/itemLogin.spec.js new file mode 100644 index 000000000..277748bc2 --- /dev/null +++ b/cypress/integration/item/itemLogin/itemLogin.spec.js @@ -0,0 +1,231 @@ +import { v4 as uuidv4 } from 'uuid'; +import { + SETTINGS, + SETTINGS_ITEM_LOGIN_DEFAULT, +} from '../../../../src/config/constants'; +import { buildItemPath } from '../../../../src/config/paths'; +import { + buildItemLoginSettingModeSelectOption, + buildItemLoginSignInModeOption, + ITEM_LOGIN_SCREEN_FORBIDDEN_ID, + ITEM_LOGIN_SCREEN_ID, + ITEM_LOGIN_SETTING_MODE_SELECT_ID, + ITEM_LOGIN_SETTING_SWITCH_ID, + ITEM_LOGIN_SIGN_IN_BUTTON_ID, + ITEM_LOGIN_SIGN_IN_MEMBER_ID_ID, + ITEM_LOGIN_SIGN_IN_MODE_ID, + ITEM_LOGIN_SIGN_IN_PASSWORD_ID, + ITEM_LOGIN_SIGN_IN_USERNAME_ID, +} from '../../../../src/config/selectors'; +import { getItemLoginExtra } from '../../../../src/utils/itemExtra'; +import { ITEM_LOGIN_ITEMS } from '../../../fixtures/items'; +import { MEMBERS } from '../../../fixtures/members'; + +const changeSignInMode = (mode) => { + cy.get(`#${ITEM_LOGIN_SIGN_IN_MODE_ID}`).click(); + cy.get(`#${buildItemLoginSignInModeOption(mode)}`).click(); +}; + +const checkItemLoginScreenLayout = ( + itemLoginSchema = SETTINGS_ITEM_LOGIN_DEFAULT, +) => { + cy.get(`#${ITEM_LOGIN_SCREEN_ID}`).should('exist'); + cy.get(`#${ITEM_LOGIN_SIGN_IN_USERNAME_ID}`).should('exist'); + if (itemLoginSchema === SETTINGS.ITEM_LOGIN.OPTIONS.USERNAME_AND_PASSWORD) { + cy.get(`#${ITEM_LOGIN_SIGN_IN_PASSWORD_ID}`).should('exist'); + } + cy.get(`#${ITEM_LOGIN_SIGN_IN_BUTTON_ID}`).should('exist'); +}; + +const fillItemLoginScreenLayout = ({ username, password, memberId }) => { + cy.get(`#${ITEM_LOGIN_SCREEN_ID}`).should('exist'); + + if (!memberId) { + changeSignInMode(SETTINGS.ITEM_LOGIN.SIGN_IN_MODE.USERNAME); + cy.get(`#${ITEM_LOGIN_SIGN_IN_USERNAME_ID}`).clear().type(username); + } else { + changeSignInMode(SETTINGS.ITEM_LOGIN.SIGN_IN_MODE.MEMBER_ID); + cy.get(`#${ITEM_LOGIN_SIGN_IN_MEMBER_ID_ID}`).clear().type(memberId); + } + + 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 }) => { + const checkedValue = isEnabled ? 'be.checked' : 'not.be.checked'; + cy.get(`#${ITEM_LOGIN_SETTING_SWITCH_ID}`).should(checkedValue); + if (isEnabled && !disabled) { + cy.get(`#${ITEM_LOGIN_SETTING_MODE_SELECT_ID} + input`).should( + 'have.value', + mode, + ); + } + if (disabled) { + cy.get(`#${ITEM_LOGIN_SETTING_SWITCH_ID}`).should('be.disabled'); + } +}; + +const editItemLoginSetting = ({ isEnabled, mode }) => { + if (isEnabled) { + cy.get(`#${ITEM_LOGIN_SETTING_SWITCH_ID}`).check(); + } else { + cy.get(`#${ITEM_LOGIN_SETTING_SWITCH_ID}`).uncheck(); + } + cy.wait('@postItemTag'); + + if (isEnabled) { + cy.get(`#${ITEM_LOGIN_SETTING_MODE_SELECT_ID}`).click(); + cy.get(`#${buildItemLoginSettingModeSelectOption(mode)}`).click(); + } +}; + +describe('Item Login', () => { + it('Item Login not allowed', () => { + cy.setUpApi({ + ...ITEM_LOGIN_ITEMS, + currentMember: MEMBERS.BOB, + }); + cy.visit(buildItemPath(ITEM_LOGIN_ITEMS.items[4].id)); + cy.wait(1000); + cy.get(`#${ITEM_LOGIN_SCREEN_FORBIDDEN_ID}`).should('exist'); + }); + + describe('User is signed out', () => { + beforeEach(() => { + cy.setUpApi({ ...ITEM_LOGIN_ITEMS, getCurrentMemberError: true }); + }); + + describe('Display Item Login Screen', () => { + it('username or member id', () => { + cy.visit(buildItemPath(ITEM_LOGIN_ITEMS.items[0].id)); + checkItemLoginScreenLayout( + getItemLoginExtra(ITEM_LOGIN_ITEMS.items[0].extra), + ); + fillItemLoginScreenLayout({ + username: 'username', + }); + cy.wait('@postItemLogin'); + + // use memberid + fillItemLoginScreenLayout({ + memberId: uuidv4(), + }); + cy.wait('@postItemLogin'); + + // use username to check no member id is incorrectly sent + fillItemLoginScreenLayout({ + username: 'username', + }); + cy.wait('@postItemLogin'); + }); + it('username or member id and password', () => { + cy.visit(buildItemPath(ITEM_LOGIN_ITEMS.items[3].id)); + checkItemLoginScreenLayout( + getItemLoginExtra(ITEM_LOGIN_ITEMS.items[3].extra), + ); + fillItemLoginScreenLayout({ + username: 'username', + password: 'password', + }); + cy.wait('@postItemLogin'); + + // use memberid + fillItemLoginScreenLayout({ + memberId: uuidv4(), + 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', () => { + cy.setUpApi({ + ...ITEM_LOGIN_ITEMS, + currentMember: MEMBERS.BOB, + }); + cy.visit(buildItemPath(ITEM_LOGIN_ITEMS.items[4].id)); + + // avoid to detect intermediate screens because of loading + // to remove when requests loading time is properly managed + cy.wait(1000); + + cy.get(`#${ITEM_LOGIN_SCREEN_FORBIDDEN_ID}`).should('exist'); + }); + }); + + describe('Display Item Login Setting', () => { + it('edit item login setting', () => { + cy.setUpApi(ITEM_LOGIN_ITEMS); + + // check item with item login enabled + cy.visit(buildItemPath(ITEM_LOGIN_ITEMS.items[0].id)); + checkItemLoginSetting({ + isEnabled: true, + mode: SETTINGS.ITEM_LOGIN.OPTIONS.USERNAME, + }); + + // allow item login + cy.visit(buildItemPath(ITEM_LOGIN_ITEMS.items[1].id)); + checkItemLoginSetting({ + isEnabled: false, + mode: SETTINGS.ITEM_LOGIN.OPTIONS.USERNAME, + }); + editItemLoginSetting({ + isEnabled: true, + mode: SETTINGS.ITEM_LOGIN.OPTIONS.USERNAME_AND_PASSWORD, + }); + + // disabled at child level + cy.visit(buildItemPath(ITEM_LOGIN_ITEMS.items[5].id)); + checkItemLoginSetting({ + isEnabled: true, + mode: SETTINGS.ITEM_LOGIN.OPTIONS.USERNAME_AND_PASSWORD, + disabled: true, + }); + }); + + it('read permission', () => { + cy.setUpApi({ + ...ITEM_LOGIN_ITEMS, + currentMember: MEMBERS.BOB, + }); + cy.visit(buildItemPath(ITEM_LOGIN_ITEMS.items[3].id)); + cy.wait(1000); + }); + }); + + describe('Error handling', () => { + it('error while signing in', () => { + cy.setUpApi({ + ...ITEM_LOGIN_ITEMS, + postItemLoginError: true, + getCurrentMemberError: true, + }); + const { id } = ITEM_LOGIN_ITEMS.items[4]; + + // go to children item + cy.visit(buildItemPath(id)); + + fillItemLoginScreenLayout({ + username: 'username', + password: 'password', + }); + + cy.wait(1000); + + cy.get(`#${ITEM_LOGIN_SCREEN_ID}`).should('exist'); + }); + }); +}); diff --git a/cypress/integration/item/move/gridMoveItem.spec.js b/cypress/integration/item/move/gridMoveItem.spec.js index e2ff45c80..a488278e5 100644 --- a/cypress/integration/item/move/gridMoveItem.spec.js +++ b/cypress/integration/item/move/gridMoveItem.spec.js @@ -23,13 +23,13 @@ const moveItem = (movedItemId, toItemId) => { describe('Move Item in Grid', () => { it('move item on Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); + cy.setUpApi(SAMPLE_ITEMS); cy.visit(HOME_PATH); cy.switchMode(ITEM_LAYOUT_MODES.GRID); // move - const { id: movedItem } = SAMPLE_ITEMS[0]; - const { id: toItem } = SAMPLE_ITEMS[1]; + const { id: movedItem } = SAMPLE_ITEMS.items[0]; + const { id: toItem } = SAMPLE_ITEMS.items[1]; moveItem(movedItem, toItem); cy.wait('@moveItem'); @@ -42,16 +42,16 @@ describe('Move Item in Grid', () => { }); it('move item in item', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); cy.switchMode(ITEM_LAYOUT_MODES.GRID); // move - const { id: movedItem } = SAMPLE_ITEMS[2]; - const { id: toItem } = SAMPLE_ITEMS[3]; + const { id: movedItem } = SAMPLE_ITEMS.items[2]; + const { id: toItem } = SAMPLE_ITEMS.items[3]; moveItem(movedItem, toItem); cy.wait('@moveItem'); @@ -64,15 +64,15 @@ describe('Move Item in Grid', () => { }); it('move item to Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); cy.switchMode(ITEM_LAYOUT_MODES.GRID); // move - const { id: movedItem } = SAMPLE_ITEMS[2]; + const { id: movedItem } = SAMPLE_ITEMS.items[2]; const toItem = ROOT_ID; moveItem(movedItem, toItem); @@ -87,16 +87,16 @@ describe('Move Item in Grid', () => { describe('Error handling', () => { it('error while moving item does not create in interface', () => { - cy.setUpApi({ items: SAMPLE_ITEMS, moveItemError: true }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi({ ...SAMPLE_ITEMS, moveItemError: true }); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); cy.switchMode(ITEM_LAYOUT_MODES.GRID); // move - const { id: movedItem } = SAMPLE_ITEMS[2]; - const { id: toItem } = SAMPLE_ITEMS[3]; + const { id: movedItem } = SAMPLE_ITEMS.items[2]; + const { id: toItem } = SAMPLE_ITEMS.items[3]; moveItem(movedItem, toItem); cy.wait('@moveItem').then(() => { diff --git a/cypress/integration/item/move/listMoveItem.spec.js b/cypress/integration/item/move/listMoveItem.spec.js index 712c7961d..077a91185 100644 --- a/cypress/integration/item/move/listMoveItem.spec.js +++ b/cypress/integration/item/move/listMoveItem.spec.js @@ -27,7 +27,7 @@ const moveItem = (movedItemId, toItemId) => { describe('Move Item in List', () => { it('move item on Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); + cy.setUpApi(SAMPLE_ITEMS); cy.visit(HOME_PATH); if (DEFAULT_ITEM_LAYOUT_MODE !== ITEM_LAYOUT_MODES.LIST) { @@ -35,8 +35,8 @@ describe('Move Item in List', () => { } // move - const { id: movedItem } = SAMPLE_ITEMS[0]; - const { id: toItem } = SAMPLE_ITEMS[1]; + const { id: movedItem } = SAMPLE_ITEMS.items[0]; + const { id: toItem } = SAMPLE_ITEMS.items[1]; moveItem(movedItem, toItem); cy.wait('@moveItem'); @@ -49,8 +49,8 @@ describe('Move Item in List', () => { }); it('move item in item', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); @@ -60,8 +60,8 @@ describe('Move Item in List', () => { } // move - const { id: movedItem } = SAMPLE_ITEMS[2]; - const { id: toItem } = SAMPLE_ITEMS[3]; + const { id: movedItem } = SAMPLE_ITEMS.items[2]; + const { id: toItem } = SAMPLE_ITEMS.items[3]; moveItem(movedItem, toItem); cy.wait('@moveItem'); @@ -74,8 +74,8 @@ describe('Move Item in List', () => { }); it('move item to Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); @@ -85,7 +85,7 @@ describe('Move Item in List', () => { } // move - const { id: movedItem } = SAMPLE_ITEMS[2]; + const { id: movedItem } = SAMPLE_ITEMS.items[2]; const toItem = ROOT_ID; moveItem(movedItem, toItem); @@ -98,10 +98,10 @@ describe('Move Item in List', () => { cy.get(`#${buildItemsTableRowId(movedItem)}`).should('exist'); }); - describe('Errors handling', () => { + describe('Error handling', () => { it('error while moving item does not create in interface', () => { - cy.setUpApi({ items: SAMPLE_ITEMS, moveItemError: true }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi({ ...SAMPLE_ITEMS, moveItemError: true }); + const { id } = SAMPLE_ITEMS.items[0]; // go to children item cy.visit(buildItemPath(id)); @@ -111,8 +111,8 @@ describe('Move Item in List', () => { } // move - const { id: movedItem } = SAMPLE_ITEMS[2]; - const { id: toItem } = SAMPLE_ITEMS[3]; + const { id: movedItem } = SAMPLE_ITEMS.items[2]; + const { id: toItem } = SAMPLE_ITEMS.items[3]; moveItem(movedItem, toItem); cy.wait('@moveItem').then(() => { diff --git a/cypress/integration/item/share/gridShareItem.spec.js b/cypress/integration/item/share/gridShareItem.spec.js index 69fc517dd..8c2a66dd3 100644 --- a/cypress/integration/item/share/gridShareItem.spec.js +++ b/cypress/integration/item/share/gridShareItem.spec.js @@ -18,12 +18,12 @@ const shareItem = ({ id, member, permission }) => { describe('Share Item in Grid', () => { it('share item on Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS, members: Object.values(MEMBERS) }); + cy.setUpApi({ ...SAMPLE_ITEMS, members: Object.values(MEMBERS) }); cy.visit(HOME_PATH); cy.switchMode(ITEM_LAYOUT_MODES.GRID); // share - const { id } = SAMPLE_ITEMS[0]; + const { id } = SAMPLE_ITEMS.items[0]; const member = MEMBERS.ANNA; shareItem({ id, member, permission: PERMISSION_LEVELS.WRITE }); @@ -33,14 +33,14 @@ describe('Share Item in Grid', () => { }); it('share item in item', () => { - cy.setUpApi({ items: SAMPLE_ITEMS, members: Object.values(MEMBERS) }); + cy.setUpApi({ ...SAMPLE_ITEMS, members: Object.values(MEMBERS) }); // go to children item - cy.visit(buildItemPath(SAMPLE_ITEMS[0].id)); + cy.visit(buildItemPath(SAMPLE_ITEMS.items[0].id)); cy.switchMode(ITEM_LAYOUT_MODES.GRID); // share - const { id } = SAMPLE_ITEMS[2]; + const { id } = SAMPLE_ITEMS.items[2]; const member = MEMBERS.ANNA; shareItem({ id, member, permission: PERMISSION_LEVELS.READ }); diff --git a/cypress/integration/item/share/listShareItem.spec.js b/cypress/integration/item/share/listShareItem.spec.js index 0657fefb9..937929539 100644 --- a/cypress/integration/item/share/listShareItem.spec.js +++ b/cypress/integration/item/share/listShareItem.spec.js @@ -19,7 +19,7 @@ const shareItem = ({ id, member, permission }) => { describe('Share Item in List', () => { it('share item on Home', () => { - cy.setUpApi({ items: SAMPLE_ITEMS, members: Object.values(MEMBERS) }); + cy.setUpApi({ ...SAMPLE_ITEMS, members: Object.values(MEMBERS) }); cy.visit(HOME_PATH); if (DEFAULT_ITEM_LAYOUT_MODE !== ITEM_LAYOUT_MODES.LIST) { @@ -27,7 +27,7 @@ describe('Share Item in List', () => { } // share - const { id } = SAMPLE_ITEMS[0]; + const { id } = SAMPLE_ITEMS.items[0]; const member = MEMBERS.ANNA; shareItem({ id, member, permission: PERMISSION_LEVELS.WRITE }); @@ -37,17 +37,17 @@ describe('Share Item in List', () => { }); it('share item in item', () => { - cy.setUpApi({ items: SAMPLE_ITEMS, members: Object.values(MEMBERS) }); + cy.setUpApi({ ...SAMPLE_ITEMS, members: Object.values(MEMBERS) }); // go to children item - cy.visit(buildItemPath(SAMPLE_ITEMS[0].id)); + cy.visit(buildItemPath(SAMPLE_ITEMS.items[0].id)); if (DEFAULT_ITEM_LAYOUT_MODE !== ITEM_LAYOUT_MODES.LIST) { cy.switchMode(ITEM_LAYOUT_MODES.LIST); } // share - const { id } = SAMPLE_ITEMS[2]; + const { id } = SAMPLE_ITEMS.items[2]; const member = MEMBERS.ANNA; shareItem({ id, member, permission: PERMISSION_LEVELS.READ }); diff --git a/cypress/integration/item/upload/gridItemUpload.spec.js b/cypress/integration/item/upload/gridItemUpload.spec.js index 362bbcf93..e76f13fc5 100644 --- a/cypress/integration/item/upload/gridItemUpload.spec.js +++ b/cypress/integration/item/upload/gridItemUpload.spec.js @@ -18,7 +18,7 @@ const dragUploadItem = (filenames) => { describe('Upload Item in Grid', () => { beforeEach(() => { - cy.setUpApi({ items: SAMPLE_ITEMS }); + cy.setUpApi(SAMPLE_ITEMS); }); describe('Drag Upload', () => { @@ -53,7 +53,7 @@ describe('Upload Item in Grid', () => { }); }); describe('upload item in item', () => { - const { id } = SAMPLE_ITEMS[0]; + const { id } = SAMPLE_ITEMS.items[0]; beforeEach(() => { cy.visit(buildItemPath(id)); diff --git a/cypress/integration/item/upload/listItemUpload.spec.js b/cypress/integration/item/upload/listItemUpload.spec.js index a019eddf9..914dc95f3 100644 --- a/cypress/integration/item/upload/listItemUpload.spec.js +++ b/cypress/integration/item/upload/listItemUpload.spec.js @@ -16,7 +16,7 @@ const dragUploadItem = (filenames) => { describe('Upload Item in List', () => { beforeEach(() => { - cy.setUpApi({ items: SAMPLE_ITEMS }); + cy.setUpApi(SAMPLE_ITEMS); }); describe('Drag Upload', () => { @@ -51,7 +51,7 @@ describe('Upload Item in List', () => { }); }); describe('upload item in item', () => { - const { id } = SAMPLE_ITEMS[0]; + const { id } = SAMPLE_ITEMS.items[0]; beforeEach(() => { cy.visit(buildItemPath(id)); diff --git a/cypress/integration/item/view/viewFolder.spec.js b/cypress/integration/item/view/viewFolder.spec.js index fe008e8f0..42ee16e5d 100644 --- a/cypress/integration/item/view/viewFolder.spec.js +++ b/cypress/integration/item/view/viewFolder.spec.js @@ -20,7 +20,7 @@ describe('View Space', () => { beforeEach(() => { cy.setUpApi({ items: [ - ...SAMPLE_ITEMS, + ...SAMPLE_ITEMS.items, GRAASP_LINK_ITEM, IMAGE_ITEM_DEFAULT, VIDEO_ITEM_S3, @@ -41,7 +41,7 @@ describe('View Space', () => { }); // visit child - const { id: childId } = SAMPLE_ITEMS[0]; + const { id: childId } = SAMPLE_ITEMS.items[0]; cy.goToItemInGrid(childId); // should get children @@ -53,7 +53,7 @@ describe('View Space', () => { }); // visit child - const { id: childChildId } = SAMPLE_ITEMS[2]; + const { id: childChildId } = SAMPLE_ITEMS.items[2]; cy.goToItemInGrid(childChildId); // expect no children @@ -71,7 +71,7 @@ describe('View Space', () => { }); it('visit item by id', () => { - const { id } = SAMPLE_ITEMS[0]; + const { id } = SAMPLE_ITEMS.items[0]; cy.visit(buildItemPath(id)); cy.switchMode(ITEM_LAYOUT_MODES.GRID); @@ -102,7 +102,7 @@ describe('View Space', () => { beforeEach(() => { cy.setUpApi({ items: [ - ...SAMPLE_ITEMS, + ...SAMPLE_ITEMS.items, GRAASP_LINK_ITEM, IMAGE_ITEM_DEFAULT, VIDEO_ITEM_S3, @@ -126,7 +126,7 @@ describe('View Space', () => { }); // visit child - const { id: childId } = SAMPLE_ITEMS[0]; + const { id: childId } = SAMPLE_ITEMS.items[0]; cy.goToItemInList(childId); // should get children @@ -138,7 +138,7 @@ describe('View Space', () => { }); // visit child - const { id: childChildId } = SAMPLE_ITEMS[2]; + const { id: childChildId } = SAMPLE_ITEMS.items[2]; cy.goToItemInList(childChildId); // expect no children @@ -156,8 +156,8 @@ describe('View Space', () => { }); it('visit folder by id', () => { - cy.setUpApi({ items: SAMPLE_ITEMS }); - const { id } = SAMPLE_ITEMS[0]; + cy.setUpApi(SAMPLE_ITEMS); + const { id } = SAMPLE_ITEMS.items[0]; cy.visit(buildItemPath(id)); if (DEFAULT_ITEM_LAYOUT_MODE !== ITEM_LAYOUT_MODES.LIST) { @@ -189,15 +189,15 @@ describe('View Space', () => { }); describe('Error Handling', () => { - it('visiting non-existing item display no item here', () => { - cy.setUpApi({ items: SAMPLE_ITEMS, getItemError: true }); - const { id } = SAMPLE_ITEMS[0]; - cy.visit(buildItemPath(id)); + // an item might either not exist or not be accessible + it.only('visiting non-existing item display error', () => { + cy.setUpApi({ ...SAMPLE_ITEMS, getItemError: true }); + cy.visit(buildItemPath('ecafbd2a-5688-22ac-ae93-0242ac130002')); // should get current item cy.wait('@getItem').then(() => { // wait for request to fail - cy.wait(5000); + cy.wait(2500); cy.get(`#${ITEM_SCREEN_ERROR_ALERT_ID}`).should('exist'); }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 7bd3d9634..f30839f03 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -15,7 +15,7 @@ import { mockPostItem, mockEditItem, mockShareItem, - mockGetMember, + mockGetMemberBy, mockDeleteItems, mockDefaultDownloadFile, mockGetS3Metadata, @@ -24,15 +24,25 @@ import { mockGetCurrentMember, mockSignInRedirection, mockSignOut, + mockPostItemLogin, + mockGetItemLogin, + mockGetItemMembershipsForItem, + mockGetItemTags, + mockGetTags, + mockPostItemTag, + mockPutItemLogin, } from './server'; import './commands/item'; import './commands/navigation'; +import { CURRENT_USER } from '../fixtures/members'; Cypress.Commands.add( 'setUpApi', ({ items = [], members = [], + currentMember = CURRENT_USER, + tags = [], deleteItemError = false, deleteItemsError = false, postItemError = false, @@ -47,6 +57,9 @@ Cypress.Commands.add( getS3MetadataError = false, getS3FileContentError = false, getCurrentMemberError = false, + postItemTagError = false, + postItemLoginError = false, + putItemLoginError = false, } = {}) => { const cachedItems = JSON.parse(JSON.stringify(items)); const cachedMembers = JSON.parse(JSON.stringify(members)); @@ -59,7 +72,10 @@ Cypress.Commands.add( mockDeleteItems(cachedItems, deleteItemsError); - mockGetItem(cachedItems, getItemError); + mockGetItem( + { items: cachedItems, currentMember }, + getItemError || getCurrentMemberError, + ); mockGetChildren(cachedItems); @@ -71,7 +87,7 @@ Cypress.Commands.add( mockShareItem(cachedItems, shareItemError); - mockGetMember(cachedMembers, getMemberError); + mockGetMemberBy(cachedMembers, getMemberError); mockUploadItem(cachedItems, defaultUploadError); @@ -81,11 +97,25 @@ Cypress.Commands.add( mockGetS3FileContent(getS3FileContentError); - mockGetCurrentMember(getCurrentMemberError); + mockGetCurrentMember(currentMember, getCurrentMemberError); mockSignInRedirection(); mockSignOut(); + + mockGetItemLogin(items); + + mockPostItemLogin(items, postItemLoginError); + + mockPutItemLogin(items, putItemLoginError); + + mockGetItemMembershipsForItem(items); + + mockGetTags(tags); + + mockGetItemTags(items); + + mockPostItemTag(items, postItemTagError); }, ); diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 61996e5b2..c7ec9bd3c 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -2,6 +2,6 @@ export const CREATE_ITEM_PAUSE = 1000; export const EDIT_ITEM_PAUSE = 1000; export const NAVIGATE_PAUSE = 500; export const PAGE_LOAD_WAITING_PAUSE = 3000; -export const REQUEST_FAILURE_LOADING_TIME = 7000; +export const REQUEST_FAILURE_LOADING_TIME = 4000; export const REDIRECTION_CONTENT = 'hello'; diff --git a/cypress/support/server.js b/cypress/support/server.js index 52b6c034f..edcadfe69 100644 --- a/cypress/support/server.js +++ b/cypress/support/server.js @@ -11,7 +11,7 @@ import { buildPostItemRoute, GET_OWN_ITEMS_ROUTE, buildShareItemWithRoute, - MEMBERS_ROUTE, + buildGetMemberBy, ITEMS_ROUTE, buildUploadFilesRoute, buildDownloadFilesRoute, @@ -20,6 +20,13 @@ import { GET_CURRENT_MEMBER_ROUTE, buildSignInPath, SIGN_OUT_ROUTE, + buildPostItemLoginSignInRoute, + buildGetItemLoginRoute, + buildGetItemMembershipForItemRoute, + buildGetItemTagsRoute, + GET_TAGS_ROUTE, + buildPutItemLoginSchema, + buildPostItemTagRoute, } from '../../src/api/routes'; import { getItemById, @@ -34,9 +41,17 @@ import { DEFAULT_GET, DEFAULT_POST, DEFAULT_DELETE, + DEFAULT_PUT, } from '../../src/api/utils'; -import { getS3FileExtra } from '../../src/utils/itemExtra'; +import { + getS3FileExtra, + getItemLoginExtra, + getItemLoginSchema, + buildItemLoginSchemaExtra, +} from '../../src/utils/itemExtra'; import { REDIRECTION_CONTENT } from './constants'; +import { SETTINGS } from '../../src/config/constants'; +import { ITEM_LOGIN_TAG } from '../fixtures/tags'; const API_HOST = Cypress.env('API_HOST'); const S3_FILES_HOST = Cypress.env('S3_FILES_HOST'); @@ -47,7 +62,10 @@ export const redirectionReply = { body: REDIRECTION_CONTENT, }; -export const mockGetCurrentMember = (shouldThrowError = false) => { +export const mockGetCurrentMember = ( + currentMember = MEMBERS.ANNA, + shouldThrowError = false, +) => { cy.intercept( { method: DEFAULT_GET.method, @@ -59,8 +77,7 @@ export const mockGetCurrentMember = (shouldThrowError = false) => { } // avoid sign in redirection - const current = MEMBERS.ANNA; - return reply(current); + return reply(currentMember); }, ).as('getCurrentMember'); }; @@ -130,7 +147,7 @@ export const mockDeleteItems = (items, shouldThrowError) => { { method: DEFAULT_DELETE.method, pathname: `/${ITEMS_ROUTE}`, - query: { id: ID_FORMAT }, + query: { id: new RegExp(ID_FORMAT) }, }, ({ url, reply }) => { const ids = qs.parse(url.slice(url.indexOf('?') + 1)).id; @@ -147,19 +164,33 @@ export const mockDeleteItems = (items, shouldThrowError) => { ).as('deleteItems'); }; -export const mockGetItem = (items, shouldThrowError) => { +export const mockGetItem = ({ items, currentMember }, shouldThrowError) => { cy.intercept( { method: DEFAULT_GET.method, url: new RegExp(`${API_HOST}/${buildGetItemRoute(ID_FORMAT)}$`), }, ({ url, reply }) => { - if (shouldThrowError) { - return reply({ statusCode: StatusCodes.BAD_REQUEST, body: null }); + const itemId = url.slice(API_HOST.length).split('/')[2]; + const item = getItemById(items, itemId); + + // item does not exist in db + if (!item) { + return reply({ + statusCode: StatusCodes.NOT_FOUND, + }); + } + + // mock membership + const creator = item?.creator; + const haveMembership = + creator === currentMember.id || + item.memberships?.find(({ memberId }) => memberId === currentMember.id); + + if (shouldThrowError || !haveMembership) { + return reply({ statusCode: StatusCodes.UNAUTHORIZED, body: null }); } - const id = url.slice(API_HOST.length).split('/')[2]; - const item = getItemById(items, id); return reply({ body: item, statusCode: StatusCodes.OK, @@ -281,27 +312,26 @@ export const mockShareItem = (items, shouldThrowError) => { ).as('shareItem'); }; -export const mockGetMember = (members, shouldThrowError) => { - const emailReg = new RegExp(EMAIL_FORMAT); +export const mockGetMemberBy = (members, shouldThrowError) => { cy.intercept( { method: DEFAULT_GET.method, - pathname: `/${MEMBERS_ROUTE}`, - query: { - email: emailReg, - }, + url: new RegExp( + `${API_HOST}/${parseStringToRegExp(buildGetMemberBy(EMAIL_FORMAT))}`, + ), }, ({ reply, url }) => { if (shouldThrowError) { return reply({ statusCode: StatusCodes.BAD_REQUEST }); } + const emailReg = new RegExp(EMAIL_FORMAT); const mail = emailReg.exec(url)[0]; const member = members.find(({ email }) => email === mail); return reply([member]); }, - ).as('getMember'); + ).as('getMemberBy'); }; // mock upload item for default and s3 upload methods @@ -331,11 +361,7 @@ export const mockDefaultDownloadFile = (items, shouldThrowError) => { cy.intercept( { method: DEFAULT_GET.method, - url: new RegExp( - `${API_HOST}/${parseStringToRegExp( - buildDownloadFilesRoute(ID_FORMAT), - )}$`, - ), + url: new RegExp(`${API_HOST}/${buildDownloadFilesRoute(ID_FORMAT)}$`), }, ({ reply, url }) => { if (shouldThrowError) { @@ -416,3 +442,152 @@ export const mockSignOut = () => { }, ).as('signOut'); }; + +export const mockPostItemLogin = (items, shouldThrowError) => { + cy.intercept( + { + method: DEFAULT_POST.method, + url: new RegExp( + `${API_HOST}/${buildPostItemLoginSignInRoute(ID_FORMAT)}$`, + ), + }, + ({ reply, url, body }) => { + if (shouldThrowError) { + reply({ statusCode: StatusCodes.BAD_REQUEST }); + return; + } + + // check query match item login schema + const id = url.slice(API_HOST.length).split('/')[2]; + const item = getItemById(items, id); + const itemLoginSchema = getItemLoginSchema(item.extra); + + // provide either username or member id + if (body.username) { + expect(body).not.to.have.keys('memberId'); + } else if (body.memberId) { + expect(body).not.to.have.keys('username'); + } + + // should have password if required + if ( + itemLoginSchema === + SETTINGS.ITEM_LOGIN.SIGN_IN_MODE.USERNAME_AND_PASSWORD + ) { + expect(body).to.have.keys('password'); + } + + reply({ + headers: { 'content-type': 'text/html' }, + statusCode: StatusCodes.OK, + }); + }, + ).as('postItemLogin'); +}; + +export const mockPutItemLogin = (items, shouldThrowError) => { + cy.intercept( + { + method: DEFAULT_PUT.method, + url: new RegExp(`${API_HOST}/${buildPutItemLoginSchema(ID_FORMAT)}$`), + }, + ({ reply, url, body }) => { + if (shouldThrowError) { + reply({ statusCode: StatusCodes.BAD_REQUEST }); + return; + } + + // check query match item login schema + const id = url.slice(API_HOST.length).split('/')[2]; + const item = getItemById(items, id); + + item.extra = buildItemLoginSchemaExtra(body.loginSchema); + + reply(item); + }, + ).as('putItemLogin'); +}; + +export const mockGetItemLogin = (items) => { + cy.intercept( + { + method: DEFAULT_GET.method, + url: new RegExp(`${API_HOST}/${buildGetItemLoginRoute(ID_FORMAT)}$`), + }, + ({ reply, url }) => { + const itemId = url.slice(API_HOST.length).split('/')[2]; + const item = items.find(({ id }) => itemId === id); + reply(getItemLoginExtra(item?.extra)); + }, + ).as('getItemLogin'); +}; + +export const mockGetItemMembershipsForItem = (items) => { + cy.intercept( + { + method: DEFAULT_GET.method, + url: new RegExp( + `${API_HOST}/${parseStringToRegExp( + buildGetItemMembershipForItemRoute(ID_FORMAT), + )}$`, + ), + }, + ({ reply, url }) => { + const { itemId } = qs.parse(url.slice(url.indexOf('?') + 1)); + const result = items.find(({ id }) => id === itemId).memberships || []; + reply(result); + }, + ).as('getItemMemberships'); +}; + +export const mockGetItemTags = (items) => { + cy.intercept( + { + method: DEFAULT_GET.method, + url: new RegExp(`${API_HOST}/${buildGetItemTagsRoute(ID_FORMAT)}$`), + }, + ({ reply, url }) => { + const itemId = url.slice(API_HOST.length).split('/')[2]; + const result = items.find(({ id }) => id === itemId).tags || []; + reply(result); + }, + ).as('getItemTags'); +}; + +export const mockGetTags = (tags) => { + cy.intercept( + { + method: DEFAULT_GET.method, + url: new RegExp(`${API_HOST}/${parseStringToRegExp(GET_TAGS_ROUTE)}$`), + }, + ({ reply }) => { + reply(tags); + }, + ).as('getTags'); +}; + +export const mockPostItemTag = (items, shouldThrowError) => { + cy.intercept( + { + method: DEFAULT_POST.method, + url: new RegExp(`${API_HOST}/${buildPostItemTagRoute(ID_FORMAT)}$`), + }, + ({ reply, url, body }) => { + if (shouldThrowError) { + reply({ statusCode: StatusCodes.BAD_REQUEST }); + return; + } + const itemId = url.slice(API_HOST.length).split('/')[2]; + const item = items.find(({ id }) => itemId === id); + + item.tags = [ + { + tagId: ITEM_LOGIN_TAG.id, + itemPath: item.path, + }, + ]; + + reply(body); + }, + ).as('postItemTag'); +}; diff --git a/cypress/support/utils.js b/cypress/support/utils.js index 82e7aed03..6c943557d 100644 --- a/cypress/support/utils.js +++ b/cypress/support/utils.js @@ -1,15 +1,22 @@ // use simple id format for tests -export const ID_FORMAT = '[a-z0-9-]*'; +export const ID_FORMAT = '(?=.*[0-9])(?=.*[a-zA-Z])([a-z0-9-]+)'; export const parseStringToRegExp = ( string, - { characters = ['?', '.'] } = {}, + { characters = ['?', '.'], parseQueryString = false } = {}, ) => { - let newString = string; + const [originalPathname, ...querystrings] = string.split('?'); + let pathname = originalPathname; + let querystring = querystrings.join('?'); characters.forEach((c) => { - newString = newString.replaceAll(c, `\\${c}`); + pathname = pathname.replaceAll(c, `\\${c}`); }); - return newString; + if (parseQueryString) { + characters.forEach((c) => { + querystring = querystring.replaceAll(c, `\\${c}`); + }); + } + return `${pathname}${querystring.length ? '\\?' : ''}${querystring}`; }; export const EMAIL_FORMAT = '[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+'; diff --git a/src/api/index.js b/src/api/index.js index e9331fb3c..9bf6eb7e1 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -2,3 +2,5 @@ export * from './member'; export * from './item'; export * from './membership'; export * from './authentication'; +export * from './itemTag'; +export * from './itemLogin'; diff --git a/src/api/itemLogin.js b/src/api/itemLogin.js new file mode 100644 index 000000000..3d03d612d --- /dev/null +++ b/src/api/itemLogin.js @@ -0,0 +1,47 @@ +import { API_HOST } from '../config/constants'; +import { failOnError, DEFAULT_POST, DEFAULT_GET, DEFAULT_PUT } from './utils'; +import { + buildGetItemLoginRoute, + buildPostItemLoginSignInRoute, + buildPutItemLoginSchema, +} from './routes'; + +// eslint-disable-next-line import/prefer-default-export +export const itemLoginSignIn = async ({ + itemId, + username, + memberId, + password, +}) => { + const res = await fetch( + `${API_HOST}/${buildPostItemLoginSignInRoute(itemId)}`, + { + ...DEFAULT_POST, + body: JSON.stringify({ + username: username?.trim(), + memberId: memberId?.trim(), + password, + }), + }, + ).then(failOnError); + + return res.ok; +}; + +export const getItemLogin = async (id) => { + const res = await fetch( + `${API_HOST}/${buildGetItemLoginRoute(id)}`, + DEFAULT_GET, + ).then(failOnError); + + return res.json(); +}; + +export const putItemLoginSchema = async ({ itemId, loginSchema }) => { + const res = await fetch(`${API_HOST}/${buildPutItemLoginSchema(itemId)}`, { + ...DEFAULT_PUT, + body: JSON.stringify({ loginSchema }), + }).then(failOnError); + + return res.json(); +}; diff --git a/src/api/itemTag.js b/src/api/itemTag.js new file mode 100644 index 000000000..91f0ded98 --- /dev/null +++ b/src/api/itemTag.js @@ -0,0 +1,49 @@ +import { API_HOST } from '../config/constants'; +import { + failOnError, + DEFAULT_DELETE, + DEFAULT_GET, + DEFAULT_POST, +} from './utils'; +import { + buildDeleteItemTagRoute, + buildGetItemTagsRoute, + buildPostItemTagRoute, + GET_TAGS_ROUTE, +} from './routes'; + +export const getTags = async () => { + const res = await fetch(`${API_HOST}/${GET_TAGS_ROUTE}`, DEFAULT_GET).then( + failOnError, + ); + + return res.json(); +}; + +export const getItemTags = async (id) => { + const res = await fetch( + `${API_HOST}/${buildGetItemTagsRoute(id)}`, + DEFAULT_GET, + ).then(failOnError); + + return res.json(); +}; + +// payload: tagId, itemPath, creator +export const postItemTag = async ({ id, tagId, itemPath, creator }) => { + const res = await fetch(`${API_HOST}/${buildPostItemTagRoute(id)}`, { + ...DEFAULT_POST, + body: JSON.stringify({ tagId, itemPath, creator }), + }).then(failOnError); + + return res.json(); +}; + +export const deleteItemTag = async ({ id, tagId }) => { + const res = await fetch( + `${API_HOST}/${buildDeleteItemTagRoute({ id, tagId })}`, + DEFAULT_DELETE, + ).then(failOnError); + + return res.ok; +}; diff --git a/src/api/routes.js b/src/api/routes.js index 4772261ba..d39646286 100644 --- a/src/api/routes.js +++ b/src/api/routes.js @@ -44,3 +44,14 @@ export const buildSignInPath = (to) => { return `signin${queryString}`; }; export const SIGN_OUT_ROUTE = 'logout'; +export const buildGetItemTagsRoute = (id) => `${ITEMS_ROUTE}/${id}/tags`; +export const buildPostItemTagRoute = (id) => `${ITEMS_ROUTE}/${id}/tags`; +export const buildPutItemLoginSchema = (id) => + `${ITEMS_ROUTE}/${id}/login-schema`; +export const buildDeleteItemTagRoute = ({ id, tagId }) => + `${ITEMS_ROUTE}/${id}/tags/${tagId}`; +export const buildPostItemLoginSignInRoute = (id) => + `${ITEMS_ROUTE}/${id}/login`; +export const GET_TAGS_ROUTE = `${ITEMS_ROUTE}/tags`; +export const buildGetItemLoginRoute = (id) => + `${ITEMS_ROUTE}/${id}/login-schema`; diff --git a/src/api/utils.js b/src/api/utils.js index afe5f9a46..8cf012e73 100644 --- a/src/api/utils.js +++ b/src/api/utils.js @@ -23,6 +23,7 @@ export const DEFAULT_PATCH = { export const DEFAULT_PUT = { method: 'PUT', + headers: { 'Content-Type': 'application/json' }, credentials: 'include', }; diff --git a/src/components/App.js b/src/components/App.js index 287fd6e19..027104128 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -17,6 +17,7 @@ import SharedItems from './SharedItems'; import Main from './main/Main'; import Authorization from './common/Authorization'; import ModalProviders from './context/ModalProviders'; +import ItemLoginAuthorization from './common/ItemLoginAuthorization'; const App = () => ( @@ -31,7 +32,7 @@ const App = () => ( /> diff --git a/src/components/SharedItems.js b/src/components/SharedItems.js index 6aa83f285..532413c87 100644 --- a/src/components/SharedItems.js +++ b/src/components/SharedItems.js @@ -1,18 +1,22 @@ import React from 'react'; import { List } from 'immutable'; import { useTranslation } from 'react-i18next'; -import { SHARED_ITEMS_ID } from '../config/selectors'; +import { + SHARED_ITEMS_ERROR_ALERT_ID, + SHARED_ITEMS_ID, +} from '../config/selectors'; import ItemHeader from './item/header/ItemHeader'; +import ErrorAlert from './common/ErrorAlert'; import Items from './main/Items'; import { useSharedItems } from '../hooks'; import Loader from './common/Loader'; const SharedItems = () => { const { t } = useTranslation(); - const { data: sharedItems, isLoading, isError, error } = useSharedItems(); + const { data: sharedItems, isLoading, isError } = useSharedItems(); if (isError) { - return error; + return ; } if (isLoading) { diff --git a/src/components/common/ErrorAlert.js b/src/components/common/ErrorAlert.js new file mode 100644 index 000000000..317f9388c --- /dev/null +++ b/src/components/common/ErrorAlert.js @@ -0,0 +1,22 @@ +import Alert from '@material-ui/lab/Alert'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; + +const ErrorAlert = ({ id }) => { + const { t } = useTranslation(); + return ( + + {t('An error occured.')} + + ); +}; + +ErrorAlert.propTypes = { + id: PropTypes.string, +}; +ErrorAlert.defaultProps = { + id: null, +}; + +export default ErrorAlert; diff --git a/src/components/common/ForbiddenText.js b/src/components/common/ForbiddenText.js new file mode 100644 index 000000000..621c06140 --- /dev/null +++ b/src/components/common/ForbiddenText.js @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Typography } from '@material-ui/core'; +import { useTranslation } from 'react-i18next'; + +const ForbiddenText = ({ id }) => { + const { t } = useTranslation(); + + return ( + + {t('You cannot access this item')} + + ); +}; + +ForbiddenText.propTypes = { + id: PropTypes.string, +}; + +ForbiddenText.defaultProps = { + id: null, +}; + +export default ForbiddenText; diff --git a/src/components/common/ItemLoginAuthorization.js b/src/components/common/ItemLoginAuthorization.js new file mode 100644 index 000000000..5613a4494 --- /dev/null +++ b/src/components/common/ItemLoginAuthorization.js @@ -0,0 +1,98 @@ +import React from 'react'; +import { useMutation } from 'react-query'; +import { useParams } from 'react-router'; +import { useTranslation } from 'react-i18next'; +import { StatusCodes, getReasonPhrase } from 'http-status-codes'; +import { Button, Container, makeStyles, Typography } from '@material-ui/core'; +import ItemLoginSignInScreen from '../item/ItemLoginSignInScreen'; +import ForbiddenText from './ForbiddenText'; +import { + ITEM_LOGIN_SCREEN_FORBIDDEN_ID, + ITEM_SCREEN_ERROR_ALERT_ID, +} from '../../config/selectors'; +import { useCurrentMember } from '../../hooks/member'; +import { useItem, useItemLogin } from '../../hooks/item'; +import Loader from './Loader'; +import { SIGN_OUT_MUTATION_KEY } from '../../config/keys'; +import ErrorAlert from './ErrorAlert'; + +const useStyles = makeStyles(() => ({ + container: { + textAlign: 'center', + }, +})); + +const ItemLoginAuthorization = () => (ChildComponent) => { + const ComposedComponent = () => { + const classes = useStyles(); + const { t } = useTranslation(); + const { itemId } = useParams(); + const { data: user, isLoading: isMemberLoading } = useCurrentMember(); + const { data: itemLogin, isLoading: isItemLoginLoading } = useItemLogin( + itemId, + ); + const { + data: item, + isLoading: isItemLoading, + error, + isError: isItemError, + } = useItem(itemId); + const { mutate: signOut } = useMutation(SIGN_OUT_MUTATION_KEY); + + const handleSignOut = () => { + signOut(); + }; + + const renderAuthenticatedAlternative = () => ( + <> + + {t('or')} + + {t('Ask the creator to share this item with you')} + + + ); + + if (isMemberLoading || isItemLoginLoading || isItemLoading) { + // get item login if the user is not authenticated and the item is empty + return ; + } + + if ( + isItemError && + [ + getReasonPhrase(StatusCodes.BAD_REQUEST), + getReasonPhrase(StatusCodes.NOT_FOUND), + ].includes(error.message) + ) { + return ; + } + + // signed out but can sign in with item login + if ((!user || user.isEmpty()) && itemLogin && !itemLogin.isEmpty()) { + return ; + } + + // the item could be fetched without errors + // because the user is signed in and has access + if (item && !item.isEmpty()) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; + } + + // either the item does not allow item login + // or the user is already signed in as normal user and hasn't the access to this item + return ( + + + {user && !user.isEmpty() && renderAuthenticatedAlternative()} + + ); + }; + + return ComposedComponent; +}; + +export default ItemLoginAuthorization; diff --git a/src/components/item/ItemLoginSignInScreen.js b/src/components/item/ItemLoginSignInScreen.js new file mode 100644 index 000000000..82d8b6e33 --- /dev/null +++ b/src/components/item/ItemLoginSignInScreen.js @@ -0,0 +1,261 @@ +import React, { useRef, useState } from 'react'; +import { + Container, + TextField, + Typography, + makeStyles, + Button, + Tooltip, +} from '@material-ui/core'; +import { useParams } from 'react-router'; +import { useMutation } from 'react-query'; +import InfoIcon from '@material-ui/icons/Info'; +import Select from '@material-ui/core/Select'; +import MenuItem from '@material-ui/core/MenuItem'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import { useTranslation } from 'react-i18next'; +import { + SETTINGS, + SETTINGS_ITEM_LOGIN_SIGN_IN_MODE_DEFAULT, +} from '../../config/constants'; +import ForbiddenText from '../common/ForbiddenText'; +import { + ITEM_LOGIN_SCREEN_ID, + ITEM_LOGIN_SIGN_IN_PASSWORD_ID, + ITEM_LOGIN_SIGN_IN_USERNAME_ID, + ITEM_LOGIN_SIGN_IN_BUTTON_ID, + ITEM_LOGIN_SIGN_IN_MEMBER_ID_ID, + ITEM_LOGIN_SIGN_IN_MODE_ID, + buildItemLoginSignInModeOption, +} from '../../config/selectors'; +import { isMemberIdValid } from '../../utils/member'; +import { useItemLogin } from '../../hooks/item'; +import Loader from '../common/Loader'; +import { ITEM_LOGIN_MUTATION_KEY } from '../../config/keys'; + +const useStyles = makeStyles((theme) => ({ + wrapper: { + margin: 'auto', + display: 'flex', + flexDirection: 'column', + textAlign: 'center', + }, + input: { + margin: theme.spacing(1, 0), + }, + usernameAndMemberId: { + display: 'flex', + alignItems: 'center', + }, + usernameInfo: { + margin: theme.spacing(0, 1), + }, + signInWithWrapper: { + justifyContent: 'flex-end', + marginLeft: theme.spacing(0), + }, + signInWithWrapperLabel: { + marginRight: theme.spacing(1), + }, +})); + +function ItemLoginSignInScreen() { + const { t } = useTranslation(); + const classes = useStyles(); + const { mutate: itemLoginSignIn } = useMutation(ITEM_LOGIN_MUTATION_KEY); + const loginModeRef = useRef(null); + const [password, setPassword] = useState(null); + const [username, setUsername] = useState(null); + const [memberId, setMemberId] = useState(null); + const [signInMode, setSignInMode] = useState( + SETTINGS_ITEM_LOGIN_SIGN_IN_MODE_DEFAULT, + ); + const { itemId } = useParams(); + const { data: itemLogin, isLoading } = useItemLogin(itemId); + const loginSchema = itemLogin?.get('loginSchema'); + + if (isLoading) { + return ; + } + + // no item login detected + if ( + !itemLogin || + itemLogin.isEmpty() || + !Object.values(SETTINGS.ITEM_LOGIN.OPTIONS).includes(loginSchema) + ) { + return ; + } + + const withPassword = [ + SETTINGS.ITEM_LOGIN.OPTIONS.USERNAME_AND_PASSWORD, + ].includes(loginSchema); + + const onClickSignIn = () => { + const signInProperties = {}; + switch (signInMode) { + case SETTINGS.ITEM_LOGIN.SIGN_IN_MODE.MEMBER_ID: + signInProperties.memberId = memberId; + break; + case SETTINGS.ITEM_LOGIN.SIGN_IN_MODE.USERNAME: + default: + signInProperties.username = username; + } + + if (withPassword) { + signInProperties.password = password; + } + + itemLoginSignIn({ itemId, ...signInProperties }); + }; + + const handleOnSignInModeChange = (e) => { + const { value } = e.target; + setSignInMode(value); + }; + + const onPasswordChange = (e) => { + setPassword(e.target.value); + }; + + const onMemberIdChange = (e) => { + setMemberId(e.target.value); + }; + + const onUsernameChange = (e) => { + setUsername(e.target.value); + }; + + const shouldSignInBeDisabled = () => { + const usernameError = + signInMode === SETTINGS.ITEM_LOGIN.SIGN_IN_MODE.USERNAME && + (!username?.length || isMemberIdValid(username)); + const memberIdError = + signInMode === SETTINGS.ITEM_LOGIN.SIGN_IN_MODE.MEMBER_ID && + !isMemberIdValid(memberId); + const passwordError = withPassword && !password?.length; + return usernameError || passwordError || memberIdError; + }; + + const renderTextField = () => { + switch (signInMode) { + case SETTINGS.ITEM_LOGIN.SIGN_IN_MODE.MEMBER_ID: { + return ( + + ); + } + case SETTINGS.ITEM_LOGIN.SIGN_IN_MODE.USERNAME: + default: { + const isMemberId = isMemberIdValid(username); + const error = username?.length && isMemberId; + const helperText = isMemberId + ? t('This is a member id. You should switch the sign in mode.') + : null; + return ( + + ); + } + } + }; + + const renderUsernameAndMemberIdField = () => { + const select = ( + <> + + + + + + ); + + return ( + <> + +
{renderTextField()}
+ + ); + }; + + return ( + + {t('Item Login Sign In')} + {renderUsernameAndMemberIdField()} + {withPassword && ( + + )} + + + ); +} + +export default ItemLoginSignInScreen; diff --git a/src/components/item/ItemMain.js b/src/components/item/ItemMain.js index 76e015cc5..520e26a3a 100644 --- a/src/components/item/ItemMain.js +++ b/src/components/item/ItemMain.js @@ -6,6 +6,7 @@ import PropTypes from 'prop-types'; import { RIGHT_MENU_WIDTH } from '../../config/constants'; import ItemHeader from './header/ItemHeader'; import ItemPanel from './ItemPanel'; +import { ITEM_MAIN_CLASS } from '../../config/selectors'; const useStyles = makeStyles((theme) => ({ root: {}, @@ -45,7 +46,7 @@ const useStyles = makeStyles((theme) => ({ }, })); -const ItemMain = ({ children, item }) => { +const ItemMain = ({ id, children, item }) => { const classes = useStyles(); const [metadataMenuOpen, setMetadataMenuOpen] = useState(true); @@ -54,7 +55,7 @@ const ItemMain = ({ children, item }) => { }; return ( - <> +
{ {children}
- +
); }; ItemMain.propTypes = { children: PropTypes.node.isRequired, item: PropTypes.instanceOf(Map).isRequired, + id: PropTypes.string, +}; + +ItemMain.defaultProps = { + id: null, }; export default ItemMain; diff --git a/src/components/item/ItemPanel.js b/src/components/item/ItemPanel.js index 174e5252a..9e13b3e29 100644 --- a/src/components/item/ItemPanel.js +++ b/src/components/item/ItemPanel.js @@ -19,6 +19,7 @@ import { ITEM_PANEL_TABLE_ID, } from '../../config/selectors'; import { getFileExtra, getS3FileExtra } from '../../utils/itemExtra'; +import ItemSettings from './settings/ItemSettings'; const styles = (theme) => ({ drawer: { @@ -155,6 +156,8 @@ class ItemPanel extends Component { {!selectedChild && this.renderItemContent(item)} {selectedChild && this.renderItemContent(selectedChild)} + + ); } diff --git a/src/components/item/settings/ItemLoginSetting.js b/src/components/item/settings/ItemLoginSetting.js new file mode 100644 index 000000000..13fef46b4 --- /dev/null +++ b/src/components/item/settings/ItemLoginSetting.js @@ -0,0 +1,142 @@ +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import Select from '@material-ui/core/Select'; +import Switch from '@material-ui/core/Switch'; +import { useParams } from 'react-router'; +import { useMutation } from 'react-query'; +import { FormControlLabel } from '@material-ui/core'; +import MenuItem from '@material-ui/core/MenuItem'; +import { + useTags, + useItem, + useItemTags, + useCurrentMember, +} from '../../../hooks'; +import { + SETTINGS, + SETTINGS_ITEM_LOGIN_DEFAULT, +} from '../../../config/constants'; +import { + getItemLoginSchema, + getItemLoginTagFromItem, +} from '../../../utils/itemExtra'; +import { + buildItemLoginSettingModeSelectOption, + ITEM_LOGIN_SETTING_MODE_SELECT_ID, + ITEM_LOGIN_SETTING_SWITCH_ID, +} from '../../../config/selectors'; +import { + DELETE_ITEM_TAG_MUTATION_KEY, + POST_ITEM_TAG_MUTATION_KEY, + PUT_ITEM_LOGIN_MUTATION_KEY, +} from '../../../config/keys'; +import Loader from '../../common/Loader'; +import { getItemLoginTag } from '../../../utils/tag'; + +const ItemLoginSwitch = () => { + const { t } = useTranslation(); + + // user + const { data: user, isLoading: isMemberLoading } = useCurrentMember(); + + // current item + const { itemId } = useParams(); + const { data: item, isLoading: isItemLoading } = useItem(itemId); + + // mutations + const { mutate: putItemLoginSchema } = useMutation( + PUT_ITEM_LOGIN_MUTATION_KEY, + ); + const { mutate: deleteItemTag } = useMutation(DELETE_ITEM_TAG_MUTATION_KEY); + const { mutate: postItemTag } = useMutation(POST_ITEM_TAG_MUTATION_KEY); + + // item login tag and item extra value + const { data: tags, isLoading: isTagsLoading } = useTags(); + const { data: itemTags, isLoading: isItemTagsLoading } = useItemTags(itemId); + const [isItemLoginEnabled, setIsItemLoginEnabled] = useState(false); + const [isSwitchDisabled, setIsSwitchDisabled] = useState(false); + const [schema, setSchema] = useState(SETTINGS.ITEM_LOGIN.OPTIONS.USERNAME); + const [ItemLoginTagValueForItem, setItemLoginTagValueForItem] = useState(); + + // update state variables depending on fetch values + useEffect(() => { + const tagValue = getItemLoginTagFromItem({ tags, itemTags }); + setItemLoginTagValueForItem(tagValue); + setIsItemLoginEnabled(Boolean(tagValue)); + // disable setting if it is set on a parent + setIsSwitchDisabled(tagValue && tagValue?.itemPath !== item?.get('path')); + setSchema( + getItemLoginSchema(item?.get('extra')) || + SETTINGS.ITEM_LOGIN.OPTIONS.USERNAME, + ); + }, [tags, itemTags, item]); + + if (isItemLoading || isTagsLoading || isItemTagsLoading || isMemberLoading) { + return ; + } + + const updateItemLoginSchema = (loginSchema) => { + putItemLoginSchema({ itemId, loginSchema }); + setSchema(loginSchema); + }; + + const handleSwitchChange = () => { + if (!isItemLoginEnabled) { + postItemTag({ + id: itemId, + // use item login tag id + tagId: getItemLoginTag(tags)?.id, + itemPath: item?.get('path'), + creator: user?.get('id'), + }); + updateItemLoginSchema(SETTINGS_ITEM_LOGIN_DEFAULT); + } else { + // use item tag id corresponding to item login + deleteItemTag({ id: itemId, tagId: ItemLoginTagValueForItem?.id }); + updateItemLoginSchema(); + } + }; + + const handleOptionOnChange = (e) => { + const { value } = e.target; + updateItemLoginSchema(value); + }; + + const renderSelect = () => ( + + ); + + const control = ( + + ); + + return ( + <> + + {!isSwitchDisabled && isItemLoginEnabled && renderSelect()} + + ); +}; + +export default ItemLoginSwitch; diff --git a/src/components/item/settings/ItemSettings.js b/src/components/item/settings/ItemSettings.js new file mode 100644 index 000000000..c35ebdaca --- /dev/null +++ b/src/components/item/settings/ItemSettings.js @@ -0,0 +1,52 @@ +import React from 'react'; +import Container from '@material-ui/core/Container'; +import Typography from '@material-ui/core/Typography'; +import { useParams } from 'react-router'; +import { useTranslation } from 'react-i18next'; +import { makeStyles } from '@material-ui/core'; +import ItemLoginSetting from './ItemLoginSetting'; +import { isSettingsEditionAllowedForUser } from '../../../utils/membership'; +import { useCurrentMember, useItemMemberships } from '../../../hooks'; +import Loader from '../../common/Loader'; + +const useStyles = makeStyles((theme) => ({ + title: { + margin: 0, + padding: 0, + }, + wrapper: { + marginTop: theme.spacing(2), + }, +})); + +const ItemSettings = () => { + const { t } = useTranslation(); + const classes = useStyles(); + const { itemId } = useParams(); + const { + data: memberships, + isLoading: isMembershipsLoading, + } = useItemMemberships(itemId); + const { data: user, isLoading: isMemberLoading } = useCurrentMember(); + const memberId = user?.get('id'); + + if (isMembershipsLoading || isMemberLoading) { + return ; + } + + // settings are not available for user without edition membership + if (!isSettingsEditionAllowedForUser({ memberships, memberId })) { + return null; + } + + return ( + + + {t('Settings')} + + + + ); +}; + +export default ItemSettings; diff --git a/src/components/layout/Navigation.js b/src/components/layout/Navigation.js index 166a9558c..b51d764fe 100644 --- a/src/components/layout/Navigation.js +++ b/src/components/layout/Navigation.js @@ -55,7 +55,7 @@ const Navigation = () => { // build root depending on user permission or pathname // todo: consider accessing from guest const ownItem = - pathname === HOME_PATH || item?.get('creator') === user.get('id'); + pathname === HOME_PATH || item?.get('creator') === user?.get('id'); const to = ownItem ? HOME_PATH : SHARED_ITEMS_PATH; const text = ownItem ? t('My Items') : t('Shared Items'); diff --git a/src/components/main/Home.js b/src/components/main/Home.js index 40d8ebc70..61a850ee0 100644 --- a/src/components/main/Home.js +++ b/src/components/main/Home.js @@ -6,17 +6,18 @@ import { List } from 'immutable'; import ItemHeader from '../item/header/ItemHeader'; import Items from './Items'; import FileUploader from './FileUploader'; -import { OWNED_ITEMS_ID } from '../../config/selectors'; +import { HOME_ERROR_ALERT_ID, OWNED_ITEMS_ID } from '../../config/selectors'; import { useOwnItems } from '../../hooks'; import Loader from '../common/Loader'; +import ErrorAlert from '../common/ErrorAlert'; const Home = () => { const { t } = useTranslation(); // get own items - const { data: ownItems, isLoading, isError, error } = useOwnItems(); + const { data: ownItems, isLoading, isError } = useOwnItems(); if (isError) { - return error; + return ; } if (isLoading) { diff --git a/src/components/main/ItemScreen.js b/src/components/main/ItemScreen.js index 3ba344308..adaf64bbc 100644 --- a/src/components/main/ItemScreen.js +++ b/src/components/main/ItemScreen.js @@ -1,7 +1,4 @@ import React from 'react'; -import Alert from '@material-ui/lab/Alert'; -import { Map, List } from 'immutable'; -import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router'; import { makeStyles } from '@material-ui/core'; import { useChildren, useItem } from '../../hooks'; @@ -14,6 +11,7 @@ import S3FileItem from '../item/S3FileItem'; import ItemMain from '../item/ItemMain'; import LinkItem from '../item/LinkItem'; import Loader from '../common/Loader'; +import ErrorAlert from '../common/ErrorAlert'; const useStyles = makeStyles(() => ({ fileWrapper: { @@ -25,17 +23,14 @@ const useStyles = makeStyles(() => ({ })); const ItemScreen = () => { - const { t } = useTranslation(); const classes = useStyles(); const { itemId } = useParams(); - const { data, isLoading } = useItem(itemId); - const item = Map(data); + const { data: item, isLoading } = useItem(itemId); // display children - const { data: childrenRaw } = useChildren(itemId); - const children = List(childrenRaw); + const { data: children, isLoading: isChildrenLoading } = useChildren(itemId); const renderContent = () => { switch (item.get('type')) { @@ -58,6 +53,11 @@ const ItemScreen = () => { ); case ITEM_TYPES.FOLDER: + // wait until all children are available + if (isChildrenLoading) { + return ; + } + // display children return ( <> @@ -67,25 +67,16 @@ const ItemScreen = () => { ); default: - return ( - - {t('An error occured.')} - - ); + return ; } }; - // wait until all children are available if (isLoading) { return ; } if (!item || !item.get('id')) { - return ( - - {t('An error occured.')} - - ); + return ; } return {renderContent()}; diff --git a/src/components/main/ItemsTable.js b/src/components/main/ItemsTable.js index f39309e52..5e5694f4d 100644 --- a/src/components/main/ItemsTable.js +++ b/src/components/main/ItemsTable.js @@ -323,13 +323,14 @@ const ItemsTable = ({ items: rows, tableTitle, id: tableId }) => { }; ItemsTable.propTypes = { - items: PropTypes.instanceOf(List).isRequired, + items: PropTypes.instanceOf(List), tableTitle: PropTypes.string.isRequired, id: PropTypes.string, }; ItemsTable.defaultProps = { id: '', + items: List(), }; export default ItemsTable; diff --git a/src/config/constants.js b/src/config/constants.js index 8f20a7134..24e1643f0 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -75,6 +75,11 @@ export const PERMISSION_LEVELS = { export const DEFAULT_PERMISSION_LEVEL = PERMISSION_LEVELS.WRITE; +export const PERMISSIONS_EDITION_ALLOWED = [ + PERMISSION_LEVELS.WRITE, + PERMISSION_LEVELS.ADMIN, +]; + export const ITEM_LAYOUT_MODES = { GRID: 'grid', LIST: 'list', @@ -120,3 +125,20 @@ export const STALE_TIME_MILLISECONDS = 1000 * 60 * 60; export const CACHE_TIME_MILLISECONDS = 1000 * 60 * 60; export const LOADING_CONTENT = '…'; +export const SETTINGS = { + ITEM_LOGIN: { + name: 'item-login', + OPTIONS: { + USERNAME: 'username', + USERNAME_AND_PASSWORD: 'username+password', + }, + SIGN_IN_MODE: { + USERNAME: 'username', + MEMBER_ID: 'memberId', + }, + }, +}; + +export const SETTINGS_ITEM_LOGIN_DEFAULT = SETTINGS.ITEM_LOGIN.OPTIONS.USERNAME; +export const SETTINGS_ITEM_LOGIN_SIGN_IN_MODE_DEFAULT = + SETTINGS.ITEM_LOGIN.SIGN_IN_MODE.USERNAME; diff --git a/src/config/keys.js b/src/config/keys.js index 54e6d128d..17c0defe6 100644 --- a/src/config/keys.js +++ b/src/config/keys.js @@ -22,3 +22,11 @@ export const MOVE_ITEM_MUTATION_KEY = 'moveItem'; export const SHARE_ITEM_MUTATION_KEY = 'shareItem'; export const FILE_UPLOAD_MUTATION_KEY = 'fileUpload'; export const SIGN_OUT_MUTATION_KEY = 'signOut'; +export const ITEM_LOGIN_MUTATION_KEY = 'itemLoginSignIn'; +export const buildItemMembershipsKey = (id) => [ITEMS_KEY, id, 'memberships']; +export const buildItemLoginKey = (id) => [ITEMS_KEY, id, 'login']; +export const PUT_ITEM_LOGIN_MUTATION_KEY = 'putItemLogin'; +export const ITEM_TAGS = 'itemTags'; +export const POST_ITEM_TAG_MUTATION_KEY = 'postItemTags'; +export const buildItemTagsKey = (id) => [ITEMS_KEY, id, 'tags']; +export const DELETE_ITEM_TAG_MUTATION_KEY = 'deleteItemTag'; diff --git a/src/config/messages.js b/src/config/messages.js index ef3803267..9a44e97d2 100644 --- a/src/config/messages.js +++ b/src/config/messages.js @@ -30,3 +30,11 @@ export const UPLOAD_FILES_PROGRESS_MESSAGE = 'The file(s) are in queue for uploading. Please wait a moment.'; export const SIGN_OUT_ERROR_MESSAGE = 'There was an error while signing out.'; export const SIGN_OUT_SUCCESS_MESSAGE = 'You successfully signed out.'; + +// todo: customize settings +export const DELETE_ITEM_TAG_ERROR_MESSAGE = + 'There was an error while deleting the tag.'; +export const POST_ITEM_TAG_ERROR_MESSAGE = + 'There was an error while posting the tag.'; +export const ITEM_LOGIN_SIGN_IN_ERROR_MESSAGE = + 'There was an error while signing in.'; diff --git a/src/config/selectors.js b/src/config/selectors.js index 30b340089..6bc2a8b96 100644 --- a/src/config/selectors.js +++ b/src/config/selectors.js @@ -1,3 +1,5 @@ +const parseStringForId = (string) => string.replaceAll('+', ''); + export const ITEM_DELETE_BUTTON_CLASS = 'itemDeleteButton'; export const buildItemCard = (id) => `itemCard-${id}`; export const CREATE_ITEM_BUTTON_ID = 'createItemButton'; @@ -53,3 +55,20 @@ export const HEADER_APP_BAR_ID = 'headerAppBar'; export const HEADER_USER_ID = 'headerUser'; export const USER_MENU_SIGN_OUT_OPTION_ID = 'userMenuSignOutOption'; export const NAVIGATION_HIDDEN_PARENTS_ID = 'navigationHiddenParents'; +export const ITEM_LOGIN_SCREEN_ID = 'itemLoginScreen'; +export const ITEM_LOGIN_SIGN_IN_USERNAME_ID = 'itemLoginSignInUsername'; +export const ITEM_LOGIN_SIGN_IN_PASSWORD_ID = 'itemLoginSignInPassword'; +export const ITEM_LOGIN_SIGN_IN_BUTTON_ID = 'itemLoginSignInButton'; +export const ITEM_SCREEN_MAIN_ID = 'itemScreenMain'; +export const ITEM_LOGIN_SCREEN_FORBIDDEN_ID = 'itemLoginScreenForbidden'; +export const ITEM_LOGIN_SETTING_SWITCH_ID = 'itemLoginSettingSwitch'; +export const ITEM_LOGIN_SETTING_MODE_SELECT_ID = 'itemLoginSettingModeSelect'; +export const buildItemLoginSettingModeSelectOption = (id) => + `itemLoginSettingModeSelectOptions-${parseStringForId(id)}`; +export const ITEM_LOGIN_SIGN_IN_MEMBER_ID_ID = 'itemLoginSignInMemberId'; +export const ITEM_LOGIN_SIGN_IN_MODE_ID = 'itemLoginSignInMode'; +export const buildItemLoginSignInModeOption = (id) => + `itemLoginSignInModeOption-${id}`; +export const ITEM_MAIN_CLASS = 'itemMain'; +export const HOME_ERROR_ALERT_ID = 'homeErrorAlert'; +export const SHARED_ITEMS_ERROR_ALERT_ID = 'sharedItemsErrorAlert'; diff --git a/src/hooks/index.js b/src/hooks/index.js index 41fd805f3..6ff81b201 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -1,2 +1,3 @@ export * from './item'; export * from './member'; +export * from './tag'; diff --git a/src/hooks/item.js b/src/hooks/item.js index 58e550b39..a079c88d3 100644 --- a/src/hooks/item.js +++ b/src/hooks/item.js @@ -5,6 +5,8 @@ import queryClient from '../config/queryClient'; import { buildChildren, buildGetItem, + buildItemLoginQuery, + buildItemMembershipsQuery, buildOwnItems, buildParents, buildSharedItems, @@ -66,3 +68,15 @@ export const useSharedItems = () => export const useItem = (id) => useQuery({ ...buildGetItem(id), enabled: Boolean(id) }); + +export const useItemMemberships = (id) => + useQuery({ + ...buildItemMembershipsQuery(id), + enabled: Boolean(id), + }); + +export const useItemLogin = (id) => + useQuery({ + ...buildItemLoginQuery(id), + enabled: Boolean(id), + }); diff --git a/src/hooks/member.js b/src/hooks/member.js index 0c9f2adbd..62565f2e1 100644 --- a/src/hooks/member.js +++ b/src/hooks/member.js @@ -1,10 +1,13 @@ import { useQuery } from 'react-query'; import { Map } from 'immutable'; import * as Api from '../api'; +import { queryConfig } from './utils'; import { CURRENT_MEMBER_KEY } from '../config/keys'; // eslint-disable-next-line import/prefer-default-export export const useCurrentMember = () => - useQuery(CURRENT_MEMBER_KEY, () => - Api.getCurrentMember().then((data) => Map(data)), - ); + useQuery({ + queryKey: CURRENT_MEMBER_KEY, + queryFn: () => Api.getCurrentMember().then((data) => Map(data)), + ...queryConfig, + }); diff --git a/src/hooks/tag.js b/src/hooks/tag.js new file mode 100644 index 000000000..9fe388e4e --- /dev/null +++ b/src/hooks/tag.js @@ -0,0 +1,10 @@ +import { useQuery } from 'react-query'; +import { buildItemTagsQuery, buildTagsQuery } from './utils'; + +export const useTags = () => useQuery(buildTagsQuery()); + +export const useItemTags = (id) => + useQuery({ + ...buildItemTagsQuery(id), + enabled: Boolean(id), + }); diff --git a/src/hooks/utils.js b/src/hooks/utils.js index 1cd5d1a1b..17d669e26 100644 --- a/src/hooks/utils.js +++ b/src/hooks/utils.js @@ -1,8 +1,13 @@ import { List, Map } from 'immutable'; +import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { buildItemChildrenKey, buildItemKey, + buildItemLoginKey, + buildItemMembershipsKey, buildItemParentsKey, + buildItemTagsKey, + ITEM_TAGS, OWN_ITEMS_KEY, SHARED_ITEMS_KEY, } from '../config/keys'; @@ -12,37 +17,69 @@ import { CACHE_TIME_MILLISECONDS, } from '../config/constants'; -const itemQueryConfig = { +export const queryConfig = { staleTime: STALE_TIME_MILLISECONDS, // time until data in cache considered stale if cache not invalidated cacheTime: CACHE_TIME_MILLISECONDS, // time before cache labeled as inactive to be garbage collected + retry: (failureCount, error) => { + // do not retry if the request was not authorized + // the user is probably not signed in + if (error.name === getReasonPhrase(StatusCodes.UNAUTHORIZED)) { + return 0; + } + return failureCount; + }, }; export const buildGetItem = (id) => ({ queryKey: buildItemKey(id), queryFn: () => Api.getItem(id).then((data) => Map(data)), - ...itemQueryConfig, + ...queryConfig, }); export const buildOwnItems = () => ({ queryKey: OWN_ITEMS_KEY, queryFn: () => Api.getOwnItems().then((data) => List(data)), - ...itemQueryConfig, + ...queryConfig, }); export const buildChildren = (id) => ({ queryKey: buildItemChildrenKey(id), queryFn: () => Api.getChildren(id).then((data) => List(data)), - ...itemQueryConfig, + ...queryConfig, }); export const buildParents = ({ id, path }) => ({ queryKey: buildItemParentsKey(id), queryFn: () => Api.getParents({ path }).then((data) => List(data)), - ...itemQueryConfig, + ...queryConfig, }); export const buildSharedItems = () => ({ queryKey: SHARED_ITEMS_KEY, queryFn: () => Api.getSharedItems().then((data) => List(data)), - ...itemQueryConfig, + ...queryConfig, +}); + +export const buildItemMembershipsQuery = (id) => ({ + queryKey: buildItemMembershipsKey(id), + queryFn: () => Api.getMembershipsForItem(id).then((data) => List(data)), + ...queryConfig, +}); + +export const buildItemLoginQuery = (id) => ({ + queryKey: buildItemLoginKey(id), + queryFn: () => Api.getItemLogin(id).then((data) => Map(data)), + ...queryConfig, +}); + +export const buildTagsQuery = () => ({ + queryKey: ITEM_TAGS, + queryFn: () => Api.getTags().then((data) => List(data)), + ...queryConfig, +}); + +export const buildItemTagsQuery = (id) => ({ + queryKey: buildItemTagsKey(id), + queryFn: () => Api.getItemTags(id).then((data) => List(data)), + ...queryConfig, }); diff --git a/src/middlewares/notifier.js b/src/middlewares/notifier.js index 73f67d09b..4e9acbb7c 100644 --- a/src/middlewares/notifier.js +++ b/src/middlewares/notifier.js @@ -23,6 +23,9 @@ import { ERROR_MESSAGE_HEADER, SIGN_OUT_ERROR_MESSAGE, SIGN_OUT_SUCCESS_MESSAGE, + POST_ITEM_TAG_ERROR_MESSAGE, + DELETE_ITEM_TAG_ERROR_MESSAGE, + ITEM_LOGIN_SIGN_IN_ERROR_MESSAGE, } from '../config/messages'; import { COPY_ITEM_ERROR, @@ -48,7 +51,10 @@ import { SHARE_ITEM_SUCCESS, SIGN_OUT_ERROR, SIGN_OUT_SUCCESS, + POST_ITEM_TAG_ERROR, + DELETE_ITEM_TAG_ERROR, } from '../types'; +import { ITEM_LOGIN_SIGN_IN_ERROR } from '../types/itemLogin'; export default ({ type, payload }) => { let message = null; @@ -97,6 +103,18 @@ export default ({ type, payload }) => { message = SIGN_OUT_ERROR_MESSAGE; break; } + case POST_ITEM_TAG_ERROR: { + message = POST_ITEM_TAG_ERROR_MESSAGE; + break; + } + case DELETE_ITEM_TAG_ERROR: { + message = DELETE_ITEM_TAG_ERROR_MESSAGE; + break; + } + case ITEM_LOGIN_SIGN_IN_ERROR: { + message = ITEM_LOGIN_SIGN_IN_ERROR_MESSAGE; + break; + } // success messages case CREATE_ITEM_SUCCESS: { message = CREATE_ITEM_SUCCESS_MESSAGE; diff --git a/src/mutations/index.js b/src/mutations/index.js index 64b933977..1d8b11805 100644 --- a/src/mutations/index.js +++ b/src/mutations/index.js @@ -1,7 +1,9 @@ import itemMutations from './item'; import memberMutations from './member'; +import tagsMutations from './tags'; export default (queryClient) => { itemMutations(queryClient); memberMutations(queryClient); + tagsMutations(queryClient); }; diff --git a/src/mutations/item.js b/src/mutations/item.js index 08757fe18..56ffe81b6 100644 --- a/src/mutations/item.js +++ b/src/mutations/item.js @@ -29,9 +29,16 @@ import { MOVE_ITEM_MUTATION_KEY, COPY_ITEM_MUTATION_KEY, DELETE_ITEMS_MUTATION_KEY, + ITEM_LOGIN_MUTATION_KEY, + PUT_ITEM_LOGIN_MUTATION_KEY, + buildItemLoginKey, } from '../config/keys'; import notifier from '../middlewares/notifier'; import { buildPath, getDirectParentId } from '../utils/item'; +import { + PUT_ITEM_LOGIN_ERROR, + PUT_ITEM_LOGIN_SUCCESS, +} from '../types/itemLogin'; export default (queryClient) => { const mutateItem = async ({ id, value }) => { @@ -338,4 +345,25 @@ export default (queryClient) => { onSettledParentItem({ id }); }, }); + + queryClient.setMutationDefaults(ITEM_LOGIN_MUTATION_KEY, { + mutationFn: Api.itemLoginSignIn, + onSettled: () => { + queryClient.resetQueries(); + }, + }); + + queryClient.setMutationDefaults(PUT_ITEM_LOGIN_MUTATION_KEY, { + mutationFn: (payload) => + Api.putItemLoginSchema(payload).then(() => payload), + onSuccess: () => { + notifier({ type: PUT_ITEM_LOGIN_SUCCESS }); + }, + onError: (error) => { + notifier({ type: PUT_ITEM_LOGIN_ERROR, payload: { error } }); + }, + onSettled: ({ itemId }) => { + queryClient.invalidateQueries(buildItemLoginKey(itemId)); + }, + }); }; diff --git a/src/mutations/member.js b/src/mutations/member.js index 64a5ca24f..7af179081 100644 --- a/src/mutations/member.js +++ b/src/mutations/member.js @@ -28,7 +28,8 @@ export default (queryClient) => { }, // Always refetch after error or success: onSettled: () => { - queryClient.invalidateQueries(CURRENT_MEMBER_KEY); + // invalidate all queries + queryClient.resetQueries(); }, }); }; diff --git a/src/mutations/tags.js b/src/mutations/tags.js new file mode 100644 index 000000000..83dc091ce --- /dev/null +++ b/src/mutations/tags.js @@ -0,0 +1,57 @@ +import { + buildItemTagsKey, + DELETE_ITEM_TAG_MUTATION_KEY, + POST_ITEM_TAG_MUTATION_KEY, +} from '../config/keys'; +import notifier from '../middlewares/notifier'; +import { + DELETE_ITEM_TAG_ERROR, + DELETE_ITEM_TAG_SUCCESS, + POST_ITEM_TAG_ERROR, + POST_ITEM_TAG_SUCCESS, +} from '../types'; +import * as Api from '../api'; + +// payload: { id, tagId, itemPath, creator } +export default (queryClient) => { + queryClient.setMutationDefaults(POST_ITEM_TAG_MUTATION_KEY, { + mutationFn: (payload) => Api.postItemTag(payload).then(() => payload), + onSuccess: () => { + notifier({ type: POST_ITEM_TAG_SUCCESS }); + }, + onError: (error) => { + notifier({ type: POST_ITEM_TAG_ERROR, payload: { error } }); + }, + onSettled: ({ id }) => { + queryClient.invalidateQueries(buildItemTagsKey(id)); + }, + }); + + // payload {id, tagId} + queryClient.setMutationDefaults(DELETE_ITEM_TAG_MUTATION_KEY, { + mutationFn: (payload) => Api.deleteItemTag(payload).then(() => payload), + onMutate: async ({ id, tagId }) => { + const itemKey = buildItemTagsKey(id); + await queryClient.cancelQueries(itemKey); + + // Snapshot the previous value + const prevValue = queryClient.getQueryData(itemKey); + + queryClient.setQueryData(itemKey, (old) => + old.filter(({ id: tId }) => tId !== tagId), + ); + return prevValue; + }, + onSuccess: () => { + notifier({ type: DELETE_ITEM_TAG_SUCCESS }); + }, + onError: (error, { id }, context) => { + const itemKey = buildItemTagsKey(id); + queryClient.setQueryData(itemKey, context.prevValue); + notifier({ type: DELETE_ITEM_TAG_ERROR, payload: { error } }); + }, + onSettled: ({ id }) => { + queryClient.invalidateQueries(buildItemTagsKey(id)); + }, + }); +}; diff --git a/src/types/index.js b/src/types/index.js index d6e66d594..c81376514 100644 --- a/src/types/index.js +++ b/src/types/index.js @@ -1,3 +1,4 @@ export * from './item'; export * from './member'; export * from './membership'; +export * from './itemTag'; diff --git a/src/types/item.js b/src/types/item.js index 557427ae8..fdb95b0d4 100644 --- a/src/types/item.js +++ b/src/types/item.js @@ -38,3 +38,7 @@ export const DELETE_ITEMS_SUCCESS = 'DELETE_ITEMS_SUCCESS'; export const UPLOAD_FILE_ERROR = 'UPLOAD_FILE_ERROR'; export const UPLOAD_FILE_SUCCESS = 'UPLOAD_FILE_SUCCESS'; export const FLAG_UPLOADING_FILE = 'FLAG_UPLOADING_FILE'; +export const GET_ITEM_LOGIN_SUCCESS = 'GET_ITEM_LOGIN_SUCCESS'; +export const GET_ITEM_LOGIN_ERROR = 'GET_ITEM_LOGIN_ERROR'; +export const CLEAR_ITEM_LOGIN_SUCCESS = 'CLEAR_ITEM_LOGIN_SUCCESS'; +export const CLEAR_ITEM_LOGIN_ERROR = 'CLEAR_ITEM_LOGIN_ERROR'; diff --git a/src/types/itemLogin.js b/src/types/itemLogin.js new file mode 100644 index 000000000..be896d1bf --- /dev/null +++ b/src/types/itemLogin.js @@ -0,0 +1,5 @@ +export const ITEM_LOGIN_SIGN_IN_SUCCESS = 'ITEM_LOGIN_SIGN_IN_SUCCESS'; +export const FLAG_ITEM_LOGIN_SIGNING_IN = 'FLAG_ITEM_LOGIN_SIGNING_IN'; +export const ITEM_LOGIN_SIGN_IN_ERROR = 'ITEM_LOGIN_SIGN_IN_ERROR'; +export const PUT_ITEM_LOGIN_SUCCESS = 'PUT_ITEM_LOGIN_SUCCESS'; +export const PUT_ITEM_LOGIN_ERROR = 'PUT_ITEM_LOGIN_ERROR'; diff --git a/src/types/itemTag.js b/src/types/itemTag.js new file mode 100644 index 000000000..0ae078493 --- /dev/null +++ b/src/types/itemTag.js @@ -0,0 +1,12 @@ +export const GET_ITEM_TAGS_SUCCESS = 'GET_ITEM_TAGS_SUCCESS'; +export const FLAG_GETTING_ITEM_TAGS = 'FLAG_GETTING_ITEM_TAGS'; +export const GET_ITEM_TAGS_ERROR = 'GET_ITEM_TAGS_ERROR'; +export const FLAG_POSTING_ITEM_TAGS = 'FLAG_POSTING_ITEM_TAGS'; +export const POST_ITEM_TAG_SUCCESS = 'POST_ITEM_TAG_SUCCESS'; +export const POST_ITEM_TAG_ERROR = 'POST_ITEM_TAG_ERROR'; +export const FLAG_DELETING_ITEM_TAG = 'FLAG_DELETING_ITEM_TAG'; +export const DELETE_ITEM_TAG_SUCCESS = 'DELETE_ITEM_TAG_SUCCESS'; +export const DELETE_ITEM_TAG_ERROR = 'DELETE_ITEM_TAG_ERROR'; +export const GET_TAGS_SUCCESS = 'GET_TAGS_SUCCESS'; +export const GET_TAGS_ERROR = 'GET_TAGS_ERROR'; +export const FLAG_GETTING_TAGS = 'FLAG_GETTING_TAGS'; diff --git a/src/types/membership.js b/src/types/membership.js index 05d224df0..b75e19e56 100644 --- a/src/types/membership.js +++ b/src/types/membership.js @@ -1,2 +1,5 @@ export const SHARE_ITEM_ERROR = 'SHARE_ITEM_ERROR'; export const SHARE_ITEM_SUCCESS = 'SHARE_ITEM_SUCCESS'; +export const GET_MEMBERSHIPS_FOR_ITEM_ERROR = 'GET_MEMBERSHIPS_FOR_ITEM_ERROR'; +export const GET_MEMBERSHIPS_FOR_ITEM_SUCCESS = + 'GET_MEMBERSHIPS_FOR_ITEM_SUCCESS'; diff --git a/src/utils/itemExtra.js b/src/utils/itemExtra.js index 130098530..3af55eab8 100644 --- a/src/utils/itemExtra.js +++ b/src/utils/itemExtra.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import { ITEM_TYPES } from '../config/constants'; +import { getItemLoginTag } from './tag'; export const getFileExtra = (extra) => extra?.[ITEM_TYPES.FILE]; @@ -26,3 +27,32 @@ export const s3FileExtraPropTypes = PropTypes.shape({ export const linkExtraPropTypes = PropTypes.shape({ icons: PropTypes.arrayOf(PropTypes.string), }); + +export const buildItemLoginSchemaExtra = (schema) => { + if (schema) { + return { + itemLogin: { loginSchema: schema }, + }; + } + + // remove setting + return { + itemLogin: {}, + }; +}; + +export const getItemLoginExtra = (extra) => extra?.itemLogin; + +export const getItemLoginSchema = (extra) => + getItemLoginExtra(extra)?.loginSchema; + +export const getItemLoginTagFromItem = ({ tags, itemTags }) => { + const itemLoginTagId = getItemLoginTag(tags)?.id; + + // the tag setting does not exist + if (!itemLoginTagId) { + return null; + } + + return itemTags?.find(({ tagId }) => tagId === itemLoginTagId); +}; diff --git a/src/utils/member.js b/src/utils/member.js new file mode 100644 index 000000000..eee5ed86a --- /dev/null +++ b/src/utils/member.js @@ -0,0 +1,4 @@ +import { validate } from 'uuid'; + +// eslint-disable-next-line import/prefer-default-export +export const isMemberIdValid = (memberId) => validate(memberId?.trim()); diff --git a/src/utils/membership.js b/src/utils/membership.js new file mode 100644 index 000000000..9f5450923 --- /dev/null +++ b/src/utils/membership.js @@ -0,0 +1,8 @@ +import { PERMISSION_LEVELS } from '../config/constants'; + +// eslint-disable-next-line import/prefer-default-export +export const isSettingsEditionAllowedForUser = ({ memberships, memberId }) => + memberships?.find( + ({ memberId: mId, permission }) => + mId === memberId && PERMISSION_LEVELS.ADMIN === permission, + ); diff --git a/src/utils/tag.js b/src/utils/tag.js new file mode 100644 index 000000000..5fb8a6243 --- /dev/null +++ b/src/utils/tag.js @@ -0,0 +1,5 @@ +import { SETTINGS } from '../config/constants'; + +// eslint-disable-next-line import/prefer-default-export +export const getItemLoginTag = (tags) => + tags?.find(({ name }) => name === SETTINGS.ITEM_LOGIN.name);