Skip to content
This repository has been archived by the owner on Jan 23, 2025. It is now read-only.

Commit

Permalink
Merge pull request #28 from graasp/16/shareItems
Browse files Browse the repository at this point in the history
feat: display shared items
  • Loading branch information
pyphilia authored Feb 3, 2021
2 parents 5325a87 + 38a6c83 commit bb5a077
Show file tree
Hide file tree
Showing 32 changed files with 628 additions and 93 deletions.
5 changes: 5 additions & 0 deletions cypress/fixtures/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export const EDITED_FIELDS = {
description: 'new description',
};

export const MEMBERS = {
ANNA: { id: 'anna-id', name: 'anna', email: 'anna@email.com' },
BOB: { id: 'bob-id', name: 'bob', email: 'bob@email.com' },
};

export const SIMPLE_ITEMS = [
{
...DEFAULT_ITEM,
Expand Down
62 changes: 62 additions & 0 deletions cypress/integration/shareItem.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { PERMISSION_LEVELS } from '../../src/config/constants';
import { buildItemPath } from '../../src/config/paths';
import {
buildItemCard,
buildItemMenu,
ITEM_MENU_BUTTON_CLASS,
ITEM_MENU_SHARE_BUTTON_CLASS,
SHARE_ITEM_MODAL_PERMISSION_SELECT_ID,
SHARE_ITEM_MODAL_SHARE_BUTTON_ID,
buildPermissionOptionId,
SHARE_ITEM_MODAL_EMAIL_INPUT_ID,
} from '../../src/config/selectors';
import { MEMBERS, SIMPLE_ITEMS } from '../fixtures/items';

const shareItem = ({ id, member, permission }) => {
const menuSelector = `#${buildItemCard(id)} .${ITEM_MENU_BUTTON_CLASS}`;
cy.get(menuSelector).click();
cy.get(`#${buildItemMenu(id)} .${ITEM_MENU_SHARE_BUTTON_CLASS}`).click();

// select permission
cy.get(`#${SHARE_ITEM_MODAL_PERMISSION_SELECT_ID}`).click();
cy.get(`#${buildPermissionOptionId(permission)}`).click();

// input mail
cy.get(`#${SHARE_ITEM_MODAL_EMAIL_INPUT_ID}`).type(member.email);

cy.get(`#${SHARE_ITEM_MODAL_SHARE_BUTTON_ID}`).click();
};

describe('Share Item', () => {
it('share item on Home', () => {
cy.setUpApi({ items: SIMPLE_ITEMS, members: Object.values(MEMBERS) });
cy.visit('/');

// share
const { id } = SIMPLE_ITEMS[0];
const member = MEMBERS.ANNA;
shareItem({ id, member, permission: PERMISSION_LEVELS.WRITE });

cy.wait('@shareItem').then(() => {
cy.get(`#${buildItemCard(id)}`).should('exist');
});
});

it('share item in item', () => {
cy.setUpApi({ items: SIMPLE_ITEMS, members: Object.values(MEMBERS) });

// go to children item
cy.visit(buildItemPath(SIMPLE_ITEMS[0].id));

// share
const { id } = SIMPLE_ITEMS[2];
const member = MEMBERS.ANNA;
shareItem({ id, member, permission: PERMISSION_LEVELS.READ });

cy.wait('@shareItem').then(() => {
cy.get(`#${buildItemCard(id)}`).should('exist');
});
});

// todo : check item permission for users
});
10 changes: 10 additions & 0 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,26 @@ import {
mockMoveItem,
mockPostItem,
mockEditItem,
mockShareItem,
mockGetMember,
} from './server';

Cypress.Commands.add(
'setUpApi',
({
items = [],
members = [],
deleteItemError = false,
postItemError = false,
moveItemError = false,
copyItemError = false,
getItemError = false,
editItemError = false,
shareItemError = false,
getMemberError = false,
} = {}) => {
const cachedItems = JSON.parse(JSON.stringify(items));
const cachedMembers = JSON.parse(JSON.stringify(members));

mockGetOwnItems(cachedItems);

Expand All @@ -43,6 +49,10 @@ Cypress.Commands.add(
mockCopyItem(cachedItems, copyItemError);

mockEditItem(cachedItems, editItemError);

mockShareItem(cachedItems, shareItemError);

mockGetMember(cachedMembers, getMemberError);
},
);

Expand Down
73 changes: 57 additions & 16 deletions cypress/support/server.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { v4 as uuidv4 } from 'uuid';
import { StatusCodes } from 'http-status-codes';
import {
buildCopyItemRoute,
buildDeleteItemRoute,
Expand All @@ -8,6 +9,8 @@ import {
buildMoveItemRoute,
buildPostItemRoute,
GET_OWN_ITEMS_ROUTE,
buildShareItemWithRoute,
MEMBERS_ROUTE,
} from '../../src/api/routes';
import {
getItemById,
Expand All @@ -16,12 +19,7 @@ import {
transformIdForPath,
} from '../../src/utils/item';
import { CURRENT_USER_ID } from '../fixtures/items';
import {
ERROR_CODE,
ID_FORMAT,
parseStringToRegExp,
SUCCESS_CODE,
} from './utils';
import { ID_FORMAT, parseStringToRegExp, EMAIL_FORMAT } from './utils';
import {
DEFAULT_PATCH,
DEFAULT_GET,
Expand Down Expand Up @@ -56,7 +54,7 @@ export const mockPostItem = (items, shouldThrowError) => {
},
({ body, reply }) => {
if (shouldThrowError) {
return reply({ statusCode: ERROR_CODE });
return reply({ statusCode: StatusCodes.BAD_REQUEST });
}

// add necessary properties id, path and creator
Expand All @@ -79,12 +77,12 @@ export const mockDeleteItem = (items, shouldThrowError) => {
},
({ url, reply }) => {
if (shouldThrowError) {
return reply({ statusCode: ERROR_CODE, body: null });
return reply({ statusCode: StatusCodes.BAD_REQUEST, body: null });
}

const id = url.slice(API_HOST.length).split('/')[2];
return reply({
statusCode: SUCCESS_CODE,
statusCode: StatusCodes.OK,
body: getItemById(items, id),
});
},
Expand All @@ -99,14 +97,14 @@ export const mockGetItem = (items, shouldThrowError) => {
},
({ url, reply }) => {
if (shouldThrowError) {
return reply({ statusCode: ERROR_CODE, body: null });
return reply({ statusCode: StatusCodes.BAD_REQUEST, body: null });
}

const id = url.slice(API_HOST.length).split('/')[2];
const item = getItemById(items, id);
return reply({
body: item,
statusCode: SUCCESS_CODE,
statusCode: StatusCodes.OK,
});
},
).as('getItem');
Expand Down Expand Up @@ -134,7 +132,7 @@ export const mockMoveItem = (items, shouldThrowError) => {
},
({ url, reply, body }) => {
if (shouldThrowError) {
return reply({ statusCode: ERROR_CODE, body: null });
return reply({ statusCode: StatusCodes.BAD_REQUEST, body: null });
}

const id = url.slice(API_HOST.length).split('/')[2];
Expand All @@ -149,7 +147,7 @@ export const mockMoveItem = (items, shouldThrowError) => {
// todo: do for all children

return reply({
statusCode: SUCCESS_CODE,
statusCode: StatusCodes.OK,
body: item, // this might not be accurate
});
},
Expand All @@ -164,7 +162,7 @@ export const mockCopyItem = (items, shouldThrowError) => {
},
({ url, reply, body }) => {
if (shouldThrowError) {
return reply({ statusCode: ERROR_CODE, body: null });
return reply({ statusCode: StatusCodes.BAD_REQUEST, body: null });
}

const id = url.slice(API_HOST.length).split('/')[2];
Expand All @@ -181,7 +179,7 @@ export const mockCopyItem = (items, shouldThrowError) => {
items.push(newItem);
// todo: do for all children
return reply({
statusCode: SUCCESS_CODE,
statusCode: StatusCodes.OK,
body: newItem,
});
},
Expand All @@ -196,10 +194,53 @@ export const mockEditItem = (items, shouldThrowError) => {
},
({ reply, body }) => {
if (shouldThrowError) {
return reply({ statusCode: ERROR_CODE });
return reply({ statusCode: StatusCodes.BAD_REQUEST });
}

return reply(body);
},
).as('editItem');
};

export const mockShareItem = (items, shouldThrowError) => {
cy.intercept(
{
method: DEFAULT_POST.method,
url: new RegExp(
`${API_HOST}/${parseStringToRegExp(
buildShareItemWithRoute(ID_FORMAT),
)}`,
),
},
({ reply, body }) => {
if (shouldThrowError) {
return reply({ statusCode: StatusCodes.BAD_REQUEST });
}

return reply(body);
},
).as('shareItem');
};

export const mockGetMember = (members, shouldThrowError) => {
const emailReg = new RegExp(EMAIL_FORMAT);
cy.intercept(
{
method: DEFAULT_GET.method,
pathname: `/${MEMBERS_ROUTE}`,
query: {
email: emailReg,
},
},
({ reply, url }) => {
if (shouldThrowError) {
return reply({ statusCode: StatusCodes.BAD_REQUEST });
}

const mail = emailReg.exec(url)[0];
const member = members.find(({ email }) => email === mail);

return reply([member]);
},
).as('getMember');
};
5 changes: 2 additions & 3 deletions cypress/support/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// use simple id format for tests
export const ID_FORMAT = '[a-z0-9-]*';

export const ERROR_CODE = 400;
export const SUCCESS_CODE = 200;

export const parseStringToRegExp = (string) => string.replaceAll('?', '\\?');

export const EMAIL_FORMAT = '[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+';
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"connected-react-router": "6.8.0",
"dexie": "3.0.3",
"history": "5.0.0",
"http-status-codes": "2.1.4",
"i18next": "19.8.4",
"immutable": "4.0.0-rc.12",
"js-cookie": "2.2.1",
Expand Down
44 changes: 35 additions & 9 deletions src/actions/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
EDIT_ITEM_SUCCESS,
FLAG_SETTING_ITEM,
FLAG_EDITING_ITEM,
GET_SHARED_ITEMS_SUCCESS,
} from '../types/item';
import { getParentsIdsFromPath } from '../utils/item';
import { createFlag } from './utils';
Expand All @@ -40,19 +41,15 @@ export const setItem = (id) => async (dispatch) => {
const item = await Api.getItem(id);

const { children, parents } = item;

// get children
let newChildren = [];
if (!children) {
newChildren = await Api.getChildren(id);
}

// get parents
let newParents = [];
if (!parents) {
newParents = await buildParentsLine(item.path);
}

dispatch({
type: SET_ITEM_SUCCESS,
payload: { item, children: newChildren, parents: newParents },
Expand Down Expand Up @@ -132,14 +129,13 @@ export const createItem = (props) => async (dispatch) => {
}
};

export const deleteItem = (id) => async (dispatch) => {
export const deleteItem = (item) => async (dispatch) => {
try {
dispatch(createFlag(FLAG_DELETING_ITEM, true));
await Api.deleteItem(id);

await Api.deleteItem(item.id);
dispatch({
type: DELETE_ITEM_SUCCESS,
payload: id,
payload: item,
});
} catch (e) {
console.error(e);
Expand Down Expand Up @@ -168,7 +164,7 @@ export const moveItem = (payload) => async (dispatch, getState) => {

dispatch({
type: MOVE_ITEM_SUCCESS,
payload: payload.id,
payload,
});
} catch (e) {
console.error(e);
Expand Down Expand Up @@ -226,3 +222,33 @@ export const editItem = (item) => async (dispatch) => {
dispatch(createFlag(FLAG_EDITING_ITEM, false));
}
};

export const getSharedItems = () => async (dispatch) => {
try {
const sharedItems = await Api.getSharedItems();

let childrenItems = [];
for (const item of sharedItems) {
const { children, id } = item;
// get children
let newChildren = [];

// an item does not come automatically with children
// thus we need to fetch them if no children is specified
// we don't need to fetch again if they already exists (cache)
if (!children) {
// eslint-disable-next-line no-await-in-loop
newChildren = await Api.getChildren(id);
childrenItems = childrenItems.concat(newChildren);
item.children = newChildren.map(({ id: childId }) => childId);
}
}

dispatch({
type: GET_SHARED_ITEMS_SUCCESS,
payload: [...sharedItems, ...childrenItems],
});
} catch (e) {
console.error(e);
}
};
8 changes: 8 additions & 0 deletions src/actions/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
SET_COPY_MODAL_SETTINGS,
SET_EDIT_MODAL_SETTINGS,
SET_MOVE_MODAL_SETTINGS,
SET_SHARE_MODAL_SETTINGS,
} from '../types/layout';

export const setMoveModalSettings = (payload) => (dispatch) => {
Expand All @@ -24,3 +25,10 @@ export const setEditModalSettings = (payload) => (dispatch) => {
payload,
});
};

export const setShareModalSettings = (payload) => (dispatch) => {
dispatch({
type: SET_SHARE_MODAL_SETTINGS,
payload,
});
};
Empty file added src/actions/member.js
Empty file.
Loading

0 comments on commit bb5a077

Please sign in to comment.