Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e28c06b85a | ||
|
|
fa2d73a2c5 | ||
|
|
80706bbdd5 | ||
|
|
db4e5a89f9 | ||
|
|
0266e4bfe2 | ||
|
|
02c26cfb4d | ||
|
|
ef096499a2 | ||
|
|
a007e1d065 | ||
|
|
c181731870 | ||
|
|
54c57a08c3 | ||
|
|
65abd7c9ab | ||
|
|
5fc811d2ef | ||
|
|
5afd4af897 | ||
|
|
ac0a3d9ae7 | ||
|
|
b0d23373c0 | ||
|
|
5381d45e50 | ||
|
|
a43af8eac5 | ||
|
|
409a7cde6b | ||
|
|
f62cba9bcd | ||
|
|
9495234066 | ||
|
|
5dcbad7b5a | ||
|
|
064b37050c | ||
|
|
bf8b8474ec | ||
|
|
37a9c9bb42 | ||
|
|
00fccbaa3d | ||
|
|
b3323308c1 | ||
|
|
9108dc5b3e | ||
|
|
513f9a8ee5 | ||
|
|
e051f624a8 | ||
|
|
172f4fcf34 | ||
|
|
9f16881965 | ||
|
|
4e876922f8 | ||
|
|
d82d928e29 | ||
|
|
94dc83706c | ||
|
|
b08e731419 | ||
|
|
1ce0fac281 | ||
|
|
e3d00c8791 | ||
|
|
e1a0b635ea | ||
|
|
cc7959bb15 | ||
|
|
56044e6d8a | ||
|
|
e5bd73f33e | ||
|
|
e65ab6ece7 | ||
|
|
14cc55a8c0 | ||
|
|
b4306d0a4b | ||
|
|
283c850ebf | ||
|
|
62de71bd97 | ||
|
|
447f88880d | ||
|
|
8109936ce2 | ||
|
|
640908320a | ||
|
|
236c1020b3 | ||
|
|
3df07c10fa | ||
|
|
543a992d98 | ||
|
|
9cde4e63f9 | ||
|
|
95738ee7d3 | ||
|
|
bb7cdf5dd4 | ||
|
|
253af55790 | ||
|
|
62c3cca9a7 | ||
|
|
b476a19b79 | ||
|
|
3379c65a87 | ||
|
|
1a6fc40804 | ||
|
|
42092efcd2 | ||
|
|
5c91bddfe7 | ||
|
|
fd8f40f1b0 | ||
|
|
5b64d465e2 | ||
|
|
212fb8ac8c | ||
|
|
cece2254d7 | ||
|
|
c940805f14 | ||
|
|
052ab7f653 | ||
|
|
5a38805588 | ||
|
|
2dbbead2e9 | ||
|
|
039eee0aac | ||
|
|
07867fc0b2 | ||
|
|
55d07b4560 | ||
|
|
289d2bf5ae | ||
|
|
6429e22d59 | ||
|
|
21ef850913 |
+23
-4
@@ -1,10 +1,29 @@
|
|||||||
*/node_modules
|
|
||||||
*/.gitignore
|
|
||||||
*/README.md
|
*/README.md
|
||||||
|
*/.gitignore
|
||||||
|
*/node_modules
|
||||||
|
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
**/.gitkeep
|
|
||||||
|
server/**/.gitkeep
|
||||||
|
|
||||||
|
server/.env
|
||||||
|
server/.editorconfig
|
||||||
|
server/.eslintignore
|
||||||
|
server/.npmrc
|
||||||
|
server/test
|
||||||
|
server/logs
|
||||||
|
server/.tmp
|
||||||
|
|
||||||
|
server/views/*
|
||||||
|
|
||||||
server/public/*
|
server/public/*
|
||||||
server/views/*
|
!server/public/user-avatars
|
||||||
|
server/public/user-avatars/*
|
||||||
|
!server/public/project-background-images
|
||||||
|
server/public/project-background-images/*
|
||||||
|
|
||||||
|
server/private/*
|
||||||
|
!server/private/attachments
|
||||||
|
server/private/attachments/*
|
||||||
|
|
||||||
client/build
|
client/build
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
ALPINE_VERSION: 3.15
|
ALPINE_VERSION: 3.16
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push-docker-base-image:
|
build-and-push-docker-base-image:
|
||||||
|
|||||||
@@ -3,3 +3,10 @@ docker-compose.override.yml
|
|||||||
|
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Prevent another lockfile than package-lock.json (npm) from being created
|
||||||
|
# If some case you are using pnpm or yarn, don't forget to generate npm lockfile
|
||||||
|
# before commiting your code by running:
|
||||||
|
# `npm i --package-lock-only`
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
|||||||
+22
-14
@@ -1,32 +1,40 @@
|
|||||||
FROM node:lts AS client-builder
|
FROM ghcr.io/plankanban/planka:base-latest as server-dependencies
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY client/package.json client/package-lock.json ./
|
COPY server/package.json server/package-lock.json .
|
||||||
|
|
||||||
RUN npm install npm@latest --global \
|
RUN npm install npm@latest --global \
|
||||||
&& npm install
|
&& npm clean-install --omit=dev
|
||||||
|
|
||||||
|
FROM node:lts AS client
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY client/package.json client/package-lock.json .
|
||||||
|
|
||||||
|
RUN npm install npm@latest --global \
|
||||||
|
&& npm clean-install --omit=dev
|
||||||
|
|
||||||
COPY client .
|
COPY client .
|
||||||
RUN npm run build
|
RUN DISABLE_ESLINT_PLUGIN=true npm run build
|
||||||
|
|
||||||
FROM ghcr.io/plankanban/planka:base-latest
|
FROM ghcr.io/plankanban/planka:base-latest
|
||||||
|
|
||||||
|
RUN apk del vips-dependencies --purge
|
||||||
|
|
||||||
|
USER node
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY server/.npmrc server/package.json server/package-lock.json ./
|
COPY --chown=node:node docker-start.sh start.sh
|
||||||
|
COPY --chown=node:node server .
|
||||||
|
|
||||||
RUN npm install npm@latest --global \
|
RUN mv .env.sample .env
|
||||||
&& npm install --production \
|
|
||||||
&& apk del vips-dependencies --purge
|
|
||||||
|
|
||||||
COPY docker-start.sh start.sh
|
COPY --from=server-dependencies --chown=node:node /app/node_modules node_modules
|
||||||
COPY server .
|
|
||||||
|
|
||||||
RUN cp .env.sample .env
|
COPY --from=client --chown=node:node /app/build public
|
||||||
|
COPY --from=client --chown=node:node /app/build/index.html views/index.ejs
|
||||||
COPY --from=client-builder /app/build public
|
|
||||||
COPY --from=client-builder /app/build/index.html views
|
|
||||||
|
|
||||||
VOLUME /app/public/user-avatars
|
VOLUME /app/public/user-avatars
|
||||||
VOLUME /app/public/project-background-images
|
VOLUME /app/public/project-background-images
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
FROM node:lts-alpine
|
FROM node:lts-alpine
|
||||||
|
|
||||||
ARG ALPINE_VERSION=3.15
|
ARG ALPINE_VERSION=3.16
|
||||||
ARG VIPS_VERSION=8.12.2
|
ARG VIPS_VERSION=8.13.3
|
||||||
|
|
||||||
RUN apk -U upgrade \
|
RUN apk -U upgrade \
|
||||||
&& apk add \
|
&& apk add \
|
||||||
|
|||||||
@@ -21,9 +21,14 @@ 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)
|
||||||
|
|
||||||
[](https://hub.docker.com/r/meltyshev/planka)
|
|
||||||
|
### 1. Docker Compose
|
||||||
|
|
||||||
|
[](https://github.com/plankanban/planka/pkgs/container/planka)
|
||||||
|
|
||||||
- Make sure you have [Docker](https://docs.docker.com/install/) and [Docker Compose](https://docs.docker.com/compose/install/) installed and operational.
|
- Make sure you have [Docker](https://docs.docker.com/install/) and [Docker Compose](https://docs.docker.com/compose/install/) installed and operational.
|
||||||
- Create `docker-compose.yml` based on [the example](https://raw.githubusercontent.com/plankanban/planka/master/docker-compose.yml). This is the ONLY file you will need. You can create this file on your own machine by copy and pasting the content.
|
- Create `docker-compose.yml` based on [the example](https://raw.githubusercontent.com/plankanban/planka/master/docker-compose.yml). This is the ONLY file you will need. You can create this file on your own machine by copy and pasting the content.
|
||||||
@@ -44,6 +49,220 @@ 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
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
Note that the directory to the left of the semicolon is regarding the host machine while the directory to the right of the semicolon is regarding the Docker container.
|
||||||
|
|
||||||
|
For example, in the above step, `./logs/:/app/logs/` will create the folder `logs` in the same directory where the `docker-compose.yml` file lives.
|
||||||
|
|
||||||
|
### Rotating Logs
|
||||||
|
|
||||||
|
Logrotate is designed to ease administration of systems that generate large numbers of log files. It allows automatic rotation, compression, removal, and mailing of log files. Each log file may be handled daily, weekly, monthly, or when it grows too large.
|
||||||
|
|
||||||
|
#### Setup logrotate for Planka logs
|
||||||
|
|
||||||
|
Create a file in `/etc/logrotate.d` named `planka` with the following contents:
|
||||||
|
|
||||||
|
```
|
||||||
|
/path/to/planka/logs/planka.log {
|
||||||
|
daily
|
||||||
|
missingok
|
||||||
|
rotate 14
|
||||||
|
compress
|
||||||
|
delaycompress
|
||||||
|
notifempty
|
||||||
|
create 640 root adm
|
||||||
|
sharedscripts
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure to replace logfile directory with your installation’s `/logs/planka.log` location.
|
||||||
|
|
||||||
|
Restart the logrotate service.
|
||||||
|
|
||||||
|
### Fail2ban
|
||||||
|
|
||||||
|
Fail2ban is a service that uses iptables to automatically drop connections for a pre-defined amount of time from IPs that continuously failed to authenticate to the configured services.
|
||||||
|
|
||||||
|
#### Setup a filter and a jail for Planka
|
||||||
|
|
||||||
|
A filter defines regex rules to identify when users fail to authenticate on Planka's user interface.
|
||||||
|
|
||||||
|
Create a file in `/etc/fail2ban/filter.d` named `planka.conf` with the following contents:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
[Definition]
|
||||||
|
failregex = ^(.*) Invalid (email or username:|password!) (\"(.*)\"!)? ?\(IP: <ADDR>\)$
|
||||||
|
ignoreregex =
|
||||||
|
```
|
||||||
|
|
||||||
|
The jail file defines how to handle the failed authentication attempts found by the Planka filter.
|
||||||
|
|
||||||
|
Create a file in `/etc/fail2ban/jail.d` named `planka.local` with the following contents:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
[planka]
|
||||||
|
enabled = true
|
||||||
|
port = http,https
|
||||||
|
filter = planka
|
||||||
|
logpath = /path/to/planka/logs/planka.log
|
||||||
|
maxretry = 5
|
||||||
|
bantime = 900
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure to replace `logpath`'s value with your installation’s `/logs/planka.log` location. If you are using ports other than 80 and 443 for your Web server you should replace those too. The bantime and findtime are defined in seconds.
|
||||||
|
|
||||||
|
Restart the fail2ban service. You can check the status of your Planka jail by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fail2ban-client status planka
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import from Trello
|
||||||
|
|
||||||
|
It is not yet implemented inside Planka, but for now you can use the great tool [trello2planka](https://github.com/christophenne/trello2planka) to do the import.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Clone the repository and install dependencies:
|
Clone the repository and install dependencies:
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const BASE_URL_PLACEHOLDER = 'BASE_URL_PLACEHOLDER';
|
||||||
|
|
||||||
|
const replaceInFile = (file, search, replace) => {
|
||||||
|
fs.readFile(file, 'utf8', (readError, data) => {
|
||||||
|
if (readError) {
|
||||||
|
throw new Error(`${readError}`);
|
||||||
|
}
|
||||||
|
const res = data.replaceAll(search, replace);
|
||||||
|
fs.writeFile(file, res, 'utf8', (writeError) => {
|
||||||
|
if (writeError) {
|
||||||
|
throw new Error(`${writeError}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaceBaseUrl = (compiler) => {
|
||||||
|
compiler.hooks.assetEmitted.tap('ReplaceBaseUrlPlaceholder', (file, info) => {
|
||||||
|
if (info.content.indexOf(BASE_URL_PLACEHOLDER) >= 0) {
|
||||||
|
if (/\.css$/.exec(info.targetPath)) {
|
||||||
|
// For CSS 'url(...)' import we can use relative import
|
||||||
|
const relPath = path.relative(path.dirname(info.targetPath), info.outputPath);
|
||||||
|
replaceInFile(info.targetPath, BASE_URL_PLACEHOLDER, `${relPath}/`);
|
||||||
|
} else if (/\.js$/.exec(info.targetPath)) {
|
||||||
|
// For JS 'import ... from "some-asset"' we can get the variable injected in the window object
|
||||||
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
|
replaceInFile(info.targetPath, `"${BASE_URL_PLACEHOLDER}"`, '`${window.BASE_URL}/`');
|
||||||
|
} else if (/index\.html$/.exec(info.targetPath)) {
|
||||||
|
// For the main html file, we set a placeholder for sails to inject the correct value as runtime
|
||||||
|
replaceInFile(info.targetPath, BASE_URL_PLACEHOLDER, '<%= BASE_URL %>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = function override(config, env) {
|
||||||
|
if (env === 'production') {
|
||||||
|
const plugins = config.plugins.map((plugin) => {
|
||||||
|
if (plugin.constructor.name === 'InterpolateHtmlPlugin') {
|
||||||
|
const newPlugin = plugin;
|
||||||
|
newPlugin.replacements.PUBLIC_URL = BASE_URL_PLACEHOLDER;
|
||||||
|
return newPlugin;
|
||||||
|
}
|
||||||
|
return plugin;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
output: { ...config.output, publicPath: BASE_URL_PLACEHOLDER },
|
||||||
|
plugins: [...plugins, { apply: replaceBaseUrl }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
};
|
||||||
Generated
+11421
-14566
File diff suppressed because it is too large
Load Diff
+40
-47
@@ -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": [
|
||||||
@@ -34,15 +34,13 @@
|
|||||||
},
|
},
|
||||||
"requireConfigFile": false
|
"requireConfigFile": false
|
||||||
},
|
},
|
||||||
"plugins": [
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"extends": [
|
"extends": [
|
||||||
"airbnb",
|
"airbnb",
|
||||||
"airbnb/hooks",
|
"airbnb/hooks",
|
||||||
"plugin:prettier/recommended"
|
"../package.json"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"no-unused-vars": "warn",
|
||||||
"import/no-extraneous-dependencies": [
|
"import/no-extraneous-dependencies": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
@@ -51,73 +49,68 @@
|
|||||||
"**/*.test.js"
|
"**/*.test.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"prettier/prettier": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"printWidth": 100,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "all"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"classnames": "^2.3.1",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"connected-react-router": "^6.9.3",
|
"classnames": "^2.3.2",
|
||||||
"date-fns": "^2.29.1",
|
"date-fns": "^2.29.3",
|
||||||
"dequal": "^2.0.3",
|
"dequal": "^2.0.3",
|
||||||
"history": "^4.10.1",
|
"easymde": "^2.18.0",
|
||||||
"i18next": "^21.8.14",
|
"history": "^5.3.0",
|
||||||
"i18next-browser-languagedetector": "^6.1.4",
|
"i18next": "^22.0.6",
|
||||||
|
"i18next-browser-languagedetector": "^7.0.1",
|
||||||
"initials": "^3.1.2",
|
"initials": "^3.1.2",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"node-sass": "^7.0.1",
|
"node-sass": "^8.0.0",
|
||||||
"photoswipe": "^5.3.0",
|
"photoswipe": "^5.3.3",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^17.0.2",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-app-rewired": "^2.2.1",
|
||||||
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-datepicker": "^4.8.0",
|
"react-datepicker": "^4.8.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.2",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-i18next": "^11.18.1",
|
"react-i18next": "^12.0.0",
|
||||||
"react-input-mask": "^2.0.4",
|
"react-input-mask": "^2.0.4",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-photoswipe-gallery": "^2.2.1",
|
"react-photoswipe-gallery": "^2.2.2",
|
||||||
"react-redux": "^7.2.8",
|
"react-redux": "^8.0.5",
|
||||||
"react-router-dom": "^5.3.1",
|
"react-router": "^6.4.3",
|
||||||
|
"react-router-dom": "^6.4.3",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-textarea-autosize": "^8.3.4",
|
"react-simplemde-editor": "^5.2.0",
|
||||||
|
"react-textarea-autosize": "^8.4.0",
|
||||||
"redux": "^4.2.0",
|
"redux": "^4.2.0",
|
||||||
|
"redux-first-history": "^5.1.1",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"redux-orm": "^0.16.2",
|
"redux-orm": "^0.16.2",
|
||||||
"redux-saga": "^1.1.3",
|
"redux-saga": "^1.2.1",
|
||||||
"remark-breaks": "^3.0.2",
|
"remark-breaks": "^3.0.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"reselect": "^4.1.6",
|
"reselect": "^4.1.7",
|
||||||
"sails.io.js": "^1.2.1",
|
"sails.io.js": "^1.2.1",
|
||||||
"semantic-ui-react": "^2.1.3",
|
"semantic-ui-react": "^2.1.3",
|
||||||
"socket.io-client": "^2.3.1",
|
"socket.io-client": "^2.5.0",
|
||||||
"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",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"babel-preset-airbnb": "^5.0.0",
|
"babel-preset-airbnb": "^5.0.0",
|
||||||
"chai": "^4.3.6",
|
"chai": "^4.3.7",
|
||||||
"enzyme": "^3.11.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint": "^8.20.0",
|
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-react": "^7.31.11",
|
||||||
"eslint-plugin-react": "^7.30.1",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"jest-enzyme": "^7.1.2",
|
"react-test-renderer": "^18.2.0"
|
||||||
"prettier": "2.7.1",
|
|
||||||
"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>
|
||||||
|
|||||||
@@ -12,18 +12,18 @@ import styles from './BoardMembershipsStep.module.scss';
|
|||||||
const BoardMembershipsStep = React.memo(
|
const BoardMembershipsStep = React.memo(
|
||||||
({ items, currentUserIds, title, onUserSelect, onUserDeselect, onBack }) => {
|
({ items, currentUserIds, title, onUserSelect, onUserDeselect, onBack }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [searchValue, handleSearchFieldChange] = useField('');
|
const [search, handleSearchChange] = useField('');
|
||||||
const search = useMemo(() => searchValue.trim().toLowerCase(), [searchValue]);
|
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
|
||||||
|
|
||||||
const filteredItems = useMemo(
|
const filteredItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
items.filter(
|
items.filter(
|
||||||
({ user }) =>
|
({ user }) =>
|
||||||
user.email.includes(search) ||
|
user.email.includes(cleanSearch) ||
|
||||||
user.name.toLowerCase().includes(search) ||
|
user.name.toLowerCase().includes(cleanSearch) ||
|
||||||
(user.username && user.username.includes(search)),
|
(user.username && user.username.includes(cleanSearch)),
|
||||||
),
|
),
|
||||||
[items, search],
|
[items, cleanSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchField = useRef(null);
|
const searchField = useRef(null);
|
||||||
@@ -48,15 +48,19 @@ const BoardMembershipsStep = React.memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup.Header onBack={onBack}>{t(title)}</Popup.Header>
|
<Popup.Header onBack={onBack}>
|
||||||
|
{t(title, {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</Popup.Header>
|
||||||
<Popup.Content>
|
<Popup.Content>
|
||||||
<Input
|
<Input
|
||||||
fluid
|
fluid
|
||||||
ref={searchField}
|
ref={searchField}
|
||||||
value={searchValue}
|
value={search}
|
||||||
placeholder={t('common.searchMembers')}
|
placeholder={t('common.searchMembers')}
|
||||||
icon="search"
|
icon="search"
|
||||||
onChange={handleSearchFieldChange}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
{filteredItems.length > 0 && (
|
{filteredItems.length > 0 && (
|
||||||
<Menu secondary vertical className={styles.menu}>
|
<Menu secondary vertical className={styles.menu}>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const CardMoveStep = React.memo(
|
|||||||
[selectedBoard, path.listId],
|
[selectedBoard, path.listId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBoardIdFieldChange = useCallback(
|
const handleBoardIdChange = useCallback(
|
||||||
(event, data) => {
|
(event, data) => {
|
||||||
if (selectedProject.boards.find((board) => board.id === data.value).isFetching === null) {
|
if (selectedProject.boards.find((board) => board.id === data.value).isFetching === null) {
|
||||||
onBoardFetch(data.value);
|
onBoardFetch(data.value);
|
||||||
@@ -102,7 +102,7 @@ const CardMoveStep = React.memo(
|
|||||||
}
|
}
|
||||||
disabled={selectedProject.boards.length === 0}
|
disabled={selectedProject.boards.length === 0}
|
||||||
className={styles.field}
|
className={styles.field}
|
||||||
onChange={handleBoardIdFieldChange}
|
onChange={handleBoardIdChange}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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:
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,11 +78,16 @@ const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup.Header>{t('common.notifications')}</Popup.Header>
|
<Popup.Header>
|
||||||
|
{t('common.notifications', {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</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
|
||||||
@@ -86,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,20 +18,31 @@ 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 [searchValue, handleSearchFieldChange] = useField('');
|
const [search, handleSearchChange] = useField('');
|
||||||
const search = useMemo(() => searchValue.trim().toLowerCase(), [searchValue]);
|
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
|
||||||
|
|
||||||
const filteredItems = useMemo(
|
const filteredItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
items.filter(
|
items.filter(
|
||||||
(label) =>
|
(label) =>
|
||||||
(label.name && label.name.toLowerCase().includes(search)) ||
|
(label.name && label.name.toLowerCase().includes(cleanSearch)) ||
|
||||||
label.color.includes(search),
|
label.color.includes(cleanSearch),
|
||||||
),
|
),
|
||||||
[items, search],
|
[items, cleanSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchField = useRef(null);
|
const searchField = useRef(null);
|
||||||
@@ -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);
|
||||||
|
|
||||||
@@ -109,15 +128,19 @@ const LabelsStep = React.memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup.Header onBack={onBack}>{t(title)}</Popup.Header>
|
<Popup.Header onBack={onBack}>
|
||||||
|
{t(title, {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</Popup.Header>
|
||||||
<Popup.Content>
|
<Popup.Content>
|
||||||
<Input
|
<Input
|
||||||
fluid
|
fluid
|
||||||
ref={searchField}
|
ref={searchField}
|
||||||
value={searchValue}
|
value={search}
|
||||||
placeholder={t('common.searchLabels')}
|
placeholder={t('common.searchLabels')}
|
||||||
icon="search"
|
icon="search"
|
||||||
onChange={handleSearchFieldChange}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
{filteredItems.length > 0 && (
|
{filteredItems.length > 0 && (
|
||||||
<div className={styles.items}>
|
<div className={styles.items}>
|
||||||
@@ -128,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)}
|
||||||
@@ -135,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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -153,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,
|
||||||
@@ -163,6 +190,7 @@ LabelsStep.propTypes = {
|
|||||||
|
|
||||||
LabelsStep.defaultProps = {
|
LabelsStep.defaultProps = {
|
||||||
title: 'common.labels',
|
title: 'common.labels',
|
||||||
|
canEdit: true,
|
||||||
onBack: undefined,
|
onBack: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,18 +17,18 @@ const AddStep = React.memo(
|
|||||||
({ users, currentUserIds, permissionsSelectStep, title, onCreate, onClose }) => {
|
({ users, currentUserIds, permissionsSelectStep, title, onCreate, onClose }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [step, openStep, handleBack] = useSteps();
|
const [step, openStep, handleBack] = useSteps();
|
||||||
const [searchValue, handleSearchFieldChange] = useField('');
|
const [search, handleSearchChange] = useField('');
|
||||||
const search = useMemo(() => searchValue.trim().toLowerCase(), [searchValue]);
|
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
|
||||||
|
|
||||||
const filteredUsers = useMemo(
|
const filteredUsers = useMemo(
|
||||||
() =>
|
() =>
|
||||||
users.filter(
|
users.filter(
|
||||||
(user) =>
|
(user) =>
|
||||||
user.email.includes(search) ||
|
user.email.includes(cleanSearch) ||
|
||||||
user.name.toLowerCase().includes(search) ||
|
user.name.toLowerCase().includes(cleanSearch) ||
|
||||||
(user.username && user.username.includes(search)),
|
(user.username && user.username.includes(cleanSearch)),
|
||||||
),
|
),
|
||||||
[users, search],
|
[users, cleanSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchField = useRef(null);
|
const searchField = useRef(null);
|
||||||
@@ -102,10 +102,10 @@ const AddStep = React.memo(
|
|||||||
<Input
|
<Input
|
||||||
fluid
|
fluid
|
||||||
ref={searchField}
|
ref={searchField}
|
||||||
value={searchValue}
|
value={search}
|
||||||
placeholder={t('common.searchUsers')}
|
placeholder={t('common.searchUsers')}
|
||||||
icon="search"
|
icon="search"
|
||||||
onChange={handleSearchFieldChange}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
{filteredUsers.length > 0 && (
|
{filteredUsers.length > 0 && (
|
||||||
<div className={styles.users}>
|
<div className={styles.users}>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
.menuItem {
|
.menuItem {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 0.28571429rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { Route, Switch } from 'react-router-dom';
|
import { Route, Routes } from 'react-router-dom';
|
||||||
import { ConnectedRouter } from 'connected-react-router';
|
import { HistoryRouter as Router } from 'redux-first-history/rr6';
|
||||||
|
|
||||||
import Paths from '../constants/Paths';
|
import Paths from '../constants/Paths';
|
||||||
import LoginContainer from '../containers/LoginContainer';
|
import LoginContainer from '../containers/LoginContainer';
|
||||||
@@ -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';
|
||||||
@@ -18,16 +19,16 @@ import '../styles.module.scss';
|
|||||||
function Root({ store, history }) {
|
function Root({ store, history }) {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ConnectedRouter history={history}>
|
<Router history={history}>
|
||||||
<Switch>
|
<Routes>
|
||||||
<Route exact path={Paths.LOGIN} component={LoginContainer} />
|
<Route path={Paths.LOGIN} element={<LoginContainer />} />
|
||||||
<Route exact path={Paths.ROOT} component={CoreWrapperContainer} />
|
<Route path={Paths.ROOT} element={<CoreWrapperContainer />} />
|
||||||
<Route exact path={Paths.PROJECTS} component={CoreWrapperContainer} />
|
<Route path={Paths.PROJECTS} element={<CoreWrapperContainer />} />
|
||||||
<Route exact path={Paths.BOARDS} component={CoreWrapperContainer} />
|
<Route path={Paths.BOARDS} element={<CoreWrapperContainer />} />
|
||||||
<Route exact path={Paths.CARDS} component={CoreWrapperContainer} />
|
<Route path={Paths.CARDS} element={<CoreWrapperContainer />} />
|
||||||
<Route path="*" component={NotFound} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Switch>
|
</Routes>
|
||||||
</ConnectedRouter>
|
</Router>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { push } from 'connected-react-router';
|
import { push } from 'redux-first-history';
|
||||||
import omit from 'lodash/omit';
|
import omit from 'lodash/omit';
|
||||||
|
|
||||||
import selectors from '../selectors';
|
import selectors from '../selectors';
|
||||||
|
|||||||
@@ -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,3 +0,0 @@
|
|||||||
import { createBrowserHistory } from 'history';
|
|
||||||
|
|
||||||
export default createBrowserHistory();
|
|
||||||
+4
-4
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
import store from './store';
|
import store, { history } from './store';
|
||||||
import history from './history';
|
|
||||||
import Root from './components/Root';
|
import Root from './components/Root';
|
||||||
|
|
||||||
import './i18n';
|
import './i18n';
|
||||||
|
|
||||||
ReactDOM.render(React.createElement(Root, { store, history }), document.getElementById('root'));
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(Root, { store, history }));
|
||||||
|
|||||||
@@ -1,22 +1,74 @@
|
|||||||
import React, { useCallback } from 'react';
|
import 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',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import de from './de';
|
|||||||
import en from './en';
|
import en from './en';
|
||||||
import es from './es';
|
import es from './es';
|
||||||
import fr from './fr';
|
import fr from './fr';
|
||||||
|
import it from './it';
|
||||||
import ja from './ja';
|
import ja from './ja';
|
||||||
import ko from './ko';
|
import ko from './ko';
|
||||||
import pl from './pl';
|
import pl from './pl';
|
||||||
@@ -13,7 +14,7 @@ import sv from './sv';
|
|||||||
import uz from './uz';
|
import uz from './uz';
|
||||||
import zh from './zh';
|
import zh from './zh';
|
||||||
|
|
||||||
const locales = [cs, da, de, en, es, fr, ja, ko, pl, ru, sk, sv, uz, zh];
|
const locales = [cs, da, de, en, es, fr, it, ja, ko, pl, ru, sk, sv, uz, zh];
|
||||||
|
|
||||||
export default locales;
|
export default locales;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
export default {
|
||||||
|
format: {
|
||||||
|
date: 'd/M/yyyy',
|
||||||
|
time: 'p',
|
||||||
|
dateTime: '$t(format:date) $t(format:time)',
|
||||||
|
longDate: 'MMM d',
|
||||||
|
longDateTime: "MMMM d 'at' p",
|
||||||
|
},
|
||||||
|
|
||||||
|
translation: {
|
||||||
|
common: {
|
||||||
|
account: 'Account',
|
||||||
|
actions: 'Azioni',
|
||||||
|
addAttachment_title: 'Aggiungi Allegato',
|
||||||
|
addComment: 'Aggiungi commento',
|
||||||
|
addManager_title: 'Aggiungi Manager',
|
||||||
|
addMember_title: 'Aggiungi Membro',
|
||||||
|
addUser_title: 'Aggiungi Utente',
|
||||||
|
administrator: 'Amministratore',
|
||||||
|
all: 'tutto',
|
||||||
|
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||||
|
'All changes will be automatically saved<br />after connection restored',
|
||||||
|
areYouSureYouWantToDeleteThisAttachment: 'Sei sicuro di voler eliminare questo allegato?',
|
||||||
|
areYouSureYouWantToDeleteThisBoard: 'Sei sicuro di voler eliminare questa bacheca?',
|
||||||
|
areYouSureYouWantToDeleteThisCard: 'Sei sicuro di voler eliminare questa card?',
|
||||||
|
areYouSureYouWantToDeleteThisComment: 'Sei sicuro di voler eliminare questo commento?',
|
||||||
|
areYouSureYouWantToDeleteThisLabel: 'Sei sicuro di voler eliminare questa etichetta?',
|
||||||
|
areYouSureYouWantToDeleteThisList: 'Sei sicuro di voler eliminare questo elenco?',
|
||||||
|
areYouSureYouWantToDeleteThisProject: 'Sei sicuro di voler eliminare questo progetto?',
|
||||||
|
areYouSureYouWantToDeleteThisTask: 'Sei sicuro di voler eliminare questo task?',
|
||||||
|
areYouSureYouWantToDeleteThisUser: 'Sei sicuro di voler eliminare questo utente?',
|
||||||
|
areYouSureYouWantToLeaveBoard: 'Sei sicuro di voler abbandonare la bacheca?',
|
||||||
|
areYouSureYouWantToLeaveProject: 'Sei sicuro di voler abbandonare il progetto?',
|
||||||
|
areYouSureYouWantToRemoveThisManagerFromProject:
|
||||||
|
'Sei sicuro di voler rimuovere questo amministratore dal progetto?',
|
||||||
|
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||||
|
'Sei sicuro di voler rimuovere questo membro dalla bacheca?',
|
||||||
|
attachment: 'Allegato',
|
||||||
|
attachments: 'Allegati',
|
||||||
|
authentication: 'Autenticazione',
|
||||||
|
background: 'Background',
|
||||||
|
board: 'Bacheca',
|
||||||
|
boardNotFound_title: 'Bacheca non trovata',
|
||||||
|
canComment: 'Può commentare',
|
||||||
|
canEditContentOfBoard: 'Può modificare il contenuto della bacheca',
|
||||||
|
canOnlyViewBoard: 'Può solo vedere la bacheca',
|
||||||
|
cardActions_title: 'Card Azioni',
|
||||||
|
cardNotFound_title: 'Card non trovata',
|
||||||
|
cardOrActionAreDeleted: "La card o l'azione vengono eliminate",
|
||||||
|
color: 'Colore',
|
||||||
|
createBoard_title: 'Crea Bacheca',
|
||||||
|
createLabel_title: 'Crea Etichetta',
|
||||||
|
createNewOneOrSelectExistingOne: 'Crea nuovo o seleziona <br /> esistente',
|
||||||
|
createProject_title: 'Crea Progetto',
|
||||||
|
createTextFile_title: 'Create File di Testo',
|
||||||
|
currentPassword: 'Password corrente',
|
||||||
|
dangerZone_title: 'Danger Zone',
|
||||||
|
date: 'Data',
|
||||||
|
dueDate_title: 'Data di scadenza',
|
||||||
|
deleteAttachment_title: 'Elimina Allegato',
|
||||||
|
deleteBoard_title: 'Elimina Bacheca',
|
||||||
|
deleteCard_title: 'Elimina card',
|
||||||
|
deleteComment_title: 'Elimina Commento',
|
||||||
|
deleteLabel_title: 'Elimina Etichetta',
|
||||||
|
deleteList_title: 'Elimina Elenco',
|
||||||
|
deleteProject_title: 'Elimina Progetto',
|
||||||
|
deleteTask_title: 'Elimina Task',
|
||||||
|
deleteUser_title: 'Elimina Utente',
|
||||||
|
description: 'Descrizione',
|
||||||
|
detectAutomatically: 'Rileva automaticamente',
|
||||||
|
dropFileToUpload: 'Trascina il file da caricare',
|
||||||
|
editor: 'Editor',
|
||||||
|
editAttachment_title: 'Modifica Allegato',
|
||||||
|
editAvatar_title: 'Modifica Avatar',
|
||||||
|
editBoard_title: 'Modifica Board',
|
||||||
|
editDueDate_title: 'Modifica Data di Scadenza',
|
||||||
|
editEmail_title: 'Modifica E-mail',
|
||||||
|
editInformation_title: 'Modifica Informazioni',
|
||||||
|
editLabel_title: 'Modifica Etichetta',
|
||||||
|
editPassword_title: 'Modifica Password',
|
||||||
|
editPermissions_title: 'Modifica Permessi',
|
||||||
|
editTimer_title: 'Modifica Timer',
|
||||||
|
editUsername_title: 'Modifica Username',
|
||||||
|
email: 'E-mail',
|
||||||
|
emailAlreadyInUse: 'E-mail già in uso',
|
||||||
|
enterCardTitle: 'Inserire titolo della card...',
|
||||||
|
enterDescription: 'Inserire descrizione...',
|
||||||
|
enterFilename: 'Inserire nome del file',
|
||||||
|
enterListTitle: 'Inserire titolo della lista...',
|
||||||
|
enterProjectTitle: 'Inserire titolo del progetto',
|
||||||
|
enterTaskDescription: 'Inserire descrizione del task...',
|
||||||
|
filterByLabels_title: 'Filtra per Etichetta',
|
||||||
|
filterByMembers_title: 'Filtra per Membro',
|
||||||
|
fromComputer_title: 'Dal Computer',
|
||||||
|
general: 'Generale',
|
||||||
|
hours: 'Ore',
|
||||||
|
invalidCurrentPassword: 'Password corrente non valida',
|
||||||
|
labels: 'Etichette',
|
||||||
|
language: 'Lingua',
|
||||||
|
leaveBoard_title: 'Lascia Bacheca',
|
||||||
|
leaveProject_title: 'Lascia Progetto',
|
||||||
|
list: 'Lista',
|
||||||
|
listActions_title: 'Lista delle Azioni',
|
||||||
|
managers: 'Managers',
|
||||||
|
members: 'Membri',
|
||||||
|
minutes: 'Minuti',
|
||||||
|
moveCard_title: 'Sposta card',
|
||||||
|
name: 'Nome',
|
||||||
|
newEmail: 'Nuova e-mail',
|
||||||
|
newPassword: 'Nuova password',
|
||||||
|
newUsername: 'Nuovo username',
|
||||||
|
noConnectionToServer: 'Nessuna connessione al server',
|
||||||
|
noBoards: 'No bacheche',
|
||||||
|
noLists: 'No liste',
|
||||||
|
noProjects: 'No progetti',
|
||||||
|
notifications: 'Notifiche',
|
||||||
|
noUnreadNotifications: 'Nessuna notifica non letta',
|
||||||
|
openBoard_title: 'Bacheca aperta',
|
||||||
|
optional_inline: 'opzionale',
|
||||||
|
organization: 'Organizazzione',
|
||||||
|
phone: 'Telefono',
|
||||||
|
preferences: 'Preferenze',
|
||||||
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
|
'Tip: prmi Ctrl-V (Cmd-V on Mac) per aggiungere un allegato dalla clipboard',
|
||||||
|
project: 'Progetto',
|
||||||
|
projectNotFound_title: 'Progetto non trovato',
|
||||||
|
removeManager_title: 'Rimuovi Manager',
|
||||||
|
removeMember_title: 'Rimuovi Membro',
|
||||||
|
searchLabels: 'Cerca etichette...',
|
||||||
|
searchMembers: 'Cerca membri...',
|
||||||
|
searchUsers: 'Cerca utenti...',
|
||||||
|
seconds: 'Secondi',
|
||||||
|
selectBoard: 'Seleziona bacheca',
|
||||||
|
selectList: 'Seleziona lista',
|
||||||
|
selectPermissions_title: 'Seleziona Permessi',
|
||||||
|
selectProject: 'Seleziona progetto',
|
||||||
|
settings: 'Impostazioni',
|
||||||
|
subscribeToMyOwnCardsByDefault: 'Abbonati alle mie card per impostazione predefinita',
|
||||||
|
taskActions_title: 'Task Azioni',
|
||||||
|
tasks: 'Tasks',
|
||||||
|
thereIsNoPreviewAvailableForThisAttachment:
|
||||||
|
'Non è disponibile alcuna anteprima per questo allegato',
|
||||||
|
time: 'Tempo',
|
||||||
|
timer: 'Timer',
|
||||||
|
title: 'Titolo',
|
||||||
|
userActions_title: 'Azioni Utente',
|
||||||
|
userAddedThisCardToList: '<0>{{user}}</0><1> ha aggiunto questa card a {{list}}</1>',
|
||||||
|
userLeftNewCommentToCard:
|
||||||
|
'{{user}} ha lasciato un nuovo commento «{{comment}}» a <2>{{card}}</2>',
|
||||||
|
userMovedCardFromListToList: '{{user}} ha mosso <2>{{card}}</2> da {{fromList}} a {{toList}}',
|
||||||
|
userMovedThisCardFromListToList:
|
||||||
|
'<0>{{user}}</0><1> ha mosso questa card da {{fromList}} a {{toList}}</1>',
|
||||||
|
username: 'Username',
|
||||||
|
usernameAlreadyInUse: 'Username già in uso',
|
||||||
|
users: 'Utenti',
|
||||||
|
viewer: 'Viewer',
|
||||||
|
writeComment: 'Scrivi un commento...',
|
||||||
|
},
|
||||||
|
|
||||||
|
action: {
|
||||||
|
addAnotherCard: "Aggiungi un'altra card",
|
||||||
|
addAnotherList: "Aggiungi un'altra lista",
|
||||||
|
addAnotherTask: 'Aggiungi un altro task',
|
||||||
|
addCard: 'Aggiungi card',
|
||||||
|
addCard_title: 'Aggiungi Card',
|
||||||
|
addComment: 'Aggiungi commento',
|
||||||
|
addList: 'Aggiungi lista',
|
||||||
|
addMember: 'Add membro',
|
||||||
|
addMoreDetailedDescription: 'Aggiungi una descrizione più dettagliata',
|
||||||
|
addTask: 'Aggiungi task',
|
||||||
|
addToCard: 'Aggiungi alla card',
|
||||||
|
addUser: 'Aggiungi utente',
|
||||||
|
createBoard: 'Crea bacheca',
|
||||||
|
createFile: 'Crea file',
|
||||||
|
createLabel: 'Crea etichetta',
|
||||||
|
createNewLabel: 'Crea nuova etichetta',
|
||||||
|
createProject: 'Crea progetto',
|
||||||
|
delete: 'Elimina',
|
||||||
|
deleteAttachment: 'Elimina allegato',
|
||||||
|
deleteAvatar: 'Elimina avatar',
|
||||||
|
deleteBoard: 'Elimina bacheca',
|
||||||
|
deleteCard: 'Elimina card',
|
||||||
|
deleteCard_title: 'Elimina Card',
|
||||||
|
deleteComment: 'Elimina commento',
|
||||||
|
deleteImage: 'Elimina immagine',
|
||||||
|
deleteLabel: 'Elimina etichetta',
|
||||||
|
deleteList: 'Elimina lista',
|
||||||
|
deleteList_title: 'Elimina Lista',
|
||||||
|
deleteProject: 'Elimina progetto',
|
||||||
|
deleteProject_title: 'Elimina Progetto',
|
||||||
|
deleteTask: 'Elimina task',
|
||||||
|
deleteTask_title: 'Elimina Task',
|
||||||
|
deleteUser: 'Elimina utente',
|
||||||
|
edit: 'Modifica',
|
||||||
|
editDueDate_title: 'Modifica data di scadenza',
|
||||||
|
editDescription_title: 'Modifica Descrizione',
|
||||||
|
editEmail_title: 'Modifica E-mail',
|
||||||
|
editInformation_title: 'Modifca Informazioni',
|
||||||
|
editPassword_title: 'Modifica Password',
|
||||||
|
editPermissions: 'Modifica permessi',
|
||||||
|
editTimer_title: 'Modifica Timer',
|
||||||
|
editTitle_title: 'Modifica Titolo',
|
||||||
|
editUsername_title: 'Modifica Username',
|
||||||
|
hideDetails: 'Nascondi dettagli',
|
||||||
|
leaveBoard: 'Lascia bacheca',
|
||||||
|
leaveProject: 'Lascia progetto',
|
||||||
|
logOut_title: 'Log Out',
|
||||||
|
makeCover_title: 'Crea Cover',
|
||||||
|
move: 'Muovi',
|
||||||
|
moveCard_title: 'Muovi Card',
|
||||||
|
remove: 'Rimuovi',
|
||||||
|
removeBackground: 'Rimuovi background',
|
||||||
|
removeCover_title: 'Rimuovi Cover',
|
||||||
|
removeFromBoard: 'Rimuovi dalla bacheca',
|
||||||
|
removeFromProject: 'Rimuovi dal Progetto',
|
||||||
|
removeManager: 'Rimuovi manager',
|
||||||
|
removeMember: 'Rimuovi membro',
|
||||||
|
save: 'Salva',
|
||||||
|
showAllAttachments: 'Mostra tutti gli allegati ({{hidden}} hidden)',
|
||||||
|
showDetails: 'Mostra dettagli',
|
||||||
|
showFewerAttachments: 'Mostra meno allegati',
|
||||||
|
start: 'Start',
|
||||||
|
stop: 'Stop',
|
||||||
|
subscribe: 'Subscribe',
|
||||||
|
unsubscribe: 'Unsubscribe',
|
||||||
|
uploadNewAvatar: 'Carica nuovo avatar',
|
||||||
|
uploadNewImage: 'Carica nuova immagine',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import login from './login';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
language: 'it',
|
||||||
|
country: 'it',
|
||||||
|
name: 'Italiano',
|
||||||
|
embeddedLocale: login,
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export default {
|
||||||
|
translation: {
|
||||||
|
common: {
|
||||||
|
emailOrUsername: 'E-mail o username',
|
||||||
|
invalidEmailOrUsername: 'E-mail or username invalido',
|
||||||
|
invalidPassword: 'Password non valida',
|
||||||
|
logInToPlanka: 'Log in Planka',
|
||||||
|
noInternetConnection: 'Nessuna connessione internet',
|
||||||
|
pageNotFound_title: 'Pagina non trovata',
|
||||||
|
password: 'Password',
|
||||||
|
projectManagement: 'Project management',
|
||||||
|
serverConnectionFailed: 'Connesione al server fallita',
|
||||||
|
unknownError: 'Errore sconosciuto, prova ancora',
|
||||||
|
},
|
||||||
|
|
||||||
|
action: {
|
||||||
|
logIn: 'Log in',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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: 'Проект не найден',
|
||||||
@@ -137,7 +137,7 @@ export default {
|
|||||||
time: 'Время',
|
time: 'Время',
|
||||||
timer: 'Таймер',
|
timer: 'Таймер',
|
||||||
title: 'Название',
|
title: 'Название',
|
||||||
userActions_title: 'Действия с пользователем',
|
userActions: 'Действия с пользователем',
|
||||||
userAddedThisCardToList: '<0>{{user}}</0><1> добавил(а) эту карточку в {{list}}</1>',
|
userAddedThisCardToList: '<0>{{user}}</0><1> добавил(а) эту карточку в {{list}}</1>',
|
||||||
userLeftNewCommentToCard: '{{user}} оставил(а) комментарий «{{comment}}» к <2>{{card}}</2>',
|
userLeftNewCommentToCard: '{{user}} оставил(а) комментарий «{{comment}}» к <2>{{card}}</2>',
|
||||||
userMovedCardFromListToList:
|
userMovedCardFromListToList:
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { LOCATION_CHANGE } from 'connected-react-router';
|
import { LOCATION_CHANGE } from 'redux-first-history';
|
||||||
|
|
||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
import ModalTypes from '../constants/ModalTypes';
|
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,5 +1,3 @@
|
|||||||
import { connectRouter } from 'connected-react-router';
|
import { routerReducer } from '../redux-history-context';
|
||||||
|
|
||||||
import history from '../history';
|
export default routerReducer;
|
||||||
|
|
||||||
export default connectRouter(history);
|
|
||||||
|
|||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
import { createReduxHistoryContext } from 'redux-first-history';
|
||||||
|
|
||||||
|
export const { createReduxHistory, routerMiddleware, routerReducer } = createReduxHistoryContext({
|
||||||
|
history: createBrowserHistory(),
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ export function* fetchBoardByCurrentPath() {
|
|||||||
|
|
||||||
if (pathsMatch) {
|
if (pathsMatch) {
|
||||||
let boardId;
|
let boardId;
|
||||||
if (pathsMatch.path === Paths.BOARDS) {
|
if (pathsMatch.pattern.path === Paths.BOARDS) {
|
||||||
boardId = pathsMatch.params.id;
|
boardId = pathsMatch.params.id;
|
||||||
} else if (pathsMatch.path === Paths.CARDS) {
|
} else if (pathsMatch.pattern.path === Paths.CARDS) {
|
||||||
({
|
({
|
||||||
item: card,
|
item: card,
|
||||||
item: { boardId },
|
item: { boardId },
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function* handleBoardMembershipCreate(boardMembership) {
|
|||||||
|
|
||||||
let body;
|
let body;
|
||||||
try {
|
try {
|
||||||
body = yield call(requests.fetchBoardByCurrentPathRequest);
|
body = yield call(requests.fetchBoardByCurrentPath);
|
||||||
} catch {} // eslint-disable-line no-empty
|
} catch {} // eslint-disable-line no-empty
|
||||||
|
|
||||||
if (body && body.project && body.project.id === board2.projectId) {
|
if (body && body.project && body.project.id === board2.projectId) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { call, put, select, take } from 'redux-saga/effects';
|
import { call, put, select, take } from 'redux-saga/effects';
|
||||||
import { push } from 'connected-react-router';
|
import { push } from 'redux-first-history';
|
||||||
|
|
||||||
import request from '../request';
|
import request from '../request';
|
||||||
import selectors from '../../../selectors';
|
import selectors from '../../../selectors';
|
||||||
@@ -31,7 +31,7 @@ export function* handleLocationChange() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (pathsMatch.path) {
|
switch (pathsMatch.pattern.path) {
|
||||||
case Paths.LOGIN:
|
case Paths.LOGIN:
|
||||||
yield call(goToRoot);
|
yield call(goToRoot);
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ export function* handleLocationChange() {
|
|||||||
let attachments;
|
let attachments;
|
||||||
let deletedNotifications;
|
let deletedNotifications;
|
||||||
|
|
||||||
switch (pathsMatch.path) {
|
switch (pathsMatch.pattern.path) {
|
||||||
case Paths.BOARDS:
|
case Paths.BOARDS:
|
||||||
case Paths.CARDS: {
|
case Paths.CARDS: {
|
||||||
const currentBoard = yield select(selectors.selectCurrentBoard);
|
const currentBoard = yield select(selectors.selectCurrentBoard);
|
||||||
@@ -85,7 +85,7 @@ export function* handleLocationChange() {
|
|||||||
} catch (error) {} // eslint-disable-line no-empty
|
} catch (error) {} // eslint-disable-line no-empty
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathsMatch.path === Paths.CARDS) {
|
if (pathsMatch.pattern.path === Paths.CARDS) {
|
||||||
const notificationIds = yield select(selectors.selectNotificationIdsForCurrentCard);
|
const notificationIds = yield select(selectors.selectNotificationIdsForCurrentCard);
|
||||||
|
|
||||||
if (notificationIds && notificationIds.length > 0) {
|
if (notificationIds && notificationIds.length > 0) {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { takeEvery } from 'redux-saga/effects';
|
import { takeEvery } from 'redux-saga/effects';
|
||||||
import { LOCATION_CHANGE } from 'connected-react-router';
|
import { LOCATION_CHANGE } from 'redux-first-history';
|
||||||
|
|
||||||
import services from '../services';
|
import services from '../services';
|
||||||
|
|
||||||
|
|||||||
@@ -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 +1,5 @@
|
|||||||
import { call, put, select } from 'redux-saga/effects';
|
import { call, put, select } from 'redux-saga/effects';
|
||||||
import { push } from 'connected-react-router';
|
import { push } from 'redux-first-history';
|
||||||
|
|
||||||
import selectors from '../../../selectors';
|
import selectors from '../../../selectors';
|
||||||
import Paths from '../../../constants/Paths';
|
import Paths from '../../../constants/Paths';
|
||||||
@@ -19,7 +19,7 @@ export function* handleLocationChange() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (pathsMatch.path) {
|
switch (pathsMatch.pattern.path) {
|
||||||
case Paths.ROOT:
|
case Paths.ROOT:
|
||||||
case Paths.PROJECTS:
|
case Paths.PROJECTS:
|
||||||
case Paths.BOARDS:
|
case Paths.BOARDS:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { takeEvery } from 'redux-saga/effects';
|
import { takeEvery } from 'redux-saga/effects';
|
||||||
import { LOCATION_CHANGE } from 'connected-react-router';
|
import { LOCATION_CHANGE } from 'redux-first-history';
|
||||||
|
|
||||||
import services from '../services';
|
import services from '../services';
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user