1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-19 05:09:43 +02:00

Initial commit

This commit is contained in:
Maksim Eltyshev 2019-08-31 04:07:25 +05:00
commit 36fe34e8e1
583 changed files with 91539 additions and 0 deletions

View file

@ -0,0 +1,24 @@
import ActionTypes from '../constants/ActionTypes';
/* Events */
export const createActionReceived = (action) => ({
type: ActionTypes.ACTION_CREATE_RECEIVED,
payload: {
action,
},
});
export const updateActionReceived = (action) => ({
type: ActionTypes.ACTION_UPDATE_RECEIVED,
payload: {
action,
},
});
export const deleteActionReceived = (action) => ({
type: ActionTypes.ACTION_DELETE_RECEIVED,
payload: {
action,
},
});

View file

@ -0,0 +1,27 @@
import ActionTypes from '../constants/ActionTypes';
/* Events */
export const fetchActionsRequested = (cardId) => ({
type: ActionTypes.ACTIONS_FETCH_REQUESTED,
payload: {
cardId,
},
});
export const fetchActionsSucceeded = (cardId, actions, users) => ({
type: ActionTypes.ACTIONS_FETCH_SUCCEEDED,
payload: {
cardId,
actions,
users,
},
});
export const fetchActionsFailed = (cardId, error) => ({
type: ActionTypes.ACTIONS_FETCH_FAILED,
payload: {
cardId,
error,
},
});

View file

@ -0,0 +1,9 @@
import ActionTypes from '../constants/ActionTypes';
/* Events */
// eslint-disable-next-line import/prefer-default-export
export const appInitialized = () => ({
type: ActionTypes.APP_INITIALIZED,
payload: {},
});

157
client/src/actions/board.js Normal file
View file

@ -0,0 +1,157 @@
import ActionTypes from '../constants/ActionTypes';
/* Actions */
export const createBoard = (board) => ({
type: ActionTypes.BOARD_CREATE,
payload: {
board,
},
});
export const updateBoard = (id, data) => ({
type: ActionTypes.BOARD_UPDATE,
payload: {
id,
data,
},
});
export const deleteBoard = (id) => ({
type: ActionTypes.BOARD_DELETE,
payload: {
id,
},
});
/* Events */
export const createBoardRequested = (localId, data) => ({
type: ActionTypes.BOARD_CREATE_REQUESTED,
payload: {
localId,
data,
},
});
export const createBoardSucceeded = (localId, board, lists, labels) => ({
type: ActionTypes.BOARD_CREATE_SUCCEEDED,
payload: {
localId,
board,
lists,
labels,
},
});
export const createBoardFailed = (localId, error) => ({
type: ActionTypes.BOARD_CREATE_FAILED,
payload: {
localId,
error,
},
});
export const createBoardReceived = (board, lists, labels) => ({
type: ActionTypes.BOARD_CREATE_RECEIVED,
payload: {
board,
lists,
labels,
},
});
export const fetchBoardRequested = (id) => ({
type: ActionTypes.BOARD_FETCH_REQUESTED,
payload: {
id,
},
});
export const fetchBoardSucceeded = (
board,
lists,
labels,
cards,
cardMemberships,
cardLabels,
tasks,
) => ({
type: ActionTypes.BOARD_FETCH_SUCCEEDED,
payload: {
board,
lists,
labels,
cards,
cardMemberships,
cardLabels,
tasks,
},
});
export const fetchBoardFailed = (id, error) => ({
type: ActionTypes.BOARD_FETCH_FAILED,
payload: {
id,
error,
},
});
export const updateBoardRequested = (id, data) => ({
type: ActionTypes.BOARD_UPDATE_REQUESTED,
payload: {
id,
data,
},
});
export const updateBoardSucceeded = (board) => ({
type: ActionTypes.BOARD_UPDATE_SUCCEEDED,
payload: {
board,
},
});
export const updateBoardFailed = (id, error) => ({
type: ActionTypes.BOARD_UPDATE_FAILED,
payload: {
id,
error,
},
});
export const updateBoardReceived = (board) => ({
type: ActionTypes.BOARD_UPDATE_RECEIVED,
payload: {
board,
},
});
export const deleteBoardRequested = (id) => ({
type: ActionTypes.BOARD_DELETE_REQUESTED,
payload: {
id,
},
});
export const deleteBoardSucceeded = (board) => ({
type: ActionTypes.BOARD_DELETE_SUCCEEDED,
payload: {
board,
},
});
export const deleteBoardFailed = (id, error) => ({
type: ActionTypes.BOARD_DELETE_FAILED,
payload: {
id,
error,
},
});
export const deleteBoardReceived = (board) => ({
type: ActionTypes.BOARD_DELETE_RECEIVED,
payload: {
board,
},
});

View file

@ -0,0 +1,62 @@
import ActionTypes from '../constants/ActionTypes';
/* Events */
export const createCardLabelRequested = (data) => ({
type: ActionTypes.CARD_LABEL_CREATE_REQUESTED,
payload: {
data,
},
});
export const createCardLabelSucceeded = (cardLabel) => ({
type: ActionTypes.CARD_LABEL_CREATE_SUCCEEDED,
payload: {
cardLabel,
},
});
export const createCardLabelFailed = (error) => ({
type: ActionTypes.CARD_LABEL_CREATE_FAILED,
payload: {
error,
},
});
export const createCardLabelReceived = (cardLabel) => ({
type: ActionTypes.CARD_LABEL_CREATE_RECEIVED,
payload: {
cardLabel,
},
});
export const deleteCardLabelRequested = (cardId, labelId) => ({
type: ActionTypes.CARD_LABEL_DELETE_REQUESTED,
payload: {
cardId,
labelId,
},
});
export const deleteCardLabelSucceeded = (cardLabel) => ({
type: ActionTypes.CARD_LABEL_DELETE_SUCCEEDED,
payload: {
cardLabel,
},
});
export const deleteCardLabelFailed = (cardId, labelId, error) => ({
type: ActionTypes.CARD_LABEL_DELETE_FAILED,
payload: {
cardId,
labelId,
error,
},
});
export const deleteCardLabelReceived = (cardLabel) => ({
type: ActionTypes.CARD_LABEL_DELETE_RECEIVED,
payload: {
cardLabel,
},
});

View file

@ -0,0 +1,62 @@
import ActionTypes from '../constants/ActionTypes';
/* Events */
export const createCardMembershipRequested = (data) => ({
type: ActionTypes.CARD_MEMBERSHIP_CREATE_REQUESTED,
payload: {
data,
},
});
export const createCardMembershipSucceeded = (cardMembership) => ({
type: ActionTypes.CARD_MEMBERSHIP_CREATE_SUCCEEDED,
payload: {
cardMembership,
},
});
export const createCardMembershipFailed = (error) => ({
type: ActionTypes.CARD_MEMBERSHIP_CREATE_FAILED,
payload: {
error,
},
});
export const createCardMembershipReceived = (cardMembership) => ({
type: ActionTypes.CARD_MEMBERSHIP_CREATE_RECEIVED,
payload: {
cardMembership,
},
});
export const deleteCardMembershipRequested = (cardId, userId) => ({
type: ActionTypes.CARD_MEMBERSHIP_DELETE_REQUESTED,
payload: {
cardId,
userId,
},
});
export const deleteCardMembershipSucceeded = (cardMembership) => ({
type: ActionTypes.CARD_MEMBERSHIP_DELETE_SUCCEEDED,
payload: {
cardMembership,
},
});
export const deleteCardMembershipFailed = (cardId, userId, error) => ({
type: ActionTypes.CARD_MEMBERSHIP_DELETE_FAILED,
payload: {
cardId,
userId,
error,
},
});
export const deleteCardMembershipReceived = (cardMembership) => ({
type: ActionTypes.CARD_MEMBERSHIP_DELETE_RECEIVED,
payload: {
cardMembership,
},
});

139
client/src/actions/card.js Normal file
View file

@ -0,0 +1,139 @@
import ActionTypes from '../constants/ActionTypes';
/* Actions */
export const createCard = (card) => ({
type: ActionTypes.CARD_CREATE,
payload: {
card,
},
});
export const updateCard = (id, data) => ({
type: ActionTypes.CARD_UPDATE,
payload: {
id,
data,
},
});
export const deleteCard = (id) => ({
type: ActionTypes.CARD_DELETE,
payload: {
id,
},
});
/* Events */
export const createCardRequested = (localId, data) => ({
type: ActionTypes.CARD_CREATE_REQUESTED,
payload: {
localId,
data,
},
});
export const createCardSucceeded = (localId, card) => ({
type: ActionTypes.CARD_CREATE_SUCCEEDED,
payload: {
localId,
card,
},
});
export const createCardFailed = (localId, error) => ({
type: ActionTypes.CARD_CREATE_FAILED,
payload: {
localId,
error,
},
});
export const createCardReceived = (card) => ({
type: ActionTypes.CARD_CREATE_RECEIVED,
payload: {
card,
},
});
export const fetchCardRequested = (id) => ({
type: ActionTypes.CARD_FETCH_REQUESTED,
payload: {
id,
},
});
export const fetchCardSucceeded = (card) => ({
type: ActionTypes.CARD_FETCH_SUCCEEDED,
payload: {
card,
},
});
export const fetchCardFailed = (id, error) => ({
type: ActionTypes.CARD_FETCH_FAILED,
payload: {
id,
error,
},
});
export const updateCardRequested = (id, data) => ({
type: ActionTypes.CARD_UPDATE_REQUESTED,
payload: {
id,
data,
},
});
export const updateCardSucceeded = (card) => ({
type: ActionTypes.CARD_UPDATE_SUCCEEDED,
payload: {
card,
},
});
export const updateCardFailed = (id, error) => ({
type: ActionTypes.CARD_UPDATE_FAILED,
payload: {
id,
error,
},
});
export const updateCardReceived = (card) => ({
type: ActionTypes.CARD_UPDATE_RECEIVED,
payload: {
card,
},
});
export const deleteCardRequested = (id) => ({
type: ActionTypes.CARD_DELETE_REQUESTED,
payload: {
id,
},
});
export const deleteCardSucceeded = (card) => ({
type: ActionTypes.CARD_DELETE_SUCCEEDED,
payload: {
card,
},
});
export const deleteCardFailed = (id, error) => ({
type: ActionTypes.CARD_DELETE_FAILED,
payload: {
id,
error,
},
});
export const deleteCardReceived = (card) => ({
type: ActionTypes.CARD_DELETE_RECEIVED,
payload: {
card,
},
});

View file

@ -0,0 +1,96 @@
import ActionTypes from '../constants/ActionTypes';
/* Actions */
export const createCommentAction = (action) => ({
type: ActionTypes.COMMENT_ACTION_CREATE,
payload: {
action,
},
});
export const updateCommentAction = (id, data) => ({
type: ActionTypes.COMMENT_ACTION_UPDATE,
payload: {
id,
data,
},
});
export const deleteCommentAction = (id) => ({
type: ActionTypes.COMMENT_ACTION_DELETE,
payload: {
id,
},
});
/* Events */
export const createCommentActionRequested = (localId, data) => ({
type: ActionTypes.COMMENT_ACTION_CREATE_REQUESTED,
payload: {
localId,
data,
},
});
export const createCommentActionSucceeded = (localId, action) => ({
type: ActionTypes.COMMENT_ACTION_CREATE_SUCCEEDED,
payload: {
localId,
action,
},
});
export const createCommentActionFailed = (localId, error) => ({
type: ActionTypes.COMMENT_ACTION_CREATE_FAILED,
payload: {
localId,
error,
},
});
export const updateCommentActionRequested = (id, data) => ({
type: ActionTypes.COMMENT_ACTION_UPDATE_REQUESTED,
payload: {
id,
data,
},
});
export const updateCommentActionSucceeded = (action) => ({
type: ActionTypes.COMMENT_ACTION_UPDATE_SUCCEEDED,
payload: {
action,
},
});
export const updateCommentActionFailed = (id, error) => ({
type: ActionTypes.COMMENT_ACTION_UPDATE_FAILED,
payload: {
id,
error,
},
});
export const deleteCommentActionRequested = (id) => ({
type: ActionTypes.COMMENT_ACTION_DELETE_REQUESTED,
payload: {
id,
},
});
export const deleteCommentActionSucceeded = (action) => ({
type: ActionTypes.COMMENT_ACTION_DELETE_SUCCEEDED,
payload: {
action,
},
});
export const deleteCommentActionFailed = (id, error) => ({
type: ActionTypes.COMMENT_ACTION_DELETE_FAILED,
payload: {
id,
error,
},
});

View file

@ -0,0 +1,7 @@
import EntryActionTypes from '../../constants/EntryActionTypes';
// eslint-disable-next-line import/prefer-default-export
export const fetchActionsInCurrentCard = () => ({
type: EntryActionTypes.ACTIONS_IN_CURRENT_CARD_FETCH,
payload: {},
});

View file

@ -0,0 +1,31 @@
import EntryActionTypes from '../../constants/EntryActionTypes';
export const createBoardInCurrentProject = (data) => ({
type: EntryActionTypes.BOARD_IN_CURRENT_PROJECT_CREATE,
payload: {
data,
},
});
export const updateBoard = (id, data) => ({
type: EntryActionTypes.BOARD_UPDATE,
payload: {
id,
data,
},
});
export const moveBoard = (id, index) => ({
type: EntryActionTypes.BOARD_MOVE,
payload: {
id,
index,
},
});
export const deleteBoard = (id) => ({
type: EntryActionTypes.BOARD_DELETE,
payload: {
id,
},
});

View file

@ -0,0 +1,45 @@
import EntryActionTypes from '../../constants/EntryActionTypes';
export const createCard = (listId, data) => ({
type: EntryActionTypes.CARD_CREATE,
payload: {
listId,
data,
},
});
export const updateCard = (id, data) => ({
type: EntryActionTypes.CARD_UPDATE,
payload: {
id,
data,
},
});
export const updateCurrentCard = (data) => ({
type: EntryActionTypes.CURRENT_CARD_UPDATE,
payload: {
data,
},
});
export const moveCard = (id, listId, index) => ({
type: EntryActionTypes.CARD_MOVE,
payload: {
id,
listId,
index,
},
});
export const deleteCard = (id) => ({
type: EntryActionTypes.CARD_DELETE,
payload: {
id,
},
});
export const deleteCurrentCard = () => ({
type: EntryActionTypes.CURRENT_CARD_DELETE,
payload: {},
});

View file

@ -0,0 +1,23 @@
import EntryActionTypes from '../../constants/EntryActionTypes';
export const createCommentActionInCurrentCard = (data) => ({
type: EntryActionTypes.COMMENT_ACTION_IN_CURRENT_CARD_CREATE,
payload: {
data,
},
});
export const updateCommentAction = (id, data) => ({
type: EntryActionTypes.COMMENT_ACTION_UPDATE,
payload: {
id,
data,
},
});
export const deleteCommentAction = (id) => ({
type: EntryActionTypes.COMMENT_ACTION_DELETE,
payload: {
id,
},
});

View file

@ -0,0 +1,13 @@
export * from './login';
export * from './modal';
export * from './user';
export * from './project';
export * from './project-membership';
export * from './board';
export * from './list';
export * from './label';
export * from './card';
export * from './task';
export * from './actions';
export * from './comment-action';
export * from './notification';

View file

@ -0,0 +1,67 @@
import EntryActionTypes from '../../constants/EntryActionTypes';
export const createLabelInCurrentBoard = (data) => ({
type: EntryActionTypes.LABEL_IN_CURRENT_BOARD_CREATE,
payload: {
data,
},
});
export const updateLabel = (id, data) => ({
type: EntryActionTypes.LABEL_UPDATE,
payload: {
id,
data,
},
});
export const deleteLabel = (id) => ({
type: EntryActionTypes.LABEL_DELETE,
payload: {
id,
},
});
export const addLabelToCard = (id, cardId) => ({
type: EntryActionTypes.LABEL_TO_CARD_ADD,
payload: {
id,
cardId,
},
});
export const addLabelToCurrentCard = (id) => ({
type: EntryActionTypes.LABEL_TO_CURRENT_CARD_ADD,
payload: {
id,
},
});
export const removeLabelFromCard = (id, cardId) => ({
type: EntryActionTypes.LABEL_FROM_CARD_REMOVE,
payload: {
id,
cardId,
},
});
export const removeLabelFromCurrentCard = (id) => ({
type: EntryActionTypes.LABEL_FROM_CURRENT_CARD_REMOVE,
payload: {
id,
},
});
export const addLabelToFilterInCurrentBoard = (id) => ({
type: EntryActionTypes.LABEL_TO_FILTER_IN_CURRENT_BOARD_ADD,
payload: {
id,
},
});
export const removeLabelFromFilterInCurrentBoard = (id) => ({
type: EntryActionTypes.LABEL_FROM_FILTER_IN_CURRENT_BOARD_REMOVE,
payload: {
id,
},
});

View file

@ -0,0 +1,31 @@
import EntryActionTypes from '../../constants/EntryActionTypes';
export const createListInCurrentBoard = (data) => ({
type: EntryActionTypes.LIST_IN_CURRENT_BOARD_CREATE,
payload: {
data,
},
});
export const updateList = (id, data) => ({
type: EntryActionTypes.LIST_UPDATE,
payload: {
id,
data,
},
});
export const moveList = (id, index) => ({
type: EntryActionTypes.LIST_MOVE,
payload: {
id,
index,
},
});
export const deleteList = (id) => ({
type: EntryActionTypes.LIST_DELETE,
payload: {
id,
},
});

View file

@ -0,0 +1,18 @@
import EntryActionTypes from '../../constants/EntryActionTypes';
export const authenticate = (data) => ({
type: EntryActionTypes.AUTHENTICATE,
payload: {
data,
},
});
export const clearAuthenticationError = () => ({
type: EntryActionTypes.AUTHENTICATION_ERROR_CLEAR,
payload: {},
});
export const logout = () => ({
type: EntryActionTypes.LOGOUT,
payload: {},
});

View file

@ -0,0 +1,21 @@
import EntryActionTypes from '../../constants/EntryActionTypes';
import ModalTypes from '../../constants/ModalTypes';
export const openUsersModal = () => ({
type: EntryActionTypes.MODAL_OPEN,
payload: {
type: ModalTypes.USERS,
},
});
export const openAddProjectModal = () => ({
type: EntryActionTypes.MODAL_OPEN,
payload: {
type: ModalTypes.ADD_PROJECT,
},
});
export const closeModal = () => ({
type: EntryActionTypes.MODAL_CLOSE,
payload: {},
});

View file

@ -0,0 +1,9 @@
import EntryActionTypes from '../../constants/EntryActionTypes';
// eslint-disable-next-line import/prefer-default-export
export const deleteNotification = (id) => ({
type: EntryActionTypes.NOTIFICATION_DELETE,
payload: {
id,
},
});

View file

@ -0,0 +1,15 @@
import EntryActionTypes from '../../constants/EntryActionTypes';
export const createMembershipInCurrentProject = (data) => ({
type: EntryActionTypes.MEMBERSHIP_IN_CURRENT_PROJECT_CREATE,
payload: {
data,
},
});
export const deleteProjectMembership = (id) => ({
type: EntryActionTypes.PROJECT_MEMBERSHIP_DELETE,
payload: {
id,
},
});

View file

@ -0,0 +1,20 @@
import EntryActionTypes from '../../constants/EntryActionTypes';
export const createProject = (data) => ({
type: EntryActionTypes.PROJECT_CREATE,
payload: {
data,
},
});
export const updateCurrentProject = (data) => ({
type: EntryActionTypes.CURRENT_PROJECT_UPDATE,
payload: {
data,
},
});
export const deleteCurrentProject = () => ({
type: EntryActionTypes.CURRENT_PROJECT_DELETE,
payload: {},
});

View file

@ -0,0 +1,23 @@
import EntryActionTypes from '../../constants/EntryActionTypes';
export const createTaskInCurrentCard = (data) => ({
type: EntryActionTypes.TASK_IN_CURRENT_CARD_CREATE,
payload: {
data,
},
});
export const updateTask = (id, data) => ({
type: EntryActionTypes.TASK_UPDATE,
payload: {
id,
data,
},
});
export const deleteTask = (id) => ({
type: EntryActionTypes.TASK_DELETE,
payload: {
id,
},
});

View file

@ -0,0 +1,86 @@
import EntryActionTypes from '../../constants/EntryActionTypes';
export const createUser = (data) => ({
type: EntryActionTypes.USER_CREATE,
payload: {
data,
},
});
export const clearUserCreationError = () => ({
type: EntryActionTypes.USER_CREATION_ERROR_CLEAR,
payload: {},
});
export const updateUser = (id, data) => ({
type: EntryActionTypes.USER_UPDATE,
payload: {
id,
data,
},
});
export const updateCurrentUser = (data) => ({
type: EntryActionTypes.CURRENT_USER_UPDATE,
payload: {
data,
},
});
export const uploadCurrentUserAvatar = (file) => ({
type: EntryActionTypes.CURRENT_USER_AVATAR_UPLOAD,
payload: {
file,
},
});
export const deleteUser = (id) => ({
type: EntryActionTypes.USER_DELETE,
payload: {
id,
},
});
export const addUserToCard = (id, cardId) => ({
type: EntryActionTypes.USER_TO_CARD_ADD,
payload: {
id,
cardId,
},
});
export const addUserToCurrentCard = (id) => ({
type: EntryActionTypes.USER_TO_CURRENT_CARD_ADD,
payload: {
id,
},
});
export const removeUserFromCard = (id, cardId) => ({
type: EntryActionTypes.USER_FROM_CARD_REMOVE,
payload: {
id,
cardId,
},
});
export const removeUserFromCurrentCard = (id) => ({
type: EntryActionTypes.USER_FROM_CURRENT_CARD_REMOVE,
payload: {
id,
},
});
export const addUserToFilterInCurrentBoard = (id) => ({
type: EntryActionTypes.USER_TO_FILTER_IN_CURRENT_BOARD_ADD,
payload: {
id,
},
});
export const removeUserFromFilterInCurrentBoard = (id) => ({
type: EntryActionTypes.USER_FROM_FILTER_IN_CURRENT_BOARD_REMOVE,
payload: {
id,
},
});

View file

@ -0,0 +1,21 @@
export * from './socket';
export * from './login';
export * from './app';
export * from './modal';
export * from './users';
export * from './user';
export * from './projects';
export * from './project';
export * from './project-membership';
export * from './board';
export * from './list';
export * from './label';
export * from './card';
export * from './card-membership';
export * from './card-label';
export * from './task';
export * from './actions';
export * from './action';
export * from './comment-action';
export * from './notifications';
export * from './notification';

149
client/src/actions/label.js Normal file
View file

@ -0,0 +1,149 @@
import ActionTypes from '../constants/ActionTypes';
/* Actions */
export const createLabel = (label) => ({
type: ActionTypes.LABEL_CREATE,
payload: {
label,
},
});
export const updateLabel = (id, data) => ({
type: ActionTypes.LABEL_UPDATE,
payload: {
id,
data,
},
});
export const deleteLabel = (id) => ({
type: ActionTypes.LABEL_DELETE,
payload: {
id,
},
});
export const addLabelToCard = (id, cardId) => ({
type: ActionTypes.LABEL_TO_CARD_ADD,
payload: {
id,
cardId,
},
});
export const removeLabelFromCard = (id, cardId) => ({
type: ActionTypes.LABEL_FROM_CARD_REMOVE,
payload: {
id,
cardId,
},
});
export const addLabelToBoardFilter = (id, boardId) => ({
type: ActionTypes.LABEL_TO_BOARD_FILTER_ADD,
payload: {
id,
boardId,
},
});
export const removeLabelFromBoardFilter = (id, boardId) => ({
type: ActionTypes.LABEL_FROM_BOARD_FILTER_REMOVE,
payload: {
id,
boardId,
},
});
/* Events */
export const createLabelRequested = (localId, data) => ({
type: ActionTypes.LABEL_CREATE_REQUESTED,
payload: {
localId,
data,
},
});
export const createLabelSucceeded = (localId, label) => ({
type: ActionTypes.LABEL_CREATE_SUCCEEDED,
payload: {
localId,
label,
},
});
export const createLabelFailed = (localId, error) => ({
type: ActionTypes.LABEL_CREATE_FAILED,
payload: {
localId,
error,
},
});
export const createLabelReceived = (label) => ({
type: ActionTypes.LABEL_CREATE_RECEIVED,
payload: {
label,
},
});
export const updateLabelRequested = (id, data) => ({
type: ActionTypes.LABEL_UPDATE_REQUESTED,
payload: {
id,
data,
},
});
export const updateLabelSucceeded = (label) => ({
type: ActionTypes.LABEL_UPDATE_SUCCEEDED,
payload: {
label,
},
});
export const updateLabelFailed = (id, error) => ({
type: ActionTypes.LABEL_UPDATE_FAILED,
payload: {
id,
error,
},
});
export const updateLabelReceived = (label) => ({
type: ActionTypes.LABEL_UPDATE_RECEIVED,
payload: {
label,
},
});
export const deleteLabelRequested = (id) => ({
type: ActionTypes.LABEL_DELETE_REQUESTED,
payload: {
id,
},
});
export const deleteLabelSucceeded = (label) => ({
type: ActionTypes.LABEL_DELETE_SUCCEEDED,
payload: {
label,
},
});
export const deleteLabelFailed = (id, error) => ({
type: ActionTypes.LABEL_DELETE_FAILED,
payload: {
id,
error,
},
});
export const deleteLabelReceived = (label) => ({
type: ActionTypes.LABEL_DELETE_RECEIVED,
payload: {
label,
},
});

117
client/src/actions/list.js Normal file
View file

@ -0,0 +1,117 @@
import ActionTypes from '../constants/ActionTypes';
/* Actions */
export const createList = (list) => ({
type: ActionTypes.LIST_CREATE,
payload: {
list,
},
});
export const updateList = (id, data) => ({
type: ActionTypes.LIST_UPDATE,
payload: {
id,
data,
},
});
export const deleteList = (id) => ({
type: ActionTypes.LIST_DELETE,
payload: {
id,
},
});
/* Events */
export const createListRequested = (localId, data) => ({
type: ActionTypes.LIST_CREATE_REQUESTED,
payload: {
localId,
data,
},
});
export const createListSucceeded = (localId, list) => ({
type: ActionTypes.LIST_CREATE_SUCCEEDED,
payload: {
localId,
list,
},
});
export const createListFailed = (localId, error) => ({
type: ActionTypes.LIST_CREATE_FAILED,
payload: {
localId,
error,
},
});
export const createListReceived = (list) => ({
type: ActionTypes.LIST_CREATE_RECEIVED,
payload: {
list,
},
});
export const updateListRequested = (id, data) => ({
type: ActionTypes.LIST_UPDATE_REQUESTED,
payload: {
id,
data,
},
});
export const updateListSucceeded = (list) => ({
type: ActionTypes.LIST_UPDATE_SUCCEEDED,
payload: {
list,
},
});
export const updateListFailed = (id, error) => ({
type: ActionTypes.LIST_UPDATE_FAILED,
payload: {
id,
error,
},
});
export const updateListReceived = (list) => ({
type: ActionTypes.LIST_UPDATE_RECEIVED,
payload: {
list,
},
});
export const deleteListRequested = (id) => ({
type: ActionTypes.LIST_DELETE_REQUESTED,
payload: {
id,
},
});
export const deleteListSucceeded = (list) => ({
type: ActionTypes.LIST_DELETE_SUCCEEDED,
payload: {
list,
},
});
export const deleteListFailed = (id, error) => ({
type: ActionTypes.LIST_DELETE_FAILED,
payload: {
id,
error,
},
});
export const deleteListReceived = (list) => ({
type: ActionTypes.LIST_DELETE_RECEIVED,
payload: {
list,
},
});

View file

@ -0,0 +1,43 @@
import ActionTypes from '../constants/ActionTypes';
/* Actions */
export const authenticate = (data) => ({
type: ActionTypes.AUTHENTICATE,
payload: {
data,
},
});
export const clearAuthenticationError = () => ({
type: ActionTypes.AUTHENTICATION_ERROR_CLEAR,
payload: {},
});
export const logout = () => ({
type: ActionTypes.LOGOUT,
payload: {},
});
/* Events */
export const authenticateRequested = (data) => ({
type: ActionTypes.AUTHENTICATE_REQUESTED,
payload: {
data,
},
});
export const authenticateSucceeded = (accessToken) => ({
type: ActionTypes.AUTHENTICATE_SUCCEEDED,
payload: {
accessToken,
},
});
export const authenticateFailed = (error) => ({
type: ActionTypes.AUTHENTICATE_FAILED,
payload: {
error,
},
});

View file

@ -0,0 +1,15 @@
import ActionTypes from '../constants/ActionTypes';
/* Actions */
export const openModal = (type) => ({
type: ActionTypes.MODAL_OPEN,
payload: {
type,
},
});
export const closeModal = () => ({
type: ActionTypes.MODAL_CLOSE,
payload: {},
});

View file

@ -0,0 +1,20 @@
import ActionTypes from '../constants/ActionTypes';
/* Events */
export const createNotificationReceived = (notification, user, card, action) => ({
type: ActionTypes.NOTIFICATION_CREATE_RECEIVED,
payload: {
notification,
user,
card,
action,
},
});
export const deleteNotificationReceived = (notification) => ({
type: ActionTypes.NOTIFICATION_DELETE_RECEIVED,
payload: {
notification,
},
});

View file

@ -0,0 +1,56 @@
import ActionTypes from '../constants/ActionTypes';
/* Actions */
export const deleteNotifications = (ids) => ({
type: ActionTypes.NOTIFICATIONS_DELETE,
payload: {
ids,
},
});
/* Events */
export const fetchNotificationsRequested = () => ({
type: ActionTypes.NOTIFICATIONS_FETCH_REQUESTED,
payload: {},
});
export const fetchNotificationsSucceeded = (notifications, users, cards, actions) => ({
type: ActionTypes.NOTIFICATIONS_FETCH_SUCCEEDED,
payload: {
notifications,
users,
cards,
actions,
},
});
export const fetchNotificationsFailed = (error) => ({
type: ActionTypes.NOTIFICATIONS_FETCH_FAILED,
payload: {
error,
},
});
export const deleteNotificationsRequested = (ids) => ({
type: ActionTypes.NOTIFICATIONS_DELETE_REQUESTED,
payload: {
ids,
},
});
export const deleteNotificationsSucceeded = (notifications) => ({
type: ActionTypes.NOTIFICATIONS_DELETE_SUCCEEDED,
payload: {
notifications,
},
});
export const deleteNotificationsFailed = (ids, error) => ({
type: ActionTypes.NOTIFICATIONS_DELETE_FAILED,
payload: {
ids,
error,
},
});

View file

@ -0,0 +1,80 @@
import ActionTypes from '../constants/ActionTypes';
/* Actions */
export const createProjectMembership = (projectMembership) => ({
type: ActionTypes.PROJECT_MEMBERSHIP_CREATE,
payload: {
projectMembership,
},
});
export const deleteProjectMembership = (id) => ({
type: ActionTypes.PROJECT_MEMBERSHIP_DELETE,
payload: {
id,
},
});
/* Events */
export const createProjectMembershipRequested = (localId, data) => ({
type: ActionTypes.PROJECT_MEMBERSHIP_CREATE_REQUESTED,
payload: {
localId,
data,
},
});
export const createProjectMembershipSucceeded = (localId, projectMembership) => ({
type: ActionTypes.PROJECT_MEMBERSHIP_CREATE_SUCCEEDED,
payload: {
localId,
projectMembership,
},
});
export const createProjectMembershipFailed = (localId, error) => ({
type: ActionTypes.PROJECT_MEMBERSHIP_CREATE_FAILED,
payload: {
localId,
error,
},
});
export const createProjectMembershipReceived = (projectMembership, user) => ({
type: ActionTypes.PROJECT_MEMBERSHIP_CREATE_RECEIVED,
payload: {
projectMembership,
user,
},
});
export const deleteProjectMembershipRequested = (id) => ({
type: ActionTypes.PROJECT_MEMBERSHIP_DELETE_REQUESTED,
payload: {
id,
},
});
export const deleteProjectMembershipSucceeded = (projectMembership) => ({
type: ActionTypes.PROJECT_MEMBERSHIP_DELETE_SUCCEEDED,
payload: {
projectMembership,
},
});
export const deleteProjectMembershipFailed = (id, error) => ({
type: ActionTypes.PROJECT_MEMBERSHIP_DELETE_FAILED,
payload: {
id,
error,
},
});
export const deleteProjectMembershipReceived = (projectMembership) => ({
type: ActionTypes.PROJECT_MEMBERSHIP_DELETE_RECEIVED,
payload: {
projectMembership,
},
});

View file

@ -0,0 +1,120 @@
import ActionTypes from '../constants/ActionTypes';
/* Actions */
export const createProject = (data) => ({
type: ActionTypes.PROJECT_CREATE,
payload: {
data,
},
});
export const updateProject = (id, data) => ({
type: ActionTypes.PROJECT_UPDATE,
payload: {
id,
data,
},
});
export const deleteProject = (id) => ({
type: ActionTypes.PROJECT_DELETE,
payload: {
id,
},
});
/* Events */
export const createProjectRequested = (data) => ({
type: ActionTypes.PROJECT_CREATE_REQUESTED,
payload: {
data,
},
});
export const createProjectSucceeded = (project, users, projectMemberships, boards) => ({
type: ActionTypes.PROJECT_CREATE_SUCCEEDED,
payload: {
project,
users,
projectMemberships,
boards,
},
});
export const createProjectFailed = (error) => ({
type: ActionTypes.PROJECT_CREATE_FAILED,
payload: {
error,
},
});
export const createProjectReceived = (project, users, projectMemberships, boards) => ({
type: ActionTypes.PROJECT_CREATE_RECEIVED,
payload: {
project,
users,
projectMemberships,
boards,
},
});
export const updateProjectRequested = (id, data) => ({
type: ActionTypes.PROJECT_UPDATE_REQUESTED,
payload: {
id,
data,
},
});
export const updateProjectSucceeded = (project) => ({
type: ActionTypes.PROJECT_UPDATE_SUCCEEDED,
payload: {
project,
},
});
export const updateProjectFailed = (id, error) => ({
type: ActionTypes.PROJECT_UPDATE_FAILED,
payload: {
id,
error,
},
});
export const updateProjectReceived = (project) => ({
type: ActionTypes.PROJECT_UPDATE_RECEIVED,
payload: {
project,
},
});
export const deleteProjectRequested = (id) => ({
type: ActionTypes.PROJECT_DELETE_REQUESTED,
payload: {
id,
},
});
export const deleteProjectSucceeded = (project) => ({
type: ActionTypes.PROJECT_DELETE_SUCCEEDED,
payload: {
project,
},
});
export const deleteProjectFailed = (id, error) => ({
type: ActionTypes.PROJECT_DELETE_FAILED,
payload: {
id,
error,
},
});
export const deleteProjectReceived = (project) => ({
type: ActionTypes.PROJECT_DELETE_RECEIVED,
payload: {
project,
},
});

View file

@ -0,0 +1,25 @@
import ActionTypes from '../constants/ActionTypes';
/* Events */
export const fetchProjectsRequested = () => ({
type: ActionTypes.PROJECTS_FETCH_REQUESTED,
payload: {},
});
export const fetchProjectsSucceeded = (projects, users, projectMemberships, boards) => ({
type: ActionTypes.PROJECTS_FETCH_SUCCEEDED,
payload: {
projects,
users,
projectMemberships,
boards,
},
});
export const fetchProjectsFailed = (error) => ({
type: ActionTypes.PROJECTS_FETCH_FAILED,
payload: {
error,
},
});

View file

@ -0,0 +1,18 @@
import ActionTypes from '../constants/ActionTypes';
import SocketStatuses from '../constants/SocketStatuses';
/* Events */
export const socketDisconnected = () => ({
type: ActionTypes.SOCKET_STATUS_CHANGED,
payload: {
status: SocketStatuses.DISCONNECTED,
},
});
export const socketReconnected = () => ({
type: ActionTypes.SOCKET_STATUS_CHANGED,
payload: {
status: SocketStatuses.RECONNECTED,
},
});

117
client/src/actions/task.js Normal file
View file

@ -0,0 +1,117 @@
import ActionTypes from '../constants/ActionTypes';
/* Actions */
export const createTask = (task) => ({
type: ActionTypes.TASK_CREATE,
payload: {
task,
},
});
export const updateTask = (id, data) => ({
type: ActionTypes.TASK_UPDATE,
payload: {
id,
data,
},
});
export const deleteTask = (id) => ({
type: ActionTypes.TASK_DELETE,
payload: {
id,
},
});
/* Events */
export const createTaskRequested = (localId, data) => ({
type: ActionTypes.TASK_CREATE_REQUESTED,
payload: {
localId,
data,
},
});
export const createTaskSucceeded = (localId, task) => ({
type: ActionTypes.TASK_CREATE_SUCCEEDED,
payload: {
localId,
task,
},
});
export const createTaskFailed = (localId, error) => ({
type: ActionTypes.TASK_CREATE_FAILED,
payload: {
localId,
error,
},
});
export const createTaskReceived = (task) => ({
type: ActionTypes.TASK_CREATE_RECEIVED,
payload: {
task,
},
});
export const updateTaskRequested = (id, data) => ({
type: ActionTypes.TASK_UPDATE_REQUESTED,
payload: {
id,
data,
},
});
export const updateTaskSucceeded = (task) => ({
type: ActionTypes.TASK_UPDATE_SUCCEEDED,
payload: {
task,
},
});
export const updateTaskFailed = (id, error) => ({
type: ActionTypes.TASK_UPDATE_FAILED,
payload: {
id,
error,
},
});
export const updateTaskReceived = (task) => ({
type: ActionTypes.TASK_UPDATE_RECEIVED,
payload: {
task,
},
});
export const deleteTaskRequested = (id) => ({
type: ActionTypes.TASK_DELETE_REQUESTED,
payload: {
id,
},
});
export const deleteTaskSucceeded = (task) => ({
type: ActionTypes.TASK_DELETE_SUCCEEDED,
payload: {
task,
},
});
export const deleteTaskFailed = (id, error) => ({
type: ActionTypes.TASK_DELETE_FAILED,
payload: {
id,
error,
},
});
export const deleteTaskReceived = (task) => ({
type: ActionTypes.TASK_DELETE_RECEIVED,
payload: {
task,
},
});

193
client/src/actions/user.js Normal file
View file

@ -0,0 +1,193 @@
import ActionTypes from '../constants/ActionTypes';
/* Actions */
export const createUser = (data) => ({
type: ActionTypes.USER_CREATE,
payload: {
data,
},
});
export const clearUserCreationError = () => ({
type: ActionTypes.USER_CREATION_ERROR_CLEAR,
payload: {},
});
export const updateUser = (id, data) => ({
type: ActionTypes.USER_UPDATE,
payload: {
id,
data,
},
});
export const deleteUser = (id) => ({
type: ActionTypes.USER_DELETE,
payload: {
id,
},
});
export const addUserToCard = (id, cardId, isCurrent) => ({
type: ActionTypes.USER_TO_CARD_ADD,
payload: {
id,
cardId,
isCurrent,
},
});
export const removeUserFromCard = (id, cardId) => ({
type: ActionTypes.USER_FROM_CARD_REMOVE,
payload: {
id,
cardId,
},
});
export const addUserToBoardFilter = (id, boardId) => ({
type: ActionTypes.USER_TO_BOARD_FILTER_ADD,
payload: {
id,
boardId,
},
});
export const removeUserFromBoardFilter = (id, boardId) => ({
type: ActionTypes.USER_FROM_BOARD_FILTER_REMOVE,
payload: {
id,
boardId,
},
});
/* Events */
export const createUserRequested = (data) => ({
type: ActionTypes.USER_CREATE_REQUESTED,
payload: {
data,
},
});
export const createUserSucceeded = (user) => ({
type: ActionTypes.USER_CREATE_SUCCEEDED,
payload: {
user,
},
});
export const createUserFailed = (error) => ({
type: ActionTypes.USER_CREATE_FAILED,
payload: {
error,
},
});
export const createUserReceived = (user) => ({
type: ActionTypes.USER_CREATE_RECEIVED,
payload: {
user,
},
});
export const fetchCurrentUserRequested = () => ({
type: ActionTypes.CURRENT_USER_FETCH_REQUESTED,
payload: {},
});
export const fetchCurrentUserSucceeded = (user) => ({
type: ActionTypes.CURRENT_USER_FETCH_SUCCEEDED,
payload: {
user,
},
});
export const fetchCurrentUserFailed = (error) => ({
type: ActionTypes.CURRENT_USER_FETCH_FAILED,
payload: {
error,
},
});
export const updateUserRequested = (id, data) => ({
type: ActionTypes.USER_UPDATE_REQUESTED,
payload: {
id,
data,
},
});
export const updateUserSucceeded = (user) => ({
type: ActionTypes.USER_UPDATE_SUCCEEDED,
payload: {
user,
},
});
export const updateUserFailed = (id, error) => ({
type: ActionTypes.USER_UPDATE_FAILED,
payload: {
id,
error,
},
});
export const updateUserReceived = (user) => ({
type: ActionTypes.USER_UPDATE_RECEIVED,
payload: {
user,
},
});
export const uploadUserAvatarRequested = (id) => ({
type: ActionTypes.USER_AVATAR_UPLOAD_REQUESTED,
payload: {
id,
},
});
export const uploadUserAvatarSucceeded = (user) => ({
type: ActionTypes.USER_AVATAR_UPLOAD_SUCCEEDED,
payload: {
user,
},
});
export const uploadUserAvatarFailed = (id, error) => ({
type: ActionTypes.USER_AVATAR_UPLOAD_FAILED,
payload: {
id,
error,
},
});
export const deleteUserRequested = (id) => ({
type: ActionTypes.USER_DELETE_REQUESTED,
payload: {
id,
},
});
export const deleteUserSucceeded = (user) => ({
type: ActionTypes.USER_DELETE_SUCCEEDED,
payload: {
user,
},
});
export const deleteUserFailed = (id, error) => ({
type: ActionTypes.USER_DELETE_FAILED,
payload: {
id,
error,
},
});
export const deleteUserReceived = (user) => ({
type: ActionTypes.USER_DELETE_RECEIVED,
payload: {
user,
},
});

View file

@ -0,0 +1,22 @@
import ActionTypes from '../constants/ActionTypes';
/* Events */
export const fetchUsersRequested = () => ({
type: ActionTypes.USERS_FETCH_REQUESTED,
payload: {},
});
export const fetchUsersSucceeded = (users) => ({
type: ActionTypes.USERS_FETCH_SUCCEEDED,
payload: {
users,
},
});
export const fetchUsersFailed = (error) => ({
type: ActionTypes.USERS_FETCH_FAILED,
payload: {
error,
},
});

View file

@ -0,0 +1,9 @@
import http from './http';
/* Actions */
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
export default {
createAccessToken,
};

35
client/src/api/actions.js Executable file
View file

@ -0,0 +1,35 @@
import socket from './socket';
/* Transformers */
export const transformAction = (action) => ({
...action,
createdAt: new Date(action.createdAt),
});
/* Actions */
const getActions = (cardId, data, headers) => socket.get(`/cards/${cardId}/actions`, data, headers).then((body) => ({
...body,
items: body.items.map(transformAction),
}));
/* Event handlers */
const makeHandleActionCreate = (next) => (body) => {
next({
...body,
item: transformAction(body.item),
});
};
const makeHandleActionUpdate = makeHandleActionCreate;
const makeHandleActionDelete = makeHandleActionCreate;
export default {
getActions,
makeHandleActionCreate,
makeHandleActionUpdate,
makeHandleActionDelete,
};

25
client/src/api/boards.js Executable file
View file

@ -0,0 +1,25 @@
import socket from './socket';
import { transformCard } from './cards';
/* Actions */
const createBoard = (projectId, data, headers) => socket.post(`/projects/${projectId}/boards`, data, headers);
const getBoard = (id, headers) => socket.get(`/boards/${id}`, undefined, headers).then((body) => ({
...body,
included: {
...body.included,
cards: body.included.cards.map(transformCard),
},
}));
const updateBoard = (id, data, headers) => socket.patch(`/boards/${id}`, data, headers);
const deleteBoard = (id, headers) => socket.delete(`/boards/${id}`, undefined, headers);
export default {
createBoard,
getBoard,
updateBoard,
deleteBoard,
};

View file

@ -0,0 +1,12 @@
import socket from './socket';
/* Actions */
const createCardLabel = (cardId, data, headers) => socket.post(`/cards/${cardId}/labels`, data, headers);
const deleteCardLabel = (cardId, labelId, headers) => socket.delete(`/cards/${cardId}/labels/${labelId}`, undefined, headers);
export default {
createCardLabel,
deleteCardLabel,
};

View file

@ -0,0 +1,12 @@
import socket from './socket';
/* Actions */
const createCardMembership = (cardId, data, headers) => socket.post(`/cards/${cardId}/memberships`, data, headers);
const deleteCardMembership = (cardId, userId, headers) => socket.delete(`/cards/${cardId}/memberships?userId=${userId}`, undefined, headers);
export default {
createCardMembership,
deleteCardMembership,
};

70
client/src/api/cards.js Executable file
View file

@ -0,0 +1,70 @@
import socket from './socket';
/* Transformers */
export const transformCard = (card) => ({
...card,
deadline: card.deadline && new Date(card.deadline),
timer: card.timer && {
...card.timer,
startedAt: card.timer.startedAt && new Date(card.timer.startedAt),
},
});
export const transformCardData = (data) => ({
...data,
...(data.deadline && {
deadline: data.deadline.toISOString(),
}),
...(data.timer && {
...data.timer,
...(data.timer.startedAt && {
startedAt: data.timer.startedAt.toISOString(),
}),
}),
});
/* Actions */
const createCard = (listId, data, headers) => socket.post(`/lists/${listId}/cards`, transformCardData(data), headers).then((body) => ({
...body,
item: transformCard(body.item),
}));
const getCard = (id, headers) => socket.get(`/cards/${id}`, undefined, headers).then((body) => ({
...body,
item: transformCard(body.item),
}));
const updateCard = (id, data, headers) => socket.patch(`/cards/${id}`, transformCardData(data), headers).then((body) => ({
...body,
item: transformCard(body.item),
}));
const deleteCard = (id, headers) => socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({
...body,
item: transformCard(body.item),
}));
/* Event handlers */
const makeHandleCardCreate = (next) => (body) => {
next({
...body,
item: transformCard(body.item),
});
};
const makeHandleCardUpdate = makeHandleCardCreate;
const makeHandleCardDelete = makeHandleCardCreate;
export default {
createCard,
getCard,
updateCard,
deleteCard,
makeHandleCardCreate,
makeHandleCardUpdate,
makeHandleCardDelete,
};

View file

@ -0,0 +1,25 @@
import socket from './socket';
import { transformAction } from './actions';
/* Actions */
const createCommentAction = (cardId, data, headers) => socket.post(`/cards/${cardId}/comment-actions`, data, headers).then((body) => ({
...body,
item: transformAction(body.item),
}));
const updateCommentAction = (id, data, headers) => socket.patch(`/comment-actions/${id}`, data, headers).then((body) => ({
...body,
item: transformAction(body.item),
}));
const deleteCommentAction = (id, headers) => socket.delete(`/comment-actions/${id}`, undefined, headers).then((body) => ({
...body,
item: transformAction(body.item),
}));
export default {
createCommentAction,
updateCommentAction,
deleteCommentAction,
};

35
client/src/api/http.js Executable file
View file

@ -0,0 +1,35 @@
import { fetch } from 'whatwg-fetch';
import Config from '../constants/Config';
const http = {};
// TODO: all methods
['POST'].forEach((method) => {
http[method.toLowerCase()] = (url, data, headers) => {
const formData = Object.keys(data).reduce((result, key) => {
result.append(key, data[key]);
return result;
}, new FormData());
return fetch(`${Config.API_URL}${Config.API_PATH}${url}`, {
method,
headers,
body: formData,
})
.then((response) => response.json().then((body) => ({
body,
isError: response.status !== 200,
})))
.then(({ body, isError }) => {
if (isError) {
throw body;
}
return body;
});
};
});
export default http;

35
client/src/api/index.js Executable file
View file

@ -0,0 +1,35 @@
import http from './http';
import socket from './socket';
import accessTokens from './access-tokens';
import users from './users';
import projects from './projects';
import projectMemberships from './project-memberships';
import boards from './boards';
import lists from './lists';
import labels from './labels';
import cards from './cards';
import cardMemberships from './card-memberships';
import cardLabels from './card-labels';
import tasks from './tasks';
import actions from './actions';
import commentActions from './comment-actions';
import notifications from './notifications';
export { http, socket };
export default {
...accessTokens,
...users,
...projects,
...projectMemberships,
...boards,
...lists,
...labels,
...cards,
...cardMemberships,
...cardLabels,
...tasks,
...actions,
...commentActions,
...notifications,
};

15
client/src/api/labels.js Executable file
View file

@ -0,0 +1,15 @@
import socket from './socket';
/* Actions */
const createLabel = (boardId, data, headers) => socket.post(`/boards/${boardId}/labels`, data, headers);
const updateLabel = (id, data, headers) => socket.patch(`/labels/${id}`, data, headers);
const deleteLabel = (id, headers) => socket.delete(`/labels/${id}`, undefined, headers);
export default {
createLabel,
updateLabel,
deleteLabel,
};

15
client/src/api/lists.js Executable file
View file

@ -0,0 +1,15 @@
import socket from './socket';
/* Actions */
const createList = (boardId, data, headers) => socket.post(`/boards/${boardId}/lists`, data, headers);
const updateList = (id, data, headers) => socket.patch(`/lists/${id}`, data, headers);
const deleteList = (id, headers) => socket.delete(`/lists/${id}`, undefined, headers);
export default {
createList,
updateList,
deleteList,
};

35
client/src/api/notifications.js Executable file
View file

@ -0,0 +1,35 @@
import socket from './socket';
import { transformCard } from './cards';
import { transformAction } from './actions';
/* Actions */
const getNotifications = (headers) => socket.get('/notifications', undefined, headers).then((body) => ({
...body,
included: {
...body.included,
cards: body.included.cards.map(transformCard),
actions: body.included.actions.map(transformAction),
},
}));
const updateNotifications = (ids, data, headers) => socket.patch(`/notifications/${ids.join(',')}`, data, headers);
/* Event handlers */
const makeHandleNotificationCreate = (next) => (body) => {
next({
...body,
included: {
...body.included,
cards: body.included.cards.map(transformCard),
actions: body.included.actions.map(transformAction),
},
});
};
export default {
getNotifications,
updateNotifications,
makeHandleNotificationCreate,
};

View file

@ -0,0 +1,12 @@
import socket from './socket';
/* Actions */
const createProjectMembership = (projectId, data, headers) => socket.post(`/projects/${projectId}/memberships`, data, headers);
const deleteProjectMembership = (id, headers) => socket.delete(`/project-memberships/${id}`, undefined, headers);
export default {
createProjectMembership,
deleteProjectMembership,
};

18
client/src/api/projects.js Executable file
View file

@ -0,0 +1,18 @@
import socket from './socket';
/* Actions */
const getProjects = (headers) => socket.get('/projects', undefined, headers);
const createProject = (data, headers) => socket.post('/projects', data, headers);
const updateProject = (id, data, headers) => socket.patch(`/projects/${id}`, data, headers);
const deleteProject = (id, headers) => socket.delete(`/projects/${id}`, undefined, headers);
export default {
getProjects,
createProject,
updateProject,
deleteProject,
};

37
client/src/api/socket.js Executable file
View file

@ -0,0 +1,37 @@
import socketIOClient from 'socket.io-client';
import sailsIOClient from 'sails.io.js';
import Config from '../constants/Config';
const io = sailsIOClient(socketIOClient);
io.sails.url = Config.API_URL;
io.sails.autoConnect = false;
io.sails.reconnection = true;
io.sails.useCORSRouteToGetCookie = false;
const { socket } = io;
socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => {
socket[method.toLowerCase()] = (url, data, headers) => new Promise((resolve, reject) => {
socket.request(
{
method,
data,
headers,
url: `${Config.API_PATH}${url}`,
},
(_, { body, error }) => {
if (error) {
reject(body);
} else {
resolve(body);
}
},
);
});
});
export default socket;

15
client/src/api/tasks.js Executable file
View file

@ -0,0 +1,15 @@
import socket from './socket';
/* Actions */
const createTask = (cardId, data, headers) => socket.post(`/cards/${cardId}/tasks`, data, headers);
const updateTask = (id, data, headers) => socket.patch(`/tasks/${id}`, data, headers);
const deleteTask = (id, headers) => socket.delete(`/tasks/${id}`, undefined, headers);
export default {
createTask,
updateTask,
deleteTask,
};

31
client/src/api/users.js Executable file
View file

@ -0,0 +1,31 @@
import http from './http';
import socket from './socket';
/* Actions */
const getUsers = (headers) => socket.get('/users', undefined, headers);
const createUser = (data, headers) => socket.post('/users', data, headers);
const getCurrentUser = (headers) => socket.get('/users/me', undefined, headers);
const updateUser = (id, data, headers) => socket.patch(`/users/${id}`, data, headers);
const uploadUserAvatar = (id, file, headers) => http.post(
`/users/${id}/upload-avatar`,
{
file,
},
headers,
);
const deleteUser = (id, headers) => socket.delete(`/users/${id}`, undefined, headers);
export default {
getUsers,
createUser,
getCurrentUser,
updateUser,
uploadUserAvatar,
deleteUser,
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 KiB

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 50 50">
<path d="M 25 2 C 12.309295 2 2 12.309295 2 25 C 2 37.690705 12.309295 48 25 48 C 37.690705 48 48 37.690705 48 25 C 48 12.309295 37.690705 2 25 2 z M 25 4 C 36.609824 4 46 13.390176 46 25 C 46 36.609824 36.609824 46 25 46 C 13.390176 46 4 36.609824 4 25 C 4 13.390176 13.390176 4 25 4 z M 24 13 L 24 24 L 13 24 L 13 26 L 24 26 L 24 37 L 26 37 L 26 26 L 37 26 L 37 24 L 26 24 L 26 13 L 24 13 z" />
</svg>

After

Width:  |  Height:  |  Size: 432 B

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 50 50">
<path d="M 24 9 L 24 24 L 9 24 L 9 26 L 24 26 L 24 41 L 26 41 L 26 26 L 41 26 L 41 24 L 26 24 L 26 9 Z" />
</svg>

After

Width:  |  Height:  |  Size: 142 B

View file

@ -0,0 +1,85 @@
import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import {
Button, Form, Header, Modal,
} from 'semantic-ui-react';
import { Input } from '../../lib/custom-ui';
import { useDeepCompareCallback, useForm } from '../../hooks';
import styles from './AddProjectModal.module.css';
const AddProjectModal = React.memo(({
defaultData, isSubmitting, onCreate, onClose,
}) => {
const [t] = useTranslation();
const [data, handleFieldChange] = useForm(() => ({
name: '',
...defaultData,
}));
const nameField = useRef(null);
const handleSubmit = useDeepCompareCallback(() => {
const cleanData = {
...data,
name: data.name.trim(),
};
if (!cleanData.name) {
nameField.current.select();
return;
}
onCreate(cleanData);
}, [onCreate, data]);
useEffect(() => {
nameField.current.select();
}, []);
return (
<Modal open basic closeIcon size="tiny" onClose={onClose}>
<Modal.Content>
<Header inverted size="huge">
{t('common.createProject', {
context: 'title',
})}
</Header>
<p>{t('common.enterProjectTitle')}</p>
<Form onSubmit={handleSubmit}>
<Input
fluid
inverted
ref={nameField}
name="name"
value={data.name}
readOnly={isSubmitting}
className={styles.field}
onChange={handleFieldChange}
/>
<Button
inverted
color="green"
icon="checkmark"
content={t('action.createProject')}
floated="right"
loading={isSubmitting}
disabled={isSubmitting}
/>
</Form>
</Modal.Content>
</Modal>
);
});
AddProjectModal.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
isSubmitting: PropTypes.bool.isRequired,
onCreate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default AddProjectModal;

View file

@ -0,0 +1,3 @@
.field {
margin-bottom: 20px;
}

View file

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

View file

@ -0,0 +1,148 @@
import isEmail from 'validator/lib/isEmail';
import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Message } from 'semantic-ui-react';
import { withPopup } from '../../lib/popup';
import { Input, Popup } from '../../lib/custom-ui';
import {
useDeepCompareCallback, useDeepCompareEffect, useForm, usePrevious,
} from '../../hooks';
import styles from './AddUserPopup.module.css';
const AddUserPopup = React.memo(
({
defaultData, isSubmitting, error, onCreate, onMessageDismiss, onClose,
}) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
const [data, handleFieldChange] = useForm(() => ({
email: '',
password: '',
name: '',
...defaultData,
}));
const emailField = useRef(null);
const passwordField = useRef(null);
const nameField = useRef(null);
const handleSubmit = useDeepCompareCallback(() => {
const cleanData = {
...data,
email: data.email.trim(),
name: data.name.trim(),
};
if (!isEmail(cleanData.email)) {
emailField.current.select();
return;
}
if (!cleanData.password) {
passwordField.current.focus();
return;
}
if (!cleanData.name) {
nameField.current.select();
return;
}
onCreate(cleanData);
}, [onCreate, data]);
useEffect(() => {
emailField.current.select();
}, []);
useDeepCompareEffect(() => {
if (wasSubmitting && !isSubmitting) {
if (!error) {
onClose();
} else if (error.message === 'userIsAlreadyExist') {
emailField.current.select();
}
}
}, [isSubmitting, wasSubmitting, error, onClose]);
return (
<>
<Popup.Header>
{t('common.addUser', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
{error && (
<Message
// eslint-disable-next-line react/jsx-props-no-spreading
{...{
[error.type || 'error']: true,
}}
visible
content={t(`common.${error.message}`)}
onDismiss={onMessageDismiss}
/>
)}
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.email')}</div>
<Input
fluid
ref={emailField}
name="email"
value={data.email}
readOnly={isSubmitting}
className={styles.field}
onChange={handleFieldChange}
/>
<div className={styles.text}>{t('common.password')}</div>
<Input
fluid
ref={passwordField}
name="password"
value={data.password}
readOnly={isSubmitting}
className={styles.field}
onChange={handleFieldChange}
/>
<div className={styles.text}>{t('common.name')}</div>
<Input
fluid
ref={nameField}
name="name"
value={data.name}
readOnly={isSubmitting}
className={styles.field}
onChange={handleFieldChange}
/>
<Button
positive
content={t('action.addUser')}
loading={isSubmitting}
disabled={isSubmitting}
/>
</Form>
</Popup.Content>
</>
);
},
);
AddUserPopup.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
isSubmitting: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
onCreate: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
AddUserPopup.defaultProps = {
error: undefined,
};
export default withPopup(AddUserPopup);

View file

@ -0,0 +1,10 @@
.field {
margin-bottom: 8px;
}
.text {
color: #444444;
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}

View file

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

23
client/src/components/App.jsx Executable file
View file

@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import HeaderContainer from '../containers/HeaderContainer';
import ProjectsContainer from '../containers/ProjectsContainer';
import UsersModalContainer from '../containers/UsersModalContainer';
import AddProjectModalContainer from '../containers/AddProjectModalContainer';
const App = ({ isUsersModalOpened, isAddProjectModalOpened }) => (
<>
<HeaderContainer />
<ProjectsContainer />
{isUsersModalOpened && <UsersModalContainer />}
{isAddProjectModalOpened && <AddProjectModalContainer />}
</>
);
App.propTypes = {
isUsersModalOpened: PropTypes.bool.isRequired,
isAddProjectModalOpened: PropTypes.bool.isRequired,
};
export default App;

View file

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Loader } from 'semantic-ui-react';
import AppContainer from '../containers/AppContainer';
import SocketStatusContainer from '../containers/SocketStatusContainer';
const AppWrapper = React.memo(({ isInitializing }) => (
<>
{isInitializing ? <Loader active size="massive" /> : <AppContainer />}
<SocketStatusContainer />
</>
));
AppWrapper.propTypes = {
isInitializing: PropTypes.bool.isRequired,
};
export default AppWrapper;

View file

@ -0,0 +1,129 @@
import React, {
useCallback, useEffect, useImperativeHandle, useRef, useState,
} from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Input } from 'semantic-ui-react';
import {
useClosableForm,
useDeepCompareCallback,
useDidUpdate,
useForm,
useToggle,
} from '../../hooks';
import styles from './AddList.module.css';
const DEFAULT_DATA = {
name: '',
};
const AddList = React.forwardRef(({ children, onCreate }, ref) => {
const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const [selectNameFieldState, selectNameField] = useToggle();
const nameField = useRef(null);
const open = useCallback(() => {
setIsOpened(true);
}, []);
const close = useCallback(() => {
setIsOpened(false);
}, []);
useImperativeHandle(
ref,
() => ({
open,
close,
}),
[open, close],
);
const handleChildrenClick = useCallback(() => {
open();
}, [open]);
const handleFieldKeyDown = useCallback(
(event) => {
if (event.key === 'Escape') {
close();
}
},
[close],
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
isOpened,
close,
);
const handleSubmit = useDeepCompareCallback(() => {
const cleanData = {
...data,
name: data.name.trim(),
};
if (!cleanData.name) {
nameField.current.select();
return;
}
onCreate(cleanData);
setData(DEFAULT_DATA);
selectNameField();
}, [onCreate, data, setData, selectNameField]);
useEffect(() => {
if (isOpened) {
nameField.current.select();
}
}, [isOpened]);
useDidUpdate(() => {
nameField.current.select();
}, [selectNameFieldState]);
if (!isOpened) {
return React.cloneElement(children, {
onClick: handleChildrenClick,
});
}
return (
<Form className={styles.wrapper} onSubmit={handleSubmit}>
<Input
ref={nameField}
name="name"
value={data.name}
placeholder={t('common.enterListTitle')}
className={styles.field}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
onBlur={handleFieldBlur}
/>
<div className={styles.controls}>
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
<Button
positive
content={t('action.addList')}
className={styles.submitButton}
onMouseOver={handleControlMouseOver}
onMouseOut={handleControlMouseOut}
/>
</div>
</Form>
);
});
AddList.propTypes = {
children: PropTypes.element.isRequired,
onCreate: PropTypes.func.isRequired,
};
export default React.memo(AddList);

View file

@ -0,0 +1,31 @@
.controls {
margin-top: 4px;
}
.field {
border: none;
border-radius: 3px !important;
box-shadow: 0 1px 0 #ccc !important;
color: #333 !important;
outline: none !important;
overflow: hidden !important;
width: 100% !important;
}
.field:focus {
border-color: #298fca;
box-shadow: 0 0 2px #298fca;
}
.submitButton {
min-height: 30px;
vertical-align: top;
}
.wrapper {
background-color: #e2e4e6;
border-radius: 3px;
padding: 4px;
transition: opacity 40ms ease-in;
width: 272px;
}

View file

@ -0,0 +1,138 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { closePopup } from '../../lib/popup';
import DroppableTypes from '../../constants/DroppableTypes';
import ListContainer from '../../containers/ListContainer';
import CardModalContainer from '../../containers/CardModalContainer';
import AddList from './AddList';
import Filter from './Filter';
import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-icon.svg';
import styles from './Board.module.css';
const parseDNDId = (dndId) => parseInt(dndId.split(':').pop(), 10);
const Board = React.memo(
({
listIds,
filterUsers,
filterLabels,
allProjectMemberships,
allLabels,
isCardModalOpened,
onListCreate,
onListMove,
onCardMove,
onUserToFilterAdd,
onUserFromFilterRemove,
onLabelToFilterAdd,
onLabelFromFilterRemove,
onLabelCreate,
onLabelUpdate,
onLabelDelete,
}) => {
const [t] = useTranslation();
const handleDragStart = useCallback(() => {
closePopup();
}, []);
const handleDragEnd = useCallback(
({
draggableId, type, source, destination,
}) => {
if (
!destination
|| (source.droppableId === destination.droppableId && source.index === destination.index)
) {
return;
}
const id = parseDNDId(draggableId);
switch (type) {
case DroppableTypes.LIST:
onListMove(id, destination.index);
break;
case DroppableTypes.CARD:
onCardMove(id, parseDNDId(destination.droppableId), destination.index);
break;
default:
}
},
[onListMove, onCardMove],
);
return (
<>
<Filter
users={filterUsers}
labels={filterLabels}
allProjectMemberships={allProjectMemberships}
allLabels={allLabels}
onUserAdd={onUserToFilterAdd}
onUserRemove={onUserFromFilterRemove}
onLabelAdd={onLabelToFilterAdd}
onLabelRemove={onLabelFromFilterRemove}
onLabelCreate={onLabelCreate}
onLabelUpdate={onLabelUpdate}
onLabelDelete={onLabelDelete}
/>
<div className={styles.wrapper}>
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="board" type={DroppableTypes.LIST} direction="horizontal">
{({ innerRef, droppableProps, placeholder }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...droppableProps} data-drag-scroller ref={innerRef} className={styles.lists}>
{listIds.map((listId, index) => (
<ListContainer key={listId} id={listId} index={index} />
))}
{placeholder}
<div data-drag-scroller className={styles.list}>
<AddList onCreate={onListCreate}>
<button type="button" className={styles.addListButton}>
<PlusMathIcon className={styles.addListButtonIcon} />
<span className={styles.addListButtonText}>
{listIds.length > 0 ? t('action.addAnotherList') : t('action.addList')}
</span>
</button>
</AddList>
</div>
</div>
)}
</Droppable>
</DragDropContext>
</div>
{isCardModalOpened && <CardModalContainer />}
</>
);
},
);
Board.propTypes = {
/* eslint-disable react/forbid-prop-types */
listIds: PropTypes.array.isRequired,
filterUsers: PropTypes.array.isRequired,
filterLabels: PropTypes.array.isRequired,
allProjectMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
isCardModalOpened: PropTypes.bool.isRequired,
onListCreate: PropTypes.func.isRequired,
onListMove: PropTypes.func.isRequired,
onCardMove: PropTypes.func.isRequired,
onUserToFilterAdd: PropTypes.func.isRequired,
onUserFromFilterRemove: PropTypes.func.isRequired,
onLabelToFilterAdd: PropTypes.func.isRequired,
onLabelFromFilterRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
};
export default Board;

View file

@ -0,0 +1,52 @@
.addListButton {
background-color: rgba(0, 0, 0, 0.12);
border: none;
border-radius: 3px;
color: hsla(0, 0%, 100%, 0.7);
cursor: pointer;
display: block;
fill: hsla(0, 0%, 100%, 0.7);
font-weight: normal;
height: 42px;
padding: 11px;
text-align: left;
transition: background 85ms ease-in, opacity 40ms ease-in,
border-color 85ms ease-in;
width: 100%;
}
.addListButton:active {
outline: none;
}
.addListButton:hover {
background-color: rgba(0, 0, 0, 0.3);
}
.addListButtonIcon {
height: 20px;
padding: 0.64px;
width: 20px;
}
.addListButtonText {
display: inline-block;
font-size: 14px;
line-height: 20px;
vertical-align: top;
}
.list {
margin: 0 20px 0 4px;
width: 272px;
}
.lists {
display: inline-flex;
height: 100%;
min-width: 100%;
}
.wrapper {
height: 100%;
}

View file

@ -0,0 +1,120 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import User from '../User';
import Label from '../Label';
import ProjectMembershipsPopup from '../ProjectMembershipsPopup';
import LabelsPopup from '../LabelsPopup';
import styles from './Filter.module.css';
const Filter = React.memo(
({
users,
labels,
allProjectMemberships,
allLabels,
onUserAdd,
onUserRemove,
onLabelAdd,
onLabelRemove,
onLabelCreate,
onLabelUpdate,
onLabelDelete,
}) => {
const [t] = useTranslation();
const handleUserRemoveClick = useCallback(
(id) => {
onUserRemove(id);
},
[onUserRemove],
);
const handleLabelRemoveClick = useCallback(
(id) => {
onLabelRemove(id);
},
[onLabelRemove],
);
return (
<div className={styles.filters}>
<span className={styles.filter}>
<ProjectMembershipsPopup
items={allProjectMemberships}
currentUserIds={users.map((user) => user.id)}
title={t('common.filterByMembers', {
context: 'title',
})}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
<button type="button" className={styles.filterButton}>
<span className={styles.filterTitle}>{`${t('common.members')}:`}</span>
{users.length === 0 && <span className={styles.filterLabel}>{t('common.all')}</span>}
</button>
</ProjectMembershipsPopup>
{users.map((user) => (
<span key={user.id} className={styles.filterItem}>
<User
name={user.name}
avatar={user.avatar}
size="small"
onClick={() => handleUserRemoveClick(user.id)}
/>
</span>
))}
</span>
<span className={styles.filter}>
<LabelsPopup
items={allLabels}
currentIds={labels.map((label) => label.id)}
title={t('common.filterByLabels', {
context: 'title',
})}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onDelete={onLabelDelete}
>
<button type="button" className={styles.filterButton}>
<span className={styles.filterTitle}>{`${t('common.labels')}:`}</span>
{labels.length === 0 && <span className={styles.filterLabel}>{t('common.all')}</span>}
</button>
</LabelsPopup>
{labels.map((label) => (
<span key={label.id} className={styles.filterItem}>
<Label
name={label.name}
color={label.color}
size="small"
onClick={() => handleLabelRemoveClick(label.id)}
/>
</span>
))}
</span>
</div>
);
},
);
Filter.propTypes = {
/* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
allProjectMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
onLabelAdd: PropTypes.func.isRequired,
onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
};
export default Filter;

View file

@ -0,0 +1,51 @@
.filter {
display: inline-block;
line-height: 0 !important;
margin-right: 16px;
}
.filterButton {
background: transparent;
border: none;
cursor: pointer;
display: inline-block;
outline: none;
padding: 0;
}
.filterItem {
display: inline-block;
font-size: 0;
line-height: 0;
margin-right: 4px;
max-width: 190px;
vertical-align: top;
}
.filterLabel {
background: #2d3034;
border-radius: 3px;
color: #fff;
display: inline-block;
font-size: 12px;
line-height: 20px;
padding: 2px 8px;
}
.filterLabel:hover {
opacity: 0.75;
}
.filterTitle {
border-radius: 3px;
color: #fff;
display: inline-block;
font-size: 12px;
line-height: 20px;
padding: 2px 12px;
}
.filters {
line-height: 0 !important;
margin-bottom: 12px;
}

View file

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

View file

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Loader } from 'semantic-ui-react';
import BoardContainer from '../containers/BoardContainer';
const BoardWrapper = React.memo(({ isFetching }) => {
if (isFetching) {
return <Loader active />;
}
return <BoardContainer />;
});
BoardWrapper.propTypes = {
isFetching: PropTypes.bool.isRequired,
};
export default BoardWrapper;

View file

@ -0,0 +1,69 @@
import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form } from 'semantic-ui-react';
import { withPopup } from '../../lib/popup';
import { Input, Popup } from '../../lib/custom-ui';
import { useDeepCompareCallback, useForm } from '../../hooks';
import styles from './AddPopup.module.css';
const AddStep = React.memo(({ onCreate, onClose }) => {
const [t] = useTranslation();
const [data, handleFieldChange] = useForm({
name: '',
});
const nameField = useRef(null);
const handleSubmit = useDeepCompareCallback(() => {
const cleanData = {
...data,
name: data.name.trim(),
};
if (!cleanData.name) {
nameField.current.select();
return;
}
onCreate(cleanData);
onClose();
}, [onCreate, onClose, data]);
useEffect(() => {
nameField.current.select();
}, []);
return (
<>
<Popup.Header>
{t('common.createBoard', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<Input
fluid
ref={nameField}
name="name"
value={data.name}
className={styles.field}
onChange={handleFieldChange}
/>
<Button positive content={t('action.createBoard')} />
</Form>
</Popup.Content>
</>
);
});
AddStep.propTypes = {
onCreate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(AddStep);

View file

@ -0,0 +1,3 @@
.field {
margin-bottom: 8px;
}

View file

@ -0,0 +1,183 @@
import pick from 'lodash/pick';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation, Trans } from 'react-i18next';
import { Link } from 'react-router-dom';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import { Button, Icon } from 'semantic-ui-react';
import { closePopup } from '../../lib/popup';
import { DragScroller } from '../../lib/custom-ui';
import Paths from '../../constants/Paths';
import DroppableTypes from '../../constants/DroppableTypes';
import BoardWrapperContainer from '../../containers/BoardWrapperContainer';
import AddPopup from './AddPopup';
import EditPopup from './EditPopup';
import styles from './Boards.module.css';
const Boards = React.memo(
({
items, currentId, isEditable, onCreate, onUpdate, onMove, onDelete,
}) => {
const [t] = useTranslation();
const handleDragStart = useCallback(() => {
closePopup();
}, []);
const handleDragEnd = useCallback(
({ draggableId, source, destination }) => {
if (!destination || source.index === destination.index) {
return;
}
onMove(draggableId, destination.index);
},
[onMove],
);
const handleUpdate = useCallback(
(id, data) => {
onUpdate(id, data);
},
[onUpdate],
);
const handleDelete = useCallback(
(id) => {
onDelete(id);
},
[onDelete],
);
const renderItems = useCallback(
(safeItems) => safeItems.map((item) => (
<div key={item.id} className={styles.tabWrapper}>
<div className={classNames(styles.tab, item.id === currentId && styles.tabActive)}>
{item.isPersisted ? (
<Link
to={Paths.BOARDS.replace(':id', item.id)}
title={item.name}
className={styles.link}
>
{item.name}
</Link>
) : (
<span className={styles.link}>{item.name}</span>
)}
</div>
</div>
)),
[currentId],
);
const renderEditableItems = useCallback(
(safeItems) => safeItems.map((item, index) => (
<Draggable
key={item.id}
draggableId={item.id}
index={index}
isDragDisabled={!item.isPersisted}
>
{({ innerRef, draggableProps, dragHandleProps }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...draggableProps} ref={innerRef} className={styles.tabWrapper}>
<div className={classNames(styles.tab, item.id === currentId && styles.tabActive)}>
{item.isPersisted ? (
<Link
{...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
to={Paths.BOARDS.replace(':id', item.id)}
title={item.name}
className={styles.link}
>
{item.name}
</Link>
) : (
// eslint-disable-next-line react/jsx-props-no-spreading
<span {...dragHandleProps} className={styles.link}>
{item.name}
</span>
)}
{item.isPersisted && (
<EditPopup
defaultData={pick(item, 'name')}
onUpdate={(data) => handleUpdate(item.id, data)}
onDelete={() => handleDelete(item.id)}
>
<Button className={classNames(styles.editButton, styles.target)}>
<Icon fitted name="pencil" size="small" />
</Button>
</EditPopup>
)}
</div>
</div>
)}
</Draggable>
)),
[currentId, handleUpdate, handleDelete],
);
return (
<div className={styles.wrapper}>
{isEditable ? (
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="boards" type={DroppableTypes.BOARD} direction="horizontal">
{({ innerRef, droppableProps, placeholder }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...droppableProps} ref={innerRef} className={styles.tabs}>
{renderEditableItems(items)}
{placeholder}
<AddPopup onCreate={onCreate}>
<Button icon="plus" className={styles.addButton} />
</AddPopup>
</div>
)}
</Droppable>
</DragDropContext>
) : (
<div className={styles.tabs}>{renderItems(items)}</div>
)}
<DragScroller className={styles.board}>
{currentId ? (
<BoardWrapperContainer />
) : (
<div className={styles.message}>
<Icon
inverted
name="hand point up outline"
size="huge"
className={styles.messageIcon}
/>
<h1 className={styles.messageTitle}>
{t('common.openBoard', {
context: 'title',
})}
</h1>
<div className={styles.messageContent}>
<Trans i18nKey="common.createNewOneOrSelectExistingOne" />
</div>
</div>
)}
</DragScroller>
</div>
);
},
);
Boards.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
currentId: PropTypes.number,
isEditable: PropTypes.bool.isRequired,
onCreate: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
Boards.defaultProps = {
currentId: undefined,
};
export default Boards;

View file

@ -0,0 +1,115 @@
.addButton {
background: transparent !important;
color: #fff !important;
margin-right: 0 !important;
vertical-align: top !important;
}
.addButton:hover {
background: rgba(34, 36, 38, 0.3) !important;
}
.board {
display: flex;
flex: 1 1 auto;
flex-direction: column;
margin: 0 -20px 8px;
overflow: auto;
padding: 0 20px;
}
.editButton {
background: transparent !important;
color: #fff !important;
line-height: 32px !important;
margin-right: 0 !important;
opacity: 0;
padding: 0 !important;
position: absolute;
right: 2px;
top: 2px;
width: 32px;
}
.editButton:hover {
background: rgba(34, 36, 38, 0.3) !important;
}
.link {
color: #fff !important;
display: block;
line-height: 20px;
padding: 10px 34px 6px 14px;
text-overflow: ellipsis;
max-width: 400px;
overflow: hidden;
}
.message {
align-content: space-between;
align-items: center;
color: #fff;
display: flex;
flex: 1 1 auto;
flex-direction: column;
justify-content: center;
}
.messageIcon {
margin-top: -84px;
}
.messageTitle {
font-size: 32px;
margin: 24px 0 8px;
}
.messageContent {
font-size: 18px;
line-height: 1.4;
margin: 4px 0 0;
text-align: center;
}
.tab {
border-radius: 3px 3px 0 0;
min-width: 160px;
position: relative;
transition: all 0.1s ease;
}
.tab:hover {
background: #353a3f !important;
}
.tab:hover .target {
opacity: 1 !important;
}
.tabActive {
background: #2c3035 !important;
}
.tabActive:hover {
background: #353a3f !important;
}
.tabWrapper {
display: flex;
flex: 0 0 auto;
}
.tabs {
border-bottom: 2px solid #2c3035;
display: flex;
height: 38px;
flex: 0 0 auto;
margin-bottom: 16px;
white-space: nowrap;
}
.wrapper {
display: flex;
flex: 1 1 auto;
flex-direction: column;
}

View file

@ -0,0 +1,108 @@
import dequal from 'dequal';
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form } from 'semantic-ui-react';
import { withPopup } from '../../lib/popup';
import { Input, Popup } from '../../lib/custom-ui';
import { useDeepCompareCallback, useForm, useSteps } from '../../hooks';
import DeleteStep from '../DeleteStep';
import styles from './EditPopup.module.css';
const StepTypes = {
DELETE: 'DELETE',
};
const EditStep = React.memo(({
defaultData, onUpdate, onDelete, onClose,
}) => {
const [t] = useTranslation();
const [data, handleFieldChange] = useForm(() => ({
name: '',
...defaultData,
}));
const [step, openStep, handleBack] = useSteps();
const nameField = useRef(null);
const handleSubmit = useDeepCompareCallback(() => {
const cleanData = {
...data,
name: data.name.trim(),
};
if (!cleanData.name) {
nameField.current.select();
return;
}
if (!dequal(cleanData, defaultData)) {
onUpdate(cleanData);
}
onClose();
}, [defaultData, onUpdate, onClose, data]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
useEffect(() => {
nameField.current.select();
}, []);
if (step && step.type === StepTypes.DELETE) {
return (
<DeleteStep
title={t('common.deleteBoard', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisBoard')}
buttonContent={t('action.deleteBoard')}
onConfirm={onDelete}
onBack={handleBack}
/>
);
}
return (
<>
<Popup.Header>
{t('common.editBoard', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<Input
fluid
ref={nameField}
name="name"
value={data.name}
className={styles.field}
onChange={handleFieldChange}
/>
<Button positive content={t('action.save')} />
</Form>
<Button
content={t('action.delete')}
className={styles.deleteButton}
onClick={handleDeleteClick}
/>
</Popup.Content>
</>
);
});
EditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(EditStep);

View file

@ -0,0 +1,10 @@
.deleteButton {
bottom: 12px;
box-shadow: 0 1px 0 #cbcccc;
position: absolute;
right: 9px;
}
.field {
margin-bottom: 8px;
}

View file

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

View file

@ -0,0 +1,216 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react';
import { withPopup } from '../../lib/popup';
import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks';
import ProjectMembershipsStep from '../ProjectMembershipsStep';
import LabelsStep from '../LabelsStep';
import EditDeadlineStep from '../EditDeadlineStep';
import EditTimerStep from '../EditTimerStep';
import DeleteStep from '../DeleteStep';
import styles from './ActionsPopup.module.css';
const StepTypes = {
USERS: 'USERS',
LABELS: 'LABELS',
EDIT_DEADLINE: 'EDIT_DEADLINE',
EDIT_TIMER: 'EDIT_TIMER',
DELETE: 'DELETE',
};
const ActionsStep = React.memo(
({
card,
projectMemberships,
currentUserIds,
labels,
currentLabelIds,
onNameEdit,
onUpdate,
onDelete,
onUserAdd,
onUserRemove,
onLabelAdd,
onLabelRemove,
onLabelCreate,
onLabelUpdate,
onLabelDelete,
onClose,
}) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleNameEditClick = useCallback(() => {
onNameEdit();
onClose();
}, [onNameEdit, onClose]);
const handleUsersClick = useCallback(() => {
openStep(StepTypes.USERS);
}, [openStep]);
const handleLabelsClick = useCallback(() => {
openStep(StepTypes.LABELS);
}, [openStep]);
const handleEditDeadlineClick = useCallback(() => {
openStep(StepTypes.EDIT_DEADLINE);
}, [openStep]);
const handleEditTimerClick = useCallback(() => {
openStep(StepTypes.EDIT_TIMER);
}, [openStep]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
const handleDeadlineUpdate = useCallback(
(deadline) => {
onUpdate({
deadline,
});
},
[onUpdate],
);
const handleTimerUpdate = useCallback(
(timer) => {
onUpdate({
timer,
});
},
[onUpdate],
);
if (step) {
switch (step.type) {
case StepTypes.USERS:
return (
<ProjectMembershipsStep
items={projectMemberships}
currentUserIds={currentUserIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
onBack={handleBack}
/>
);
case StepTypes.LABELS:
return (
<LabelsStep
items={labels}
currentIds={currentLabelIds}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onDelete={onLabelDelete}
onBack={handleBack}
/>
);
case StepTypes.EDIT_DEADLINE:
return (
<EditDeadlineStep
defaultValue={card.deadline}
onUpdate={handleDeadlineUpdate}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.EDIT_TIMER:
return (
<EditTimerStep
defaultValue={card.timer}
onUpdate={handleTimerUpdate}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.DELETE:
return (
<DeleteStep
title={t('common.deleteCard', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisCard')}
buttonContent={t('action.deleteCard')}
onConfirm={onDelete}
onBack={handleBack}
/>
);
default:
}
}
return (
<>
<Popup.Header>
{t('common.cardActions', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleNameEditClick}>
{t('action.editTitle', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleUsersClick}>
{t('common.members', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleLabelsClick}>
{t('common.labels', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditDeadlineClick}>
{t('action.editDeadline', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditTimerClick}>
{t('action.editTimer', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteCard', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
},
);
ActionsStep.propTypes = {
/* eslint-disable react/forbid-prop-types */
card: PropTypes.object.isRequired,
projectMemberships: PropTypes.array.isRequired,
currentUserIds: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
currentLabelIds: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
onNameEdit: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
onLabelAdd: PropTypes.func.isRequired,
onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(ActionsStep);

View file

@ -0,0 +1,9 @@
.menu {
margin: -7px -12px -5px !important;
width: calc(100% + 24px) !important;
}
.menuItem {
margin: 0 !important;
padding-left: 14px !important;
}

View file

@ -0,0 +1,201 @@
import React, { useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Button, Icon } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { Draggable } from 'react-beautiful-dnd';
import Paths from '../../constants/Paths';
import Tasks from './Tasks';
import EditName from './EditName';
import ActionsPopup from './ActionsPopup';
import User from '../User';
import Label from '../Label';
import Deadline from '../Deadline';
import Timer from '../Timer';
import styles from './Card.module.css';
const Card = React.memo(
({
id,
index,
name,
deadline,
timer,
isPersisted,
notificationsTotal,
users,
labels,
tasks,
allProjectMemberships,
allLabels,
onUpdate,
onDelete,
onUserAdd,
onUserRemove,
onLabelAdd,
onLabelRemove,
onLabelCreate,
onLabelUpdate,
onLabelDelete,
}) => {
const editName = useRef(null);
const handleClick = useCallback(() => {
if (document.activeElement) {
document.activeElement.blur();
}
}, []);
const handleNameUpdate = useCallback(
(newName) => {
onUpdate({
name: newName,
});
},
[onUpdate],
);
const handleNameEdit = useCallback(() => {
editName.current.open();
}, []);
const contentNode = (
<>
{labels.length > 0 && (
<span className={styles.labels}>
{labels.map((label) => (
<span key={label.id} className={classNames(styles.attachment, styles.attachmentLeft)}>
<Label name={label.name} color={label.color} size="tiny" />
</span>
))}
</span>
)}
<div className={styles.name}>{name}</div>
{tasks.length > 0 && <Tasks items={tasks} />}
{(deadline || timer) && (
<span className={styles.attachments}>
{notificationsTotal > 0 && (
<span
className={classNames(
styles.attachment,
styles.attachmentLeft,
styles.notification,
)}
>
{notificationsTotal}
</span>
)}
{deadline && (
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
<Deadline value={deadline} size="tiny" />
</span>
)}
{timer && (
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
<Timer startedAt={timer.startedAt} total={timer.total} size="tiny" />
</span>
)}
</span>
)}
{users.length > 0 && (
<span className={classNames(styles.attachments, styles.attachmentsRight)}>
{users.map((user) => (
<span key={user.id} className={classNames(styles.attachment, styles.attachmentRight)}>
<User name={user.name} avatar={user.avatar} size="tiny" />
</span>
))}
</span>
)}
</>
);
return (
<Draggable draggableId={`card:${id}`} index={index} isDragDisabled={!isPersisted}>
{({ innerRef, draggableProps, dragHandleProps }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...draggableProps} {...dragHandleProps} ref={innerRef} className={styles.wrapper}>
<EditName ref={editName} defaultValue={name} onUpdate={handleNameUpdate}>
<div className={styles.card}>
{isPersisted ? (
<>
<Link
to={isPersisted && Paths.CARDS.replace(':id', id)}
className={styles.content}
onClick={handleClick}
>
{contentNode}
</Link>
<ActionsPopup
card={{
id,
name,
deadline,
timer,
isPersisted,
}}
projectMemberships={allProjectMemberships}
currentUserIds={users.map((user) => user.id)}
labels={allLabels}
currentLabelIds={labels.map((label) => label.id)}
onNameEdit={handleNameEdit}
onUpdate={onUpdate}
onDelete={onDelete}
onUserAdd={onUserAdd}
onUserRemove={onUserRemove}
onLabelAdd={onLabelAdd}
onLabelRemove={onLabelRemove}
onLabelCreate={onLabelCreate}
onLabelUpdate={onLabelUpdate}
onLabelDelete={onLabelDelete}
>
<Button className={classNames(styles.actionsButton, styles.target)}>
<Icon fitted name="pencil" size="small" />
</Button>
</ActionsPopup>
</>
) : (
<span className={styles.content}>{contentNode}</span>
)}
</div>
</EditName>
</div>
)}
</Draggable>
);
},
);
Card.propTypes = {
id: PropTypes.number.isRequired,
index: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
deadline: PropTypes.instanceOf(Date),
timer: PropTypes.object, // eslint-disable-line react/forbid-prop-types
isPersisted: PropTypes.bool.isRequired,
notificationsTotal: PropTypes.number.isRequired,
/* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
tasks: PropTypes.array.isRequired,
allProjectMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
onLabelAdd: PropTypes.func.isRequired,
onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
};
Card.defaultProps = {
deadline: undefined,
timer: undefined,
};
export default Card;

View file

@ -0,0 +1,112 @@
.actionsButton {
background: none !important;
box-shadow: none !important;
border-radius: 3px !important;
box-sizing: content-box;
color: #798d99 !important;
display: inline-block !important;
margin: 0 !important;
min-height: auto !important;
opacity: 0;
outline: none;
padding: 4px !important;
position: absolute;
right: 2px;
top: 2px;
transition: background 85ms ease !important;
width: 20px;
}
.actionsButton:hover {
background: #ebeef0 !important;
color: #516b7a !important;
}
.attachment {
display: inline-block;
line-height: 0;
margin: 0 0 6px 0;
max-width: 100%;
vertical-align: top;
}
.attachmentLeft {
margin-right: 4px;
}
.attachmentRight {
margin-left: 2px;
}
.attachments {
display: inline-block;
padding-bottom: 2px;
}
.attachmentsRight {
float: right;
line-height: 0;
}
.card {
background-color: #fff;
border-radius: 3px;
box-shadow: 0 1px 0 #ccc;
position: relative;
cursor: pointer;
}
.card:hover {
background-color: #f5f6f7;
border-bottom-color: rgba(9, 45, 66, 0.25);
}
.card:hover .target {
opacity: 1 !important;
}
.content {
cursor: grab;
display: block;
padding: 6px 8px 0;
}
.content:after {
content: "";
display: table;
clear: both;
}
.labels {
display: block;
max-width: 100%;
overflow: hidden;
}
.name {
color: #17394d;
font-size: 14px;
line-height: 18px;
padding-bottom: 6px;
word-wrap: break-word;
}
.notification {
background: #eb5a46;
color: #fff;
font-size: 12px;
line-height: 20px;
padding: 0px 6px;
border: none;
border-radius: 3px;
display: inline-block;
outline: none;
text-align: left;
transition: background 0.3s ease;
vertical-align: top;
}
.wrapper {
display: block;
margin-bottom: 8px;
}

View file

@ -0,0 +1,128 @@
import React, {
useCallback, useEffect, useImperativeHandle, useRef, useState,
} from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { useClosableForm, useField } from '../../hooks';
import styles from './EditName.module.css';
const EditName = React.forwardRef(({ children, defaultValue, onUpdate }, ref) => {
const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
const [value, handleFieldChange, setValue] = useField(defaultValue);
const field = useRef(null);
const open = useCallback(() => {
setIsOpened(true);
setValue(defaultValue);
}, [defaultValue, setValue]);
const close = useCallback(() => {
setIsOpened(false);
setValue(null);
}, [setValue]);
const submit = useCallback(() => {
const cleanValue = value.trim();
if (!cleanValue) {
field.current.ref.current.select();
return;
}
if (cleanValue !== defaultValue) {
onUpdate(cleanValue);
}
close();
}, [defaultValue, onUpdate, value, close]);
useImperativeHandle(
ref,
() => ({
open,
close,
}),
[open, close],
);
const handleFieldKeyDown = useCallback(
(event) => {
switch (event.key) {
case 'Enter':
event.preventDefault();
submit();
break;
case 'Escape':
close();
break;
default:
}
},
[close, submit],
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
isOpened,
close,
);
const handleSubmit = useCallback(() => {
submit();
}, [submit]);
useEffect(() => {
if (isOpened) {
field.current.ref.current.select();
}
}, [isOpened]);
if (!isOpened) {
return children;
}
return (
<Form onSubmit={handleSubmit}>
<div className={styles.fieldWrapper}>
<TextArea
ref={field}
as={TextareaAutosize}
value={value}
minRows={3}
maxRows={8}
spellCheck={false}
className={styles.field}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
onBlur={handleFieldBlur}
/>
</div>
<div className={styles.controls}>
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
<Button
positive
content={t('action.save')}
className={styles.submitButton}
onMouseOver={handleControlMouseOver}
onMouseOut={handleControlMouseOut}
/>
</div>
</Form>
);
});
EditName.propTypes = {
children: PropTypes.element.isRequired,
defaultValue: PropTypes.string.isRequired,
onUpdate: PropTypes.func.isRequired,
};
export default React.memo(EditName);

View file

@ -0,0 +1,24 @@
.field {
border: none !important;
margin-bottom: 4px !important;
outline: none !important;
overflow: hidden !important;
padding: 0 !important;
resize: none !important;
width: 100% !important;
word-wrap: break-word !important;
}
.fieldWrapper {
background-color: #fff !important;
border-radius: 3px !important;
box-shadow: 0 1px 0 #ccc !important;
margin-bottom: 8px !important;
min-height: 20px !important;
padding: 6px 8px 2px !important;
}
.submitButton {
margin-bottom: 8px;
vertical-align: top;
}

View file

@ -0,0 +1,69 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Progress } from 'semantic-ui-react';
import { useToggle } from '../../hooks';
import styles from './Tasks.module.css';
const Tasks = React.memo(({ items }) => {
const [isOpened, toggleOpened] = useToggle();
const handleToggleClick = useCallback(
(event) => {
event.preventDefault();
toggleOpened();
},
[toggleOpened],
);
const completedItems = items.filter((item) => item.isCompleted);
return (
<>
{/* eslint-disable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
<div className={styles.button} onClick={handleToggleClick}>
{/* eslint-enable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
<span className={styles.progressWrapper}>
<Progress
autoSuccess
value={completedItems.length}
total={items.length}
color="blue"
size="tiny"
className={styles.progress}
/>
</span>
<span
className={classNames(styles.count, isOpened ? styles.countOpened : styles.countClosed)}
>
{completedItems.length}
{'/'}
{items.length}
</span>
</div>
{isOpened && (
<ul className={styles.tasks}>
{items.map((item) => (
<li
key={item.id}
className={classNames(styles.task, item.isCompleted && styles.taskCompleted)}
>
{item.name}
</li>
))}
</ul>
)}
</>
);
});
Tasks.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
};
export default Tasks;

View file

@ -0,0 +1,83 @@
.button {
background: transparent;
border: none;
cursor: pointer;
line-height: 0;
margin: 0 -8px;
outline: none;
padding: 0px 8px 8px;
width: calc(100% + 16px);
}
.count {
color: #888;
cursor: pointer;
display: inline-block;
font-size: 12px;
line-height: 12px;
text-align: right;
vertical-align: top;
width: 50px;
}
.count:after {
content: "";
opacity: 0.4;
}
.count:hover {
opacity: 0.75;
}
.countOpened:after {
background: url("data:image/gif;base64,R0lGODlhCwALAJEAAAAAAP///xUVFf///yH5BAEAAAMALAAAAAALAAsAAAIPnI+py+0/hJzz0IruwjsVADs=")
no-repeat center right;
margin-left: 2px;
padding: 6px 6px 0px;
}
.countClosed:after {
background: url("data:image/gif;base64,R0lGODlhCwALAJEAAAAAAP///xUVFf///yH5BAEAAAMALAAAAAALAAsAAAIRnC2nKLnT4or00Puy3rx7VQAAOw==")
no-repeat center right;
margin-left: 2px;
padding: 0 6px 6px;
}
.progress {
margin: 0 !important;
}
.progressWrapper {
display: inline-block;
padding: 3px 0;
vertical-align: top;
width: calc(100% - 50px);
}
.task {
display: block;
font-size: 12px;
line-height: 14px;
padding-bottom: 6px;
padding-left: 14px;
word-break: break-all;
}
.task:before {
content: "";
position: absolute;
left: 10px;
}
.taskCompleted {
color: #aaa;
text-decoration: line-through;
}
.tasks {
color: #333;
cursor: grab;
list-style: none;
margin: -2px 0 0;
padding-left: 0;
}

View file

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

View file

@ -0,0 +1,101 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import {
Comment, Icon, Loader, Visibility,
} from 'semantic-ui-react';
import { ActionTypes } from '../../../constants/Enums';
import AddComment from './AddComment';
import Item from './Item';
import styles from './Actions.module.css';
const Actions = React.memo(
({
items,
isFetching,
isAllFetched,
isEditable,
onFetch,
onCommentCreate,
onCommentUpdate,
onCommentDelete,
}) => {
const [t] = useTranslation();
const handleCommentUpdate = useCallback(
(id, data) => {
onCommentUpdate(id, data);
},
[onCommentUpdate],
);
const handleCommentDelete = useCallback(
(id) => {
onCommentDelete(id);
},
[onCommentDelete],
);
return (
<>
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="comment outline" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.addComment')}</div>
<AddComment onCreate={onCommentCreate} />
</div>
</div>
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="list ul" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.actions')}</div>
<div className={styles.wrapper}>
<Comment.Group>
{items.map((item) => (item.type === ActionTypes.COMMENT_CARD ? (
<Item.Comment
key={item.id}
data={item.data}
createdAt={item.createdAt}
isPersisted={item.isPersisted}
user={item.user}
isEditable={isEditable}
onUpdate={(data) => handleCommentUpdate(item.id, data)}
onDelete={() => handleCommentDelete(item.id)}
/>
) : (
<Item
key={item.id}
type={item.type}
data={item.data}
createdAt={item.createdAt}
user={item.user}
/>
)))}
</Comment.Group>
</div>
{isFetching ? (
<Loader active inverted inline="centered" size="small" className={styles.loader} />
) : (
!isAllFetched && <Visibility fireOnMount onOnScreen={onFetch} />
)}
</div>
</div>
</>
);
},
);
Actions.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
isFetching: PropTypes.bool.isRequired,
isAllFetched: PropTypes.bool.isRequired,
isEditable: PropTypes.bool.isRequired,
onFetch: PropTypes.func.isRequired,
onCommentCreate: PropTypes.func.isRequired,
onCommentUpdate: PropTypes.func.isRequired,
onCommentDelete: PropTypes.func.isRequired,
};
export default Actions;

View file

@ -0,0 +1,38 @@
.contentModule {
margin-bottom: 24px;
}
.loader {
margin-top: 10px !important;
}
.moduleHeader {
color: #17394d;
font-size: 16px;
font-weight: bold;
line-height: 20px;
margin: 0 0 4px;
padding: 8px 0;
}
.moduleIcon {
color: #17394d;
font-size: 17px !important;
height: 32px !important;
left: -40px;
line-height: 32px;
margin-right: 0 !important;
position: absolute;
top: 2px;
width: 32px !important;
}
.moduleWrapper {
margin: 0 0 0 40px;
position: relative;
}
.wrapper {
margin-left: -40px;
margin-top: 12px;
}

View file

@ -0,0 +1,75 @@
import React, { useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { useDeepCompareCallback, useForm } from '../../../hooks';
import styles from './AddComment.module.css';
const DEFAULT_DATA = {
text: '',
};
const AddComment = React.memo(({ onCreate }) => {
const [t] = useTranslation();
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const textField = useRef(null);
const submit = useDeepCompareCallback(() => {
const cleanData = {
...data,
text: data.text.trim(),
};
if (!cleanData.text) {
textField.current.ref.current.select();
return;
}
onCreate(cleanData);
setData(DEFAULT_DATA);
}, [onCreate, data, setData]);
const handleFieldKeyDown = useCallback(
(event) => {
if (event.ctrlKey && event.key === 'Enter') {
submit();
}
},
[submit],
);
const handleSubmit = useCallback(() => {
submit();
}, [submit]);
return (
<Form onSubmit={handleSubmit}>
<TextArea
ref={textField}
as={TextareaAutosize}
name="text"
value={data.text}
placeholder={t('common.writeComment')}
minRows={3}
spellCheck={false}
className={styles.field}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
/>
<div className={styles.controls}>
<Button positive content={t('action.addComment')} disabled={!data.text} />
</div>
</Form>
);
});
AddComment.propTypes = {
onCreate: PropTypes.func.isRequired,
};
export default AddComment;

View file

@ -0,0 +1,23 @@
.controls {
clear: both;
margin-top: 6px;
}
.field {
background: #fff !important;
border: 0 !important;
box-sizing: border-box;
color: #333 !important;
display: block;
line-height: 1.5 !important;
font-size: 14px !important;
margin-bottom: 6px !important;
overflow: hidden;
padding: 8px 12px !important;
resize: none !important;
width: 100% !important;
}
.field:focus {
outline: none;
}

View file

@ -0,0 +1,122 @@
import dequal from 'dequal';
import React, {
useCallback, useEffect, useImperativeHandle, useRef, useState,
} from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { useClosableForm, useDeepCompareCallback, useForm } from '../../../hooks';
import styles from './EditComment.module.css';
const EditComment = React.forwardRef(({ children, defaultData, onUpdate }, ref) => {
const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
const [data, handleFieldChange, setData] = useForm(null);
const textField = useRef(null);
const open = useDeepCompareCallback(() => {
setIsOpened(true);
setData({
text: '',
...defaultData,
});
}, [defaultData, setData]);
const close = useCallback(() => {
setIsOpened(false);
setData(null);
}, [setData]);
const submit = useDeepCompareCallback(() => {
const cleanData = {
...data,
text: data.text.trim(),
};
if (!cleanData.text) {
textField.current.ref.current.select();
return;
}
if (!dequal(cleanData, defaultData)) {
onUpdate(cleanData);
}
close();
}, [defaultData, onUpdate, data, close]);
useImperativeHandle(
ref,
() => ({
open,
close,
}),
[open, close],
);
const handleFieldKeyDown = useCallback(
(event) => {
if (event.ctrlKey && event.key === 'Enter') {
submit();
}
},
[submit],
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
isOpened,
close,
);
const handleSubmit = useCallback(() => {
submit();
}, [submit]);
useEffect(() => {
if (isOpened) {
textField.current.ref.current.select();
}
}, [isOpened]);
if (!isOpened) {
return children;
}
return (
<Form onSubmit={handleSubmit}>
<TextArea
ref={textField}
as={TextareaAutosize}
name="text"
value={data.text}
minRows={3}
spellCheck={false}
className={styles.field}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
onBlur={handleFieldBlur}
/>
<div className={styles.controls}>
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
<Button
positive
content={t('action.save')}
onMouseOver={handleControlMouseOver}
onMouseOut={handleControlMouseOut}
/>
</div>
</Form>
);
});
EditComment.propTypes = {
children: PropTypes.element.isRequired,
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
};
export default React.memo(EditComment);

View file

@ -0,0 +1,24 @@
.controls {
clear: both;
margin-top: 6px;
}
.field {
background: #fff !important;
border: 1px solid rgba(9, 45, 66, 0.13) !important;
border-radius: 3px !important;
box-sizing: border-box;
color: #333 !important;
display: block;
line-height: 1.4 !important;
font-size: 14px !important;
margin-bottom: 4px !important;
overflow: hidden;
padding: 8px 12px !important;
resize: none !important;
width: 100% !important;
}
.field:focus {
outline: none;
}

View file

@ -0,0 +1,91 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation, Trans } from 'react-i18next';
import { Comment } from 'semantic-ui-react';
import { ActionTypes } from '../../../constants/Enums';
import ItemComment from './ItemComment';
import User from '../../User';
import styles from './Item.module.css';
const Item = React.memo(({
type, data, createdAt, user,
}) => {
const [t] = useTranslation();
let contentNode;
switch (type) {
case ActionTypes.CREATE_CARD:
contentNode = (
<Trans
i18nKey="common.userAddedThisCardToList"
values={{
user: user.name,
list: data.list.name,
}}
>
<span className={styles.author}>{user.name}</span>
<span className={styles.text}>
{' added this card to '}
{data.list.name}
</span>
</Trans>
);
break;
case ActionTypes.MOVE_CARD:
contentNode = (
<Trans
i18nKey="common.userMovedThisCardFromListToList"
values={{
user: user.name,
fromList: data.fromList.name,
toList: data.toList.name,
}}
>
<span className={styles.author}>{user.name}</span>
<span className={styles.text}>
{' moved this card from '}
{data.fromList.name}
{' to '}
{data.toList.name}
</span>
</Trans>
);
break;
default:
contentNode = null;
}
return (
<Comment>
<span className={styles.user}>
<User name={user.name} avatar={user.avatar} />
</span>
<div className={classNames(styles.content)}>
<div>{contentNode}</div>
<span className={styles.date}>
{t('format:longDateTime', {
postProcess: 'formatDate',
value: createdAt,
})}
</span>
</div>
</Comment>
);
});
Item.Comment = ItemComment;
Item.propTypes = {
type: PropTypes.string.isRequired,
data: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
createdAt: PropTypes.instanceOf(Date).isRequired,
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
export default Item;

View file

@ -0,0 +1,31 @@
.author {
color: #17394d;
display: inline-block;
font-weight: bold;
line-height: 20px;
}
.content {
border-bottom: 1px solid #092d4221;
display: inline-block;
padding-bottom: 14px;
vertical-align: top;
width: calc(100% - 40px);
}
.date {
color: #6b808c;
display: inline-block;
font-size: 12px;
line-height: 20px;
}
.text {
line-height: 20px;
}
.user {
display: inline-block;
padding: 4px 8px 0 0;
vertical-align: top;
}

View file

@ -0,0 +1,87 @@
import React, { useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Comment } from 'semantic-ui-react';
import EditComment from './EditComment';
import User from '../../User';
import DeletePopup from '../../DeletePopup';
import styles from './ItemComment.module.css';
const ItemComment = React.memo(
({
data, createdAt, isPersisted, user, isEditable, onUpdate, onDelete,
}) => {
const [t] = useTranslation();
const editComment = useRef(null);
const handleEditClick = useCallback(() => {
editComment.current.open();
}, []);
return (
<Comment>
<span className={styles.user}>
<User name={user.name} avatar={user.avatar} />
</span>
<div className={classNames(styles.content)}>
<div className={styles.title}>
<span className={styles.author}>{user.name}</span>
<span className={styles.date}>
{t('format:longDateTime', {
postProcess: 'formatDate',
value: createdAt,
})}
</span>
</div>
<EditComment ref={editComment} defaultData={data} onUpdate={onUpdate}>
<>
<p className={styles.text}>{data.text}</p>
<Comment.Actions>
{user.isCurrent && (
<Comment.Action
as="button"
content={t('action.edit')}
disabled={!isPersisted}
onClick={handleEditClick}
/>
)}
{(user.isCurrent || isEditable) && (
<DeletePopup
title={t('common.deleteComment', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisComment')}
buttonContent={t('action.deleteComment')}
onConfirm={onDelete}
>
<Comment.Action
as="button"
content={t('action.delete')}
disabled={!isPersisted}
/>
</DeletePopup>
)}
</Comment.Actions>
</>
</EditComment>
</div>
</Comment>
);
},
);
ItemComment.propTypes = {
data: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
createdAt: PropTypes.instanceOf(Date).isRequired,
isPersisted: PropTypes.bool.isRequired,
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
isEditable: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
export default ItemComment;

View file

@ -0,0 +1,49 @@
.author {
color: #17394d;
display: inline-block;
font-weight: bold;
line-height: 20px;
margin-right: 8px;
}
.content {
border-bottom: 1px solid #092d4221;
display: inline-block;
padding-bottom: 14px;
vertical-align: top;
width: calc(100% - 40px);
}
.date {
color: #6b808c;
display: inline-block;
font-size: 12px;
line-height: 20px;
}
.text {
background-color: #fff;
border-radius: 0px 8px 8px;
box-shadow: 0 1px 2px -1px rgba(9, 45, 66, 0.25),
0 0 0 1px rgba(9, 45, 66, 0.08);
box-sizing: border-box;
color: #17394d;
display: inline-block;
margin: 1px 2px 4px 1px;
max-width: 100%;
overflow: hidden;
padding: 8px 12px;
text-overflow: ellipsis;
white-space: pre-line;
word-break: break-word;
}
.title {
padding-bottom: 4px;
}
.user {
display: inline-block;
padding: 4px 8px 0 0;
vertical-align: top;
}

View file

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

View file

@ -0,0 +1,388 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import {
Button, Grid, Icon, Modal,
} from 'semantic-ui-react';
import NameField from './NameField';
import EditDescription from './EditDescription';
import Tasks from './Tasks';
import Actions from './Actions';
import User from '../User';
import Label from '../Label';
import Deadline from '../Deadline';
import Timer from '../Timer';
import ProjectMembershipsPopup from '../ProjectMembershipsPopup';
import LabelsPopup from '../LabelsPopup';
import EditDeadlinePopup from '../EditDeadlinePopup';
import EditTimerPopup from '../EditTimerPopup';
import DeletePopup from '../DeletePopup';
import styles from './CardModal.module.css';
const CardModal = React.memo(
({
name,
description,
deadline,
timer,
isSubscribed,
isActionsFetching,
isAllActionsFetched,
users,
labels,
tasks,
actions,
allProjectMemberships,
allLabels,
isEditable,
onUpdate,
onDelete,
onUserAdd,
onUserRemove,
onLabelAdd,
onLabelRemove,
onLabelCreate,
onLabelUpdate,
onLabelDelete,
onTaskCreate,
onTaskUpdate,
onTaskDelete,
onActionsFetch,
onCommentActionCreate,
onCommentActionUpdate,
onCommentActionDelete,
onClose,
}) => {
const [t] = useTranslation();
const handleNameUpdate = useCallback(
(newName) => {
onUpdate({
name: newName,
});
},
[onUpdate],
);
const handleDescriptionUpdate = useCallback(
(newDescription) => {
onUpdate({
description: newDescription,
});
},
[onUpdate],
);
const handleDeadlineUpdate = useCallback(
(newDeadline) => {
onUpdate({
deadline: newDeadline,
});
},
[onUpdate],
);
const handleTimerUpdate = useCallback(
(newTimer) => {
onUpdate({
timer: newTimer,
});
},
[onUpdate],
);
const handleToggleSubscribeClick = useCallback(() => {
onUpdate({
isSubscribed: !isSubscribed,
});
}, [isSubscribed, onUpdate]);
const userIds = users.map((user) => user.id);
const labelIds = labels.map((label) => label.id);
return (
<Modal
open
closeIcon
size="small"
centered={false}
className={styles.wrapper}
onClose={onClose}
>
<Grid className={styles.grid}>
<Grid.Row className={styles.headerPadding}>
<Grid.Column width={16} className={styles.headerPadding}>
<div className={styles.headerWrapper}>
<Icon name="list alternate outline" className={styles.moduleIcon} />
<div className={styles.headerTitle}>
<NameField defaultValue={name} onUpdate={handleNameUpdate} />
</div>
</div>
</Grid.Column>
</Grid.Row>
<Grid.Row className={styles.modalPadding}>
<Grid.Column width={12} className={styles.contentPadding}>
{(users.length > 0 || labels.length > 0 || deadline || timer) && (
<div className={styles.moduleWrapper}>
{users.length > 0 && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.members', {
context: 'title',
})}
</div>
{users.map((user) => (
<span key={user.id} className={styles.attachment}>
<ProjectMembershipsPopup
items={allProjectMemberships}
currentUserIds={userIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
<User name={user.name} avatar={user.avatar} />
</ProjectMembershipsPopup>
</span>
))}
<ProjectMembershipsPopup
items={allProjectMemberships}
currentUserIds={userIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
<button
type="button"
className={classNames(styles.attachment, styles.deadline)}
>
<Icon name="add" size="small" className={styles.addAttachment} />
</button>
</ProjectMembershipsPopup>
</div>
)}
{labels.length > 0 && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.labels', {
context: 'title',
})}
</div>
{labels.map((label) => (
<span key={label.id} className={styles.attachment}>
<LabelsPopup
key={label.id}
items={allLabels}
currentIds={labelIds}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onDelete={onLabelDelete}
>
<Label name={label.name} color={label.color} />
</LabelsPopup>
</span>
))}
<LabelsPopup
items={allLabels}
currentIds={labelIds}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onDelete={onLabelDelete}
>
<button
type="button"
className={classNames(styles.attachment, styles.deadline)}
>
<Icon name="add" size="small" className={styles.addAttachment} />
</button>
</LabelsPopup>
</div>
)}
{deadline && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.deadline', {
context: 'title',
})}
</div>
<span className={styles.attachment}>
<EditDeadlinePopup defaultValue={deadline} onUpdate={handleDeadlineUpdate}>
<Deadline value={deadline} />
</EditDeadlinePopup>
</span>
</div>
)}
{timer && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.timer', {
context: 'title',
})}
</div>
<span className={styles.attachment}>
<EditTimerPopup defaultValue={timer} onUpdate={handleTimerUpdate}>
<Timer startedAt={timer.startedAt} total={timer.total} />
</EditTimerPopup>
</span>
</div>
)}
</div>
)}
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="align justify" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.description')}</div>
<EditDescription defaultValue={description} onUpdate={handleDescriptionUpdate}>
{description ? (
<button type="button" className={styles.descriptionText}>
{description}
</button>
) : (
<button type="button" className={styles.descriptionButton}>
<span className={styles.descriptionButtonText}>
{t('action.addMoreDetailedDescription')}
</span>
</button>
)}
</EditDescription>
</div>
</div>
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="check square outline" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.tasks')}</div>
<Tasks
items={tasks}
onCreate={onTaskCreate}
onUpdate={onTaskUpdate}
onDelete={onTaskDelete}
/>
</div>
</div>
<Actions
items={actions}
isFetching={isActionsFetching}
isAllFetched={isAllActionsFetched}
isEditable={isEditable}
onFetch={onActionsFetch}
onCommentCreate={onCommentActionCreate}
onCommentUpdate={onCommentActionUpdate}
onCommentDelete={onCommentActionDelete}
/>
</Grid.Column>
<Grid.Column width={4} className={styles.sidebarPadding}>
<div className={styles.actions}>
<span className={styles.actionsTitle}>{t('action.addToCard')}</span>
<ProjectMembershipsPopup
items={allProjectMemberships}
currentUserIds={userIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
<Button fluid className={styles.actionButton}>
<Icon name="user outline" className={styles.actionIcon} />
{t('common.members')}
</Button>
</ProjectMembershipsPopup>
<LabelsPopup
items={allLabels}
currentIds={labelIds}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onDelete={onLabelDelete}
>
<Button fluid className={styles.actionButton}>
<Icon name="bookmark outline" className={styles.actionIcon} />
{t('common.labels')}
</Button>
</LabelsPopup>
<EditDeadlinePopup defaultValue={deadline} onUpdate={handleDeadlineUpdate}>
<Button fluid className={styles.actionButton}>
<Icon name="calendar check outline" className={styles.actionIcon} />
{t('common.deadline')}
</Button>
</EditDeadlinePopup>
<EditTimerPopup defaultValue={timer} onUpdate={handleTimerUpdate}>
<Button fluid className={styles.actionButton}>
<Icon name="clock outline" className={styles.actionIcon} />
{t('common.timer')}
</Button>
</EditTimerPopup>
</div>
<div className={styles.actions}>
<span className={styles.actionsTitle}>{t('common.actions')}</span>
<Button fluid className={styles.actionButton} onClick={handleToggleSubscribeClick}>
<Icon name="paper plane outline" className={styles.actionIcon} />
{isSubscribed ? t('action.unsubscribe') : t('action.subscribe')}
</Button>
<DeletePopup
title={t('common.deleteCard', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisCard')}
buttonContent={t('action.deleteCard')}
onConfirm={onDelete}
>
<Button fluid className={styles.actionButton}>
<Icon name="trash alternate outline" className={styles.actionIcon} />
{t('action.delete')}
</Button>
</DeletePopup>
</div>
</Grid.Column>
</Grid.Row>
</Grid>
</Modal>
);
},
);
CardModal.propTypes = {
name: PropTypes.string.isRequired,
description: PropTypes.string,
deadline: PropTypes.instanceOf(Date),
timer: PropTypes.object, // eslint-disable-line react/forbid-prop-types
isSubscribed: PropTypes.bool.isRequired,
isActionsFetching: PropTypes.bool.isRequired,
isAllActionsFetched: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
tasks: PropTypes.array.isRequired,
actions: PropTypes.array.isRequired,
allProjectMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
isEditable: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
onLabelAdd: PropTypes.func.isRequired,
onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
onTaskCreate: PropTypes.func.isRequired,
onTaskUpdate: PropTypes.func.isRequired,
onTaskDelete: PropTypes.func.isRequired,
onActionsFetch: PropTypes.func.isRequired,
onCommentActionCreate: PropTypes.func.isRequired,
onCommentActionUpdate: PropTypes.func.isRequired,
onCommentActionDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
CardModal.defaultProps = {
description: undefined,
deadline: undefined,
timer: undefined,
};
export default CardModal;

Some files were not shown because too many files have changed in this diff Show more