mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 12:49:43 +02:00
Initial commit
This commit is contained in:
commit
36fe34e8e1
583 changed files with 91539 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