Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c658665ee8 | ||
|
|
a37bad4867 | ||
|
|
738ed19e7f | ||
|
|
9ce8b2ab9c | ||
|
|
a8d312922a | ||
|
|
90c8b4d548 | ||
|
|
4941b51fa4 | ||
|
|
a858419f3c | ||
|
|
e28c06b85a | ||
|
|
fa2d73a2c5 | ||
|
|
80706bbdd5 | ||
|
|
db4e5a89f9 | ||
|
|
0266e4bfe2 | ||
|
|
02c26cfb4d | ||
|
|
ef096499a2 | ||
|
|
a007e1d065 | ||
|
|
c181731870 | ||
|
|
54c57a08c3 | ||
|
|
65abd7c9ab | ||
|
|
5fc811d2ef | ||
|
|
5afd4af897 | ||
|
|
ac0a3d9ae7 | ||
|
|
b0d23373c0 | ||
|
|
5381d45e50 | ||
|
|
a43af8eac5 | ||
|
|
409a7cde6b | ||
|
|
f62cba9bcd | ||
|
|
9495234066 | ||
|
|
5dcbad7b5a | ||
|
|
064b37050c | ||
|
|
bf8b8474ec | ||
|
|
37a9c9bb42 | ||
|
|
00fccbaa3d | ||
|
|
b3323308c1 | ||
|
|
9108dc5b3e | ||
|
|
513f9a8ee5 | ||
|
|
e051f624a8 | ||
|
|
172f4fcf34 | ||
|
|
9f16881965 | ||
|
|
4e876922f8 | ||
|
|
d82d928e29 | ||
|
|
94dc83706c | ||
|
|
b08e731419 | ||
|
|
1ce0fac281 | ||
|
|
e3d00c8791 | ||
|
|
e1a0b635ea | ||
|
|
cc7959bb15 | ||
|
|
56044e6d8a | ||
|
|
e5bd73f33e | ||
|
|
e65ab6ece7 | ||
|
|
14cc55a8c0 | ||
|
|
b4306d0a4b | ||
|
|
283c850ebf | ||
|
|
62de71bd97 | ||
|
|
447f88880d | ||
|
|
8109936ce2 | ||
|
|
640908320a | ||
|
|
236c1020b3 | ||
|
|
3df07c10fa | ||
|
|
543a992d98 | ||
|
|
9cde4e63f9 | ||
|
|
95738ee7d3 | ||
|
|
bb7cdf5dd4 | ||
|
|
253af55790 | ||
|
|
62c3cca9a7 | ||
|
|
b476a19b79 | ||
|
|
3379c65a87 | ||
|
|
1a6fc40804 | ||
|
|
42092efcd2 | ||
|
|
5c91bddfe7 |
+23
-5
@@ -1,11 +1,29 @@
|
||||
*/node_modules
|
||||
*/.gitignore
|
||||
*/README.md
|
||||
*/.gitignore
|
||||
*/node_modules
|
||||
|
||||
**/.DS_Store
|
||||
**/.gitkeep
|
||||
|
||||
server/**/.gitkeep
|
||||
|
||||
server/.env
|
||||
server/.editorconfig
|
||||
server/.eslintignore
|
||||
server/.npmrc
|
||||
server/test
|
||||
server/logs
|
||||
server/.tmp
|
||||
|
||||
server/views/*
|
||||
|
||||
server/public/*
|
||||
server/views/*
|
||||
server/logs
|
||||
!server/public/user-avatars
|
||||
server/public/user-avatars/*
|
||||
!server/public/project-background-images
|
||||
server/public/project-background-images/*
|
||||
|
||||
server/private/*
|
||||
!server/private/attachments
|
||||
server/private/attachments/*
|
||||
|
||||
client/build
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
ALPINE_VERSION: 3.15
|
||||
ALPINE_VERSION: 3.16
|
||||
|
||||
jobs:
|
||||
build-and-push-docker-base-image:
|
||||
|
||||
@@ -3,3 +3,10 @@ docker-compose.override.yml
|
||||
|
||||
.idea
|
||||
.DS_Store
|
||||
|
||||
# Prevent another lockfile than package-lock.json (npm) from being created
|
||||
# If some case you are using pnpm or yarn, don't forget to generate npm lockfile
|
||||
# before commiting your code by running:
|
||||
# `npm i --package-lock-only`
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
+22
-14
@@ -1,32 +1,40 @@
|
||||
FROM node:lts AS client-builder
|
||||
FROM ghcr.io/plankanban/planka:base-latest as server-dependencies
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY client/package.json client/package-lock.json ./
|
||||
COPY server/package.json server/package-lock.json .
|
||||
|
||||
RUN npm install npm@latest --global \
|
||||
&& npm install
|
||||
&& npm clean-install --omit=dev
|
||||
|
||||
FROM node:lts AS client
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY client/package.json client/package-lock.json .
|
||||
|
||||
RUN npm install npm@latest --global \
|
||||
&& npm clean-install --omit=dev
|
||||
|
||||
COPY client .
|
||||
RUN npm run build
|
||||
RUN DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
|
||||
FROM ghcr.io/plankanban/planka:base-latest
|
||||
|
||||
RUN apk del vips-dependencies --purge
|
||||
|
||||
USER node
|
||||
WORKDIR /app
|
||||
|
||||
COPY server/.npmrc server/package.json server/package-lock.json ./
|
||||
COPY --chown=node:node docker-start.sh start.sh
|
||||
COPY --chown=node:node server .
|
||||
|
||||
RUN npm install npm@latest --global \
|
||||
&& npm install --production \
|
||||
&& apk del vips-dependencies --purge
|
||||
RUN mv .env.sample .env
|
||||
|
||||
COPY docker-start.sh start.sh
|
||||
COPY server .
|
||||
COPY --from=server-dependencies --chown=node:node /app/node_modules node_modules
|
||||
|
||||
RUN cp .env.sample .env
|
||||
|
||||
COPY --from=client-builder /app/build public
|
||||
COPY --from=client-builder /app/build/index.html views
|
||||
COPY --from=client --chown=node:node /app/build public
|
||||
COPY --from=client --chown=node:node /app/build/index.html views/index.ejs
|
||||
|
||||
VOLUME /app/public/user-avatars
|
||||
VOLUME /app/public/project-background-images
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
FROM node:lts-alpine
|
||||
|
||||
ARG ALPINE_VERSION=3.15
|
||||
ARG VIPS_VERSION=8.12.2
|
||||
ARG ALPINE_VERSION=3.16
|
||||
ARG VIPS_VERSION=8.13.3
|
||||
|
||||
RUN apk -U upgrade \
|
||||
&& apk add \
|
||||
|
||||
@@ -21,9 +21,14 @@ A Trello-like kanban board built with React and Redux.
|
||||
|
||||
## Deploy
|
||||
|
||||
### Docker Compose
|
||||
There are 2 types of installation:
|
||||
1. [Dockerized](#1-docker-compose)
|
||||
2. [Without Docker](#2-without-docker)
|
||||
|
||||
[](https://hub.docker.com/r/meltyshev/planka)
|
||||
|
||||
### 1. Docker Compose
|
||||
|
||||
[](https://github.com/plankanban/planka/pkgs/container/planka)
|
||||
|
||||
- Make sure you have [Docker](https://docs.docker.com/install/) and [Docker Compose](https://docs.docker.com/compose/install/) installed and operational.
|
||||
- Create `docker-compose.yml` based on [the example](https://raw.githubusercontent.com/plankanban/planka/master/docker-compose.yml). This is the ONLY file you will need. You can create this file on your own machine by copy and pasting the content.
|
||||
@@ -44,9 +49,146 @@ docker-compose up -d
|
||||
|
||||
Demo user: demo@demo.demo demo
|
||||
|
||||
### 2. Without Docker
|
||||
|
||||
Installing without Docker is a bit more complicated, here's what you need to do:
|
||||
|
||||
1. Clone this repository into a directory of your choice. (e.g. `/var/www/planka`)
|
||||
|
||||
```bash
|
||||
mkdir -p /var/www/planka
|
||||
cd /var/www/planka
|
||||
git clone https://github.com/plankanban/planka.git .
|
||||
```
|
||||
|
||||
2. Install dependencies for client and build it.
|
||||
|
||||
```bash
|
||||
cd client
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Note**: You can use `yarn` or `pnpm` instead of `npm`.
|
||||
|
||||
3. Copy the `build` directory to the `server/public` directory.
|
||||
|
||||
```bash
|
||||
cp -r build ../server/public
|
||||
cp build/index.html ../server/views/index.ejs
|
||||
```
|
||||
|
||||
4. Install dependencies for server.
|
||||
|
||||
```bash
|
||||
cd ../server
|
||||
npm install
|
||||
```
|
||||
|
||||
5. Configure environment variables.
|
||||
|
||||
```bash
|
||||
cp .env.sample .env
|
||||
|
||||
# Edit .env file (You could use nano, vim, etc.)
|
||||
nano .env
|
||||
```
|
||||
|
||||
**Note**: Before continuing, make sure you have your selected database created and running.
|
||||
|
||||
6. Copy start script from the root directory to the `server` directory.
|
||||
|
||||
```bash
|
||||
cp ../docker-start.sh start.sh
|
||||
```
|
||||
|
||||
7. Start the server.
|
||||
|
||||
```bash
|
||||
./start.sh
|
||||
```
|
||||
|
||||
**Note**: You can use `pm2` or `systemd` to run the server in the background.
|
||||
|
||||
## Additional information
|
||||
|
||||
### Nginx configuration
|
||||
|
||||
Here is an example of Nginx configuration for Planka, make sure to replace `<domain>` with your domain name, and make sure to configure the SSL.
|
||||
|
||||
```nginx
|
||||
upstream planka {
|
||||
server localhost:1337;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name <domain>;
|
||||
|
||||
access_log /var/log/nginx/planka-access.log;
|
||||
error_log /var/log/nginx/planka-error.log error;
|
||||
|
||||
# SSL Configuration - Replace the example <domain> with your domain
|
||||
ssl_certificate /etc/letsencrypt/live/<domain>/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/<domain>/privkey.pem;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
client_max_body_size 120M;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Max-Age 3600;
|
||||
add_header Access-Control-Expose-Headers Content-Length;
|
||||
add_header Access-Control-Allow-Headers Range;
|
||||
|
||||
# Make sure to allow socket.io connections
|
||||
location ~* \.io {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
client_max_body_size 50M;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Frame-Options SAMEORIGIN;
|
||||
proxy_buffers 256 16k;
|
||||
proxy_buffer_size 16k;
|
||||
client_body_timeout 60;
|
||||
send_timeout 300;
|
||||
lingering_timeout 5;
|
||||
proxy_connect_timeout 1d;
|
||||
proxy_send_timeout 1d;
|
||||
proxy_read_timeout 1d;
|
||||
proxy_pass http://planka;
|
||||
}
|
||||
|
||||
location / {
|
||||
client_max_body_size 50M;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Frame-Options SAMEORIGIN;
|
||||
proxy_buffers 256 16k;
|
||||
proxy_buffer_size 16k;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_cache_revalidate on;
|
||||
proxy_cache_min_uses 2;
|
||||
proxy_cache_use_stale timeout;
|
||||
proxy_cache_lock on;
|
||||
proxy_http_version 1.1;
|
||||
proxy_pass http://planka;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
Planka currently allows you to expose the applciation's logfile directory to the host machine via a shared volume. This feature is not enabled by default.
|
||||
Planka currently allows you to expose the application's logfile directory to the host machine via a shared volume. This feature is not enabled by default.
|
||||
|
||||
To expose the logfile director to the host machine, add the item `./logs/:/app/logs/` under `services.planka.volumes`.
|
||||
|
||||
@@ -117,6 +259,10 @@ Restart the fail2ban service. You can check the status of your Planka jail by ru
|
||||
fail2ban-client status planka
|
||||
```
|
||||
|
||||
### Import from Trello
|
||||
|
||||
It's already available in Planka, or you can also use the great tool [trello2planka](https://github.com/christophenne/trello2planka) to do the import.
|
||||
|
||||
## Development
|
||||
|
||||
Clone the repository and install dependencies:
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BASE_URL_PLACEHOLDER = 'BASE_URL_PLACEHOLDER';
|
||||
|
||||
const replaceInFile = (file, search, replace) => {
|
||||
fs.readFile(file, 'utf8', (readError, data) => {
|
||||
if (readError) {
|
||||
throw new Error(`${readError}`);
|
||||
}
|
||||
const res = data.replaceAll(search, replace);
|
||||
fs.writeFile(file, res, 'utf8', (writeError) => {
|
||||
if (writeError) {
|
||||
throw new Error(`${writeError}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const replaceBaseUrl = (compiler) => {
|
||||
compiler.hooks.assetEmitted.tap('ReplaceBaseUrlPlaceholder', (file, info) => {
|
||||
if (info.content.indexOf(BASE_URL_PLACEHOLDER) >= 0) {
|
||||
if (/\.css$/.exec(info.targetPath)) {
|
||||
// For CSS 'url(...)' import we can use relative import
|
||||
const relPath = path.relative(path.dirname(info.targetPath), info.outputPath);
|
||||
replaceInFile(info.targetPath, BASE_URL_PLACEHOLDER, `${relPath}/`);
|
||||
} else if (/\.js$/.exec(info.targetPath)) {
|
||||
// For JS 'import ... from "some-asset"' we can get the variable injected in the window object
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
replaceInFile(info.targetPath, `"${BASE_URL_PLACEHOLDER}"`, '`${window.BASE_URL}/`');
|
||||
} else if (/index\.html$/.exec(info.targetPath)) {
|
||||
// For the main html file, we set a placeholder for sails to inject the correct value as runtime
|
||||
replaceInFile(info.targetPath, BASE_URL_PLACEHOLDER, '<%= BASE_URL %>');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = function override(config, env) {
|
||||
if (env === 'production') {
|
||||
const plugins = config.plugins.map((plugin) => {
|
||||
if (plugin.constructor.name === 'InterpolateHtmlPlugin') {
|
||||
const newPlugin = plugin;
|
||||
newPlugin.replacements.PUBLIC_URL = BASE_URL_PLACEHOLDER;
|
||||
return newPlugin;
|
||||
}
|
||||
return plugin;
|
||||
});
|
||||
return {
|
||||
...config,
|
||||
output: { ...config.output, publicPath: BASE_URL_PLACEHOLDER },
|
||||
plugins: [...plugins, { apply: replaceBaseUrl }],
|
||||
};
|
||||
}
|
||||
return config;
|
||||
};
|
||||
Generated
+11648
-14810
File diff suppressed because it is too large
Load Diff
+38
-47
@@ -2,11 +2,11 @@
|
||||
"name": "planka-client",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "react-scripts build",
|
||||
"build": "react-app-rewired build",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint --ext js,jsx src",
|
||||
"start": "react-scripts start",
|
||||
"test": "react-scripts test"
|
||||
"lint": "eslint --ext js,jsx src config-overrides.js",
|
||||
"start": "react-app-rewired start",
|
||||
"test": "react-app-rewired test"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -34,15 +34,13 @@
|
||||
},
|
||||
"requireConfigFile": false
|
||||
},
|
||||
"plugins": [
|
||||
"prettier"
|
||||
],
|
||||
"extends": [
|
||||
"airbnb",
|
||||
"airbnb/hooks",
|
||||
"plugin:prettier/recommended"
|
||||
"../package.json"
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-vars": "warn",
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{
|
||||
@@ -51,73 +49,66 @@
|
||||
"**/*.test.js"
|
||||
]
|
||||
}
|
||||
],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.3.1",
|
||||
"connected-react-router": "^6.9.3",
|
||||
"date-fns": "^2.29.1",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"classnames": "^2.3.2",
|
||||
"date-fns": "^2.29.3",
|
||||
"dequal": "^2.0.3",
|
||||
"history": "^4.10.1",
|
||||
"i18next": "^21.8.14",
|
||||
"i18next-browser-languagedetector": "^6.1.4",
|
||||
"easymde": "^2.18.0",
|
||||
"history": "^5.3.0",
|
||||
"i18next": "^22.0.6",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"initials": "^3.1.2",
|
||||
"js-cookie": "^3.0.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"node-sass": "^7.0.1",
|
||||
"photoswipe": "^5.3.0",
|
||||
"node-sass": "^8.0.0",
|
||||
"photoswipe": "^5.3.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-datepicker": "^4.8.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-i18next": "^11.18.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-i18next": "^12.0.0",
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-photoswipe-gallery": "^2.2.1",
|
||||
"react-redux": "^7.2.8",
|
||||
"react-router-dom": "^5.3.1",
|
||||
"react-photoswipe-gallery": "^2.2.2",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.4.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-textarea-autosize": "^8.3.4",
|
||||
"react-simplemde-editor": "^5.2.0",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
"redux": "^4.2.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-orm": "^0.16.2",
|
||||
"redux-saga": "^1.1.3",
|
||||
"redux-saga": "^1.2.1",
|
||||
"remark-breaks": "^3.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"reselect": "^4.1.6",
|
||||
"reselect": "^4.1.7",
|
||||
"sails.io.js": "^1.2.1",
|
||||
"semantic-ui-react": "^2.1.3",
|
||||
"socket.io-client": "^2.3.1",
|
||||
"socket.io-client": "^2.5.0",
|
||||
"validator": "^13.7.0",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
"whatwg-fetch": "^3.6.2",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"babel-preset-airbnb": "^5.0.0",
|
||||
"chai": "^4.3.6",
|
||||
"enzyme": "^3.11.0",
|
||||
"eslint": "^8.20.0",
|
||||
"chai": "^4.3.7",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"jest-enzyme": "^7.1.2",
|
||||
"prettier": "2.7.1",
|
||||
"react-test-renderer": "^17.0.2"
|
||||
"react-test-renderer": "^18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
-->
|
||||
<title>Planka</title>
|
||||
</head>
|
||||
<script>window.BASE_URL = "%PUBLIC_URL%";</script>
|
||||
<body id="app">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -44,6 +44,11 @@ const logout = () => ({
|
||||
payload: {},
|
||||
});
|
||||
|
||||
logout.invalidateAccessToken = () => ({
|
||||
type: ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
export default {
|
||||
initializeCore,
|
||||
logout,
|
||||
|
||||
@@ -103,10 +103,11 @@ const updateUserPassword = (id, data) => ({
|
||||
},
|
||||
});
|
||||
|
||||
updateUserPassword.success = (user) => ({
|
||||
updateUserPassword.success = (user, accessToken) => ({
|
||||
type: ActionTypes.USER_PASSWORD_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
user,
|
||||
accessToken,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import http from './http';
|
||||
import socket from './socket';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
|
||||
|
||||
const deleteCurrentAccessToken = (headers) =>
|
||||
socket.delete('/access-tokens/me', undefined, headers);
|
||||
|
||||
export default {
|
||||
createAccessToken,
|
||||
deleteCurrentAccessToken,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import socket from './socket';
|
||||
import http from './http';
|
||||
import { transformCard } from './cards';
|
||||
import { transformAttachment } from './attachments';
|
||||
|
||||
@@ -7,6 +8,9 @@ import { transformAttachment } from './attachments';
|
||||
const createBoard = (projectId, data, headers) =>
|
||||
socket.post(`/projects/${projectId}/boards`, data, headers);
|
||||
|
||||
const createBoardWithImport = (projectId, data, requestId, headers) =>
|
||||
http.post(`/projects/${projectId}/boards?requestId=${requestId}`, data, headers);
|
||||
|
||||
const getBoard = (id, headers) =>
|
||||
socket.get(`/boards/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
@@ -23,6 +27,7 @@ const deleteBoard = (id, headers) => socket.delete(`/boards/${id}`, undefined, h
|
||||
|
||||
export default {
|
||||
createBoard,
|
||||
createBoardWithImport,
|
||||
getBoard,
|
||||
updateBoard,
|
||||
deleteBoard,
|
||||
|
||||
@@ -5,7 +5,7 @@ import Config from '../constants/Config';
|
||||
|
||||
const io = sailsIOClient(socketIOClient);
|
||||
|
||||
io.sails.url = Config.SERVER_BASE_URL;
|
||||
io.sails.url = Config.SERVER_HOST_NAME;
|
||||
io.sails.autoConnect = false;
|
||||
io.sails.reconnection = true;
|
||||
io.sails.useCORSRouteToGetCookie = false;
|
||||
@@ -13,6 +13,7 @@ io.sails.environment = process.env.NODE_ENV;
|
||||
|
||||
const { socket } = io;
|
||||
|
||||
socket.path = `${Config.BASE_PATH}/socket.io`;
|
||||
socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
|
||||
|
||||
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -14,6 +14,7 @@ const BoardActions = React.memo(
|
||||
filterUsers,
|
||||
filterLabels,
|
||||
allUsers,
|
||||
canEdit,
|
||||
canEditMemberships,
|
||||
onMembershipCreate,
|
||||
onMembershipUpdate,
|
||||
@@ -27,6 +28,7 @@ const BoardActions = React.memo(
|
||||
onLabelDelete,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.actions}>
|
||||
<div className={styles.action}>
|
||||
<Memberships
|
||||
@@ -45,6 +47,7 @@ const BoardActions = React.memo(
|
||||
labels={filterLabels}
|
||||
allBoardMemberships={memberships}
|
||||
allLabels={labels}
|
||||
canEdit={canEdit}
|
||||
onUserAdd={onUserToFilterAdd}
|
||||
onUserRemove={onUserFromFilterRemove}
|
||||
onLabelAdd={onLabelToFilterAdd}
|
||||
@@ -55,6 +58,7 @@ const BoardActions = React.memo(
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -67,6 +71,7 @@ BoardActions.propTypes = {
|
||||
filterLabels: PropTypes.array.isRequired,
|
||||
allUsers: PropTypes.array.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
canEditMemberships: PropTypes.bool.isRequired,
|
||||
onMembershipCreate: PropTypes.func.isRequired,
|
||||
onMembershipUpdate: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
:global(#app) {
|
||||
.action {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
@@ -8,4 +9,15 @@
|
||||
display: flex;
|
||||
margin: 20px 20px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ const Filters = React.memo(
|
||||
labels,
|
||||
allBoardMemberships,
|
||||
allLabels,
|
||||
canEdit,
|
||||
onUserAdd,
|
||||
onUserRemove,
|
||||
onLabelAdd,
|
||||
@@ -74,6 +75,7 @@ const Filters = React.memo(
|
||||
title={t('common.filterByLabels', {
|
||||
context: 'title',
|
||||
})}
|
||||
canEdit={canEdit}
|
||||
onSelect={onLabelAdd}
|
||||
onDeselect={onLabelRemove}
|
||||
onCreate={onLabelCreate}
|
||||
@@ -108,6 +110,7 @@ Filters.propTypes = {
|
||||
allBoardMemberships: PropTypes.array.isRequired,
|
||||
allLabels: PropTypes.array.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
onUserAdd: PropTypes.func.isRequired,
|
||||
onUserRemove: PropTypes.func.isRequired,
|
||||
onLabelAdd: PropTypes.func.isRequired,
|
||||
|
||||
+3
-3
@@ -19,7 +19,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
|
||||
...defaultData,
|
||||
}));
|
||||
|
||||
const handleRoleSelectClick = useCallback((role) => {
|
||||
const handleSelectRoleClick = useCallback((role) => {
|
||||
setData((prevData) => ({
|
||||
...prevData,
|
||||
role,
|
||||
@@ -50,7 +50,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
<Menu.Item
|
||||
active={data.role === BoardMembershipRoles.EDITOR}
|
||||
onClick={() => handleRoleSelectClick(BoardMembershipRoles.EDITOR)}
|
||||
onClick={() => handleSelectRoleClick(BoardMembershipRoles.EDITOR)}
|
||||
>
|
||||
<div className={styles.menuItemTitle}>{t('common.editor')}</div>
|
||||
<div className={styles.menuItemDescription}>
|
||||
@@ -59,7 +59,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
active={data.role === BoardMembershipRoles.VIEWER}
|
||||
onClick={() => handleRoleSelectClick(BoardMembershipRoles.VIEWER)}
|
||||
onClick={() => handleSelectRoleClick(BoardMembershipRoles.VIEWER)}
|
||||
>
|
||||
<div className={styles.menuItemTitle}>{t('common.viewer')}</div>
|
||||
<div className={styles.menuItemDescription}>{t('common.canOnlyViewBoard')}</div>
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
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 { useForm } from '../../hooks';
|
||||
|
||||
import styles from './AddPopup.module.scss';
|
||||
|
||||
const AddStep = React.memo(({ onCreate, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const [data, handleFieldChange] = useForm({
|
||||
name: '',
|
||||
});
|
||||
|
||||
const nameField = useRef(null);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const cleanData = {
|
||||
...data,
|
||||
type: 'kanban',
|
||||
name: data.name.trim(),
|
||||
};
|
||||
|
||||
if (!cleanData.name) {
|
||||
nameField.current.select();
|
||||
return;
|
||||
}
|
||||
|
||||
onCreate(cleanData);
|
||||
onClose();
|
||||
}, [onCreate, onClose, data]);
|
||||
|
||||
useEffect(() => {
|
||||
nameField.current.focus();
|
||||
}, []);
|
||||
|
||||
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);
|
||||
@@ -1,5 +0,0 @@
|
||||
:global(#app) {
|
||||
.field {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Icon } from 'semantic-ui-react';
|
||||
import { useDidUpdate, useToggle } from '../../../lib/hooks';
|
||||
import { withPopup } from '../../../lib/popup';
|
||||
import { Input, Popup } from '../../../lib/custom-ui';
|
||||
|
||||
import { useForm, useSteps } from '../../../hooks';
|
||||
import ImportStep from './ImportStep';
|
||||
|
||||
import styles from './AddPopup.module.scss';
|
||||
|
||||
const StepTypes = {
|
||||
IMPORT: 'IMPORT',
|
||||
};
|
||||
|
||||
const AddStep = React.memo(({ onCreate, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const [data, handleFieldChange, setData] = useForm({
|
||||
name: '',
|
||||
import: null,
|
||||
});
|
||||
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
const [focusNameFieldState, focusNameField] = useToggle();
|
||||
|
||||
const nameField = useRef(null);
|
||||
|
||||
const handleImportSelect = useCallback(
|
||||
(nextImport) => {
|
||||
setData((prevData) => ({
|
||||
...prevData,
|
||||
import: nextImport,
|
||||
}));
|
||||
},
|
||||
[setData],
|
||||
);
|
||||
|
||||
const handleImportBack = useCallback(() => {
|
||||
handleBack();
|
||||
focusNameField();
|
||||
}, [handleBack, focusNameField]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const cleanData = {
|
||||
...data,
|
||||
type: 'kanban',
|
||||
name: data.name.trim(),
|
||||
};
|
||||
|
||||
if (!cleanData.name) {
|
||||
nameField.current.select();
|
||||
return;
|
||||
}
|
||||
|
||||
onCreate(cleanData);
|
||||
onClose();
|
||||
}, [onClose, data, onCreate]);
|
||||
|
||||
const handleImportClick = useCallback(() => {
|
||||
openStep(StepTypes.IMPORT);
|
||||
}, [openStep]);
|
||||
|
||||
useEffect(() => {
|
||||
nameField.current.focus();
|
||||
}, []);
|
||||
|
||||
useDidUpdate(() => {
|
||||
nameField.current.focus();
|
||||
}, [focusNameFieldState]);
|
||||
|
||||
if (step && step.type === StepTypes.IMPORT) {
|
||||
return <ImportStep onSelect={handleImportSelect} onBack={handleImportBack} />;
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
<div className={styles.controls}>
|
||||
<Button positive content={t('action.createBoard')} className={styles.createButton} />
|
||||
<Button type="button" className={styles.importButton} onClick={handleImportClick}>
|
||||
<Icon
|
||||
name={data.import ? data.import.type : 'arrow down'}
|
||||
className={styles.importButtonIcon}
|
||||
/>
|
||||
{data.import ? data.import.file.name : t('action.import')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
AddStep.propTypes = {
|
||||
onCreate: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withPopup(AddStep);
|
||||
@@ -0,0 +1,42 @@
|
||||
:global(#app) {
|
||||
.controls {
|
||||
display: flex;
|
||||
max-width: 280px;
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
max-width: 226px;
|
||||
}
|
||||
}
|
||||
|
||||
.createButton {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.importButton {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: #6b808c;
|
||||
font-weight: normal;
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
text-decoration: underline;
|
||||
text-overflow: ellipsis;
|
||||
transition: none;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: rgba(9, 30, 66, 0.08);
|
||||
color: #092d42;
|
||||
}
|
||||
}
|
||||
|
||||
.importButtonIcon {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
import { FilePicker, Popup } from '../../../lib/custom-ui';
|
||||
|
||||
import styles from './ImportStep.module.scss';
|
||||
|
||||
const ImportStep = React.memo(({ onSelect, onBack }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleFileSelect = useCallback(
|
||||
(type, file) => {
|
||||
onSelect({
|
||||
type,
|
||||
file,
|
||||
});
|
||||
|
||||
onBack();
|
||||
},
|
||||
[onSelect, onBack],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t('common.importBoard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<FilePicker onSelect={(file) => handleFileSelect('trello', file)} accept=".json">
|
||||
<Button
|
||||
fluid
|
||||
type="button"
|
||||
icon="trello"
|
||||
content={t('common.fromTrello')}
|
||||
className={styles.button}
|
||||
/>
|
||||
</FilePicker>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ImportStep.propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ImportStep;
|
||||
@@ -0,0 +1,17 @@
|
||||
:global(#app) {
|
||||
.button {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: #6b808c;
|
||||
font-weight: normal;
|
||||
margin-top: 8px;
|
||||
padding: 6px 11px;
|
||||
text-align: left;
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
background: rgba(9, 30, 66, 0.08);
|
||||
color: #092d42;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import AddPopup from './AddPopup';
|
||||
|
||||
export default AddPopup;
|
||||
@@ -5,6 +5,7 @@ import { Button, Icon } from 'semantic-ui-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
import { startTimer, stopTimer } from '../../utils/timer';
|
||||
import Paths from '../../constants/Paths';
|
||||
import Tasks from './Tasks';
|
||||
import NameEdit from './NameEdit';
|
||||
@@ -57,6 +58,17 @@ const Card = React.memo(
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleToggleTimerClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
onUpdate({
|
||||
timer: timer.startedAt ? stopTimer(timer) : startTimer(timer),
|
||||
});
|
||||
},
|
||||
[timer, onUpdate],
|
||||
);
|
||||
|
||||
const handleNameUpdate = useCallback(
|
||||
(newName) => {
|
||||
onUpdate({
|
||||
@@ -108,7 +120,13 @@ const Card = React.memo(
|
||||
)}
|
||||
{timer && (
|
||||
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
||||
<Timer startedAt={timer.startedAt} total={timer.total} size="tiny" />
|
||||
<Timer
|
||||
as="span"
|
||||
startedAt={timer.startedAt}
|
||||
total={timer.total}
|
||||
size="tiny"
|
||||
onClick={canEdit ? handleToggleTimerClick : undefined}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
@@ -148,14 +166,11 @@ const Card = React.memo(
|
||||
{canEdit && (
|
||||
<ActionsPopup
|
||||
card={{
|
||||
id,
|
||||
name,
|
||||
dueDate,
|
||||
timer,
|
||||
boardId,
|
||||
listId,
|
||||
projectId,
|
||||
isPersisted,
|
||||
}}
|
||||
projectsToLists={allProjectsToLists}
|
||||
boardMemberships={allBoardMemberships}
|
||||
|
||||
@@ -78,7 +78,9 @@
|
||||
.cover {
|
||||
border-radius: 3px 3px 0 0;
|
||||
margin-bottom: 2px;
|
||||
padding: 2px 2px 0;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.details {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
z-index: 2000;
|
||||
z-index: 2001;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
padding: 6px 32px 6px 128px;
|
||||
margin: 0;
|
||||
min-height: 80px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.extension {
|
||||
@@ -95,7 +94,6 @@
|
||||
text-decoration: none;
|
||||
top: 50%;
|
||||
width: 112px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.thumbnailLabel {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Button, Grid, Icon, Modal } from 'semantic-ui-react';
|
||||
import { Markdown } from '../../lib/custom-ui';
|
||||
|
||||
import { startTimer, stopTimer } from '../../utils/timer';
|
||||
import NameField from './NameField';
|
||||
import DescriptionEdit from './DescriptionEdit';
|
||||
import Tasks from './Tasks';
|
||||
@@ -80,6 +81,12 @@ const CardModal = React.memo(
|
||||
|
||||
const isGalleryOpened = useRef(false);
|
||||
|
||||
const handleToggleTimerClick = useCallback(() => {
|
||||
onUpdate({
|
||||
timer: timer.startedAt ? stopTimer(timer) : startTimer(timer),
|
||||
});
|
||||
}, [timer, onUpdate]);
|
||||
|
||||
const handleNameUpdate = useCallback(
|
||||
(newName) => {
|
||||
onUpdate({
|
||||
@@ -291,6 +298,19 @@ const CardModal = React.memo(
|
||||
<Timer startedAt={timer.startedAt} total={timer.total} />
|
||||
)}
|
||||
</span>
|
||||
{canEdit && (
|
||||
<button
|
||||
onClick={handleToggleTimerClick}
|
||||
type="button"
|
||||
className={classNames(styles.attachment, styles.dueDate)}
|
||||
>
|
||||
<Icon
|
||||
name={timer.startedAt ? 'pause' : 'play'}
|
||||
size="small"
|
||||
className={styles.addAttachment}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import React, { useCallback, useImperativeHandle, useMemo, 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 { Button, Form } from 'semantic-ui-react';
|
||||
import SimpleMDE from 'react-simplemde-editor';
|
||||
|
||||
import styles from './DescriptionEdit.module.scss';
|
||||
|
||||
const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) => {
|
||||
const [t] = useTranslation();
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
const [value, handleFieldChange, setValue] = useField(null);
|
||||
|
||||
const field = useRef(null);
|
||||
const [value, setValue] = useState(null);
|
||||
|
||||
const open = useCallback(() => {
|
||||
setIsOpened(true);
|
||||
@@ -55,20 +51,37 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
|
||||
[close],
|
||||
);
|
||||
|
||||
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
|
||||
close,
|
||||
isOpened,
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
close();
|
||||
}, [close]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpened) {
|
||||
field.current.ref.current.focus();
|
||||
}
|
||||
}, [isOpened]);
|
||||
const mdEditorOptions = useMemo(
|
||||
() => ({
|
||||
autofocus: true,
|
||||
spellChecker: false,
|
||||
status: false,
|
||||
toolbar: [
|
||||
'bold',
|
||||
'italic',
|
||||
'heading',
|
||||
'strikethrough',
|
||||
'|',
|
||||
'quote',
|
||||
'unordered-list',
|
||||
'ordered-list',
|
||||
'table',
|
||||
'|',
|
||||
'link',
|
||||
'image',
|
||||
'|',
|
||||
'undo',
|
||||
'redo',
|
||||
'|',
|
||||
'guide',
|
||||
],
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
if (!isOpened) {
|
||||
return React.cloneElement(children, {
|
||||
@@ -78,26 +91,17 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<TextArea
|
||||
ref={field}
|
||||
as={TextareaAutosize}
|
||||
<SimpleMDE
|
||||
value={value}
|
||||
options={mdEditorOptions}
|
||||
placeholder={t('common.enterDescription')}
|
||||
minRows={3}
|
||||
spellCheck={false}
|
||||
className={styles.field}
|
||||
onKeyDown={handleFieldKeyDown}
|
||||
onChange={handleFieldChange}
|
||||
onBlur={handleFieldBlur}
|
||||
onChange={setValue}
|
||||
/>
|
||||
<div className={styles.controls}>
|
||||
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
|
||||
<Button
|
||||
positive
|
||||
content={t('action.save')}
|
||||
onMouseOver={handleControlMouseOver}
|
||||
onMouseOut={handleControlMouseOut}
|
||||
/>
|
||||
<Button positive content={t('action.save')} />
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -6,15 +6,12 @@
|
||||
|
||||
.field {
|
||||
background: #fff;
|
||||
border: 1px solid rgba(9, 30, 66, 0.13);
|
||||
border-radius: 3px;
|
||||
color: #17394d;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
padding: 8px 12px;
|
||||
resize: none;
|
||||
|
||||
&:focus {
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
.wrapper {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ const Header = React.memo(
|
||||
project,
|
||||
user,
|
||||
notifications,
|
||||
isLogouting,
|
||||
canEditProject,
|
||||
canEditUsers,
|
||||
onProjectSettingsClick,
|
||||
@@ -75,7 +76,11 @@ const Header = React.memo(
|
||||
)}
|
||||
</Menu.Item>
|
||||
</NotificationsPopup>
|
||||
<UserPopup onSettingsClick={onUserSettingsClick} onLogout={onLogout}>
|
||||
<UserPopup
|
||||
isLogouting={isLogouting}
|
||||
onSettingsClick={onUserSettingsClick}
|
||||
onLogout={onLogout}
|
||||
>
|
||||
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
|
||||
{user.name}
|
||||
</Menu.Item>
|
||||
@@ -93,6 +98,7 @@ Header.propTypes = {
|
||||
user: PropTypes.object.isRequired,
|
||||
notifications: PropTypes.array.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
isLogouting: PropTypes.bool.isRequired,
|
||||
canEditProject: PropTypes.bool.isRequired,
|
||||
canEditUsers: PropTypes.bool.isRequired,
|
||||
onProjectSettingsClick: PropTypes.func.isRequired,
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
background: none;
|
||||
}
|
||||
|
||||
&:active, &:hover {
|
||||
&:active,
|
||||
&:hover {
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
@@ -42,7 +43,6 @@
|
||||
height: 50px;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.notification {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import truncate from 'lodash/truncate';
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
@@ -47,23 +48,26 @@ const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
|
||||
{activity.data.toList.name}
|
||||
</Trans>
|
||||
);
|
||||
case ActivityTypes.COMMENT_CARD:
|
||||
case ActivityTypes.COMMENT_CARD: {
|
||||
const commentText = truncate(activity.data.text);
|
||||
|
||||
return (
|
||||
<Trans
|
||||
i18nKey="common.userLeftNewCommentToCard"
|
||||
values={{
|
||||
user: activity.user.name,
|
||||
comment: activity.data.text,
|
||||
comment: commentText,
|
||||
card: card.name,
|
||||
}}
|
||||
>
|
||||
{activity.user.name}
|
||||
{` left a new comment «${activity.data.text}» to `}
|
||||
{` left a new comment «${commentText}» to `}
|
||||
<Link to={Paths.CARDS.replace(':id', card.id)} onClick={onClose}>
|
||||
{card.name}
|
||||
</Link>
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -80,9 +84,10 @@ const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
{items.length > 0
|
||||
? items.map((item) => (
|
||||
<div key={item.id} className={styles.wrapper}>
|
||||
{items.length > 0 ? (
|
||||
<div className={styles.wrapper}>
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className={styles.item}>
|
||||
{item.card && item.activity ? (
|
||||
<>
|
||||
<User
|
||||
@@ -90,20 +95,23 @@ const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
|
||||
avatarUrl={item.activity.user.avatarUrl}
|
||||
size="large"
|
||||
/>
|
||||
<span className={styles.content}>{renderItemContent(item)}</span>
|
||||
<span className={styles.itemContent}>{renderItemContent(item)}</span>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.deletedContent}>{t('common.cardOrActionAreDeleted')}</div>
|
||||
<div className={styles.itemDeleted}>{t('common.cardOrActionAreDeleted')}</div>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
icon="close"
|
||||
className={styles.button}
|
||||
icon="trash alternate outline"
|
||||
className={styles.itemButton}
|
||||
onClick={() => handleDelete(item.id)}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: t('common.noUnreadNotifications')}
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
t('common.noUnreadNotifications')
|
||||
)}
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
:global(#app) {
|
||||
.button {
|
||||
.item {
|
||||
padding: 12px;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.itemButton {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
float: right;
|
||||
@@ -7,7 +15,7 @@
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
min-height: auto;
|
||||
padding: 5px 0;
|
||||
padding: 0;
|
||||
transition: background 0.3s ease;
|
||||
width: 20px;
|
||||
|
||||
@@ -16,7 +24,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.itemContent {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
min-height: 36px;
|
||||
@@ -27,7 +35,7 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.deletedContent {
|
||||
.itemDeleted {
|
||||
display: inline-block;
|
||||
line-height: 20px;
|
||||
min-height: 20px;
|
||||
@@ -38,10 +46,21 @@
|
||||
|
||||
.wrapper {
|
||||
margin: 0 -12px;
|
||||
padding: 12px;
|
||||
max-height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@ import Editor from './Editor';
|
||||
|
||||
import styles from './AddStep.module.scss';
|
||||
|
||||
const AddStep = React.memo(({ onCreate, onBack }) => {
|
||||
const AddStep = React.memo(({ defaultData, onCreate, onBack }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const [data, handleFieldChange] = useForm(() => ({
|
||||
name: '',
|
||||
color: LabelColors[0],
|
||||
...defaultData,
|
||||
}));
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
@@ -46,6 +47,7 @@ const AddStep = React.memo(({ onCreate, onBack }) => {
|
||||
});
|
||||
|
||||
AddStep.propTypes = {
|
||||
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
onCreate: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,8 @@ import { Button } from 'semantic-ui-react';
|
||||
import styles from './Item.module.scss';
|
||||
import globalStyles from '../../styles.module.scss';
|
||||
|
||||
const Item = React.memo(({ name, color, isPersisted, isActive, onSelect, onDeselect, onEdit }) => {
|
||||
const Item = React.memo(
|
||||
({ name, color, isPersisted, isActive, canEdit, onSelect, onDeselect, onEdit }) => {
|
||||
const handleToggleClick = useCallback(() => {
|
||||
if (isActive) {
|
||||
onDeselect();
|
||||
@@ -31,22 +32,27 @@ const Item = React.memo(({ name, color, isPersisted, isActive, onSelect, onDesel
|
||||
)}
|
||||
onClick={handleToggleClick}
|
||||
/>
|
||||
{canEdit && (
|
||||
<Button
|
||||
icon="pencil"
|
||||
size="small"
|
||||
floated="right"
|
||||
disabled={!isPersisted}
|
||||
className={styles.editButton}
|
||||
onClick={onEdit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Item.propTypes = {
|
||||
name: PropTypes.string,
|
||||
color: PropTypes.string.isRequired,
|
||||
isPersisted: PropTypes.bool.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onDeselect: PropTypes.func.isRequired,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
box-shadow: none;
|
||||
flex: 0 0 auto;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
text-decoration: underline;
|
||||
|
||||
@@ -18,6 +17,7 @@
|
||||
flex: 1 1 auto;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-right: 0;
|
||||
overflow: hidden;
|
||||
padding: 8px 32px 8px 10px;
|
||||
position: relative;
|
||||
|
||||
@@ -18,7 +18,18 @@ const StepTypes = {
|
||||
};
|
||||
|
||||
const LabelsStep = React.memo(
|
||||
({ items, currentIds, title, onSelect, onDeselect, onCreate, onUpdate, onDelete, onBack }) => {
|
||||
({
|
||||
items,
|
||||
currentIds,
|
||||
title,
|
||||
canEdit,
|
||||
onSelect,
|
||||
onDeselect,
|
||||
onCreate,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
onBack,
|
||||
}) => {
|
||||
const [t] = useTranslation();
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
const [search, handleSearchChange] = useField('');
|
||||
@@ -84,7 +95,15 @@ const LabelsStep = React.memo(
|
||||
if (step) {
|
||||
switch (step.type) {
|
||||
case StepTypes.ADD:
|
||||
return <AddStep onCreate={onCreate} onBack={handleBack} />;
|
||||
return (
|
||||
<AddStep
|
||||
defaultData={{
|
||||
name: search,
|
||||
}}
|
||||
onCreate={onCreate}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
case StepTypes.EDIT: {
|
||||
const currentItem = items.find((item) => item.id === step.params.id);
|
||||
|
||||
@@ -132,6 +151,7 @@ const LabelsStep = React.memo(
|
||||
color={item.color}
|
||||
isPersisted={item.isPersisted}
|
||||
isActive={currentIds.includes(item.id)}
|
||||
canEdit={canEdit}
|
||||
onSelect={() => handleSelect(item.id)}
|
||||
onDeselect={() => handleDeselect(item.id)}
|
||||
onEdit={() => handleEdit(item.id)}
|
||||
@@ -139,12 +159,14 @@ const LabelsStep = React.memo(
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Button
|
||||
fluid
|
||||
content={t('action.createNewLabel')}
|
||||
className={styles.addButton}
|
||||
onClick={handleAddClick}
|
||||
/>
|
||||
)}
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
@@ -157,6 +179,7 @@ LabelsStep.propTypes = {
|
||||
currentIds: PropTypes.array.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
title: PropTypes.string,
|
||||
canEdit: PropTypes.bool,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onDeselect: PropTypes.func.isRequired,
|
||||
onCreate: PropTypes.func.isRequired,
|
||||
@@ -167,6 +190,7 @@ LabelsStep.propTypes = {
|
||||
|
||||
LabelsStep.defaultProps = {
|
||||
title: 'common.labels',
|
||||
canEdit: true,
|
||||
onBack: undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { ConnectedRouter } from 'connected-react-router';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { ReduxRouter } from '../lib/redux-router';
|
||||
|
||||
import Paths from '../constants/Paths';
|
||||
import LoginContainer from '../containers/LoginContainer';
|
||||
@@ -11,6 +11,7 @@ import NotFound from './NotFound';
|
||||
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import 'photoswipe/dist/photoswipe.css';
|
||||
import 'easymde/dist/easymde.min.css';
|
||||
import '../lib/custom-ui/styles.css';
|
||||
|
||||
import '../styles.module.scss';
|
||||
@@ -18,16 +19,16 @@ import '../styles.module.scss';
|
||||
function Root({ store, history }) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<Switch>
|
||||
<Route exact path={Paths.LOGIN} component={LoginContainer} />
|
||||
<Route exact path={Paths.ROOT} component={CoreWrapperContainer} />
|
||||
<Route exact path={Paths.PROJECTS} component={CoreWrapperContainer} />
|
||||
<Route exact path={Paths.BOARDS} component={CoreWrapperContainer} />
|
||||
<Route exact path={Paths.CARDS} component={CoreWrapperContainer} />
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
</ConnectedRouter>
|
||||
<ReduxRouter history={history}>
|
||||
<Routes>
|
||||
<Route path={Paths.LOGIN} element={<LoginContainer />} />
|
||||
<Route path={Paths.ROOT} element={<CoreWrapperContainer />} />
|
||||
<Route path={Paths.PROJECTS} element={<CoreWrapperContainer />} />
|
||||
<Route path={Paths.BOARDS} element={<CoreWrapperContainer />} />
|
||||
<Route path={Paths.CARDS} element={<CoreWrapperContainer />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</ReduxRouter>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const SIZES = {
|
||||
MEDIUM: 'medium',
|
||||
};
|
||||
|
||||
const Timer = React.memo(({ startedAt, total, size, isDisabled, onClick }) => {
|
||||
const Timer = React.memo(({ as, startedAt, total, size, isDisabled, onClick }) => {
|
||||
const prevStartedAt = usePrevious(startedAt);
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
@@ -52,6 +52,7 @@ const Timer = React.memo(({ startedAt, total, size, isDisabled, onClick }) => {
|
||||
className={classNames(
|
||||
styles.wrapper,
|
||||
styles[`wrapper${upperFirst(size)}`],
|
||||
startedAt && styles.wrapperActive,
|
||||
onClick && styles.wrapperHoverable,
|
||||
)}
|
||||
>
|
||||
@@ -59,16 +60,19 @@ const Timer = React.memo(({ startedAt, total, size, isDisabled, onClick }) => {
|
||||
</span>
|
||||
);
|
||||
|
||||
const ElementType = as;
|
||||
|
||||
return onClick ? (
|
||||
<button type="button" disabled={isDisabled} className={styles.button} onClick={onClick}>
|
||||
<ElementType type="button" disabled={isDisabled} className={styles.button} onClick={onClick}>
|
||||
{contentNode}
|
||||
</button>
|
||||
</ElementType>
|
||||
) : (
|
||||
contentNode
|
||||
);
|
||||
});
|
||||
|
||||
Timer.propTypes = {
|
||||
as: PropTypes.elementType,
|
||||
startedAt: PropTypes.instanceOf(Date),
|
||||
total: PropTypes.number.isRequired, // eslint-disable-line react/no-unused-prop-types
|
||||
size: PropTypes.oneOf(Object.values(SIZES)),
|
||||
@@ -77,6 +81,7 @@ Timer.propTypes = {
|
||||
};
|
||||
|
||||
Timer.defaultProps = {
|
||||
as: 'button',
|
||||
startedAt: undefined,
|
||||
size: SIZES.MEDIUM,
|
||||
isDisabled: false,
|
||||
|
||||
@@ -9,17 +9,28 @@
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
background: #f3f4f6;
|
||||
background: #dce0e4;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
color: #7e8ea3;
|
||||
color: #6a808b;
|
||||
display: inline-block;
|
||||
font-variant-numeric: tabular-nums;
|
||||
outline: none;
|
||||
text-align: left;
|
||||
transition: background 0.3s ease;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.wrapperActive {
|
||||
background: #21ba45;
|
||||
color: #fff;
|
||||
|
||||
&.wrapperHoverable:hover {
|
||||
background: #16ab39;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapperHoverable:hover {
|
||||
background: #d2d8dc;
|
||||
color: #17394d;
|
||||
@@ -42,6 +53,5 @@
|
||||
.wrapperMedium {
|
||||
line-height: 20px;
|
||||
padding: 6px 12px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { withPopup } from '../../lib/popup';
|
||||
import { Input, Popup } from '../../lib/custom-ui';
|
||||
|
||||
import { useForm } from '../../hooks';
|
||||
import { isUsername } from '../../utils/validator';
|
||||
import { isPassword, isUsername } from '../../utils/validator';
|
||||
|
||||
import styles from './UserAddPopup.module.scss';
|
||||
|
||||
@@ -69,7 +69,7 @@ const UserAddStep = React.memo(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cleanData.password) {
|
||||
if (!cleanData.password || !isPassword(cleanData.password)) {
|
||||
passwordField.current.focus();
|
||||
return;
|
||||
}
|
||||
@@ -143,6 +143,7 @@ const UserAddStep = React.memo(
|
||||
/>
|
||||
<div className={styles.text}>{t('common.password')}</div>
|
||||
<Input.Password
|
||||
withStrengthBar
|
||||
fluid
|
||||
ref={passwordField}
|
||||
name="password"
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
|
||||
import { Input, Popup } from '../../lib/custom-ui';
|
||||
|
||||
import { useForm } from '../../hooks';
|
||||
import { isPassword } from '../../utils/validator';
|
||||
|
||||
import styles from './UserPasswordEditStep.module.scss';
|
||||
|
||||
@@ -56,7 +57,7 @@ const UserPasswordEditStep = React.memo(
|
||||
const currentPasswordField = useRef(null);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!data.password) {
|
||||
if (!data.password || !isPassword(data.password)) {
|
||||
passwordField.current.select();
|
||||
return;
|
||||
}
|
||||
@@ -113,6 +114,7 @@ const UserPasswordEditStep = React.memo(
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div className={styles.text}>{t('common.newPassword')}</div>
|
||||
<Input.Password
|
||||
withStrengthBar
|
||||
fluid
|
||||
ref={passwordField}
|
||||
name="password"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Menu } from 'semantic-ui-react';
|
||||
import { Button, Menu } from 'semantic-ui-react';
|
||||
import { withPopup } from '../../lib/popup';
|
||||
import { Popup } from '../../lib/custom-ui';
|
||||
|
||||
import styles from './UserPopup.module.scss';
|
||||
|
||||
const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
||||
const UserStep = React.memo(({ isLogouting, onSettingsClick, onLogout, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleSettingsClick = useCallback(() => {
|
||||
@@ -15,6 +15,17 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
||||
onClose();
|
||||
}, [onSettingsClick, onClose]);
|
||||
|
||||
let logoutMenuItemProps;
|
||||
if (isLogouting) {
|
||||
logoutMenuItemProps = {
|
||||
as: Button,
|
||||
fluid: true,
|
||||
basic: true,
|
||||
loading: true,
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
@@ -29,7 +40,11 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={onLogout}>
|
||||
<Menu.Item
|
||||
{...logoutMenuItemProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
className={styles.menuItem}
|
||||
onClick={onLogout}
|
||||
>
|
||||
{t('action.logOut', {
|
||||
context: 'title',
|
||||
})}
|
||||
@@ -41,6 +56,7 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
||||
});
|
||||
|
||||
UserStep.propTypes = {
|
||||
isLogouting: PropTypes.bool.isRequired,
|
||||
onSettingsClick: PropTypes.func.isRequired,
|
||||
onLogout: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
// import { useTranslation } from 'react-i18next';
|
||||
import { Image, Tab } from 'semantic-ui-react';
|
||||
|
||||
import logo from '../../assets/images/logo.png';
|
||||
|
||||
import styles from './AboutPane.module.scss';
|
||||
|
||||
const AboutPane = React.memo(() => {
|
||||
// const [t] = useTranslation();
|
||||
|
||||
return (
|
||||
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||
<Image centered src={logo} size="large" className={styles.logo} />
|
||||
</Tab.Pane>
|
||||
);
|
||||
});
|
||||
|
||||
export default AboutPane;
|
||||
@@ -0,0 +1,10 @@
|
||||
:global(#app) {
|
||||
/* .logo {
|
||||
margin-bottom: 16px;
|
||||
} */
|
||||
|
||||
.wrapper {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Modal, Tab } from 'semantic-ui-react';
|
||||
|
||||
import AccountPane from './AccountPane';
|
||||
import PreferencesPane from './PreferencesPane';
|
||||
import AboutPane from './AboutPane';
|
||||
|
||||
const UserSettingsModal = React.memo(
|
||||
({
|
||||
@@ -71,6 +72,10 @@ const UserSettingsModal = React.memo(
|
||||
<PreferencesPane subscribeToOwnCards={subscribeToOwnCards} onUpdate={onUpdate} />
|
||||
),
|
||||
},
|
||||
{
|
||||
menuItem: 'About Planka',
|
||||
render: () => <AboutPane />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -21,6 +21,7 @@ export default {
|
||||
|
||||
CORE_INITIALIZE: 'CORE_INITIALIZE',
|
||||
LOGOUT: 'LOGOUT',
|
||||
LOGOUT__ACCESS_TOKEN_INVALIDATE: 'LOGOUT__ACCESS_TOKEN_INVALIDATE',
|
||||
|
||||
/* Modals */
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
const { BASE_URL } = window;
|
||||
const BASE_PATH = BASE_URL.replace(/^.*\/\/[^/]*(.*)[^?#]*.*$/, '$1');
|
||||
|
||||
const SERVER_BASE_URL =
|
||||
process.env.REACT_APP_SERVER_BASE_URL ||
|
||||
(process.env.NODE_ENV === 'production' ? '' : 'http://localhost:1337');
|
||||
(process.env.NODE_ENV === 'production' ? BASE_URL : 'http://localhost:1337');
|
||||
|
||||
const SERVER_HOST_NAME = SERVER_BASE_URL.replace(/^(.*\/\/[^/?#]*).*$/, '$1');
|
||||
|
||||
const ACCESS_TOKEN_KEY = 'accessToken';
|
||||
const ACCESS_TOKEN_VERSION_KEY = 'accessTokenVersion';
|
||||
@@ -10,7 +15,9 @@ const POSITION_GAP = 65535;
|
||||
const ACTIVITIES_LIMIT = 50;
|
||||
|
||||
export default {
|
||||
BASE_PATH,
|
||||
SERVER_BASE_URL,
|
||||
SERVER_HOST_NAME,
|
||||
ACCESS_TOKEN_KEY,
|
||||
ACCESS_TOKEN_VERSION_KEY,
|
||||
ACCESS_TOKEN_VERSION,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
const ROOT = '/';
|
||||
const LOGIN = '/login';
|
||||
const PROJECTS = '/projects/:id';
|
||||
const BOARDS = '/boards/:id';
|
||||
const CARDS = '/cards/:id';
|
||||
import Config from './Config';
|
||||
|
||||
const ROOT = `${Config.BASE_PATH}/`;
|
||||
const LOGIN = `${Config.BASE_PATH}/login`;
|
||||
const PROJECTS = `${Config.BASE_PATH}/projects/:id`;
|
||||
const BOARDS = `${Config.BASE_PATH}/boards/:id`;
|
||||
const CARDS = `${Config.BASE_PATH}/cards/:id`;
|
||||
|
||||
export default {
|
||||
ROOT,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
|
||||
|
||||
import selectors from '../selectors';
|
||||
import entryActions from '../entry-actions';
|
||||
import { BoardMembershipRoles } from '../constants/Enums';
|
||||
import BoardActions from '../components/BoardActions';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
@@ -12,6 +13,7 @@ const mapStateToProps = (state) => {
|
||||
const labels = selectors.selectLabelsForCurrentBoard(state);
|
||||
const filterUsers = selectors.selectFilterUsersForCurrentBoard(state);
|
||||
const filterLabels = selectors.selectFilterLabelsForCurrentBoard(state);
|
||||
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
|
||||
return {
|
||||
memberships,
|
||||
@@ -19,6 +21,7 @@ const mapStateToProps = (state) => {
|
||||
filterUsers,
|
||||
filterLabels,
|
||||
allUsers,
|
||||
canEdit: !!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR,
|
||||
canEditMemberships: isCurrentUserManager,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { push } from 'connected-react-router';
|
||||
import omit from 'lodash/omit';
|
||||
import { push } from '../lib/redux-router';
|
||||
|
||||
import selectors from '../selectors';
|
||||
import entryActions from '../entry-actions';
|
||||
|
||||
@@ -6,6 +6,7 @@ import entryActions from '../entry-actions';
|
||||
import Header from '../components/Header';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const isLogouting = selectors.selectIsLogouting(state);
|
||||
const currentUser = selectors.selectCurrentUser(state);
|
||||
const currentProject = selectors.selectCurrentProject(state);
|
||||
const notifications = selectors.selectNotificationsForCurrentUser(state);
|
||||
@@ -13,6 +14,7 @@ const mapStateToProps = (state) => {
|
||||
|
||||
return {
|
||||
notifications,
|
||||
isLogouting,
|
||||
project: currentProject,
|
||||
user: currentUser,
|
||||
canEditProject: isCurrentUserManager,
|
||||
|
||||
@@ -7,10 +7,11 @@ const createBoardInCurrentProject = (data) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const handleBoardCreate = (board) => ({
|
||||
const handleBoardCreate = (board, requestId) => ({
|
||||
type: EntryActionTypes.BOARD_CREATE_HANDLE,
|
||||
payload: {
|
||||
board,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import store from './store';
|
||||
import history from './history';
|
||||
@@ -7,4 +7,5 @@ import Root from './components/Root';
|
||||
|
||||
import './i18n';
|
||||
|
||||
ReactDOM.render(React.createElement(Root, { store, history }), document.getElementById('root'));
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(React.createElement(Root, { store, history }));
|
||||
|
||||
@@ -1,22 +1,74 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Icon, Input } from 'semantic-ui-react';
|
||||
import zxcvbn from 'zxcvbn';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon, Input, Progress } from 'semantic-ui-react';
|
||||
import { useToggle } from '../../../hooks';
|
||||
|
||||
const InputPassword = React.forwardRef((props, ref) => {
|
||||
import styles from './InputPassword.module.css';
|
||||
|
||||
const STRENGTH_SCORE_COLORS = ['red', 'orange', 'yellow', 'olive', 'green'];
|
||||
|
||||
const InputPassword = React.forwardRef(
|
||||
({ value, withStrengthBar, minStrengthScore, className, ...props }, ref) => {
|
||||
const [isVisible, toggleVisible] = useToggle();
|
||||
|
||||
const strengthScore = useMemo(() => {
|
||||
if (!withStrengthBar) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return zxcvbn(value).score;
|
||||
}, [value, withStrengthBar]);
|
||||
|
||||
const handleToggleClick = useCallback(() => {
|
||||
toggleVisible();
|
||||
}, [toggleVisible]);
|
||||
|
||||
const inputProps = {
|
||||
...props,
|
||||
ref,
|
||||
type: isVisible ? 'text' : 'password',
|
||||
icon: <Icon link name={isVisible ? 'eye' : 'eye slash'} onClick={handleToggleClick} />,
|
||||
};
|
||||
|
||||
if (!withStrengthBar) {
|
||||
return (
|
||||
<Input
|
||||
{...props} // eslint-disable-line react/jsx-props-no-spreading
|
||||
ref={ref}
|
||||
type={isVisible ? 'text' : 'password'}
|
||||
icon={<Icon link name={isVisible ? 'eye' : 'eye slash'} onClick={handleToggleClick} />}
|
||||
{...inputProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Input
|
||||
{...inputProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
error={!!value && strengthScore < minStrengthScore}
|
||||
/>
|
||||
<Progress
|
||||
value={value ? strengthScore + 1 : 0}
|
||||
total={5}
|
||||
color={STRENGTH_SCORE_COLORS[strengthScore]}
|
||||
size="tiny"
|
||||
className={styles.strengthBar}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
InputPassword.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
withStrengthBar: PropTypes.bool,
|
||||
minStrengthScore: PropTypes.number,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
InputPassword.defaultProps = {
|
||||
withStrengthBar: false,
|
||||
minStrengthScore: 2,
|
||||
className: undefined,
|
||||
};
|
||||
|
||||
export default React.memo(InputPassword);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.strengthBar {
|
||||
margin: 4px 0 0 !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import remarkBreaks from 'remark-breaks';
|
||||
|
||||
import './Markdown.module.scss'; // FIXME: import as styles?
|
||||
|
||||
const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i;
|
||||
|
||||
const Markdown = React.memo(({ linkStopPropagation, ...props }) => {
|
||||
const handleLinkClick = useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
@@ -16,25 +18,30 @@ const Markdown = React.memo(({ linkStopPropagation, ...props }) => {
|
||||
jsx-a11y/click-events-have-key-events,
|
||||
jsx-a11y/no-static-element-interactions,
|
||||
react/jsx-props-no-spreading */
|
||||
({ node, ...linkProps }) => <a {...linkProps} onClick={handleLinkClick} />,
|
||||
({ node, ...linkProps }) => (
|
||||
<a
|
||||
{...linkProps}
|
||||
rel={
|
||||
ABSOLUTE_URL_REGEX.test(linkProps.href) && linkProps.target === '_blank'
|
||||
? 'noreferrer'
|
||||
: undefined
|
||||
}
|
||||
onClick={linkStopPropagation ? handleLinkClick : undefined}
|
||||
/>
|
||||
),
|
||||
/* eslint-enable jsx-a11y/anchor-has-content,
|
||||
jsx-a11y/click-events-have-key-events,
|
||||
jsx-a11y/no-static-element-interactions,
|
||||
react/jsx-props-no-spreading */
|
||||
[handleLinkClick],
|
||||
[linkStopPropagation, handleLinkClick],
|
||||
);
|
||||
|
||||
let components;
|
||||
if (linkStopPropagation) {
|
||||
components = {
|
||||
a: linkRenderer,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
{...props} // eslint-disable-line react/jsx-props-no-spreading
|
||||
components={components}
|
||||
components={{
|
||||
a: linkRenderer,
|
||||
}}
|
||||
remarkPlugins={[remarkGfm, remarkBreaks]}
|
||||
className="markdown-body"
|
||||
/>
|
||||
|
||||
Vendored
+5
-5
@@ -14453,11 +14453,11 @@ img.ui.bordered.image {
|
||||
---------------------*/
|
||||
|
||||
.ui.input.error > input {
|
||||
background-color: #fff6f6;
|
||||
border-color: #e0b4b4;
|
||||
color: #9f3a38;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
background-color: #fff6f6 !important;
|
||||
border-color: #e0b4b4 !important;
|
||||
color: #9f3a38 !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Error Placeholder */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { ResizeObserver } from '@juggle/resize-observer';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react';
|
||||
|
||||
@@ -8,6 +9,9 @@ export default (WrappedComponent, defaultProps) => {
|
||||
const Popup = React.memo(({ children, ...props }) => {
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
|
||||
const wrapper = useRef(null);
|
||||
const resizeObserver = useRef(null);
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
setIsOpened(true);
|
||||
}, []);
|
||||
@@ -37,6 +41,29 @@ export default (WrappedComponent, defaultProps) => {
|
||||
[children],
|
||||
);
|
||||
|
||||
const handleContentRef = useCallback((element) => {
|
||||
if (resizeObserver.current) {
|
||||
resizeObserver.current.disconnect();
|
||||
}
|
||||
|
||||
if (!element) {
|
||||
resizeObserver.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
resizeObserver.current = new ResizeObserver(() => {
|
||||
if (resizeObserver.current.isInitial) {
|
||||
resizeObserver.current.isInitial = false;
|
||||
return;
|
||||
}
|
||||
|
||||
wrapper.current.positionUpdate();
|
||||
});
|
||||
|
||||
resizeObserver.current.isInitial = true;
|
||||
resizeObserver.current.observe(element);
|
||||
}, []);
|
||||
|
||||
const tigger = React.cloneElement(children, {
|
||||
onClick: handleTriggerClick,
|
||||
});
|
||||
@@ -45,6 +72,7 @@ export default (WrappedComponent, defaultProps) => {
|
||||
<SemanticUIPopup
|
||||
basic
|
||||
wide
|
||||
ref={wrapper}
|
||||
trigger={tigger}
|
||||
on="click"
|
||||
open={isOpened}
|
||||
@@ -52,8 +80,10 @@ export default (WrappedComponent, defaultProps) => {
|
||||
popperModifiers={[
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
enabled: true,
|
||||
options: {
|
||||
boundariesElement: 'window',
|
||||
altAxis: true,
|
||||
padding: 20,
|
||||
},
|
||||
},
|
||||
]}
|
||||
@@ -64,9 +94,11 @@ export default (WrappedComponent, defaultProps) => {
|
||||
onClick={handleClick}
|
||||
{...defaultProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
>
|
||||
<div ref={handleContentRef}>
|
||||
<Button icon="close" onClick={handleClose} className={styles.closeButton} />
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<WrappedComponent {...props} onClose={handleClose} />
|
||||
</div>
|
||||
</SemanticUIPopup>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { useLayoutEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import { handleLocationChange } from './actions';
|
||||
|
||||
function ReduxRouter({ children, history, selector, basename }) {
|
||||
const state = useSelector(selector);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const unlisten = history.listen((nextState) => {
|
||||
dispatch(handleLocationChange(nextState.location, nextState.action));
|
||||
});
|
||||
|
||||
dispatch(handleLocationChange(history.location, history.action, true));
|
||||
|
||||
return unlisten;
|
||||
}, [history, dispatch]);
|
||||
|
||||
return (
|
||||
<Router
|
||||
basename={basename}
|
||||
location={state.location}
|
||||
navigationType={state.action}
|
||||
navigator={history}
|
||||
>
|
||||
{children}
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
ReduxRouter.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
history: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
selector: PropTypes.func,
|
||||
basename: PropTypes.string,
|
||||
};
|
||||
|
||||
ReduxRouter.defaultProps = {
|
||||
selector: ({ router }) => router,
|
||||
basename: undefined,
|
||||
};
|
||||
|
||||
export default ReduxRouter;
|
||||
@@ -0,0 +1,28 @@
|
||||
export const LOCATION_CHANGE_HANDLE = '@@router/LOCATION_CHANGE_HANDLE';
|
||||
|
||||
export const handleLocationChange = (location, action, isFirstRendering = false) => ({
|
||||
type: LOCATION_CHANGE_HANDLE,
|
||||
payload: {
|
||||
location,
|
||||
action,
|
||||
isFirstRendering,
|
||||
},
|
||||
});
|
||||
|
||||
export const HISTORY_METHOD_CALL = '@@router/HISTORY_METHOD_CALL';
|
||||
|
||||
const createHistoryMethodCaller = (method) => {
|
||||
return (...args) => ({
|
||||
type: HISTORY_METHOD_CALL,
|
||||
payload: {
|
||||
method,
|
||||
args,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const push = createHistoryMethodCaller('push');
|
||||
export const replace = createHistoryMethodCaller('replace');
|
||||
export const go = createHistoryMethodCaller('go');
|
||||
export const back = createHistoryMethodCaller('back');
|
||||
export const forward = createHistoryMethodCaller('forward');
|
||||
@@ -0,0 +1,18 @@
|
||||
import { HISTORY_METHOD_CALL } from './actions';
|
||||
|
||||
const createRouterMiddleware = (history) => {
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => (next) => (action) => {
|
||||
if (action.type !== HISTORY_METHOD_CALL) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
const {
|
||||
payload: { method, args },
|
||||
} = action;
|
||||
|
||||
history[method](...args);
|
||||
};
|
||||
};
|
||||
|
||||
export default createRouterMiddleware;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { LOCATION_CHANGE_HANDLE } from './actions';
|
||||
|
||||
const createRouterReducer = (history) => {
|
||||
const initialState = {
|
||||
location: history.location,
|
||||
action: history.action,
|
||||
};
|
||||
|
||||
return (state = initialState, { type, payload } = {}) => {
|
||||
if (type === LOCATION_CHANGE_HANDLE) {
|
||||
const { location, action, isFirstRendering } = payload;
|
||||
|
||||
if (isFirstRendering) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
location,
|
||||
action,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
};
|
||||
|
||||
export default createRouterReducer;
|
||||
@@ -0,0 +1,12 @@
|
||||
export {
|
||||
LOCATION_CHANGE_HANDLE,
|
||||
HISTORY_METHOD_CALL,
|
||||
push,
|
||||
replace,
|
||||
go,
|
||||
back,
|
||||
forward,
|
||||
} from './actions';
|
||||
export { default as createRouterReducer } from './create-router-reducer';
|
||||
export { default as createRouterMiddleware } from './create-router-middleware';
|
||||
export { default as ReduxRouter } from './ReduxRouter';
|
||||
@@ -117,7 +117,7 @@ export default {
|
||||
phone: 'Telefon',
|
||||
preferences: 'Volby',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: dejte Ctrl-V (Cmd-V na Mac) pro vložení přílohy ze schránky.',
|
||||
'Tip: dejte Ctrl-V (Cmd-V na Mac) pro vložení přílohy ze schránky',
|
||||
project: 'Projekt',
|
||||
projectNotFound_title: 'Projekt nenalezen',
|
||||
removeManager_title: 'Odstranit vedoucího',
|
||||
|
||||
@@ -4,11 +4,11 @@ export default {
|
||||
dateFns,
|
||||
|
||||
format: {
|
||||
date: 'd/M/yyyy',
|
||||
date: 'd.MM.yyyy',
|
||||
time: 'p',
|
||||
dateTime: '$t(format:date) $t(format:time)',
|
||||
longDate: 'MMM d',
|
||||
longDateTime: "d MMMM 'at' p",
|
||||
longDate: 'd. MMM',
|
||||
longDateTime: "d. MMMM 'um' p",
|
||||
},
|
||||
|
||||
translation: {
|
||||
@@ -23,7 +23,7 @@ export default {
|
||||
administrator: 'Administrator',
|
||||
all: 'Alle',
|
||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||
'Alle Änderungen werden automatisch gespeichert,<br />sobald die Verbindung wiederhergestellt wurde.',
|
||||
'Alle Änderungen werden automatisch gespeichert, sobald die Verbindung wiederhergestellt wurde.',
|
||||
areYouSureYouWantToDeleteThisAttachment:
|
||||
'Sind Sie sicher, dass Sie diesen Anhang löschen möchten?',
|
||||
areYouSureYouWantToDeleteThisBoard: 'Sind Sie sicher, dass Sie dieses Board löschen möchten?',
|
||||
@@ -55,10 +55,11 @@ export default {
|
||||
color: 'Farbe',
|
||||
createBoard_title: 'Board erstellen',
|
||||
createLabel_title: 'Label erstellen',
|
||||
createNewOneOrSelectExistingOne: 'Ein Neues erstellen, oder <br />ein Bestehendes auswählen',
|
||||
createNewOneOrSelectExistingOne:
|
||||
'Erstellen Sie ein neues oder wählen Sie ein bestehendes aus.',
|
||||
createProject_title: 'Projekt erstellen',
|
||||
createTextFile_title: 'Textdatei erstellen',
|
||||
currentPassword: 'Derzeitiges Password',
|
||||
currentPassword: 'Derzeitiges Passwort',
|
||||
dangerZone_title: 'Gefährlicher Bereich',
|
||||
date: 'Datum',
|
||||
dueDate_title: 'Fälligkeitsdatum',
|
||||
@@ -73,18 +74,18 @@ export default {
|
||||
deleteUser_title: 'Benutzer löschen',
|
||||
description: 'Beschreibung',
|
||||
detectAutomatically: 'Automatische Erkennung',
|
||||
dropFileToUpload: 'Datei ablegen, um hochzuladen',
|
||||
dropFileToUpload: 'Datei ablegen, um sie hochzuladen',
|
||||
editAttachment_title: 'Anhang bearbieten',
|
||||
editAvatar_title: 'Avatar bearbeiten',
|
||||
editBoard_title: 'Board bearbeiten',
|
||||
editDueDate_title: 'Fälligkeitsdatum bearbeiten',
|
||||
editEmail_title: 'E-Mail bearbeiten',
|
||||
editEmail_title: 'E-Mail-Adresse bearbeiten',
|
||||
editLabel_title: 'Label bearbeiten',
|
||||
editPassword_title: 'Passwort ändern',
|
||||
editTimer_title: 'Timer bearbeiten',
|
||||
editUsername_title: 'Benutzername ändern',
|
||||
email: 'E-Mail',
|
||||
emailAlreadyInUse: 'E-Mail bereits in Verwendung',
|
||||
email: 'E-Mail-Adresse',
|
||||
emailAlreadyInUse: 'E-Mail-Adresse bereits in Verwendung',
|
||||
enterCardTitle: 'Kartentitel eingeben...',
|
||||
enterDescription: 'Beschreibung eingeben...',
|
||||
enterFilename: 'Dateiname eingeben',
|
||||
@@ -108,7 +109,7 @@ export default {
|
||||
minutes: 'Minuten',
|
||||
moveCard_title: 'Karte verschieben',
|
||||
name: 'Name',
|
||||
newEmail: 'Neue E-Mail',
|
||||
newEmail: 'Neue E-Mail-Adresse',
|
||||
newPassword: 'Neues Passwort',
|
||||
newUsername: 'Neuer Benutzername',
|
||||
noConnectionToServer: 'Keine Verbindung zum Server',
|
||||
@@ -123,7 +124,7 @@ export default {
|
||||
phone: 'Telefon',
|
||||
preferences: 'Voreinstellungen',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tipp: Drücken Sie STRG-V (Cmd-V auf Mac), um einen Anhang aus der Zwischenablage hinzuzufügen.',
|
||||
'Tipp: Drücken Sie STRG-V (Cmd-V auf Mac), um einen Anhang aus der Zwischenablage hinzuzufügen',
|
||||
project: 'Projekt',
|
||||
projectNotFound_title: 'Projekt nicht gefunden',
|
||||
removeManager_title: 'Manager entfernen',
|
||||
@@ -140,10 +141,10 @@ export default {
|
||||
time: 'Zeit',
|
||||
timer: 'Timer',
|
||||
title: 'Titel',
|
||||
userActions_title: 'Benutzerraktionen',
|
||||
userActions_title: 'Benutzeraktionen',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> hat diese Karte hinzugefügt zu {{list}}</1>',
|
||||
userLeftNewCommentToCard:
|
||||
'{{user}} hat einen neuen Kommentar verfasst «{{comment}}» to <2>{{card}}</2>',
|
||||
'{{user}} hat einen neuen Kommentar verfasst: «{{comment}}» in <2>{{card}}</2>',
|
||||
userMovedCardFromListToList:
|
||||
'{{user}} bewegte <2>{{card}}</2> von {{fromList}} nach {{toList}}',
|
||||
userMovedThisCardFromListToList:
|
||||
@@ -190,7 +191,7 @@ export default {
|
||||
edit: 'Bearbeiten',
|
||||
editDueDate_title: 'Fälligkeitsdatum bearbeiten',
|
||||
editDescription_title: 'Beschreibung ändern',
|
||||
editEmail_title: 'E-Mail bearbeiten',
|
||||
editEmail_title: 'E-Mail-Adresse bearbeiten',
|
||||
editPassword_title: 'Passwort ändern',
|
||||
editTimer_title: 'Timer bearbeiten',
|
||||
editTitle_title: 'Titel bearbeiten',
|
||||
@@ -199,7 +200,7 @@ export default {
|
||||
leaveBoard: 'Board verlassen',
|
||||
leaveProject: 'Projekt verlassen',
|
||||
logOut_title: 'Ausloggen',
|
||||
makeCover_title: 'als Vorschau festlegen',
|
||||
makeCover_title: 'Als Vorschau festlegen',
|
||||
move: 'Verschieben',
|
||||
moveCard_title: 'Karte bewegen',
|
||||
remove: 'Löschen',
|
||||
@@ -216,7 +217,7 @@ export default {
|
||||
start: 'Start',
|
||||
stop: 'Stopp',
|
||||
subscribe: 'Abonnieren',
|
||||
unsubscribe: 'Deabonnieren',
|
||||
unsubscribe: 'De-abonnieren',
|
||||
uploadNewAvatar: 'Neuen Avatar hochladen',
|
||||
uploadNewImage: 'Neues Bild hochladen',
|
||||
},
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
export default {
|
||||
translation: {
|
||||
common: {
|
||||
emailOrUsername: 'E-mail oder Benutzername',
|
||||
invalidEmailOrUsername: 'Ungültige E-mail oder Benutzername',
|
||||
emailOrUsername: 'E-Mail-Adresse oder Benutzername',
|
||||
invalidEmailOrUsername: 'Ungültige E-Mail-Adresse oder Benutzername',
|
||||
invalidPassword: 'Ungültiges Passwort',
|
||||
logInToPlanka: 'Einloggen',
|
||||
noInternetConnection: 'Keine Internetverbindung',
|
||||
pageNotFound_title: 'Seite nicht gefunden',
|
||||
password: 'Passwort',
|
||||
projectManagement: 'Projekt Management',
|
||||
projectManagement: 'Projekt-Management',
|
||||
serverConnectionFailed: 'Serververbindung fehlgeschlagen',
|
||||
unknownError: 'Unbekannter Fehler, bitte später erneut versuchen',
|
||||
},
|
||||
|
||||
@@ -92,8 +92,10 @@ export default {
|
||||
filterByLabels_title: 'Filter By Labels',
|
||||
filterByMembers_title: 'Filter By Members',
|
||||
fromComputer_title: 'From Computer',
|
||||
fromTrello: 'From Trello',
|
||||
general: 'General',
|
||||
hours: 'Hours',
|
||||
importBoard_title: 'Import Board',
|
||||
invalidCurrentPassword: 'Invalid current password',
|
||||
labels: 'Labels',
|
||||
language: 'Language',
|
||||
@@ -121,7 +123,7 @@ export default {
|
||||
phone: 'Phone',
|
||||
preferences: 'Preferences',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: press Ctrl-V (Cmd-V on Mac) to add an attachment from the clipboard.',
|
||||
'Tip: press Ctrl-V (Cmd-V on Mac) to add an attachment from the clipboard',
|
||||
project: 'Project',
|
||||
projectNotFound_title: 'Project Not Found',
|
||||
removeManager_title: 'Remove Manager',
|
||||
@@ -201,6 +203,7 @@ export default {
|
||||
editTitle_title: 'Edit Title',
|
||||
editUsername_title: 'Edit Username',
|
||||
hideDetails: 'Hide details',
|
||||
import: 'Import',
|
||||
leaveBoard: 'Leave board',
|
||||
leaveProject: 'Leave project',
|
||||
logOut_title: 'Log Out',
|
||||
|
||||
@@ -5,7 +5,7 @@ import core from './core';
|
||||
|
||||
export default {
|
||||
language: 'en',
|
||||
country: 'us',
|
||||
country: 'gb',
|
||||
name: 'English',
|
||||
embeddedLocale: merge(login, core),
|
||||
};
|
||||
|
||||
@@ -106,7 +106,7 @@ export default {
|
||||
phone: 'Teléfono',
|
||||
preferences: 'Preferencias',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: presiona Ctrl-V (Cmd-V en Mac) para añadir adjuntos desde el portapapeles.',
|
||||
'Tip: presiona Ctrl-V (Cmd-V en Mac) para añadir adjuntos desde el portapapeles',
|
||||
project: 'Proyecto',
|
||||
projectNotFound_title: 'Proyecto no encontrado',
|
||||
removeMember_title: 'Remover Miembro',
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
selectList: 'Seleccionar Lista',
|
||||
selectProject: 'Seleccionar Proyecto',
|
||||
settings: 'Configuraciones',
|
||||
subscribeToMyOwnCardsByDefault: 'Suscribirme a mis propias tarjetas por defecto.',
|
||||
subscribeToMyOwnCardsByDefault: 'Suscribirme a mis propias tarjetas por defecto',
|
||||
taskActions_title: 'Acciones de Tareas',
|
||||
tasks: 'Tareas',
|
||||
time: 'Tiempo',
|
||||
|
||||
@@ -121,7 +121,7 @@ export default {
|
||||
phone: 'Telefono',
|
||||
preferences: 'Preferenze',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: prmi Ctrl-V (Cmd-V on Mac) per aggiungere un allegato dalla clipboard.',
|
||||
'Tip: prmi Ctrl-V (Cmd-V on Mac) per aggiungere un allegato dalla clipboard',
|
||||
project: 'Progetto',
|
||||
projectNotFound_title: 'Progetto non trovato',
|
||||
removeManager_title: 'Rimuovi Manager',
|
||||
|
||||
@@ -117,7 +117,7 @@ export default {
|
||||
phone: '電話番号',
|
||||
preferences: '環境設定',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'ヒント: Ctrl-V(MacではCmd-V)を押して、クリップボードから添付ファイルを追加します。',
|
||||
'ヒント: Ctrl-V(MacではCmd-V)を押して、クリップボードから添付ファイルを追加します',
|
||||
project: 'プロジェクト',
|
||||
projectNotFound_title: 'プロジェクトがありません',
|
||||
removeManager_title: 'マネージャーを削除',
|
||||
|
||||
@@ -106,7 +106,7 @@ export default {
|
||||
phone: 'Telefon',
|
||||
preferences: 'Preferencje',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Podpowiedź: naciśnij Ctrl-V (Cmd-V na Macu) aby dodać załącznik ze schowka.',
|
||||
'Podpowiedź: naciśnij Ctrl-V (Cmd-V na Macu) aby dodać załącznik ze schowka',
|
||||
project: 'Projekt',
|
||||
projectNotFound_title: 'Projekt Nie Znaleziony',
|
||||
removeMember_title: 'Usuń Członka',
|
||||
|
||||
@@ -116,7 +116,7 @@ export default {
|
||||
phone: 'Телефон',
|
||||
preferences: 'Предпочтения',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Совет: нажмите Ctrl-V (Cmd-V на Mac), чтобы добавить вложение из буфера обмена.',
|
||||
'Совет: нажмите Ctrl-V (Cmd-V на Mac), чтобы добавить вложение из буфера обмена',
|
||||
project: 'Проект',
|
||||
projectActions: 'Действия с проектом',
|
||||
projectNotFound: 'Проект не найден',
|
||||
|
||||
@@ -118,7 +118,7 @@ export default {
|
||||
phone: 'Telefón',
|
||||
preferences: 'Voľby',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: stlačte Ctrl-V (Cmd-V na Mac) pre vloženie prílohy zo schránky.',
|
||||
'Tip: stlačte Ctrl-V (Cmd-V na Mac) pre vloženie prílohy zo schránky',
|
||||
project: 'Projekt',
|
||||
projectNotFound_title: 'Projekt neexistuje',
|
||||
removeManager_title: 'Odstrániť správcu',
|
||||
|
||||
@@ -119,7 +119,7 @@ export default {
|
||||
phone: 'Telefon',
|
||||
preferences: 'Preferenser',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tips: tryck på Ctrl-V (Cmd-V på Mac) för att lägga till en bilaga från urklipp.',
|
||||
'Tips: tryck på Ctrl-V (Cmd-V på Mac) för att lägga till en bilaga från urklipp',
|
||||
project: 'Projekt',
|
||||
projectNotFound_title: 'Projekt Ej Funnet',
|
||||
removeManager_title: 'Ta Bort Projektledare',
|
||||
|
||||
@@ -112,7 +112,7 @@ export default {
|
||||
phone: 'Telefon',
|
||||
preferences: 'Afzalliklar',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
"Tip: Buferdan ilova qo'shish uchun Ctrl-V (Mac da Cmd-V) ni bosing.",
|
||||
"Tip: Buferdan ilova qo'shish uchun Ctrl-V (Mac da Cmd-V) ni bosing",
|
||||
project: 'Loyiha',
|
||||
projectNotFound_title: 'Loyiha Topilmadi',
|
||||
removeManager_title: "Boshqaruvchini O'chirish",
|
||||
|
||||
@@ -110,7 +110,7 @@ export default {
|
||||
phone: '电话',
|
||||
preferences: '偏好',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'提示: 点击 Ctrl-V (Mac: Cmd-V) 从剪切板添加附件.',
|
||||
'提示: 点击 Ctrl-V (Mac: Cmd-V) 从剪切板添加附件',
|
||||
project: '项目',
|
||||
projectNotFound_title: '项目未找到',
|
||||
removeManager_title: '删除管理员',
|
||||
|
||||
@@ -30,11 +30,12 @@ export default class extends Model {
|
||||
static reducer({ type, payload }, Board) {
|
||||
switch (type) {
|
||||
case ActionTypes.LOCATION_CHANGE_HANDLE:
|
||||
case ActionTypes.BOARD_FETCH__SUCCESS:
|
||||
if (payload.board) {
|
||||
Board.upsert({
|
||||
...payload.board,
|
||||
isFetching: false,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
case ActionTypes.LOCATION_CHANGE_HANDLE__BOARD_FETCH:
|
||||
@@ -124,7 +125,10 @@ export default class extends Model {
|
||||
break;
|
||||
case ActionTypes.BOARD_CREATE__SUCCESS:
|
||||
Board.withId(payload.localId).delete();
|
||||
Board.upsert(payload.board);
|
||||
|
||||
break;
|
||||
case ActionTypes.BOARD_FETCH__SUCCESS:
|
||||
Board.upsert({
|
||||
...payload.board,
|
||||
isFetching: false,
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
import { getAccessToken } from '../utils/access-token-storage';
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const initialState = {
|
||||
accessToken: getAccessToken(),
|
||||
userId: null,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line default-param-last
|
||||
export default (state = initialState, { type, payload }) => {
|
||||
switch (type) {
|
||||
case ActionTypes.AUTHENTICATE__SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
accessToken: payload.accessToken,
|
||||
};
|
||||
case ActionTypes.SOCKET_RECONNECT_HANDLE:
|
||||
case ActionTypes.CORE_INITIALIZE:
|
||||
return {
|
||||
...state,
|
||||
userId: payload.user.id,
|
||||
};
|
||||
case ActionTypes.USER_PASSWORD_UPDATE__SUCCESS:
|
||||
if (payload.accessToken) {
|
||||
return {
|
||||
...state,
|
||||
accessToken: payload.accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { LOCATION_CHANGE } from 'connected-react-router';
|
||||
import { LOCATION_CHANGE_HANDLE } from '../lib/redux-router';
|
||||
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
import ModalTypes from '../constants/ModalTypes';
|
||||
|
||||
const initialState = {
|
||||
isInitializing: true,
|
||||
isLogouting: false,
|
||||
currentModal: null,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line default-param-last
|
||||
export default (state = initialState, { type, payload }) => {
|
||||
switch (type) {
|
||||
case LOCATION_CHANGE:
|
||||
case LOCATION_CHANGE_HANDLE:
|
||||
case ActionTypes.MODAL_CLOSE:
|
||||
return {
|
||||
...state,
|
||||
@@ -22,6 +23,11 @@ export default (state = initialState, { type, payload }) => {
|
||||
...state,
|
||||
isInitializing: false,
|
||||
};
|
||||
case ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE:
|
||||
return {
|
||||
...state,
|
||||
isLogouting: true,
|
||||
};
|
||||
case ActionTypes.MODAL_OPEN:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { connectRouter } from 'connected-react-router';
|
||||
import { createRouterReducer } from '../lib/redux-router';
|
||||
|
||||
import history from '../history';
|
||||
|
||||
export default connectRouter(history);
|
||||
export default createRouterReducer(history);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { all, apply, call, fork, take } from 'redux-saga/effects';
|
||||
import { all, apply, fork, take } from 'redux-saga/effects';
|
||||
|
||||
import watchers from './watchers';
|
||||
import services from './services';
|
||||
import { socket } from '../../api';
|
||||
import { removeAccessToken } from '../../utils/access-token-storage';
|
||||
import ActionTypes from '../../constants/ActionTypes';
|
||||
import Paths from '../../constants/Paths';
|
||||
|
||||
@@ -15,6 +14,5 @@ export default function* coreSaga() {
|
||||
|
||||
yield take(ActionTypes.LOGOUT);
|
||||
|
||||
yield call(removeAccessToken);
|
||||
window.location.href = Paths.LOGIN;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { call, fork, join, put, take } from 'redux-saga/effects';
|
||||
import { call, fork, join, put, select, take } from 'redux-saga/effects';
|
||||
|
||||
import selectors from '../../selectors';
|
||||
import actions from '../../actions';
|
||||
import { getAccessToken } from '../../utils/access-token-storage';
|
||||
import { removeAccessToken } from '../../utils/access-token-storage';
|
||||
import ErrorCodes from '../../constants/ErrorCodes';
|
||||
|
||||
let lastRequestTask;
|
||||
@@ -13,7 +14,7 @@ function* queueRequest(method, ...args) {
|
||||
} catch {} // eslint-disable-line no-empty
|
||||
}
|
||||
|
||||
const accessToken = yield call(getAccessToken);
|
||||
const accessToken = yield select(selectors.selectAccessToken);
|
||||
|
||||
try {
|
||||
return yield call(method, ...args, {
|
||||
@@ -21,6 +22,7 @@ function* queueRequest(method, ...args) {
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === ErrorCodes.UNAUTHORIZED) {
|
||||
yield call(removeAccessToken);
|
||||
yield put(actions.logout()); // TODO: next url
|
||||
yield take();
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ export function* fetchBoardByCurrentPath() {
|
||||
|
||||
if (pathsMatch) {
|
||||
let boardId;
|
||||
if (pathsMatch.path === Paths.BOARDS) {
|
||||
if (pathsMatch.pattern.path === Paths.BOARDS) {
|
||||
boardId = pathsMatch.params.id;
|
||||
} else if (pathsMatch.path === Paths.CARDS) {
|
||||
} else if (pathsMatch.pattern.path === Paths.CARDS) {
|
||||
({
|
||||
item: card,
|
||||
item: { boardId },
|
||||
|
||||
@@ -7,7 +7,7 @@ import actions from '../../../actions';
|
||||
import api from '../../../api';
|
||||
import { createLocalId } from '../../../utils/local-id';
|
||||
|
||||
export function* createBoard(projectId, data) {
|
||||
export function* createBoard(projectId, { import: boardImport, ...data }) {
|
||||
const nextData = {
|
||||
...data,
|
||||
position: yield select(selectors.selectNextBoardPosition, projectId),
|
||||
@@ -30,7 +30,19 @@ export function* createBoard(projectId, data) {
|
||||
({
|
||||
item: board,
|
||||
included: { boardMemberships },
|
||||
} = yield call(request, api.createBoard, projectId, nextData));
|
||||
} = yield boardImport
|
||||
? call(
|
||||
request,
|
||||
api.createBoardWithImport,
|
||||
projectId,
|
||||
{
|
||||
...nextData,
|
||||
importType: boardImport.type,
|
||||
importFile: boardImport.file,
|
||||
},
|
||||
localId,
|
||||
)
|
||||
: call(request, api.createBoard, projectId, nextData));
|
||||
} catch (error) {
|
||||
yield put(actions.createBoard.failure(localId, error));
|
||||
return;
|
||||
@@ -46,9 +58,13 @@ export function* createBoardInCurrentProject(data) {
|
||||
yield call(createBoard, projectId, data);
|
||||
}
|
||||
|
||||
export function* handleBoardCreate(board) {
|
||||
export function* handleBoardCreate(board, requestId) {
|
||||
const isExists = yield select(selectors.selectIsBoardWithIdExists, requestId);
|
||||
|
||||
if (!isExists) {
|
||||
yield put(actions.handleBoardCreate(board));
|
||||
}
|
||||
}
|
||||
|
||||
export function* fetchBoard(id) {
|
||||
yield put(actions.fetchBoard(id));
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { call, put, take } from 'redux-saga/effects';
|
||||
|
||||
import request from '../request';
|
||||
import requests from '../requests';
|
||||
import actions from '../../../actions';
|
||||
import api from '../../../api';
|
||||
import i18n from '../../../i18n';
|
||||
import { removeAccessToken } from '../../../utils/access-token-storage';
|
||||
|
||||
export function* initializeCore() {
|
||||
const {
|
||||
@@ -60,7 +63,17 @@ export function* changeCoreLanguage(language) {
|
||||
}
|
||||
}
|
||||
|
||||
export function* logout() {
|
||||
export function* logout(invalidateAccessToken = true) {
|
||||
yield call(removeAccessToken);
|
||||
|
||||
if (invalidateAccessToken) {
|
||||
yield put(actions.logout.invalidateAccessToken());
|
||||
|
||||
try {
|
||||
yield call(request, api.deleteCurrentAccessToken);
|
||||
} catch (error) {} // eslint-disable-line no-empty
|
||||
}
|
||||
|
||||
yield put(actions.logout());
|
||||
yield take();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { call, put, select, take } from 'redux-saga/effects';
|
||||
import { push } from 'connected-react-router';
|
||||
import { push } from '../../../lib/redux-router';
|
||||
|
||||
import request from '../request';
|
||||
import selectors from '../../../selectors';
|
||||
@@ -31,7 +31,7 @@ export function* handleLocationChange() {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (pathsMatch.path) {
|
||||
switch (pathsMatch.pattern.path) {
|
||||
case Paths.LOGIN:
|
||||
yield call(goToRoot);
|
||||
|
||||
@@ -58,7 +58,7 @@ export function* handleLocationChange() {
|
||||
let attachments;
|
||||
let deletedNotifications;
|
||||
|
||||
switch (pathsMatch.path) {
|
||||
switch (pathsMatch.pattern.path) {
|
||||
case Paths.BOARDS:
|
||||
case Paths.CARDS: {
|
||||
const currentBoard = yield select(selectors.selectCurrentBoard);
|
||||
@@ -85,7 +85,7 @@ export function* handleLocationChange() {
|
||||
} catch (error) {} // eslint-disable-line no-empty
|
||||
}
|
||||
|
||||
if (pathsMatch.path === Paths.CARDS) {
|
||||
if (pathsMatch.pattern.path === Paths.CARDS) {
|
||||
const notificationIds = yield select(selectors.selectNotificationIdsForCurrentCard);
|
||||
|
||||
if (notificationIds && notificationIds.length > 0) {
|
||||
|
||||
@@ -124,11 +124,13 @@ export function* updateUserPassword(id, data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (accessTokens && accessTokens[0]) {
|
||||
yield call(setAccessToken, accessTokens[0]);
|
||||
const accessToken = accessTokens && accessTokens[0];
|
||||
|
||||
if (accessToken) {
|
||||
yield call(setAccessToken, accessToken);
|
||||
}
|
||||
|
||||
yield put(actions.updateUserPassword.success(user));
|
||||
yield put(actions.updateUserPassword.success(user, accessToken));
|
||||
}
|
||||
|
||||
export function* updateCurrentUserPassword(data) {
|
||||
@@ -215,7 +217,7 @@ export function* handleUserDelete(user) {
|
||||
const currentUserId = yield select(selectors.selectCurrentUserId);
|
||||
|
||||
if (user.id === currentUserId) {
|
||||
yield call(logout);
|
||||
yield call(logout, false);
|
||||
}
|
||||
|
||||
yield put(actions.handleUserDelete(user));
|
||||
|
||||
@@ -8,8 +8,8 @@ export default function* boardsWatchers() {
|
||||
takeEvery(EntryActionTypes.BOARD_IN_CURRENT_PROJECT_CREATE, ({ payload: { data } }) =>
|
||||
services.createBoardInCurrentProject(data),
|
||||
),
|
||||
takeEvery(EntryActionTypes.BOARD_CREATE_HANDLE, ({ payload: { board } }) =>
|
||||
services.handleBoardCreate(board),
|
||||
takeEvery(EntryActionTypes.BOARD_CREATE_HANDLE, ({ payload: { board, requestId } }) =>
|
||||
services.handleBoardCreate(board, requestId),
|
||||
),
|
||||
takeEvery(EntryActionTypes.BOARD_FETCH, ({ payload: { id } }) => services.fetchBoard(id)),
|
||||
takeEvery(EntryActionTypes.BOARD_UPDATE, ({ payload: { id, data } }) =>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { takeEvery } from 'redux-saga/effects';
|
||||
import { LOCATION_CHANGE } from 'connected-react-router';
|
||||
import { LOCATION_CHANGE_HANDLE } from '../../../lib/redux-router';
|
||||
|
||||
import services from '../services';
|
||||
|
||||
export default function* routerWatchers() {
|
||||
yield takeEvery(LOCATION_CHANGE, () => services.handleLocationChange());
|
||||
yield takeEvery(LOCATION_CHANGE_HANDLE, () => services.handleLocationChange());
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ const createSocketEventsChannel = () =>
|
||||
emit(entryActions.handleProjectManagerDelete(item));
|
||||
};
|
||||
|
||||
const handleBoardCreate = ({ item }) => {
|
||||
emit(entryActions.handleBoardCreate(item));
|
||||
const handleBoardCreate = ({ item, requestId }) => {
|
||||
emit(entryActions.handleBoardCreate(item, requestId));
|
||||
};
|
||||
|
||||
const handleBoardUpdate = ({ item }) => {
|
||||
|
||||
@@ -2,18 +2,13 @@ import { all, call, cancel, fork, take } from 'redux-saga/effects';
|
||||
|
||||
import watchers from './watchers';
|
||||
import services from './services';
|
||||
import { setAccessToken } from '../../utils/access-token-storage';
|
||||
import ActionTypes from '../../constants/ActionTypes';
|
||||
|
||||
export default function* loginSaga() {
|
||||
const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));
|
||||
|
||||
const {
|
||||
payload: { accessToken },
|
||||
} = yield take(ActionTypes.AUTHENTICATE__SUCCESS);
|
||||
yield take(ActionTypes.AUTHENTICATE__SUCCESS);
|
||||
|
||||
yield cancel(watcherTasks);
|
||||
|
||||
yield call(setAccessToken, accessToken);
|
||||
yield call(services.goToRoot);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { call, put } from 'redux-saga/effects';
|
||||
|
||||
import actions from '../../../actions';
|
||||
import api from '../../../api';
|
||||
import { setAccessToken } from '../../../utils/access-token-storage';
|
||||
|
||||
export function* authenticate(data) {
|
||||
yield put(actions.authenticate(data));
|
||||
@@ -14,6 +15,7 @@ export function* authenticate(data) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield call(setAccessToken, accessToken);
|
||||
yield put(actions.authenticate.success(accessToken));
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user