Compare commits

...
30 Commits
Author SHA1 Message Date
Maksim Eltyshev a43af8eac5 chore: Update version 2022-11-20 15:34:04 +01:00
Maksim Eltyshev 409a7cde6b fix: Fix notifications popup styles 2022-11-20 15:30:07 +01:00
Maksim Eltyshev f62cba9bcd fix: Fix board actions for mobile 2022-11-20 15:14:49 +01:00
Maksim Eltyshev 9495234066 ref: Little refactoring 2022-11-20 15:05:21 +01:00
Rafly Maulana 5dcbad7b5a feat: Add markdown editor in card description (#333) 2022-11-20 18:48:42 +05:00
Maksim Eltyshev 064b37050c fix: Hide label actions based on role 2022-11-10 15:28:43 +01:00
Rafly Maulana bf8b8474ec meta: Fix documentation for installing Planka without Docker (#324) 2022-11-10 18:56:55 +05:00
Rafly Maulana 37a9c9bb42 meta: Add documentation for installing Planka without Docker (#323) 2022-11-10 18:48:38 +05:00
Maksim Eltyshev 00fccbaa3d fix: Fix z-indexes 2022-11-02 21:54:30 +01:00
Maksim Eltyshev b3323308c1 fix: Lock postgres version
Closes #312
2022-10-20 12:22:35 +02:00
Jacob Weisz 9108dc5b3e meta: Fix readme typo (#309) 2022-10-12 22:46:00 +05:00
Maksim Eltyshev 513f9a8ee5 chore: Update version 2022-10-03 13:13:59 +02:00
Maksim Eltyshev e051f624a8 ref: Little change for consistency 2022-10-03 13:12:16 +02:00
Maksim Eltyshev 172f4fcf34 fix: Fix missing board memberships loading 2022-10-03 13:04:52 +02:00
Maksim Eltyshev 9f16881965 fix: Socket bug fixes and improvements 2022-10-03 12:11:19 +02:00
Connor Lanigan 4e876922f8 feat: Prefill label creation with search term (#307) 2022-10-03 14:08:39 +05:00
Connor Lanigan d82d928e29 fix: Update German translation (#306) 2022-10-03 14:06:51 +05:00
Maksim Eltyshev 94dc83706c chore: Update version 2022-09-30 11:52:19 +02:00
Jacques Lorentz b08e731419 fix: Front-end base url with path (#303)
Closes #43, closes #111, closes #272
2022-09-30 14:48:58 +05:00
Maksim Eltyshev 1ce0fac281 chore: Update version 2022-09-28 18:46:37 +02:00
Maksim Eltyshev e3d00c8791 fix: Disable quick timer actions for viewers 2022-09-28 18:38:03 +02:00
Maksim Eltyshev e1a0b635ea ref: Little refactoring 2022-09-28 18:30:03 +02:00
Maksim Eltyshev cc7959bb15 chore: Update version 2022-09-16 15:08:51 +05:00
Maksim Eltyshev 56044e6d8a ref: Fix filename 2022-09-16 15:08:09 +05:00
Maksim Eltyshev e5bd73f33e fix: Prevent popup from leaving window 2022-09-16 14:39:41 +05:00
Maksim Eltyshev e65ab6ece7 fix: Fix flag for English 2022-09-15 09:07:57 +05:00
Maksim Eltyshev 14cc55a8c0 feat: Highlight active timer, add quick actions 2022-09-15 00:20:05 +05:00
Maksim Eltyshev b4306d0a4b fix: Fix font of timer numbers 2022-09-14 21:22:08 +05:00
Maksim Eltyshev 283c850ebf feat: Add about pane 2022-09-14 16:52:50 +05:00
Maksim Eltyshev 62de71bd97 fix: Visually highlight card cover 2022-09-13 16:55:22 +05:00
51 changed files with 853 additions and 224 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ COPY server .
RUN cp .env.sample .env
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/project-background-images
+144 -2
View File
@@ -21,7 +21,12 @@ A Trello-like kanban board built with React and Redux.
## Deploy
### Docker Compose
There are 2 types of installation:
1. [Dockerized](#1-docker-compose)
2. [Without Docker](#2-without-docker)
### 1. Docker Compose
[![](https://d207aa93qlcgug.cloudfront.net/1.95.5.qa/img/nav/docker-logo-loggedout.png)](https://hub.docker.com/r/meltyshev/planka)
@@ -44,9 +49,146 @@ docker-compose up -d
Demo user: demo@demo.demo demo
### 2. Without Docker
Installing without Docker is a bit more complicated, here's what you need to do:
1. Clone this repository into a directory of your choice. (e.g. `/var/www/planka`)
```bash
mkdir -p /var/www/planka
cd /var/www/planka
git clone https://github.com/plankanban/planka.git .
```
2. Install dependencies for client and build it.
```bash
cd client
npm install
npm run build
```
**Note**: You can use `yarn` or `pnpm` instead of `npm`.
3. Copy the `build` directory to the `server/public` directory.
```bash
cp -r build ../server/public
cp build/index.html ../server/views/index.ejs
```
4. Install dependencies for server.
```bash
cd ../server
npm install
```
5. Configure environment variables.
```bash
cp .env.sample .env
# Edit .env file (You could use nano, vim, etc.)
nano .env
```
**Note**: Before continuing, make sure you have your selected database created and running.
6. Copy start script from the root directory to the `server` directory.
```bash
cp ../docker-start.sh start.sh
```
7. Start the server.
```bash
./start.sh
```
**Note**: You can use `pm2` or `systemd` to run the server in the background.
## Additional information
### Nginx configuration
Here is an example of Nginx configuration for Planka, make sure to replace `<domain>` with your domain name, and make sure to configure the SSL.
```nginx
upstream planka {
server localhost:1337;
keepalive 32;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name <domain>;
access_log /var/log/nginx/planka-access.log;
error_log /var/log/nginx/planka-error.log error;
# SSL Configuration - Replace the example <domain> with your domain
ssl_certificate /etc/letsencrypt/live/<domain>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<domain>/privkey.pem;
ssl_session_cache shared:SSL:10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
ssl_prefer_server_ciphers on;
client_max_body_size 120M;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Max-Age 3600;
add_header Access-Control-Expose-Headers Content-Length;
add_header Access-Control-Allow-Headers Range;
# Make sure to allow socket.io connections
location ~* \.io {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
client_max_body_size 50M;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_buffers 256 16k;
proxy_buffer_size 16k;
client_body_timeout 60;
send_timeout 300;
lingering_timeout 5;
proxy_connect_timeout 1d;
proxy_send_timeout 1d;
proxy_read_timeout 1d;
proxy_pass http://planka;
}
location / {
client_max_body_size 50M;
proxy_set_header Connection "";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_buffers 256 16k;
proxy_buffer_size 16k;
proxy_read_timeout 600s;
proxy_cache_revalidate on;
proxy_cache_min_uses 2;
proxy_cache_use_stale timeout;
proxy_cache_lock on;
proxy_http_version 1.1;
proxy_pass http://planka;
}
}
```
### Logging
Planka currently allows you to expose the applciation's logfile directory to the host machine via a shared volume. This feature is not enabled by default.
Planka currently allows you to expose the application's logfile directory to the host machine via a shared volume. This feature is not enabled by default.
To expose the logfile director to the host machine, add the item `./logs/:/app/logs/` under `services.planka.volumes`.
+56
View File
@@ -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;
};
+194
View File
@@ -6,10 +6,12 @@
"": {
"name": "planka-client",
"dependencies": {
"@juggle/resize-observer": "^3.4.0",
"classnames": "^2.3.1",
"connected-react-router": "^6.9.3",
"date-fns": "^2.29.1",
"dequal": "^2.0.3",
"easymde": "^2.18.0",
"history": "^4.10.1",
"i18next": "^21.8.14",
"i18next-browser-languagedetector": "^6.1.4",
@@ -32,6 +34,7 @@
"react-redux": "^7.2.8",
"react-router-dom": "^5.3.1",
"react-scripts": "5.0.1",
"react-simplemde-editor": "^5.2.0",
"react-textarea-autosize": "^8.3.4",
"redux": "^4.2.0",
"redux-logger": "^3.0.6",
@@ -62,6 +65,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"jest-enzyme": "^7.1.2",
"prettier": "2.7.1",
"react-app-rewired": "^2.2.1",
"react-test-renderer": "^17.0.2"
}
},
@@ -3030,6 +3034,11 @@
"@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": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
@@ -3688,6 +3697,14 @@
"@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": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@@ -3826,6 +3843,11 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"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": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
@@ -3963,6 +3985,14 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
@@ -6165,6 +6195,19 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
@@ -7412,6 +7455,18 @@
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
"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": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -14756,6 +14811,17 @@
"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": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.1.tgz",
@@ -18659,6 +18725,30 @@
"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": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz",
@@ -20485,6 +20575,19 @@
"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": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz",
@@ -23842,6 +23945,11 @@
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
@@ -27333,6 +27441,11 @@
"@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": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
@@ -27778,6 +27891,14 @@
"@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": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@@ -27916,6 +28037,11 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"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": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
@@ -28053,6 +28179,14 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
@@ -29697,6 +29831,19 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
@@ -30609,6 +30756,18 @@
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
"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": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -36136,6 +36295,11 @@
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.2.tgz",
"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": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.1.tgz",
@@ -38706,6 +38870,23 @@
"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": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz",
@@ -40067,6 +40248,14 @@
"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": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz",
@@ -42683,6 +42872,11 @@
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+8 -4
View File
@@ -2,11 +2,11 @@
"name": "planka-client",
"private": true,
"scripts": {
"build": "react-scripts build",
"build": "react-app-rewired build",
"eject": "react-scripts eject",
"lint": "eslint --ext js,jsx src",
"start": "react-scripts start",
"test": "react-scripts test"
"lint": "eslint --ext js,jsx src config-overrides.js",
"start": "react-app-rewired start",
"test": "react-app-rewired test"
},
"browserslist": {
"production": [
@@ -63,10 +63,12 @@
}
},
"dependencies": {
"@juggle/resize-observer": "^3.4.0",
"classnames": "^2.3.1",
"connected-react-router": "^6.9.3",
"date-fns": "^2.29.1",
"dequal": "^2.0.3",
"easymde": "^2.18.0",
"history": "^4.10.1",
"i18next": "^21.8.14",
"i18next-browser-languagedetector": "^6.1.4",
@@ -89,6 +91,7 @@
"react-redux": "^7.2.8",
"react-router-dom": "^5.3.1",
"react-scripts": "5.0.1",
"react-simplemde-editor": "^5.2.0",
"react-textarea-autosize": "^8.3.4",
"redux": "^4.2.0",
"redux-logger": "^3.0.6",
@@ -119,6 +122,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"jest-enzyme": "^7.1.2",
"prettier": "2.7.1",
"react-app-rewired": "^2.2.1",
"react-test-renderer": "^17.0.2"
}
}
+1
View File
@@ -26,6 +26,7 @@
-->
<title>Planka</title>
</head>
<script>window.BASE_URL = "%PUBLIC_URL%";</script>
<body id="app">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
+2 -1
View File
@@ -5,7 +5,7 @@ import Config from '../constants/Config';
const io = sailsIOClient(socketIOClient);
io.sails.url = Config.SERVER_BASE_URL;
io.sails.url = Config.SERVER_HOST_NAME;
io.sails.autoConnect = false;
io.sails.reconnection = true;
io.sails.useCORSRouteToGetCookie = false;
@@ -13,6 +13,7 @@ io.sails.environment = process.env.NODE_ENV;
const { socket } = io;
socket.path = `${Config.BASE_PATH}/socket.io`;
socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => {
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@@ -14,6 +14,7 @@ const BoardActions = React.memo(
filterUsers,
filterLabels,
allUsers,
canEdit,
canEditMemberships,
onMembershipCreate,
onMembershipUpdate,
@@ -27,32 +28,35 @@ const BoardActions = React.memo(
onLabelDelete,
}) => {
return (
<div className={styles.actions}>
<div className={styles.action}>
<Memberships
items={memberships}
allUsers={allUsers}
permissionsSelectStep={BoardMembershipPermissionsSelectStep}
canEdit={canEditMemberships}
onCreate={onMembershipCreate}
onUpdate={onMembershipUpdate}
onDelete={onMembershipDelete}
/>
</div>
<div className={styles.action}>
<Filters
users={filterUsers}
labels={filterLabels}
allBoardMemberships={memberships}
allLabels={labels}
onUserAdd={onUserToFilterAdd}
onUserRemove={onUserFromFilterRemove}
onLabelAdd={onLabelToFilterAdd}
onLabelRemove={onLabelFromFilterRemove}
onLabelCreate={onLabelCreate}
onLabelUpdate={onLabelUpdate}
onLabelDelete={onLabelDelete}
/>
<div className={styles.wrapper}>
<div className={styles.actions}>
<div className={styles.action}>
<Memberships
items={memberships}
allUsers={allUsers}
permissionsSelectStep={BoardMembershipPermissionsSelectStep}
canEdit={canEditMemberships}
onCreate={onMembershipCreate}
onUpdate={onMembershipUpdate}
onDelete={onMembershipDelete}
/>
</div>
<div className={styles.action}>
<Filters
users={filterUsers}
labels={filterLabels}
allBoardMemberships={memberships}
allLabels={labels}
canEdit={canEdit}
onUserAdd={onUserToFilterAdd}
onUserRemove={onUserFromFilterRemove}
onLabelAdd={onLabelToFilterAdd}
onLabelRemove={onLabelFromFilterRemove}
onLabelCreate={onLabelCreate}
onLabelUpdate={onLabelUpdate}
onLabelDelete={onLabelDelete}
/>
</div>
</div>
</div>
);
@@ -67,6 +71,7 @@ BoardActions.propTypes = {
filterLabels: PropTypes.array.isRequired,
allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
canEdit: PropTypes.bool.isRequired,
canEditMemberships: PropTypes.bool.isRequired,
onMembershipCreate: PropTypes.func.isRequired,
onMembershipUpdate: PropTypes.func.isRequired,
@@ -1,5 +1,6 @@
:global(#app) {
.action {
flex: 0 0 auto;
margin-right: 20px;
}
@@ -8,4 +9,15 @@
display: flex;
margin: 20px 20px;
}
.wrapper {
overflow-x: auto;
overflow-y: hidden;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
}
@@ -15,6 +15,7 @@ const Filters = React.memo(
labels,
allBoardMemberships,
allLabels,
canEdit,
onUserAdd,
onUserRemove,
onLabelAdd,
@@ -74,6 +75,7 @@ const Filters = React.memo(
title={t('common.filterByLabels', {
context: 'title',
})}
canEdit={canEdit}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
@@ -108,6 +110,7 @@ Filters.propTypes = {
allBoardMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
canEdit: PropTypes.bool.isRequired,
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
onLabelAdd: PropTypes.func.isRequired,
@@ -19,7 +19,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
...defaultData,
}));
const handleRoleSelectClick = useCallback((role) => {
const handleSelectRoleClick = useCallback((role) => {
setData((prevData) => ({
...prevData,
role,
@@ -50,7 +50,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
<Menu secondary vertical className={styles.menu}>
<Menu.Item
active={data.role === BoardMembershipRoles.EDITOR}
onClick={() => handleRoleSelectClick(BoardMembershipRoles.EDITOR)}
onClick={() => handleSelectRoleClick(BoardMembershipRoles.EDITOR)}
>
<div className={styles.menuItemTitle}>{t('common.editor')}</div>
<div className={styles.menuItemDescription}>
@@ -59,7 +59,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
</Menu.Item>
<Menu.Item
active={data.role === BoardMembershipRoles.VIEWER}
onClick={() => handleRoleSelectClick(BoardMembershipRoles.VIEWER)}
onClick={() => handleSelectRoleClick(BoardMembershipRoles.VIEWER)}
>
<div className={styles.menuItemTitle}>{t('common.viewer')}</div>
<div className={styles.menuItemDescription}>{t('common.canOnlyViewBoard')}</div>
+19 -4
View File
@@ -5,6 +5,7 @@ import { Button, Icon } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { Draggable } from 'react-beautiful-dnd';
import { startTimer, stopTimer } from '../../utils/timer';
import Paths from '../../constants/Paths';
import Tasks from './Tasks';
import NameEdit from './NameEdit';
@@ -57,6 +58,17 @@ const Card = React.memo(
}
}, []);
const handleToggleTimerClick = useCallback(
(event) => {
event.preventDefault();
onUpdate({
timer: timer.startedAt ? stopTimer(timer) : startTimer(timer),
});
},
[timer, onUpdate],
);
const handleNameUpdate = useCallback(
(newName) => {
onUpdate({
@@ -108,7 +120,13 @@ const Card = React.memo(
)}
{timer && (
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
<Timer startedAt={timer.startedAt} total={timer.total} size="tiny" />
<Timer
as="span"
startedAt={timer.startedAt}
total={timer.total}
size="tiny"
onClick={canEdit ? handleToggleTimerClick : undefined}
/>
</span>
)}
</span>
@@ -148,14 +166,11 @@ const Card = React.memo(
{canEdit && (
<ActionsPopup
card={{
id,
name,
dueDate,
timer,
boardId,
listId,
projectId,
isPersisted,
}}
projectsToLists={allProjectsToLists}
boardMemberships={allBoardMemberships}
@@ -78,7 +78,9 @@
.cover {
border-radius: 3px 3px 0 0;
margin-bottom: 2px;
padding: 2px 2px 0;
vertical-align: middle;
width: 100%;
}
.details {
@@ -10,7 +10,7 @@
position: absolute;
text-align: center;
width: 100%;
z-index: 2000;
z-index: 2001;
}
.wrapper {
@@ -29,7 +29,6 @@
padding: 6px 32px 6px 128px;
margin: 0;
min-height: 80px;
z-index: 0;
}
.extension {
@@ -95,7 +94,6 @@
text-decoration: none;
top: 50%;
width: 112px;
z-index: 1;
}
.thumbnailLabel {
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { Button, Grid, Icon, Modal } from 'semantic-ui-react';
import { Markdown } from '../../lib/custom-ui';
import { startTimer, stopTimer } from '../../utils/timer';
import NameField from './NameField';
import DescriptionEdit from './DescriptionEdit';
import Tasks from './Tasks';
@@ -80,6 +81,12 @@ const CardModal = React.memo(
const isGalleryOpened = useRef(false);
const handleToggleTimerClick = useCallback(() => {
onUpdate({
timer: timer.startedAt ? stopTimer(timer) : startTimer(timer),
});
}, [timer, onUpdate]);
const handleNameUpdate = useCallback(
(newName) => {
onUpdate({
@@ -291,6 +298,19 @@ const CardModal = React.memo(
<Timer startedAt={timer.startedAt} total={timer.total} />
)}
</span>
{canEdit && (
<button
onClick={handleToggleTimerClick}
type="button"
className={classNames(styles.attachment, styles.dueDate)}
>
<Icon
name={timer.startedAt ? 'pause' : 'play'}
size="small"
className={styles.addAttachment}
/>
</button>
)}
</div>
)}
</div>
@@ -1,19 +1,15 @@
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import React, { useCallback, useImperativeHandle, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { useClosableForm, useField } from '../../hooks';
import { Button, Form } from 'semantic-ui-react';
import SimpleMDE from 'react-simplemde-editor';
import styles from './DescriptionEdit.module.scss';
const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) => {
const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
const [value, handleFieldChange, setValue] = useField(null);
const field = useRef(null);
const [value, setValue] = useState(null);
const open = useCallback(() => {
setIsOpened(true);
@@ -55,20 +51,37 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
[close],
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
close,
isOpened,
);
const handleSubmit = useCallback(() => {
close();
}, [close]);
useEffect(() => {
if (isOpened) {
field.current.ref.current.focus();
}
}, [isOpened]);
const mdEditorOptions = useMemo(
() => ({
autofocus: true,
spellChecker: false,
status: false,
toolbar: [
'bold',
'italic',
'heading',
'strikethrough',
'|',
'quote',
'unordered-list',
'ordered-list',
'table',
'|',
'link',
'image',
'|',
'undo',
'redo',
'|',
'guide',
],
}),
[],
);
if (!isOpened) {
return React.cloneElement(children, {
@@ -78,26 +91,17 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
return (
<Form onSubmit={handleSubmit}>
<TextArea
ref={field}
as={TextareaAutosize}
<SimpleMDE
value={value}
options={mdEditorOptions}
placeholder={t('common.enterDescription')}
minRows={3}
spellCheck={false}
className={styles.field}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
onBlur={handleFieldBlur}
onChange={setValue}
/>
<div className={styles.controls}>
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
<Button
positive
content={t('action.save')}
onMouseOver={handleControlMouseOver}
onMouseOut={handleControlMouseOut}
/>
<Button positive content={t('action.save')} />
</div>
</Form>
);
@@ -6,15 +6,12 @@
.field {
background: #fff;
border: 1px solid rgba(9, 30, 66, 0.13);
border-radius: 3px;
color: #17394d;
display: block;
font-size: 14px;
line-height: 1.5;
margin-bottom: 4px;
overflow: hidden;
padding: 8px 12px;
resize: none;
&:focus {
@@ -2,5 +2,6 @@
.wrapper {
position: fixed;
width: 100%;
z-index: 1;
}
}
@@ -7,7 +7,8 @@
background: none;
}
&:active, &:hover {
&:active,
&:hover {
background: transparent;
color: rgba(255, 255, 255, 0.9);
}
@@ -42,7 +43,6 @@
height: 50px;
margin: 0;
width: 100%;
z-index: 1;
}
.notification {
@@ -1,3 +1,4 @@
import truncate from 'lodash/truncate';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation, Trans } from 'react-i18next';
@@ -47,23 +48,26 @@ const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
{activity.data.toList.name}
</Trans>
);
case ActivityTypes.COMMENT_CARD:
case ActivityTypes.COMMENT_CARD: {
const commentText = truncate(activity.data.text);
return (
<Trans
i18nKey="common.userLeftNewCommentToCard"
values={{
user: activity.user.name,
comment: activity.data.text,
comment: commentText,
card: card.name,
}}
>
{activity.user.name}
{` left a new comment «${activity.data.text}» to `}
{` left a new comment «${commentText}» to `}
<Link to={Paths.CARDS.replace(':id', card.id)} onClick={onClose}>
{card.name}
</Link>
</Trans>
);
}
default:
}
@@ -80,9 +84,10 @@ const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
})}
</Popup.Header>
<Popup.Content>
{items.length > 0
? items.map((item) => (
<div key={item.id} className={styles.wrapper}>
{items.length > 0 ? (
<div className={styles.wrapper}>
{items.map((item) => (
<div key={item.id} className={styles.item}>
{item.card && item.activity ? (
<>
<User
@@ -90,20 +95,23 @@ const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
avatarUrl={item.activity.user.avatarUrl}
size="large"
/>
<span className={styles.content}>{renderItemContent(item)}</span>
<span className={styles.itemContent}>{renderItemContent(item)}</span>
</>
) : (
<div className={styles.deletedContent}>{t('common.cardOrActionAreDeleted')}</div>
<div className={styles.itemDeleted}>{t('common.cardOrActionAreDeleted')}</div>
)}
<Button
type="button"
icon="close"
className={styles.button}
icon="trash alternate outline"
className={styles.itemButton}
onClick={() => handleDelete(item.id)}
/>
</div>
))
: t('common.noUnreadNotifications')}
))}
</div>
) : (
t('common.noUnreadNotifications')
)}
</Popup.Content>
</>
);
@@ -1,5 +1,13 @@
:global(#app) {
.button {
.item {
padding: 12px;
&:hover {
background: #f0f0f0;
}
}
.itemButton {
background: transparent;
box-shadow: none;
float: right;
@@ -7,7 +15,7 @@
line-height: 20px;
margin: 0;
min-height: auto;
padding: 5px 0;
padding: 0;
transition: background 0.3s ease;
width: 20px;
@@ -16,7 +24,7 @@
}
}
.content {
.itemContent {
display: inline-block;
font-size: 12px;
min-height: 36px;
@@ -27,7 +35,7 @@
word-break: break-word;
}
.deletedContent {
.itemDeleted {
display: inline-block;
line-height: 20px;
min-height: 20px;
@@ -38,10 +46,21 @@
.wrapper {
margin: 0 -12px;
padding: 12px;
max-height: 60vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
&:hover {
background: #f0f0f0;
&::-webkit-scrollbar {
width: 5px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
border-radius: 3px;
}
}
}
+3 -1
View File
@@ -10,12 +10,13 @@ import Editor from './Editor';
import styles from './AddStep.module.scss';
const AddStep = React.memo(({ onCreate, onBack }) => {
const AddStep = React.memo(({ defaultData, onCreate, onBack }) => {
const [t] = useTranslation();
const [data, handleFieldChange] = useForm(() => ({
name: '',
color: LabelColors[0],
...defaultData,
}));
const handleSubmit = useCallback(() => {
@@ -46,6 +47,7 @@ const AddStep = React.memo(({ onCreate, onBack }) => {
});
AddStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onCreate: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
+37 -31
View File
@@ -8,45 +8,51 @@ import { Button } from 'semantic-ui-react';
import styles from './Item.module.scss';
import globalStyles from '../../styles.module.scss';
const Item = React.memo(({ name, color, isPersisted, isActive, onSelect, onDeselect, onEdit }) => {
const handleToggleClick = useCallback(() => {
if (isActive) {
onDeselect();
} else {
onSelect();
}
}, [isActive, onSelect, onDeselect]);
const Item = React.memo(
({ name, color, isPersisted, isActive, canEdit, onSelect, onDeselect, onEdit }) => {
const handleToggleClick = useCallback(() => {
if (isActive) {
onDeselect();
} else {
onSelect();
}
}, [isActive, onSelect, onDeselect]);
return (
<div className={styles.wrapper}>
<Button
fluid
content={name}
active={isActive}
disabled={!isPersisted}
className={classNames(
styles.labelButton,
isActive && styles.labelButtonActive,
globalStyles[`background${upperFirst(camelCase(color))}`],
return (
<div className={styles.wrapper}>
<Button
fluid
content={name}
active={isActive}
disabled={!isPersisted}
className={classNames(
styles.labelButton,
isActive && styles.labelButtonActive,
globalStyles[`background${upperFirst(camelCase(color))}`],
)}
onClick={handleToggleClick}
/>
{canEdit && (
<Button
icon="pencil"
size="small"
floated="right"
disabled={!isPersisted}
className={styles.editButton}
onClick={onEdit}
/>
)}
onClick={handleToggleClick}
/>
<Button
icon="pencil"
size="small"
disabled={!isPersisted}
className={styles.editButton}
onClick={onEdit}
/>
</div>
);
});
</div>
);
},
);
Item.propTypes = {
name: PropTypes.string,
color: PropTypes.string.isRequired,
isPersisted: PropTypes.bool.isRequired,
isActive: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
onSelect: PropTypes.func.isRequired,
onDeselect: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
@@ -4,7 +4,6 @@
box-shadow: none;
flex: 0 0 auto;
font-weight: normal;
margin: 0;
padding: 8px 10px;
text-decoration: underline;
@@ -18,6 +17,7 @@
flex: 1 1 auto;
font-size: 14px;
font-weight: bold;
margin-right: 0;
overflow: hidden;
padding: 8px 32px 8px 10px;
position: relative;
@@ -18,7 +18,18 @@ const StepTypes = {
};
const LabelsStep = React.memo(
({ items, currentIds, title, onSelect, onDeselect, onCreate, onUpdate, onDelete, onBack }) => {
({
items,
currentIds,
title,
canEdit,
onSelect,
onDeselect,
onCreate,
onUpdate,
onDelete,
onBack,
}) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const [search, handleSearchChange] = useField('');
@@ -84,7 +95,15 @@ const LabelsStep = React.memo(
if (step) {
switch (step.type) {
case StepTypes.ADD:
return <AddStep onCreate={onCreate} onBack={handleBack} />;
return (
<AddStep
defaultData={{
name: search,
}}
onCreate={onCreate}
onBack={handleBack}
/>
);
case StepTypes.EDIT: {
const currentItem = items.find((item) => item.id === step.params.id);
@@ -132,6 +151,7 @@ const LabelsStep = React.memo(
color={item.color}
isPersisted={item.isPersisted}
isActive={currentIds.includes(item.id)}
canEdit={canEdit}
onSelect={() => handleSelect(item.id)}
onDeselect={() => handleDeselect(item.id)}
onEdit={() => handleEdit(item.id)}
@@ -139,12 +159,14 @@ const LabelsStep = React.memo(
))}
</div>
)}
<Button
fluid
content={t('action.createNewLabel')}
className={styles.addButton}
onClick={handleAddClick}
/>
{canEdit && (
<Button
fluid
content={t('action.createNewLabel')}
className={styles.addButton}
onClick={handleAddClick}
/>
)}
</Popup.Content>
</>
);
@@ -157,6 +179,7 @@ LabelsStep.propTypes = {
currentIds: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
title: PropTypes.string,
canEdit: PropTypes.bool,
onSelect: PropTypes.func.isRequired,
onDeselect: PropTypes.func.isRequired,
onCreate: PropTypes.func.isRequired,
@@ -167,6 +190,7 @@ LabelsStep.propTypes = {
LabelsStep.defaultProps = {
title: 'common.labels',
canEdit: true,
onBack: undefined,
};
+1
View File
@@ -11,6 +11,7 @@ import NotFound from './NotFound';
import 'react-datepicker/dist/react-datepicker.css';
import 'photoswipe/dist/photoswipe.css';
import 'easymde/dist/easymde.min.css';
import '../lib/custom-ui/styles.css';
import '../styles.module.scss';
+8 -3
View File
@@ -14,7 +14,7 @@ const SIZES = {
MEDIUM: 'medium',
};
const Timer = React.memo(({ startedAt, total, size, isDisabled, onClick }) => {
const Timer = React.memo(({ as, startedAt, total, size, isDisabled, onClick }) => {
const prevStartedAt = usePrevious(startedAt);
const forceUpdate = useForceUpdate();
@@ -52,6 +52,7 @@ const Timer = React.memo(({ startedAt, total, size, isDisabled, onClick }) => {
className={classNames(
styles.wrapper,
styles[`wrapper${upperFirst(size)}`],
startedAt && styles.wrapperActive,
onClick && styles.wrapperHoverable,
)}
>
@@ -59,16 +60,19 @@ const Timer = React.memo(({ startedAt, total, size, isDisabled, onClick }) => {
</span>
);
const ElementType = as;
return onClick ? (
<button type="button" disabled={isDisabled} className={styles.button} onClick={onClick}>
<ElementType type="button" disabled={isDisabled} className={styles.button} onClick={onClick}>
{contentNode}
</button>
</ElementType>
) : (
contentNode
);
});
Timer.propTypes = {
as: PropTypes.elementType,
startedAt: PropTypes.instanceOf(Date),
total: PropTypes.number.isRequired, // eslint-disable-line react/no-unused-prop-types
size: PropTypes.oneOf(Object.values(SIZES)),
@@ -77,6 +81,7 @@ Timer.propTypes = {
};
Timer.defaultProps = {
as: 'button',
startedAt: undefined,
size: SIZES.MEDIUM,
isDisabled: false,
+13 -3
View File
@@ -9,17 +9,28 @@
}
.wrapper {
background: #f3f4f6;
background: #dce0e4;
border: none;
border-radius: 3px;
color: #7e8ea3;
color: #6a808b;
display: inline-block;
font-variant-numeric: tabular-nums;
outline: none;
text-align: left;
transition: background 0.3s ease;
vertical-align: top;
}
.wrapperActive {
background: #21ba45;
color: #fff;
&.wrapperHoverable:hover {
background: #16ab39;
color: #fff;
}
}
.wrapperHoverable:hover {
background: #d2d8dc;
color: #17394d;
@@ -42,6 +53,5 @@
.wrapperMedium {
line-height: 20px;
padding: 6px 12px;
text-decoration: underline;
}
}
@@ -0,0 +1,19 @@
import React from 'react';
// import { useTranslation } from 'react-i18next';
import { Image, Tab } from 'semantic-ui-react';
import logo from '../../assets/images/logo.png';
import styles from './AboutPane.module.scss';
const AboutPane = React.memo(() => {
// const [t] = useTranslation();
return (
<Tab.Pane attached={false} className={styles.wrapper}>
<Image centered src={logo} size="large" className={styles.logo} />
</Tab.Pane>
);
});
export default AboutPane;
@@ -0,0 +1,10 @@
:global(#app) {
/* .logo {
margin-bottom: 16px;
} */
.wrapper {
border: none;
box-shadow: none;
}
}
@@ -5,6 +5,7 @@ import { Modal, Tab } from 'semantic-ui-react';
import AccountPane from './AccountPane';
import PreferencesPane from './PreferencesPane';
import AboutPane from './AboutPane';
const UserSettingsModal = React.memo(
({
@@ -71,6 +72,10 @@ const UserSettingsModal = React.memo(
<PreferencesPane subscribeToOwnCards={subscribeToOwnCards} onUpdate={onUpdate} />
),
},
{
menuItem: 'About Planka',
render: () => <AboutPane />,
},
];
return (
+8 -1
View File
@@ -1,6 +1,11 @@
const { BASE_URL } = window;
const BASE_PATH = BASE_URL.replace(/^.*\/\/[^/]*(.*)[^?#]*.*$/, '$1');
const SERVER_BASE_URL =
process.env.REACT_APP_SERVER_BASE_URL ||
(process.env.NODE_ENV === 'production' ? '' : 'http://localhost:1337');
(process.env.NODE_ENV === 'production' ? BASE_URL : 'http://localhost:1337');
const SERVER_HOST_NAME = SERVER_BASE_URL.replace(/^(.*\/\/[^/?#]*).*$/, '$1');
const ACCESS_TOKEN_KEY = 'accessToken';
const ACCESS_TOKEN_VERSION_KEY = 'accessTokenVersion';
@@ -10,7 +15,9 @@ const POSITION_GAP = 65535;
const ACTIVITIES_LIMIT = 50;
export default {
BASE_PATH,
SERVER_BASE_URL,
SERVER_HOST_NAME,
ACCESS_TOKEN_KEY,
ACCESS_TOKEN_VERSION_KEY,
ACCESS_TOKEN_VERSION,
+7 -5
View File
@@ -1,8 +1,10 @@
const ROOT = '/';
const LOGIN = '/login';
const PROJECTS = '/projects/:id';
const BOARDS = '/boards/:id';
const CARDS = '/cards/:id';
import Config from './Config';
const ROOT = `${Config.BASE_PATH}/`;
const LOGIN = `${Config.BASE_PATH}/login`;
const PROJECTS = `${Config.BASE_PATH}/projects/:id`;
const BOARDS = `${Config.BASE_PATH}/boards/:id`;
const CARDS = `${Config.BASE_PATH}/cards/:id`;
export default {
ROOT,
@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import selectors from '../selectors';
import entryActions from '../entry-actions';
import { BoardMembershipRoles } from '../constants/Enums';
import BoardActions from '../components/BoardActions';
const mapStateToProps = (state) => {
@@ -12,6 +13,7 @@ const mapStateToProps = (state) => {
const labels = selectors.selectLabelsForCurrentBoard(state);
const filterUsers = selectors.selectFilterUsersForCurrentBoard(state);
const filterLabels = selectors.selectFilterLabelsForCurrentBoard(state);
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
return {
memberships,
@@ -19,6 +21,7 @@ const mapStateToProps = (state) => {
filterUsers,
filterLabels,
allUsers,
canEdit: !!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR,
canEditMemberships: isCurrentUserManager,
};
};
+34 -4
View File
@@ -1,4 +1,5 @@
import React, { useCallback, useState } from 'react';
import { ResizeObserver } from '@juggle/resize-observer';
import React, { useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react';
@@ -8,6 +9,9 @@ export default (WrappedComponent, defaultProps) => {
const Popup = React.memo(({ children, ...props }) => {
const [isOpened, setIsOpened] = useState(false);
const wrapper = useRef(null);
const resizeObserver = useRef(null);
const handleOpen = useCallback(() => {
setIsOpened(true);
}, []);
@@ -37,6 +41,29 @@ export default (WrappedComponent, defaultProps) => {
[children],
);
const handleContentRef = useCallback((element) => {
if (resizeObserver.current) {
resizeObserver.current.disconnect();
}
if (!element) {
resizeObserver.current = null;
return;
}
resizeObserver.current = new ResizeObserver(() => {
if (resizeObserver.current.isInitial) {
resizeObserver.current.isInitial = false;
return;
}
wrapper.current.positionUpdate();
});
resizeObserver.current.isInitial = true;
resizeObserver.current.observe(element);
}, []);
const tigger = React.cloneElement(children, {
onClick: handleTriggerClick,
});
@@ -45,6 +72,7 @@ export default (WrappedComponent, defaultProps) => {
<SemanticUIPopup
basic
wide
ref={wrapper}
trigger={tigger}
on="click"
open={isOpened}
@@ -64,9 +92,11 @@ export default (WrappedComponent, defaultProps) => {
onClick={handleClick}
{...defaultProps} // eslint-disable-line react/jsx-props-no-spreading
>
<Button icon="close" onClick={handleClose} className={styles.closeButton} />
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<WrappedComponent {...props} onClose={handleClose} />
<div ref={handleContentRef}>
<Button icon="close" onClick={handleClose} className={styles.closeButton} />
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<WrappedComponent {...props} onClose={handleClose} />
</div>
</SemanticUIPopup>
);
});
+17 -16
View File
@@ -4,11 +4,11 @@ export default {
dateFns,
format: {
date: 'd/M/yyyy',
date: 'd.MM.yyyy',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMM d',
longDateTime: "d MMMM 'at' p",
longDate: 'd. MMM',
longDateTime: "d. MMMM 'um' p",
},
translation: {
@@ -23,7 +23,7 @@ export default {
administrator: 'Administrator',
all: 'Alle',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Alle Änderungen werden automatisch gespeichert,<br />sobald die Verbindung wiederhergestellt wurde',
'Alle Änderungen werden automatisch gespeichert, sobald die Verbindung wiederhergestellt wurde.',
areYouSureYouWantToDeleteThisAttachment:
'Sind Sie sicher, dass Sie diesen Anhang löschen möchten?',
areYouSureYouWantToDeleteThisBoard: 'Sind Sie sicher, dass Sie dieses Board löschen möchten?',
@@ -55,10 +55,11 @@ export default {
color: 'Farbe',
createBoard_title: 'Board erstellen',
createLabel_title: 'Label erstellen',
createNewOneOrSelectExistingOne: 'Ein Neues erstellen, oder <br />ein Bestehendes auswählen',
createNewOneOrSelectExistingOne:
'Erstellen Sie ein neues oder wählen Sie ein bestehendes aus.',
createProject_title: 'Projekt erstellen',
createTextFile_title: 'Textdatei erstellen',
currentPassword: 'Derzeitiges Password',
currentPassword: 'Derzeitiges Passwort',
dangerZone_title: 'Gefährlicher Bereich',
date: 'Datum',
dueDate_title: 'Fälligkeitsdatum',
@@ -73,18 +74,18 @@ export default {
deleteUser_title: 'Benutzer löschen',
description: 'Beschreibung',
detectAutomatically: 'Automatische Erkennung',
dropFileToUpload: 'Datei ablegen, um hochzuladen',
dropFileToUpload: 'Datei ablegen, um sie hochzuladen',
editAttachment_title: 'Anhang bearbieten',
editAvatar_title: 'Avatar bearbeiten',
editBoard_title: 'Board bearbeiten',
editDueDate_title: 'Fälligkeitsdatum bearbeiten',
editEmail_title: 'E-Mail bearbeiten',
editEmail_title: 'E-Mail-Adresse bearbeiten',
editLabel_title: 'Label bearbeiten',
editPassword_title: 'Passwort ändern',
editTimer_title: 'Timer bearbeiten',
editUsername_title: 'Benutzername ändern',
email: 'E-Mail',
emailAlreadyInUse: 'E-Mail bereits in Verwendung',
email: 'E-Mail-Adresse',
emailAlreadyInUse: 'E-Mail-Adresse bereits in Verwendung',
enterCardTitle: 'Kartentitel eingeben...',
enterDescription: 'Beschreibung eingeben...',
enterFilename: 'Dateiname eingeben',
@@ -108,7 +109,7 @@ export default {
minutes: 'Minuten',
moveCard_title: 'Karte verschieben',
name: 'Name',
newEmail: 'Neue E-Mail',
newEmail: 'Neue E-Mail-Adresse',
newPassword: 'Neues Passwort',
newUsername: 'Neuer Benutzername',
noConnectionToServer: 'Keine Verbindung zum Server',
@@ -140,10 +141,10 @@ export default {
time: 'Zeit',
timer: 'Timer',
title: 'Titel',
userActions_title: 'Benutzerraktionen',
userActions_title: 'Benutzeraktionen',
userAddedThisCardToList: '<0>{{user}}</0><1> hat diese Karte hinzugefügt zu {{list}}</1>',
userLeftNewCommentToCard:
'{{user}} hat einen neuen Kommentar verfasst «{{comment}}» to <2>{{card}}</2>',
'{{user}} hat einen neuen Kommentar verfasst: «{{comment}}» in <2>{{card}}</2>',
userMovedCardFromListToList:
'{{user}} bewegte <2>{{card}}</2> von {{fromList}} nach {{toList}}',
userMovedThisCardFromListToList:
@@ -190,7 +191,7 @@ export default {
edit: 'Bearbeiten',
editDueDate_title: 'Fälligkeitsdatum bearbeiten',
editDescription_title: 'Beschreibung ändern',
editEmail_title: 'E-Mail bearbeiten',
editEmail_title: 'E-Mail-Adresse bearbeiten',
editPassword_title: 'Passwort ändern',
editTimer_title: 'Timer bearbeiten',
editTitle_title: 'Titel bearbeiten',
@@ -199,7 +200,7 @@ export default {
leaveBoard: 'Board verlassen',
leaveProject: 'Projekt verlassen',
logOut_title: 'Ausloggen',
makeCover_title: 'als Vorschau festlegen',
makeCover_title: 'Als Vorschau festlegen',
move: 'Verschieben',
moveCard_title: 'Karte bewegen',
remove: 'Löschen',
@@ -216,7 +217,7 @@ export default {
start: 'Start',
stop: 'Stopp',
subscribe: 'Abonnieren',
unsubscribe: 'Deabonnieren',
unsubscribe: 'De-abonnieren',
uploadNewAvatar: 'Neuen Avatar hochladen',
uploadNewImage: 'Neues Bild hochladen',
},
+3 -3
View File
@@ -1,14 +1,14 @@
export default {
translation: {
common: {
emailOrUsername: 'E-mail oder Benutzername',
invalidEmailOrUsername: 'Ungültige E-mail oder Benutzername',
emailOrUsername: 'E-Mail-Adresse oder Benutzername',
invalidEmailOrUsername: 'Ungültige E-Mail-Adresse oder Benutzername',
invalidPassword: 'Ungültiges Passwort',
logInToPlanka: 'Einloggen',
noInternetConnection: 'Keine Internetverbindung',
pageNotFound_title: 'Seite nicht gefunden',
password: 'Passwort',
projectManagement: 'Projekt Management',
projectManagement: 'Projekt-Management',
serverConnectionFailed: 'Serververbindung fehlgeschlagen',
unknownError: 'Unbekannter Fehler, bitte später erneut versuchen',
},
+1 -1
View File
@@ -5,7 +5,7 @@ import core from './core';
export default {
language: 'en',
country: 'us',
country: 'gb',
name: 'English',
embeddedLocale: merge(login, core),
};
+1 -1
View File
@@ -27,7 +27,7 @@ services:
- postgres
postgres:
image: postgres:alpine
image: postgres:14-alpine
restart: unless-stopped
volumes:
- db-data:/var/lib/postgresql/data
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "planka",
"version": "1.8.0",
"version": "1.8.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "planka",
"version": "1.8.0",
"version": "1.8.5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "planka",
"version": "1.8.0",
"version": "1.8.5",
"private": true,
"homepage": "https://plankanban.github.io/planka",
"repository": {
+11 -11
View File
@@ -7,7 +7,7 @@ module.exports = {
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({
id: membershipBoardIds,
@@ -21,16 +21,6 @@ module.exports = {
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 projects = [...managerProjects, ...membershipProjects];
@@ -40,7 +30,17 @@ module.exports = {
const users = await sails.helpers.users.getMany(userIds);
const managerBoards = await sails.helpers.projects.getBoards(managerProjectIds);
membershipBoards = membershipBoards.filter((membershipBoard) =>
membershipProjectIds.includes(membershipBoard.projectId),
);
const boards = [...managerBoards, ...membershipBoards];
const boardIds = sails.helpers.utils.mapRecords(boards);
boardMemberships = boardMemberships.filter((boardMembership) =>
boardIds.includes(boardMembership.boardId),
);
return {
items: projects,
@@ -1,3 +1,5 @@
const { v4: uuid } = require('uuid');
module.exports = {
inputs: {
record: {
@@ -29,18 +31,9 @@ module.exports = {
const boardMembership = await BoardMembership.destroyOne(inputs.record.id);
if (boardMembership) {
sails.sockets.broadcast(
`user:${boardMembership.userId}`,
'boardMembershipDelete',
{
item: boardMembership,
},
inputs.request,
);
const notifyBoard = () => {
const notify = (room) => {
sails.sockets.broadcast(
`board:${boardMembership.boardId}`,
room,
'boardMembershipDelete',
{
item: boardMembership,
@@ -54,16 +47,32 @@ module.exports = {
inputs.project.id,
);
if (isProjectManager) {
notifyBoard();
} else {
// TODO: also remove if unsubscribed to user
if (!isProjectManager) {
sails.sockets.removeRoomMembersFromRooms(
`user:${boardMembership.userId}`,
`@user:${boardMembership.userId}`,
`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;
+16 -9
View File
@@ -17,16 +17,23 @@ module.exports = {
const board = await Board.archiveOne(inputs.record.id);
if (board) {
sails.sockets.leaveAll(`board:${board.id}`);
sails.sockets.removeRoomMembersFromRooms(`board:${board.id}`, `board:${board.id}`);
sails.sockets.broadcast(
`project:${board.projectId}`,
'boardDelete',
{
item: board,
},
inputs.request,
);
const managerUserIds = await sails.helpers.projects.getManagerUserIds(board.projectId);
const memberUserIds = await sails.helpers.boards.getMemberUserIds(board.id);
const userIds = _.union(managerUserIds, memberUserIds);
userIds.forEach((userId) => {
sails.sockets.broadcast(
`user:${userId}`,
'boardDelete',
{
item: board,
},
inputs.request,
);
});
}
return board;
+4 -3
View File
@@ -25,9 +25,10 @@ module.exports = {
},
async fn(inputs) {
const userIds = await sails.helpers.projects.getManagerAndBoardMemberUserIds(
inputs.record.projectId,
);
const managerUserIds = await sails.helpers.projects.getManagerUserIds(inputs.record.projectId);
const memberUserIds = await sails.helpers.boards.getMemberUserIds(inputs.record.id);
const userIds = _.union(managerUserIds, memberUserIds);
if (!_.isUndefined(inputs.values.position)) {
const boards = await sails.helpers.projects.getBoards(
+1 -1
View File
@@ -26,7 +26,7 @@ module.exports = {
const userIds = _.union(managerUserIds, memberUserIds);
userIds.forEach((userId) => {
sails.sockets.removeRoomMembersFromRooms(`user:${userId}`, boardRooms);
sails.sockets.removeRoomMembersFromRooms(`@user:${userId}`, boardRooms);
sails.sockets.broadcast(
`user:${userId}`,
+2 -2
View File
@@ -123,13 +123,13 @@ module.exports = {
if (user.id === inputs.user.id && inputs.request && inputs.request.isSocket) {
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.leaveAll(tempRoom);
});
});
} else {
sails.sockets.leaveAll(`user:${user.id}`);
sails.sockets.leaveAll(`@user:${user.id}`);
}
}
+4
View File
@@ -59,6 +59,10 @@ module.exports = function defineCurrentUserHook(sails) {
accessToken,
currentUser,
});
if (req.isSocket) {
sails.sockets.join(req, `@user:${currentUser.id}`);
}
}
}
+2 -1
View File
@@ -24,7 +24,7 @@ module.exports.views = {
*
*/
extension: 'html',
extension: 'ejs',
/**
*
@@ -36,4 +36,5 @@ module.exports.views = {
*/
layout: false,
locals: { BASE_URL: process.env.BASE_URL },
};