Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
793ddd5e20 | ||
|
|
3d84888eb1 | ||
|
|
02eabd203c | ||
|
|
d0a2734161 | ||
|
|
eea57ff121 | ||
|
|
7e2b74627c | ||
|
|
bee199c13b | ||
|
|
8dd331b895 | ||
|
|
13f26b4be1 |
@@ -1,9 +1,8 @@
|
||||
# Planka
|
||||
#### Elegant open source project tracking
|
||||
|
||||
  
|
||||
|
||||
A Trello-like kanban board built with React and Redux.
|
||||
|
||||

|
||||
|
||||
[**Client demo**](https://plankanban.github.io/planka) (without server features).
|
||||
@@ -19,306 +18,24 @@ A Trello-like kanban board built with React and Redux.
|
||||
- User notifications
|
||||
- Internationalization
|
||||
|
||||
## Deploy
|
||||
## How to deploy Planka
|
||||
|
||||
There are 2 types of installation:
|
||||
1. [Dockerized](#1-docker-compose)
|
||||
2. [Without Docker](#2-without-docker)
|
||||
- [Without Docker](https://docs.planka.cloud/docs/installl-planka/Debian%20&%20Ubuntu) ([for Windows](https://docs.planka.cloud/docs/installl-planka/Windows))
|
||||
- [Dockerized](https://docs.planka.cloud/docs/installl-planka/Docker%20Compose)
|
||||
|
||||
### 1. Docker Compose
|
||||
For configuration, please see the [configuration section](https://docs.planka.cloud/docs/category/configuration).
|
||||
|
||||
[](https://github.com/plankanban/planka/pkgs/container/planka)
|
||||
## Contact
|
||||
|
||||
- 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.
|
||||
- Edit `BASE_URL` to match your domain name or IP address.
|
||||
- Edit `SECRET_KEY` with random value. You can generate it by `openssl rand -hex 64`.
|
||||
- If you want to get a hosted version of Planka, you can contact us via email contact@planka.cloud
|
||||
- For any security issues, please do not create a public issue on GitHub, instead please write to security@planka.cloud
|
||||
|
||||
Download the docker-compose.yml:
|
||||
|
||||
```
|
||||
curl -L https://raw.githubusercontent.com/plankanban/planka/master/docker-compose.yml -o docker-compose.yml
|
||||
```
|
||||
|
||||
Pull images and start services:
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Demo user: demo@demo.demo demo
|
||||
|
||||
### 2. Without Docker
|
||||
|
||||
Installing without Docker is a bit more complicated, here's what you need to do:
|
||||
|
||||
1. Clone this repository into a directory of your choice. (e.g. `/var/www/planka`)
|
||||
|
||||
```bash
|
||||
mkdir -p /var/www/planka
|
||||
cd /var/www/planka
|
||||
git clone https://github.com/plankanban/planka.git .
|
||||
```
|
||||
|
||||
2. Install dependencies and build the client.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run client:build
|
||||
```
|
||||
|
||||
**Note**: You can use `yarn` or `pnpm` instead of `npm`.
|
||||
|
||||
3. Copy the content of the `client/build` directory to the server.
|
||||
|
||||
```bash
|
||||
cp -r client/build/. server/public
|
||||
cp client/build/index.html server/views/index.ejs
|
||||
```
|
||||
|
||||
4. Copy the start script and navigate to the `server` directory.
|
||||
|
||||
```bash
|
||||
cp start.sh server
|
||||
cd server
|
||||
```
|
||||
|
||||
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. 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's already available in Planka, or you can also use the great tool [trello2planka](https://github.com/christophenne/trello2planka) to do the import.
|
||||
|
||||
### Backup and Restore
|
||||
|
||||
Planka comes with two scripts that allow for manual backup and restore when running Planka with docker-compose.yml.
|
||||
Backups can be triggered with `docker-backup.sh` which will export the Database, User Avatars, Project Backgrounds and Attachments into a single tgz file.
|
||||
|
||||
```
|
||||
$ ./docker-backup.sh
|
||||
Exporting postgres database ... Success!
|
||||
Exporting user-avatars ... Success!
|
||||
Exporting project-background-images ... Success!
|
||||
Exporting attachments ... Success!
|
||||
Creating final tarball 2023-01-17T15-37-22Z-backup.tgz ... Success!
|
||||
Cleaning up temporary files and folders ... Success!
|
||||
Backup Complete!
|
||||
```
|
||||
|
||||
The resulting backup can be restored using the `docker-restore.sh` script.
|
||||
|
||||
```
|
||||
$ ./docker-restore.sh 2023-01-17T15-37-22Z-backup.tgz
|
||||
Extracting tarball 2023-01-17T11-10-54Z-backup.tgz ... Success!
|
||||
Importing postgres database ...
|
||||
|
||||
[Many lines of postgres output]
|
||||
...
|
||||
|
||||
Success!
|
||||
Importing user-avatars ... Success!
|
||||
Importing project-background-images ... Success!
|
||||
Importing attachments ... Success!
|
||||
Cleaning up temporary files and folders ... Success!
|
||||
Restore complete!
|
||||
```
|
||||
We do NOT offer any public support via email, please use GitHub.
|
||||
|
||||
## Development
|
||||
|
||||
Clone the repository and install dependencies:
|
||||
|
||||
```
|
||||
git clone https://github.com/plankanban/planka.git
|
||||
|
||||
cd planka
|
||||
npm install
|
||||
```
|
||||
|
||||
Either use a local database or start the provided development database:
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-dev.yml up
|
||||
```
|
||||
|
||||
Create `server/.env` based on `server/.env.sample` and edit `DATABASE_URL` if needed, then initialize the database:
|
||||
|
||||
```
|
||||
npm run server:db:init
|
||||
```
|
||||
|
||||
Start the development server:
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
Demo user: demo@demo.demo demo
|
||||
See the [development section](https://docs.planka.cloud/docs/Development).
|
||||
|
||||
## Tech stack
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ Most recent release.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report any security issues you discovered to meltyshev@gmail.com. If the issue is confirmed, we will release a patch as soon as possible depending on complexity.
|
||||
Please report any security issues you discovered to security@planka.cloud. If the issue is confirmed, we will release a patch as soon as possible depending on complexity.
|
||||
|
||||
**Do NOT create public issues on GitHub for security vulnerabilities.**
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
REACT_APP_VERSION=1.10.2
|
||||
REACT_APP_VERSION=1.10.3
|
||||
|
||||
@@ -22,7 +22,9 @@ const replaceBaseUrl = (compiler) => {
|
||||
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);
|
||||
const relPath = path
|
||||
.relative(path.dirname(info.targetPath), info.outputPath)
|
||||
.replace(/\\/g, '/');
|
||||
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
|
||||
|
||||
+10
-10
@@ -8,11 +8,11 @@ export const transformCard = (card) => ({
|
||||
...(card.dueDate && {
|
||||
dueDate: new Date(card.dueDate),
|
||||
}),
|
||||
...(card.timer && {
|
||||
timer: {
|
||||
...card.timer,
|
||||
...(card.timer.startedAt && {
|
||||
startedAt: new Date(card.timer.startedAt),
|
||||
...(card.stopwatch && {
|
||||
stopwatch: {
|
||||
...card.stopwatch,
|
||||
...(card.stopwatch.startedAt && {
|
||||
startedAt: new Date(card.stopwatch.startedAt),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
@@ -23,11 +23,11 @@ export const transformCardData = (data) => ({
|
||||
...(data.dueDate && {
|
||||
dueDate: data.dueDate.toISOString(),
|
||||
}),
|
||||
...(data.timer && {
|
||||
timer: {
|
||||
...data.timer,
|
||||
...(data.timer.startedAt && {
|
||||
startedAt: data.timer.startedAt.toISOString(),
|
||||
...(data.stopwatch && {
|
||||
stopwatch: {
|
||||
...data.stopwatch,
|
||||
...(data.stopwatch.startedAt && {
|
||||
startedAt: data.stopwatch.startedAt.toISOString(),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -62,6 +62,11 @@ const Board = React.memo(
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(event) => {
|
||||
// If button is defined and not equal to 0 (left click)
|
||||
if (event.button) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target !== wrapper.current && !event.target.dataset.dragScroller) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useSteps } from '../../hooks';
|
||||
import BoardMembershipsStep from '../BoardMembershipsStep';
|
||||
import LabelsStep from '../LabelsStep';
|
||||
import DueDateEditStep from '../DueDateEditStep';
|
||||
import TimerEditStep from '../TimerEditStep';
|
||||
import StopwatchEditStep from '../StopwatchEditStep';
|
||||
import CardMoveStep from '../CardMoveStep';
|
||||
import DeleteStep from '../DeleteStep';
|
||||
|
||||
@@ -19,7 +19,7 @@ const StepTypes = {
|
||||
USERS: 'USERS',
|
||||
LABELS: 'LABELS',
|
||||
EDIT_DUE_DATE: 'EDIT_DUE_DATE',
|
||||
EDIT_TIMER: 'EDIT_TIMER',
|
||||
EDIT_STOPWATCH: 'EDIT_STOPWATCH',
|
||||
MOVE: 'MOVE',
|
||||
DELETE: 'DELETE',
|
||||
};
|
||||
@@ -68,8 +68,8 @@ const ActionsStep = React.memo(
|
||||
openStep(StepTypes.EDIT_DUE_DATE);
|
||||
}, [openStep]);
|
||||
|
||||
const handleEditTimerClick = useCallback(() => {
|
||||
openStep(StepTypes.EDIT_TIMER);
|
||||
const handleEditStopwatchClick = useCallback(() => {
|
||||
openStep(StepTypes.EDIT_STOPWATCH);
|
||||
}, [openStep]);
|
||||
|
||||
const handleMoveClick = useCallback(() => {
|
||||
@@ -89,10 +89,10 @@ const ActionsStep = React.memo(
|
||||
[onUpdate],
|
||||
);
|
||||
|
||||
const handleTimerUpdate = useCallback(
|
||||
(timer) => {
|
||||
const handleStopwatchUpdate = useCallback(
|
||||
(stopwatch) => {
|
||||
onUpdate({
|
||||
timer,
|
||||
stopwatch,
|
||||
});
|
||||
},
|
||||
[onUpdate],
|
||||
@@ -133,11 +133,11 @@ const ActionsStep = React.memo(
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
case StepTypes.EDIT_TIMER:
|
||||
case StepTypes.EDIT_STOPWATCH:
|
||||
return (
|
||||
<TimerEditStep
|
||||
defaultValue={card.timer}
|
||||
onUpdate={handleTimerUpdate}
|
||||
<StopwatchEditStep
|
||||
defaultValue={card.stopwatch}
|
||||
onUpdate={handleStopwatchUpdate}
|
||||
onBack={handleBack}
|
||||
onClose={onClose}
|
||||
/>
|
||||
@@ -197,8 +197,8 @@ const ActionsStep = React.memo(
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditTimerClick}>
|
||||
{t('action.editTimer', {
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditStopwatchClick}>
|
||||
{t('action.editStopwatch', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Link } from 'react-router-dom';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { usePopup } from '../../lib/popup';
|
||||
|
||||
import { startTimer, stopTimer } from '../../utils/timer';
|
||||
import { startStopwatch, stopStopwatch } from '../../utils/stopwatch';
|
||||
import Paths from '../../constants/Paths';
|
||||
import Tasks from './Tasks';
|
||||
import NameEdit from './NameEdit';
|
||||
@@ -14,7 +14,7 @@ import ActionsStep from './ActionsStep';
|
||||
import User from '../User';
|
||||
import Label from '../Label';
|
||||
import DueDate from '../DueDate';
|
||||
import Timer from '../Timer';
|
||||
import Stopwatch from '../Stopwatch';
|
||||
|
||||
import styles from './Card.module.scss';
|
||||
|
||||
@@ -24,7 +24,7 @@ const Card = React.memo(
|
||||
index,
|
||||
name,
|
||||
dueDate,
|
||||
timer,
|
||||
stopwatch,
|
||||
coverUrl,
|
||||
boardId,
|
||||
listId,
|
||||
@@ -60,15 +60,15 @@ const Card = React.memo(
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleToggleTimerClick = useCallback(
|
||||
const handleToggleStopwatchClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
onUpdate({
|
||||
timer: timer.startedAt ? stopTimer(timer) : startTimer(timer),
|
||||
stopwatch: stopwatch.startedAt ? stopStopwatch(stopwatch) : startStopwatch(stopwatch),
|
||||
});
|
||||
},
|
||||
[timer, onUpdate],
|
||||
[stopwatch, onUpdate],
|
||||
);
|
||||
|
||||
const handleNameUpdate = useCallback(
|
||||
@@ -104,7 +104,7 @@ const Card = React.memo(
|
||||
)}
|
||||
<div className={styles.name}>{name}</div>
|
||||
{tasks.length > 0 && <Tasks items={tasks} />}
|
||||
{(dueDate || timer || notificationsTotal > 0) && (
|
||||
{(dueDate || stopwatch || notificationsTotal > 0) && (
|
||||
<span className={styles.attachments}>
|
||||
{notificationsTotal > 0 && (
|
||||
<span
|
||||
@@ -122,14 +122,14 @@ const Card = React.memo(
|
||||
<DueDate value={dueDate} size="tiny" />
|
||||
</span>
|
||||
)}
|
||||
{timer && (
|
||||
{stopwatch && (
|
||||
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
||||
<Timer
|
||||
<Stopwatch
|
||||
as="span"
|
||||
startedAt={timer.startedAt}
|
||||
total={timer.total}
|
||||
startedAt={stopwatch.startedAt}
|
||||
total={stopwatch.total}
|
||||
size="tiny"
|
||||
onClick={canEdit ? handleToggleTimerClick : undefined}
|
||||
onClick={canEdit ? handleToggleStopwatchClick : undefined}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
@@ -171,7 +171,7 @@ const Card = React.memo(
|
||||
<ActionsPopup
|
||||
card={{
|
||||
dueDate,
|
||||
timer,
|
||||
stopwatch,
|
||||
boardId,
|
||||
listId,
|
||||
projectId,
|
||||
@@ -219,7 +219,7 @@ Card.propTypes = {
|
||||
index: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
dueDate: PropTypes.instanceOf(Date),
|
||||
timer: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
coverUrl: PropTypes.string,
|
||||
boardId: PropTypes.string.isRequired,
|
||||
listId: PropTypes.string.isRequired,
|
||||
@@ -252,7 +252,7 @@ Card.propTypes = {
|
||||
|
||||
Card.defaultProps = {
|
||||
dueDate: undefined,
|
||||
timer: undefined,
|
||||
stopwatch: undefined,
|
||||
coverUrl: undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Button, Grid, Icon, Modal } from 'semantic-ui-react';
|
||||
import { usePopup } from '../../lib/popup';
|
||||
import { Markdown } from '../../lib/custom-ui';
|
||||
|
||||
import { startTimer, stopTimer } from '../../utils/timer';
|
||||
import { startStopwatch, stopStopwatch } from '../../utils/stopwatch';
|
||||
import NameField from './NameField';
|
||||
import DescriptionEdit from './DescriptionEdit';
|
||||
import Tasks from './Tasks';
|
||||
@@ -17,11 +17,11 @@ import Activities from './Activities';
|
||||
import User from '../User';
|
||||
import Label from '../Label';
|
||||
import DueDate from '../DueDate';
|
||||
import Timer from '../Timer';
|
||||
import Stopwatch from '../Stopwatch';
|
||||
import BoardMembershipsStep from '../BoardMembershipsStep';
|
||||
import LabelsStep from '../LabelsStep';
|
||||
import DueDateEditStep from '../DueDateEditStep';
|
||||
import TimerEditStep from '../TimerEditStep';
|
||||
import StopwatchEditStep from '../StopwatchEditStep';
|
||||
import CardMoveStep from '../CardMoveStep';
|
||||
import DeleteStep from '../DeleteStep';
|
||||
|
||||
@@ -32,7 +32,7 @@ const CardModal = React.memo(
|
||||
name,
|
||||
description,
|
||||
dueDate,
|
||||
timer,
|
||||
stopwatch,
|
||||
isSubscribed,
|
||||
isActivitiesFetching,
|
||||
isAllActivitiesFetched,
|
||||
@@ -83,11 +83,11 @@ const CardModal = React.memo(
|
||||
|
||||
const isGalleryOpened = useRef(false);
|
||||
|
||||
const handleToggleTimerClick = useCallback(() => {
|
||||
const handleToggleStopwatchClick = useCallback(() => {
|
||||
onUpdate({
|
||||
timer: timer.startedAt ? stopTimer(timer) : startTimer(timer),
|
||||
stopwatch: stopwatch.startedAt ? stopStopwatch(stopwatch) : startStopwatch(stopwatch),
|
||||
});
|
||||
}, [timer, onUpdate]);
|
||||
}, [stopwatch, onUpdate]);
|
||||
|
||||
const handleNameUpdate = useCallback(
|
||||
(newName) => {
|
||||
@@ -116,10 +116,10 @@ const CardModal = React.memo(
|
||||
[onUpdate],
|
||||
);
|
||||
|
||||
const handleTimerUpdate = useCallback(
|
||||
(newTimer) => {
|
||||
const handleStopwatchUpdate = useCallback(
|
||||
(newStopwatch) => {
|
||||
onUpdate({
|
||||
timer: newTimer,
|
||||
stopwatch: newStopwatch,
|
||||
});
|
||||
},
|
||||
[onUpdate],
|
||||
@@ -160,7 +160,7 @@ const CardModal = React.memo(
|
||||
const BoardMembershipsPopup = usePopup(BoardMembershipsStep);
|
||||
const LabelsPopup = usePopup(LabelsStep);
|
||||
const DueDateEditPopup = usePopup(DueDateEditStep);
|
||||
const TimerEditPopup = usePopup(TimerEditStep);
|
||||
const StopwatchEditPopup = usePopup(StopwatchEditStep);
|
||||
const CardMovePopup = usePopup(CardMoveStep);
|
||||
const DeletePopup = usePopup(DeleteStep);
|
||||
|
||||
@@ -185,7 +185,7 @@ const CardModal = React.memo(
|
||||
</Grid.Row>
|
||||
<Grid.Row className={styles.modalPadding}>
|
||||
<Grid.Column width={canEdit ? 12 : 16} className={styles.contentPadding}>
|
||||
{(users.length > 0 || labels.length > 0 || dueDate || timer) && (
|
||||
{(users.length > 0 || labels.length > 0 || dueDate || stopwatch) && (
|
||||
<div className={styles.moduleWrapper}>
|
||||
{users.length > 0 && (
|
||||
<div className={styles.attachments}>
|
||||
@@ -294,30 +294,33 @@ const CardModal = React.memo(
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{timer && (
|
||||
{stopwatch && (
|
||||
<div className={styles.attachments}>
|
||||
<div className={styles.text}>
|
||||
{t('common.timer', {
|
||||
{t('common.stopwatch', {
|
||||
context: 'title',
|
||||
})}
|
||||
</div>
|
||||
<span className={styles.attachment}>
|
||||
{canEdit ? (
|
||||
<TimerEditPopup defaultValue={timer} onUpdate={handleTimerUpdate}>
|
||||
<Timer startedAt={timer.startedAt} total={timer.total} />
|
||||
</TimerEditPopup>
|
||||
<StopwatchEditPopup
|
||||
defaultValue={stopwatch}
|
||||
onUpdate={handleStopwatchUpdate}
|
||||
>
|
||||
<Stopwatch startedAt={stopwatch.startedAt} total={stopwatch.total} />
|
||||
</StopwatchEditPopup>
|
||||
) : (
|
||||
<Timer startedAt={timer.startedAt} total={timer.total} />
|
||||
<Stopwatch startedAt={stopwatch.startedAt} total={stopwatch.total} />
|
||||
)}
|
||||
</span>
|
||||
{canEdit && (
|
||||
<button
|
||||
onClick={handleToggleTimerClick}
|
||||
onClick={handleToggleStopwatchClick}
|
||||
type="button"
|
||||
className={classNames(styles.attachment, styles.dueDate)}
|
||||
>
|
||||
<Icon
|
||||
name={timer.startedAt ? 'pause' : 'play'}
|
||||
name={stopwatch.startedAt ? 'pause' : 'play'}
|
||||
size="small"
|
||||
className={styles.addAttachment}
|
||||
/>
|
||||
@@ -447,12 +450,12 @@ const CardModal = React.memo(
|
||||
})}
|
||||
</Button>
|
||||
</DueDateEditPopup>
|
||||
<TimerEditPopup defaultValue={timer} onUpdate={handleTimerUpdate}>
|
||||
<StopwatchEditPopup defaultValue={stopwatch} onUpdate={handleStopwatchUpdate}>
|
||||
<Button fluid className={styles.actionButton}>
|
||||
<Icon name="clock outline" className={styles.actionIcon} />
|
||||
{t('common.timer')}
|
||||
{t('common.stopwatch')}
|
||||
</Button>
|
||||
</TimerEditPopup>
|
||||
</StopwatchEditPopup>
|
||||
<AttachmentAddPopup onCreate={onAttachmentCreate}>
|
||||
<Button fluid className={styles.actionButton}>
|
||||
<Icon name="attach" className={styles.actionIcon} />
|
||||
@@ -524,7 +527,7 @@ CardModal.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
dueDate: PropTypes.instanceOf(Date),
|
||||
timer: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
isSubscribed: PropTypes.bool.isRequired,
|
||||
isActivitiesFetching: PropTypes.bool.isRequired,
|
||||
isAllActivitiesFetched: PropTypes.bool.isRequired,
|
||||
@@ -577,7 +580,7 @@ CardModal.propTypes = {
|
||||
CardModal.defaultProps = {
|
||||
description: undefined,
|
||||
dueDate: undefined,
|
||||
timer: undefined,
|
||||
stopwatch: undefined,
|
||||
};
|
||||
|
||||
export default CardModal;
|
||||
|
||||
+7
-7
@@ -4,9 +4,9 @@ import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useForceUpdate, usePrevious } from '../../lib/hooks';
|
||||
|
||||
import { formatTimer } from '../../utils/timer';
|
||||
import { formatStopwatch } from '../../utils/stopwatch';
|
||||
|
||||
import styles from './Timer.module.scss';
|
||||
import styles from './Stopwatch.module.scss';
|
||||
|
||||
const SIZES = {
|
||||
TINY: 'tiny',
|
||||
@@ -14,7 +14,7 @@ const SIZES = {
|
||||
MEDIUM: 'medium',
|
||||
};
|
||||
|
||||
const Timer = React.memo(({ as, startedAt, total, size, isDisabled, onClick }) => {
|
||||
const Stopwatch = React.memo(({ as, startedAt, total, size, isDisabled, onClick }) => {
|
||||
const prevStartedAt = usePrevious(startedAt);
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
@@ -56,7 +56,7 @@ const Timer = React.memo(({ as, startedAt, total, size, isDisabled, onClick }) =
|
||||
onClick && styles.wrapperHoverable,
|
||||
)}
|
||||
>
|
||||
{formatTimer({ startedAt, total })}
|
||||
{formatStopwatch({ startedAt, total })}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -71,7 +71,7 @@ const Timer = React.memo(({ as, startedAt, total, size, isDisabled, onClick }) =
|
||||
);
|
||||
});
|
||||
|
||||
Timer.propTypes = {
|
||||
Stopwatch.propTypes = {
|
||||
as: PropTypes.elementType,
|
||||
startedAt: PropTypes.instanceOf(Date),
|
||||
total: PropTypes.number.isRequired, // eslint-disable-line react/no-unused-prop-types
|
||||
@@ -80,7 +80,7 @@ Timer.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
Timer.defaultProps = {
|
||||
Stopwatch.defaultProps = {
|
||||
as: 'button',
|
||||
startedAt: undefined,
|
||||
size: SIZES.MEDIUM,
|
||||
@@ -88,4 +88,4 @@ Timer.defaultProps = {
|
||||
onClick: undefined,
|
||||
};
|
||||
|
||||
export default Timer;
|
||||
export default Stopwatch;
|
||||
@@ -0,0 +1,3 @@
|
||||
import Stopwatch from './Stopwatch';
|
||||
|
||||
export default Stopwatch;
|
||||
+21
-15
@@ -7,12 +7,18 @@ import { useToggle } from '../../lib/hooks';
|
||||
import { Input, Popup } from '../../lib/custom-ui';
|
||||
|
||||
import { useForm } from '../../hooks';
|
||||
import { createTimer, getTimerParts, startTimer, stopTimer, updateTimer } from '../../utils/timer';
|
||||
import {
|
||||
createStopwatch,
|
||||
getStopwatchParts,
|
||||
startStopwatch,
|
||||
stopStopwatch,
|
||||
updateStopwatch,
|
||||
} from '../../utils/stopwatch';
|
||||
|
||||
import styles from './TimerEditStep.module.scss';
|
||||
import styles from './StopwatchEditStep.module.scss';
|
||||
|
||||
const createData = (timer) => {
|
||||
if (!timer) {
|
||||
const createData = (stopwatch) => {
|
||||
if (!stopwatch) {
|
||||
return {
|
||||
hours: '0',
|
||||
minutes: '0',
|
||||
@@ -20,7 +26,7 @@ const createData = (timer) => {
|
||||
};
|
||||
}
|
||||
|
||||
const { hours, minutes, seconds } = getTimerParts(timer);
|
||||
const { hours, minutes, seconds } = getStopwatchParts(stopwatch);
|
||||
|
||||
return {
|
||||
hours: `${hours}`,
|
||||
@@ -29,7 +35,7 @@ const createData = (timer) => {
|
||||
};
|
||||
};
|
||||
|
||||
const TimerEditStep = React.memo(({ defaultValue, onUpdate, onBack, onClose }) => {
|
||||
const StopwatchEditStep = React.memo(({ defaultValue, onUpdate, onBack, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
const [data, handleFieldChange, setData] = useForm(() => createData(defaultValue));
|
||||
const [isEditing, toggleEditing] = useToggle();
|
||||
@@ -39,12 +45,12 @@ const TimerEditStep = React.memo(({ defaultValue, onUpdate, onBack, onClose }) =
|
||||
const secondsField = useRef(null);
|
||||
|
||||
const handleStartClick = useCallback(() => {
|
||||
onUpdate(startTimer(defaultValue));
|
||||
onUpdate(startStopwatch(defaultValue));
|
||||
onClose();
|
||||
}, [defaultValue, onUpdate, onClose]);
|
||||
|
||||
const handleStopClick = useCallback(() => {
|
||||
onUpdate(stopTimer(defaultValue));
|
||||
onUpdate(stopStopwatch(defaultValue));
|
||||
}, [defaultValue, onUpdate]);
|
||||
|
||||
const handleClearClick = useCallback(() => {
|
||||
@@ -83,11 +89,11 @@ const TimerEditStep = React.memo(({ defaultValue, onUpdate, onBack, onClose }) =
|
||||
}
|
||||
|
||||
if (defaultValue) {
|
||||
if (!dequal(parts, getTimerParts(defaultValue))) {
|
||||
onUpdate(updateTimer(defaultValue, parts));
|
||||
if (!dequal(parts, getStopwatchParts(defaultValue))) {
|
||||
onUpdate(updateStopwatch(defaultValue, parts));
|
||||
}
|
||||
} else {
|
||||
onUpdate(createTimer(parts));
|
||||
onUpdate(createStopwatch(parts));
|
||||
}
|
||||
|
||||
onClose();
|
||||
@@ -102,7 +108,7 @@ const TimerEditStep = React.memo(({ defaultValue, onUpdate, onBack, onClose }) =
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t('common.editTimer', {
|
||||
{t('common.editStopwatch', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
@@ -171,16 +177,16 @@ const TimerEditStep = React.memo(({ defaultValue, onUpdate, onBack, onClose }) =
|
||||
);
|
||||
});
|
||||
|
||||
TimerEditStep.propTypes = {
|
||||
StopwatchEditStep.propTypes = {
|
||||
defaultValue: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
TimerEditStep.defaultProps = {
|
||||
StopwatchEditStep.defaultProps = {
|
||||
defaultValue: undefined,
|
||||
onBack: undefined,
|
||||
};
|
||||
|
||||
export default TimerEditStep;
|
||||
export default StopwatchEditStep;
|
||||
@@ -0,0 +1,3 @@
|
||||
import StopwatchEditStep from './StopwatchEditStep';
|
||||
|
||||
export default StopwatchEditStep;
|
||||
@@ -1,3 +0,0 @@
|
||||
import Timer from './Timer';
|
||||
|
||||
export default Timer;
|
||||
@@ -1,3 +0,0 @@
|
||||
import TimerEditStep from './TimerEditStep';
|
||||
|
||||
export default TimerEditStep;
|
||||
@@ -20,7 +20,7 @@ const makeMapStateToProps = () => {
|
||||
const allLabels = selectors.selectLabelsForCurrentBoard(state);
|
||||
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
|
||||
const { name, dueDate, timer, coverUrl, boardId, listId, isPersisted } = selectCardById(
|
||||
const { name, dueDate, stopwatch, coverUrl, boardId, listId, isPersisted } = selectCardById(
|
||||
state,
|
||||
id,
|
||||
);
|
||||
@@ -38,7 +38,7 @@ const makeMapStateToProps = () => {
|
||||
index,
|
||||
name,
|
||||
dueDate,
|
||||
timer,
|
||||
stopwatch,
|
||||
coverUrl,
|
||||
boardId,
|
||||
listId,
|
||||
|
||||
@@ -21,7 +21,7 @@ const mapStateToProps = (state) => {
|
||||
name,
|
||||
description,
|
||||
dueDate,
|
||||
timer,
|
||||
stopwatch,
|
||||
isSubscribed,
|
||||
isActivitiesFetching,
|
||||
isAllActivitiesFetched,
|
||||
@@ -49,7 +49,7 @@ const mapStateToProps = (state) => {
|
||||
name,
|
||||
description,
|
||||
dueDate,
|
||||
timer,
|
||||
stopwatch,
|
||||
isSubscribed,
|
||||
isActivitiesFetching,
|
||||
isAllActivitiesFetched,
|
||||
|
||||
@@ -76,7 +76,7 @@ export default {
|
||||
editEmail_title: 'Upravit e-mail',
|
||||
editLabel_title: 'Upravit štítek',
|
||||
editPassword_title: 'Upravit heslo',
|
||||
editTimer_title: 'Upravit časovač',
|
||||
editStopwatch_title: 'Upravit časovač',
|
||||
editUsername_title: 'Upravit uživatelské jméno',
|
||||
email: 'E-mail',
|
||||
emailAlreadyInUse: 'E-mail se již používá',
|
||||
@@ -127,11 +127,11 @@ export default {
|
||||
selectList: 'Vybrat seznam',
|
||||
selectProject: 'Vybrat projekt',
|
||||
settings: 'Nastavení',
|
||||
stopwatch: 'Časovač',
|
||||
subscribeToMyOwnCardsByDefault: 'Ve výchozím nastavení odebírat vlastní karty',
|
||||
taskActions_title: 'Akce na úkolu',
|
||||
tasks: 'Úkoly',
|
||||
time: 'Čas',
|
||||
timer: 'Časovač',
|
||||
title: 'Titulek',
|
||||
userActions_title: 'Akce na uživateli',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> přidal kartu do {{list}}</1>',
|
||||
@@ -183,7 +183,7 @@ export default {
|
||||
editDescription_title: 'Upravit popis',
|
||||
editEmail_title: 'Upravit e-mail',
|
||||
editPassword_title: 'Upravit heslo',
|
||||
editTimer_title: 'Upravit časovač',
|
||||
editStopwatch_title: 'Upravit časovač',
|
||||
editTitle_title: 'Upravit titulek',
|
||||
editUsername_title: 'Upravit uživatelské jméno',
|
||||
leaveBoard: 'Opustit tabuli',
|
||||
|
||||
@@ -75,7 +75,7 @@ export default {
|
||||
editEmail_title: 'Ændre E-mail',
|
||||
editLabel_title: 'ændre mærkat',
|
||||
editPassword_title: 'Ændre løsen',
|
||||
editTimer_title: 'Ændre timer',
|
||||
editStopwatch_title: 'Ændre timer',
|
||||
editUsername_title: 'Ændre brugernavn',
|
||||
email: 'E-mail',
|
||||
emailAlreadyInUse: 'E-mail allerede i brug',
|
||||
@@ -126,11 +126,11 @@ export default {
|
||||
selectList: 'Vælg liste',
|
||||
selectProject: 'Vælg projekt',
|
||||
settings: 'Indstillinger',
|
||||
stopwatch: 'Tidsmåler',
|
||||
subscribeToMyOwnCardsByDefault: 'Abonnér på egne kort som standard',
|
||||
taskActions_title: 'Opgave handlinger',
|
||||
tasks: 'Opgaver',
|
||||
time: 'Tid',
|
||||
timer: 'Tidsmåler',
|
||||
title: 'Overskrift',
|
||||
userActions_title: 'Brugerhandlinger',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> tilføjede kortet til {{list}}</1>',
|
||||
@@ -183,7 +183,7 @@ export default {
|
||||
editDescription_title: 'Ændre beskrivelse',
|
||||
editEmail_title: 'Ændre e-mail',
|
||||
editPassword_title: 'Ændre Løsen',
|
||||
editTimer_title: 'Ændre tidstager',
|
||||
editStopwatch_title: 'Ændre tidstager',
|
||||
editTitle_title: 'Ændre overskrift',
|
||||
editUsername_title: 'Ændre brugernavn',
|
||||
leaveBoard: 'Forlad bræt',
|
||||
|
||||
@@ -82,7 +82,7 @@ export default {
|
||||
editEmail_title: 'E-Mail-Adresse bearbeiten',
|
||||
editLabel_title: 'Label bearbeiten',
|
||||
editPassword_title: 'Passwort ändern',
|
||||
editTimer_title: 'Timer bearbeiten',
|
||||
editStopwatch_title: 'Stoppuhr bearbeiten',
|
||||
editUsername_title: 'Benutzername ändern',
|
||||
email: 'E-Mail-Adresse',
|
||||
emailAlreadyInUse: 'E-Mail-Adresse bereits in Verwendung',
|
||||
@@ -134,12 +134,12 @@ export default {
|
||||
selectList: 'Liste auswählen',
|
||||
selectProject: 'Projekt auswählen',
|
||||
settings: 'Einstellungen',
|
||||
stopwatch: 'Stoppuhr',
|
||||
subscribeToMyOwnCardsByDefault: 'Standardmäßig meine eigenen Karten abonnieren',
|
||||
taskActions_title: 'Aufgabenaktionen',
|
||||
tasks: 'Aufgaben',
|
||||
thereIsNoPreviewAvailableForThisAttachment: 'Für diesen Anhang ist keine Vorschau verfügbar',
|
||||
time: 'Zeit',
|
||||
timer: 'Timer',
|
||||
title: 'Titel',
|
||||
userActions_title: 'Benutzeraktionen',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> hat diese Karte hinzugefügt zu {{list}}</1>',
|
||||
@@ -193,7 +193,7 @@ export default {
|
||||
editDescription_title: 'Beschreibung ändern',
|
||||
editEmail_title: 'E-Mail-Adresse bearbeiten',
|
||||
editPassword_title: 'Passwort ändern',
|
||||
editTimer_title: 'Timer bearbeiten',
|
||||
editStopwatch_title: 'Stoppuhr bearbeiten',
|
||||
editTitle_title: 'Titel bearbeiten',
|
||||
editUsername_title: 'Benutzername ändern',
|
||||
hideDetails: 'Details ausblenden',
|
||||
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
editLabel_title: 'Edit Label',
|
||||
editPassword_title: 'Edit Password',
|
||||
editPermissions_title: 'Edit Permissions',
|
||||
editTimer_title: 'Edit Timer',
|
||||
editStopwatch_title: 'Edit Stopwatch',
|
||||
editUsername_title: 'Edit Username',
|
||||
email: 'E-mail',
|
||||
emailAlreadyInUse: 'E-mail already in use',
|
||||
@@ -138,13 +138,13 @@ export default {
|
||||
selectPermissions_title: 'Select Permissions',
|
||||
selectProject: 'Select project',
|
||||
settings: 'Settings',
|
||||
stopwatch: 'Stopwatch',
|
||||
subscribeToMyOwnCardsByDefault: 'Subscribe to my own cards by default',
|
||||
taskActions_title: 'Task Actions',
|
||||
tasks: 'Tasks',
|
||||
thereIsNoPreviewAvailableForThisAttachment:
|
||||
'There is no preview available for this attachment',
|
||||
time: 'Time',
|
||||
timer: 'Timer',
|
||||
title: 'Title',
|
||||
userActions_title: 'User Actions',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> added this card to {{list}}</1>',
|
||||
@@ -201,7 +201,7 @@ export default {
|
||||
editInformation_title: 'Edit Information',
|
||||
editPassword_title: 'Edit Password',
|
||||
editPermissions: 'Edit permissions',
|
||||
editTimer_title: 'Edit Timer',
|
||||
editStopwatch_title: 'Edit Stopwatch',
|
||||
editTitle_title: 'Edit Title',
|
||||
editUsername_title: 'Edit Username',
|
||||
hideDetails: 'Hide details',
|
||||
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
editEmail_title: 'Editar Correo',
|
||||
editLabel_title: 'Editar Etiqueta',
|
||||
editPassword_title: 'Editar Contraseña',
|
||||
editTimer_title: 'Editar Temporizador',
|
||||
editStopwatch_title: 'Editar Temporizador',
|
||||
editUsername_title: 'Edit nombre de usuario',
|
||||
email: 'Correo',
|
||||
emailAlreadyInUse: 'El correo ya está en uso',
|
||||
@@ -115,11 +115,11 @@ export default {
|
||||
selectList: 'Seleccionar Lista',
|
||||
selectProject: 'Seleccionar Proyecto',
|
||||
settings: 'Configuraciones',
|
||||
stopwatch: 'Temporizador',
|
||||
subscribeToMyOwnCardsByDefault: 'Suscribirme a mis propias tarjetas por defecto',
|
||||
taskActions_title: 'Acciones de Tareas',
|
||||
tasks: 'Tareas',
|
||||
time: 'Tiempo',
|
||||
timer: 'Temporizador',
|
||||
title: 'Título',
|
||||
userActions_title: 'Acciones de Usuario',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> añadido a esta tarjeta en {{list}}</1>',
|
||||
@@ -172,7 +172,7 @@ export default {
|
||||
editDescription_title: 'Editar Descripción',
|
||||
editEmail_title: 'Editar Correo',
|
||||
editPassword_title: 'Editar Contraseña',
|
||||
editTimer_title: 'Edit Temporizador',
|
||||
editStopwatch_title: 'Edit Temporizador',
|
||||
editTitle_title: 'Edit Título',
|
||||
editUsername_title: 'Editar Nombre de Usuario',
|
||||
logOut_title: 'Cerrar Sesión',
|
||||
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
editEmail_title: "Modifier l'e-mail",
|
||||
editLabel_title: "Modifier l'étiquette",
|
||||
editPassword_title: 'Modifier le mot de passe',
|
||||
editTimer_title: 'Modifier la minuterie',
|
||||
editStopwatch_title: 'Modifier la minuterie',
|
||||
editUsername_title: "Modifier le nom d'utilisateur",
|
||||
email: 'E-mail',
|
||||
emailAlreadyInUse: 'Email déjà utilisé',
|
||||
@@ -114,11 +114,11 @@ export default {
|
||||
selectList: 'Sélectionner une liste',
|
||||
selectProject: 'Sélectionner un projet',
|
||||
settings: 'Réglages',
|
||||
stopwatch: 'Minuteur',
|
||||
subscribeToMyOwnCardsByDefault: 'Abonnez-vous à mes propres cartes par défaut',
|
||||
taskActions_title: 'Actions de tâche',
|
||||
tasks: 'Tâches',
|
||||
time: 'Temps',
|
||||
timer: 'Minuteur',
|
||||
title: 'Titre',
|
||||
userActions_title: "Actions de l'utilisateur",
|
||||
userAddedThisCardToList: '<0> {{user}} </0> <1> a ajouté cette carte à {{list}} </1>',
|
||||
@@ -172,7 +172,7 @@ export default {
|
||||
editDescription_title: 'Éditer la description',
|
||||
editEmail_title: "Modifier l'e-mail",
|
||||
editPassword_title: 'Modifier le mot de passe',
|
||||
editTimer_title: 'Modifier la minuterie',
|
||||
editStopwatch_title: 'Modifier la minuterie',
|
||||
editTitle_title: 'Modifier le titre',
|
||||
editUsername_title: "Modifier le nom d'utilisateur",
|
||||
logOut_title: 'Se déconnecter',
|
||||
|
||||
@@ -79,7 +79,7 @@ export default {
|
||||
editLabel_title: 'Modifica Etichetta',
|
||||
editPassword_title: 'Modifica Password',
|
||||
editPermissions_title: 'Modifica Permessi',
|
||||
editTimer_title: 'Modifica Timer',
|
||||
editStopwatch_title: 'Modifica Timer',
|
||||
editUsername_title: 'Modifica Username',
|
||||
email: 'E-mail',
|
||||
emailAlreadyInUse: 'E-mail già in uso',
|
||||
@@ -135,13 +135,13 @@ export default {
|
||||
selectPermissions_title: 'Seleziona Permessi',
|
||||
selectProject: 'Seleziona progetto',
|
||||
settings: 'Impostazioni',
|
||||
stopwatch: 'Timer',
|
||||
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>',
|
||||
@@ -198,7 +198,7 @@ export default {
|
||||
editInformation_title: 'Modifca Informazioni',
|
||||
editPassword_title: 'Modifica Password',
|
||||
editPermissions: 'Modifica permessi',
|
||||
editTimer_title: 'Modifica Timer',
|
||||
editStopwatch_title: 'Modifica Timer',
|
||||
editTitle_title: 'Modifica Titolo',
|
||||
editUsername_title: 'Modifica Username',
|
||||
hideDetails: 'Nascondi dettagli',
|
||||
|
||||
@@ -76,7 +76,7 @@ export default {
|
||||
editEmail_title: 'Eメールを編集',
|
||||
editLabel_title: 'ラベルを編集',
|
||||
editPassword_title: 'パスワードを編集',
|
||||
editTimer_title: 'タイマーを編集',
|
||||
editStopwatch_title: 'タイマーを編集',
|
||||
editUsername_title: 'ユーザー名を編集',
|
||||
email: 'Eメール',
|
||||
emailAlreadyInUse: 'Eメールは既に使われています',
|
||||
@@ -127,11 +127,11 @@ export default {
|
||||
selectList: 'リストを選択',
|
||||
selectProject: 'プロジェクトを選択',
|
||||
settings: '設定',
|
||||
stopwatch: 'タイマー',
|
||||
subscribeToMyOwnCardsByDefault: '自分のカードをデフォルトで購読する',
|
||||
taskActions_title: 'タスクのアクション',
|
||||
tasks: 'タスク',
|
||||
time: '時間',
|
||||
timer: 'タイマー',
|
||||
title: 'タイトル',
|
||||
userActions_title: 'ユーザーのアクション',
|
||||
userAddedThisCardToList: '<0>{{user}}</0> は <1>{{list}} をこのカードに追加しました</1>',
|
||||
@@ -185,7 +185,7 @@ export default {
|
||||
editDescription_title: '説明を編集',
|
||||
editEmail_title: 'Eメールを編集',
|
||||
editPassword_title: 'パスワードの編集',
|
||||
editTimer_title: 'タイマーの編集',
|
||||
editStopwatch_title: 'タイマーの編集',
|
||||
editTitle_title: 'タイトルの編集',
|
||||
editUsername_title: 'ユーザー名の編集',
|
||||
leaveBoard: 'ボードから退出',
|
||||
|
||||
@@ -78,7 +78,7 @@ export default {
|
||||
editInformation_title: '정보 수정',
|
||||
editLabel_title: '라벨 수정',
|
||||
editPassword_title: '비밀번호 수정',
|
||||
editTimer_title: '타이머 수정',
|
||||
editStopwatch_title: '타이머 수정',
|
||||
editUsername_title: '사용자이름 수정',
|
||||
email: '메일',
|
||||
emailAlreadyInUse: '기 가입된 이메일이 존재합니다.',
|
||||
@@ -130,13 +130,13 @@ export default {
|
||||
selectList: '리스트 선택',
|
||||
selectProject: '프로젝트 선택',
|
||||
settings: '설정',
|
||||
stopwatch: '활동시간',
|
||||
subscribeToMyOwnCardsByDefault: '내 항목카드는 기본으로 구독',
|
||||
taskActions_title: '태스크 활동',
|
||||
tasks: '태스크',
|
||||
thereIsNoPreviewAvailableForThisAttachment:
|
||||
'이 첨부 파일에 사용할 수 있는 미리보기가 없습니다.',
|
||||
time: '시간',
|
||||
timer: '활동시간',
|
||||
title: '제목',
|
||||
userActions_title: '',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1>이 카드를 {{list}}</1>에 추가했습니다.',
|
||||
@@ -190,7 +190,7 @@ export default {
|
||||
editEmail_title: '메일 수정',
|
||||
editInformation_title: '정보 수정',
|
||||
editPassword_title: '패스워드 수정',
|
||||
editTimer_title: '시간 수정',
|
||||
editStopwatch_title: '시간 수정',
|
||||
editTitle_title: '제목 수정',
|
||||
editUsername_title: '이름 수정',
|
||||
leaveBoard: '보드에서 나가기',
|
||||
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
editEmail_title: 'Edytuj E-mail',
|
||||
editLabel_title: 'Edytuj Oznaczenie',
|
||||
editPassword_title: 'Edytuj Hasło',
|
||||
editTimer_title: 'Edytuj Odliczanie',
|
||||
editStopwatch_title: 'Edytuj Odliczanie',
|
||||
editUsername_title: 'Edytuj Nazwę Użytkownika',
|
||||
email: 'E-mail',
|
||||
emailAlreadyInUse: 'E-mail jest już używany',
|
||||
@@ -115,11 +115,11 @@ export default {
|
||||
selectList: 'Wybierz listę',
|
||||
selectProject: 'Wybierz projekt',
|
||||
settings: 'Ustawienia',
|
||||
stopwatch: 'Odliczanie',
|
||||
subscribeToMyOwnCardsByDefault: 'Zapisz mnie do moich kart automatycznie',
|
||||
taskActions_title: 'Akcje Zadania',
|
||||
tasks: 'Zadania',
|
||||
time: 'Czas',
|
||||
timer: 'Odliczanie',
|
||||
title: 'Tytuł',
|
||||
userActions_title: 'Akcje użytkownika',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> dodał tę kartę w {{list}}</1>',
|
||||
@@ -172,7 +172,7 @@ export default {
|
||||
editDescription_title: 'Edytuj Opis',
|
||||
editEmail_title: 'Edytuj E-mail',
|
||||
editPassword_title: 'Edytuj Hasło',
|
||||
editTimer_title: 'Edytuj Odliczanie',
|
||||
editStopwatch_title: 'Edytuj Odliczanie',
|
||||
editTitle_title: 'Edytuj Tytuł',
|
||||
editUsername_title: 'Edytuj Nazwę Użytkownika',
|
||||
logOut_title: 'Wyloguj',
|
||||
|
||||
@@ -77,7 +77,7 @@ export default {
|
||||
editLabel: 'Изменения метки',
|
||||
editPassword: 'Изменение пароля',
|
||||
editPermissions: 'Редактирование разрешений',
|
||||
editTimer: 'Изменение таймера',
|
||||
editStopwatch: 'Изменение секундомера',
|
||||
editTitle: 'Изменение названия',
|
||||
editUsername: 'Изменение имени пользователя',
|
||||
email: 'E-mail',
|
||||
@@ -130,12 +130,12 @@ export default {
|
||||
selectPermissions: 'Выбор разрешений',
|
||||
selectProject: 'Выберите проект',
|
||||
settings: 'Настройки',
|
||||
stopwatch: 'Секундомер',
|
||||
subscribeToMyOwnCardsByDefault: 'По умолчанию подписаться на мои собственные карточки',
|
||||
taskActions: 'Действия с задачей',
|
||||
tasks: 'Задачи',
|
||||
thereIsNoPreviewAvailableForThisAttachment: 'Предпросмотр для этого вложения недоступен',
|
||||
time: 'Время',
|
||||
timer: 'Таймер',
|
||||
title: 'Название',
|
||||
userActions: 'Действия с пользователем',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> добавил(а) эту карточку в {{list}}</1>',
|
||||
@@ -187,8 +187,8 @@ export default {
|
||||
editEmail: 'Изменить e-mail',
|
||||
editPassword: 'Изменить пароль',
|
||||
editPermissions: 'Изменить разрешения',
|
||||
editStopwatch: 'Изменить секундомер',
|
||||
editTask: 'Изменить задачу',
|
||||
editTimer: 'Изменить таймер',
|
||||
editTitle: 'Изменить название',
|
||||
editUsername: 'Изменить имя пользователя',
|
||||
hideDetails: 'Скрыть подробности',
|
||||
|
||||
@@ -77,7 +77,7 @@ export default {
|
||||
editInformation_title: 'Upraviť informácie',
|
||||
editLabel_title: 'Upraviť štítok',
|
||||
editPassword_title: 'Upraviť heslo',
|
||||
editTimer_title: 'Upraviť časovač',
|
||||
editStopwatch_title: 'Upraviť časovač',
|
||||
editUsername_title: 'Upraviť používateľské meno',
|
||||
email: 'E-mail',
|
||||
emailAlreadyInUse: 'E-mail je už použitý',
|
||||
@@ -128,11 +128,11 @@ export default {
|
||||
selectList: 'Vybrať zoznam',
|
||||
selectProject: 'Vybrať projekt',
|
||||
settings: 'Nastavenia',
|
||||
stopwatch: 'Časovač',
|
||||
subscribeToMyOwnCardsByDefault: 'Predvolene odoberať vlastné karty',
|
||||
taskActions_title: 'Akcie na úlohe',
|
||||
tasks: 'Úlohy',
|
||||
time: 'Čas',
|
||||
timer: 'Časovač',
|
||||
title: 'Názov',
|
||||
userActions_title: 'Akcie na používateľovi',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> pridal kartu do {{list}}</1>',
|
||||
@@ -185,7 +185,7 @@ export default {
|
||||
editEmail_title: 'Upraviť e-mail',
|
||||
editInformation_title: 'Upraviť informácie',
|
||||
editPassword_title: 'Upraviť heslo',
|
||||
editTimer_title: 'Upraviť časovač',
|
||||
editStopwatch_title: 'Upraviť časovač',
|
||||
editTitle_title: 'Upraviť názov',
|
||||
editUsername_title: 'Upraviť používateľské meno',
|
||||
leaveBoard: 'Opustiť tabuľu',
|
||||
|
||||
@@ -78,7 +78,7 @@ export default {
|
||||
editEmail_title: 'Redigera E-mail',
|
||||
editLabel_title: 'Redigera Etikett',
|
||||
editPassword_title: 'Redigera Lösenord',
|
||||
editTimer_title: 'Redigera Timer',
|
||||
editStopwatch_title: 'Redigera Timer',
|
||||
editUsername_title: 'Redigera Användarnamn',
|
||||
email: 'E-mail',
|
||||
emailAlreadyInUse: 'E-mail används redan',
|
||||
@@ -129,11 +129,11 @@ export default {
|
||||
selectList: 'Välj lista',
|
||||
selectProject: 'Välj projekt',
|
||||
settings: 'Inställningar',
|
||||
stopwatch: 'Timer',
|
||||
subscribeToMyOwnCardsByDefault: 'Prenumerera på mina egna kort som standard',
|
||||
taskActions_title: 'Uppgiftsåtgärder',
|
||||
tasks: 'Uppgifter',
|
||||
time: 'Tid',
|
||||
timer: 'Timer',
|
||||
title: 'Titel',
|
||||
userActions_title: 'Användaråtgärder',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> lade till detta kort i {{list}}</1>',
|
||||
@@ -186,7 +186,7 @@ export default {
|
||||
editDescription_title: 'Redigera Beskrivning',
|
||||
editEmail_title: 'Redigera E-mail',
|
||||
editPassword_title: 'Redigera Lösenord',
|
||||
editTimer_title: 'Redigera Timer',
|
||||
editStopwatch_title: 'Redigera Timer',
|
||||
editTitle_title: 'Redigera Titel',
|
||||
editUsername_title: 'Redigera Användarnamn',
|
||||
leaveBoard: 'Lämna tavla',
|
||||
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
editEmail_title: 'E-mail ni Tahrirlash',
|
||||
editLabel_title: 'Yorliqni Tahrirlash',
|
||||
editPassword_title: 'Parolni Tahrirlash',
|
||||
editTimer_title: 'Taymerni Tahrirlash',
|
||||
editStopwatch_title: 'Taymerni Tahrirlash',
|
||||
editUsername_title: 'Foydalanuvchi nomini Tahrirlash',
|
||||
email: 'E-mail',
|
||||
emailAlreadyInUse: 'E-mail allaqachon mavjud',
|
||||
@@ -122,11 +122,11 @@ export default {
|
||||
selectList: "Ro'yxatni tanlash",
|
||||
selectProject: "Loyihani '",
|
||||
settings: 'Sozlamalar',
|
||||
stopwatch: 'Taymer',
|
||||
subscribeToMyOwnCardsByDefault: "Odatiy holda o'z kartalarimga obuna bo'ling",
|
||||
taskActions_title: 'Vazifa Amallari',
|
||||
tasks: 'Vazifalar',
|
||||
time: 'Vaqt',
|
||||
timer: 'Taymer',
|
||||
title: 'Sarlavha',
|
||||
userActions_title: 'Foydalanuvchi Amallari',
|
||||
userAddedThisCardToList: "<1>Ushbu kartani {{list}} ga</1><0>{{user}}</0> qo'shdi",
|
||||
@@ -179,7 +179,7 @@ export default {
|
||||
editDescription_title: 'Izohni Tahrirlash',
|
||||
editEmail_title: 'E-mailni Tahrirlash',
|
||||
editPassword_title: 'Parolni Tahrirlash',
|
||||
editTimer_title: 'Taymerni Tahrirlash',
|
||||
editStopwatch_title: 'Taymerni Tahrirlash',
|
||||
editTitle_title: 'Sarlavhani Tahrirlash',
|
||||
editUsername_title: 'Foydalanuvchi nomini Tahrirlash',
|
||||
leaveBoard: 'Doskadan chiqish',
|
||||
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
editEmail_title: '编辑邮箱',
|
||||
editLabel_title: '编辑标签',
|
||||
editPassword_title: '修改密码',
|
||||
editTimer_title: '修改时间',
|
||||
editStopwatch_title: '修改时间',
|
||||
editUsername_title: '修改用户名',
|
||||
email: '邮箱',
|
||||
emailAlreadyInUse: '邮箱已使用',
|
||||
@@ -120,11 +120,11 @@ export default {
|
||||
selectList: '选择列表',
|
||||
selectProject: '选择项目',
|
||||
settings: '设置',
|
||||
stopwatch: '计时器',
|
||||
subscribeToMyOwnCardsByDefault: '默认关注自己创建的卡片',
|
||||
taskActions_title: '任务操作',
|
||||
tasks: '任务',
|
||||
time: '时间',
|
||||
timer: '计时器',
|
||||
title: '标题',
|
||||
userActions_title: '用户操作',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> 向列表 {{list}} 添加了该卡片</1>',
|
||||
@@ -177,7 +177,7 @@ export default {
|
||||
editDescription_title: '编辑描述',
|
||||
editEmail_title: '编辑邮箱',
|
||||
editPassword_title: '编辑密码',
|
||||
editTimer_title: '编辑实践',
|
||||
editStopwatch_title: '编辑实践',
|
||||
editTitle_title: '编辑标题',
|
||||
editUsername_title: '编辑用户名',
|
||||
leaveBoard: '离开面板',
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class extends BaseModel {
|
||||
name: attr(),
|
||||
description: attr(),
|
||||
dueDate: attr(),
|
||||
timer: attr(),
|
||||
stopwatch: attr(),
|
||||
isSubscribed: attr({
|
||||
getDefault: () => false,
|
||||
}),
|
||||
|
||||
@@ -6,28 +6,28 @@ const getFullSeconds = ({ startedAt, total }) => {
|
||||
return total;
|
||||
};
|
||||
|
||||
export const createTimer = ({ hours, minutes, seconds }) => ({
|
||||
export const createStopwatch = ({ hours, minutes, seconds }) => ({
|
||||
startedAt: null,
|
||||
total: hours * 60 * 60 + minutes * 60 + seconds,
|
||||
});
|
||||
|
||||
export const updateTimer = ({ startedAt }, parts) => ({
|
||||
...createTimer(parts),
|
||||
export const updateStopwatch = ({ startedAt }, parts) => ({
|
||||
...createStopwatch(parts),
|
||||
startedAt: startedAt && new Date(),
|
||||
});
|
||||
|
||||
export const startTimer = (timer) => ({
|
||||
export const startStopwatch = (stopwatch) => ({
|
||||
startedAt: new Date(),
|
||||
total: timer ? timer.total : 0,
|
||||
total: stopwatch ? stopwatch.total : 0,
|
||||
});
|
||||
|
||||
export const stopTimer = (timer) => ({
|
||||
export const stopStopwatch = (stopwatch) => ({
|
||||
startedAt: null,
|
||||
total: getFullSeconds(timer),
|
||||
total: getFullSeconds(stopwatch),
|
||||
});
|
||||
|
||||
export const getTimerParts = (timer) => {
|
||||
const fullSeconds = getFullSeconds(timer);
|
||||
export const getStopwatchParts = (stopwatch) => {
|
||||
const fullSeconds = getFullSeconds(stopwatch);
|
||||
|
||||
const hours = Math.floor(fullSeconds / 3600);
|
||||
const minutes = Math.floor((fullSeconds - hours * 3600) / 60);
|
||||
@@ -40,8 +40,8 @@ export const getTimerParts = (timer) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const formatTimer = (timer) => {
|
||||
const { hours, minutes, seconds } = getTimerParts(timer);
|
||||
export const formatStopwatch = (stopwatch) => {
|
||||
const { hours, minutes, seconds } = getStopwatchParts(stopwatch);
|
||||
|
||||
return [hours, ...[minutes, seconds].map((part) => (part < 10 ? `0${part}` : part))].join(':');
|
||||
};
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "planka",
|
||||
"version": "1.10.2",
|
||||
"version": "1.11.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "planka",
|
||||
"version": "1.10.2",
|
||||
"version": "1.11.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planka",
|
||||
"version": "1.10.2",
|
||||
"version": "1.11.0",
|
||||
"private": true,
|
||||
"homepage": "https://plankanban.github.io/planka",
|
||||
"repository": {
|
||||
|
||||
@@ -14,7 +14,7 @@ const Errors = {
|
||||
|
||||
const dueDateValidator = (value) => moment(value, moment.ISO_8601, true).isValid();
|
||||
|
||||
const timerValidator = (value) => {
|
||||
const stopwatchValidator = (value) => {
|
||||
if (!_.isPlainObject(value) || _.size(value) !== 2) {
|
||||
return false;
|
||||
}
|
||||
@@ -57,9 +57,9 @@ module.exports = {
|
||||
type: 'string',
|
||||
custom: dueDateValidator,
|
||||
},
|
||||
timer: {
|
||||
stopwatch: {
|
||||
type: 'json',
|
||||
custom: timerValidator,
|
||||
custom: stopwatchValidator,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -95,7 +95,7 @@ module.exports = {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
const values = _.pick(inputs, ['position', 'name', 'description', 'dueDate', 'timer']);
|
||||
const values = _.pick(inputs, ['position', 'name', 'description', 'dueDate', 'stopwatch']);
|
||||
|
||||
const card = await sails.helpers.cards.createOne
|
||||
.with({
|
||||
|
||||
@@ -23,7 +23,7 @@ const Errors = {
|
||||
|
||||
const dueDateValidator = (value) => moment(value, moment.ISO_8601, true).isValid();
|
||||
|
||||
const timerValidator = (value) => {
|
||||
const stopwatchValidator = (value) => {
|
||||
if (!_.isPlainObject(value) || _.size(value) !== 2) {
|
||||
return false;
|
||||
}
|
||||
@@ -80,9 +80,9 @@ module.exports = {
|
||||
custom: dueDateValidator,
|
||||
allowNull: true,
|
||||
},
|
||||
timer: {
|
||||
stopwatch: {
|
||||
type: 'json',
|
||||
custom: timerValidator,
|
||||
custom: stopwatchValidator,
|
||||
},
|
||||
isSubscribed: {
|
||||
type: 'boolean',
|
||||
@@ -171,7 +171,7 @@ module.exports = {
|
||||
'name',
|
||||
'description',
|
||||
'dueDate',
|
||||
'timer',
|
||||
'stopwatch',
|
||||
'isSubscribed',
|
||||
]);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ module.exports = {
|
||||
type: 'ref',
|
||||
columnName: 'due_date',
|
||||
},
|
||||
timer: {
|
||||
stopwatch: {
|
||||
type: 'json',
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports.up = (knex) =>
|
||||
knex.schema.table('card', (table) => {
|
||||
table.renameColumn('timer', 'stopwatch');
|
||||
});
|
||||
|
||||
module.exports.down = (knex) =>
|
||||
knex.schema.table('card', (table) => {
|
||||
table.renameColumn('stopwatch', 'timer');
|
||||
});
|
||||
Reference in New Issue
Block a user