mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 20:59:44 +02:00
Initial commit
This commit is contained in:
commit
5ffef61fe7
613 changed files with 91659 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.DS_Store
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Maksim Eltyshev
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
68
README.md
Normal file
68
README.md
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
# Planka
|
||||||
|
|
||||||
|
A Trello-like application built with React and Redux.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Online demo is coming soon.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Create projects, boards, lists, cards, labels and tasks
|
||||||
|
- Add card members, track time, set a deadline, write comments
|
||||||
|
- Filter by members and labels
|
||||||
|
- User notifications
|
||||||
|
- Internationalization
|
||||||
|
|
||||||
|
### Tech stack
|
||||||
|
|
||||||
|
- React, Redux, Redux-Saga, Redux-ORM, Semantic UI React, react-beautiful-dnd
|
||||||
|
- Sails.js, Knex.js
|
||||||
|
- PostgreSQL
|
||||||
|
|
||||||
|
### Run in development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/meltyshev/planka.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a database and edit DATABASE_URL in .env file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:migrate
|
||||||
|
npm run db:seed
|
||||||
|
|
||||||
|
npm run start:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Client (second terminal window):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd client
|
||||||
|
|
||||||
|
npm install
|
||||||
|
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Demo user: demo@demo.demo demo
|
||||||
|
|
||||||
|
## Run in production
|
||||||
|
|
||||||
|
In progress..
|
||||||
|
|
||||||
|
## Todo
|
||||||
|
|
||||||
|
In progress..
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Planka is [MIT licensed](./LICENSE).
|
21
client/.gitignore
vendored
Executable file
21
client/.gitignore
vendored
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
1
client/README.md
Executable file
1
client/README.md
Executable file
|
@ -0,0 +1 @@
|
||||||
|
# Planka client
|
14258
client/package-lock.json
generated
Normal file
14258
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
70
client/package.json
Executable file
70
client/package.json
Executable file
|
@ -0,0 +1,70 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"eject": "react-scripts eject",
|
||||||
|
"lint": "eslint src",
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"test": "react-scripts test"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not ie <= 11",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"eslintConfig": {
|
||||||
|
"env": {
|
||||||
|
"browser": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"airbnb",
|
||||||
|
"airbnb/hooks"
|
||||||
|
],
|
||||||
|
"parser": "babel-eslint"
|
||||||
|
},
|
||||||
|
"eslintIgnore": [
|
||||||
|
"src/service-worker.js"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.19.0",
|
||||||
|
"classnames": "^2.2.6",
|
||||||
|
"connected-react-router": "^6.5.2",
|
||||||
|
"date-fns": "^2.0.1",
|
||||||
|
"dequal": "^1.0.0",
|
||||||
|
"history": "^4.9.0",
|
||||||
|
"i18next": "^17.0.13",
|
||||||
|
"i18next-browser-languagedetector": "^3.0.3",
|
||||||
|
"initials": "^3.0.1",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react": "^16.9.0",
|
||||||
|
"react-beautiful-dnd": "^11.0.5",
|
||||||
|
"react-datepicker": "^2.9.3",
|
||||||
|
"react-dom": "^16.9.0",
|
||||||
|
"react-i18next": "^10.12.2",
|
||||||
|
"react-input-mask": "^2.0.4",
|
||||||
|
"react-redux": "^7.1.1",
|
||||||
|
"react-router-dom": "^5.0.1",
|
||||||
|
"react-scripts": "3.1.1",
|
||||||
|
"react-textarea-autosize": "^7.1.0",
|
||||||
|
"redux": "^4.0.4",
|
||||||
|
"redux-logger": "^3.0.6",
|
||||||
|
"redux-orm": "^0.13.3",
|
||||||
|
"redux-saga": "^1.0.5",
|
||||||
|
"reselect": "^4.0.0",
|
||||||
|
"sails.io.js": "^1.2.1",
|
||||||
|
"semantic-ui-react": "^0.88.0",
|
||||||
|
"socket.io-client": "^2.2.0",
|
||||||
|
"validator": "^11.1.0",
|
||||||
|
"whatwg-fetch": "^3.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^6.2.2",
|
||||||
|
"eslint-config-airbnb": "^18.0.1",
|
||||||
|
"eslint-plugin-import": "^2.18.2",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||||
|
"eslint-plugin-react": "^7.14.3",
|
||||||
|
"eslint-plugin-react-hooks": "^2.0.1"
|
||||||
|
}
|
||||||
|
}
|
BIN
client/public/favicon.ico
Executable file
BIN
client/public/favicon.ico
Executable file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
30
client/public/index.html
Executable file
30
client/public/index.html
Executable file
|
@ -0,0 +1,30 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="theme-color" content="#000000">
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is added to the
|
||||||
|
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>Planka</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
You need to enable JavaScript to run this app.
|
||||||
|
</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
15
client/public/manifest.json
Executable file
15
client/public/manifest.json
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
24
client/src/actions/action.js
Normal file
24
client/src/actions/action.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
27
client/src/actions/actions.js
Normal file
27
client/src/actions/actions.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
9
client/src/actions/app.js
Normal file
9
client/src/actions/app.js
Normal 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
157
client/src/actions/board.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
62
client/src/actions/card-label.js
Normal file
62
client/src/actions/card-label.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
62
client/src/actions/card-membership.js
Normal file
62
client/src/actions/card-membership.js
Normal 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
139
client/src/actions/card.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
96
client/src/actions/comment-action.js
Normal file
96
client/src/actions/comment-action.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
7
client/src/actions/entry/actions.js
Executable file
7
client/src/actions/entry/actions.js
Executable 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: {},
|
||||||
|
});
|
31
client/src/actions/entry/board.js
Executable file
31
client/src/actions/entry/board.js
Executable 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,
|
||||||
|
},
|
||||||
|
});
|
45
client/src/actions/entry/card.js
Executable file
45
client/src/actions/entry/card.js
Executable 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: {},
|
||||||
|
});
|
23
client/src/actions/entry/comment-action.js
Executable file
23
client/src/actions/entry/comment-action.js
Executable 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,
|
||||||
|
},
|
||||||
|
});
|
13
client/src/actions/entry/index.js
Executable file
13
client/src/actions/entry/index.js
Executable 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';
|
67
client/src/actions/entry/label.js
Executable file
67
client/src/actions/entry/label.js
Executable 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,
|
||||||
|
},
|
||||||
|
});
|
31
client/src/actions/entry/list.js
Executable file
31
client/src/actions/entry/list.js
Executable 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,
|
||||||
|
},
|
||||||
|
});
|
18
client/src/actions/entry/login.js
Executable file
18
client/src/actions/entry/login.js
Executable 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: {},
|
||||||
|
});
|
21
client/src/actions/entry/modal.js
Executable file
21
client/src/actions/entry/modal.js
Executable 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: {},
|
||||||
|
});
|
9
client/src/actions/entry/notification.js
Executable file
9
client/src/actions/entry/notification.js
Executable 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,
|
||||||
|
},
|
||||||
|
});
|
15
client/src/actions/entry/project-membership.js
Executable file
15
client/src/actions/entry/project-membership.js
Executable 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,
|
||||||
|
},
|
||||||
|
});
|
20
client/src/actions/entry/project.js
Executable file
20
client/src/actions/entry/project.js
Executable 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: {},
|
||||||
|
});
|
23
client/src/actions/entry/task.js
Executable file
23
client/src/actions/entry/task.js
Executable 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,
|
||||||
|
},
|
||||||
|
});
|
86
client/src/actions/entry/user.js
Executable file
86
client/src/actions/entry/user.js
Executable 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,
|
||||||
|
},
|
||||||
|
});
|
21
client/src/actions/index.js
Normal file
21
client/src/actions/index.js
Normal 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
149
client/src/actions/label.js
Normal 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
117
client/src/actions/list.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
43
client/src/actions/login.js
Normal file
43
client/src/actions/login.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
15
client/src/actions/modal.js
Normal file
15
client/src/actions/modal.js
Normal 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: {},
|
||||||
|
});
|
20
client/src/actions/notification.js
Normal file
20
client/src/actions/notification.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
56
client/src/actions/notifications.js
Normal file
56
client/src/actions/notifications.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
80
client/src/actions/project-membership.js
Normal file
80
client/src/actions/project-membership.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
120
client/src/actions/project.js
Normal file
120
client/src/actions/project.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
25
client/src/actions/projects.js
Normal file
25
client/src/actions/projects.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
18
client/src/actions/socket.js
Normal file
18
client/src/actions/socket.js
Normal 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
117
client/src/actions/task.js
Normal 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
193
client/src/actions/user.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
22
client/src/actions/users.js
Normal file
22
client/src/actions/users.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
9
client/src/api/access-tokens.js
Executable file
9
client/src/api/access-tokens.js
Executable 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
35
client/src/api/actions.js
Executable 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
25
client/src/api/boards.js
Executable 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,
|
||||||
|
};
|
12
client/src/api/card-labels.js
Normal file
12
client/src/api/card-labels.js
Normal 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,
|
||||||
|
};
|
12
client/src/api/card-memberships.js
Normal file
12
client/src/api/card-memberships.js
Normal 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
70
client/src/api/cards.js
Executable 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,
|
||||||
|
};
|
25
client/src/api/comment-actions.js
Executable file
25
client/src/api/comment-actions.js
Executable 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
35
client/src/api/http.js
Executable 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
35
client/src/api/index.js
Executable 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
15
client/src/api/labels.js
Executable 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
15
client/src/api/lists.js
Executable 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
35
client/src/api/notifications.js
Executable 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,
|
||||||
|
};
|
12
client/src/api/project-memberships.js
Executable file
12
client/src/api/project-memberships.js
Executable 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
18
client/src/api/projects.js
Executable 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
37
client/src/api/socket.js
Executable 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
15
client/src/api/tasks.js
Executable 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
31
client/src/api/users.js
Executable 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,
|
||||||
|
};
|
BIN
client/src/assets/images/cover.jpg
Normal file
BIN
client/src/assets/images/cover.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 559 KiB |
3
client/src/assets/images/plus-icon.svg
Executable file
3
client/src/assets/images/plus-icon.svg
Executable 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 |
3
client/src/assets/images/plus-math-icon.svg
Executable file
3
client/src/assets/images/plus-math-icon.svg
Executable 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 |
85
client/src/components/AddProjectModal/AddProjectModal.jsx
Executable file
85
client/src/components/AddProjectModal/AddProjectModal.jsx
Executable 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;
|
|
@ -0,0 +1,3 @@
|
||||||
|
.field {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
3
client/src/components/AddProjectModal/index.js
Normal file
3
client/src/components/AddProjectModal/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import AddProjectModal from './AddProjectModal';
|
||||||
|
|
||||||
|
export default AddProjectModal;
|
148
client/src/components/AddUserPopup/AddUserPopup.jsx
Executable file
148
client/src/components/AddUserPopup/AddUserPopup.jsx
Executable 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);
|
10
client/src/components/AddUserPopup/AddUserPopup.module.css
Normal file
10
client/src/components/AddUserPopup/AddUserPopup.module.css
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.field {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: #444444;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
3
client/src/components/AddUserPopup/index.js
Normal file
3
client/src/components/AddUserPopup/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import AddUserPopup from './AddUserPopup';
|
||||||
|
|
||||||
|
export default AddUserPopup;
|
23
client/src/components/App.jsx
Executable file
23
client/src/components/App.jsx
Executable 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;
|
19
client/src/components/AppWrapper.jsx
Executable file
19
client/src/components/AppWrapper.jsx
Executable 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;
|
129
client/src/components/Board/AddList.jsx
Executable file
129
client/src/components/Board/AddList.jsx
Executable 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);
|
31
client/src/components/Board/AddList.module.css
Normal file
31
client/src/components/Board/AddList.module.css
Normal 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;
|
||||||
|
}
|
138
client/src/components/Board/Board.jsx
Executable file
138
client/src/components/Board/Board.jsx
Executable 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;
|
52
client/src/components/Board/Board.module.css
Normal file
52
client/src/components/Board/Board.module.css
Normal 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%;
|
||||||
|
}
|
120
client/src/components/Board/Filter.jsx
Normal file
120
client/src/components/Board/Filter.jsx
Normal 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;
|
51
client/src/components/Board/Filter.module.css
Normal file
51
client/src/components/Board/Filter.module.css
Normal 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;
|
||||||
|
}
|
3
client/src/components/Board/index.js
Executable file
3
client/src/components/Board/index.js
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
import Board from './Board';
|
||||||
|
|
||||||
|
export default Board;
|
19
client/src/components/BoardWrapper.jsx
Executable file
19
client/src/components/BoardWrapper.jsx
Executable 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;
|
69
client/src/components/Boards/AddPopup.jsx
Executable file
69
client/src/components/Boards/AddPopup.jsx
Executable 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);
|
3
client/src/components/Boards/AddPopup.module.css
Normal file
3
client/src/components/Boards/AddPopup.module.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.field {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
183
client/src/components/Boards/Boards.jsx
Executable file
183
client/src/components/Boards/Boards.jsx
Executable 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;
|
115
client/src/components/Boards/Boards.module.css
Normal file
115
client/src/components/Boards/Boards.module.css
Normal 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;
|
||||||
|
}
|
108
client/src/components/Boards/EditPopup.jsx
Executable file
108
client/src/components/Boards/EditPopup.jsx
Executable 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);
|
10
client/src/components/Boards/EditPopup.module.css
Normal file
10
client/src/components/Boards/EditPopup.module.css
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.deleteButton {
|
||||||
|
bottom: 12px;
|
||||||
|
box-shadow: 0 1px 0 #cbcccc;
|
||||||
|
position: absolute;
|
||||||
|
right: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
3
client/src/components/Boards/index.js
Executable file
3
client/src/components/Boards/index.js
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
import Boards from './Boards';
|
||||||
|
|
||||||
|
export default Boards;
|
216
client/src/components/Card/ActionsPopup.jsx
Normal file
216
client/src/components/Card/ActionsPopup.jsx
Normal 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);
|
9
client/src/components/Card/ActionsPopup.module.css
Normal file
9
client/src/components/Card/ActionsPopup.module.css
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.menu {
|
||||||
|
margin: -7px -12px -5px !important;
|
||||||
|
width: calc(100% + 24px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuItem {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding-left: 14px !important;
|
||||||
|
}
|
201
client/src/components/Card/Card.jsx
Executable file
201
client/src/components/Card/Card.jsx
Executable 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;
|
112
client/src/components/Card/Card.module.css
Normal file
112
client/src/components/Card/Card.module.css
Normal 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;
|
||||||
|
}
|
128
client/src/components/Card/EditName.jsx
Normal file
128
client/src/components/Card/EditName.jsx
Normal 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);
|
24
client/src/components/Card/EditName.module.css
Normal file
24
client/src/components/Card/EditName.module.css
Normal 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;
|
||||||
|
}
|
69
client/src/components/Card/Tasks.jsx
Normal file
69
client/src/components/Card/Tasks.jsx
Normal 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;
|
83
client/src/components/Card/Tasks.module.css
Normal file
83
client/src/components/Card/Tasks.module.css
Normal 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;
|
||||||
|
}
|
3
client/src/components/Card/index.js
Normal file
3
client/src/components/Card/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import Card from './Card';
|
||||||
|
|
||||||
|
export default Card;
|
101
client/src/components/CardModal/Actions/Actions.jsx
Executable file
101
client/src/components/CardModal/Actions/Actions.jsx
Executable 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;
|
38
client/src/components/CardModal/Actions/Actions.module.css
Normal file
38
client/src/components/CardModal/Actions/Actions.module.css
Normal 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;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue