1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 20:59:44 +02:00

feat: Permissions for board members

Closes #262
This commit is contained in:
Maksim Eltyshev 2022-08-19 14:00:40 +02:00
parent 281cb4a71b
commit f9e0147f33
61 changed files with 1063 additions and 191 deletions

View file

@ -69,6 +69,36 @@ handleBoardMembershipCreate.fetchProject = (id, currentUserId, currentBoardId) =
}, },
}); });
const updateBoardMembership = (id, data) => ({
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE,
payload: {
id,
data,
},
});
updateBoardMembership.success = (boardMembership) => ({
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE__SUCCESS,
payload: {
boardMembership,
},
});
updateBoardMembership.failure = (id, error) => ({
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const handleBoardMembershipUpdate = (boardMembership) => ({
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE_HANDLE,
payload: {
boardMembership,
},
});
const deleteBoardMembership = (id) => ({ const deleteBoardMembership = (id) => ({
type: ActionTypes.BOARD_MEMBERSHIP_DELETE, type: ActionTypes.BOARD_MEMBERSHIP_DELETE,
payload: { payload: {
@ -101,6 +131,8 @@ const handleBoardMembershipDelete = (boardMembership) => ({
export default { export default {
createBoardMembership, createBoardMembership,
handleBoardMembershipCreate, handleBoardMembershipCreate,
updateBoardMembership,
handleBoardMembershipUpdate,
deleteBoardMembership, deleteBoardMembership,
handleBoardMembershipDelete, handleBoardMembershipDelete,
}; };

View file

@ -5,10 +5,14 @@ import socket from './socket';
const createBoardMembership = (boardId, data, headers) => const createBoardMembership = (boardId, data, headers) =>
socket.post(`/boards/${boardId}/memberships`, data, headers); socket.post(`/boards/${boardId}/memberships`, data, headers);
const updateBoardMembership = (id, data, headers) =>
socket.patch(`/board-memberships/${id}`, data, headers);
const deleteBoardMembership = (id, headers) => const deleteBoardMembership = (id, headers) =>
socket.delete(`/board-memberships/${id}`, undefined, headers); socket.delete(`/board-memberships/${id}`, undefined, headers);
export default { export default {
createBoardMembership, createBoardMembership,
updateBoardMembership,
deleteBoardMembership, deleteBoardMembership,
}; };

View file

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import Filters from './Filters'; import Filters from './Filters';
import Memberships from '../Memberships'; import Memberships from '../Memberships';
import BoardMembershipPermissionsSelectStep from '../BoardMembershipPermissionsSelectStep';
import styles from './BoardActions.module.scss'; import styles from './BoardActions.module.scss';
@ -15,6 +16,7 @@ const BoardActions = React.memo(
allUsers, allUsers,
canEditMemberships, canEditMemberships,
onMembershipCreate, onMembershipCreate,
onMembershipUpdate,
onMembershipDelete, onMembershipDelete,
onUserToFilterAdd, onUserToFilterAdd,
onUserFromFilterRemove, onUserFromFilterRemove,
@ -30,8 +32,10 @@ const BoardActions = React.memo(
<Memberships <Memberships
items={memberships} items={memberships}
allUsers={allUsers} allUsers={allUsers}
permissionsSelectStep={BoardMembershipPermissionsSelectStep}
canEdit={canEditMemberships} canEdit={canEditMemberships}
onCreate={onMembershipCreate} onCreate={onMembershipCreate}
onUpdate={onMembershipUpdate}
onDelete={onMembershipDelete} onDelete={onMembershipDelete}
/> />
</div> </div>
@ -65,6 +69,7 @@ BoardActions.propTypes = {
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
canEditMemberships: PropTypes.bool.isRequired, canEditMemberships: PropTypes.bool.isRequired,
onMembershipCreate: PropTypes.func.isRequired, onMembershipCreate: PropTypes.func.isRequired,
onMembershipUpdate: PropTypes.func.isRequired,
onMembershipDelete: PropTypes.func.isRequired, onMembershipDelete: PropTypes.func.isRequired,
onUserToFilterAdd: PropTypes.func.isRequired, onUserToFilterAdd: PropTypes.func.isRequired,
onUserFromFilterRemove: PropTypes.func.isRequired, onUserFromFilterRemove: PropTypes.func.isRequired,

View file

@ -0,0 +1,101 @@
import omit from 'lodash/omit';
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Menu, Radio, Segment } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { BoardMembershipRoles } from '../../constants/Enums';
import styles from './BoardMembershipPermissionsSelectStep.module.scss';
const BoardMembershipPermissionsSelectStep = React.memo(
({ defaultData, title, buttonContent, onSelect, onBack }) => {
const [t] = useTranslation();
const [data, setData] = useState(() => ({
role: BoardMembershipRoles.EDITOR,
canComment: null,
...defaultData,
}));
const handleRoleSelectClick = useCallback((role) => {
setData((prevData) => ({
...prevData,
role,
canComment: role === BoardMembershipRoles.EDITOR ? null : !!prevData.canComment,
}));
}, []);
const handleSettingChange = useCallback((_, { name: fieldName, checked: value }) => {
setData((prevData) => ({
...prevData,
[fieldName]: value,
}));
}, []);
const handleSubmit = useCallback(() => {
onSelect(data.role === BoardMembershipRoles.EDITOR ? omit(data, 'canComment') : data);
}, [onSelect, data]);
return (
<>
<Popup.Header onBack={onBack}>
{t(title, {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<Menu secondary vertical className={styles.menu}>
<Menu.Item
active={data.role === BoardMembershipRoles.EDITOR}
onClick={() => handleRoleSelectClick(BoardMembershipRoles.EDITOR)}
>
<div className={styles.menuItemTitle}>{t('common.editor')}</div>
<div className={styles.menuItemDescription}>
{t('common.canEditContentOfBoard')}
</div>
</Menu.Item>
<Menu.Item
active={data.role === BoardMembershipRoles.VIEWER}
onClick={() => handleRoleSelectClick(BoardMembershipRoles.VIEWER)}
>
<div className={styles.menuItemTitle}>{t('common.viewer')}</div>
<div className={styles.menuItemDescription}>{t('common.canOnlyViewBoard')}</div>
</Menu.Item>
</Menu>
{data.role !== BoardMembershipRoles.EDITOR && (
<Segment basic className={styles.settings}>
<Radio
toggle
name="canComment"
checked={data.canComment}
label={t('common.canComment')}
onChange={handleSettingChange}
/>
</Segment>
)}
<Button positive content={t(buttonContent)} />
</Form>
</Popup.Content>
</>
);
},
);
BoardMembershipPermissionsSelectStep.propTypes = {
defaultData: PropTypes.object, // eslint-disable-line react/forbid-prop-types
title: PropTypes.string,
buttonContent: PropTypes.string,
onSelect: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
BoardMembershipPermissionsSelectStep.defaultProps = {
defaultData: undefined,
title: 'common.selectPermissions',
buttonContent: 'action.selectPermissions',
};
export default BoardMembershipPermissionsSelectStep;

View file

@ -0,0 +1,18 @@
:global(#app) {
.menu {
margin: 0 auto 8px;
width: 100%;
}
.menuItemDescription {
opacity: 0.5;
}
.menuItemTitle {
margin-bottom: 8px;
}
.settings {
margin: 0 0 8px;
}
}

View file

@ -0,0 +1,3 @@
import BoardMembershipPermissionsSelectStep from './BoardMembershipPermissionsSelectStep';
export default BoardMembershipPermissionsSelectStep;

View file

@ -13,7 +13,7 @@ import styles from './Attachments.module.scss';
const INITIALLY_VISIBLE = 4; const INITIALLY_VISIBLE = 4;
const Attachments = React.memo( const Attachments = React.memo(
({ items, onUpdate, onDelete, onCoverUpdate, onGalleryOpen, onGalleryClose }) => { ({ items, canEdit, onUpdate, onDelete, onCoverUpdate, onGalleryOpen, onGalleryClose }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [isAllVisible, toggleAllVisible] = useToggle(); const [isAllVisible, toggleAllVisible] = useToggle();
@ -99,6 +99,7 @@ const Attachments = React.memo(
createdAt={item.createdAt} createdAt={item.createdAt}
isCover={item.isCover} isCover={item.isCover}
isPersisted={item.isPersisted} isPersisted={item.isPersisted}
canEdit={canEdit}
onClick={item.image || isPdf ? open : undefined} onClick={item.image || isPdf ? open : undefined}
onCoverSelect={() => handleCoverSelect(item.id)} onCoverSelect={() => handleCoverSelect(item.id)}
onCoverDeselect={handleCoverDeselect} onCoverDeselect={handleCoverDeselect}
@ -151,6 +152,7 @@ const Attachments = React.memo(
Attachments.propTypes = { Attachments.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onCoverUpdate: PropTypes.func.isRequired, onCoverUpdate: PropTypes.func.isRequired,

View file

@ -17,6 +17,7 @@ const Item = React.forwardRef(
createdAt, createdAt,
isCover, isCover,
isPersisted, isPersisted,
canEdit,
onCoverSelect, onCoverSelect,
onCoverDeselect, onCoverDeselect,
onClick, onClick,
@ -96,7 +97,7 @@ const Item = React.forwardRef(
value: createdAt, value: createdAt,
})} })}
</span> </span>
{coverUrl && ( {coverUrl && canEdit && (
<span className={styles.options}> <span className={styles.options}>
<button type="button" className={styles.option} onClick={handleToggleCoverClick}> <button type="button" className={styles.option} onClick={handleToggleCoverClick}>
<Icon <Icon
@ -118,17 +119,19 @@ const Item = React.forwardRef(
</span> </span>
)} )}
</div> </div>
<EditPopup {canEdit && (
defaultData={{ <EditPopup
name, defaultData={{
}} name,
onUpdate={onUpdate} }}
onDelete={onDelete} onUpdate={onUpdate}
> onDelete={onDelete}
<Button className={classNames(styles.button, styles.target)}> >
<Icon fitted name="pencil" size="small" /> <Button className={classNames(styles.button, styles.target)}>
</Button> <Icon fitted name="pencil" size="small" />
</EditPopup> </Button>
</EditPopup>
)}
</div> </div>
); );
}, },
@ -141,6 +144,7 @@ Item.propTypes = {
createdAt: PropTypes.instanceOf(Date), createdAt: PropTypes.instanceOf(Date),
isCover: PropTypes.bool.isRequired, isCover: PropTypes.bool.isRequired,
isPersisted: PropTypes.bool.isRequired, isPersisted: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
onClick: PropTypes.func, onClick: PropTypes.func,
onCoverSelect: PropTypes.func.isRequired, onCoverSelect: PropTypes.func.isRequired,
onCoverDeselect: PropTypes.func.isRequired, onCoverDeselect: PropTypes.func.isRequired,

View file

@ -48,6 +48,7 @@ const CardModal = React.memo(
allBoardMemberships, allBoardMemberships,
allLabels, allLabels,
canEdit, canEdit,
canEditCommentActivities,
canEditAllCommentActivities, canEditAllCommentActivities,
onUpdate, onUpdate,
onMove, onMove,
@ -302,7 +303,10 @@ const CardModal = React.memo(
{canEdit ? ( {canEdit ? (
<DescriptionEdit defaultValue={description} onUpdate={handleDescriptionUpdate}> <DescriptionEdit defaultValue={description} onUpdate={handleDescriptionUpdate}>
{description ? ( {description ? (
<button type="button" className={styles.descriptionText}> <button
type="button"
className={classNames(styles.descriptionText, styles.cursorPointer)}
>
<Markdown linkStopPropagation linkTarget="_blank"> <Markdown linkStopPropagation linkTarget="_blank">
{description} {description}
</Markdown> </Markdown>
@ -348,6 +352,7 @@ const CardModal = React.memo(
<div className={styles.moduleHeader}>{t('common.attachments')}</div> <div className={styles.moduleHeader}>{t('common.attachments')}</div>
<Attachments <Attachments
items={attachments} items={attachments}
canEdit={canEdit}
onUpdate={onAttachmentUpdate} onUpdate={onAttachmentUpdate}
onDelete={onAttachmentDelete} onDelete={onAttachmentDelete}
onCoverUpdate={handleCoverUpdate} onCoverUpdate={handleCoverUpdate}
@ -363,7 +368,7 @@ const CardModal = React.memo(
isAllFetched={isAllActivitiesFetched} isAllFetched={isAllActivitiesFetched}
isDetailsVisible={isActivitiesDetailsVisible} isDetailsVisible={isActivitiesDetailsVisible}
isDetailsFetching={isActivitiesDetailsFetching} isDetailsFetching={isActivitiesDetailsFetching}
canEdit={canEdit} canEdit={canEditCommentActivities}
canEditAllComments={canEditAllCommentActivities} canEditAllComments={canEditAllCommentActivities}
onFetch={onActivitiesFetch} onFetch={onActivitiesFetch}
onDetailsToggle={onActivitiesDetailsToggle} onDetailsToggle={onActivitiesDetailsToggle}
@ -508,6 +513,7 @@ CardModal.propTypes = {
allLabels: PropTypes.array.isRequired, allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
canEdit: PropTypes.bool.isRequired, canEdit: PropTypes.bool.isRequired,
canEditCommentActivities: PropTypes.bool.isRequired,
canEditAllCommentActivities: PropTypes.bool.isRequired, canEditAllCommentActivities: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired, onMove: PropTypes.func.isRequired,

View file

@ -64,6 +64,10 @@
padding: 8px 8px 0 16px; padding: 8px 8px 0 16px;
} }
.cursorPointer {
cursor: pointer;
}
.dueDate { .dueDate {
background: rgba(9, 30, 66, 0.04); background: rgba(9, 30, 66, 0.04);
border: none; border: none;
@ -114,7 +118,6 @@
background: transparent; background: transparent;
border: none; border: none;
color: #17394d; color: #17394d;
cursor: pointer;
font-size: 15px; font-size: 15px;
margin-bottom: 8px; margin-bottom: 8px;
outline: none; outline: none;

View file

@ -1,3 +1,4 @@
import pick from 'lodash/pick';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -11,12 +12,14 @@ import DeleteStep from '../DeleteStep';
import styles from './ActionsPopup.module.scss'; import styles from './ActionsPopup.module.scss';
const StepTypes = { const StepTypes = {
EDIT_PERMISSIONS: 'EDIT_PERMISSIONS',
DELETE: 'DELETE', DELETE: 'DELETE',
}; };
const ActionsStep = React.memo( const ActionsStep = React.memo(
({ ({
user, membership,
permissionsSelectStep,
leaveButtonContent, leaveButtonContent,
leaveConfirmationTitle, leaveConfirmationTitle,
leaveConfirmationContent, leaveConfirmationContent,
@ -26,63 +29,115 @@ const ActionsStep = React.memo(
deleteConfirmationContent, deleteConfirmationContent,
deleteConfirmationButtonContent, deleteConfirmationButtonContent,
canLeave, canLeave,
canDelete, canEdit,
onUpdate,
onDelete, onDelete,
onClose,
}) => { }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps(); const [step, openStep, handleBack] = useSteps();
const handleEditPermissionsClick = useCallback(() => {
openStep(StepTypes.EDIT_PERMISSIONS);
}, [openStep]);
const handleDeleteClick = useCallback(() => { const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE); openStep(StepTypes.DELETE);
}, [openStep]); }, [openStep]);
if (step && step.type === StepTypes.DELETE) { const handleRoleSelect = useCallback(
return ( (data) => {
<DeleteStep if (onUpdate) {
title={t(user.isCurrent ? leaveConfirmationTitle : deleteConfirmationTitle, { onUpdate(data);
context: 'title', }
})}
content={t(user.isCurrent ? leaveConfirmationContent : deleteConfirmationContent)} onClose();
buttonContent={t( },
user.isCurrent ? leaveConfirmationButtonContent : deleteConfirmationButtonContent, [onUpdate, onClose],
)} );
onConfirm={onDelete}
onBack={handleBack} if (step) {
/> switch (step.type) {
); case StepTypes.EDIT_PERMISSIONS: {
const PermissionsSelectStep = permissionsSelectStep;
return (
<PermissionsSelectStep
defaultData={pick(membership, ['role', 'canComment'])}
title="common.editPermissions"
buttonContent="action.save"
onSelect={handleRoleSelect}
onBack={handleBack}
/>
);
}
case StepTypes.DELETE:
return (
<DeleteStep
title={t(
membership.user.isCurrent ? leaveConfirmationTitle : deleteConfirmationTitle,
{
context: 'title',
},
)}
content={t(
membership.user.isCurrent ? leaveConfirmationContent : deleteConfirmationContent,
)}
buttonContent={t(
membership.user.isCurrent
? leaveConfirmationButtonContent
: deleteConfirmationButtonContent,
)}
onConfirm={onDelete}
onBack={handleBack}
/>
);
default:
}
} }
return ( return (
<> <>
<span className={styles.user}> <span className={styles.user}>
<User name={user.name} avatarUrl={user.avatarUrl} size="large" /> <User name={membership.user.name} avatarUrl={membership.user.avatarUrl} size="large" />
</span> </span>
<span className={styles.content}> <span className={styles.content}>
<div className={styles.name}>{user.name}</div> <div className={styles.name}>{membership.user.name}</div>
<div className={styles.email}>{user.email}</div> <div className={styles.email}>{membership.user.email}</div>
{user.isCurrent
? canLeave && (
<Button
content={t(leaveButtonContent)}
className={styles.deleteButton}
onClick={handleDeleteClick}
/>
)
: canDelete && (
<Button
content={t(deleteButtonContent)}
className={styles.deleteButton}
onClick={handleDeleteClick}
/>
)}
</span> </span>
{permissionsSelectStep && canEdit && (
<Button
fluid
content={t('action.editPermissions')}
className={styles.button}
onClick={handleEditPermissionsClick}
/>
)}
{membership.user.isCurrent
? canLeave && (
<Button
fluid
content={t(leaveButtonContent)}
className={styles.button}
onClick={handleDeleteClick}
/>
)
: canEdit && (
<Button
fluid
content={t(deleteButtonContent)}
className={styles.button}
onClick={handleDeleteClick}
/>
)}
</> </>
); );
}, },
); );
ActionsStep.propTypes = { ActionsStep.propTypes = {
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types membership: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
permissionsSelectStep: PropTypes.elementType,
leaveButtonContent: PropTypes.string, leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string, leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string, leaveConfirmationContent: PropTypes.string,
@ -92,11 +147,14 @@ ActionsStep.propTypes = {
deleteConfirmationContent: PropTypes.string, deleteConfirmationContent: PropTypes.string,
deleteConfirmationButtonContent: PropTypes.string, deleteConfirmationButtonContent: PropTypes.string,
canLeave: PropTypes.bool.isRequired, canLeave: PropTypes.bool.isRequired,
canDelete: PropTypes.bool.isRequired, canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
}; };
ActionsStep.defaultProps = { ActionsStep.defaultProps = {
permissionsSelectStep: undefined,
leaveButtonContent: 'action.leaveBoard', leaveButtonContent: 'action.leaveBoard',
leaveConfirmationTitle: 'common.leaveBoard', leaveConfirmationTitle: 'common.leaveBoard',
leaveConfirmationContent: 'common.areYouSureYouWantToLeaveBoard', leaveConfirmationContent: 'common.areYouSureYouWantToLeaveBoard',
@ -105,6 +163,7 @@ ActionsStep.defaultProps = {
deleteConfirmationTitle: 'common.removeMember', deleteConfirmationTitle: 'common.removeMember',
deleteConfirmationContent: 'common.areYouSureYouWantToRemoveThisMemberFromBoard', deleteConfirmationContent: 'common.areYouSureYouWantToRemoveThisMemberFromBoard',
deleteConfirmationButtonContent: 'action.removeMember', deleteConfirmationButtonContent: 'action.removeMember',
onUpdate: undefined,
}; };
export default withPopup(ActionsStep); export default withPopup(ActionsStep);

View file

@ -1,28 +1,31 @@
:global(#app) { :global(#app) {
.button {
background: transparent;
box-shadow: none;
color: #6b808c;
font-weight: normal;
margin-top: 8px;
padding: 6px 11px;
text-align: left;
text-decoration: underline;
transition: none;
&:hover {
background: rgba(9, 30, 66, 0.08);
color: #092d42;
}
}
.content { .content {
display: inline-block; display: inline-block;
width: calc(100% - 44px); width: calc(100% - 44px);
} }
.deleteButton {
background: transparent;
box-shadow: none;
flex: 0 0 auto;
font-weight: normal;
margin: 0 0 0 -10px;
padding: 11px 10px;
text-decoration: underline;
&:hover {
background: #e9e9e9;
}
}
.email { .email {
color: #888888; color: #888888;
font-size: 14px; font-size: 14px;
line-height: 1.2; line-height: 1.2;
padding: 4px 0 4px 2px; padding: 2px 0 2px 2px;
} }
.name { .name {
@ -36,7 +39,7 @@
.user { .user {
display: inline-block; display: inline-block;
padding-right: 8px; padding-right: 8px;
padding-top: 8px; padding-top: 10px;
vertical-align: top; vertical-align: top;
} }
} }

View file

@ -4,89 +4,141 @@ import { useTranslation } from 'react-i18next';
import { withPopup } from '../../../lib/popup'; import { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui'; import { Input, Popup } from '../../../lib/custom-ui';
import { useField } from '../../../hooks'; import { useField, useSteps } from '../../../hooks';
import UserItem from './UserItem'; import UserItem from './UserItem';
import styles from './AddPopup.module.scss'; import styles from './AddPopup.module.scss';
const AddStep = React.memo(({ users, currentUserIds, title, onCreate, onClose }) => { const StepTypes = {
const [t] = useTranslation(); SELECT_PERMISSIONS: 'SELECT_PERMISSIONS',
const [searchValue, handleSearchFieldChange] = useField(''); };
const search = useMemo(() => searchValue.trim().toLowerCase(), [searchValue]);
const filteredUsers = useMemo( const AddStep = React.memo(
() => ({ users, currentUserIds, permissionsSelectStep, title, onCreate, onClose }) => {
users.filter( const [t] = useTranslation();
(user) => const [step, openStep, handleBack] = useSteps();
user.email.includes(search) || const [searchValue, handleSearchFieldChange] = useField('');
user.name.toLowerCase().includes(search) || const search = useMemo(() => searchValue.trim().toLowerCase(), [searchValue]);
(user.username && user.username.includes(search)),
),
[users, search],
);
const searchField = useRef(null); const filteredUsers = useMemo(
() =>
users.filter(
(user) =>
user.email.includes(search) ||
user.name.toLowerCase().includes(search) ||
(user.username && user.username.includes(search)),
),
[users, search],
);
const handleUserSelect = useCallback( const searchField = useRef(null);
(id) => {
onCreate({
userId: id,
});
onClose(); const handleUserSelect = useCallback(
}, (id) => {
[onCreate, onClose], if (permissionsSelectStep) {
); openStep(StepTypes.SELECT_PERMISSIONS, {
userId: id,
});
} else {
onCreate({
userId: id,
});
useEffect(() => { onClose();
searchField.current.focus(); }
}, []); },
[permissionsSelectStep, onCreate, onClose, openStep],
);
return ( const handleRoleSelect = useCallback(
<> (data) => {
<Popup.Header> onCreate({
{t(title, { userId: step.params.userId,
context: 'title', ...data,
})} });
</Popup.Header>
<Popup.Content> onClose();
<Input },
fluid [onCreate, onClose, step],
ref={searchField} );
value={searchValue}
placeholder={t('common.searchUsers')} useEffect(() => {
icon="search" searchField.current.focus();
onChange={handleSearchFieldChange} }, []);
/>
{filteredUsers.length > 0 && ( if (step) {
<div className={styles.users}> switch (step.type) {
{filteredUsers.map((user) => ( case StepTypes.SELECT_PERMISSIONS: {
<UserItem const currentUser = users.find((user) => user.id === step.params.userId);
key={user.id}
name={user.name} if (currentUser) {
avatarUrl={user.avatarUrl} const PermissionsSelectStep = permissionsSelectStep;
isActive={currentUserIds.includes(user.id)}
onSelect={() => handleUserSelect(user.id)} return (
<PermissionsSelectStep
buttonContent="action.addMember"
onSelect={handleRoleSelect}
onBack={handleBack}
/> />
))} );
</div> }
)}
</Popup.Content> openStep(null);
</>
); break;
}); }
default:
}
}
return (
<>
<Popup.Header>
{t(title, {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Input
fluid
ref={searchField}
value={searchValue}
placeholder={t('common.searchUsers')}
icon="search"
onChange={handleSearchFieldChange}
/>
{filteredUsers.length > 0 && (
<div className={styles.users}>
{filteredUsers.map((user) => (
<UserItem
key={user.id}
name={user.name}
avatarUrl={user.avatarUrl}
isActive={currentUserIds.includes(user.id)}
onSelect={() => handleUserSelect(user.id)}
/>
))}
</div>
)}
</Popup.Content>
</>
);
},
);
AddStep.propTypes = { AddStep.propTypes = {
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired, users: PropTypes.array.isRequired,
currentUserIds: PropTypes.array.isRequired, currentUserIds: PropTypes.array.isRequired,
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
permissionsSelectStep: PropTypes.elementType,
title: PropTypes.string, title: PropTypes.string,
onCreate: PropTypes.func.isRequired, onCreate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
AddStep.defaultProps = { AddStep.defaultProps = {
permissionsSelectStep: undefined,
title: 'common.addMember', title: 'common.addMember',
}; };

View file

@ -12,6 +12,7 @@ const Memberships = React.memo(
({ ({
items, items,
allUsers, allUsers,
permissionsSelectStep,
addTitle, addTitle,
leaveButtonContent, leaveButtonContent,
leaveConfirmationTitle, leaveConfirmationTitle,
@ -24,6 +25,7 @@ const Memberships = React.memo(
canEdit, canEdit,
canLeaveIfLast, canLeaveIfLast,
onCreate, onCreate,
onUpdate,
onDelete, onDelete,
}) => { }) => {
return ( return (
@ -32,7 +34,8 @@ const Memberships = React.memo(
{items.map((item) => ( {items.map((item) => (
<span key={item.id} className={styles.user}> <span key={item.id} className={styles.user}>
<ActionsPopup <ActionsPopup
user={item.user} membership={item}
permissionsSelectStep={permissionsSelectStep}
leaveButtonContent={leaveButtonContent} leaveButtonContent={leaveButtonContent}
leaveConfirmationTitle={leaveConfirmationTitle} leaveConfirmationTitle={leaveConfirmationTitle}
leaveConfirmationContent={leaveConfirmationContent} leaveConfirmationContent={leaveConfirmationContent}
@ -42,7 +45,8 @@ const Memberships = React.memo(
deleteConfirmationContent={deleteConfirmationContent} deleteConfirmationContent={deleteConfirmationContent}
deleteConfirmationButtonContent={deleteConfirmationButtonContent} deleteConfirmationButtonContent={deleteConfirmationButtonContent}
canLeave={items.length > 1 || canLeaveIfLast} canLeave={items.length > 1 || canLeaveIfLast}
canDelete={canEdit} canEdit={canEdit}
onUpdate={(data) => onUpdate(item.id, data)}
onDelete={() => onDelete(item.id)} onDelete={() => onDelete(item.id)}
> >
<User <User
@ -59,6 +63,7 @@ const Memberships = React.memo(
<AddPopup <AddPopup
users={allUsers} users={allUsers}
currentUserIds={items.map((item) => item.user.id)} currentUserIds={items.map((item) => item.user.id)}
permissionsSelectStep={permissionsSelectStep}
title={addTitle} title={addTitle}
onCreate={onCreate} onCreate={onCreate}
> >
@ -75,6 +80,7 @@ Memberships.propTypes = {
items: PropTypes.array.isRequired, items: PropTypes.array.isRequired,
allUsers: PropTypes.array.isRequired, allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
permissionsSelectStep: PropTypes.elementType,
addTitle: PropTypes.string, addTitle: PropTypes.string,
leaveButtonContent: PropTypes.string, leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string, leaveConfirmationTitle: PropTypes.string,
@ -87,10 +93,12 @@ Memberships.propTypes = {
canEdit: PropTypes.bool, canEdit: PropTypes.bool,
canLeaveIfLast: PropTypes.bool, canLeaveIfLast: PropTypes.bool,
onCreate: PropTypes.func.isRequired, onCreate: PropTypes.func.isRequired,
onUpdate: PropTypes.func,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
}; };
Memberships.defaultProps = { Memberships.defaultProps = {
permissionsSelectStep: undefined,
addTitle: undefined, addTitle: undefined,
leaveButtonContent: undefined, leaveButtonContent: undefined,
leaveConfirmationTitle: undefined, leaveConfirmationTitle: undefined,
@ -102,6 +110,7 @@ Memberships.defaultProps = {
deleteConfirmationButtonContent: undefined, deleteConfirmationButtonContent: undefined,
canEdit: true, canEdit: true,
canLeaveIfLast: true, canLeaveIfLast: true,
onUpdate: undefined,
}; };
export default Memberships; export default Memberships;

View file

@ -123,6 +123,10 @@ export default {
BOARD_MEMBERSHIP_CREATE__FAILURE: 'BOARD_MEMBERSHIP_CREATE__FAILURE', BOARD_MEMBERSHIP_CREATE__FAILURE: 'BOARD_MEMBERSHIP_CREATE__FAILURE',
BOARD_MEMBERSHIP_CREATE_HANDLE: 'BOARD_MEMBERSHIP_CREATE_HANDLE', BOARD_MEMBERSHIP_CREATE_HANDLE: 'BOARD_MEMBERSHIP_CREATE_HANDLE',
BOARD_MEMBERSHIP_CREATE_HANDLE__PROJECT_FETCH: 'BOARD_MEMBERSHIP_CREATE_HANDLE__PROJECT_FETCH', BOARD_MEMBERSHIP_CREATE_HANDLE__PROJECT_FETCH: 'BOARD_MEMBERSHIP_CREATE_HANDLE__PROJECT_FETCH',
BOARD_MEMBERSHIP_UPDATE: 'BOARD_MEMBERSHIP_UPDATE',
BOARD_MEMBERSHIP_UPDATE__SUCCESS: 'BOARD_MEMBERSHIP_UPDATE__SUCCESS',
BOARD_MEMBERSHIP_UPDATE__FAILURE: 'BOARD_MEMBERSHIP_UPDATE__FAILURE',
BOARD_MEMBERSHIP_UPDATE_HANDLE: 'BOARD_MEMBERSHIP_UPDATE_HANDLE',
BOARD_MEMBERSHIP_DELETE: 'BOARD_MEMBERSHIP_DELETE', BOARD_MEMBERSHIP_DELETE: 'BOARD_MEMBERSHIP_DELETE',
BOARD_MEMBERSHIP_DELETE__SUCCESS: 'BOARD_MEMBERSHIP_DELETE__SUCCESS', BOARD_MEMBERSHIP_DELETE__SUCCESS: 'BOARD_MEMBERSHIP_DELETE__SUCCESS',
BOARD_MEMBERSHIP_DELETE__FAILURE: 'BOARD_MEMBERSHIP_DELETE__FAILURE', BOARD_MEMBERSHIP_DELETE__FAILURE: 'BOARD_MEMBERSHIP_DELETE__FAILURE',

View file

@ -88,6 +88,8 @@ export default {
MEMBERSHIP_IN_CURRENT_BOARD_CREATE: `${PREFIX}/MEMBERSHIP_IN_CURRENT_BOARD_CREATE`, MEMBERSHIP_IN_CURRENT_BOARD_CREATE: `${PREFIX}/MEMBERSHIP_IN_CURRENT_BOARD_CREATE`,
BOARD_MEMBERSHIP_CREATE_HANDLE: `${PREFIX}/BOARD_MEMBERSHIP_CREATE_HANDLE`, BOARD_MEMBERSHIP_CREATE_HANDLE: `${PREFIX}/BOARD_MEMBERSHIP_CREATE_HANDLE`,
BOARD_MEMBERSHIP_UPDATE: `${PREFIX}/BOARD_MEMBERSHIP_UPDATE`,
BOARD_MEMBERSHIP_UPDATE_HANDLE: `${PREFIX}/BOARD_MEMBERSHIP_UPDATE_HANDLE`,
BOARD_MEMBERSHIP_DELETE: `${PREFIX}/BOARD_MEMBERSHIP_DELETE`, BOARD_MEMBERSHIP_DELETE: `${PREFIX}/BOARD_MEMBERSHIP_DELETE`,
BOARD_MEMBERSHIP_DELETE_HANDLE: `${PREFIX}/BOARD_MEMBERSHIP_DELETE_HANDLE`, BOARD_MEMBERSHIP_DELETE_HANDLE: `${PREFIX}/BOARD_MEMBERSHIP_DELETE_HANDLE`,

View file

@ -7,6 +7,11 @@ export const BoardTypes = {
KANBAN: 'kanban', KANBAN: 'kanban',
}; };
export const BoardMembershipRoles = {
EDITOR: 'editor',
VIEWER: 'viewer',
};
export const ActivityTypes = { export const ActivityTypes = {
CREATE_CARD: 'createCard', CREATE_CARD: 'createCard',
MOVE_CARD: 'moveCard', MOVE_CARD: 'moveCard',

View file

@ -27,6 +27,7 @@ const mapDispatchToProps = (dispatch) =>
bindActionCreators( bindActionCreators(
{ {
onMembershipCreate: entryActions.createMembershipInCurrentBoard, onMembershipCreate: entryActions.createMembershipInCurrentBoard,
onMembershipUpdate: entryActions.updateBoardMembership,
onMembershipDelete: entryActions.deleteBoardMembership, onMembershipDelete: entryActions.deleteBoardMembership,
onUserToFilterAdd: entryActions.addUserToFilterInCurrentBoard, onUserToFilterAdd: entryActions.addUserToFilterInCurrentBoard,
onUserFromFilterRemove: entryActions.removeUserFromFilterInCurrentBoard, onUserFromFilterRemove: entryActions.removeUserFromFilterInCurrentBoard,

View file

@ -3,17 +3,18 @@ import { connect } from 'react-redux';
import selectors from '../selectors'; import selectors from '../selectors';
import entryActions from '../entry-actions'; import entryActions from '../entry-actions';
import { BoardMembershipRoles } from '../constants/Enums';
import BoardKanban from '../components/BoardKanban'; import BoardKanban from '../components/BoardKanban';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { cardId } = selectors.selectPath(state); const { cardId } = selectors.selectPath(state);
const isCurrentUserMember = selectors.selectIsCurrentUserMemberForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const listIds = selectors.selectListIdsForCurrentBoard(state); const listIds = selectors.selectListIdsForCurrentBoard(state);
return { return {
listIds, listIds,
isCardModalOpened: !!cardId, isCardModalOpened: !!cardId,
canEdit: isCurrentUserMember, canEdit: !!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR,
}; };
}; };

View file

@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import selectors from '../selectors'; import selectors from '../selectors';
import entryActions from '../entry-actions'; import entryActions from '../entry-actions';
import { BoardMembershipRoles } from '../constants/Enums';
import Card from '../components/Card'; import Card from '../components/Card';
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
@ -17,7 +18,7 @@ const makeMapStateToProps = () => {
const allProjectsToLists = selectors.selectProjectsToListsForCurrentUser(state); const allProjectsToLists = selectors.selectProjectsToListsForCurrentUser(state);
const allBoardMemberships = selectors.selectMembershipsForCurrentBoard(state); const allBoardMemberships = selectors.selectMembershipsForCurrentBoard(state);
const allLabels = selectors.selectLabelsForCurrentBoard(state); const allLabels = selectors.selectLabelsForCurrentBoard(state);
const isCurrentUserMember = selectors.selectIsCurrentUserMemberForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const { name, dueDate, timer, coverUrl, boardId, listId, isPersisted } = selectCardById( const { name, dueDate, timer, coverUrl, boardId, listId, isPersisted } = selectCardById(
state, state,
@ -47,7 +48,8 @@ const makeMapStateToProps = () => {
allProjectsToLists, allProjectsToLists,
allBoardMemberships, allBoardMemberships,
allLabels, allLabels,
canEdit: isCurrentUserMember, canEdit:
!!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR,
}; };
}; };
}; };

View file

@ -6,6 +6,7 @@ import omit from 'lodash/omit';
import selectors from '../selectors'; import selectors from '../selectors';
import entryActions from '../entry-actions'; import entryActions from '../entry-actions';
import Paths from '../constants/Paths'; import Paths from '../constants/Paths';
import { BoardMembershipRoles } from '../constants/Enums';
import CardModal from '../components/CardModal'; import CardModal from '../components/CardModal';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
@ -14,7 +15,7 @@ const mapStateToProps = (state) => {
const isCurrentUserManager = selectors.selectIsCurrentUserManagerForCurrentProject(state); const isCurrentUserManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
const allBoardMemberships = selectors.selectMembershipsForCurrentBoard(state); const allBoardMemberships = selectors.selectMembershipsForCurrentBoard(state);
const allLabels = selectors.selectLabelsForCurrentBoard(state); const allLabels = selectors.selectLabelsForCurrentBoard(state);
const isCurrentUserMember = selectors.selectIsCurrentUserMemberForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const { const {
name, name,
@ -57,7 +58,11 @@ const mapStateToProps = (state) => {
allProjectsToLists, allProjectsToLists,
allBoardMemberships, allBoardMemberships,
allLabels, allLabels,
canEdit: isCurrentUserMember, canEdit: !!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR,
canEditCommentActivities:
!!currentUserMembership &&
(currentUserMembership.role === BoardMembershipRoles.EDITOR ||
currentUserMembership.canComment),
canEditAllCommentActivities: isCurrentUserManager, canEditAllCommentActivities: isCurrentUserManager,
}; };
}; };

View file

@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import selectors from '../selectors'; import selectors from '../selectors';
import entryActions from '../entry-actions'; import entryActions from '../entry-actions';
import { BoardMembershipRoles } from '../constants/Enums';
import List from '../components/List'; import List from '../components/List';
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
@ -12,7 +13,7 @@ const makeMapStateToProps = () => {
return (state, { id, index }) => { return (state, { id, index }) => {
const { name, isPersisted } = selectListById(state, id); const { name, isPersisted } = selectListById(state, id);
const cardIds = selectCardIdsByListId(state, id); const cardIds = selectCardIdsByListId(state, id);
const isCurrentUserMember = selectors.selectIsCurrentUserMemberForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
return { return {
id, id,
@ -20,7 +21,8 @@ const makeMapStateToProps = () => {
name, name,
isPersisted, isPersisted,
cardIds, cardIds,
canEdit: isCurrentUserMember, canEdit:
!!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR,
}; };
}; };
}; };

View file

@ -14,6 +14,21 @@ const handleBoardMembershipCreate = (boardMembership) => ({
}, },
}); });
const updateBoardMembership = (id, data) => ({
type: EntryActionTypes.BOARD_MEMBERSHIP_UPDATE,
payload: {
id,
data,
},
});
const handleBoardMembershipUpdate = (boardMembership) => ({
type: EntryActionTypes.BOARD_MEMBERSHIP_UPDATE_HANDLE,
payload: {
boardMembership,
},
});
const deleteBoardMembership = (id) => ({ const deleteBoardMembership = (id) => ({
type: EntryActionTypes.BOARD_MEMBERSHIP_DELETE, type: EntryActionTypes.BOARD_MEMBERSHIP_DELETE,
payload: { payload: {
@ -31,6 +46,8 @@ const handleBoardMembershipDelete = (boardMembership) => ({
export default { export default {
createMembershipInCurrentBoard, createMembershipInCurrentBoard,
handleBoardMembershipCreate, handleBoardMembershipCreate,
updateBoardMembership,
handleBoardMembershipUpdate,
deleteBoardMembership, deleteBoardMembership,
handleBoardMembershipDelete, handleBoardMembershipDelete,
}; };

View file

@ -41,6 +41,9 @@ export default {
background: 'Background', background: 'Background',
board: 'Board', board: 'Board',
boardNotFound_title: 'Board Not Found', boardNotFound_title: 'Board Not Found',
canComment: 'Can comment',
canEditContentOfBoard: 'Can edit the content of the board',
canOnlyViewBoard: 'Can only view the board',
cardActions_title: 'Card Actions', cardActions_title: 'Card Actions',
cardNotFound_title: 'Card Not Found', cardNotFound_title: 'Card Not Found',
cardOrActionAreDeleted: 'Card or action are deleted', cardOrActionAreDeleted: 'Card or action are deleted',
@ -66,6 +69,7 @@ export default {
description: 'Description', description: 'Description',
detectAutomatically: 'Detect automatically', detectAutomatically: 'Detect automatically',
dropFileToUpload: 'Drop file to upload', dropFileToUpload: 'Drop file to upload',
editor: 'Editor',
editAttachment_title: 'Edit Attachment', editAttachment_title: 'Edit Attachment',
editAvatar_title: 'Edit Avatar', editAvatar_title: 'Edit Avatar',
editBoard_title: 'Edit Board', editBoard_title: 'Edit Board',
@ -74,6 +78,7 @@ export default {
editInformation_title: 'Edit Information', editInformation_title: 'Edit Information',
editLabel_title: 'Edit Label', editLabel_title: 'Edit Label',
editPassword_title: 'Edit Password', editPassword_title: 'Edit Password',
editPermissions_title: 'Edit Permissions',
editTimer_title: 'Edit Timer', editTimer_title: 'Edit Timer',
editUsername_title: 'Edit Username', editUsername_title: 'Edit Username',
email: 'E-mail', email: 'E-mail',
@ -127,6 +132,7 @@ export default {
seconds: 'Seconds', seconds: 'Seconds',
selectBoard: 'Select board', selectBoard: 'Select board',
selectList: 'Select list', selectList: 'Select list',
selectPermissions_title: 'Select Permissions',
selectProject: 'Select project', selectProject: 'Select project',
settings: 'Settings', settings: 'Settings',
subscribeToMyOwnCardsByDefault: 'Subscribe to my own cards by default', subscribeToMyOwnCardsByDefault: 'Subscribe to my own cards by default',
@ -146,6 +152,7 @@ export default {
username: 'Username', username: 'Username',
usernameAlreadyInUse: 'Username already in use', usernameAlreadyInUse: 'Username already in use',
users: 'Users', users: 'Users',
viewer: 'Viewer',
writeComment: 'Write a comment...', writeComment: 'Write a comment...',
}, },
@ -157,6 +164,7 @@ export default {
addCard_title: 'Add Card', addCard_title: 'Add Card',
addComment: 'Add comment', addComment: 'Add comment',
addList: 'Add list', addList: 'Add list',
addMember: 'Add member',
addMoreDetailedDescription: 'Add more detailed description', addMoreDetailedDescription: 'Add more detailed description',
addTask: 'Add task', addTask: 'Add task',
addToCard: 'Add to card', addToCard: 'Add to card',
@ -188,6 +196,7 @@ export default {
editEmail_title: 'Edit E-mail', editEmail_title: 'Edit E-mail',
editInformation_title: 'Edit Information', editInformation_title: 'Edit Information',
editPassword_title: 'Edit Password', editPassword_title: 'Edit Password',
editPermissions: 'Edit permissions',
editTimer_title: 'Edit Timer', editTimer_title: 'Edit Timer',
editTitle_title: 'Edit Title', editTitle_title: 'Edit Title',
editUsername_title: 'Edit Username', editUsername_title: 'Edit Username',

View file

@ -39,6 +39,9 @@ export default {
authentication: 'Аутентификация', authentication: 'Аутентификация',
board: 'Доска', board: 'Доска',
boardNotFound: 'Доска не найдена', boardNotFound: 'Доска не найдена',
canComment: 'Может комментировать',
canEditContentOfBoard: 'Может редактировать содержимое доски',
canOnlyViewBoard: 'Может только просматривать доску',
cardActions: 'Действия с карточкой', cardActions: 'Действия с карточкой',
cardNotFound: 'Карточка не найдена', cardNotFound: 'Карточка не найдена',
cardOrActionAreDeleted: 'Карточка или действие удалены', cardOrActionAreDeleted: 'Карточка или действие удалены',
@ -64,6 +67,7 @@ export default {
description: 'Описание', description: 'Описание',
detectAutomatically: 'Определить автоматически', detectAutomatically: 'Определить автоматически',
dropFileToUpload: 'Перетяните файл, чтобы загрузить', dropFileToUpload: 'Перетяните файл, чтобы загрузить',
editor: 'Редактор',
editAttachment: 'Изменение вложения', editAttachment: 'Изменение вложения',
editAvatar: 'Изменение аватара', editAvatar: 'Изменение аватара',
editBoard: 'Изменение доски', editBoard: 'Изменение доски',
@ -72,6 +76,7 @@ export default {
editEmail: 'Изменение e-mail', editEmail: 'Изменение e-mail',
editLabel: 'Изменения метки', editLabel: 'Изменения метки',
editPassword: 'Изменение пароля', editPassword: 'Изменение пароля',
editPermissions: 'Редактирование разрешений',
editTimer: 'Изменение таймера', editTimer: 'Изменение таймера',
editTitle: 'Изменение названия', editTitle: 'Изменение названия',
editUsername: 'Изменение имени пользователя', editUsername: 'Изменение имени пользователя',
@ -122,6 +127,7 @@ export default {
seconds: 'Секунды', seconds: 'Секунды',
selectBoard: 'Выберите доску', selectBoard: 'Выберите доску',
selectList: 'Выберите список', selectList: 'Выберите список',
selectPermissions: 'Выбор разрешений',
selectProject: 'Выберите проект', selectProject: 'Выберите проект',
settings: 'Настройки', settings: 'Настройки',
subscribeToMyOwnCardsByDefault: 'По умолчанию подписаться на мои собственные карточки', subscribeToMyOwnCardsByDefault: 'По умолчанию подписаться на мои собственные карточки',
@ -141,6 +147,7 @@ export default {
username: 'Имя пользователя', username: 'Имя пользователя',
usernameAlreadyInUse: 'Имя пользователя уже занято', usernameAlreadyInUse: 'Имя пользователя уже занято',
users: 'Пользователи', users: 'Пользователи',
viewer: 'Читатель',
writeComment: 'Напишите комментарий...', writeComment: 'Напишите комментарий...',
}, },
@ -151,6 +158,7 @@ export default {
addCard: 'Добавить карточку', addCard: 'Добавить карточку',
addComment: 'Добавить комментарий', addComment: 'Добавить комментарий',
addList: 'Добавить список', addList: 'Добавить список',
addMember: 'Добавить участника',
addMoreDetailedDescription: 'Добавить более подробное описание', addMoreDetailedDescription: 'Добавить более подробное описание',
addTask: 'Добавить задачу', addTask: 'Добавить задачу',
addToCard: 'Добавить на карточку', addToCard: 'Добавить на карточку',
@ -178,6 +186,7 @@ export default {
editDescription: 'Изменить описание', editDescription: 'Изменить описание',
editEmail: 'Изменить e-mail', editEmail: 'Изменить e-mail',
editPassword: 'Изменить пароль', editPassword: 'Изменить пароль',
editPermissions: 'Изменить разрешения',
editTask: 'Изменить задачу', editTask: 'Изменить задачу',
editTimer: 'Изменить таймер', editTimer: 'Изменить таймер',
editTitle: 'Изменить название', editTitle: 'Изменить название',

View file

@ -175,6 +175,14 @@ export default class extends Model {
return this.lists.orderBy('position'); return this.lists.orderBy('position');
} }
getMembershipModel(userId) {
return this.memberships
.filter({
userId,
})
.first();
}
hasMemberUser(userId) { hasMemberUser(userId) {
return this.memberships return this.memberships
.filter({ .filter({

View file

@ -7,6 +7,8 @@ export default class extends Model {
static fields = { static fields = {
id: attr(), id: attr(),
role: attr(),
canComment: attr(),
boardId: fk({ boardId: fk({
to: 'Board', to: 'Board',
as: 'board', as: 'board',
@ -65,6 +67,15 @@ export default class extends Model {
}); });
} }
break;
case ActionTypes.BOARD_MEMBERSHIP_UPDATE:
BoardMembership.withId(payload.id).update(payload.data);
break;
case ActionTypes.BOARD_MEMBERSHIP_UPDATE__SUCCESS:
case ActionTypes.BOARD_MEMBERSHIP_UPDATE_HANDLE:
BoardMembership.upsert(payload.boardMembership);
break; break;
case ActionTypes.BOARD_MEMBERSHIP_DELETE: case ActionTypes.BOARD_MEMBERSHIP_DELETE:
BoardMembership.withId(payload.id).deleteWithRelated(); BoardMembership.withId(payload.id).deleteWithRelated();

View file

@ -135,6 +135,24 @@ export function* handleBoardMembershipCreate(boardMembership) {
); );
} }
export function* updateBoardMembership(id, data) {
yield put(actions.updateBoardMembership(id, data));
let boardMembership;
try {
({ item: boardMembership } = yield call(request, api.updateBoardMembership, id, data));
} catch (error) {
yield put(actions.updateBoardMembership.failure(id, error));
return;
}
yield put(actions.updateBoardMembership.success(boardMembership));
}
export function* handleBoardMembershipUpdate(boardMembership) {
yield put(actions.handleBoardMembershipUpdate(boardMembership));
}
export function* deleteBoardMembership(id) { export function* deleteBoardMembership(id) {
let boardMembership = yield select(selectors.selectBoardMembershipById, id); let boardMembership = yield select(selectors.selectBoardMembershipById, id);
@ -184,6 +202,8 @@ export default {
createBoardMembership, createBoardMembership,
createMembershipInCurrentBoard, createMembershipInCurrentBoard,
handleBoardMembershipCreate, handleBoardMembershipCreate,
updateBoardMembership,
handleBoardMembershipUpdate,
deleteBoardMembership, deleteBoardMembership,
handleBoardMembershipDelete, handleBoardMembershipDelete,
}; };

View file

@ -11,6 +11,12 @@ export default function* boardMembershipsWatchers() {
takeEvery(EntryActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE, ({ payload: { boardMembership } }) => takeEvery(EntryActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE, ({ payload: { boardMembership } }) =>
services.handleBoardMembershipCreate(boardMembership), services.handleBoardMembershipCreate(boardMembership),
), ),
takeEvery(EntryActionTypes.BOARD_MEMBERSHIP_UPDATE, ({ payload: { id, data } }) =>
services.updateBoardMembership(id, data),
),
takeEvery(EntryActionTypes.BOARD_MEMBERSHIP_UPDATE_HANDLE, ({ payload: { boardMembership } }) =>
services.handleBoardMembershipUpdate(boardMembership),
),
takeEvery(EntryActionTypes.BOARD_MEMBERSHIP_DELETE, ({ payload: { id } }) => takeEvery(EntryActionTypes.BOARD_MEMBERSHIP_DELETE, ({ payload: { id } }) =>
services.deleteBoardMembership(id), services.deleteBoardMembership(id),
), ),

View file

@ -64,6 +64,10 @@ const createSocketEventsChannel = () =>
emit(entryActions.handleBoardMembershipCreate(item)); emit(entryActions.handleBoardMembershipCreate(item));
}; };
const handleBoardMembershipUpdate = ({ item }) => {
emit(entryActions.handleBoardMembershipUpdate(item));
};
const handleBoardMembershipDelete = ({ item }) => { const handleBoardMembershipDelete = ({ item }) => {
emit(entryActions.handleBoardMembershipDelete(item)); emit(entryActions.handleBoardMembershipDelete(item));
}; };
@ -183,6 +187,7 @@ const createSocketEventsChannel = () =>
socket.on('boardDelete', handleBoardDelete); socket.on('boardDelete', handleBoardDelete);
socket.on('boardMembershipCreate', handleBoardMembershipCreate); socket.on('boardMembershipCreate', handleBoardMembershipCreate);
socket.on('boardMembershipUpdate', handleBoardMembershipUpdate);
socket.on('boardMembershipDelete', handleBoardMembershipDelete); socket.on('boardMembershipDelete', handleBoardMembershipDelete);
socket.on('listCreate', handleListCreate); socket.on('listCreate', handleListCreate);
@ -238,6 +243,7 @@ const createSocketEventsChannel = () =>
socket.off('boardDelete', handleBoardDelete); socket.off('boardDelete', handleBoardDelete);
socket.off('boardMembershipCreate', handleBoardMembershipCreate); socket.off('boardMembershipCreate', handleBoardMembershipCreate);
socket.off('boardMembershipUpdate', handleBoardMembershipUpdate);
socket.off('boardMembershipDelete', handleBoardMembershipDelete); socket.off('boardMembershipDelete', handleBoardMembershipDelete);
socket.off('listCreate', handleListCreate); socket.off('listCreate', handleListCreate);

View file

@ -147,22 +147,28 @@ export const selectFilterLabelsForCurrentBoard = createSelector(
}, },
); );
export const selectIsCurrentUserMemberForCurrentBoard = createSelector( export const selectCurrentUserMembershipForCurrentBoard = createSelector(
orm, orm,
(state) => selectPath(state).boardId, (state) => selectPath(state).boardId,
(state) => selectCurrentUserId(state), (state) => selectCurrentUserId(state),
({ Board }, id, currentUserId) => { ({ Board }, id, currentUserId) => {
if (!id) { if (!id) {
return false; return id;
} }
const boardModel = Board.withId(id); const boardModel = Board.withId(id);
if (!boardModel) { if (!boardModel) {
return false; return boardModel;
} }
return boardModel.hasMemberUser(currentUserId); const boardMembershipModel = boardModel.getMembershipModel(currentUserId);
if (!boardMembershipModel) {
return boardMembershipModel;
}
return boardMembershipModel.ref;
}, },
); );
@ -175,5 +181,5 @@ export default {
selectListIdsForCurrentBoard, selectListIdsForCurrentBoard,
selectFilterUsersForCurrentBoard, selectFilterUsersForCurrentBoard,
selectFilterLabelsForCurrentBoard, selectFilterLabelsForCurrentBoard,
selectIsCurrentUserMemberForCurrentBoard, selectCurrentUserMembershipForCurrentBoard,
}; };

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@ -18,6 +21,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -33,12 +39,19 @@ module.exports = {
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId); const boardMembership = await BoardMembership.findOne({
boardId: card.boardId,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.CARD_NOT_FOUND; // Forbidden throw Errors.CARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
this.req this.req
.file('file') .file('file')
.upload(sails.helpers.utils.createAttachmentReceiver(), async (error, files) => { .upload(sails.helpers.utils.createAttachmentReceiver(), async (error, files) => {

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
ATTACHMENT_NOT_FOUND: { ATTACHMENT_NOT_FOUND: {
attachmentNotFound: 'Attachment not found', attachmentNotFound: 'Attachment not found',
}, },
@ -14,6 +17,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
attachmentNotFound: { attachmentNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -29,12 +35,19 @@ module.exports = {
let { attachment } = path; let { attachment } = path;
const { card, board } = path; const { card, board } = path;
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id); const boardMembership = await BoardMembership.findOne({
boardId: board.id,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
attachment = await sails.helpers.attachments.deleteOne(attachment, board, card, this.req); attachment = await sails.helpers.attachments.deleteOne(attachment, board, card, this.req);
if (!attachment) { if (!attachment) {

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
ATTACHMENT_NOT_FOUND: { ATTACHMENT_NOT_FOUND: {
attachmentNotFound: 'Attachment not found', attachmentNotFound: 'Attachment not found',
}, },
@ -18,6 +21,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
attachmentNotFound: { attachmentNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -33,12 +39,19 @@ module.exports = {
let { attachment } = path; let { attachment } = path;
const { board } = path; const { board } = path;
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id); const boardMembership = await BoardMembership.findOne({
boardId: board.id,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, ['name']); const values = _.pick(inputs, ['name']);
attachment = await sails.helpers.attachments.updateOne(attachment, values, board, this.req); attachment = await sails.helpers.attachments.updateOne(attachment, values, board, this.req);

View file

@ -22,6 +22,14 @@ module.exports = {
regex: /^[0-9]+$/, regex: /^[0-9]+$/,
required: true, required: true,
}, },
role: {
type: 'string',
isIn: Object.values(BoardMembership.Roles),
required: true,
},
canComment: {
type: 'boolean',
},
}, },
exits: { exits: {
@ -58,8 +66,10 @@ module.exports = {
throw Error.USER_NOT_FOUND; throw Error.USER_NOT_FOUND;
} }
const values = _.pick(inputs, ['role', 'canComment']);
const boardMembership = await sails.helpers.boardMemberships const boardMembership = await sails.helpers.boardMemberships
.createOne(user, board, this.req) .createOne(values, user, board, this.req)
.intercept('userAlreadyBoardMember', () => Errors.USER_ALREADY_BOARD_MEMBER); .intercept('userAlreadyBoardMember', () => Errors.USER_ALREADY_BOARD_MEMBER);
return { return {

View file

@ -0,0 +1,57 @@
const Errors = {
BOARD_MEMBERSHIP_NOT_FOUND: {
boardMembershipNotFound: 'Board membership not found',
},
};
module.exports = {
inputs: {
id: {
type: 'string',
regex: /^[0-9]+$/,
required: true,
},
role: {
type: 'string',
isIn: Object.values(BoardMembership.Roles),
},
canComment: {
type: 'boolean',
},
},
exits: {
boardMembershipNotFound: {
responseType: 'notFound',
},
},
async fn(inputs) {
const { currentUser } = this.req;
const path = await sails.helpers.boardMemberships
.getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.BOARD_MEMBERSHIP_NOT_FOUND);
let { boardMembership } = path;
const { project } = path;
const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id);
if (!isProjectManager) {
throw Errors.BOARD_MEMBERSHIP_NOT_FOUND; // Forbidden
}
const values = _.pick(inputs, ['role', 'canComment']);
boardMembership = await sails.helpers.boardMemberships.updateOne(
boardMembership,
values,
this.req,
);
return {
item: boardMembership,
};
},
};

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@ -25,6 +28,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -43,12 +49,19 @@ module.exports = {
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId); const boardMembership = await BoardMembership.findOne({
boardId: card.boardId,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.CARD_NOT_FOUND; // Forbidden throw Errors.CARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const label = await Label.findOne({ const label = await Label.findOne({
id: inputs.labelId, id: inputs.labelId,
boardId: card.boardId, boardId: card.boardId,

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@ -22,6 +25,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -37,12 +43,19 @@ module.exports = {
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id); const boardMembership = await BoardMembership.findOne({
boardId: board.id,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.CARD_NOT_FOUND; // Forbidden throw Errors.CARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
let cardLabel = await CardLabel.findOne({ let cardLabel = await CardLabel.findOne({
cardId: inputs.cardId, cardId: inputs.cardId,
labelId: inputs.labelId, labelId: inputs.labelId,

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@ -25,6 +28,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -43,13 +49,20 @@ module.exports = {
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
let isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId); const boardMembership = await BoardMembership.findOne({
boardId: card.boardId,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.CARD_NOT_FOUND; // Forbidden throw Errors.CARD_NOT_FOUND; // Forbidden
} }
isBoardMember = await sails.helpers.users.isBoardMember(inputs.userId, card.boardId); if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const isBoardMember = await sails.helpers.users.isBoardMember(inputs.userId, card.boardId);
if (!isBoardMember) { if (!isBoardMember) {
throw Errors.USER_NOT_FOUND; throw Errors.USER_NOT_FOUND;

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@ -22,6 +25,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -37,12 +43,19 @@ module.exports = {
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id); const boardMembership = await BoardMembership.findOne({
boardId: board.id,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.CARD_NOT_FOUND; // Forbidden throw Errors.CARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
let cardMembership = await CardMembership.findOne({ let cardMembership = await CardMembership.findOne({
cardId: inputs.cardId, cardId: inputs.cardId,
userId: inputs.userId, userId: inputs.userId,

View file

@ -1,6 +1,9 @@
const moment = require('moment'); const moment = require('moment');
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
BOARD_NOT_FOUND: { BOARD_NOT_FOUND: {
boardNotFound: 'Board not found', boardNotFound: 'Board not found',
}, },
@ -67,6 +70,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
boardNotFound: { boardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -88,12 +94,19 @@ module.exports = {
.getProjectPath(inputs.boardId) .getProjectPath(inputs.boardId)
.intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id); const boardMembership = await BoardMembership.findOne({
boardId: board.id,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.BOARD_NOT_FOUND; // Forbidden throw Errors.BOARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
let list; let list;
if (!_.isUndefined(inputs.listId)) { if (!_.isUndefined(inputs.listId)) {
list = await List.findOne({ list = await List.findOne({

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@ -14,6 +17,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -26,12 +32,19 @@ module.exports = {
.getProjectPath(inputs.id) .getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId); const boardMembership = await BoardMembership.findOne({
boardId: card.boardId,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.CARD_NOT_FOUND; // Forbidden throw Errors.CARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
card = await sails.helpers.cards.deleteOne(card, this.req); card = await sails.helpers.cards.deleteOne(card, this.req);
if (!card) { if (!card) {

View file

@ -1,6 +1,9 @@
const moment = require('moment'); const moment = require('moment');
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@ -83,6 +86,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -110,23 +116,37 @@ module.exports = {
let { card } = path; let { card } = path;
const { list, board } = path; const { list, board } = path;
let isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id); let boardMembership = await BoardMembership.findOne({
boardId: board.id,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.CARD_NOT_FOUND; // Forbidden throw Errors.CARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
let nextBoard; let nextBoard;
if (!_.isUndefined(inputs.boardId)) { if (!_.isUndefined(inputs.boardId)) {
({ board: nextBoard } = await sails.helpers.boards ({ board: nextBoard } = await sails.helpers.boards
.getProjectPath(inputs.boardId) .getProjectPath(inputs.boardId)
.intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND)); .intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND));
isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, nextBoard.id); boardMembership = await BoardMembership.findOne({
boardId: nextBoard.id,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.BOARD_NOT_FOUND; // Forbidden throw Errors.BOARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
} }
let nextList; let nextList;

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@ -18,6 +21,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -30,12 +36,19 @@ module.exports = {
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId); const boardMembership = await BoardMembership.findOne({
boardId: card.boardId,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.CARD_NOT_FOUND; // Forbidden throw Errors.CARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR && !boardMembership.canComment) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = { const values = {
type: Action.Types.COMMENT_CARD, type: Action.Types.COMMENT_CARD,
data: _.pick(inputs, ['text']), data: _.pick(inputs, ['text']),

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
COMMENT_ACTION_NOT_FOUND: { COMMENT_ACTION_NOT_FOUND: {
commentActionNotFound: 'Comment action not found', commentActionNotFound: 'Comment action not found',
}, },
@ -14,6 +17,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
commentActionNotFound: { commentActionNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -39,11 +45,18 @@ module.exports = {
throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden
} }
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id); const boardMembership = await BoardMembership.findOne({
boardId: board.id,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR && !boardMembership.canComment) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
} }
action = await sails.helpers.actions.deleteOne(action, board, this.req); action = await sails.helpers.actions.deleteOne(action, board, this.req);

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
COMMENT_ACTION_NOT_FOUND: { COMMENT_ACTION_NOT_FOUND: {
commentActionNotFound: 'Comment action not found', commentActionNotFound: 'Comment action not found',
}, },
@ -18,6 +21,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
commentActionNotFound: { commentActionNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -43,11 +49,18 @@ module.exports = {
throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden
} }
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id); const boardMembership = await BoardMembership.findOne({
boardId: board.id,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR && !boardMembership.canComment) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
} }
const values = { const values = {

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
BOARD_NOT_FOUND: { BOARD_NOT_FOUND: {
boardNotFound: 'Board not found', boardNotFound: 'Board not found',
}, },
@ -24,6 +27,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
boardNotFound: { boardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -36,12 +42,19 @@ module.exports = {
.getProjectPath(inputs.boardId) .getProjectPath(inputs.boardId)
.intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id); const boardMembership = await BoardMembership.findOne({
boardId: board.id,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.BOARD_NOT_FOUND; // Forbidden throw Errors.BOARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, ['name', 'color']); const values = _.pick(inputs, ['name', 'color']);
const label = await sails.helpers.labels.createOne(values, board, this.req); const label = await sails.helpers.labels.createOne(values, board, this.req);

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
LABEL_NOT_FOUND: { LABEL_NOT_FOUND: {
labelNotFound: 'Label not found', labelNotFound: 'Label not found',
}, },
@ -14,6 +17,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
labelNotFound: { labelNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -26,12 +32,19 @@ module.exports = {
.getProjectPath(inputs.id) .getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.LABEL_NOT_FOUND); .intercept('pathNotFound', () => Errors.LABEL_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, label.boardId); const boardMembership = await BoardMembership.findOne({
boardId: label.boardId,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.LABEL_NOT_FOUND; // Forbidden throw Errors.LABEL_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
label = await sails.helpers.labels.deleteOne(label, this.req); label = await sails.helpers.labels.deleteOne(label, this.req);
if (!label) { if (!label) {

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
LABEL_NOT_FOUND: { LABEL_NOT_FOUND: {
labelNotFound: 'Label not found', labelNotFound: 'Label not found',
}, },
@ -24,6 +27,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
labelNotFound: { labelNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -36,12 +42,19 @@ module.exports = {
.getProjectPath(inputs.id) .getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.LABEL_NOT_FOUND); .intercept('pathNotFound', () => Errors.LABEL_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, label.boardId); const boardMembership = await BoardMembership.findOne({
boardId: label.boardId,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.LABEL_NOT_FOUND; // Forbidden throw Errors.LABEL_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, ['name', 'color']); const values = _.pick(inputs, ['name', 'color']);
label = await sails.helpers.labels.updateOne(label, values, this.req); label = await sails.helpers.labels.updateOne(label, values, this.req);

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
BOARD_NOT_FOUND: { BOARD_NOT_FOUND: {
boardNotFound: 'Board not found', boardNotFound: 'Board not found',
}, },
@ -22,6 +25,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
boardNotFound: { boardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -34,12 +40,19 @@ module.exports = {
.getProjectPath(inputs.boardId) .getProjectPath(inputs.boardId)
.intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id); const boardMembership = await BoardMembership.findOne({
boardId: board.id,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.BOARD_NOT_FOUND; // Forbidden throw Errors.BOARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, ['position', 'name']); const values = _.pick(inputs, ['position', 'name']);
const list = await sails.helpers.lists.createOne(values, board, this.req); const list = await sails.helpers.lists.createOne(values, board, this.req);

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
LIST_NOT_FOUND: { LIST_NOT_FOUND: {
listNotFound: 'List not found', listNotFound: 'List not found',
}, },
@ -14,6 +17,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
listNotFound: { listNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -26,12 +32,19 @@ module.exports = {
.getProjectPath(inputs.id) .getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.LIST_NOT_FOUND); .intercept('pathNotFound', () => Errors.LIST_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, list.boardId); const boardMembership = await BoardMembership.findOne({
boardId: list.boardId,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.LIST_NOT_FOUND; // Forbidden throw Errors.LIST_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
list = await sails.helpers.lists.deleteOne(list, this.req); list = await sails.helpers.lists.deleteOne(list, this.req);
if (!list) { if (!list) {

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
LIST_NOT_FOUND: { LIST_NOT_FOUND: {
listNotFound: 'List not found', listNotFound: 'List not found',
}, },
@ -21,6 +24,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
listNotFound: { listNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -33,12 +39,19 @@ module.exports = {
.getProjectPath(inputs.id) .getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.LIST_NOT_FOUND); .intercept('pathNotFound', () => Errors.LIST_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, list.boardId); const boardMembership = await BoardMembership.findOne({
boardId: list.boardId,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.LIST_NOT_FOUND; // Forbidden throw Errors.LIST_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, ['position', 'name']); const values = _.pick(inputs, ['position', 'name']);
list = await sails.helpers.lists.updateOne(list, values, this.req); list = await sails.helpers.lists.updateOne(list, values, this.req);

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@ -25,6 +28,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -37,12 +43,19 @@ module.exports = {
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId); const boardMembership = await BoardMembership.findOne({
boardId: card.boardId,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.CARD_NOT_FOUND; // Forbidden throw Errors.CARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, ['position', 'name', 'isCompleted']); const values = _.pick(inputs, ['position', 'name', 'isCompleted']);
const task = await sails.helpers.tasks.createOne(values, card, this.req); const task = await sails.helpers.tasks.createOne(values, card, this.req);

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
TASK_NOT_FOUND: { TASK_NOT_FOUND: {
taskNotFound: 'Task not found', taskNotFound: 'Task not found',
}, },
@ -14,6 +17,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
taskNotFound: { taskNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -29,12 +35,19 @@ module.exports = {
let { task } = path; let { task } = path;
const { board } = path; const { board } = path;
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id); const boardMembership = await BoardMembership.findOne({
boardId: board.id,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.TASK_NOT_FOUND; // Forbidden throw Errors.TASK_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
task = await sails.helpers.tasks.deleteOne(task, board, this.req); task = await sails.helpers.tasks.deleteOne(task, board, this.req);
if (!task) { if (!task) {

View file

@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
TASK_NOT_FOUND: { TASK_NOT_FOUND: {
taskNotFound: 'Task not found', taskNotFound: 'Task not found',
}, },
@ -24,6 +27,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
taskNotFound: { taskNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@ -39,12 +45,19 @@ module.exports = {
let { task } = path; let { task } = path;
const { board } = path; const { board } = path;
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id); const boardMembership = await BoardMembership.findOne({
boardId: board.id,
userId: currentUser.id,
});
if (!isBoardMember) { if (!boardMembership) {
throw Errors.TASK_NOT_FOUND; // Forbidden throw Errors.TASK_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, ['position', 'name', 'isCompleted']); const values = _.pick(inputs, ['position', 'name', 'isCompleted']);
task = await sails.helpers.tasks.updateOne(task, values, board, this.req); task = await sails.helpers.tasks.updateOne(task, values, board, this.req);

View file

@ -1,5 +1,9 @@
module.exports = { module.exports = {
inputs: { inputs: {
values: {
type: 'json',
required: true,
},
user: { user: {
type: 'ref', type: 'ref',
required: true, required: true,
@ -18,7 +22,16 @@ module.exports = {
}, },
async fn(inputs) { async fn(inputs) {
if (inputs.values.role === BoardMembership.Roles.EDITOR) {
delete inputs.values.canComment; // eslint-disable-line no-param-reassign
} else if (inputs.values.role === BoardMembership.Roles.VIEWER) {
if (_.isNil(inputs.values.canComment)) {
inputs.values.canComment = false; // eslint-disable-line no-param-reassign
}
}
const boardMembership = await BoardMembership.create({ const boardMembership = await BoardMembership.create({
...inputs.values,
boardId: inputs.board.id, boardId: inputs.board.id,
userId: inputs.user.id, userId: inputs.user.id,
}) })

View file

@ -0,0 +1,46 @@
module.exports = {
inputs: {
record: {
type: 'ref',
required: true,
},
values: {
type: 'json',
required: true,
},
request: {
type: 'ref',
},
},
async fn(inputs) {
const role = inputs.values.role || inputs.record.role;
if (role === BoardMembership.Roles.EDITOR) {
inputs.values.canComment = null; // eslint-disable-line no-param-reassign
} else if (role === BoardMembership.Roles.VIEWER) {
const canComment = _.isUndefined(inputs.values.canComment)
? inputs.record.canComment
: inputs.values.canComment;
if (_.isNull(canComment)) {
inputs.values.canComment = false; // eslint-disable-line no-param-reassign
}
}
const boardMembership = await BoardMembership.updateOne(inputs.record.id).set(inputs.values);
if (boardMembership) {
sails.sockets.broadcast(
`board:${boardMembership.boardId}`,
'boardMembershipUpdate',
{
item: boardMembership,
},
inputs.request,
);
}
return boardMembership;
},
};

View file

@ -57,6 +57,7 @@ module.exports = {
const boardMembership = await BoardMembership.create({ const boardMembership = await BoardMembership.create({
boardId: board.id, boardId: board.id,
userId: inputs.user.id, userId: inputs.user.id,
role: BoardMembership.Roles.EDITOR,
}).fetch(); }).fetch();
managerUserIds.forEach((userId) => { managerUserIds.forEach((userId) => {

View file

@ -5,12 +5,30 @@
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models * @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
*/ */
const Roles = {
EDITOR: 'editor',
VIEWER: 'viewer',
};
module.exports = { module.exports = {
Roles,
attributes: { attributes: {
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗ // ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗ // ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝ // ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
role: {
type: 'string',
isIn: Object.values(Roles),
required: true,
},
canComment: {
type: 'boolean',
allowNull: true,
columnName: 'can_comment',
},
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗ // ║╣ ║║║╠╩╗║╣ ║║╚═╗
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝ // ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝

View file

@ -37,6 +37,7 @@ module.exports.routes = {
'DELETE /api/boards/:id': 'boards/delete', 'DELETE /api/boards/:id': 'boards/delete',
'POST /api/boards/:boardId/memberships': 'board-memberships/create', 'POST /api/boards/:boardId/memberships': 'board-memberships/create',
'PATCH /api/board-memberships/:id': 'board-memberships/update',
'DELETE /api/board-memberships/:id': 'board-memberships/delete', 'DELETE /api/board-memberships/:id': 'board-memberships/delete',
'POST /api/boards/:boardId/labels': 'labels/create', 'POST /api/boards/:boardId/labels': 'labels/create',

View file

@ -0,0 +1,18 @@
module.exports.up = async (knex) => {
await knex.schema.table('board_membership', (table) => {
/* Columns */
table.text('role').notNullable().defaultTo('editor');
table.boolean('can_comment');
});
return knex.schema.alterTable('board_membership', (table) => {
table.text('role').notNullable().alter();
});
};
module.exports.down = (knex) =>
knex.schema.table('board_membership', (table) => {
table.dropColumn('role');
table.dropColumn('can_comment');
});