Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
fd8f40f1b0 | ||
|
|
5b64d465e2 | ||
|
|
212fb8ac8c | ||
|
|
cece2254d7 |
+1
-1
@@ -26,7 +26,7 @@ COPY server .
|
|||||||
RUN cp .env.sample .env
|
RUN cp .env.sample .env
|
||||||
|
|
||||||
COPY --from=client-builder /app/build public
|
COPY --from=client-builder /app/build public
|
||||||
COPY --from=client-builder /app/build/index.html views
|
COPY --from=client-builder /app/build/index.html views/index.ejs
|
||||||
|
|
||||||
VOLUME /app/public/user-avatars
|
VOLUME /app/public/user-avatars
|
||||||
VOLUME /app/public/project-background-images
|
VOLUME /app/public/project-background-images
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ A Trello-like kanban board built with React and Redux.
|
|||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
### Docker Compose
|
There are 2 types of installation:
|
||||||
|
1. [Dockerized](#1-docker-compose)
|
||||||
|
2. [Without Docker](#2-without-docker)
|
||||||
|
|
||||||
|
|
||||||
|
### 1. Docker Compose
|
||||||
|
|
||||||
[](https://hub.docker.com/r/meltyshev/planka)
|
[](https://hub.docker.com/r/meltyshev/planka)
|
||||||
|
|
||||||
@@ -44,9 +49,146 @@ docker-compose up -d
|
|||||||
|
|
||||||
Demo user: demo@demo.demo demo
|
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
|
### 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`.
|
To expose the logfile director to the host machine, add the item `./logs/:/app/logs/` under `services.planka.volumes`.
|
||||||
|
|
||||||
|
|||||||
@@ -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
+206
-1
@@ -6,10 +6,12 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "planka-client",
|
"name": "planka-client",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"connected-react-router": "^6.9.3",
|
"connected-react-router": "^6.9.3",
|
||||||
"date-fns": "^2.29.1",
|
"date-fns": "^2.29.1",
|
||||||
"dequal": "^2.0.3",
|
"dequal": "^2.0.3",
|
||||||
|
"easymde": "^2.18.0",
|
||||||
"history": "^4.10.1",
|
"history": "^4.10.1",
|
||||||
"i18next": "^21.8.14",
|
"i18next": "^21.8.14",
|
||||||
"i18next-browser-languagedetector": "^6.1.4",
|
"i18next-browser-languagedetector": "^6.1.4",
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
"react-redux": "^7.2.8",
|
"react-redux": "^7.2.8",
|
||||||
"react-router-dom": "^5.3.1",
|
"react-router-dom": "^5.3.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"react-simplemde-editor": "^5.2.0",
|
||||||
"react-textarea-autosize": "^8.3.4",
|
"react-textarea-autosize": "^8.3.4",
|
||||||
"redux": "^4.2.0",
|
"redux": "^4.2.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
@@ -44,7 +47,8 @@
|
|||||||
"semantic-ui-react": "^2.1.3",
|
"semantic-ui-react": "^2.1.3",
|
||||||
"socket.io-client": "^2.3.1",
|
"socket.io-client": "^2.3.1",
|
||||||
"validator": "^13.7.0",
|
"validator": "^13.7.0",
|
||||||
"whatwg-fetch": "^3.6.2"
|
"whatwg-fetch": "^3.6.2",
|
||||||
|
"zxcvbn": "^4.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
|
||||||
@@ -61,6 +65,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"jest-enzyme": "^7.1.2",
|
"jest-enzyme": "^7.1.2",
|
||||||
"prettier": "2.7.1",
|
"prettier": "2.7.1",
|
||||||
|
"react-app-rewired": "^2.2.1",
|
||||||
"react-test-renderer": "^17.0.2"
|
"react-test-renderer": "^17.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3029,6 +3034,11 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@juggle/resize-observer": {
|
||||||
|
"version": "3.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||||
|
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||||
|
},
|
||||||
"node_modules/@leichtgewicht/ip-codec": {
|
"node_modules/@leichtgewicht/ip-codec": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
||||||
@@ -3687,6 +3697,14 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/codemirror": {
|
||||||
|
"version": "5.60.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz",
|
||||||
|
"integrity": "sha512-TiECZmm8St5YxjFUp64LK0c8WU5bxMDt9YaAek1UqUb9swrSCoJhh92fWu1p3mTEqlHjhB5sY7OFBhWroJXZVg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/tern": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/connect": {
|
"node_modules/@types/connect": {
|
||||||
"version": "3.4.35",
|
"version": "3.4.35",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||||
@@ -3825,6 +3843,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
|
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/marked": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw=="
|
||||||
|
},
|
||||||
"node_modules/@types/mdast": {
|
"node_modules/@types/mdast": {
|
||||||
"version": "3.0.10",
|
"version": "3.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
|
||||||
@@ -3962,6 +3985,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
|
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/tern": {
|
||||||
|
"version": "0.23.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz",
|
||||||
|
"integrity": "sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
|
||||||
@@ -6164,6 +6195,19 @@
|
|||||||
"node": ">= 4.0"
|
"node": ">= 4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codemirror": {
|
||||||
|
"version": "5.65.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.9.tgz",
|
||||||
|
"integrity": "sha512-19Jox5sAKpusTDgqgKB5dawPpQcY+ipQK7xoEI+MVucEF9qqFaXpeqY1KaoyGBso/wHQoDa4HMMxMjdsS3Zzzw=="
|
||||||
|
},
|
||||||
|
"node_modules/codemirror-spell-checker": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-2Tl6n0v+GJRsC9K3MLCdLaMOmvWL0uukajNJseorZJsslaxZyZMgENocPU8R0DyoTAiKsyqiemSOZo7kjGV0LQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"typo-js": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/collect-v8-coverage": {
|
"node_modules/collect-v8-coverage": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
||||||
@@ -7411,6 +7455,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/easymde": {
|
||||||
|
"version": "2.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/easymde/-/easymde-2.18.0.tgz",
|
||||||
|
"integrity": "sha512-IxVVUxNWIoXLeqtBU4BLc+eS/ScYhT1Dcb6yF5Wchoj1iXAV+TIIDWx+NCaZhY7RcSHqDPKllbYq7nwGKILnoA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/codemirror": "^5.60.4",
|
||||||
|
"@types/marked": "^4.0.7",
|
||||||
|
"codemirror": "^5.63.1",
|
||||||
|
"codemirror-spell-checker": "1.1.2",
|
||||||
|
"marked": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ecc-jsbn": {
|
"node_modules/ecc-jsbn": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||||
@@ -14755,6 +14811,17 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/marked": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-JjBTFTAvuTgANXx82a5vzK9JLSMoV6V3LBVn4Uhdso6t7vXrGx7g1Cd2r6NYSsxrYbQGFCMqBDhFHyK5q2UvcQ==",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mdast-util-definitions": {
|
"node_modules/mdast-util-definitions": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.1.tgz",
|
||||||
@@ -18658,6 +18725,30 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-app-rewired": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-app-rewired/-/react-app-rewired-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-uFQWTErXeLDrMzOJHKp0h8P1z0LV9HzPGsJ6adOtGlA/B9WfT6Shh4j2tLTTGlXOfiVx6w6iWpp7SOC5pvk+gA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^5.6.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"react-app-rewired": "bin/index.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-scripts": ">=2.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-app-rewired/node_modules/semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-beautiful-dnd": {
|
"node_modules/react-beautiful-dnd": {
|
||||||
"version": "13.1.0",
|
"version": "13.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz",
|
||||||
@@ -20484,6 +20575,19 @@
|
|||||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-simplemde-editor": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-simplemde-editor/-/react-simplemde-editor-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-GkTg1MlQHVK2Rks++7sjuQr/GVS/xm6y+HchZ4GPBWrhcgLieh4CjK04GTKbsfYorSRYKa0n37rtNSJmOzEDkQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/codemirror": "~5.60.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"easymde": ">= 2.0.0 < 3.0.0",
|
||||||
|
"react": ">=16.8.2",
|
||||||
|
"react-dom": ">=16.8.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-test-renderer": {
|
"node_modules/react-test-renderer": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz",
|
||||||
@@ -23841,6 +23945,11 @@
|
|||||||
"typescript-compare": "^0.0.2"
|
"typescript-compare": "^0.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typo-js": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-C7pYBQK17EjSg8tVNY91KHdUt5Nf6FMJ+c3js076quPmBML57PmNMzAcIq/2kf/hSYtFABNDIYNYlJRl5BJhGw=="
|
||||||
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
|
||||||
@@ -25310,6 +25419,11 @@
|
|||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zxcvbn": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ=="
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -27327,6 +27441,11 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@juggle/resize-observer": {
|
||||||
|
"version": "3.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||||
|
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||||
|
},
|
||||||
"@leichtgewicht/ip-codec": {
|
"@leichtgewicht/ip-codec": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
||||||
@@ -27772,6 +27891,14 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/codemirror": {
|
||||||
|
"version": "5.60.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz",
|
||||||
|
"integrity": "sha512-TiECZmm8St5YxjFUp64LK0c8WU5bxMDt9YaAek1UqUb9swrSCoJhh92fWu1p3mTEqlHjhB5sY7OFBhWroJXZVg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/tern": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/connect": {
|
"@types/connect": {
|
||||||
"version": "3.4.35",
|
"version": "3.4.35",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||||
@@ -27910,6 +28037,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
|
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
|
||||||
},
|
},
|
||||||
|
"@types/marked": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw=="
|
||||||
|
},
|
||||||
"@types/mdast": {
|
"@types/mdast": {
|
||||||
"version": "3.0.10",
|
"version": "3.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
|
||||||
@@ -28047,6 +28179,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
|
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
|
||||||
},
|
},
|
||||||
|
"@types/tern": {
|
||||||
|
"version": "0.23.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz",
|
||||||
|
"integrity": "sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/estree": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/trusted-types": {
|
"@types/trusted-types": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
|
||||||
@@ -29691,6 +29831,19 @@
|
|||||||
"q": "^1.1.2"
|
"q": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codemirror": {
|
||||||
|
"version": "5.65.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.9.tgz",
|
||||||
|
"integrity": "sha512-19Jox5sAKpusTDgqgKB5dawPpQcY+ipQK7xoEI+MVucEF9qqFaXpeqY1KaoyGBso/wHQoDa4HMMxMjdsS3Zzzw=="
|
||||||
|
},
|
||||||
|
"codemirror-spell-checker": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-2Tl6n0v+GJRsC9K3MLCdLaMOmvWL0uukajNJseorZJsslaxZyZMgENocPU8R0DyoTAiKsyqiemSOZo7kjGV0LQ==",
|
||||||
|
"requires": {
|
||||||
|
"typo-js": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"collect-v8-coverage": {
|
"collect-v8-coverage": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
||||||
@@ -30603,6 +30756,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
||||||
},
|
},
|
||||||
|
"easymde": {
|
||||||
|
"version": "2.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/easymde/-/easymde-2.18.0.tgz",
|
||||||
|
"integrity": "sha512-IxVVUxNWIoXLeqtBU4BLc+eS/ScYhT1Dcb6yF5Wchoj1iXAV+TIIDWx+NCaZhY7RcSHqDPKllbYq7nwGKILnoA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/codemirror": "^5.60.4",
|
||||||
|
"@types/marked": "^4.0.7",
|
||||||
|
"codemirror": "^5.63.1",
|
||||||
|
"codemirror-spell-checker": "1.1.2",
|
||||||
|
"marked": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ecc-jsbn": {
|
"ecc-jsbn": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||||
@@ -36130,6 +36295,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.2.tgz",
|
||||||
"integrity": "sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA=="
|
"integrity": "sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA=="
|
||||||
},
|
},
|
||||||
|
"marked": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-JjBTFTAvuTgANXx82a5vzK9JLSMoV6V3LBVn4Uhdso6t7vXrGx7g1Cd2r6NYSsxrYbQGFCMqBDhFHyK5q2UvcQ=="
|
||||||
|
},
|
||||||
"mdast-util-definitions": {
|
"mdast-util-definitions": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.1.tgz",
|
||||||
@@ -38700,6 +38870,23 @@
|
|||||||
"whatwg-fetch": "^3.6.2"
|
"whatwg-fetch": "^3.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-app-rewired": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-app-rewired/-/react-app-rewired-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-uFQWTErXeLDrMzOJHKp0h8P1z0LV9HzPGsJ6adOtGlA/B9WfT6Shh4j2tLTTGlXOfiVx6w6iWpp7SOC5pvk+gA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"semver": "^5.6.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-beautiful-dnd": {
|
"react-beautiful-dnd": {
|
||||||
"version": "13.1.0",
|
"version": "13.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz",
|
||||||
@@ -40061,6 +40248,14 @@
|
|||||||
"react-is": "^16.12.0 || ^17.0.0 || ^18.0.0"
|
"react-is": "^16.12.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-simplemde-editor": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-simplemde-editor/-/react-simplemde-editor-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-GkTg1MlQHVK2Rks++7sjuQr/GVS/xm6y+HchZ4GPBWrhcgLieh4CjK04GTKbsfYorSRYKa0n37rtNSJmOzEDkQ==",
|
||||||
|
"requires": {
|
||||||
|
"@types/codemirror": "~5.60.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-test-renderer": {
|
"react-test-renderer": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz",
|
||||||
@@ -42677,6 +42872,11 @@
|
|||||||
"typescript-compare": "^0.0.2"
|
"typescript-compare": "^0.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"typo-js": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-C7pYBQK17EjSg8tVNY91KHdUt5Nf6FMJ+c3js076quPmBML57PmNMzAcIq/2kf/hSYtFABNDIYNYlJRl5BJhGw=="
|
||||||
|
},
|
||||||
"unbox-primitive": {
|
"unbox-primitive": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
|
||||||
@@ -43805,6 +44005,11 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz",
|
||||||
"integrity": "sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA=="
|
"integrity": "sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA=="
|
||||||
|
},
|
||||||
|
"zxcvbn": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-5
@@ -2,11 +2,11 @@
|
|||||||
"name": "planka-client",
|
"name": "planka-client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "react-scripts build",
|
"build": "react-app-rewired build",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"lint": "eslint --ext js,jsx src",
|
"lint": "eslint --ext js,jsx src config-overrides.js",
|
||||||
"start": "react-scripts start",
|
"start": "react-app-rewired start",
|
||||||
"test": "react-scripts test"
|
"test": "react-app-rewired test"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
@@ -63,10 +63,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"connected-react-router": "^6.9.3",
|
"connected-react-router": "^6.9.3",
|
||||||
"date-fns": "^2.29.1",
|
"date-fns": "^2.29.1",
|
||||||
"dequal": "^2.0.3",
|
"dequal": "^2.0.3",
|
||||||
|
"easymde": "^2.18.0",
|
||||||
"history": "^4.10.1",
|
"history": "^4.10.1",
|
||||||
"i18next": "^21.8.14",
|
"i18next": "^21.8.14",
|
||||||
"i18next-browser-languagedetector": "^6.1.4",
|
"i18next-browser-languagedetector": "^6.1.4",
|
||||||
@@ -89,6 +91,7 @@
|
|||||||
"react-redux": "^7.2.8",
|
"react-redux": "^7.2.8",
|
||||||
"react-router-dom": "^5.3.1",
|
"react-router-dom": "^5.3.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"react-simplemde-editor": "^5.2.0",
|
||||||
"react-textarea-autosize": "^8.3.4",
|
"react-textarea-autosize": "^8.3.4",
|
||||||
"redux": "^4.2.0",
|
"redux": "^4.2.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
@@ -101,7 +104,8 @@
|
|||||||
"semantic-ui-react": "^2.1.3",
|
"semantic-ui-react": "^2.1.3",
|
||||||
"socket.io-client": "^2.3.1",
|
"socket.io-client": "^2.3.1",
|
||||||
"validator": "^13.7.0",
|
"validator": "^13.7.0",
|
||||||
"whatwg-fetch": "^3.6.2"
|
"whatwg-fetch": "^3.6.2",
|
||||||
|
"zxcvbn": "^4.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
|
||||||
@@ -118,6 +122,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"jest-enzyme": "^7.1.2",
|
"jest-enzyme": "^7.1.2",
|
||||||
"prettier": "2.7.1",
|
"prettier": "2.7.1",
|
||||||
|
"react-app-rewired": "^2.2.1",
|
||||||
"react-test-renderer": "^17.0.2"
|
"react-test-renderer": "^17.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
-->
|
-->
|
||||||
<title>Planka</title>
|
<title>Planka</title>
|
||||||
</head>
|
</head>
|
||||||
|
<script>window.BASE_URL = "%PUBLIC_URL%";</script>
|
||||||
<body id="app">
|
<body id="app">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ const logout = () => ({
|
|||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logout.invalidateAccessToken = () => ({
|
||||||
|
type: ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE,
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
initializeCore,
|
initializeCore,
|
||||||
logout,
|
logout,
|
||||||
|
|||||||
@@ -103,10 +103,11 @@ const updateUserPassword = (id, data) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
updateUserPassword.success = (user) => ({
|
updateUserPassword.success = (user, accessToken) => ({
|
||||||
type: ActionTypes.USER_PASSWORD_UPDATE__SUCCESS,
|
type: ActionTypes.USER_PASSWORD_UPDATE__SUCCESS,
|
||||||
payload: {
|
payload: {
|
||||||
user,
|
user,
|
||||||
|
accessToken,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import http from './http';
|
import http from './http';
|
||||||
|
import socket from './socket';
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
|
|
||||||
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
|
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
|
||||||
|
|
||||||
|
const deleteCurrentAccessToken = (headers) =>
|
||||||
|
socket.delete('/access-tokens/me', undefined, headers);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createAccessToken,
|
createAccessToken,
|
||||||
|
deleteCurrentAccessToken,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Config from '../constants/Config';
|
|||||||
|
|
||||||
const io = sailsIOClient(socketIOClient);
|
const io = sailsIOClient(socketIOClient);
|
||||||
|
|
||||||
io.sails.url = Config.SERVER_BASE_URL;
|
io.sails.url = Config.SERVER_HOST_NAME;
|
||||||
io.sails.autoConnect = false;
|
io.sails.autoConnect = false;
|
||||||
io.sails.reconnection = true;
|
io.sails.reconnection = true;
|
||||||
io.sails.useCORSRouteToGetCookie = false;
|
io.sails.useCORSRouteToGetCookie = false;
|
||||||
@@ -13,6 +13,7 @@ io.sails.environment = process.env.NODE_ENV;
|
|||||||
|
|
||||||
const { socket } = io;
|
const { socket } = io;
|
||||||
|
|
||||||
|
socket.path = `${Config.BASE_PATH}/socket.io`;
|
||||||
socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
|
socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
|
||||||
|
|
||||||
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => {
|
['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,
|
filterUsers,
|
||||||
filterLabels,
|
filterLabels,
|
||||||
allUsers,
|
allUsers,
|
||||||
|
canEdit,
|
||||||
canEditMemberships,
|
canEditMemberships,
|
||||||
onMembershipCreate,
|
onMembershipCreate,
|
||||||
onMembershipUpdate,
|
onMembershipUpdate,
|
||||||
@@ -27,32 +28,35 @@ const BoardActions = React.memo(
|
|||||||
onLabelDelete,
|
onLabelDelete,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.actions}>
|
<div className={styles.wrapper}>
|
||||||
<div className={styles.action}>
|
<div className={styles.actions}>
|
||||||
<Memberships
|
<div className={styles.action}>
|
||||||
items={memberships}
|
<Memberships
|
||||||
allUsers={allUsers}
|
items={memberships}
|
||||||
permissionsSelectStep={BoardMembershipPermissionsSelectStep}
|
allUsers={allUsers}
|
||||||
canEdit={canEditMemberships}
|
permissionsSelectStep={BoardMembershipPermissionsSelectStep}
|
||||||
onCreate={onMembershipCreate}
|
canEdit={canEditMemberships}
|
||||||
onUpdate={onMembershipUpdate}
|
onCreate={onMembershipCreate}
|
||||||
onDelete={onMembershipDelete}
|
onUpdate={onMembershipUpdate}
|
||||||
/>
|
onDelete={onMembershipDelete}
|
||||||
</div>
|
/>
|
||||||
<div className={styles.action}>
|
</div>
|
||||||
<Filters
|
<div className={styles.action}>
|
||||||
users={filterUsers}
|
<Filters
|
||||||
labels={filterLabels}
|
users={filterUsers}
|
||||||
allBoardMemberships={memberships}
|
labels={filterLabels}
|
||||||
allLabels={labels}
|
allBoardMemberships={memberships}
|
||||||
onUserAdd={onUserToFilterAdd}
|
allLabels={labels}
|
||||||
onUserRemove={onUserFromFilterRemove}
|
canEdit={canEdit}
|
||||||
onLabelAdd={onLabelToFilterAdd}
|
onUserAdd={onUserToFilterAdd}
|
||||||
onLabelRemove={onLabelFromFilterRemove}
|
onUserRemove={onUserFromFilterRemove}
|
||||||
onLabelCreate={onLabelCreate}
|
onLabelAdd={onLabelToFilterAdd}
|
||||||
onLabelUpdate={onLabelUpdate}
|
onLabelRemove={onLabelFromFilterRemove}
|
||||||
onLabelDelete={onLabelDelete}
|
onLabelCreate={onLabelCreate}
|
||||||
/>
|
onLabelUpdate={onLabelUpdate}
|
||||||
|
onLabelDelete={onLabelDelete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -67,6 +71,7 @@ BoardActions.propTypes = {
|
|||||||
filterLabels: PropTypes.array.isRequired,
|
filterLabels: PropTypes.array.isRequired,
|
||||||
allUsers: PropTypes.array.isRequired,
|
allUsers: PropTypes.array.isRequired,
|
||||||
/* eslint-enable react/forbid-prop-types */
|
/* eslint-enable react/forbid-prop-types */
|
||||||
|
canEdit: PropTypes.bool.isRequired,
|
||||||
canEditMemberships: PropTypes.bool.isRequired,
|
canEditMemberships: PropTypes.bool.isRequired,
|
||||||
onMembershipCreate: PropTypes.func.isRequired,
|
onMembershipCreate: PropTypes.func.isRequired,
|
||||||
onMembershipUpdate: PropTypes.func.isRequired,
|
onMembershipUpdate: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
:global(#app) {
|
:global(#app) {
|
||||||
.action {
|
.action {
|
||||||
|
flex: 0 0 auto;
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8,4 +9,15 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
margin: 20px 20px;
|
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,
|
labels,
|
||||||
allBoardMemberships,
|
allBoardMemberships,
|
||||||
allLabels,
|
allLabels,
|
||||||
|
canEdit,
|
||||||
onUserAdd,
|
onUserAdd,
|
||||||
onUserRemove,
|
onUserRemove,
|
||||||
onLabelAdd,
|
onLabelAdd,
|
||||||
@@ -74,6 +75,7 @@ const Filters = React.memo(
|
|||||||
title={t('common.filterByLabels', {
|
title={t('common.filterByLabels', {
|
||||||
context: 'title',
|
context: 'title',
|
||||||
})}
|
})}
|
||||||
|
canEdit={canEdit}
|
||||||
onSelect={onLabelAdd}
|
onSelect={onLabelAdd}
|
||||||
onDeselect={onLabelRemove}
|
onDeselect={onLabelRemove}
|
||||||
onCreate={onLabelCreate}
|
onCreate={onLabelCreate}
|
||||||
@@ -108,6 +110,7 @@ Filters.propTypes = {
|
|||||||
allBoardMemberships: PropTypes.array.isRequired,
|
allBoardMemberships: PropTypes.array.isRequired,
|
||||||
allLabels: PropTypes.array.isRequired,
|
allLabels: PropTypes.array.isRequired,
|
||||||
/* eslint-enable react/forbid-prop-types */
|
/* eslint-enable react/forbid-prop-types */
|
||||||
|
canEdit: PropTypes.bool.isRequired,
|
||||||
onUserAdd: PropTypes.func.isRequired,
|
onUserAdd: PropTypes.func.isRequired,
|
||||||
onUserRemove: PropTypes.func.isRequired,
|
onUserRemove: PropTypes.func.isRequired,
|
||||||
onLabelAdd: PropTypes.func.isRequired,
|
onLabelAdd: PropTypes.func.isRequired,
|
||||||
|
|||||||
+3
-3
@@ -19,7 +19,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
|
|||||||
...defaultData,
|
...defaultData,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleRoleSelectClick = useCallback((role) => {
|
const handleSelectRoleClick = useCallback((role) => {
|
||||||
setData((prevData) => ({
|
setData((prevData) => ({
|
||||||
...prevData,
|
...prevData,
|
||||||
role,
|
role,
|
||||||
@@ -50,7 +50,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
|
|||||||
<Menu secondary vertical className={styles.menu}>
|
<Menu secondary vertical className={styles.menu}>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
active={data.role === BoardMembershipRoles.EDITOR}
|
active={data.role === BoardMembershipRoles.EDITOR}
|
||||||
onClick={() => handleRoleSelectClick(BoardMembershipRoles.EDITOR)}
|
onClick={() => handleSelectRoleClick(BoardMembershipRoles.EDITOR)}
|
||||||
>
|
>
|
||||||
<div className={styles.menuItemTitle}>{t('common.editor')}</div>
|
<div className={styles.menuItemTitle}>{t('common.editor')}</div>
|
||||||
<div className={styles.menuItemDescription}>
|
<div className={styles.menuItemDescription}>
|
||||||
@@ -59,7 +59,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
active={data.role === BoardMembershipRoles.VIEWER}
|
active={data.role === BoardMembershipRoles.VIEWER}
|
||||||
onClick={() => handleRoleSelectClick(BoardMembershipRoles.VIEWER)}
|
onClick={() => handleSelectRoleClick(BoardMembershipRoles.VIEWER)}
|
||||||
>
|
>
|
||||||
<div className={styles.menuItemTitle}>{t('common.viewer')}</div>
|
<div className={styles.menuItemTitle}>{t('common.viewer')}</div>
|
||||||
<div className={styles.menuItemDescription}>{t('common.canOnlyViewBoard')}</div>
|
<div className={styles.menuItemDescription}>{t('common.canOnlyViewBoard')}</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Button, Icon } from 'semantic-ui-react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Draggable } from 'react-beautiful-dnd';
|
import { Draggable } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import { startTimer, stopTimer } from '../../utils/timer';
|
||||||
import Paths from '../../constants/Paths';
|
import Paths from '../../constants/Paths';
|
||||||
import Tasks from './Tasks';
|
import Tasks from './Tasks';
|
||||||
import NameEdit from './NameEdit';
|
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(
|
const handleNameUpdate = useCallback(
|
||||||
(newName) => {
|
(newName) => {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
@@ -108,7 +120,13 @@ const Card = React.memo(
|
|||||||
)}
|
)}
|
||||||
{timer && (
|
{timer && (
|
||||||
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
<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>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@@ -148,14 +166,11 @@ const Card = React.memo(
|
|||||||
{canEdit && (
|
{canEdit && (
|
||||||
<ActionsPopup
|
<ActionsPopup
|
||||||
card={{
|
card={{
|
||||||
id,
|
|
||||||
name,
|
|
||||||
dueDate,
|
dueDate,
|
||||||
timer,
|
timer,
|
||||||
boardId,
|
boardId,
|
||||||
listId,
|
listId,
|
||||||
projectId,
|
projectId,
|
||||||
isPersisted,
|
|
||||||
}}
|
}}
|
||||||
projectsToLists={allProjectsToLists}
|
projectsToLists={allProjectsToLists}
|
||||||
boardMemberships={allBoardMemberships}
|
boardMemberships={allBoardMemberships}
|
||||||
|
|||||||
@@ -78,7 +78,9 @@
|
|||||||
.cover {
|
.cover {
|
||||||
border-radius: 3px 3px 0 0;
|
border-radius: 3px 3px 0 0;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
|
padding: 2px 2px 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 2000;
|
z-index: 2001;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
padding: 6px 32px 6px 128px;
|
padding: 6px 32px 6px 128px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
z-index: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.extension {
|
.extension {
|
||||||
@@ -95,7 +94,6 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
width: 112px;
|
width: 112px;
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnailLabel {
|
.thumbnailLabel {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Button, Grid, Icon, Modal } from 'semantic-ui-react';
|
import { Button, Grid, Icon, Modal } from 'semantic-ui-react';
|
||||||
import { Markdown } from '../../lib/custom-ui';
|
import { Markdown } from '../../lib/custom-ui';
|
||||||
|
|
||||||
|
import { startTimer, stopTimer } from '../../utils/timer';
|
||||||
import NameField from './NameField';
|
import NameField from './NameField';
|
||||||
import DescriptionEdit from './DescriptionEdit';
|
import DescriptionEdit from './DescriptionEdit';
|
||||||
import Tasks from './Tasks';
|
import Tasks from './Tasks';
|
||||||
@@ -80,6 +81,12 @@ const CardModal = React.memo(
|
|||||||
|
|
||||||
const isGalleryOpened = useRef(false);
|
const isGalleryOpened = useRef(false);
|
||||||
|
|
||||||
|
const handleToggleTimerClick = useCallback(() => {
|
||||||
|
onUpdate({
|
||||||
|
timer: timer.startedAt ? stopTimer(timer) : startTimer(timer),
|
||||||
|
});
|
||||||
|
}, [timer, onUpdate]);
|
||||||
|
|
||||||
const handleNameUpdate = useCallback(
|
const handleNameUpdate = useCallback(
|
||||||
(newName) => {
|
(newName) => {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
@@ -291,6 +298,19 @@ const CardModal = React.memo(
|
|||||||
<Timer startedAt={timer.startedAt} total={timer.total} />
|
<Timer startedAt={timer.startedAt} total={timer.total} />
|
||||||
)}
|
)}
|
||||||
</span>
|
</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>
|
||||||
)}
|
)}
|
||||||
</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 PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import { Button, Form } from 'semantic-ui-react';
|
||||||
import { Button, Form, TextArea } from 'semantic-ui-react';
|
import SimpleMDE from 'react-simplemde-editor';
|
||||||
|
|
||||||
import { useClosableForm, useField } from '../../hooks';
|
|
||||||
|
|
||||||
import styles from './DescriptionEdit.module.scss';
|
import styles from './DescriptionEdit.module.scss';
|
||||||
|
|
||||||
const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) => {
|
const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [isOpened, setIsOpened] = useState(false);
|
const [isOpened, setIsOpened] = useState(false);
|
||||||
const [value, handleFieldChange, setValue] = useField(null);
|
const [value, setValue] = useState(null);
|
||||||
|
|
||||||
const field = useRef(null);
|
|
||||||
|
|
||||||
const open = useCallback(() => {
|
const open = useCallback(() => {
|
||||||
setIsOpened(true);
|
setIsOpened(true);
|
||||||
@@ -55,20 +51,37 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
|
|||||||
[close],
|
[close],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
|
|
||||||
close,
|
|
||||||
isOpened,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
close();
|
close();
|
||||||
}, [close]);
|
}, [close]);
|
||||||
|
|
||||||
useEffect(() => {
|
const mdEditorOptions = useMemo(
|
||||||
if (isOpened) {
|
() => ({
|
||||||
field.current.ref.current.focus();
|
autofocus: true,
|
||||||
}
|
spellChecker: false,
|
||||||
}, [isOpened]);
|
status: false,
|
||||||
|
toolbar: [
|
||||||
|
'bold',
|
||||||
|
'italic',
|
||||||
|
'heading',
|
||||||
|
'strikethrough',
|
||||||
|
'|',
|
||||||
|
'quote',
|
||||||
|
'unordered-list',
|
||||||
|
'ordered-list',
|
||||||
|
'table',
|
||||||
|
'|',
|
||||||
|
'link',
|
||||||
|
'image',
|
||||||
|
'|',
|
||||||
|
'undo',
|
||||||
|
'redo',
|
||||||
|
'|',
|
||||||
|
'guide',
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
if (!isOpened) {
|
if (!isOpened) {
|
||||||
return React.cloneElement(children, {
|
return React.cloneElement(children, {
|
||||||
@@ -78,26 +91,17 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<TextArea
|
<SimpleMDE
|
||||||
ref={field}
|
|
||||||
as={TextareaAutosize}
|
|
||||||
value={value}
|
value={value}
|
||||||
|
options={mdEditorOptions}
|
||||||
placeholder={t('common.enterDescription')}
|
placeholder={t('common.enterDescription')}
|
||||||
minRows={3}
|
|
||||||
spellCheck={false}
|
|
||||||
className={styles.field}
|
className={styles.field}
|
||||||
onKeyDown={handleFieldKeyDown}
|
onKeyDown={handleFieldKeyDown}
|
||||||
onChange={handleFieldChange}
|
onChange={setValue}
|
||||||
onBlur={handleFieldBlur}
|
|
||||||
/>
|
/>
|
||||||
<div className={styles.controls}>
|
<div className={styles.controls}>
|
||||||
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
|
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
|
||||||
<Button
|
<Button positive content={t('action.save')} />
|
||||||
positive
|
|
||||||
content={t('action.save')}
|
|
||||||
onMouseOver={handleControlMouseOver}
|
|
||||||
onMouseOut={handleControlMouseOut}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,15 +6,12 @@
|
|||||||
|
|
||||||
.field {
|
.field {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid rgba(9, 30, 66, 0.13);
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #17394d;
|
color: #17394d;
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 8px 12px;
|
|
||||||
resize: none;
|
resize: none;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
.wrapper {
|
.wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const Header = React.memo(
|
|||||||
project,
|
project,
|
||||||
user,
|
user,
|
||||||
notifications,
|
notifications,
|
||||||
|
isLogouting,
|
||||||
canEditProject,
|
canEditProject,
|
||||||
canEditUsers,
|
canEditUsers,
|
||||||
onProjectSettingsClick,
|
onProjectSettingsClick,
|
||||||
@@ -75,7 +76,11 @@ const Header = React.memo(
|
|||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</NotificationsPopup>
|
</NotificationsPopup>
|
||||||
<UserPopup onSettingsClick={onUserSettingsClick} onLogout={onLogout}>
|
<UserPopup
|
||||||
|
isLogouting={isLogouting}
|
||||||
|
onSettingsClick={onUserSettingsClick}
|
||||||
|
onLogout={onLogout}
|
||||||
|
>
|
||||||
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
|
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
|
||||||
{user.name}
|
{user.name}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -93,6 +98,7 @@ Header.propTypes = {
|
|||||||
user: PropTypes.object.isRequired,
|
user: PropTypes.object.isRequired,
|
||||||
notifications: PropTypes.array.isRequired,
|
notifications: PropTypes.array.isRequired,
|
||||||
/* eslint-enable react/forbid-prop-types */
|
/* eslint-enable react/forbid-prop-types */
|
||||||
|
isLogouting: PropTypes.bool.isRequired,
|
||||||
canEditProject: PropTypes.bool.isRequired,
|
canEditProject: PropTypes.bool.isRequired,
|
||||||
canEditUsers: PropTypes.bool.isRequired,
|
canEditUsers: PropTypes.bool.isRequired,
|
||||||
onProjectSettingsClick: PropTypes.func.isRequired,
|
onProjectSettingsClick: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active, &:hover {
|
&:active,
|
||||||
|
&:hover {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
@@ -42,7 +43,6 @@
|
|||||||
height: 50px;
|
height: 50px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import truncate from 'lodash/truncate';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation, Trans } from 'react-i18next';
|
import { useTranslation, Trans } from 'react-i18next';
|
||||||
@@ -47,23 +48,26 @@ const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
|
|||||||
{activity.data.toList.name}
|
{activity.data.toList.name}
|
||||||
</Trans>
|
</Trans>
|
||||||
);
|
);
|
||||||
case ActivityTypes.COMMENT_CARD:
|
case ActivityTypes.COMMENT_CARD: {
|
||||||
|
const commentText = truncate(activity.data.text);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="common.userLeftNewCommentToCard"
|
i18nKey="common.userLeftNewCommentToCard"
|
||||||
values={{
|
values={{
|
||||||
user: activity.user.name,
|
user: activity.user.name,
|
||||||
comment: activity.data.text,
|
comment: commentText,
|
||||||
card: card.name,
|
card: card.name,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activity.user.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}>
|
<Link to={Paths.CARDS.replace(':id', card.id)} onClick={onClose}>
|
||||||
{card.name}
|
{card.name}
|
||||||
</Link>
|
</Link>
|
||||||
</Trans>
|
</Trans>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,9 +84,10 @@ const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
|
|||||||
})}
|
})}
|
||||||
</Popup.Header>
|
</Popup.Header>
|
||||||
<Popup.Content>
|
<Popup.Content>
|
||||||
{items.length > 0
|
{items.length > 0 ? (
|
||||||
? items.map((item) => (
|
<div className={styles.wrapper}>
|
||||||
<div key={item.id} className={styles.wrapper}>
|
{items.map((item) => (
|
||||||
|
<div key={item.id} className={styles.item}>
|
||||||
{item.card && item.activity ? (
|
{item.card && item.activity ? (
|
||||||
<>
|
<>
|
||||||
<User
|
<User
|
||||||
@@ -90,20 +95,23 @@ const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
|
|||||||
avatarUrl={item.activity.user.avatarUrl}
|
avatarUrl={item.activity.user.avatarUrl}
|
||||||
size="large"
|
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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
icon="close"
|
icon="trash alternate outline"
|
||||||
className={styles.button}
|
className={styles.itemButton}
|
||||||
onClick={() => handleDelete(item.id)}
|
onClick={() => handleDelete(item.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
))}
|
||||||
: t('common.noUnreadNotifications')}
|
</div>
|
||||||
|
) : (
|
||||||
|
t('common.noUnreadNotifications')
|
||||||
|
)}
|
||||||
</Popup.Content>
|
</Popup.Content>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
:global(#app) {
|
:global(#app) {
|
||||||
.button {
|
.item {
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemButton {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
float: right;
|
float: right;
|
||||||
@@ -7,7 +15,7 @@
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
padding: 5px 0;
|
padding: 0;
|
||||||
transition: background 0.3s ease;
|
transition: background 0.3s ease;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
|
||||||
@@ -16,7 +24,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.itemContent {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
@@ -27,7 +35,7 @@
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.deletedContent {
|
.itemDeleted {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
@@ -38,10 +46,21 @@
|
|||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
margin: 0 -12px;
|
margin: 0 -12px;
|
||||||
padding: 12px;
|
max-height: 60vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
|
||||||
&:hover {
|
&::-webkit-scrollbar {
|
||||||
background: #f0f0f0;
|
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';
|
import styles from './AddStep.module.scss';
|
||||||
|
|
||||||
const AddStep = React.memo(({ onCreate, onBack }) => {
|
const AddStep = React.memo(({ defaultData, onCreate, onBack }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
|
|
||||||
const [data, handleFieldChange] = useForm(() => ({
|
const [data, handleFieldChange] = useForm(() => ({
|
||||||
name: '',
|
name: '',
|
||||||
color: LabelColors[0],
|
color: LabelColors[0],
|
||||||
|
...defaultData,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
@@ -46,6 +47,7 @@ const AddStep = React.memo(({ onCreate, onBack }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddStep.propTypes = {
|
AddStep.propTypes = {
|
||||||
|
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
onCreate: PropTypes.func.isRequired,
|
onCreate: PropTypes.func.isRequired,
|
||||||
onBack: PropTypes.func.isRequired,
|
onBack: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,45 +8,51 @@ import { Button } from 'semantic-ui-react';
|
|||||||
import styles from './Item.module.scss';
|
import styles from './Item.module.scss';
|
||||||
import globalStyles from '../../styles.module.scss';
|
import globalStyles from '../../styles.module.scss';
|
||||||
|
|
||||||
const Item = React.memo(({ name, color, isPersisted, isActive, onSelect, onDeselect, onEdit }) => {
|
const Item = React.memo(
|
||||||
const handleToggleClick = useCallback(() => {
|
({ name, color, isPersisted, isActive, canEdit, onSelect, onDeselect, onEdit }) => {
|
||||||
if (isActive) {
|
const handleToggleClick = useCallback(() => {
|
||||||
onDeselect();
|
if (isActive) {
|
||||||
} else {
|
onDeselect();
|
||||||
onSelect();
|
} else {
|
||||||
}
|
onSelect();
|
||||||
}, [isActive, onSelect, onDeselect]);
|
}
|
||||||
|
}, [isActive, onSelect, onDeselect]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<Button
|
<Button
|
||||||
fluid
|
fluid
|
||||||
content={name}
|
content={name}
|
||||||
active={isActive}
|
active={isActive}
|
||||||
disabled={!isPersisted}
|
disabled={!isPersisted}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
styles.labelButton,
|
styles.labelButton,
|
||||||
isActive && styles.labelButtonActive,
|
isActive && styles.labelButtonActive,
|
||||||
globalStyles[`background${upperFirst(camelCase(color))}`],
|
globalStyles[`background${upperFirst(camelCase(color))}`],
|
||||||
|
)}
|
||||||
|
onClick={handleToggleClick}
|
||||||
|
/>
|
||||||
|
{canEdit && (
|
||||||
|
<Button
|
||||||
|
icon="pencil"
|
||||||
|
size="small"
|
||||||
|
floated="right"
|
||||||
|
disabled={!isPersisted}
|
||||||
|
className={styles.editButton}
|
||||||
|
onClick={onEdit}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
onClick={handleToggleClick}
|
</div>
|
||||||
/>
|
);
|
||||||
<Button
|
},
|
||||||
icon="pencil"
|
);
|
||||||
size="small"
|
|
||||||
disabled={!isPersisted}
|
|
||||||
className={styles.editButton}
|
|
||||||
onClick={onEdit}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Item.propTypes = {
|
Item.propTypes = {
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
color: PropTypes.string.isRequired,
|
color: PropTypes.string.isRequired,
|
||||||
isPersisted: PropTypes.bool.isRequired,
|
isPersisted: PropTypes.bool.isRequired,
|
||||||
isActive: PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
|
canEdit: PropTypes.bool.isRequired,
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
onDeselect: PropTypes.func.isRequired,
|
onDeselect: PropTypes.func.isRequired,
|
||||||
onEdit: PropTypes.func.isRequired,
|
onEdit: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin: 0;
|
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
||||||
@@ -18,6 +17,7 @@
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
margin-right: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 8px 32px 8px 10px;
|
padding: 8px 32px 8px 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -18,7 +18,18 @@ const StepTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LabelsStep = React.memo(
|
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 [t] = useTranslation();
|
||||||
const [step, openStep, handleBack] = useSteps();
|
const [step, openStep, handleBack] = useSteps();
|
||||||
const [search, handleSearchChange] = useField('');
|
const [search, handleSearchChange] = useField('');
|
||||||
@@ -84,7 +95,15 @@ const LabelsStep = React.memo(
|
|||||||
if (step) {
|
if (step) {
|
||||||
switch (step.type) {
|
switch (step.type) {
|
||||||
case StepTypes.ADD:
|
case StepTypes.ADD:
|
||||||
return <AddStep onCreate={onCreate} onBack={handleBack} />;
|
return (
|
||||||
|
<AddStep
|
||||||
|
defaultData={{
|
||||||
|
name: search,
|
||||||
|
}}
|
||||||
|
onCreate={onCreate}
|
||||||
|
onBack={handleBack}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case StepTypes.EDIT: {
|
case StepTypes.EDIT: {
|
||||||
const currentItem = items.find((item) => item.id === step.params.id);
|
const currentItem = items.find((item) => item.id === step.params.id);
|
||||||
|
|
||||||
@@ -132,6 +151,7 @@ const LabelsStep = React.memo(
|
|||||||
color={item.color}
|
color={item.color}
|
||||||
isPersisted={item.isPersisted}
|
isPersisted={item.isPersisted}
|
||||||
isActive={currentIds.includes(item.id)}
|
isActive={currentIds.includes(item.id)}
|
||||||
|
canEdit={canEdit}
|
||||||
onSelect={() => handleSelect(item.id)}
|
onSelect={() => handleSelect(item.id)}
|
||||||
onDeselect={() => handleDeselect(item.id)}
|
onDeselect={() => handleDeselect(item.id)}
|
||||||
onEdit={() => handleEdit(item.id)}
|
onEdit={() => handleEdit(item.id)}
|
||||||
@@ -139,12 +159,14 @@ const LabelsStep = React.memo(
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button
|
{canEdit && (
|
||||||
fluid
|
<Button
|
||||||
content={t('action.createNewLabel')}
|
fluid
|
||||||
className={styles.addButton}
|
content={t('action.createNewLabel')}
|
||||||
onClick={handleAddClick}
|
className={styles.addButton}
|
||||||
/>
|
onClick={handleAddClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Popup.Content>
|
</Popup.Content>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -157,6 +179,7 @@ LabelsStep.propTypes = {
|
|||||||
currentIds: PropTypes.array.isRequired,
|
currentIds: PropTypes.array.isRequired,
|
||||||
/* eslint-enable react/forbid-prop-types */
|
/* eslint-enable react/forbid-prop-types */
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
canEdit: PropTypes.bool,
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
onDeselect: PropTypes.func.isRequired,
|
onDeselect: PropTypes.func.isRequired,
|
||||||
onCreate: PropTypes.func.isRequired,
|
onCreate: PropTypes.func.isRequired,
|
||||||
@@ -167,6 +190,7 @@ LabelsStep.propTypes = {
|
|||||||
|
|
||||||
LabelsStep.defaultProps = {
|
LabelsStep.defaultProps = {
|
||||||
title: 'common.labels',
|
title: 'common.labels',
|
||||||
|
canEdit: true,
|
||||||
onBack: undefined,
|
onBack: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import NotFound from './NotFound';
|
|||||||
|
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
import 'photoswipe/dist/photoswipe.css';
|
import 'photoswipe/dist/photoswipe.css';
|
||||||
|
import 'easymde/dist/easymde.min.css';
|
||||||
import '../lib/custom-ui/styles.css';
|
import '../lib/custom-ui/styles.css';
|
||||||
|
|
||||||
import '../styles.module.scss';
|
import '../styles.module.scss';
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const SIZES = {
|
|||||||
MEDIUM: 'medium',
|
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 prevStartedAt = usePrevious(startedAt);
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
@@ -52,6 +52,7 @@ const Timer = React.memo(({ startedAt, total, size, isDisabled, onClick }) => {
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
styles.wrapper,
|
styles.wrapper,
|
||||||
styles[`wrapper${upperFirst(size)}`],
|
styles[`wrapper${upperFirst(size)}`],
|
||||||
|
startedAt && styles.wrapperActive,
|
||||||
onClick && styles.wrapperHoverable,
|
onClick && styles.wrapperHoverable,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -59,16 +60,19 @@ const Timer = React.memo(({ startedAt, total, size, isDisabled, onClick }) => {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ElementType = as;
|
||||||
|
|
||||||
return onClick ? (
|
return onClick ? (
|
||||||
<button type="button" disabled={isDisabled} className={styles.button} onClick={onClick}>
|
<ElementType type="button" disabled={isDisabled} className={styles.button} onClick={onClick}>
|
||||||
{contentNode}
|
{contentNode}
|
||||||
</button>
|
</ElementType>
|
||||||
) : (
|
) : (
|
||||||
contentNode
|
contentNode
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Timer.propTypes = {
|
Timer.propTypes = {
|
||||||
|
as: PropTypes.elementType,
|
||||||
startedAt: PropTypes.instanceOf(Date),
|
startedAt: PropTypes.instanceOf(Date),
|
||||||
total: PropTypes.number.isRequired, // eslint-disable-line react/no-unused-prop-types
|
total: PropTypes.number.isRequired, // eslint-disable-line react/no-unused-prop-types
|
||||||
size: PropTypes.oneOf(Object.values(SIZES)),
|
size: PropTypes.oneOf(Object.values(SIZES)),
|
||||||
@@ -77,6 +81,7 @@ Timer.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Timer.defaultProps = {
|
Timer.defaultProps = {
|
||||||
|
as: 'button',
|
||||||
startedAt: undefined,
|
startedAt: undefined,
|
||||||
size: SIZES.MEDIUM,
|
size: SIZES.MEDIUM,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
|
|||||||
@@ -9,17 +9,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
background: #f3f4f6;
|
background: #dce0e4;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
color: #7e8ea3;
|
color: #6a808b;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
outline: none;
|
outline: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
transition: background 0.3s ease;
|
transition: background 0.3s ease;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapperActive {
|
||||||
|
background: #21ba45;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&.wrapperHoverable:hover {
|
||||||
|
background: #16ab39;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.wrapperHoverable:hover {
|
.wrapperHoverable:hover {
|
||||||
background: #d2d8dc;
|
background: #d2d8dc;
|
||||||
color: #17394d;
|
color: #17394d;
|
||||||
@@ -42,6 +53,5 @@
|
|||||||
.wrapperMedium {
|
.wrapperMedium {
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
text-decoration: underline;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { withPopup } from '../../lib/popup';
|
|||||||
import { Input, Popup } from '../../lib/custom-ui';
|
import { Input, Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useForm } from '../../hooks';
|
import { useForm } from '../../hooks';
|
||||||
import { isUsername } from '../../utils/validator';
|
import { isPassword, isUsername } from '../../utils/validator';
|
||||||
|
|
||||||
import styles from './UserAddPopup.module.scss';
|
import styles from './UserAddPopup.module.scss';
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ const UserAddStep = React.memo(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cleanData.password) {
|
if (!cleanData.password || !isPassword(cleanData.password)) {
|
||||||
passwordField.current.focus();
|
passwordField.current.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -143,6 +143,7 @@ const UserAddStep = React.memo(
|
|||||||
/>
|
/>
|
||||||
<div className={styles.text}>{t('common.password')}</div>
|
<div className={styles.text}>{t('common.password')}</div>
|
||||||
<Input.Password
|
<Input.Password
|
||||||
|
withStrengthBar
|
||||||
fluid
|
fluid
|
||||||
ref={passwordField}
|
ref={passwordField}
|
||||||
name="password"
|
name="password"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
|
|||||||
import { Input, Popup } from '../../lib/custom-ui';
|
import { Input, Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useForm } from '../../hooks';
|
import { useForm } from '../../hooks';
|
||||||
|
import { isPassword } from '../../utils/validator';
|
||||||
|
|
||||||
import styles from './UserPasswordEditStep.module.scss';
|
import styles from './UserPasswordEditStep.module.scss';
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ const UserPasswordEditStep = React.memo(
|
|||||||
const currentPasswordField = useRef(null);
|
const currentPasswordField = useRef(null);
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
if (!data.password) {
|
if (!data.password || !isPassword(data.password)) {
|
||||||
passwordField.current.select();
|
passwordField.current.select();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -113,6 +114,7 @@ const UserPasswordEditStep = React.memo(
|
|||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<div className={styles.text}>{t('common.newPassword')}</div>
|
<div className={styles.text}>{t('common.newPassword')}</div>
|
||||||
<Input.Password
|
<Input.Password
|
||||||
|
withStrengthBar
|
||||||
fluid
|
fluid
|
||||||
ref={passwordField}
|
ref={passwordField}
|
||||||
name="password"
|
name="password"
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Menu } from 'semantic-ui-react';
|
import { Button, Menu } from 'semantic-ui-react';
|
||||||
import { withPopup } from '../../lib/popup';
|
import { withPopup } from '../../lib/popup';
|
||||||
import { Popup } from '../../lib/custom-ui';
|
import { Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import styles from './UserPopup.module.scss';
|
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 [t] = useTranslation();
|
||||||
|
|
||||||
const handleSettingsClick = useCallback(() => {
|
const handleSettingsClick = useCallback(() => {
|
||||||
@@ -15,6 +15,17 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
|||||||
onClose();
|
onClose();
|
||||||
}, [onSettingsClick, onClose]);
|
}, [onSettingsClick, onClose]);
|
||||||
|
|
||||||
|
let logoutMenuItemProps;
|
||||||
|
if (isLogouting) {
|
||||||
|
logoutMenuItemProps = {
|
||||||
|
as: Button,
|
||||||
|
fluid: true,
|
||||||
|
basic: true,
|
||||||
|
loading: true,
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup.Header>
|
<Popup.Header>
|
||||||
@@ -29,7 +40,11 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
|||||||
context: 'title',
|
context: 'title',
|
||||||
})}
|
})}
|
||||||
</Menu.Item>
|
</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', {
|
{t('action.logOut', {
|
||||||
context: 'title',
|
context: 'title',
|
||||||
})}
|
})}
|
||||||
@@ -41,6 +56,7 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
UserStep.propTypes = {
|
UserStep.propTypes = {
|
||||||
|
isLogouting: PropTypes.bool.isRequired,
|
||||||
onSettingsClick: PropTypes.func.isRequired,
|
onSettingsClick: PropTypes.func.isRequired,
|
||||||
onLogout: PropTypes.func.isRequired,
|
onLogout: PropTypes.func.isRequired,
|
||||||
onClose: 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
:global(#app) {
|
:global(#app) {
|
||||||
.action {
|
.action {
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 0.28571429rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Modal, Tab } from 'semantic-ui-react';
|
|||||||
|
|
||||||
import AccountPane from './AccountPane';
|
import AccountPane from './AccountPane';
|
||||||
import PreferencesPane from './PreferencesPane';
|
import PreferencesPane from './PreferencesPane';
|
||||||
|
import AboutPane from './AboutPane';
|
||||||
|
|
||||||
const UserSettingsModal = React.memo(
|
const UserSettingsModal = React.memo(
|
||||||
({
|
({
|
||||||
@@ -71,6 +72,10 @@ const UserSettingsModal = React.memo(
|
|||||||
<PreferencesPane subscribeToOwnCards={subscribeToOwnCards} onUpdate={onUpdate} />
|
<PreferencesPane subscribeToOwnCards={subscribeToOwnCards} onUpdate={onUpdate} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
menuItem: 'About Planka',
|
||||||
|
render: () => <AboutPane />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export default {
|
|||||||
|
|
||||||
CORE_INITIALIZE: 'CORE_INITIALIZE',
|
CORE_INITIALIZE: 'CORE_INITIALIZE',
|
||||||
LOGOUT: 'LOGOUT',
|
LOGOUT: 'LOGOUT',
|
||||||
|
LOGOUT__ACCESS_TOKEN_INVALIDATE: 'LOGOUT__ACCESS_TOKEN_INVALIDATE',
|
||||||
|
|
||||||
/* Modals */
|
/* Modals */
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
const { BASE_URL } = window;
|
||||||
|
const BASE_PATH = BASE_URL.replace(/^.*\/\/[^/]*(.*)[^?#]*.*$/, '$1');
|
||||||
|
|
||||||
const SERVER_BASE_URL =
|
const SERVER_BASE_URL =
|
||||||
process.env.REACT_APP_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_KEY = 'accessToken';
|
||||||
const ACCESS_TOKEN_VERSION_KEY = 'accessTokenVersion';
|
const ACCESS_TOKEN_VERSION_KEY = 'accessTokenVersion';
|
||||||
@@ -10,7 +15,9 @@ const POSITION_GAP = 65535;
|
|||||||
const ACTIVITIES_LIMIT = 50;
|
const ACTIVITIES_LIMIT = 50;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
BASE_PATH,
|
||||||
SERVER_BASE_URL,
|
SERVER_BASE_URL,
|
||||||
|
SERVER_HOST_NAME,
|
||||||
ACCESS_TOKEN_KEY,
|
ACCESS_TOKEN_KEY,
|
||||||
ACCESS_TOKEN_VERSION_KEY,
|
ACCESS_TOKEN_VERSION_KEY,
|
||||||
ACCESS_TOKEN_VERSION,
|
ACCESS_TOKEN_VERSION,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
const ROOT = '/';
|
import Config from './Config';
|
||||||
const LOGIN = '/login';
|
|
||||||
const PROJECTS = '/projects/:id';
|
const ROOT = `${Config.BASE_PATH}/`;
|
||||||
const BOARDS = '/boards/:id';
|
const LOGIN = `${Config.BASE_PATH}/login`;
|
||||||
const CARDS = '/cards/:id';
|
const PROJECTS = `${Config.BASE_PATH}/projects/:id`;
|
||||||
|
const BOARDS = `${Config.BASE_PATH}/boards/:id`;
|
||||||
|
const CARDS = `${Config.BASE_PATH}/cards/:id`;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
ROOT,
|
ROOT,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
|
|||||||
|
|
||||||
import selectors from '../selectors';
|
import selectors from '../selectors';
|
||||||
import entryActions from '../entry-actions';
|
import entryActions from '../entry-actions';
|
||||||
|
import { BoardMembershipRoles } from '../constants/Enums';
|
||||||
import BoardActions from '../components/BoardActions';
|
import BoardActions from '../components/BoardActions';
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
@@ -12,6 +13,7 @@ const mapStateToProps = (state) => {
|
|||||||
const labels = selectors.selectLabelsForCurrentBoard(state);
|
const labels = selectors.selectLabelsForCurrentBoard(state);
|
||||||
const filterUsers = selectors.selectFilterUsersForCurrentBoard(state);
|
const filterUsers = selectors.selectFilterUsersForCurrentBoard(state);
|
||||||
const filterLabels = selectors.selectFilterLabelsForCurrentBoard(state);
|
const filterLabels = selectors.selectFilterLabelsForCurrentBoard(state);
|
||||||
|
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
memberships,
|
memberships,
|
||||||
@@ -19,6 +21,7 @@ const mapStateToProps = (state) => {
|
|||||||
filterUsers,
|
filterUsers,
|
||||||
filterLabels,
|
filterLabels,
|
||||||
allUsers,
|
allUsers,
|
||||||
|
canEdit: !!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR,
|
||||||
canEditMemberships: isCurrentUserManager,
|
canEditMemberships: isCurrentUserManager,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import entryActions from '../entry-actions';
|
|||||||
import Header from '../components/Header';
|
import Header from '../components/Header';
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
|
const isLogouting = selectors.selectIsLogouting(state);
|
||||||
const currentUser = selectors.selectCurrentUser(state);
|
const currentUser = selectors.selectCurrentUser(state);
|
||||||
const currentProject = selectors.selectCurrentProject(state);
|
const currentProject = selectors.selectCurrentProject(state);
|
||||||
const notifications = selectors.selectNotificationsForCurrentUser(state);
|
const notifications = selectors.selectNotificationsForCurrentUser(state);
|
||||||
@@ -13,6 +14,7 @@ const mapStateToProps = (state) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
notifications,
|
notifications,
|
||||||
|
isLogouting,
|
||||||
project: currentProject,
|
project: currentProject,
|
||||||
user: currentUser,
|
user: currentUser,
|
||||||
canEditProject: isCurrentUserManager,
|
canEditProject: isCurrentUserManager,
|
||||||
|
|||||||
@@ -1,22 +1,74 @@
|
|||||||
import React, { useCallback } from 'react';
|
import zxcvbn from 'zxcvbn';
|
||||||
import { Icon, Input } from 'semantic-ui-react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Icon, Input, Progress } from 'semantic-ui-react';
|
||||||
import { useToggle } from '../../../hooks';
|
import { useToggle } from '../../../hooks';
|
||||||
|
|
||||||
const InputPassword = React.forwardRef((props, ref) => {
|
import styles from './InputPassword.module.css';
|
||||||
const [isVisible, toggleVisible] = useToggle();
|
|
||||||
|
|
||||||
const handleToggleClick = useCallback(() => {
|
const STRENGTH_SCORE_COLORS = ['red', 'orange', 'yellow', 'olive', 'green'];
|
||||||
toggleVisible();
|
|
||||||
}, [toggleVisible]);
|
|
||||||
|
|
||||||
return (
|
const InputPassword = React.forwardRef(
|
||||||
<Input
|
({ value, withStrengthBar, minStrengthScore, className, ...props }, ref) => {
|
||||||
{...props} // eslint-disable-line react/jsx-props-no-spreading
|
const [isVisible, toggleVisible] = useToggle();
|
||||||
ref={ref}
|
|
||||||
type={isVisible ? 'text' : 'password'}
|
const strengthScore = useMemo(() => {
|
||||||
icon={<Icon link name={isVisible ? 'eye' : 'eye slash'} onClick={handleToggleClick} />}
|
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
|
||||||
|
{...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);
|
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?
|
import './Markdown.module.scss'; // FIXME: import as styles?
|
||||||
|
|
||||||
|
const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i;
|
||||||
|
|
||||||
const Markdown = React.memo(({ linkStopPropagation, ...props }) => {
|
const Markdown = React.memo(({ linkStopPropagation, ...props }) => {
|
||||||
const handleLinkClick = useCallback((event) => {
|
const handleLinkClick = useCallback((event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -16,25 +18,30 @@ const Markdown = React.memo(({ linkStopPropagation, ...props }) => {
|
|||||||
jsx-a11y/click-events-have-key-events,
|
jsx-a11y/click-events-have-key-events,
|
||||||
jsx-a11y/no-static-element-interactions,
|
jsx-a11y/no-static-element-interactions,
|
||||||
react/jsx-props-no-spreading */
|
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,
|
/* eslint-enable jsx-a11y/anchor-has-content,
|
||||||
jsx-a11y/click-events-have-key-events,
|
jsx-a11y/click-events-have-key-events,
|
||||||
jsx-a11y/no-static-element-interactions,
|
jsx-a11y/no-static-element-interactions,
|
||||||
react/jsx-props-no-spreading */
|
react/jsx-props-no-spreading */
|
||||||
[handleLinkClick],
|
[linkStopPropagation, handleLinkClick],
|
||||||
);
|
);
|
||||||
|
|
||||||
let components;
|
|
||||||
if (linkStopPropagation) {
|
|
||||||
components = {
|
|
||||||
a: linkRenderer,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
{...props} // eslint-disable-line react/jsx-props-no-spreading
|
{...props} // eslint-disable-line react/jsx-props-no-spreading
|
||||||
components={components}
|
components={{
|
||||||
|
a: linkRenderer,
|
||||||
|
}}
|
||||||
remarkPlugins={[remarkGfm, remarkBreaks]}
|
remarkPlugins={[remarkGfm, remarkBreaks]}
|
||||||
className="markdown-body"
|
className="markdown-body"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Vendored
+5
-5
@@ -14453,11 +14453,11 @@ img.ui.bordered.image {
|
|||||||
---------------------*/
|
---------------------*/
|
||||||
|
|
||||||
.ui.input.error > input {
|
.ui.input.error > input {
|
||||||
background-color: #fff6f6;
|
background-color: #fff6f6 !important;
|
||||||
border-color: #e0b4b4;
|
border-color: #e0b4b4 !important;
|
||||||
color: #9f3a38;
|
color: #9f3a38 !important;
|
||||||
-webkit-box-shadow: none;
|
-webkit-box-shadow: none !important;
|
||||||
box-shadow: none;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Error Placeholder */
|
/* 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 PropTypes from 'prop-types';
|
||||||
import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react';
|
import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react';
|
||||||
|
|
||||||
@@ -8,6 +9,9 @@ export default (WrappedComponent, defaultProps) => {
|
|||||||
const Popup = React.memo(({ children, ...props }) => {
|
const Popup = React.memo(({ children, ...props }) => {
|
||||||
const [isOpened, setIsOpened] = useState(false);
|
const [isOpened, setIsOpened] = useState(false);
|
||||||
|
|
||||||
|
const wrapper = useRef(null);
|
||||||
|
const resizeObserver = useRef(null);
|
||||||
|
|
||||||
const handleOpen = useCallback(() => {
|
const handleOpen = useCallback(() => {
|
||||||
setIsOpened(true);
|
setIsOpened(true);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -37,6 +41,29 @@ export default (WrappedComponent, defaultProps) => {
|
|||||||
[children],
|
[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, {
|
const tigger = React.cloneElement(children, {
|
||||||
onClick: handleTriggerClick,
|
onClick: handleTriggerClick,
|
||||||
});
|
});
|
||||||
@@ -45,6 +72,7 @@ export default (WrappedComponent, defaultProps) => {
|
|||||||
<SemanticUIPopup
|
<SemanticUIPopup
|
||||||
basic
|
basic
|
||||||
wide
|
wide
|
||||||
|
ref={wrapper}
|
||||||
trigger={tigger}
|
trigger={tigger}
|
||||||
on="click"
|
on="click"
|
||||||
open={isOpened}
|
open={isOpened}
|
||||||
@@ -64,9 +92,11 @@ export default (WrappedComponent, defaultProps) => {
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
{...defaultProps} // eslint-disable-line react/jsx-props-no-spreading
|
{...defaultProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||||
>
|
>
|
||||||
<Button icon="close" onClick={handleClose} className={styles.closeButton} />
|
<div ref={handleContentRef}>
|
||||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
<Button icon="close" onClick={handleClose} className={styles.closeButton} />
|
||||||
<WrappedComponent {...props} onClose={handleClose} />
|
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||||
|
<WrappedComponent {...props} onClose={handleClose} />
|
||||||
|
</div>
|
||||||
</SemanticUIPopup>
|
</SemanticUIPopup>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export default {
|
|||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
preferences: 'Volby',
|
preferences: 'Volby',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
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',
|
project: 'Projekt',
|
||||||
projectNotFound_title: 'Projekt nenalezen',
|
projectNotFound_title: 'Projekt nenalezen',
|
||||||
removeManager_title: 'Odstranit vedoucího',
|
removeManager_title: 'Odstranit vedoucího',
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ export default {
|
|||||||
dateFns,
|
dateFns,
|
||||||
|
|
||||||
format: {
|
format: {
|
||||||
date: 'd/M/yyyy',
|
date: 'd.MM.yyyy',
|
||||||
time: 'p',
|
time: 'p',
|
||||||
dateTime: '$t(format:date) $t(format:time)',
|
dateTime: '$t(format:date) $t(format:time)',
|
||||||
longDate: 'MMM d',
|
longDate: 'd. MMM',
|
||||||
longDateTime: "d MMMM 'at' p",
|
longDateTime: "d. MMMM 'um' p",
|
||||||
},
|
},
|
||||||
|
|
||||||
translation: {
|
translation: {
|
||||||
@@ -23,7 +23,7 @@ export default {
|
|||||||
administrator: 'Administrator',
|
administrator: 'Administrator',
|
||||||
all: 'Alle',
|
all: 'Alle',
|
||||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||||
'Alle Änderungen werden automatisch gespeichert,<br />sobald die Verbindung wiederhergestellt wurde.',
|
'Alle Änderungen werden automatisch gespeichert, sobald die Verbindung wiederhergestellt wurde.',
|
||||||
areYouSureYouWantToDeleteThisAttachment:
|
areYouSureYouWantToDeleteThisAttachment:
|
||||||
'Sind Sie sicher, dass Sie diesen Anhang löschen möchten?',
|
'Sind Sie sicher, dass Sie diesen Anhang löschen möchten?',
|
||||||
areYouSureYouWantToDeleteThisBoard: 'Sind Sie sicher, dass Sie dieses Board löschen möchten?',
|
areYouSureYouWantToDeleteThisBoard: 'Sind Sie sicher, dass Sie dieses Board löschen möchten?',
|
||||||
@@ -55,10 +55,11 @@ export default {
|
|||||||
color: 'Farbe',
|
color: 'Farbe',
|
||||||
createBoard_title: 'Board erstellen',
|
createBoard_title: 'Board erstellen',
|
||||||
createLabel_title: 'Label 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',
|
createProject_title: 'Projekt erstellen',
|
||||||
createTextFile_title: 'Textdatei erstellen',
|
createTextFile_title: 'Textdatei erstellen',
|
||||||
currentPassword: 'Derzeitiges Password',
|
currentPassword: 'Derzeitiges Passwort',
|
||||||
dangerZone_title: 'Gefährlicher Bereich',
|
dangerZone_title: 'Gefährlicher Bereich',
|
||||||
date: 'Datum',
|
date: 'Datum',
|
||||||
dueDate_title: 'Fälligkeitsdatum',
|
dueDate_title: 'Fälligkeitsdatum',
|
||||||
@@ -73,18 +74,18 @@ export default {
|
|||||||
deleteUser_title: 'Benutzer löschen',
|
deleteUser_title: 'Benutzer löschen',
|
||||||
description: 'Beschreibung',
|
description: 'Beschreibung',
|
||||||
detectAutomatically: 'Automatische Erkennung',
|
detectAutomatically: 'Automatische Erkennung',
|
||||||
dropFileToUpload: 'Datei ablegen, um hochzuladen',
|
dropFileToUpload: 'Datei ablegen, um sie hochzuladen',
|
||||||
editAttachment_title: 'Anhang bearbieten',
|
editAttachment_title: 'Anhang bearbieten',
|
||||||
editAvatar_title: 'Avatar bearbeiten',
|
editAvatar_title: 'Avatar bearbeiten',
|
||||||
editBoard_title: 'Board bearbeiten',
|
editBoard_title: 'Board bearbeiten',
|
||||||
editDueDate_title: 'Fälligkeitsdatum bearbeiten',
|
editDueDate_title: 'Fälligkeitsdatum bearbeiten',
|
||||||
editEmail_title: 'E-Mail bearbeiten',
|
editEmail_title: 'E-Mail-Adresse bearbeiten',
|
||||||
editLabel_title: 'Label bearbeiten',
|
editLabel_title: 'Label bearbeiten',
|
||||||
editPassword_title: 'Passwort ändern',
|
editPassword_title: 'Passwort ändern',
|
||||||
editTimer_title: 'Timer bearbeiten',
|
editTimer_title: 'Timer bearbeiten',
|
||||||
editUsername_title: 'Benutzername ändern',
|
editUsername_title: 'Benutzername ändern',
|
||||||
email: 'E-Mail',
|
email: 'E-Mail-Adresse',
|
||||||
emailAlreadyInUse: 'E-Mail bereits in Verwendung',
|
emailAlreadyInUse: 'E-Mail-Adresse bereits in Verwendung',
|
||||||
enterCardTitle: 'Kartentitel eingeben...',
|
enterCardTitle: 'Kartentitel eingeben...',
|
||||||
enterDescription: 'Beschreibung eingeben...',
|
enterDescription: 'Beschreibung eingeben...',
|
||||||
enterFilename: 'Dateiname eingeben',
|
enterFilename: 'Dateiname eingeben',
|
||||||
@@ -108,7 +109,7 @@ export default {
|
|||||||
minutes: 'Minuten',
|
minutes: 'Minuten',
|
||||||
moveCard_title: 'Karte verschieben',
|
moveCard_title: 'Karte verschieben',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
newEmail: 'Neue E-Mail',
|
newEmail: 'Neue E-Mail-Adresse',
|
||||||
newPassword: 'Neues Passwort',
|
newPassword: 'Neues Passwort',
|
||||||
newUsername: 'Neuer Benutzername',
|
newUsername: 'Neuer Benutzername',
|
||||||
noConnectionToServer: 'Keine Verbindung zum Server',
|
noConnectionToServer: 'Keine Verbindung zum Server',
|
||||||
@@ -123,7 +124,7 @@ export default {
|
|||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
preferences: 'Voreinstellungen',
|
preferences: 'Voreinstellungen',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
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',
|
project: 'Projekt',
|
||||||
projectNotFound_title: 'Projekt nicht gefunden',
|
projectNotFound_title: 'Projekt nicht gefunden',
|
||||||
removeManager_title: 'Manager entfernen',
|
removeManager_title: 'Manager entfernen',
|
||||||
@@ -140,10 +141,10 @@ export default {
|
|||||||
time: 'Zeit',
|
time: 'Zeit',
|
||||||
timer: 'Timer',
|
timer: 'Timer',
|
||||||
title: 'Titel',
|
title: 'Titel',
|
||||||
userActions_title: 'Benutzerraktionen',
|
userActions_title: 'Benutzeraktionen',
|
||||||
userAddedThisCardToList: '<0>{{user}}</0><1> hat diese Karte hinzugefügt zu {{list}}</1>',
|
userAddedThisCardToList: '<0>{{user}}</0><1> hat diese Karte hinzugefügt zu {{list}}</1>',
|
||||||
userLeftNewCommentToCard:
|
userLeftNewCommentToCard:
|
||||||
'{{user}} hat einen neuen Kommentar verfasst «{{comment}}» to <2>{{card}}</2>',
|
'{{user}} hat einen neuen Kommentar verfasst: «{{comment}}» in <2>{{card}}</2>',
|
||||||
userMovedCardFromListToList:
|
userMovedCardFromListToList:
|
||||||
'{{user}} bewegte <2>{{card}}</2> von {{fromList}} nach {{toList}}',
|
'{{user}} bewegte <2>{{card}}</2> von {{fromList}} nach {{toList}}',
|
||||||
userMovedThisCardFromListToList:
|
userMovedThisCardFromListToList:
|
||||||
@@ -190,7 +191,7 @@ export default {
|
|||||||
edit: 'Bearbeiten',
|
edit: 'Bearbeiten',
|
||||||
editDueDate_title: 'Fälligkeitsdatum bearbeiten',
|
editDueDate_title: 'Fälligkeitsdatum bearbeiten',
|
||||||
editDescription_title: 'Beschreibung ändern',
|
editDescription_title: 'Beschreibung ändern',
|
||||||
editEmail_title: 'E-Mail bearbeiten',
|
editEmail_title: 'E-Mail-Adresse bearbeiten',
|
||||||
editPassword_title: 'Passwort ändern',
|
editPassword_title: 'Passwort ändern',
|
||||||
editTimer_title: 'Timer bearbeiten',
|
editTimer_title: 'Timer bearbeiten',
|
||||||
editTitle_title: 'Titel bearbeiten',
|
editTitle_title: 'Titel bearbeiten',
|
||||||
@@ -199,7 +200,7 @@ export default {
|
|||||||
leaveBoard: 'Board verlassen',
|
leaveBoard: 'Board verlassen',
|
||||||
leaveProject: 'Projekt verlassen',
|
leaveProject: 'Projekt verlassen',
|
||||||
logOut_title: 'Ausloggen',
|
logOut_title: 'Ausloggen',
|
||||||
makeCover_title: 'als Vorschau festlegen',
|
makeCover_title: 'Als Vorschau festlegen',
|
||||||
move: 'Verschieben',
|
move: 'Verschieben',
|
||||||
moveCard_title: 'Karte bewegen',
|
moveCard_title: 'Karte bewegen',
|
||||||
remove: 'Löschen',
|
remove: 'Löschen',
|
||||||
@@ -216,7 +217,7 @@ export default {
|
|||||||
start: 'Start',
|
start: 'Start',
|
||||||
stop: 'Stopp',
|
stop: 'Stopp',
|
||||||
subscribe: 'Abonnieren',
|
subscribe: 'Abonnieren',
|
||||||
unsubscribe: 'Deabonnieren',
|
unsubscribe: 'De-abonnieren',
|
||||||
uploadNewAvatar: 'Neuen Avatar hochladen',
|
uploadNewAvatar: 'Neuen Avatar hochladen',
|
||||||
uploadNewImage: 'Neues Bild hochladen',
|
uploadNewImage: 'Neues Bild hochladen',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
export default {
|
export default {
|
||||||
translation: {
|
translation: {
|
||||||
common: {
|
common: {
|
||||||
emailOrUsername: 'E-mail oder Benutzername',
|
emailOrUsername: 'E-Mail-Adresse oder Benutzername',
|
||||||
invalidEmailOrUsername: 'Ungültige E-mail oder Benutzername',
|
invalidEmailOrUsername: 'Ungültige E-Mail-Adresse oder Benutzername',
|
||||||
invalidPassword: 'Ungültiges Passwort',
|
invalidPassword: 'Ungültiges Passwort',
|
||||||
logInToPlanka: 'Einloggen',
|
logInToPlanka: 'Einloggen',
|
||||||
noInternetConnection: 'Keine Internetverbindung',
|
noInternetConnection: 'Keine Internetverbindung',
|
||||||
pageNotFound_title: 'Seite nicht gefunden',
|
pageNotFound_title: 'Seite nicht gefunden',
|
||||||
password: 'Passwort',
|
password: 'Passwort',
|
||||||
projectManagement: 'Projekt Management',
|
projectManagement: 'Projekt-Management',
|
||||||
serverConnectionFailed: 'Serververbindung fehlgeschlagen',
|
serverConnectionFailed: 'Serververbindung fehlgeschlagen',
|
||||||
unknownError: 'Unbekannter Fehler, bitte später erneut versuchen',
|
unknownError: 'Unbekannter Fehler, bitte später erneut versuchen',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export default {
|
|||||||
phone: 'Phone',
|
phone: 'Phone',
|
||||||
preferences: 'Preferences',
|
preferences: 'Preferences',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
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',
|
project: 'Project',
|
||||||
projectNotFound_title: 'Project Not Found',
|
projectNotFound_title: 'Project Not Found',
|
||||||
removeManager_title: 'Remove Manager',
|
removeManager_title: 'Remove Manager',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import core from './core';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
language: 'en',
|
language: 'en',
|
||||||
country: 'us',
|
country: 'gb',
|
||||||
name: 'English',
|
name: 'English',
|
||||||
embeddedLocale: merge(login, core),
|
embeddedLocale: merge(login, core),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default {
|
|||||||
phone: 'Teléfono',
|
phone: 'Teléfono',
|
||||||
preferences: 'Preferencias',
|
preferences: 'Preferencias',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
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',
|
project: 'Proyecto',
|
||||||
projectNotFound_title: 'Proyecto no encontrado',
|
projectNotFound_title: 'Proyecto no encontrado',
|
||||||
removeMember_title: 'Remover Miembro',
|
removeMember_title: 'Remover Miembro',
|
||||||
@@ -115,7 +115,7 @@ export default {
|
|||||||
selectList: 'Seleccionar Lista',
|
selectList: 'Seleccionar Lista',
|
||||||
selectProject: 'Seleccionar Proyecto',
|
selectProject: 'Seleccionar Proyecto',
|
||||||
settings: 'Configuraciones',
|
settings: 'Configuraciones',
|
||||||
subscribeToMyOwnCardsByDefault: 'Suscribirme a mis propias tarjetas por defecto.',
|
subscribeToMyOwnCardsByDefault: 'Suscribirme a mis propias tarjetas por defecto',
|
||||||
taskActions_title: 'Acciones de Tareas',
|
taskActions_title: 'Acciones de Tareas',
|
||||||
tasks: 'Tareas',
|
tasks: 'Tareas',
|
||||||
time: 'Tiempo',
|
time: 'Tiempo',
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export default {
|
|||||||
phone: 'Telefono',
|
phone: 'Telefono',
|
||||||
preferences: 'Preferenze',
|
preferences: 'Preferenze',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
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',
|
project: 'Progetto',
|
||||||
projectNotFound_title: 'Progetto non trovato',
|
projectNotFound_title: 'Progetto non trovato',
|
||||||
removeManager_title: 'Rimuovi Manager',
|
removeManager_title: 'Rimuovi Manager',
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export default {
|
|||||||
phone: '電話番号',
|
phone: '電話番号',
|
||||||
preferences: '環境設定',
|
preferences: '環境設定',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
'ヒント: Ctrl-V(MacではCmd-V)を押して、クリップボードから添付ファイルを追加します。',
|
'ヒント: Ctrl-V(MacではCmd-V)を押して、クリップボードから添付ファイルを追加します',
|
||||||
project: 'プロジェクト',
|
project: 'プロジェクト',
|
||||||
projectNotFound_title: 'プロジェクトがありません',
|
projectNotFound_title: 'プロジェクトがありません',
|
||||||
removeManager_title: 'マネージャーを削除',
|
removeManager_title: 'マネージャーを削除',
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default {
|
|||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
preferences: 'Preferencje',
|
preferences: 'Preferencje',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
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',
|
project: 'Projekt',
|
||||||
projectNotFound_title: 'Projekt Nie Znaleziony',
|
projectNotFound_title: 'Projekt Nie Znaleziony',
|
||||||
removeMember_title: 'Usuń Członka',
|
removeMember_title: 'Usuń Członka',
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export default {
|
|||||||
phone: 'Телефон',
|
phone: 'Телефон',
|
||||||
preferences: 'Предпочтения',
|
preferences: 'Предпочтения',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
'Совет: нажмите Ctrl-V (Cmd-V на Mac), чтобы добавить вложение из буфера обмена.',
|
'Совет: нажмите Ctrl-V (Cmd-V на Mac), чтобы добавить вложение из буфера обмена',
|
||||||
project: 'Проект',
|
project: 'Проект',
|
||||||
projectActions: 'Действия с проектом',
|
projectActions: 'Действия с проектом',
|
||||||
projectNotFound: 'Проект не найден',
|
projectNotFound: 'Проект не найден',
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export default {
|
|||||||
phone: 'Telefón',
|
phone: 'Telefón',
|
||||||
preferences: 'Voľby',
|
preferences: 'Voľby',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
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',
|
project: 'Projekt',
|
||||||
projectNotFound_title: 'Projekt neexistuje',
|
projectNotFound_title: 'Projekt neexistuje',
|
||||||
removeManager_title: 'Odstrániť správcu',
|
removeManager_title: 'Odstrániť správcu',
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export default {
|
|||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
preferences: 'Preferenser',
|
preferences: 'Preferenser',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
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',
|
project: 'Projekt',
|
||||||
projectNotFound_title: 'Projekt Ej Funnet',
|
projectNotFound_title: 'Projekt Ej Funnet',
|
||||||
removeManager_title: 'Ta Bort Projektledare',
|
removeManager_title: 'Ta Bort Projektledare',
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export default {
|
|||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
preferences: 'Afzalliklar',
|
preferences: 'Afzalliklar',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
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',
|
project: 'Loyiha',
|
||||||
projectNotFound_title: 'Loyiha Topilmadi',
|
projectNotFound_title: 'Loyiha Topilmadi',
|
||||||
removeManager_title: "Boshqaruvchini O'chirish",
|
removeManager_title: "Boshqaruvchini O'chirish",
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export default {
|
|||||||
phone: '电话',
|
phone: '电话',
|
||||||
preferences: '偏好',
|
preferences: '偏好',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
'提示: 点击 Ctrl-V (Mac: Cmd-V) 从剪切板添加附件.',
|
'提示: 点击 Ctrl-V (Mac: Cmd-V) 从剪切板添加附件',
|
||||||
project: '项目',
|
project: '项目',
|
||||||
projectNotFound_title: '项目未找到',
|
projectNotFound_title: '项目未找到',
|
||||||
removeManager_title: '删除管理员',
|
removeManager_title: '删除管理员',
|
||||||
|
|||||||
@@ -1,18 +1,34 @@
|
|||||||
|
import { getAccessToken } from '../utils/access-token-storage';
|
||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
accessToken: getAccessToken(),
|
||||||
userId: null,
|
userId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line default-param-last
|
// eslint-disable-next-line default-param-last
|
||||||
export default (state = initialState, { type, payload }) => {
|
export default (state = initialState, { type, payload }) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case ActionTypes.AUTHENTICATE__SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
accessToken: payload.accessToken,
|
||||||
|
};
|
||||||
case ActionTypes.SOCKET_RECONNECT_HANDLE:
|
case ActionTypes.SOCKET_RECONNECT_HANDLE:
|
||||||
case ActionTypes.CORE_INITIALIZE:
|
case ActionTypes.CORE_INITIALIZE:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
userId: payload.user.id,
|
userId: payload.user.id,
|
||||||
};
|
};
|
||||||
|
case ActionTypes.USER_PASSWORD_UPDATE__SUCCESS:
|
||||||
|
if (payload.accessToken) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
accessToken: payload.accessToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import ModalTypes from '../constants/ModalTypes';
|
|||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
isInitializing: true,
|
isInitializing: true,
|
||||||
|
isLogouting: false,
|
||||||
currentModal: null,
|
currentModal: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,6 +23,11 @@ export default (state = initialState, { type, payload }) => {
|
|||||||
...state,
|
...state,
|
||||||
isInitializing: false,
|
isInitializing: false,
|
||||||
};
|
};
|
||||||
|
case ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLogouting: true,
|
||||||
|
};
|
||||||
case ActionTypes.MODAL_OPEN:
|
case ActionTypes.MODAL_OPEN:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -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 watchers from './watchers';
|
||||||
import services from './services';
|
import services from './services';
|
||||||
import { socket } from '../../api';
|
import { socket } from '../../api';
|
||||||
import { removeAccessToken } from '../../utils/access-token-storage';
|
|
||||||
import ActionTypes from '../../constants/ActionTypes';
|
import ActionTypes from '../../constants/ActionTypes';
|
||||||
import Paths from '../../constants/Paths';
|
import Paths from '../../constants/Paths';
|
||||||
|
|
||||||
@@ -15,6 +14,5 @@ export default function* coreSaga() {
|
|||||||
|
|
||||||
yield take(ActionTypes.LOGOUT);
|
yield take(ActionTypes.LOGOUT);
|
||||||
|
|
||||||
yield call(removeAccessToken);
|
|
||||||
window.location.href = Paths.LOGIN;
|
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 actions from '../../actions';
|
||||||
import { getAccessToken } from '../../utils/access-token-storage';
|
import { removeAccessToken } from '../../utils/access-token-storage';
|
||||||
import ErrorCodes from '../../constants/ErrorCodes';
|
import ErrorCodes from '../../constants/ErrorCodes';
|
||||||
|
|
||||||
let lastRequestTask;
|
let lastRequestTask;
|
||||||
@@ -13,7 +14,7 @@ function* queueRequest(method, ...args) {
|
|||||||
} catch {} // eslint-disable-line no-empty
|
} catch {} // eslint-disable-line no-empty
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = yield call(getAccessToken);
|
const accessToken = yield select(selectors.selectAccessToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return yield call(method, ...args, {
|
return yield call(method, ...args, {
|
||||||
@@ -21,6 +22,7 @@ function* queueRequest(method, ...args) {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === ErrorCodes.UNAUTHORIZED) {
|
if (error.code === ErrorCodes.UNAUTHORIZED) {
|
||||||
|
yield call(removeAccessToken);
|
||||||
yield put(actions.logout()); // TODO: next url
|
yield put(actions.logout()); // TODO: next url
|
||||||
yield take();
|
yield take();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { call, put, take } from 'redux-saga/effects';
|
import { call, put, take } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
import request from '../request';
|
||||||
import requests from '../requests';
|
import requests from '../requests';
|
||||||
import actions from '../../../actions';
|
import actions from '../../../actions';
|
||||||
|
import api from '../../../api';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
|
import { removeAccessToken } from '../../../utils/access-token-storage';
|
||||||
|
|
||||||
export function* initializeCore() {
|
export function* initializeCore() {
|
||||||
const {
|
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 put(actions.logout());
|
||||||
yield take();
|
yield take();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,11 +124,13 @@ export function* updateUserPassword(id, data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessTokens && accessTokens[0]) {
|
const accessToken = accessTokens && accessTokens[0];
|
||||||
yield call(setAccessToken, 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) {
|
export function* updateCurrentUserPassword(data) {
|
||||||
@@ -215,7 +217,7 @@ export function* handleUserDelete(user) {
|
|||||||
const currentUserId = yield select(selectors.selectCurrentUserId);
|
const currentUserId = yield select(selectors.selectCurrentUserId);
|
||||||
|
|
||||||
if (user.id === currentUserId) {
|
if (user.id === currentUserId) {
|
||||||
yield call(logout);
|
yield call(logout, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
yield put(actions.handleUserDelete(user));
|
yield put(actions.handleUserDelete(user));
|
||||||
|
|||||||
@@ -2,18 +2,13 @@ import { all, call, cancel, fork, take } from 'redux-saga/effects';
|
|||||||
|
|
||||||
import watchers from './watchers';
|
import watchers from './watchers';
|
||||||
import services from './services';
|
import services from './services';
|
||||||
import { setAccessToken } from '../../utils/access-token-storage';
|
|
||||||
import ActionTypes from '../../constants/ActionTypes';
|
import ActionTypes from '../../constants/ActionTypes';
|
||||||
|
|
||||||
export default function* loginSaga() {
|
export default function* loginSaga() {
|
||||||
const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));
|
const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));
|
||||||
|
|
||||||
const {
|
yield take(ActionTypes.AUTHENTICATE__SUCCESS);
|
||||||
payload: { accessToken },
|
|
||||||
} = yield take(ActionTypes.AUTHENTICATE__SUCCESS);
|
|
||||||
|
|
||||||
yield cancel(watcherTasks);
|
yield cancel(watcherTasks);
|
||||||
|
|
||||||
yield call(setAccessToken, accessToken);
|
|
||||||
yield call(services.goToRoot);
|
yield call(services.goToRoot);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { call, put } from 'redux-saga/effects';
|
|||||||
|
|
||||||
import actions from '../../../actions';
|
import actions from '../../../actions';
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
|
import { setAccessToken } from '../../../utils/access-token-storage';
|
||||||
|
|
||||||
export function* authenticate(data) {
|
export function* authenticate(data) {
|
||||||
yield put(actions.authenticate(data));
|
yield put(actions.authenticate(data));
|
||||||
@@ -14,6 +15,7 @@ export function* authenticate(data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yield call(setAccessToken, accessToken);
|
||||||
yield put(actions.authenticate.success(accessToken));
|
yield put(actions.authenticate.success(accessToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
selectAccessToken,
|
|
||||||
};
|
|
||||||
@@ -4,8 +4,12 @@ import isUndefined from 'lodash/isUndefined';
|
|||||||
import orm from '../orm';
|
import orm from '../orm';
|
||||||
import Config from '../constants/Config';
|
import Config from '../constants/Config';
|
||||||
|
|
||||||
|
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
|
||||||
|
|
||||||
export const selectIsCoreInitializing = ({ core: { isInitializing } }) => isInitializing;
|
export const selectIsCoreInitializing = ({ core: { isInitializing } }) => isInitializing;
|
||||||
|
|
||||||
|
export const selectIsLogouting = ({ core: { isLogouting } }) => isLogouting;
|
||||||
|
|
||||||
const nextPosition = (items, index, excludedId) => {
|
const nextPosition = (items, index, excludedId) => {
|
||||||
const filteredItems = isUndefined(excludedId)
|
const filteredItems = isUndefined(excludedId)
|
||||||
? items
|
? items
|
||||||
@@ -94,7 +98,9 @@ export const selectNextTaskPosition = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
selectAccessToken,
|
||||||
selectIsCoreInitializing,
|
selectIsCoreInitializing,
|
||||||
|
selectIsLogouting,
|
||||||
selectNextBoardPosition,
|
selectNextBoardPosition,
|
||||||
selectNextListPosition,
|
selectNextListPosition,
|
||||||
selectNextCardPosition,
|
selectNextCardPosition,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import router from './router';
|
import router from './router';
|
||||||
import auth from './auth';
|
|
||||||
import core from './core';
|
import core from './core';
|
||||||
import modals from './modals';
|
import modals from './modals';
|
||||||
import users from './users';
|
import users from './users';
|
||||||
@@ -14,7 +13,6 @@ import attachments from './attachments';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
...router,
|
...router,
|
||||||
...auth,
|
|
||||||
...core,
|
...core,
|
||||||
...modals,
|
...modals,
|
||||||
...users,
|
...users,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import zxcvbn from 'zxcvbn';
|
||||||
|
|
||||||
const USERNAME_REGEX = /^[a-zA-Z0-9]+((_|\.)?[a-zA-Z0-9])*$/;
|
const USERNAME_REGEX = /^[a-zA-Z0-9]+((_|\.)?[a-zA-Z0-9])*$/;
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
export const isPassword = (string) => zxcvbn(string).score >= 2; // TODO: move to config
|
||||||
export const isUsername = (string) => {
|
|
||||||
return string.length >= 3 && string.length <= 16 && USERNAME_REGEX.test(string);
|
export const isUsername = (string) =>
|
||||||
};
|
string.length >= 3 && string.length <= 16 && USERNAME_REGEX.test(string);
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ services:
|
|||||||
- postgres
|
- postgres
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:alpine
|
image: postgres:14-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- db-data:/var/lib/postgresql/data
|
- db-data:/var/lib/postgresql/data
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "planka",
|
"name": "planka",
|
||||||
"version": "1.7.1",
|
"version": "1.8.5",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "planka",
|
"name": "planka",
|
||||||
"version": "1.7.1",
|
"version": "1.8.5",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "planka",
|
"name": "planka",
|
||||||
"version": "1.7.1",
|
"version": "1.8.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://plankanban.github.io/planka",
|
"homepage": "https://plankanban.github.io/planka",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -40,24 +40,33 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
|
const remoteAddress = getRemoteAddress(this.req);
|
||||||
|
|
||||||
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
|
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
sails.log.warn(
|
sails.log.warn(
|
||||||
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${getRemoteAddress(
|
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${remoteAddress})`,
|
||||||
this.req,
|
|
||||||
)})`,
|
|
||||||
);
|
);
|
||||||
throw Errors.INVALID_EMAIL_OR_USERNAME;
|
throw Errors.INVALID_EMAIL_OR_USERNAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bcrypt.compareSync(inputs.password, user.password)) {
|
if (!bcrypt.compareSync(inputs.password, user.password)) {
|
||||||
sails.log.warn(`Invalid password! (IP: ${getRemoteAddress(this.req)})`);
|
sails.log.warn(`Invalid password! (IP: ${remoteAddress})`);
|
||||||
throw Errors.INVALID_PASSWORD;
|
throw Errors.INVALID_PASSWORD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accessToken = sails.helpers.utils.createToken(user.id);
|
||||||
|
|
||||||
|
await Session.create({
|
||||||
|
accessToken,
|
||||||
|
remoteAddress,
|
||||||
|
userId: user.id,
|
||||||
|
userAgent: this.req.headers['user-agent'],
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
item: sails.helpers.utils.createToken(user.id),
|
item: accessToken,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
async fn() {
|
||||||
|
const { accessToken } = this.req;
|
||||||
|
|
||||||
|
await Session.updateOne({
|
||||||
|
accessToken,
|
||||||
|
deletedAt: null,
|
||||||
|
}).set({
|
||||||
|
deletedAt: new Date().toUTCString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
item: accessToken,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
const util = require('util');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
NOT_ENOUGH_RIGHTS: {
|
NOT_ENOUGH_RIGHTS: {
|
||||||
notEnoughRights: 'Not enough rights',
|
notEnoughRights: 'Not enough rights',
|
||||||
@@ -5,6 +8,9 @@ const Errors = {
|
|||||||
CARD_NOT_FOUND: {
|
CARD_NOT_FOUND: {
|
||||||
cardNotFound: 'Card not found',
|
cardNotFound: 'Card not found',
|
||||||
},
|
},
|
||||||
|
NO_FILE_WAS_UPLOADED: {
|
||||||
|
noFileWasUploaded: 'No file was uploaded',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -27,6 +33,9 @@ module.exports = {
|
|||||||
cardNotFound: {
|
cardNotFound: {
|
||||||
responseType: 'notFound',
|
responseType: 'notFound',
|
||||||
},
|
},
|
||||||
|
noFileWasUploaded: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
uploadError: {
|
uploadError: {
|
||||||
responseType: 'unprocessableEntity',
|
responseType: 'unprocessableEntity',
|
||||||
},
|
},
|
||||||
@@ -52,33 +61,37 @@ module.exports = {
|
|||||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.req
|
const upload = util.promisify((options, callback) =>
|
||||||
.file('file')
|
this.req.file('file').upload(options, (error, files) => callback(error, files)),
|
||||||
.upload(sails.helpers.utils.createAttachmentReceiver(), async (error, files) => {
|
);
|
||||||
if (error) {
|
|
||||||
return exits.uploadError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
let files;
|
||||||
return exits.uploadError('No file was uploaded');
|
try {
|
||||||
}
|
files = await upload({
|
||||||
|
saveAs: uuid(),
|
||||||
const file = files[0];
|
maxBytes: null,
|
||||||
|
|
||||||
const attachment = await sails.helpers.attachments.createOne(
|
|
||||||
{
|
|
||||||
...file.extra,
|
|
||||||
filename: file.filename,
|
|
||||||
},
|
|
||||||
currentUser,
|
|
||||||
card,
|
|
||||||
inputs.requestId,
|
|
||||||
this.req,
|
|
||||||
);
|
|
||||||
|
|
||||||
return exits.success({
|
|
||||||
item: attachment.toJSON(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return exits.uploadError(error.message); // TODO: add error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw Errors.NO_FILE_WAS_UPLOADED;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = _.last(files);
|
||||||
|
const fileData = await sails.helpers.attachments.processUploadedFile(file);
|
||||||
|
|
||||||
|
const attachment = await sails.helpers.attachments.createOne(
|
||||||
|
fileData,
|
||||||
|
currentUser,
|
||||||
|
card,
|
||||||
|
inputs.requestId,
|
||||||
|
this.req,
|
||||||
|
);
|
||||||
|
|
||||||
|
return exits.success({
|
||||||
|
item: attachment,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
async fn(inputs, exits) {
|
async fn(inputs) {
|
||||||
const { currentUser } = this.req;
|
const { currentUser } = this.req;
|
||||||
|
|
||||||
const { board } = await sails.helpers.boards
|
const { board } = await sails.helpers.boards
|
||||||
@@ -61,7 +61,7 @@ module.exports = {
|
|||||||
const tasks = await sails.helpers.cards.getTasks(cardIds);
|
const tasks = await sails.helpers.cards.getTasks(cardIds);
|
||||||
const attachments = await sails.helpers.cards.getAttachments(cardIds);
|
const attachments = await sails.helpers.cards.getAttachments(cardIds);
|
||||||
|
|
||||||
return exits.success({
|
return {
|
||||||
items: cards,
|
items: cards,
|
||||||
included: {
|
included: {
|
||||||
cardMemberships,
|
cardMemberships,
|
||||||
@@ -69,6 +69,6 @@ module.exports = {
|
|||||||
tasks,
|
tasks,
|
||||||
attachments,
|
attachments,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ module.exports = {
|
|||||||
|
|
||||||
let boardMemberships = await sails.helpers.users.getBoardMemberships(currentUser.id);
|
let boardMemberships = await sails.helpers.users.getBoardMemberships(currentUser.id);
|
||||||
|
|
||||||
let membershipBoardIds = sails.helpers.utils.mapRecords(boardMemberships, 'boardId');
|
const membershipBoardIds = sails.helpers.utils.mapRecords(boardMemberships, 'boardId');
|
||||||
|
|
||||||
let membershipBoards = await sails.helpers.boards.getMany({
|
let membershipBoards = await sails.helpers.boards.getMany({
|
||||||
id: membershipBoardIds,
|
id: membershipBoardIds,
|
||||||
@@ -21,16 +21,6 @@ module.exports = {
|
|||||||
|
|
||||||
membershipProjectIds = sails.helpers.utils.mapRecords(membershipProjects);
|
membershipProjectIds = sails.helpers.utils.mapRecords(membershipProjects);
|
||||||
|
|
||||||
membershipBoards = membershipBoards.filter((membershipBoard) =>
|
|
||||||
membershipProjectIds.includes(membershipBoard.projectId),
|
|
||||||
);
|
|
||||||
|
|
||||||
membershipBoardIds = sails.helpers.utils.mapRecords(membershipBoards);
|
|
||||||
|
|
||||||
boardMemberships = boardMemberships.filter((boardMembership) =>
|
|
||||||
membershipBoardIds.includes(boardMembership.boardId),
|
|
||||||
);
|
|
||||||
|
|
||||||
const projectIds = [...managerProjectIds, ...membershipProjectIds];
|
const projectIds = [...managerProjectIds, ...membershipProjectIds];
|
||||||
const projects = [...managerProjects, ...membershipProjects];
|
const projects = [...managerProjects, ...membershipProjects];
|
||||||
|
|
||||||
@@ -40,7 +30,17 @@ module.exports = {
|
|||||||
const users = await sails.helpers.users.getMany(userIds);
|
const users = await sails.helpers.users.getMany(userIds);
|
||||||
|
|
||||||
const managerBoards = await sails.helpers.projects.getBoards(managerProjectIds);
|
const managerBoards = await sails.helpers.projects.getBoards(managerProjectIds);
|
||||||
|
|
||||||
|
membershipBoards = membershipBoards.filter((membershipBoard) =>
|
||||||
|
membershipProjectIds.includes(membershipBoard.projectId),
|
||||||
|
);
|
||||||
|
|
||||||
const boards = [...managerBoards, ...membershipBoards];
|
const boards = [...managerBoards, ...membershipBoards];
|
||||||
|
const boardIds = sails.helpers.utils.mapRecords(boards);
|
||||||
|
|
||||||
|
boardMemberships = boardMemberships.filter((boardMembership) =>
|
||||||
|
boardIds.includes(boardMembership.boardId),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: projects,
|
items: projects,
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
|
const util = require('util');
|
||||||
|
const rimraf = require('rimraf');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
PROJECT_NOT_FOUND: {
|
PROJECT_NOT_FOUND: {
|
||||||
projectNotFound: 'Project not found',
|
projectNotFound: 'Project not found',
|
||||||
},
|
},
|
||||||
|
NO_FILE_WAS_UPLOADED: {
|
||||||
|
noFileWasUploaded: 'No file was uploaded',
|
||||||
|
},
|
||||||
|
FILE_IS_NOT_IMAGE: {
|
||||||
|
fileIsNotImage: 'File is not image',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -17,6 +27,12 @@ module.exports = {
|
|||||||
projectNotFound: {
|
projectNotFound: {
|
||||||
responseType: 'notFound',
|
responseType: 'notFound',
|
||||||
},
|
},
|
||||||
|
noFileWasUploaded: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
|
fileIsNotImage: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
uploadError: {
|
uploadError: {
|
||||||
responseType: 'unprocessableEntity',
|
responseType: 'unprocessableEntity',
|
||||||
},
|
},
|
||||||
@@ -37,32 +53,52 @@ module.exports = {
|
|||||||
throw Errors.PROJECT_NOT_FOUND; // Forbidden
|
throw Errors.PROJECT_NOT_FOUND; // Forbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
this.req
|
const upload = util.promisify((options, callback) =>
|
||||||
.file('file')
|
this.req.file('file').upload(options, (error, files) => callback(error, files)),
|
||||||
.upload(sails.helpers.utils.createProjectBackgroundImageReceiver(), async (error, files) => {
|
);
|
||||||
if (error) {
|
|
||||||
return exits.uploadError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
let files;
|
||||||
return exits.uploadError('No file was uploaded');
|
try {
|
||||||
}
|
files = await upload({
|
||||||
|
saveAs: uuid(),
|
||||||
project = await sails.helpers.projects.updateOne(
|
maxBytes: null,
|
||||||
project,
|
|
||||||
{
|
|
||||||
backgroundImageDirname: files[0].extra.dirname,
|
|
||||||
},
|
|
||||||
this.req,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
throw Errors.PROJECT_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
return exits.success({
|
|
||||||
item: project.toJSON(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return exits.uploadError(error.message); // TODO: add error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw Errors.NO_FILE_WAS_UPLOADED;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = _.last(files);
|
||||||
|
|
||||||
|
const fileData = await sails.helpers.projects
|
||||||
|
.processUploadedBackgroundImageFile(file)
|
||||||
|
.intercept('fileIsNotImage', () => {
|
||||||
|
try {
|
||||||
|
rimraf.sync(file.fd);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
return Errors.FILE_IS_NOT_IMAGE;
|
||||||
|
});
|
||||||
|
|
||||||
|
project = await sails.helpers.projects.updateOne(
|
||||||
|
project,
|
||||||
|
{
|
||||||
|
backgroundImageDirname: fileData.dirname,
|
||||||
|
},
|
||||||
|
this.req,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw Errors.PROJECT_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exits.success({
|
||||||
|
item: project,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
const zxcvbn = require('zxcvbn');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
EMAIL_ALREADY_IN_USE: {
|
EMAIL_ALREADY_IN_USE: {
|
||||||
emailAlreadyInUse: 'Email already in use',
|
emailAlreadyInUse: 'Email already in use',
|
||||||
@@ -16,6 +18,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
custom: (value) => zxcvbn(value).score >= 2, // TODO: move to config
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
|
const util = require('util');
|
||||||
|
const rimraf = require('rimraf');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
USER_NOT_FOUND: {
|
USER_NOT_FOUND: {
|
||||||
userNotFound: 'User not found',
|
userNotFound: 'User not found',
|
||||||
},
|
},
|
||||||
|
NO_FILE_WAS_UPLOADED: {
|
||||||
|
noFileWasUploaded: 'No file was uploaded',
|
||||||
|
},
|
||||||
|
FILE_IS_NOT_IMAGE: {
|
||||||
|
fileIsNotImage: 'File is not image',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -17,6 +27,12 @@ module.exports = {
|
|||||||
userNotFound: {
|
userNotFound: {
|
||||||
responseType: 'notFound',
|
responseType: 'notFound',
|
||||||
},
|
},
|
||||||
|
noFileWasUploaded: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
|
fileIsNotImage: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
uploadError: {
|
uploadError: {
|
||||||
responseType: 'unprocessableEntity',
|
responseType: 'unprocessableEntity',
|
||||||
},
|
},
|
||||||
@@ -38,33 +54,53 @@ module.exports = {
|
|||||||
user = currentUser;
|
user = currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.req
|
const upload = util.promisify((options, callback) =>
|
||||||
.file('file')
|
this.req.file('file').upload(options, (error, files) => callback(error, files)),
|
||||||
.upload(sails.helpers.utils.createUserAvatarReceiver(), async (error, files) => {
|
);
|
||||||
if (error) {
|
|
||||||
return exits.uploadError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
let files;
|
||||||
return exits.uploadError('No file was uploaded');
|
try {
|
||||||
}
|
files = await upload({
|
||||||
|
saveAs: uuid(),
|
||||||
user = await sails.helpers.users.updateOne(
|
maxBytes: null,
|
||||||
user,
|
|
||||||
{
|
|
||||||
avatarDirname: files[0].extra.dirname,
|
|
||||||
},
|
|
||||||
currentUser,
|
|
||||||
this.req,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw Errors.USER_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
return exits.success({
|
|
||||||
item: user.toJSON(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return exits.uploadError(error.message); // TODO: add error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw Errors.NO_FILE_WAS_UPLOADED;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = _.last(files);
|
||||||
|
|
||||||
|
const fileData = await sails.helpers.users
|
||||||
|
.processUploadedAvatarFile(file)
|
||||||
|
.intercept('fileIsNotImage', () => {
|
||||||
|
try {
|
||||||
|
rimraf.sync(file.fd);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
return Errors.FILE_IS_NOT_IMAGE;
|
||||||
|
});
|
||||||
|
|
||||||
|
user = await sails.helpers.users.updateOne(
|
||||||
|
user,
|
||||||
|
{
|
||||||
|
avatarDirname: fileData.dirname,
|
||||||
|
},
|
||||||
|
currentUser,
|
||||||
|
this.req,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw Errors.USER_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exits.success({
|
||||||
|
item: user,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
|
const zxcvbn = require('zxcvbn');
|
||||||
|
|
||||||
|
const { getRemoteAddress } = require('../../../utils/remoteAddress');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
USER_NOT_FOUND: {
|
USER_NOT_FOUND: {
|
||||||
@@ -18,6 +21,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
custom: (value) => zxcvbn(value).score >= 2, // TODO: move to config
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
currentPassword: {
|
currentPassword: {
|
||||||
@@ -69,6 +73,13 @@ module.exports = {
|
|||||||
if (user.id === currentUser.id) {
|
if (user.id === currentUser.id) {
|
||||||
const accessToken = sails.helpers.utils.createToken(user.id, user.passwordUpdatedAt);
|
const accessToken = sails.helpers.utils.createToken(user.id, user.passwordUpdatedAt);
|
||||||
|
|
||||||
|
await Session.create({
|
||||||
|
accessToken,
|
||||||
|
userId: user.id,
|
||||||
|
remoteAddress: getRemoteAddress(this.req),
|
||||||
|
userAgent: this.req.headers['user-agent'],
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
item: user,
|
item: user,
|
||||||
included: {
|
included: {
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const rimraf = require('rimraf');
|
||||||
|
const moveFile = require('move-file');
|
||||||
|
const filenamify = require('filenamify');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
const sharp = require('sharp');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
file: {
|
||||||
|
type: 'json',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs) {
|
||||||
|
const dirname = uuid();
|
||||||
|
|
||||||
|
// FIXME: https://github.com/sindresorhus/filenamify/issues/13
|
||||||
|
const filename = filenamify(inputs.file.filename);
|
||||||
|
|
||||||
|
const rootPath = path.join(sails.config.custom.attachmentsPath, dirname);
|
||||||
|
const filePath = path.join(rootPath, filename);
|
||||||
|
|
||||||
|
fs.mkdirSync(rootPath);
|
||||||
|
await moveFile(inputs.file.fd, filePath);
|
||||||
|
|
||||||
|
const image = sharp(filePath);
|
||||||
|
let metadata;
|
||||||
|
|
||||||
|
try {
|
||||||
|
metadata = await image.metadata();
|
||||||
|
} catch (error) {} // eslint-disable-line no-empty
|
||||||
|
|
||||||
|
const fileData = {
|
||||||
|
dirname,
|
||||||
|
filename,
|
||||||
|
image: null,
|
||||||
|
name: inputs.file.filename,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (metadata && !['svg', 'pdf'].includes(metadata.format)) {
|
||||||
|
const thumbnailsPath = path.join(rootPath, 'thumbnails');
|
||||||
|
fs.mkdirSync(thumbnailsPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await image
|
||||||
|
.resize(
|
||||||
|
metadata.height > metadata.width
|
||||||
|
? {
|
||||||
|
width: 256,
|
||||||
|
height: 320,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
width: 256,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.jpeg({
|
||||||
|
quality: 100,
|
||||||
|
chromaSubsampling: '4:4:4',
|
||||||
|
})
|
||||||
|
.toFile(path.join(thumbnailsPath, 'cover-256.jpg'));
|
||||||
|
|
||||||
|
fileData.image = _.pick(metadata, ['width', 'height']);
|
||||||
|
} catch (error1) {
|
||||||
|
try {
|
||||||
|
rimraf.sync(thumbnailsPath);
|
||||||
|
} catch (error2) {
|
||||||
|
console.warn(error2.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileData;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
inputs: {
|
inputs: {
|
||||||
record: {
|
record: {
|
||||||
@@ -29,18 +31,9 @@ module.exports = {
|
|||||||
const boardMembership = await BoardMembership.destroyOne(inputs.record.id);
|
const boardMembership = await BoardMembership.destroyOne(inputs.record.id);
|
||||||
|
|
||||||
if (boardMembership) {
|
if (boardMembership) {
|
||||||
sails.sockets.broadcast(
|
const notify = (room) => {
|
||||||
`user:${boardMembership.userId}`,
|
|
||||||
'boardMembershipDelete',
|
|
||||||
{
|
|
||||||
item: boardMembership,
|
|
||||||
},
|
|
||||||
inputs.request,
|
|
||||||
);
|
|
||||||
|
|
||||||
const notifyBoard = () => {
|
|
||||||
sails.sockets.broadcast(
|
sails.sockets.broadcast(
|
||||||
`board:${boardMembership.boardId}`,
|
room,
|
||||||
'boardMembershipDelete',
|
'boardMembershipDelete',
|
||||||
{
|
{
|
||||||
item: boardMembership,
|
item: boardMembership,
|
||||||
@@ -54,16 +47,32 @@ module.exports = {
|
|||||||
inputs.project.id,
|
inputs.project.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isProjectManager) {
|
if (!isProjectManager) {
|
||||||
notifyBoard();
|
|
||||||
} else {
|
|
||||||
// TODO: also remove if unsubscribed to user
|
|
||||||
sails.sockets.removeRoomMembersFromRooms(
|
sails.sockets.removeRoomMembersFromRooms(
|
||||||
`user:${boardMembership.userId}`,
|
`@user:${boardMembership.userId}`,
|
||||||
`board:${boardMembership.boardId}`,
|
`board:${boardMembership.boardId}`,
|
||||||
notifyBoard,
|
() => {
|
||||||
|
notify(`board:${boardMembership.boardId}`);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notify(`user:${boardMembership.userId}`);
|
||||||
|
|
||||||
|
if (isProjectManager) {
|
||||||
|
const tempRoom = uuid();
|
||||||
|
|
||||||
|
sails.sockets.addRoomMembersToRooms(`board:${boardMembership.boardId}`, tempRoom, () => {
|
||||||
|
sails.sockets.removeRoomMembersFromRooms(
|
||||||
|
`user:${boardMembership.userId}`,
|
||||||
|
tempRoom,
|
||||||
|
() => {
|
||||||
|
notify(tempRoom);
|
||||||
|
sails.sockets.removeRoomMembersFromRooms(tempRoom, tempRoom);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return boardMembership;
|
return boardMembership;
|
||||||
|
|||||||
@@ -17,16 +17,23 @@ module.exports = {
|
|||||||
const board = await Board.archiveOne(inputs.record.id);
|
const board = await Board.archiveOne(inputs.record.id);
|
||||||
|
|
||||||
if (board) {
|
if (board) {
|
||||||
sails.sockets.leaveAll(`board:${board.id}`);
|
sails.sockets.removeRoomMembersFromRooms(`board:${board.id}`, `board:${board.id}`);
|
||||||
|
|
||||||
sails.sockets.broadcast(
|
const managerUserIds = await sails.helpers.projects.getManagerUserIds(board.projectId);
|
||||||
`project:${board.projectId}`,
|
const memberUserIds = await sails.helpers.boards.getMemberUserIds(board.id);
|
||||||
'boardDelete',
|
|
||||||
{
|
const userIds = _.union(managerUserIds, memberUserIds);
|
||||||
item: board,
|
|
||||||
},
|
userIds.forEach((userId) => {
|
||||||
inputs.request,
|
sails.sockets.broadcast(
|
||||||
);
|
`user:${userId}`,
|
||||||
|
'boardDelete',
|
||||||
|
{
|
||||||
|
item: board,
|
||||||
|
},
|
||||||
|
inputs.request,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return board;
|
return board;
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
const userIds = await sails.helpers.projects.getManagerAndBoardMemberUserIds(
|
const managerUserIds = await sails.helpers.projects.getManagerUserIds(inputs.record.projectId);
|
||||||
inputs.record.projectId,
|
const memberUserIds = await sails.helpers.boards.getMemberUserIds(inputs.record.id);
|
||||||
);
|
|
||||||
|
const userIds = _.union(managerUserIds, memberUserIds);
|
||||||
|
|
||||||
if (!_.isUndefined(inputs.values.position)) {
|
if (!_.isUndefined(inputs.values.position)) {
|
||||||
const boards = await sails.helpers.projects.getBoards(
|
const boards = await sails.helpers.projects.getBoards(
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ module.exports = {
|
|||||||
const userIds = _.union(managerUserIds, memberUserIds);
|
const userIds = _.union(managerUserIds, memberUserIds);
|
||||||
|
|
||||||
userIds.forEach((userId) => {
|
userIds.forEach((userId) => {
|
||||||
sails.sockets.removeRoomMembersFromRooms(`user:${userId}`, boardRooms);
|
sails.sockets.removeRoomMembersFromRooms(`@user:${userId}`, boardRooms);
|
||||||
|
|
||||||
sails.sockets.broadcast(
|
sails.sockets.broadcast(
|
||||||
`user:${userId}`,
|
`user:${userId}`,
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const rimraf = require('rimraf');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
const sharp = require('sharp');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
file: {
|
||||||
|
type: 'json',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
exits: {
|
||||||
|
fileIsNotImage: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs) {
|
||||||
|
const image = sharp(inputs.file.fd);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await image.metadata();
|
||||||
|
} catch (error) {
|
||||||
|
throw 'fileIsNotImage';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirname = uuid();
|
||||||
|
const rootPath = path.join(sails.config.custom.projectBackgroundImagesPath, dirname);
|
||||||
|
|
||||||
|
fs.mkdirSync(rootPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await image.jpeg().toFile(path.join(rootPath, 'original.jpg'));
|
||||||
|
|
||||||
|
await image
|
||||||
|
.resize(336, 200)
|
||||||
|
.jpeg({
|
||||||
|
quality: 100,
|
||||||
|
chromaSubsampling: '4:4:4',
|
||||||
|
})
|
||||||
|
.toFile(path.join(rootPath, 'cover-336.jpg'));
|
||||||
|
} catch (error1) {
|
||||||
|
try {
|
||||||
|
rimraf.sync(rootPath);
|
||||||
|
} catch (error2) {
|
||||||
|
console.warn(error2.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
throw 'fileIsNotImage';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
rimraf.sync(inputs.file.fd);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dirname,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const rimraf = require('rimraf');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
const sharp = require('sharp');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
file: {
|
||||||
|
type: 'json',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
exits: {
|
||||||
|
fileIsNotImage: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs) {
|
||||||
|
const image = sharp(inputs.file.fd);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await image.metadata();
|
||||||
|
} catch (error) {
|
||||||
|
throw 'fileIsNotImage';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirname = uuid();
|
||||||
|
const rootPath = path.join(sails.config.custom.userAvatarsPath, dirname);
|
||||||
|
|
||||||
|
fs.mkdirSync(rootPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await image
|
||||||
|
.jpeg({
|
||||||
|
quality: 100,
|
||||||
|
chromaSubsampling: '4:4:4',
|
||||||
|
})
|
||||||
|
.toFile(path.join(rootPath, 'original.jpg'));
|
||||||
|
|
||||||
|
await image
|
||||||
|
.resize(100, 100)
|
||||||
|
.jpeg({
|
||||||
|
quality: 100,
|
||||||
|
chromaSubsampling: '4:4:4',
|
||||||
|
})
|
||||||
|
.toFile(path.join(rootPath, 'square-100.jpg'));
|
||||||
|
} catch (error1) {
|
||||||
|
try {
|
||||||
|
rimraf.sync(rootPath);
|
||||||
|
} catch (error2) {
|
||||||
|
console.warn(error2.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
throw 'fileIsNotImage';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
rimraf.sync(inputs.file.fd);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dirname,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -123,13 +123,13 @@ module.exports = {
|
|||||||
if (user.id === inputs.user.id && inputs.request && inputs.request.isSocket) {
|
if (user.id === inputs.user.id && inputs.request && inputs.request.isSocket) {
|
||||||
const tempRoom = uuid();
|
const tempRoom = uuid();
|
||||||
|
|
||||||
sails.sockets.addRoomMembersToRooms(`user:${user.id}`, tempRoom, () => {
|
sails.sockets.addRoomMembersToRooms(`@user:${user.id}`, tempRoom, () => {
|
||||||
sails.sockets.leave(inputs.request, tempRoom, () => {
|
sails.sockets.leave(inputs.request, tempRoom, () => {
|
||||||
sails.sockets.leaveAll(tempRoom);
|
sails.sockets.leaveAll(tempRoom);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
sails.sockets.leaveAll(`user:${user.id}`);
|
sails.sockets.leaveAll(`@user:${user.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const util = require('util');
|
|
||||||
const stream = require('stream');
|
|
||||||
const streamToArray = require('stream-to-array');
|
|
||||||
const filenamify = require('filenamify');
|
|
||||||
const { v4: uuid } = require('uuid');
|
|
||||||
const sharp = require('sharp');
|
|
||||||
|
|
||||||
const writeFile = util.promisify(fs.writeFile);
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
sync: true,
|
|
||||||
|
|
||||||
fn() {
|
|
||||||
const receiver = stream.Writable({
|
|
||||||
objectMode: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let firstFileHandled = false;
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
|
||||||
receiver._write = async (file, receiverEncoding, done) => {
|
|
||||||
if (firstFileHandled) {
|
|
||||||
file.pipe(new stream.Writable());
|
|
||||||
|
|
||||||
return done();
|
|
||||||
}
|
|
||||||
firstFileHandled = true;
|
|
||||||
|
|
||||||
const buffer = await streamToArray(file).then((parts) =>
|
|
||||||
Buffer.concat(parts.map((part) => (Buffer.isBuffer(part) ? part : Buffer.from(part)))),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const dirname = uuid();
|
|
||||||
|
|
||||||
// FIXME: https://github.com/sindresorhus/filenamify/issues/13
|
|
||||||
const filename = filenamify(file.filename);
|
|
||||||
|
|
||||||
const rootPath = path.join(sails.config.custom.attachmentsPath, dirname);
|
|
||||||
fs.mkdirSync(rootPath);
|
|
||||||
|
|
||||||
await writeFile(path.join(rootPath, filename), buffer);
|
|
||||||
|
|
||||||
const image = sharp(buffer);
|
|
||||||
let metadata;
|
|
||||||
|
|
||||||
try {
|
|
||||||
metadata = await image.metadata();
|
|
||||||
} catch (error) {} // eslint-disable-line no-empty
|
|
||||||
|
|
||||||
const extra = {
|
|
||||||
dirname,
|
|
||||||
name: file.filename,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (metadata && !['svg', 'pdf'].includes(metadata.format)) {
|
|
||||||
let cover256Buffer;
|
|
||||||
if (metadata.height > metadata.width) {
|
|
||||||
cover256Buffer = await image
|
|
||||||
.resize(256, 320)
|
|
||||||
.jpeg({
|
|
||||||
quality: 100,
|
|
||||||
chromaSubsampling: '4:4:4',
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
} else {
|
|
||||||
cover256Buffer = await image
|
|
||||||
.resize({
|
|
||||||
width: 256,
|
|
||||||
})
|
|
||||||
.jpeg({
|
|
||||||
quality: 100,
|
|
||||||
chromaSubsampling: '4:4:4',
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
const thumbnailsPath = path.join(rootPath, 'thumbnails');
|
|
||||||
fs.mkdirSync(thumbnailsPath);
|
|
||||||
|
|
||||||
await writeFile(path.join(thumbnailsPath, 'cover-256.jpg'), cover256Buffer);
|
|
||||||
|
|
||||||
extra.image = _.pick(metadata, ['width', 'height']);
|
|
||||||
} else {
|
|
||||||
extra.image = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(file, {
|
|
||||||
filename,
|
|
||||||
extra,
|
|
||||||
});
|
|
||||||
|
|
||||||
return done();
|
|
||||||
} catch (error) {
|
|
||||||
return done(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return receiver;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const util = require('util');
|
|
||||||
const stream = require('stream');
|
|
||||||
const streamToArray = require('stream-to-array');
|
|
||||||
const { v4: uuid } = require('uuid');
|
|
||||||
const sharp = require('sharp');
|
|
||||||
|
|
||||||
const writeFile = util.promisify(fs.writeFile);
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
sync: true,
|
|
||||||
|
|
||||||
fn() {
|
|
||||||
const receiver = stream.Writable({
|
|
||||||
objectMode: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let firstFileHandled = false;
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
|
||||||
receiver._write = async (file, receiverEncoding, done) => {
|
|
||||||
if (firstFileHandled) {
|
|
||||||
file.pipe(new stream.Writable());
|
|
||||||
|
|
||||||
return done();
|
|
||||||
}
|
|
||||||
firstFileHandled = true;
|
|
||||||
|
|
||||||
const buffer = await streamToArray(file).then((parts) =>
|
|
||||||
Buffer.concat(parts.map((part) => (Buffer.isBuffer(part) ? part : Buffer.from(part)))),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const originalBuffer = await sharp(buffer).jpeg().toBuffer();
|
|
||||||
|
|
||||||
const cover336Buffer = await sharp(buffer)
|
|
||||||
.resize(336, 200)
|
|
||||||
.jpeg({
|
|
||||||
quality: 100,
|
|
||||||
chromaSubsampling: '4:4:4',
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
const dirname = uuid();
|
|
||||||
|
|
||||||
const rootPath = path.join(sails.config.custom.projectBackgroundImagesPath, dirname);
|
|
||||||
fs.mkdirSync(rootPath);
|
|
||||||
|
|
||||||
await writeFile(path.join(rootPath, 'original.jpg'), originalBuffer);
|
|
||||||
await writeFile(path.join(rootPath, 'cover-336.jpg'), cover336Buffer);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
file.extra = {
|
|
||||||
dirname,
|
|
||||||
};
|
|
||||||
|
|
||||||
return done();
|
|
||||||
} catch (error) {
|
|
||||||
return done(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return receiver;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { v4: uuid } = require('uuid');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -24,6 +25,9 @@ module.exports = {
|
|||||||
exp: iat + sails.config.custom.tokenExpiresIn * 24 * 60 * 60,
|
exp: iat + sails.config.custom.tokenExpiresIn * 24 * 60 * 60,
|
||||||
},
|
},
|
||||||
sails.config.session.secret,
|
sails.config.session.secret,
|
||||||
|
{
|
||||||
|
keyid: uuid(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user