Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2944206fac | ||
|
|
9fadb8978c | ||
|
|
1eba6917a5 | ||
|
|
5b0902ab3e | ||
|
|
50b1275d08 | ||
|
|
02d2a5d278 | ||
|
|
8fd4394b8e | ||
|
|
2bee32b79b | ||
|
|
31860d164b | ||
|
|
6f8ba52593 | ||
|
|
165e5c6e86 | ||
|
|
aeff1eb5d0 | ||
|
|
5eba8b7fda | ||
|
|
fa3bb3e803 | ||
|
|
22ad2360a5 | ||
|
|
c83244da31 | ||
|
|
2a6740a1f0 | ||
|
|
c32baac2c5 | ||
|
|
4d4f5a1f89 | ||
|
|
773e99e36e | ||
|
|
c55e811ba2 | ||
|
|
c63d5bea8c | ||
|
|
2e73b08ba2 | ||
|
|
d58674c56a | ||
|
|
0c7e8dc634 | ||
|
|
a750f5c7b6 | ||
|
|
759eb50b49 | ||
|
|
0010051149 | ||
|
|
915cce046e | ||
|
|
467c837589 | ||
|
|
b2c7261cd3 | ||
|
|
9c29656f23 | ||
|
|
c17ca9d5e8 | ||
|
|
c08ac75512 | ||
|
|
13e15a3954 | ||
|
|
1db2271e77 | ||
|
|
9a9596db90 | ||
|
|
a640c10270 | ||
|
|
8c3c53d3e0 | ||
|
|
913d79fabc | ||
|
|
ec74e38392 | ||
|
|
f5ef07f493 | ||
|
|
55b31459bf | ||
|
|
5b87537262 | ||
|
|
fb7dafba9b | ||
|
|
67fbd23cc4 | ||
|
|
b62f3adf3a | ||
|
|
0a57440fe2 | ||
|
|
afe6da7599 | ||
|
|
0114af1128 | ||
|
|
0cbf36f9e0 | ||
|
|
a9f1546741 | ||
|
|
76ca466725 | ||
|
|
b5a6179af7 | ||
|
|
0262213f8d | ||
|
|
a065c328f8 | ||
|
|
968708cd41 | ||
|
|
89411d4437 | ||
|
|
ce57fbc410 | ||
|
|
47eb32d841 | ||
|
|
8b5fdd05e1 | ||
|
|
ea742e2682 | ||
|
|
809ace238e | ||
|
|
c787454796 | ||
|
|
6a0c3c32d9 | ||
|
|
4ca45d33be | ||
|
|
ba160b9028 | ||
|
|
8a26e41e07 | ||
|
|
238456f931 | ||
|
|
ac1cf9c070 | ||
|
|
69ab041b2b | ||
|
|
fc7bc7d74c | ||
|
|
01c1afb329 |
@@ -1,29 +0,0 @@
|
||||
*/README.md
|
||||
*/.gitignore
|
||||
*/node_modules
|
||||
|
||||
**/.DS_Store
|
||||
|
||||
server/**/.gitkeep
|
||||
|
||||
server/.env
|
||||
server/.editorconfig
|
||||
server/.eslintignore
|
||||
server/.npmrc
|
||||
server/test
|
||||
server/logs
|
||||
server/.tmp
|
||||
|
||||
server/views/*
|
||||
|
||||
server/public/*
|
||||
!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
|
||||
@@ -1 +0,0 @@
|
||||
client/src/lib/custom-ui/styles.css linguist-vendored
|
||||
@@ -1,13 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: meltyshev
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -1,44 +0,0 @@
|
||||
name: Build and push Docker base image
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- ./Dockerfile.base
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
ALPINE_VERSION: 3.16
|
||||
|
||||
jobs:
|
||||
build-and-push-docker-base-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
file: Dockerfile.base
|
||||
build-args: ALPINE_VERSION=${{ env.ALPINE_VERSION }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/plankanban/planka:base-latest
|
||||
ghcr.io/plankanban/planka:base-${{ env.ALPINE_VERSION }}
|
||||
@@ -1,42 +0,0 @@
|
||||
name: Build and push Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
build-and-push-docker-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set version
|
||||
uses: actions/github-script@v6
|
||||
id: set-version
|
||||
with:
|
||||
result-encoding: string
|
||||
script: return context.payload.release.tag_name.replace('v', '')
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/plankanban/planka:latest
|
||||
ghcr.io/plankanban/planka:${{ steps.set-version.outputs.result }}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
node_modules
|
||||
docker-compose.override.yml
|
||||
|
||||
.idea
|
||||
.DS_Store
|
||||
|
||||
# Prevent another lockfile than package-lock.json (npm) from being created
|
||||
# If some case you are using pnpm or yarn, don't forget to generate npm lockfile
|
||||
# before commiting your code by running:
|
||||
# `npm i --package-lock-only`
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
Vendored
-18
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnSave": true,
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimFinalNewlines": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"eslint.format.enable": true,
|
||||
"eslint.workingDirectories": [
|
||||
"./client",
|
||||
"./server"
|
||||
],
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at contact@plankanban.org. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@@ -1,49 +0,0 @@
|
||||
# Contributing to Planka
|
||||
|
||||
First off, thanks for taking the time to contribute!
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project and everyone participating in it is governed by the [Planka Code of Conduct](https://github.com/plankanban/planka/blob/master/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code.
|
||||
|
||||
## How Can I Contribute?
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
Feel free to create a bug report as a new issue on GitHub. Before creating, please check if there is already existing one. When creating a bug report, please include as many details as possible.
|
||||
|
||||
### Suggesting Enhancements
|
||||
|
||||
Feel free to create an enhancement suggestion as a new issue on GitHub. Before creating, please check if there is already existing one. When creating an enhancement suggestion, please include as many details as possible.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
Before submitting a pull request please discuss with the core team by creating or commenting in an issue on GitHub – we'd also love to hear from you in the discussions. This way we can ensure that an approach is agreed on before code is written. This will result in a much higher liklihood of your code being accepted.
|
||||
|
||||
If you’re looking for ways to get started, here's a list of ways to help us improve Planka:
|
||||
|
||||
- [Translation](https://github.com/plankanban/planka/issues/66) into other languages
|
||||
- Issues with [`good first issue`](https://github.com/plankanban/planka/labels/good%20first%20issue) label
|
||||
- Performance improvements, both on client and server
|
||||
- Developer happiness and documentation
|
||||
- Bugs and other issues listed on GitHub
|
||||
|
||||
## Styleguides
|
||||
|
||||
### Git Commit Messages
|
||||
|
||||
Commit messages should follow the [commit message convention](https://conventionalcommits.org), so changelogs could be generated automatically by that.
|
||||
|
||||
Additional rules:
|
||||
|
||||
- Separate subject from body with a blank line
|
||||
- Limit the subject line to 70 characters
|
||||
- Capitalize the subject line
|
||||
- Do not end the subject line with a period
|
||||
- Use the imperative mood in the subject line
|
||||
- Use the body to explain what and why vs. how
|
||||
- Each commit should be a single, stable change
|
||||
|
||||
### JavaScript
|
||||
|
||||
All JavaScript code should follow this [JavaScript style guide](https://github.com/airbnb/javascript). The pre-commit hook will help you find linting errors before committing.
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
FROM ghcr.io/plankanban/planka:base-latest as server-dependencies
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY server/package.json server/package-lock.json .
|
||||
|
||||
RUN npm install npm@latest --global \
|
||||
&& npm clean-install --omit=dev
|
||||
|
||||
FROM node:lts AS client
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY client/package.json client/package-lock.json .
|
||||
|
||||
RUN npm install npm@latest --global \
|
||||
&& npm clean-install --omit=dev
|
||||
|
||||
COPY client .
|
||||
RUN DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
|
||||
FROM ghcr.io/plankanban/planka:base-latest
|
||||
|
||||
RUN apk del vips-dependencies --purge
|
||||
|
||||
USER node
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=node:node docker-start.sh start.sh
|
||||
COPY --chown=node:node server .
|
||||
|
||||
RUN mv .env.sample .env
|
||||
|
||||
COPY --from=server-dependencies --chown=node:node /app/node_modules node_modules
|
||||
|
||||
COPY --from=client --chown=node:node /app/build public
|
||||
COPY --from=client --chown=node:node /app/build/index.html views/index.ejs
|
||||
|
||||
VOLUME /app/public/user-avatars
|
||||
VOLUME /app/public/project-background-images
|
||||
VOLUME /app/private/attachments
|
||||
|
||||
EXPOSE 1337
|
||||
|
||||
CMD ["./start.sh"]
|
||||
@@ -1,27 +0,0 @@
|
||||
FROM node:lts-alpine
|
||||
|
||||
ARG ALPINE_VERSION=3.16
|
||||
ARG VIPS_VERSION=8.13.3
|
||||
|
||||
RUN apk -U upgrade \
|
||||
&& apk add \
|
||||
bash giflib glib lcms2 libexif \
|
||||
libgsf libjpeg-turbo libpng librsvg libwebp \
|
||||
orc pango tiff \
|
||||
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
|
||||
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
|
||||
--no-cache \
|
||||
&& apk add \
|
||||
build-base giflib-dev glib-dev lcms2-dev libexif-dev \
|
||||
libgsf-dev libjpeg-turbo-dev libpng-dev librsvg-dev libwebp-dev \
|
||||
orc-dev pango-dev tiff-dev \
|
||||
--virtual vips-dependencies \
|
||||
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
|
||||
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
|
||||
--no-cache \
|
||||
&& wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.gz | tar xzC /tmp \
|
||||
&& cd /tmp/vips-${VIPS_VERSION} \
|
||||
&& ./configure \
|
||||
&& make \
|
||||
&& make install-strip \
|
||||
&& rm -rf /tmp/vips-${VIPS_VERSION}
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Maksim Eltyshev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,305 +0,0 @@
|
||||
# Planka
|
||||
|
||||
  
|
||||
|
||||
A Trello-like kanban board built with React and Redux.
|
||||
|
||||

|
||||
|
||||
[**Client demo**](https://plankanban.github.io/planka) (without server features).
|
||||
|
||||
## Features
|
||||
|
||||
- Create projects, boards, lists, cards, labels and tasks
|
||||
- Add card members, track time, set a due date, add attachments, write comments
|
||||
- Markdown support in a card description and comment
|
||||
- Filter by members and labels
|
||||
- Customize project background
|
||||
- Real-time updates
|
||||
- User notifications
|
||||
- Internationalization
|
||||
|
||||
## Deploy
|
||||
|
||||
There are 2 types of installation:
|
||||
1. [Dockerized](#1-docker-compose)
|
||||
2. [Without Docker](#2-without-docker)
|
||||
|
||||
|
||||
### 1. Docker Compose
|
||||
|
||||
[](https://github.com/plankanban/planka/pkgs/container/planka)
|
||||
|
||||
- Make sure you have [Docker](https://docs.docker.com/install/) and [Docker Compose](https://docs.docker.com/compose/install/) installed and operational.
|
||||
- Create `docker-compose.yml` based on [the example](https://raw.githubusercontent.com/plankanban/planka/master/docker-compose.yml). This is the ONLY file you will need. You can create this file on your own machine by copy and pasting the content.
|
||||
- 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`.
|
||||
|
||||
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 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
|
||||
|
||||
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
|
||||
|
||||
## Tech stack
|
||||
|
||||
- React, Redux, Redux-Saga, Redux-ORM, Semantic UI React, react-beautiful-dnd
|
||||
- Sails.js, Knex.js
|
||||
- PostgreSQL
|
||||
|
||||
## License
|
||||
|
||||
Planka is [MIT licensed](https://github.com/plankanban/planka/blob/master/LICENSE).
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
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.
|
||||
|
||||
**Do NOT create public issues on GitHub for security vulnerabilities.**
|
||||
|
||||
Thank you for your contribution!
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/planka/static/css/main.859cd79e.css",
|
||||
"main.js": "/planka/static/js/main.5081d4cc.js",
|
||||
"static/js/801.9773e58e.chunk.js": "/planka/static/js/801.9773e58e.chunk.js",
|
||||
"static/js/467.d02e66c6.chunk.js": "/planka/static/js/467.d02e66c6.chunk.js",
|
||||
"static/js/514.592fb053.chunk.js": "/planka/static/js/514.592fb053.chunk.js",
|
||||
"static/js/294.e0243dff.chunk.js": "/planka/static/js/294.e0243dff.chunk.js",
|
||||
"static/js/394.5692e060.chunk.js": "/planka/static/js/394.5692e060.chunk.js",
|
||||
"static/js/117.2c68642f.chunk.js": "/planka/static/js/117.2c68642f.chunk.js",
|
||||
"static/js/181.24dbd8ba.chunk.js": "/planka/static/js/181.24dbd8ba.chunk.js",
|
||||
"static/js/902.1af73487.chunk.js": "/planka/static/js/902.1af73487.chunk.js",
|
||||
"static/js/875.4756cbe5.chunk.js": "/planka/static/js/875.4756cbe5.chunk.js",
|
||||
"static/js/694.e5cf50a6.chunk.js": "/planka/static/js/694.e5cf50a6.chunk.js",
|
||||
"static/js/632.9315886b.chunk.js": "/planka/static/js/632.9315886b.chunk.js",
|
||||
"static/js/812.77d21c8d.chunk.js": "/planka/static/js/812.77d21c8d.chunk.js",
|
||||
"static/js/215.bb7d98af.chunk.js": "/planka/static/js/215.bb7d98af.chunk.js",
|
||||
"static/js/683.e8a7c172.chunk.js": "/planka/static/js/683.e8a7c172.chunk.js",
|
||||
"static/media/big-picture.jpg": "/planka/static/media/big-picture.b78130bf7100f3416c1c.jpg",
|
||||
"static/media/cover.jpg": "/planka/static/media/cover.a3c21cab987867165c62.jpg",
|
||||
"static/media/simple-picture.jpg": "/planka/static/media/simple-picture.0442003259f074c6cc5d.jpg",
|
||||
"static/media/brand-icons.svg": "/planka/static/media/brand-icons.6729d29753e000c17489.svg",
|
||||
"static/media/simple-pdf.pdf": "/planka/static/media/simple-pdf.2b466d228796d7c86d15.pdf",
|
||||
"static/media/fontawesome-webfont.svg?v=4.7.0": "/planka/static/media/fontawesome-webfont.c1e38fd9e0e74ba58f7a.svg",
|
||||
"static/media/icons.svg": "/planka/static/media/icons.62d9dae4e0040e81c980.svg",
|
||||
"static/media/original.jpg": "/planka/static/media/original.fd3fb2a1d11429b0ec28.jpg",
|
||||
"static/media/high-picture.jpg": "/planka/static/media/high-picture.28d94c085a2a9ac5ccf3.jpg",
|
||||
"static/media/fontawesome-webfont.eot": "/planka/static/media/fontawesome-webfont.8b43027f47b20503057d.eot",
|
||||
"static/media/fontawesome-webfont.ttf?v=4.7.0": "/planka/static/media/fontawesome-webfont.1e59d2330b4c6deb84b3.ttf",
|
||||
"static/media/outline-icons.svg": "/planka/static/media/outline-icons.9c4845b4b41ef40a22fa.svg",
|
||||
"static/media/icons.eot": "/planka/static/media/icons.a01e3f2d6c83dc3aee17.eot",
|
||||
"static/media/icons.ttf": "/planka/static/media/icons.c656b8caa454ed19b9a2.ttf",
|
||||
"static/media/cover-256.jpg": "/planka/static/media/cover-256.4acd3319c6762c5d1b4b.jpg",
|
||||
"static/media/brand-icons.eot": "/planka/static/media/brand-icons.d68fa3e67dbb653a13ce.eot",
|
||||
"static/media/brand-icons.ttf": "/planka/static/media/brand-icons.65a2fb6d9aaa164b41a0.ttf",
|
||||
"static/media/fontawesome-webfont.woff?v=4.7.0": "/planka/static/media/fontawesome-webfont.f691f37e57f04c152e23.woff",
|
||||
"static/media/cover-336.jpg": "/planka/static/media/cover-336.d1a186776f06837a4ec3.jpg",
|
||||
"static/media/fontawesome-webfont.woff2?v=4.7.0": "/planka/static/media/fontawesome-webfont.20fd1704ea223900efa9.woff2",
|
||||
"static/media/brand-icons.woff": "/planka/static/media/brand-icons.cac87dc00c87a5d74711.woff",
|
||||
"static/media/brand-icons.woff2": "/planka/static/media/brand-icons.278156e41e0ad908cf7f.woff2",
|
||||
"static/media/marked.cjs": "/planka/static/media/marked.2244abd198e53bc0b3d6.cjs",
|
||||
"static/media/icons.woff": "/planka/static/media/icons.425399f81e4ce7cbd967.woff",
|
||||
"static/media/Nunitoga-Medium.woff2": "/planka/static/media/Nunitoga-Medium.bae369ea6c0c9ad31db6.woff2",
|
||||
"static/media/Nunitoga-BoldItalic.woff2": "/planka/static/media/Nunitoga-BoldItalic.06ec745f47d8c9431b42.woff2",
|
||||
"static/media/Nunitoga-MediumItalic.woff2": "/planka/static/media/Nunitoga-MediumItalic.5ce0915188332f04e46d.woff2",
|
||||
"static/media/Nunitoga-LightItalic.woff2": "/planka/static/media/Nunitoga-LightItalic.f1b251d4faa9826b3ea2.woff2",
|
||||
"static/media/Nunitoga-Bold.woff2": "/planka/static/media/Nunitoga-Bold.46fd9a43c89bf2b6a1fe.woff2",
|
||||
"static/media/Nunitoga-Light.woff2": "/planka/static/media/Nunitoga-Light.e9d89c979b64722f34ec.woff2",
|
||||
"static/media/icons.woff2": "/planka/static/media/icons.38c6d8bab26db77d8c80.woff2",
|
||||
"static/media/outline-icons.eot": "/planka/static/media/outline-icons.752905fa5edf21fc52a1.eot",
|
||||
"static/media/outline-icons.ttf": "/planka/static/media/outline-icons.5367103510b27b784827.ttf",
|
||||
"static/media/flags.png": "/planka/static/media/flags.99f63ae7a743f21ab308.png",
|
||||
"static/media/logo.png": "/planka/static/media/logo.f8c2b962d0f2bd7f46cb.png",
|
||||
"static/media/outline-icons.woff": "/planka/static/media/outline-icons.ddae9b1ba9b0b42f5880.woff",
|
||||
"static/media/outline-icons.woff2": "/planka/static/media/outline-icons.687a4990ea22bb1a49d4.woff2",
|
||||
"index.html": "/planka/index.html",
|
||||
"static/media/plus-icon.svg": "/planka/static/media/plus-icon.e46c682424b8c98c4f7bd0f367a260c2.svg",
|
||||
"static/media/plus-math-icon.svg": "/planka/static/media/plus-math-icon.cfa58840e75860dcf79a0b21a1cca781.svg",
|
||||
"main.859cd79e.css.map": "/planka/static/css/main.859cd79e.css.map",
|
||||
"main.5081d4cc.js.map": "/planka/static/js/main.5081d4cc.js.map",
|
||||
"801.9773e58e.chunk.js.map": "/planka/static/js/801.9773e58e.chunk.js.map",
|
||||
"467.d02e66c6.chunk.js.map": "/planka/static/js/467.d02e66c6.chunk.js.map",
|
||||
"514.592fb053.chunk.js.map": "/planka/static/js/514.592fb053.chunk.js.map",
|
||||
"294.e0243dff.chunk.js.map": "/planka/static/js/294.e0243dff.chunk.js.map",
|
||||
"394.5692e060.chunk.js.map": "/planka/static/js/394.5692e060.chunk.js.map",
|
||||
"117.2c68642f.chunk.js.map": "/planka/static/js/117.2c68642f.chunk.js.map",
|
||||
"181.24dbd8ba.chunk.js.map": "/planka/static/js/181.24dbd8ba.chunk.js.map",
|
||||
"902.1af73487.chunk.js.map": "/planka/static/js/902.1af73487.chunk.js.map",
|
||||
"875.4756cbe5.chunk.js.map": "/planka/static/js/875.4756cbe5.chunk.js.map",
|
||||
"694.e5cf50a6.chunk.js.map": "/planka/static/js/694.e5cf50a6.chunk.js.map",
|
||||
"632.9315886b.chunk.js.map": "/planka/static/js/632.9315886b.chunk.js.map",
|
||||
"812.77d21c8d.chunk.js.map": "/planka/static/js/812.77d21c8d.chunk.js.map",
|
||||
"215.bb7d98af.chunk.js.map": "/planka/static/js/215.bb7d98af.chunk.js.map",
|
||||
"683.e8a7c172.chunk.js.map": "/planka/static/js/683.e8a7c172.chunk.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.859cd79e.css",
|
||||
"static/js/main.5081d4cc.js"
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -1 +0,0 @@
|
||||
# Planka client
|
||||
@@ -1,56 +0,0 @@
|
||||
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
-40665
File diff suppressed because it is too large
Load Diff
@@ -1,116 +0,0 @@
|
||||
{
|
||||
"name": "planka-client",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "react-app-rewired build",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint --ext js,jsx src config-overrides.js",
|
||||
"start": "react-app-rewired start",
|
||||
"test": "react-app-rewired test"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"jest": true
|
||||
},
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parserOptions": {
|
||||
"babelOptions": {
|
||||
"presets": [
|
||||
"airbnb"
|
||||
]
|
||||
},
|
||||
"requireConfigFile": false
|
||||
},
|
||||
"extends": [
|
||||
"airbnb",
|
||||
"airbnb/hooks",
|
||||
"../package.json"
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-vars": "warn",
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{
|
||||
"devDependencies": [
|
||||
"src/setupTests.js",
|
||||
"**/*.test.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"classnames": "^2.3.2",
|
||||
"date-fns": "^2.29.3",
|
||||
"dequal": "^2.0.3",
|
||||
"easymde": "^2.18.0",
|
||||
"history": "^5.3.0",
|
||||
"i18next": "^22.0.6",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"initials": "^3.1.2",
|
||||
"js-cookie": "^3.0.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"node-sass": "^8.0.0",
|
||||
"photoswipe": "^5.3.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-datepicker": "^4.8.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-i18next": "^12.0.0",
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-photoswipe-gallery": "^2.2.2",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router": "^6.4.3",
|
||||
"react-router-dom": "^6.4.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-simplemde-editor": "^5.2.0",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
"redux": "^4.2.0",
|
||||
"redux-first-history": "^5.1.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-orm": "^0.16.2",
|
||||
"redux-saga": "^1.2.1",
|
||||
"remark-breaks": "^3.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"reselect": "^4.1.7",
|
||||
"sails.io.js": "^1.2.1",
|
||||
"semantic-ui-react": "^2.1.3",
|
||||
"socket.io-client": "^2.5.0",
|
||||
"validator": "^13.7.0",
|
||||
"whatwg-fetch": "^3.6.2",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"babel-preset-airbnb": "^5.0.0",
|
||||
"chai": "^4.3.7",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"react-test-renderer": "^18.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Planka is an open source project management software"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Planka</title>
|
||||
</head>
|
||||
<script>window.BASE_URL = "%PUBLIC_URL%";</script>
|
||||
<body id="app">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,79 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const fetchActivities = (cardId) => ({
|
||||
type: ActionTypes.ACTIVITIES_FETCH,
|
||||
payload: {
|
||||
cardId,
|
||||
},
|
||||
});
|
||||
|
||||
fetchActivities.success = (cardId, activities, users) => ({
|
||||
type: ActionTypes.ACTIVITIES_FETCH__SUCCESS,
|
||||
payload: {
|
||||
cardId,
|
||||
activities,
|
||||
users,
|
||||
},
|
||||
});
|
||||
|
||||
fetchActivities.failure = (cardId, error) => ({
|
||||
type: ActionTypes.ACTIVITIES_FETCH__FAILURE,
|
||||
payload: {
|
||||
cardId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const toggleActivitiesDetails = (cardId, isVisible) => ({
|
||||
type: ActionTypes.ACTIVITIES_DETAILS_TOGGLE,
|
||||
payload: {
|
||||
cardId,
|
||||
isVisible,
|
||||
},
|
||||
});
|
||||
|
||||
toggleActivitiesDetails.success = (cardId, activities, users) => ({
|
||||
type: ActionTypes.ACTIVITIES_DETAILS_TOGGLE__SUCCESS,
|
||||
payload: {
|
||||
cardId,
|
||||
activities,
|
||||
users,
|
||||
},
|
||||
});
|
||||
|
||||
toggleActivitiesDetails.failure = (cardId, error) => ({
|
||||
type: ActionTypes.ACTIVITIES_DETAILS_TOGGLE__FAILURE,
|
||||
payload: {
|
||||
cardId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleActivityCreate = (activity) => ({
|
||||
type: ActionTypes.ACTIVITY_CREATE_HANDLE,
|
||||
payload: {
|
||||
activity,
|
||||
},
|
||||
});
|
||||
|
||||
const handleActivityUpdate = (activity) => ({
|
||||
type: ActionTypes.ACTIVITY_UPDATE_HANDLE,
|
||||
payload: {
|
||||
activity,
|
||||
},
|
||||
});
|
||||
|
||||
const handleActivityDelete = (activity) => ({
|
||||
type: ActionTypes.ACTIVITY_DELETE_HANDLE,
|
||||
payload: {
|
||||
activity,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
fetchActivities,
|
||||
toggleActivitiesDetails,
|
||||
handleActivityCreate,
|
||||
handleActivityUpdate,
|
||||
handleActivityDelete,
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const createAttachment = (attachment) => ({
|
||||
type: ActionTypes.ATTACHMENT_CREATE,
|
||||
payload: {
|
||||
attachment,
|
||||
},
|
||||
});
|
||||
|
||||
createAttachment.success = (localId, attachment) => ({
|
||||
type: ActionTypes.ATTACHMENT_CREATE__SUCCESS,
|
||||
payload: {
|
||||
localId,
|
||||
attachment,
|
||||
},
|
||||
});
|
||||
|
||||
createAttachment.failure = (localId, error) => ({
|
||||
type: ActionTypes.ATTACHMENT_CREATE__FAILURE,
|
||||
payload: {
|
||||
localId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleAttachmentCreate = (attachment) => ({
|
||||
type: ActionTypes.ATTACHMENT_CREATE_HANDLE,
|
||||
payload: {
|
||||
attachment,
|
||||
},
|
||||
});
|
||||
|
||||
const updateAttachment = (id, data) => ({
|
||||
type: ActionTypes.ATTACHMENT_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
updateAttachment.success = (attachment) => ({
|
||||
type: ActionTypes.ATTACHMENT_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
attachment,
|
||||
},
|
||||
});
|
||||
|
||||
updateAttachment.failure = (id, error) => ({
|
||||
type: ActionTypes.ATTACHMENT_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleAttachmentUpdate = (attachment) => ({
|
||||
type: ActionTypes.ATTACHMENT_UPDATE_HANDLE,
|
||||
payload: {
|
||||
attachment,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteAttachment = (id) => ({
|
||||
type: ActionTypes.ATTACHMENT_DELETE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
deleteAttachment.success = (attachment) => ({
|
||||
type: ActionTypes.ATTACHMENT_DELETE__SUCCESS,
|
||||
payload: {
|
||||
attachment,
|
||||
},
|
||||
});
|
||||
|
||||
deleteAttachment.failure = (id, error) => ({
|
||||
type: ActionTypes.ATTACHMENT_DELETE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleAttachmentDelete = (attachment) => ({
|
||||
type: ActionTypes.ATTACHMENT_DELETE_HANDLE,
|
||||
payload: {
|
||||
attachment,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
createAttachment,
|
||||
handleAttachmentCreate,
|
||||
updateAttachment,
|
||||
handleAttachmentUpdate,
|
||||
deleteAttachment,
|
||||
handleAttachmentDelete,
|
||||
};
|
||||
@@ -1,138 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const createBoardMembership = (boardMembership) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_CREATE,
|
||||
payload: {
|
||||
boardMembership,
|
||||
},
|
||||
});
|
||||
|
||||
createBoardMembership.success = (localId, boardMembership) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_CREATE__SUCCESS,
|
||||
payload: {
|
||||
localId,
|
||||
boardMembership,
|
||||
},
|
||||
});
|
||||
|
||||
createBoardMembership.failure = (localId, error) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_CREATE__FAILURE,
|
||||
payload: {
|
||||
localId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleBoardMembershipCreate = (
|
||||
boardMembership,
|
||||
project,
|
||||
board,
|
||||
users,
|
||||
projectManagers,
|
||||
boards,
|
||||
boardMemberships,
|
||||
labels,
|
||||
lists,
|
||||
cards,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
attachments,
|
||||
deletedNotifications,
|
||||
) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE,
|
||||
payload: {
|
||||
boardMembership,
|
||||
project,
|
||||
board,
|
||||
users,
|
||||
projectManagers,
|
||||
boards,
|
||||
boardMemberships,
|
||||
labels,
|
||||
lists,
|
||||
cards,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
attachments,
|
||||
deletedNotifications,
|
||||
},
|
||||
});
|
||||
|
||||
handleBoardMembershipCreate.fetchProject = (id, currentUserId, currentBoardId) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE__PROJECT_FETCH,
|
||||
payload: {
|
||||
id,
|
||||
currentUserId,
|
||||
currentBoardId,
|
||||
},
|
||||
});
|
||||
|
||||
const updateBoardMembership = (id, data) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
updateBoardMembership.success = (boardMembership) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
boardMembership,
|
||||
},
|
||||
});
|
||||
|
||||
updateBoardMembership.failure = (id, error) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleBoardMembershipUpdate = (boardMembership) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE_HANDLE,
|
||||
payload: {
|
||||
boardMembership,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteBoardMembership = (id) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_DELETE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
deleteBoardMembership.success = (boardMembership) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_DELETE__SUCCESS,
|
||||
payload: {
|
||||
boardMembership,
|
||||
},
|
||||
});
|
||||
|
||||
deleteBoardMembership.failure = (id, error) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_DELETE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleBoardMembershipDelete = (boardMembership) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_DELETE_HANDLE,
|
||||
payload: {
|
||||
boardMembership,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
createBoardMembership,
|
||||
handleBoardMembershipCreate,
|
||||
updateBoardMembership,
|
||||
handleBoardMembershipUpdate,
|
||||
deleteBoardMembership,
|
||||
handleBoardMembershipDelete,
|
||||
};
|
||||
@@ -1,145 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const createBoard = (board) => ({
|
||||
type: ActionTypes.BOARD_CREATE,
|
||||
payload: {
|
||||
board,
|
||||
},
|
||||
});
|
||||
|
||||
createBoard.success = (localId, board, boardMemberships) => ({
|
||||
type: ActionTypes.BOARD_CREATE__SUCCESS,
|
||||
payload: {
|
||||
localId,
|
||||
board,
|
||||
boardMemberships,
|
||||
},
|
||||
});
|
||||
|
||||
createBoard.failure = (localId, error) => ({
|
||||
type: ActionTypes.BOARD_CREATE__FAILURE,
|
||||
payload: {
|
||||
localId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleBoardCreate = (board) => ({
|
||||
type: ActionTypes.BOARD_CREATE_HANDLE,
|
||||
payload: {
|
||||
board,
|
||||
},
|
||||
});
|
||||
|
||||
const fetchBoard = (id) => ({
|
||||
type: ActionTypes.BOARD_FETCH,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
fetchBoard.success = (
|
||||
board,
|
||||
users,
|
||||
projects,
|
||||
boardMemberships,
|
||||
labels,
|
||||
lists,
|
||||
cards,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
attachments,
|
||||
) => ({
|
||||
type: ActionTypes.BOARD_FETCH__SUCCESS,
|
||||
payload: {
|
||||
board,
|
||||
users,
|
||||
projects,
|
||||
boardMemberships,
|
||||
labels,
|
||||
lists,
|
||||
cards,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
attachments,
|
||||
},
|
||||
});
|
||||
|
||||
fetchBoard.failure = (id, error) => ({
|
||||
type: ActionTypes.BOARD_FETCH__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const updateBoard = (id, data) => ({
|
||||
type: ActionTypes.BOARD_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
updateBoard.success = (board) => ({
|
||||
type: ActionTypes.BOARD_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
board,
|
||||
},
|
||||
});
|
||||
|
||||
updateBoard.failure = (id, error) => ({
|
||||
type: ActionTypes.BOARD_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleBoardUpdate = (board) => ({
|
||||
type: ActionTypes.BOARD_UPDATE_HANDLE,
|
||||
payload: {
|
||||
board,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteBoard = (id) => ({
|
||||
type: ActionTypes.BOARD_DELETE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
deleteBoard.success = (board) => ({
|
||||
type: ActionTypes.BOARD_DELETE__SUCCESS,
|
||||
payload: {
|
||||
board,
|
||||
},
|
||||
});
|
||||
|
||||
deleteBoard.failure = (id, error) => ({
|
||||
type: ActionTypes.BOARD_DELETE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleBoardDelete = (board) => ({
|
||||
type: ActionTypes.BOARD_DELETE_HANDLE,
|
||||
payload: {
|
||||
board,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
createBoard,
|
||||
handleBoardCreate,
|
||||
fetchBoard,
|
||||
updateBoard,
|
||||
handleBoardUpdate,
|
||||
deleteBoard,
|
||||
handleBoardDelete,
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const createCard = (card) => ({
|
||||
type: ActionTypes.CARD_CREATE,
|
||||
payload: {
|
||||
card,
|
||||
},
|
||||
});
|
||||
|
||||
createCard.success = (localId, card) => ({
|
||||
type: ActionTypes.CARD_CREATE__SUCCESS,
|
||||
payload: {
|
||||
localId,
|
||||
card,
|
||||
},
|
||||
});
|
||||
|
||||
createCard.failure = (localId, error) => ({
|
||||
type: ActionTypes.CARD_CREATE__FAILURE,
|
||||
payload: {
|
||||
localId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleCardCreate = (card) => ({
|
||||
type: ActionTypes.CARD_CREATE_HANDLE,
|
||||
payload: {
|
||||
card,
|
||||
},
|
||||
});
|
||||
|
||||
const updateCard = (id, data) => ({
|
||||
type: ActionTypes.CARD_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
updateCard.success = (card) => ({
|
||||
type: ActionTypes.CARD_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
card,
|
||||
},
|
||||
});
|
||||
|
||||
updateCard.failure = (id, error) => ({
|
||||
type: ActionTypes.CARD_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleCardUpdate = (card) => ({
|
||||
type: ActionTypes.CARD_UPDATE_HANDLE,
|
||||
payload: {
|
||||
card,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteCard = (id) => ({
|
||||
type: ActionTypes.CARD_DELETE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
deleteCard.success = (card) => ({
|
||||
type: ActionTypes.CARD_DELETE__SUCCESS,
|
||||
payload: {
|
||||
card,
|
||||
},
|
||||
});
|
||||
|
||||
deleteCard.failure = (id, error) => ({
|
||||
type: ActionTypes.CARD_DELETE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleCardDelete = (card) => ({
|
||||
type: ActionTypes.CARD_DELETE_HANDLE,
|
||||
payload: {
|
||||
card,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
createCard,
|
||||
handleCardCreate,
|
||||
updateCard,
|
||||
handleCardUpdate,
|
||||
deleteCard,
|
||||
handleCardDelete,
|
||||
};
|
||||
@@ -1,75 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const createCommentActivity = (activity) => ({
|
||||
type: ActionTypes.COMMENT_ACTIVITY_CREATE,
|
||||
payload: {
|
||||
activity,
|
||||
},
|
||||
});
|
||||
|
||||
createCommentActivity.success = (localId, activity) => ({
|
||||
type: ActionTypes.COMMENT_ACTIVITY_CREATE__SUCCESS,
|
||||
payload: {
|
||||
localId,
|
||||
activity,
|
||||
},
|
||||
});
|
||||
|
||||
createCommentActivity.failure = (localId, error) => ({
|
||||
type: ActionTypes.COMMENT_ACTIVITY_CREATE__FAILURE,
|
||||
payload: {
|
||||
localId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const updateCommentActivity = (id, data) => ({
|
||||
type: ActionTypes.COMMENT_ACTIVITY_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
updateCommentActivity.success = (activity) => ({
|
||||
type: ActionTypes.COMMENT_ACTIVITY_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
activity,
|
||||
},
|
||||
});
|
||||
|
||||
updateCommentActivity.failure = (id, error) => ({
|
||||
type: ActionTypes.COMMENT_ACTIVITY_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteCommentActivity = (id) => ({
|
||||
type: ActionTypes.COMMENT_ACTIVITY_DELETE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
deleteCommentActivity.success = (activity) => ({
|
||||
type: ActionTypes.COMMENT_ACTIVITY_DELETE__SUCCESS,
|
||||
payload: {
|
||||
activity,
|
||||
},
|
||||
});
|
||||
|
||||
deleteCommentActivity.failure = (id, error) => ({
|
||||
type: ActionTypes.COMMENT_ACTIVITY_DELETE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
createCommentActivity,
|
||||
updateCommentActivity,
|
||||
deleteCommentActivity,
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const initializeCore = (
|
||||
user,
|
||||
board,
|
||||
users,
|
||||
projects,
|
||||
projectManagers,
|
||||
boards,
|
||||
boardMemberships,
|
||||
labels,
|
||||
lists,
|
||||
cards,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
attachments,
|
||||
activities,
|
||||
notifications,
|
||||
) => ({
|
||||
type: ActionTypes.CORE_INITIALIZE,
|
||||
payload: {
|
||||
user,
|
||||
board,
|
||||
users,
|
||||
projects,
|
||||
projectManagers,
|
||||
boards,
|
||||
boardMemberships,
|
||||
labels,
|
||||
lists,
|
||||
cards,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
attachments,
|
||||
activities,
|
||||
notifications,
|
||||
},
|
||||
});
|
||||
|
||||
const logout = () => ({
|
||||
type: ActionTypes.LOGOUT,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
logout.invalidateAccessToken = () => ({
|
||||
type: ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
export default {
|
||||
initializeCore,
|
||||
logout,
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import router from './router';
|
||||
import socket from './socket';
|
||||
import login from './login';
|
||||
import core from './core';
|
||||
import modals from './modals';
|
||||
import users from './users';
|
||||
import projects from './projects';
|
||||
import projectManagers from './project-managers';
|
||||
import boards from './boards';
|
||||
import boardMemberships from './board-memberships';
|
||||
import labels from './labels';
|
||||
import lists from './lists';
|
||||
import cards from './cards';
|
||||
import tasks from './tasks';
|
||||
import attachments from './attachments';
|
||||
import activities from './activities';
|
||||
import commentActivities from './comment-activities';
|
||||
import notifications from './notifications';
|
||||
|
||||
export default {
|
||||
...router,
|
||||
...socket,
|
||||
...login,
|
||||
...core,
|
||||
...modals,
|
||||
...users,
|
||||
...projects,
|
||||
...projectManagers,
|
||||
...boards,
|
||||
...boardMemberships,
|
||||
...labels,
|
||||
...lists,
|
||||
...cards,
|
||||
...tasks,
|
||||
...attachments,
|
||||
...activities,
|
||||
...commentActivities,
|
||||
...notifications,
|
||||
};
|
||||
@@ -1,183 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const createLabel = (label) => ({
|
||||
type: ActionTypes.LABEL_CREATE,
|
||||
payload: {
|
||||
label,
|
||||
},
|
||||
});
|
||||
|
||||
createLabel.success = (localId, label) => ({
|
||||
type: ActionTypes.LABEL_CREATE__SUCCESS,
|
||||
payload: {
|
||||
localId,
|
||||
label,
|
||||
},
|
||||
});
|
||||
|
||||
createLabel.failure = (localId, error) => ({
|
||||
type: ActionTypes.LABEL_CREATE__FAILURE,
|
||||
payload: {
|
||||
localId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleLabelCreate = (label) => ({
|
||||
type: ActionTypes.LABEL_CREATE_HANDLE,
|
||||
payload: {
|
||||
label,
|
||||
},
|
||||
});
|
||||
|
||||
const updateLabel = (id, data) => ({
|
||||
type: ActionTypes.LABEL_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
updateLabel.success = (label) => ({
|
||||
type: ActionTypes.LABEL_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
label,
|
||||
},
|
||||
});
|
||||
|
||||
updateLabel.failure = (id, error) => ({
|
||||
type: ActionTypes.LABEL_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleLabelUpdate = (label) => ({
|
||||
type: ActionTypes.LABEL_UPDATE_HANDLE,
|
||||
payload: {
|
||||
label,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteLabel = (id) => ({
|
||||
type: ActionTypes.LABEL_DELETE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
deleteLabel.success = (label) => ({
|
||||
type: ActionTypes.LABEL_DELETE__SUCCESS,
|
||||
payload: {
|
||||
label,
|
||||
},
|
||||
});
|
||||
|
||||
deleteLabel.failure = (id, error) => ({
|
||||
type: ActionTypes.LABEL_DELETE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleLabelDelete = (label) => ({
|
||||
type: ActionTypes.LABEL_DELETE_HANDLE,
|
||||
payload: {
|
||||
label,
|
||||
},
|
||||
});
|
||||
|
||||
const addLabelToCard = (id, cardId) => ({
|
||||
type: ActionTypes.LABEL_TO_CARD_ADD,
|
||||
payload: {
|
||||
id,
|
||||
cardId,
|
||||
},
|
||||
});
|
||||
|
||||
addLabelToCard.success = (cardLabel) => ({
|
||||
type: ActionTypes.LABEL_TO_CARD_ADD__SUCCESS,
|
||||
payload: {
|
||||
cardLabel,
|
||||
},
|
||||
});
|
||||
|
||||
addLabelToCard.failure = (id, cardId, error) => ({
|
||||
type: ActionTypes.LABEL_TO_CARD_ADD__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
cardId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleLabelToCardAdd = (cardLabel) => ({
|
||||
type: ActionTypes.LABEL_TO_CARD_ADD_HANDLE,
|
||||
payload: {
|
||||
cardLabel,
|
||||
},
|
||||
});
|
||||
|
||||
const removeLabelFromCard = (id, cardId) => ({
|
||||
type: ActionTypes.LABEL_FROM_CARD_REMOVE,
|
||||
payload: {
|
||||
id,
|
||||
cardId,
|
||||
},
|
||||
});
|
||||
|
||||
removeLabelFromCard.success = (cardLabel) => ({
|
||||
type: ActionTypes.LABEL_FROM_CARD_REMOVE__SUCCESS,
|
||||
payload: {
|
||||
cardLabel,
|
||||
},
|
||||
});
|
||||
|
||||
removeLabelFromCard.failure = (id, cardId, error) => ({
|
||||
type: ActionTypes.LABEL_FROM_CARD_REMOVE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
cardId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleLabelFromCardRemove = (cardLabel) => ({
|
||||
type: ActionTypes.LABEL_FROM_CARD_REMOVE_HANDLE,
|
||||
payload: {
|
||||
cardLabel,
|
||||
},
|
||||
});
|
||||
|
||||
const addLabelToBoardFilter = (id, boardId) => ({
|
||||
type: ActionTypes.LABEL_TO_BOARD_FILTER_ADD,
|
||||
payload: {
|
||||
id,
|
||||
boardId,
|
||||
},
|
||||
});
|
||||
|
||||
const removeLabelFromBoardFilter = (id, boardId) => ({
|
||||
type: ActionTypes.LABEL_FROM_BOARD_FILTER_REMOVE,
|
||||
payload: {
|
||||
id,
|
||||
boardId,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
createLabel,
|
||||
handleLabelCreate,
|
||||
updateLabel,
|
||||
handleLabelUpdate,
|
||||
deleteLabel,
|
||||
handleLabelDelete,
|
||||
addLabelToCard,
|
||||
handleLabelToCardAdd,
|
||||
removeLabelFromCard,
|
||||
handleLabelFromCardRemove,
|
||||
addLabelToBoardFilter,
|
||||
removeLabelFromBoardFilter,
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const createList = (list) => ({
|
||||
type: ActionTypes.LIST_CREATE,
|
||||
payload: {
|
||||
list,
|
||||
},
|
||||
});
|
||||
|
||||
createList.success = (localId, list) => ({
|
||||
type: ActionTypes.LIST_CREATE__SUCCESS,
|
||||
payload: {
|
||||
localId,
|
||||
list,
|
||||
},
|
||||
});
|
||||
|
||||
createList.failure = (localId, error) => ({
|
||||
type: ActionTypes.LIST_CREATE__FAILURE,
|
||||
payload: {
|
||||
localId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleListCreate = (list) => ({
|
||||
type: ActionTypes.LIST_CREATE_HANDLE,
|
||||
payload: {
|
||||
list,
|
||||
},
|
||||
});
|
||||
|
||||
const updateList = (id, data) => ({
|
||||
type: ActionTypes.LIST_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
updateList.success = (list) => ({
|
||||
type: ActionTypes.LIST_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
list,
|
||||
},
|
||||
});
|
||||
|
||||
updateList.failure = (id, error) => ({
|
||||
type: ActionTypes.LIST_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleListUpdate = (list) => ({
|
||||
type: ActionTypes.LIST_UPDATE_HANDLE,
|
||||
payload: {
|
||||
list,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteList = (id) => ({
|
||||
type: ActionTypes.LIST_DELETE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
deleteList.success = (list) => ({
|
||||
type: ActionTypes.LIST_DELETE__SUCCESS,
|
||||
payload: {
|
||||
list,
|
||||
},
|
||||
});
|
||||
|
||||
deleteList.failure = (id, error) => ({
|
||||
type: ActionTypes.LIST_DELETE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleListDelete = (list) => ({
|
||||
type: ActionTypes.LIST_DELETE_HANDLE,
|
||||
payload: {
|
||||
list,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
createList,
|
||||
handleListCreate,
|
||||
updateList,
|
||||
handleListUpdate,
|
||||
deleteList,
|
||||
handleListDelete,
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const authenticate = (data) => ({
|
||||
type: ActionTypes.AUTHENTICATE,
|
||||
payload: {
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
authenticate.success = (accessToken) => ({
|
||||
type: ActionTypes.AUTHENTICATE__SUCCESS,
|
||||
payload: {
|
||||
accessToken,
|
||||
},
|
||||
});
|
||||
|
||||
authenticate.failure = (error) => ({
|
||||
type: ActionTypes.AUTHENTICATE__FAILURE,
|
||||
payload: {
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const clearAuthenticateError = () => ({
|
||||
type: ActionTypes.AUTHENTICATE_ERROR_CLEAR,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
export default {
|
||||
authenticate,
|
||||
clearAuthenticateError,
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const openModal = (type) => ({
|
||||
type: ActionTypes.MODAL_OPEN,
|
||||
payload: {
|
||||
type,
|
||||
},
|
||||
});
|
||||
|
||||
const closeModal = () => ({
|
||||
type: ActionTypes.MODAL_CLOSE,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
export default {
|
||||
openModal,
|
||||
closeModal,
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const handleNotificationCreate = (notification, users, cards, activities) => ({
|
||||
type: ActionTypes.NOTIFICATION_CREATE_HANDLE,
|
||||
payload: {
|
||||
notification,
|
||||
users,
|
||||
cards,
|
||||
activities,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteNotification = (id) => ({
|
||||
type: ActionTypes.NOTIFICATION_DELETE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
deleteNotification.success = (notification) => ({
|
||||
type: ActionTypes.NOTIFICATION_DELETE__SUCCESS,
|
||||
payload: {
|
||||
notification,
|
||||
},
|
||||
});
|
||||
|
||||
deleteNotification.failure = (id, error) => ({
|
||||
type: ActionTypes.NOTIFICATION_DELETE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleNotificationDelete = (notification) => ({
|
||||
type: ActionTypes.NOTIFICATION_DELETE_HANDLE,
|
||||
payload: {
|
||||
notification,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
handleNotificationCreate,
|
||||
deleteNotification,
|
||||
handleNotificationDelete,
|
||||
};
|
||||
@@ -1,110 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const createProjectManager = (projectManager) => ({
|
||||
type: ActionTypes.PROJECT_MANAGER_CREATE,
|
||||
payload: {
|
||||
projectManager,
|
||||
},
|
||||
});
|
||||
|
||||
createProjectManager.success = (localId, projectManager) => ({
|
||||
type: ActionTypes.PROJECT_MANAGER_CREATE__SUCCESS,
|
||||
payload: {
|
||||
localId,
|
||||
projectManager,
|
||||
},
|
||||
});
|
||||
|
||||
createProjectManager.failure = (localId, error) => ({
|
||||
type: ActionTypes.PROJECT_MANAGER_CREATE__FAILURE,
|
||||
payload: {
|
||||
localId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleProjectManagerCreate = (
|
||||
projectManager,
|
||||
project,
|
||||
board,
|
||||
users,
|
||||
projectManagers,
|
||||
boards,
|
||||
boardMemberships,
|
||||
labels,
|
||||
lists,
|
||||
cards,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
attachments,
|
||||
deletedNotifications,
|
||||
) => ({
|
||||
type: ActionTypes.PROJECT_MANAGER_CREATE_HANDLE,
|
||||
payload: {
|
||||
projectManager,
|
||||
project,
|
||||
board,
|
||||
users,
|
||||
projectManagers,
|
||||
boards,
|
||||
boardMemberships,
|
||||
labels,
|
||||
lists,
|
||||
cards,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
attachments,
|
||||
deletedNotifications,
|
||||
},
|
||||
});
|
||||
|
||||
handleProjectManagerCreate.fetchProject = (id, currentUserId, currentBoardId) => ({
|
||||
type: ActionTypes.PROJECT_MANAGER_CREATE_HANDLE__PROJECT_FETCH,
|
||||
payload: {
|
||||
id,
|
||||
currentUserId,
|
||||
currentBoardId,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteProjectManager = (id, isCurrentUser, isCurrentProject) => ({
|
||||
type: ActionTypes.PROJECT_MANAGER_DELETE,
|
||||
payload: {
|
||||
id,
|
||||
isCurrentUser,
|
||||
isCurrentProject,
|
||||
},
|
||||
});
|
||||
|
||||
deleteProjectManager.success = (projectManager) => ({
|
||||
type: ActionTypes.PROJECT_MANAGER_DELETE__SUCCESS,
|
||||
payload: {
|
||||
projectManager,
|
||||
},
|
||||
});
|
||||
|
||||
deleteProjectManager.failure = (id, error) => ({
|
||||
type: ActionTypes.PROJECT_MANAGER_DELETE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleProjectManagerDelete = (projectManager, isCurrentUser, isCurrentProject) => ({
|
||||
type: ActionTypes.PROJECT_MANAGER_DELETE_HANDLE,
|
||||
payload: {
|
||||
projectManager,
|
||||
isCurrentUser,
|
||||
isCurrentProject,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
createProjectManager,
|
||||
handleProjectManagerCreate,
|
||||
deleteProjectManager,
|
||||
handleProjectManagerDelete,
|
||||
};
|
||||
@@ -1,125 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const createProject = (data) => ({
|
||||
type: ActionTypes.PROJECT_CREATE,
|
||||
payload: {
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
createProject.success = (project, projectManagers) => ({
|
||||
type: ActionTypes.PROJECT_CREATE__SUCCESS,
|
||||
payload: {
|
||||
project,
|
||||
projectManagers,
|
||||
},
|
||||
});
|
||||
|
||||
createProject.failure = (error) => ({
|
||||
type: ActionTypes.PROJECT_CREATE__FAILURE,
|
||||
payload: {
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleProjectCreate = (project, users, projectManagers, boards, boardMemberships) => ({
|
||||
type: ActionTypes.PROJECT_CREATE_HANDLE,
|
||||
payload: {
|
||||
project,
|
||||
users,
|
||||
projectManagers,
|
||||
boards,
|
||||
boardMemberships,
|
||||
},
|
||||
});
|
||||
|
||||
const updateProject = (id, data) => ({
|
||||
type: ActionTypes.PROJECT_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
updateProject.success = (project) => ({
|
||||
type: ActionTypes.PROJECT_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
project,
|
||||
},
|
||||
});
|
||||
|
||||
updateProject.failure = (id, error) => ({
|
||||
type: ActionTypes.PROJECT_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleProjectUpdate = (project) => ({
|
||||
type: ActionTypes.PROJECT_UPDATE_HANDLE,
|
||||
payload: {
|
||||
project,
|
||||
},
|
||||
});
|
||||
|
||||
const updateProjectBackgroundImage = (id) => ({
|
||||
type: ActionTypes.PROJECT_BACKGROUND_IMAGE_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
updateProjectBackgroundImage.success = (project) => ({
|
||||
type: ActionTypes.PROJECT_BACKGROUND_IMAGE_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
project,
|
||||
},
|
||||
});
|
||||
|
||||
updateProjectBackgroundImage.failure = (id, error) => ({
|
||||
type: ActionTypes.PROJECT_BACKGROUND_IMAGE_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteProject = (id) => ({
|
||||
type: ActionTypes.PROJECT_DELETE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
deleteProject.success = (project) => ({
|
||||
type: ActionTypes.PROJECT_DELETE__SUCCESS,
|
||||
payload: {
|
||||
project,
|
||||
},
|
||||
});
|
||||
|
||||
deleteProject.failure = (id, error) => ({
|
||||
type: ActionTypes.PROJECT_DELETE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleProjectDelete = (project) => ({
|
||||
type: ActionTypes.PROJECT_DELETE_HANDLE,
|
||||
payload: {
|
||||
project,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
createProject,
|
||||
handleProjectCreate,
|
||||
updateProject,
|
||||
handleProjectUpdate,
|
||||
updateProjectBackgroundImage,
|
||||
deleteProject,
|
||||
handleProjectDelete,
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const handleLocationChange = (
|
||||
board,
|
||||
users,
|
||||
projects,
|
||||
boardMemberships,
|
||||
labels,
|
||||
lists,
|
||||
cards,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
attachments,
|
||||
deletedNotifications,
|
||||
) => ({
|
||||
type: ActionTypes.LOCATION_CHANGE_HANDLE,
|
||||
payload: {
|
||||
board,
|
||||
users,
|
||||
projects,
|
||||
boardMemberships,
|
||||
labels,
|
||||
lists,
|
||||
cards,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
attachments,
|
||||
deletedNotifications,
|
||||
},
|
||||
});
|
||||
|
||||
handleLocationChange.fetchBoard = (id) => ({
|
||||
type: ActionTypes.LOCATION_CHANGE_HANDLE__BOARD_FETCH,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
handleLocationChange,
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const handleSocketDisconnect = () => ({
|
||||
type: ActionTypes.SOCKET_DISCONNECT_HANDLE,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
const handleSocketReconnect = (
|
||||
user,
|
||||
board,
|
||||
users,
|
||||
projects,
|
||||
projectManagers,
|
||||
boards,
|
||||
boardMemberships,
|
||||
labels,
|
||||
lists,
|
||||
cards,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
attachments,
|
||||
activities,
|
||||
notifications,
|
||||
) => ({
|
||||
type: ActionTypes.SOCKET_RECONNECT_HANDLE,
|
||||
payload: {
|
||||
user,
|
||||
board,
|
||||
users,
|
||||
projects,
|
||||
projectManagers,
|
||||
boards,
|
||||
boardMemberships,
|
||||
labels,
|
||||
lists,
|
||||
cards,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
attachments,
|
||||
activities,
|
||||
notifications,
|
||||
},
|
||||
});
|
||||
|
||||
handleSocketReconnect.fetchCore = (currentUserId, currentBoardId) => ({
|
||||
type: ActionTypes.SOCKET_RECONNECT_HANDLE__CORE_FETCH,
|
||||
payload: {
|
||||
currentUserId,
|
||||
currentBoardId,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
handleSocketDisconnect,
|
||||
handleSocketReconnect,
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const createTask = (task) => ({
|
||||
type: ActionTypes.TASK_CREATE,
|
||||
payload: {
|
||||
task,
|
||||
},
|
||||
});
|
||||
|
||||
createTask.success = (localId, task) => ({
|
||||
type: ActionTypes.TASK_CREATE__SUCCESS,
|
||||
payload: {
|
||||
localId,
|
||||
task,
|
||||
},
|
||||
});
|
||||
|
||||
createTask.failure = (localId, error) => ({
|
||||
type: ActionTypes.TASK_CREATE__FAILURE,
|
||||
payload: {
|
||||
localId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleTaskCreate = (task) => ({
|
||||
type: ActionTypes.TASK_CREATE_HANDLE,
|
||||
payload: {
|
||||
task,
|
||||
},
|
||||
});
|
||||
|
||||
const updateTask = (id, data) => ({
|
||||
type: ActionTypes.TASK_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
updateTask.success = (task) => ({
|
||||
type: ActionTypes.TASK_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
task,
|
||||
},
|
||||
});
|
||||
|
||||
updateTask.failure = (id, error) => ({
|
||||
type: ActionTypes.TASK_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleTaskUpdate = (task) => ({
|
||||
type: ActionTypes.TASK_UPDATE_HANDLE,
|
||||
payload: {
|
||||
task,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteTask = (id) => ({
|
||||
type: ActionTypes.TASK_DELETE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
deleteTask.success = (task) => ({
|
||||
type: ActionTypes.TASK_DELETE__SUCCESS,
|
||||
payload: {
|
||||
task,
|
||||
},
|
||||
});
|
||||
|
||||
deleteTask.failure = (id, error) => ({
|
||||
type: ActionTypes.TASK_DELETE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleTaskDelete = (task) => ({
|
||||
type: ActionTypes.TASK_DELETE_HANDLE,
|
||||
payload: {
|
||||
task,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
createTask,
|
||||
handleTaskCreate,
|
||||
updateTask,
|
||||
handleTaskUpdate,
|
||||
deleteTask,
|
||||
handleTaskDelete,
|
||||
};
|
||||
@@ -1,310 +0,0 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const createUser = (data) => ({
|
||||
type: ActionTypes.USER_CREATE,
|
||||
payload: {
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
createUser.success = (user) => ({
|
||||
type: ActionTypes.USER_CREATE__SUCCESS,
|
||||
payload: {
|
||||
user,
|
||||
},
|
||||
});
|
||||
|
||||
createUser.failure = (error) => ({
|
||||
type: ActionTypes.USER_CREATE__FAILURE,
|
||||
payload: {
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleUserCreate = (user) => ({
|
||||
type: ActionTypes.USER_CREATE_HANDLE,
|
||||
payload: {
|
||||
user,
|
||||
},
|
||||
});
|
||||
|
||||
const clearUserCreateError = () => ({
|
||||
type: ActionTypes.USER_CREATE_ERROR_CLEAR,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
const updateUser = (id, data) => ({
|
||||
type: ActionTypes.USER_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
updateUser.success = (user) => ({
|
||||
type: ActionTypes.USER_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
user,
|
||||
},
|
||||
});
|
||||
|
||||
updateUser.failure = (id, error) => ({
|
||||
type: ActionTypes.USER_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleUserUpdate = (user, users, isCurrent) => ({
|
||||
type: ActionTypes.USER_UPDATE_HANDLE,
|
||||
payload: {
|
||||
user,
|
||||
users,
|
||||
isCurrent,
|
||||
},
|
||||
});
|
||||
|
||||
const updateUserEmail = (id, data) => ({
|
||||
type: ActionTypes.USER_EMAIL_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
updateUserEmail.success = (user) => ({
|
||||
type: ActionTypes.USER_EMAIL_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
user,
|
||||
},
|
||||
});
|
||||
|
||||
updateUserEmail.failure = (id, error) => ({
|
||||
type: ActionTypes.USER_EMAIL_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const clearUserEmailUpdateError = (id) => ({
|
||||
type: ActionTypes.USER_EMAIL_UPDATE_ERROR_CLEAR,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
const updateUserPassword = (id, data) => ({
|
||||
type: ActionTypes.USER_PASSWORD_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
updateUserPassword.success = (user, accessToken) => ({
|
||||
type: ActionTypes.USER_PASSWORD_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
user,
|
||||
accessToken,
|
||||
},
|
||||
});
|
||||
|
||||
updateUserPassword.failure = (id, error) => ({
|
||||
type: ActionTypes.USER_PASSWORD_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const clearUserPasswordUpdateError = (id) => ({
|
||||
type: ActionTypes.USER_PASSWORD_UPDATE_ERROR_CLEAR,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
const updateUserUsername = (id, data) => ({
|
||||
type: ActionTypes.USER_USERNAME_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
updateUserUsername.success = (user) => ({
|
||||
type: ActionTypes.USER_USERNAME_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
user,
|
||||
},
|
||||
});
|
||||
|
||||
updateUserUsername.failure = (id, error) => ({
|
||||
type: ActionTypes.USER_USERNAME_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const clearUserUsernameUpdateError = (id) => ({
|
||||
type: ActionTypes.USER_USERNAME_UPDATE_ERROR_CLEAR,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
const updateUserAvatar = (id) => ({
|
||||
type: ActionTypes.USER_AVATAR_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
updateUserAvatar.success = (user) => ({
|
||||
type: ActionTypes.USER_AVATAR_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
user,
|
||||
},
|
||||
});
|
||||
|
||||
updateUserAvatar.failure = (id, error) => ({
|
||||
type: ActionTypes.USER_AVATAR_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteUser = (id) => ({
|
||||
type: ActionTypes.USER_DELETE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
deleteUser.success = (user) => ({
|
||||
type: ActionTypes.USER_DELETE__SUCCESS,
|
||||
payload: {
|
||||
user,
|
||||
},
|
||||
});
|
||||
|
||||
deleteUser.failure = (id, error) => ({
|
||||
type: ActionTypes.USER_DELETE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleUserDelete = (user) => ({
|
||||
type: ActionTypes.USER_DELETE_HANDLE,
|
||||
payload: {
|
||||
user,
|
||||
},
|
||||
});
|
||||
|
||||
const addUserToCard = (id, cardId, isCurrent) => ({
|
||||
type: ActionTypes.USER_TO_CARD_ADD,
|
||||
payload: {
|
||||
id,
|
||||
cardId,
|
||||
isCurrent,
|
||||
},
|
||||
});
|
||||
|
||||
addUserToCard.success = (cardMembership) => ({
|
||||
type: ActionTypes.USER_TO_CARD_ADD__SUCCESS,
|
||||
payload: {
|
||||
cardMembership,
|
||||
},
|
||||
});
|
||||
|
||||
addUserToCard.failure = (id, cardId, error) => ({
|
||||
type: ActionTypes.USER_TO_CARD_ADD__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
cardId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleUserToCardAdd = (cardMembership) => ({
|
||||
type: ActionTypes.USER_TO_CARD_ADD_HANDLE,
|
||||
payload: {
|
||||
cardMembership,
|
||||
},
|
||||
});
|
||||
|
||||
const removeUserFromCard = (id, cardId) => ({
|
||||
type: ActionTypes.USER_FROM_CARD_REMOVE,
|
||||
payload: {
|
||||
id,
|
||||
cardId,
|
||||
},
|
||||
});
|
||||
|
||||
removeUserFromCard.success = (cardMembership) => ({
|
||||
type: ActionTypes.USER_FROM_CARD_REMOVE__SUCCESS,
|
||||
payload: {
|
||||
cardMembership,
|
||||
},
|
||||
});
|
||||
|
||||
removeUserFromCard.failure = (id, cardId, error) => ({
|
||||
type: ActionTypes.USER_FROM_CARD_REMOVE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
cardId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleUserFromCardRemove = (cardMembership) => ({
|
||||
type: ActionTypes.USER_FROM_CARD_REMOVE_HANDLE,
|
||||
payload: {
|
||||
cardMembership,
|
||||
},
|
||||
});
|
||||
|
||||
const addUserToBoardFilter = (id, boardId) => ({
|
||||
type: ActionTypes.USER_TO_BOARD_FILTER_ADD,
|
||||
payload: {
|
||||
id,
|
||||
boardId,
|
||||
},
|
||||
});
|
||||
|
||||
const removeUserFromBoardFilter = (id, boardId) => ({
|
||||
type: ActionTypes.USER_FROM_BOARD_FILTER_REMOVE,
|
||||
payload: {
|
||||
id,
|
||||
boardId,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
createUser,
|
||||
handleUserCreate,
|
||||
clearUserCreateError,
|
||||
updateUser,
|
||||
handleUserUpdate,
|
||||
updateUserEmail,
|
||||
clearUserEmailUpdateError,
|
||||
updateUserPassword,
|
||||
clearUserPasswordUpdateError,
|
||||
updateUserUsername,
|
||||
clearUserUsernameUpdateError,
|
||||
updateUserAvatar,
|
||||
deleteUser,
|
||||
handleUserDelete,
|
||||
addUserToCard,
|
||||
handleUserToCardAdd,
|
||||
removeUserFromCard,
|
||||
handleUserFromCardRemove,
|
||||
addUserToBoardFilter,
|
||||
removeUserFromBoardFilter,
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import http from './http';
|
||||
import socket from './socket';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
|
||||
|
||||
const deleteCurrentAccessToken = (headers) =>
|
||||
socket.delete('/access-tokens/me', undefined, headers);
|
||||
|
||||
export default {
|
||||
createAccessToken,
|
||||
deleteCurrentAccessToken,
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import socket from './socket';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
export const transformActivity = (activity) => ({
|
||||
...activity,
|
||||
createdAt: new Date(activity.createdAt),
|
||||
});
|
||||
|
||||
/* Actions */
|
||||
|
||||
const getActivities = (cardId, data, headers) =>
|
||||
socket.get(`/cards/${cardId}/actions`, data, headers).then((body) => ({
|
||||
...body,
|
||||
items: body.items.map(transformActivity),
|
||||
}));
|
||||
|
||||
/* Event handlers */
|
||||
|
||||
const makeHandleActivityCreate = (next) => (body) => {
|
||||
next({
|
||||
...body,
|
||||
item: transformActivity(body.item),
|
||||
});
|
||||
};
|
||||
|
||||
const makeHandleActivityUpdate = makeHandleActivityCreate;
|
||||
|
||||
const makeHandleActivityDelete = makeHandleActivityCreate;
|
||||
|
||||
export default {
|
||||
getActivities,
|
||||
makeHandleActivityCreate,
|
||||
makeHandleActivityUpdate,
|
||||
makeHandleActivityDelete,
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import http from './http';
|
||||
import socket from './socket';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
export const transformAttachment = (attachment) => ({
|
||||
...attachment,
|
||||
createdAt: new Date(attachment.createdAt),
|
||||
});
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createAttachment = (cardId, data, requestId, headers) =>
|
||||
http.post(`/cards/${cardId}/attachments?requestId=${requestId}`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformAttachment(body.item),
|
||||
}));
|
||||
|
||||
const updateAttachment = (id, data, headers) =>
|
||||
socket.patch(`/attachments/${id}`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformAttachment(body.item),
|
||||
}));
|
||||
|
||||
const deleteAttachment = (id, headers) =>
|
||||
socket.delete(`/attachments/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformAttachment(body.item),
|
||||
}));
|
||||
|
||||
/* Event handlers */
|
||||
|
||||
const makeHandleAttachmentCreate = (next) => (body) => {
|
||||
next({
|
||||
...body,
|
||||
item: transformAttachment(body.item),
|
||||
});
|
||||
};
|
||||
|
||||
const makeHandleAttachmentUpdate = makeHandleAttachmentCreate;
|
||||
|
||||
const makeHandleAttachmentDelete = makeHandleAttachmentCreate;
|
||||
|
||||
export default {
|
||||
createAttachment,
|
||||
updateAttachment,
|
||||
deleteAttachment,
|
||||
makeHandleAttachmentCreate,
|
||||
makeHandleAttachmentUpdate,
|
||||
makeHandleAttachmentDelete,
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import socket from './socket';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createBoardMembership = (boardId, data, headers) =>
|
||||
socket.post(`/boards/${boardId}/memberships`, data, headers);
|
||||
|
||||
const updateBoardMembership = (id, data, headers) =>
|
||||
socket.patch(`/board-memberships/${id}`, data, headers);
|
||||
|
||||
const deleteBoardMembership = (id, headers) =>
|
||||
socket.delete(`/board-memberships/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createBoardMembership,
|
||||
updateBoardMembership,
|
||||
deleteBoardMembership,
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import socket from './socket';
|
||||
import { transformCard } from './cards';
|
||||
import { transformAttachment } from './attachments';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createBoard = (projectId, data, headers) =>
|
||||
socket.post(`/projects/${projectId}/boards`, data, headers);
|
||||
|
||||
const getBoard = (id, headers) =>
|
||||
socket.get(`/boards/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
included: {
|
||||
...body.included,
|
||||
cards: body.included.cards.map(transformCard),
|
||||
attachments: body.included.attachments.map(transformAttachment),
|
||||
},
|
||||
}));
|
||||
|
||||
const updateBoard = (id, data, headers) => socket.patch(`/boards/${id}`, data, headers);
|
||||
|
||||
const deleteBoard = (id, headers) => socket.delete(`/boards/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createBoard,
|
||||
getBoard,
|
||||
updateBoard,
|
||||
deleteBoard,
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import socket from './socket';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createCardLabel = (cardId, data, headers) =>
|
||||
socket.post(`/cards/${cardId}/labels`, data, headers);
|
||||
|
||||
const deleteCardLabel = (cardId, labelId, headers) =>
|
||||
socket.delete(`/cards/${cardId}/labels/${labelId}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createCardLabel,
|
||||
deleteCardLabel,
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import socket from './socket';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createCardMembership = (cardId, data, headers) =>
|
||||
socket.post(`/cards/${cardId}/memberships`, data, headers);
|
||||
|
||||
const deleteCardMembership = (cardId, userId, headers) =>
|
||||
socket.delete(`/cards/${cardId}/memberships?userId=${userId}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createCardMembership,
|
||||
deleteCardMembership,
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
import socket from './socket';
|
||||
import { transformAttachment } from './attachments';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
export const transformCard = (card) => ({
|
||||
...card,
|
||||
...(card.dueDate && {
|
||||
dueDate: new Date(card.dueDate),
|
||||
}),
|
||||
...(card.timer && {
|
||||
timer: {
|
||||
...card.timer,
|
||||
...(card.timer.startedAt && {
|
||||
startedAt: new Date(card.timer.startedAt),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export const transformCardData = (data) => ({
|
||||
...data,
|
||||
...(data.dueDate && {
|
||||
dueDate: data.dueDate.toISOString(),
|
||||
}),
|
||||
...(data.timer && {
|
||||
timer: {
|
||||
...data.timer,
|
||||
...(data.timer.startedAt && {
|
||||
startedAt: data.timer.startedAt.toISOString(),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/* Actions */
|
||||
|
||||
const getCards = (boardId, data, headers) =>
|
||||
socket.get(`/board/${boardId}/cards`, data, headers).then((body) => ({
|
||||
...body,
|
||||
items: body.items.map(transformCard),
|
||||
included: {
|
||||
...body.included,
|
||||
attachments: body.included.attachments.map(transformAttachment),
|
||||
},
|
||||
}));
|
||||
|
||||
const createCard = (boardId, data, headers) =>
|
||||
socket.post(`/boards/${boardId}/cards`, transformCardData(data), headers).then((body) => ({
|
||||
...body,
|
||||
item: transformCard(body.item),
|
||||
}));
|
||||
|
||||
const getCard = (id, headers) =>
|
||||
socket.get(`/cards/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformCard(body.item),
|
||||
}));
|
||||
|
||||
const updateCard = (id, data, headers) =>
|
||||
socket.patch(`/cards/${id}`, transformCardData(data), headers).then((body) => ({
|
||||
...body,
|
||||
item: transformCard(body.item),
|
||||
}));
|
||||
|
||||
const deleteCard = (id, headers) =>
|
||||
socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformCard(body.item),
|
||||
}));
|
||||
|
||||
/* Event handlers */
|
||||
|
||||
const makeHandleCardCreate = (next) => (body) => {
|
||||
next({
|
||||
...body,
|
||||
item: transformCard(body.item),
|
||||
});
|
||||
};
|
||||
|
||||
const makeHandleCardUpdate = makeHandleCardCreate;
|
||||
|
||||
const makeHandleCardDelete = makeHandleCardCreate;
|
||||
|
||||
export default {
|
||||
getCards,
|
||||
createCard,
|
||||
getCard,
|
||||
updateCard,
|
||||
deleteCard,
|
||||
makeHandleCardCreate,
|
||||
makeHandleCardUpdate,
|
||||
makeHandleCardDelete,
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import socket from './socket';
|
||||
import { transformActivity } from './activities';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createCommentActivity = (cardId, data, headers) =>
|
||||
socket.post(`/cards/${cardId}/comment-actions`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformActivity(body.item),
|
||||
}));
|
||||
|
||||
const updateCommentActivity = (id, data, headers) =>
|
||||
socket.patch(`/comment-actions/${id}`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformActivity(body.item),
|
||||
}));
|
||||
|
||||
const deleteCommentActivity = (id, headers) =>
|
||||
socket.delete(`/comment-actions/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformActivity(body.item),
|
||||
}));
|
||||
|
||||
export default {
|
||||
createCommentActivity,
|
||||
updateCommentActivity,
|
||||
deleteCommentActivity,
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import { fetch } from 'whatwg-fetch';
|
||||
|
||||
import Config from '../constants/Config';
|
||||
|
||||
const http = {};
|
||||
|
||||
// TODO: add all methods
|
||||
['POST'].forEach((method) => {
|
||||
http[method.toLowerCase()] = (url, data, headers) => {
|
||||
const formData = Object.keys(data).reduce((result, key) => {
|
||||
result.append(key, data[key]);
|
||||
|
||||
return result;
|
||||
}, new FormData());
|
||||
|
||||
return fetch(`${Config.SERVER_BASE_URL}/api${url}`, {
|
||||
method,
|
||||
headers,
|
||||
body: formData,
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((body) => ({
|
||||
body,
|
||||
isError: response.status !== 200,
|
||||
})),
|
||||
)
|
||||
.then(({ body, isError }) => {
|
||||
if (isError) {
|
||||
throw body;
|
||||
}
|
||||
|
||||
return body;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default http;
|
||||
@@ -1,39 +0,0 @@
|
||||
import http from './http';
|
||||
import socket from './socket';
|
||||
import accessTokens from './access-tokens';
|
||||
import users from './users';
|
||||
import projects from './projects';
|
||||
import projectManagers from './project-managers';
|
||||
import boards from './boards';
|
||||
import boardMemberships from './board-memberships';
|
||||
import labels from './labels';
|
||||
import lists from './lists';
|
||||
import cards from './cards';
|
||||
import cardMemberships from './card-memberships';
|
||||
import cardLabels from './card-labels';
|
||||
import tasks from './tasks';
|
||||
import attachments from './attachments';
|
||||
import activities from './activities';
|
||||
import commentActivities from './comment-activities';
|
||||
import notifications from './notifications';
|
||||
|
||||
export { http, socket };
|
||||
|
||||
export default {
|
||||
...accessTokens,
|
||||
...users,
|
||||
...projects,
|
||||
...projectManagers,
|
||||
...boards,
|
||||
...boardMemberships,
|
||||
...labels,
|
||||
...lists,
|
||||
...cards,
|
||||
...cardMemberships,
|
||||
...cardLabels,
|
||||
...tasks,
|
||||
...attachments,
|
||||
...activities,
|
||||
...commentActivities,
|
||||
...notifications,
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import socket from './socket';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createLabel = (boardId, data, headers) =>
|
||||
socket.post(`/boards/${boardId}/labels`, data, headers);
|
||||
|
||||
const updateLabel = (id, data, headers) => socket.patch(`/labels/${id}`, data, headers);
|
||||
|
||||
const deleteLabel = (id, headers) => socket.delete(`/labels/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createLabel,
|
||||
updateLabel,
|
||||
deleteLabel,
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import socket from './socket';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createList = (boardId, data, headers) =>
|
||||
socket.post(`/boards/${boardId}/lists`, data, headers);
|
||||
|
||||
const updateList = (id, data, headers) => socket.patch(`/lists/${id}`, data, headers);
|
||||
|
||||
const deleteList = (id, headers) => socket.delete(`/lists/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createList,
|
||||
updateList,
|
||||
deleteList,
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
import omit from 'lodash/omit';
|
||||
|
||||
import socket from './socket';
|
||||
import { transformCard } from './cards';
|
||||
import { transformActivity } from './activities';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
export const transformNotification = (notification) => ({
|
||||
...omit(notification, 'actionId'),
|
||||
activityId: notification.actionId,
|
||||
});
|
||||
|
||||
/* Actions */
|
||||
|
||||
const getNotifications = (headers) =>
|
||||
socket.get('/notifications', undefined, headers).then((body) => ({
|
||||
...body,
|
||||
items: body.items.map(transformNotification),
|
||||
included: {
|
||||
...omit(body.included, 'actions'),
|
||||
cards: body.included.cards.map(transformCard),
|
||||
activities: body.included.actions.map(transformActivity),
|
||||
},
|
||||
}));
|
||||
|
||||
const getNotification = (id, headers) =>
|
||||
socket.get(`/notifications/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformNotification(body.item),
|
||||
included: {
|
||||
...omit(body.included, 'actions'),
|
||||
cards: body.included.cards.map(transformCard),
|
||||
activities: body.included.actions.map(transformActivity),
|
||||
},
|
||||
}));
|
||||
|
||||
const updateNotifications = (ids, data, headers) =>
|
||||
socket.patch(`/notifications/${ids.join(',')}`, data, headers).then((body) => ({
|
||||
...body,
|
||||
items: body.items.map(transformNotification),
|
||||
}));
|
||||
|
||||
/* Event handlers */
|
||||
|
||||
const makeHandleNotificationCreate = (next) => (body) => {
|
||||
next({
|
||||
...body,
|
||||
item: transformNotification(body.item),
|
||||
});
|
||||
};
|
||||
|
||||
const makeHandleNotificationUpdate = makeHandleNotificationCreate;
|
||||
|
||||
export default {
|
||||
getNotifications,
|
||||
getNotification,
|
||||
updateNotifications,
|
||||
makeHandleNotificationCreate,
|
||||
makeHandleNotificationUpdate,
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import socket from './socket';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createProjectManager = (projectId, data, headers) =>
|
||||
socket.post(`/projects/${projectId}/managers`, data, headers);
|
||||
|
||||
const deleteProjectManager = (id, headers) =>
|
||||
socket.delete(`/project-managers/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createProjectManager,
|
||||
deleteProjectManager,
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import http from './http';
|
||||
import socket from './socket';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const getProjects = (headers) => socket.get('/projects', undefined, headers);
|
||||
|
||||
const createProject = (data, headers) => socket.post('/projects', data, headers);
|
||||
|
||||
const getProject = (id, headers) => socket.get(`/projects/${id}`, undefined, headers);
|
||||
|
||||
const updateProject = (id, data, headers) => socket.patch(`/projects/${id}`, data, headers);
|
||||
|
||||
const updateProjectBackgroundImage = (id, data, headers) =>
|
||||
http.post(`/projects/${id}/background-image`, data, headers);
|
||||
|
||||
const deleteProject = (id, headers) => socket.delete(`/projects/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
getProjects,
|
||||
createProject,
|
||||
getProject,
|
||||
updateProject,
|
||||
updateProjectBackgroundImage,
|
||||
deleteProject,
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import socketIOClient from 'socket.io-client';
|
||||
import sailsIOClient from 'sails.io.js';
|
||||
|
||||
import Config from '../constants/Config';
|
||||
|
||||
const io = sailsIOClient(socketIOClient);
|
||||
|
||||
io.sails.url = Config.SERVER_HOST_NAME;
|
||||
io.sails.autoConnect = false;
|
||||
io.sails.reconnection = true;
|
||||
io.sails.useCORSRouteToGetCookie = false;
|
||||
io.sails.environment = process.env.NODE_ENV;
|
||||
|
||||
const { socket } = io;
|
||||
|
||||
socket.path = `${Config.BASE_PATH}/socket.io`;
|
||||
socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
|
||||
|
||||
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => {
|
||||
socket[method.toLowerCase()] = (url, data, headers) =>
|
||||
new Promise((resolve, reject) => {
|
||||
socket.request(
|
||||
{
|
||||
method,
|
||||
data,
|
||||
headers,
|
||||
url: `/api${url}`,
|
||||
},
|
||||
(_, { body, error }) => {
|
||||
if (error) {
|
||||
reject(body);
|
||||
} else {
|
||||
resolve(body);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
export default socket;
|
||||
@@ -1,15 +0,0 @@
|
||||
import socket from './socket';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createTask = (cardId, data, headers) => socket.post(`/cards/${cardId}/tasks`, data, headers);
|
||||
|
||||
const updateTask = (id, data, headers) => socket.patch(`/tasks/${id}`, data, headers);
|
||||
|
||||
const deleteTask = (id, headers) => socket.delete(`/tasks/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createTask,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import http from './http';
|
||||
import socket from './socket';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const getUsers = (headers) => socket.get('/users', undefined, headers);
|
||||
|
||||
const createUser = (data, headers) => socket.post('/users', data, headers);
|
||||
|
||||
const getUser = (id, headers) => socket.get(`/users/${id}`, undefined, headers);
|
||||
|
||||
const getCurrentUser = (headers) => socket.get('/users/me', undefined, headers);
|
||||
|
||||
const updateUser = (id, data, headers) => socket.patch(`/users/${id}`, data, headers);
|
||||
|
||||
const updateUserEmail = (id, data, headers) => socket.patch(`/users/${id}/email`, data, headers);
|
||||
|
||||
const updateUserPassword = (id, data, headers) =>
|
||||
socket.patch(`/users/${id}/password`, data, headers);
|
||||
|
||||
const updateUserUsername = (id, data, headers) =>
|
||||
socket.patch(`/users/${id}/username`, data, headers);
|
||||
|
||||
const updateUserAvatar = (id, data, headers) => http.post(`/users/${id}/avatar`, data, headers);
|
||||
|
||||
const deleteUser = (id, headers) => socket.delete(`/users/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
getUsers,
|
||||
createUser,
|
||||
getUser,
|
||||
getCurrentUser,
|
||||
updateUser,
|
||||
updateUserEmail,
|
||||
updateUserPassword,
|
||||
updateUserUsername,
|
||||
updateUserAvatar,
|
||||
deleteUser,
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { ProjectBackgroundTypes } from '../../constants/Enums';
|
||||
|
||||
import styles from './Background.module.scss';
|
||||
import globalStyles from '../../styles.module.scss';
|
||||
|
||||
function Background({ type, name, imageUrl }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.wrapper,
|
||||
type === ProjectBackgroundTypes.GRADIENT &&
|
||||
globalStyles[`background${upperFirst(camelCase(name))}`],
|
||||
)}
|
||||
style={{
|
||||
background: type === 'image' && `url("${imageUrl}") center / cover`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Background.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
imageUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
Background.defaultProps = {
|
||||
name: undefined,
|
||||
imageUrl: undefined,
|
||||
};
|
||||
|
||||
export default Background;
|
||||
@@ -1,8 +0,0 @@
|
||||
:global(#app) {
|
||||
.wrapper {
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import Background from './Background';
|
||||
|
||||
export default Background;
|
||||
@@ -1,88 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Filters from './Filters';
|
||||
import Memberships from '../Memberships';
|
||||
import BoardMembershipPermissionsSelectStep from '../BoardMembershipPermissionsSelectStep';
|
||||
|
||||
import styles from './BoardActions.module.scss';
|
||||
|
||||
const BoardActions = React.memo(
|
||||
({
|
||||
memberships,
|
||||
labels,
|
||||
filterUsers,
|
||||
filterLabels,
|
||||
allUsers,
|
||||
canEdit,
|
||||
canEditMemberships,
|
||||
onMembershipCreate,
|
||||
onMembershipUpdate,
|
||||
onMembershipDelete,
|
||||
onUserToFilterAdd,
|
||||
onUserFromFilterRemove,
|
||||
onLabelToFilterAdd,
|
||||
onLabelFromFilterRemove,
|
||||
onLabelCreate,
|
||||
onLabelUpdate,
|
||||
onLabelDelete,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.actions}>
|
||||
<div className={styles.action}>
|
||||
<Memberships
|
||||
items={memberships}
|
||||
allUsers={allUsers}
|
||||
permissionsSelectStep={BoardMembershipPermissionsSelectStep}
|
||||
canEdit={canEditMemberships}
|
||||
onCreate={onMembershipCreate}
|
||||
onUpdate={onMembershipUpdate}
|
||||
onDelete={onMembershipDelete}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.action}>
|
||||
<Filters
|
||||
users={filterUsers}
|
||||
labels={filterLabels}
|
||||
allBoardMemberships={memberships}
|
||||
allLabels={labels}
|
||||
canEdit={canEdit}
|
||||
onUserAdd={onUserToFilterAdd}
|
||||
onUserRemove={onUserFromFilterRemove}
|
||||
onLabelAdd={onLabelToFilterAdd}
|
||||
onLabelRemove={onLabelFromFilterRemove}
|
||||
onLabelCreate={onLabelCreate}
|
||||
onLabelUpdate={onLabelUpdate}
|
||||
onLabelDelete={onLabelDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
BoardActions.propTypes = {
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
memberships: PropTypes.array.isRequired,
|
||||
labels: PropTypes.array.isRequired,
|
||||
filterUsers: PropTypes.array.isRequired,
|
||||
filterLabels: PropTypes.array.isRequired,
|
||||
allUsers: PropTypes.array.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
canEditMemberships: PropTypes.bool.isRequired,
|
||||
onMembershipCreate: PropTypes.func.isRequired,
|
||||
onMembershipUpdate: PropTypes.func.isRequired,
|
||||
onMembershipDelete: PropTypes.func.isRequired,
|
||||
onUserToFilterAdd: PropTypes.func.isRequired,
|
||||
onUserFromFilterRemove: PropTypes.func.isRequired,
|
||||
onLabelToFilterAdd: PropTypes.func.isRequired,
|
||||
onLabelFromFilterRemove: PropTypes.func.isRequired,
|
||||
onLabelCreate: PropTypes.func.isRequired,
|
||||
onLabelUpdate: PropTypes.func.isRequired,
|
||||
onLabelDelete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default BoardActions;
|
||||
@@ -1,23 +0,0 @@
|
||||
:global(#app) {
|
||||
.action {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: 20px 20px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import User from '../User';
|
||||
import Label from '../Label';
|
||||
import BoardMembershipsPopup from '../BoardMembershipsPopup';
|
||||
import LabelsPopup from '../LabelsPopup';
|
||||
|
||||
import styles from './Filters.module.scss';
|
||||
|
||||
const Filters = React.memo(
|
||||
({
|
||||
users,
|
||||
labels,
|
||||
allBoardMemberships,
|
||||
allLabels,
|
||||
canEdit,
|
||||
onUserAdd,
|
||||
onUserRemove,
|
||||
onLabelAdd,
|
||||
onLabelRemove,
|
||||
onLabelCreate,
|
||||
onLabelUpdate,
|
||||
onLabelDelete,
|
||||
}) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleRemoveUserClick = useCallback(
|
||||
(id) => {
|
||||
onUserRemove(id);
|
||||
},
|
||||
[onUserRemove],
|
||||
);
|
||||
|
||||
const handleRemoveLabelClick = useCallback(
|
||||
(id) => {
|
||||
onLabelRemove(id);
|
||||
},
|
||||
[onLabelRemove],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={styles.filter}>
|
||||
<BoardMembershipsPopup
|
||||
items={allBoardMemberships}
|
||||
currentUserIds={users.map((user) => user.id)}
|
||||
title={t('common.filterByMembers', {
|
||||
context: 'title',
|
||||
})}
|
||||
onUserSelect={onUserAdd}
|
||||
onUserDeselect={onUserRemove}
|
||||
>
|
||||
<button type="button" className={styles.filterButton}>
|
||||
<span className={styles.filterTitle}>{`${t('common.members')}:`}</span>
|
||||
{users.length === 0 && <span className={styles.filterLabel}>{t('common.all')}</span>}
|
||||
</button>
|
||||
</BoardMembershipsPopup>
|
||||
{users.map((user) => (
|
||||
<span key={user.id} className={styles.filterItem}>
|
||||
<User
|
||||
name={user.name}
|
||||
avatarUrl={user.avatarUrl}
|
||||
size="tiny"
|
||||
onClick={() => handleRemoveUserClick(user.id)}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
<span className={styles.filter}>
|
||||
<LabelsPopup
|
||||
items={allLabels}
|
||||
currentIds={labels.map((label) => label.id)}
|
||||
title={t('common.filterByLabels', {
|
||||
context: 'title',
|
||||
})}
|
||||
canEdit={canEdit}
|
||||
onSelect={onLabelAdd}
|
||||
onDeselect={onLabelRemove}
|
||||
onCreate={onLabelCreate}
|
||||
onUpdate={onLabelUpdate}
|
||||
onDelete={onLabelDelete}
|
||||
>
|
||||
<button type="button" className={styles.filterButton}>
|
||||
<span className={styles.filterTitle}>{`${t('common.labels')}:`}</span>
|
||||
{labels.length === 0 && <span className={styles.filterLabel}>{t('common.all')}</span>}
|
||||
</button>
|
||||
</LabelsPopup>
|
||||
{labels.map((label) => (
|
||||
<span key={label.id} className={styles.filterItem}>
|
||||
<Label
|
||||
name={label.name}
|
||||
color={label.color}
|
||||
size="small"
|
||||
onClick={() => handleRemoveLabelClick(label.id)}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Filters.propTypes = {
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
users: PropTypes.array.isRequired,
|
||||
labels: PropTypes.array.isRequired,
|
||||
allBoardMemberships: PropTypes.array.isRequired,
|
||||
allLabels: PropTypes.array.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
onUserAdd: PropTypes.func.isRequired,
|
||||
onUserRemove: PropTypes.func.isRequired,
|
||||
onLabelAdd: PropTypes.func.isRequired,
|
||||
onLabelRemove: PropTypes.func.isRequired,
|
||||
onLabelCreate: PropTypes.func.isRequired,
|
||||
onLabelUpdate: PropTypes.func.isRequired,
|
||||
onLabelDelete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Filters;
|
||||
@@ -1,46 +0,0 @@
|
||||
:global(#app) {
|
||||
.filter {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.filterButton {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filterItem {
|
||||
display: inline-block;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
margin-right: 4px;
|
||||
max-width: 190px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.filterLabel {
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 2px 8px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
}
|
||||
|
||||
.filterTitle {
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 2px 12px;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import BoardActions from './BoardActions';
|
||||
|
||||
export default BoardActions;
|
||||
@@ -1,178 +0,0 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import { closePopup } from '../../lib/popup';
|
||||
|
||||
import DroppableTypes from '../../constants/DroppableTypes';
|
||||
import ListContainer from '../../containers/ListContainer';
|
||||
import CardModalContainer from '../../containers/CardModalContainer';
|
||||
import ListAdd from './ListAdd';
|
||||
import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-icon.svg';
|
||||
|
||||
import styles from './BoardKanban.module.scss';
|
||||
|
||||
const parseDndId = (dndId) => dndId.split(':')[1];
|
||||
|
||||
const BoardKanban = React.memo(
|
||||
({ listIds, isCardModalOpened, canEdit, onListCreate, onListMove, onCardMove }) => {
|
||||
const [t] = useTranslation();
|
||||
const [isListAddOpened, setIsListAddOpened] = useState(false);
|
||||
|
||||
const wrapper = useRef(null);
|
||||
const prevPosition = useRef(null);
|
||||
|
||||
const handleAddListClick = useCallback(() => {
|
||||
setIsListAddOpened(true);
|
||||
}, []);
|
||||
|
||||
const handleAddListClose = useCallback(() => {
|
||||
setIsListAddOpened(false);
|
||||
}, []);
|
||||
|
||||
const handleDragStart = useCallback(() => {
|
||||
closePopup();
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
({ draggableId, type, source, destination }) => {
|
||||
if (
|
||||
!destination ||
|
||||
(source.droppableId === destination.droppableId && source.index === destination.index)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = parseDndId(draggableId);
|
||||
|
||||
switch (type) {
|
||||
case DroppableTypes.LIST:
|
||||
onListMove(id, destination.index);
|
||||
|
||||
break;
|
||||
case DroppableTypes.CARD:
|
||||
onCardMove(id, parseDndId(destination.droppableId), destination.index);
|
||||
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
[onListMove, onCardMove],
|
||||
);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(event) => {
|
||||
if (event.target !== wrapper.current && !event.target.dataset.dragScroller) {
|
||||
return;
|
||||
}
|
||||
|
||||
prevPosition.current = event.clientX;
|
||||
},
|
||||
[wrapper],
|
||||
);
|
||||
|
||||
const handleWindowMouseMove = useCallback(
|
||||
(event) => {
|
||||
if (!prevPosition.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
window.scrollBy({
|
||||
left: prevPosition.current - event.clientX,
|
||||
});
|
||||
|
||||
prevPosition.current = event.clientX;
|
||||
},
|
||||
[prevPosition],
|
||||
);
|
||||
|
||||
const handleWindowMouseUp = useCallback(() => {
|
||||
prevPosition.current = null;
|
||||
}, [prevPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflowX = 'auto';
|
||||
|
||||
return () => {
|
||||
document.body.style.overflowX = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isListAddOpened) {
|
||||
window.scroll(document.body.scrollWidth, 0);
|
||||
}
|
||||
}, [listIds, isListAddOpened]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mouseup', handleWindowMouseUp);
|
||||
window.addEventListener('mousemove', handleWindowMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mouseup', handleWindowMouseUp);
|
||||
window.removeEventListener('mousemove', handleWindowMouseMove);
|
||||
};
|
||||
}, [handleWindowMouseUp, handleWindowMouseMove]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div ref={wrapper} className={styles.wrapper} onMouseDown={handleMouseDown}>
|
||||
<div>
|
||||
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<Droppable droppableId="board" type={DroppableTypes.LIST} direction="horizontal">
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<div
|
||||
{...droppableProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
data-drag-scroller
|
||||
ref={innerRef}
|
||||
className={styles.lists}
|
||||
>
|
||||
{listIds.map((listId, index) => (
|
||||
<ListContainer key={listId} id={listId} index={index} />
|
||||
))}
|
||||
{placeholder}
|
||||
{canEdit && (
|
||||
<div data-drag-scroller className={styles.list}>
|
||||
{isListAddOpened ? (
|
||||
<ListAdd onCreate={onListCreate} onClose={handleAddListClose} />
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.addListButton}
|
||||
onClick={handleAddListClick}
|
||||
>
|
||||
<PlusMathIcon className={styles.addListButtonIcon} />
|
||||
<span className={styles.addListButtonText}>
|
||||
{listIds.length > 0
|
||||
? t('action.addAnotherList')
|
||||
: t('action.addList')}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
</div>
|
||||
{isCardModalOpened && <CardModalContainer />}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
BoardKanban.propTypes = {
|
||||
listIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
isCardModalOpened: PropTypes.bool.isRequired,
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
onListCreate: PropTypes.func.isRequired,
|
||||
onListMove: PropTypes.func.isRequired,
|
||||
onCardMove: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default BoardKanban;
|
||||
@@ -1,64 +0,0 @@
|
||||
:global(#app) {
|
||||
.addListButton {
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
fill: rgba(255, 255, 255, 0.72);
|
||||
font-weight: normal;
|
||||
height: 42px;
|
||||
padding: 11px;
|
||||
text-align: left;
|
||||
transition: background 85ms ease-in, opacity 40ms ease-in,
|
||||
border-color 85ms ease-in;
|
||||
width: 100%;
|
||||
|
||||
&:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
}
|
||||
|
||||
.addListButtonIcon {
|
||||
height: 20px;
|
||||
padding: 0.64px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.addListButtonText {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-right: 20px;
|
||||
width: 272px;
|
||||
}
|
||||
|
||||
.lists {
|
||||
display: inline-flex;
|
||||
height: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.panel {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.panelItem {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
margin: 0 20px;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Input } from 'semantic-ui-react';
|
||||
import { useDidUpdate, useToggle } from '../../lib/hooks';
|
||||
|
||||
import { useClosableForm, useForm } from '../../hooks';
|
||||
|
||||
import styles from './ListAdd.module.scss';
|
||||
|
||||
const DEFAULT_DATA = {
|
||||
name: '',
|
||||
};
|
||||
|
||||
const ListAdd = React.memo(({ onCreate, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
|
||||
const [focusNameFieldState, focusNameField] = useToggle();
|
||||
|
||||
const nameField = useRef(null);
|
||||
|
||||
const handleFieldKeyDown = useCallback(
|
||||
(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(onClose);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const cleanData = {
|
||||
...data,
|
||||
name: data.name.trim(),
|
||||
};
|
||||
|
||||
if (!cleanData.name) {
|
||||
nameField.current.select();
|
||||
return;
|
||||
}
|
||||
|
||||
onCreate(cleanData);
|
||||
|
||||
setData(DEFAULT_DATA);
|
||||
focusNameField();
|
||||
}, [onCreate, data, setData, focusNameField]);
|
||||
|
||||
useEffect(() => {
|
||||
nameField.current.focus();
|
||||
}, []);
|
||||
|
||||
useDidUpdate(() => {
|
||||
nameField.current.focus();
|
||||
}, [focusNameFieldState]);
|
||||
|
||||
return (
|
||||
<Form className={styles.wrapper} onSubmit={handleSubmit}>
|
||||
<Input
|
||||
ref={nameField}
|
||||
name="name"
|
||||
value={data.name}
|
||||
placeholder={t('common.enterListTitle')}
|
||||
className={styles.field}
|
||||
onKeyDown={handleFieldKeyDown}
|
||||
onChange={handleFieldChange}
|
||||
onBlur={handleFieldBlur}
|
||||
/>
|
||||
<div className={styles.controls}>
|
||||
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
|
||||
<Button
|
||||
positive
|
||||
content={t('action.addList')}
|
||||
className={styles.submitButton}
|
||||
onMouseOver={handleControlMouseOver}
|
||||
onMouseOut={handleControlMouseOut}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
||||
ListAdd.propTypes = {
|
||||
onCreate: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ListAdd;
|
||||
@@ -1,33 +0,0 @@
|
||||
:global(#app) {
|
||||
.controls {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.field {
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 0 #ccc;
|
||||
color: #333;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
border-color: #298fca;
|
||||
box-shadow: 0 0 2px #298fca;
|
||||
}
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
min-height: 30px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
background: #e2e4e6;
|
||||
border-radius: 3px;
|
||||
padding: 4px;
|
||||
transition: opacity 40ms ease-in;
|
||||
width: 272px;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import BoardKanban from './BoardKanban';
|
||||
|
||||
export default BoardKanban;
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
import omit from 'lodash/omit';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Menu, Radio, Segment } from 'semantic-ui-react';
|
||||
import { Popup } from '../../lib/custom-ui';
|
||||
|
||||
import { BoardMembershipRoles } from '../../constants/Enums';
|
||||
|
||||
import styles from './BoardMembershipPermissionsSelectStep.module.scss';
|
||||
|
||||
const BoardMembershipPermissionsSelectStep = React.memo(
|
||||
({ defaultData, title, buttonContent, onSelect, onBack }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const [data, setData] = useState(() => ({
|
||||
role: BoardMembershipRoles.EDITOR,
|
||||
canComment: null,
|
||||
...defaultData,
|
||||
}));
|
||||
|
||||
const handleSelectRoleClick = useCallback((role) => {
|
||||
setData((prevData) => ({
|
||||
...prevData,
|
||||
role,
|
||||
canComment: role === BoardMembershipRoles.EDITOR ? null : !!prevData.canComment,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleSettingChange = useCallback((_, { name: fieldName, checked: value }) => {
|
||||
setData((prevData) => ({
|
||||
...prevData,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
onSelect(data.role === BoardMembershipRoles.EDITOR ? omit(data, 'canComment') : data);
|
||||
}, [onSelect, data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t(title, {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
<Menu.Item
|
||||
active={data.role === BoardMembershipRoles.EDITOR}
|
||||
onClick={() => handleSelectRoleClick(BoardMembershipRoles.EDITOR)}
|
||||
>
|
||||
<div className={styles.menuItemTitle}>{t('common.editor')}</div>
|
||||
<div className={styles.menuItemDescription}>
|
||||
{t('common.canEditContentOfBoard')}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
active={data.role === BoardMembershipRoles.VIEWER}
|
||||
onClick={() => handleSelectRoleClick(BoardMembershipRoles.VIEWER)}
|
||||
>
|
||||
<div className={styles.menuItemTitle}>{t('common.viewer')}</div>
|
||||
<div className={styles.menuItemDescription}>{t('common.canOnlyViewBoard')}</div>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
{data.role !== BoardMembershipRoles.EDITOR && (
|
||||
<Segment basic className={styles.settings}>
|
||||
<Radio
|
||||
toggle
|
||||
name="canComment"
|
||||
checked={data.canComment}
|
||||
label={t('common.canComment')}
|
||||
onChange={handleSettingChange}
|
||||
/>
|
||||
</Segment>
|
||||
)}
|
||||
<Button positive content={t(buttonContent)} />
|
||||
</Form>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
BoardMembershipPermissionsSelectStep.propTypes = {
|
||||
defaultData: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
title: PropTypes.string,
|
||||
buttonContent: PropTypes.string,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
BoardMembershipPermissionsSelectStep.defaultProps = {
|
||||
defaultData: undefined,
|
||||
title: 'common.selectPermissions',
|
||||
buttonContent: 'action.selectPermissions',
|
||||
};
|
||||
|
||||
export default BoardMembershipPermissionsSelectStep;
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
:global(#app) {
|
||||
.menu {
|
||||
margin: 0 auto 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menuItemDescription {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.menuItemTitle {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.settings {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import BoardMembershipPermissionsSelectStep from './BoardMembershipPermissionsSelectStep';
|
||||
|
||||
export default BoardMembershipPermissionsSelectStep;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { withPopup } from '../lib/popup';
|
||||
|
||||
import BoardMembershipsStep from './BoardMembershipsStep';
|
||||
|
||||
export default withPopup(BoardMembershipsStep);
|
||||
@@ -1,101 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Menu } from 'semantic-ui-react';
|
||||
import { Input, Popup } from '../../lib/custom-ui';
|
||||
|
||||
import { useField } from '../../hooks';
|
||||
import Item from './Item';
|
||||
|
||||
import styles from './BoardMembershipsStep.module.scss';
|
||||
|
||||
const BoardMembershipsStep = React.memo(
|
||||
({ items, currentUserIds, title, onUserSelect, onUserDeselect, onBack }) => {
|
||||
const [t] = useTranslation();
|
||||
const [search, handleSearchChange] = useField('');
|
||||
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
|
||||
|
||||
const filteredItems = useMemo(
|
||||
() =>
|
||||
items.filter(
|
||||
({ user }) =>
|
||||
user.email.includes(cleanSearch) ||
|
||||
user.name.toLowerCase().includes(cleanSearch) ||
|
||||
(user.username && user.username.includes(cleanSearch)),
|
||||
),
|
||||
[items, cleanSearch],
|
||||
);
|
||||
|
||||
const searchField = useRef(null);
|
||||
|
||||
const handleUserSelect = useCallback(
|
||||
(id) => {
|
||||
onUserSelect(id);
|
||||
},
|
||||
[onUserSelect],
|
||||
);
|
||||
|
||||
const handleUserDeselect = useCallback(
|
||||
(id) => {
|
||||
onUserDeselect(id);
|
||||
},
|
||||
[onUserDeselect],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
searchField.current.focus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t(title, {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Input
|
||||
fluid
|
||||
ref={searchField}
|
||||
value={search}
|
||||
placeholder={t('common.searchMembers')}
|
||||
icon="search"
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
{filteredItems.length > 0 && (
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
{filteredItems.map((item) => (
|
||||
<Item
|
||||
key={item.id}
|
||||
isPersisted={item.isPersisted}
|
||||
isActive={currentUserIds.includes(item.user.id)}
|
||||
user={item.user}
|
||||
onUserSelect={() => handleUserSelect(item.user.id)}
|
||||
onUserDeselect={() => handleUserDeselect(item.user.id)}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
)}
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
BoardMembershipsStep.propTypes = {
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
items: PropTypes.array.isRequired,
|
||||
currentUserIds: PropTypes.array.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
title: PropTypes.string,
|
||||
onUserSelect: PropTypes.func.isRequired,
|
||||
onUserDeselect: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func,
|
||||
};
|
||||
|
||||
BoardMembershipsStep.defaultProps = {
|
||||
title: 'common.members',
|
||||
onBack: undefined,
|
||||
};
|
||||
|
||||
export default BoardMembershipsStep;
|
||||
@@ -1,22 +0,0 @@
|
||||
:global(#app) {
|
||||
.menu {
|
||||
margin: 8px auto 0;
|
||||
max-height: 320px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
width: 100%;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Menu } from 'semantic-ui-react';
|
||||
|
||||
import User from '../User';
|
||||
|
||||
import styles from './Item.module.scss';
|
||||
|
||||
const Item = React.memo(({ isPersisted, isActive, user, onUserSelect, onUserDeselect }) => {
|
||||
const handleToggleClick = useCallback(() => {
|
||||
if (isActive) {
|
||||
onUserDeselect();
|
||||
} else {
|
||||
onUserSelect();
|
||||
}
|
||||
}, [isActive, onUserSelect, onUserDeselect]);
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
active={isActive}
|
||||
disabled={!isPersisted}
|
||||
className={classNames(styles.menuItem, isActive && styles.menuItemActive)}
|
||||
onClick={handleToggleClick}
|
||||
>
|
||||
<span className={styles.user}>
|
||||
<User name={user.name} avatarUrl={user.avatarUrl} />
|
||||
</span>
|
||||
<div className={classNames(styles.menuItemText, isActive && styles.menuItemTextActive)}>
|
||||
{user.name}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
);
|
||||
});
|
||||
|
||||
Item.propTypes = {
|
||||
isPersisted: PropTypes.bool.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
onUserSelect: PropTypes.func.isRequired,
|
||||
onUserDeselect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Item;
|
||||
@@ -1,43 +0,0 @@
|
||||
:global(#app) {
|
||||
.menuItem {
|
||||
display: block;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.menuItemActive {
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.menuItemText {
|
||||
display: inline-block;
|
||||
line-height: 32px;
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.menuItemTextActive:before {
|
||||
bottom: 2px;
|
||||
color: #798d99;
|
||||
content: "Г";
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
line-height: 36px;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
text-align: center;
|
||||
transform: rotate(-135deg);
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.user {
|
||||
display: inline-block;
|
||||
line-height: 32px;
|
||||
padding-right: 8px;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import BoardMembershipsStep from './BoardMembershipsStep';
|
||||
|
||||
export default BoardMembershipsStep;
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Loader } from 'semantic-ui-react';
|
||||
|
||||
import { BoardTypes } from '../constants/Enums';
|
||||
import BoardKanbanContainer from '../containers/BoardKanbanContainer';
|
||||
|
||||
const BoardWrapper = React.memo(({ type, isFetching }) => {
|
||||
if (isFetching) {
|
||||
return <Loader active />;
|
||||
}
|
||||
|
||||
if (type === BoardTypes.KANBAN) {
|
||||
return <BoardKanbanContainer />;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
BoardWrapper.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default BoardWrapper;
|
||||
@@ -1,70 +0,0 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form } from 'semantic-ui-react';
|
||||
import { withPopup } from '../../lib/popup';
|
||||
import { Input, Popup } from '../../lib/custom-ui';
|
||||
|
||||
import { useForm } from '../../hooks';
|
||||
|
||||
import styles from './AddPopup.module.scss';
|
||||
|
||||
const AddStep = React.memo(({ onCreate, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const [data, handleFieldChange] = useForm({
|
||||
name: '',
|
||||
});
|
||||
|
||||
const nameField = useRef(null);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const cleanData = {
|
||||
...data,
|
||||
type: 'kanban',
|
||||
name: data.name.trim(),
|
||||
};
|
||||
|
||||
if (!cleanData.name) {
|
||||
nameField.current.select();
|
||||
return;
|
||||
}
|
||||
|
||||
onCreate(cleanData);
|
||||
onClose();
|
||||
}, [onCreate, onClose, data]);
|
||||
|
||||
useEffect(() => {
|
||||
nameField.current.focus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t('common.createBoard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
fluid
|
||||
ref={nameField}
|
||||
name="name"
|
||||
value={data.name}
|
||||
className={styles.field}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
<Button positive content={t('action.createBoard')} />
|
||||
</Form>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
AddStep.propTypes = {
|
||||
onCreate: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withPopup(AddStep);
|
||||
@@ -1,5 +0,0 @@
|
||||
:global(#app) {
|
||||
.field {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import pick from 'lodash/pick';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import { Button, Icon } from 'semantic-ui-react';
|
||||
import { closePopup } from '../../lib/popup';
|
||||
|
||||
import Paths from '../../constants/Paths';
|
||||
import DroppableTypes from '../../constants/DroppableTypes';
|
||||
import AddPopup from './AddPopup';
|
||||
import EditPopup from './EditPopup';
|
||||
|
||||
import styles from './Boards.module.scss';
|
||||
|
||||
const Boards = React.memo(({ items, currentId, canEdit, onCreate, onUpdate, onMove, onDelete }) => {
|
||||
const tabsWrapper = useRef(null);
|
||||
|
||||
const handleWheel = useCallback(({ deltaY }) => {
|
||||
tabsWrapper.current.scrollBy({
|
||||
left: deltaY,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDragStart = useCallback(() => {
|
||||
closePopup();
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
({ draggableId, source, destination }) => {
|
||||
if (!destination || source.index === destination.index) {
|
||||
return;
|
||||
}
|
||||
|
||||
onMove(draggableId, destination.index);
|
||||
},
|
||||
[onMove],
|
||||
);
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
(id, data) => {
|
||||
onUpdate(id, data);
|
||||
},
|
||||
[onUpdate],
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(id) => {
|
||||
onDelete(id);
|
||||
},
|
||||
[onDelete],
|
||||
);
|
||||
|
||||
const itemsNode = items.map((item, index) => (
|
||||
<Draggable
|
||||
key={item.id}
|
||||
draggableId={item.id}
|
||||
index={index}
|
||||
isDragDisabled={!item.isPersisted || !canEdit}
|
||||
>
|
||||
{({ innerRef, draggableProps, dragHandleProps }) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<div {...draggableProps} ref={innerRef} className={styles.tabWrapper}>
|
||||
<div className={classNames(styles.tab, item.id === currentId && styles.tabActive)}>
|
||||
{item.isPersisted ? (
|
||||
<>
|
||||
<Link
|
||||
{...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
to={Paths.BOARDS.replace(':id', item.id)}
|
||||
title={item.name}
|
||||
className={styles.link}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
{canEdit && (
|
||||
<EditPopup
|
||||
defaultData={pick(item, 'name')}
|
||||
onUpdate={(data) => handleUpdate(item.id, data)}
|
||||
onDelete={() => handleDelete(item.id)}
|
||||
>
|
||||
<Button className={classNames(styles.editButton, styles.target)}>
|
||||
<Icon fitted name="pencil" size="small" />
|
||||
</Button>
|
||||
</EditPopup>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<span {...dragHandleProps} className={styles.link}>
|
||||
{item.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper} onWheel={handleWheel}>
|
||||
<div ref={tabsWrapper} className={styles.tabsWrapper}>
|
||||
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<Droppable droppableId="boards" type={DroppableTypes.BOARD} direction="horizontal">
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<div {...droppableProps} ref={innerRef} className={styles.tabs}>
|
||||
{itemsNode}
|
||||
{placeholder}
|
||||
{canEdit && (
|
||||
<AddPopup onCreate={onCreate}>
|
||||
<Button icon="plus" className={styles.addButton} />
|
||||
</AddPopup>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Boards.propTypes = {
|
||||
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
currentId: PropTypes.string,
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
onCreate: PropTypes.func.isRequired,
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onMove: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Boards.defaultProps = {
|
||||
currentId: undefined,
|
||||
};
|
||||
|
||||
export default Boards;
|
||||
@@ -1,110 +0,0 @@
|
||||
:global(#app) {
|
||||
.addButton {
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
margin-right: 0;
|
||||
vertical-align: top;
|
||||
|
||||
&:hover {
|
||||
background: rgba(34, 36, 38, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.editButton {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: #fff;
|
||||
line-height: 32px;
|
||||
margin-right: 0;
|
||||
opacity: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
width: 32px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
padding: 10px 34px 6px 14px;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab {
|
||||
border-radius: 3px 3px 0 0;
|
||||
min-width: 100px;
|
||||
position: relative;
|
||||
transition: all 0.1s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
|
||||
.target {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabActive {
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
}
|
||||
|
||||
.tabWrapper {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
height: 38px;
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tabsWrapper {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
height: 56px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&:hover {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.24);
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
height: 38px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { dequal } from 'dequal';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form } from 'semantic-ui-react';
|
||||
import { withPopup } from '../../lib/popup';
|
||||
import { Input, Popup } from '../../lib/custom-ui';
|
||||
|
||||
import { useForm, useSteps } from '../../hooks';
|
||||
import DeleteStep from '../DeleteStep';
|
||||
|
||||
import styles from './EditPopup.module.scss';
|
||||
|
||||
const StepTypes = {
|
||||
DELETE: 'DELETE',
|
||||
};
|
||||
|
||||
const EditStep = React.memo(({ defaultData, onUpdate, onDelete, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const [data, handleFieldChange] = useForm(() => ({
|
||||
name: '',
|
||||
...defaultData,
|
||||
}));
|
||||
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
|
||||
const nameField = useRef(null);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const cleanData = {
|
||||
...data,
|
||||
name: data.name.trim(),
|
||||
};
|
||||
|
||||
if (!cleanData.name) {
|
||||
nameField.current.select();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dequal(cleanData, defaultData)) {
|
||||
onUpdate(cleanData);
|
||||
}
|
||||
|
||||
onClose();
|
||||
}, [defaultData, onUpdate, onClose, data]);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
openStep(StepTypes.DELETE);
|
||||
}, [openStep]);
|
||||
|
||||
useEffect(() => {
|
||||
nameField.current.select();
|
||||
}, []);
|
||||
|
||||
if (step && step.type === StepTypes.DELETE) {
|
||||
return (
|
||||
<DeleteStep
|
||||
title={t('common.deleteBoard', {
|
||||
context: 'title',
|
||||
})}
|
||||
content={t('common.areYouSureYouWantToDeleteThisBoard')}
|
||||
buttonContent={t('action.deleteBoard')}
|
||||
onConfirm={onDelete}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t('common.editBoard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div className={styles.text}>{t('common.title')}</div>
|
||||
<Input
|
||||
fluid
|
||||
ref={nameField}
|
||||
name="name"
|
||||
value={data.name}
|
||||
className={styles.field}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
<Button positive content={t('action.save')} />
|
||||
</Form>
|
||||
<Button
|
||||
content={t('action.delete')}
|
||||
className={styles.deleteButton}
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
EditStep.propTypes = {
|
||||
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withPopup(EditStep);
|
||||
@@ -1,19 +0,0 @@
|
||||
:global(#app) {
|
||||
.deleteButton {
|
||||
bottom: 12px;
|
||||
box-shadow: 0 1px 0 #cbcccc;
|
||||
position: absolute;
|
||||
right: 9px;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #444444;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import Boards from './Boards';
|
||||
|
||||
export default Boards;
|
||||
@@ -1,248 +0,0 @@
|
||||
import pick from 'lodash/pick';
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Menu } from 'semantic-ui-react';
|
||||
import { withPopup } from '../../lib/popup';
|
||||
import { Popup } from '../../lib/custom-ui';
|
||||
|
||||
import { useSteps } from '../../hooks';
|
||||
import BoardMembershipsStep from '../BoardMembershipsStep';
|
||||
import LabelsStep from '../LabelsStep';
|
||||
import DueDateEditStep from '../DueDateEditStep';
|
||||
import TimerEditStep from '../TimerEditStep';
|
||||
import CardMoveStep from '../CardMoveStep';
|
||||
import DeleteStep from '../DeleteStep';
|
||||
|
||||
import styles from './ActionsPopup.module.scss';
|
||||
|
||||
const StepTypes = {
|
||||
USERS: 'USERS',
|
||||
LABELS: 'LABELS',
|
||||
EDIT_DUE_DATE: 'EDIT_DUE_DATE',
|
||||
EDIT_TIMER: 'EDIT_TIMER',
|
||||
MOVE: 'MOVE',
|
||||
DELETE: 'DELETE',
|
||||
};
|
||||
|
||||
const ActionsStep = React.memo(
|
||||
({
|
||||
card,
|
||||
projectsToLists,
|
||||
boardMemberships,
|
||||
currentUserIds,
|
||||
labels,
|
||||
currentLabelIds,
|
||||
onNameEdit,
|
||||
onUpdate,
|
||||
onMove,
|
||||
onTransfer,
|
||||
onDelete,
|
||||
onUserAdd,
|
||||
onUserRemove,
|
||||
onBoardFetch,
|
||||
onLabelAdd,
|
||||
onLabelRemove,
|
||||
onLabelCreate,
|
||||
onLabelUpdate,
|
||||
onLabelDelete,
|
||||
onClose,
|
||||
}) => {
|
||||
const [t] = useTranslation();
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
|
||||
const handleEditNameClick = useCallback(() => {
|
||||
onNameEdit();
|
||||
onClose();
|
||||
}, [onNameEdit, onClose]);
|
||||
|
||||
const handleUsersClick = useCallback(() => {
|
||||
openStep(StepTypes.USERS);
|
||||
}, [openStep]);
|
||||
|
||||
const handleLabelsClick = useCallback(() => {
|
||||
openStep(StepTypes.LABELS);
|
||||
}, [openStep]);
|
||||
|
||||
const handleEditDueDateClick = useCallback(() => {
|
||||
openStep(StepTypes.EDIT_DUE_DATE);
|
||||
}, [openStep]);
|
||||
|
||||
const handleEditTimerClick = useCallback(() => {
|
||||
openStep(StepTypes.EDIT_TIMER);
|
||||
}, [openStep]);
|
||||
|
||||
const handleMoveClick = useCallback(() => {
|
||||
openStep(StepTypes.MOVE);
|
||||
}, [openStep]);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
openStep(StepTypes.DELETE);
|
||||
}, [openStep]);
|
||||
|
||||
const handleDueDateUpdate = useCallback(
|
||||
(dueDate) => {
|
||||
onUpdate({
|
||||
dueDate,
|
||||
});
|
||||
},
|
||||
[onUpdate],
|
||||
);
|
||||
|
||||
const handleTimerUpdate = useCallback(
|
||||
(timer) => {
|
||||
onUpdate({
|
||||
timer,
|
||||
});
|
||||
},
|
||||
[onUpdate],
|
||||
);
|
||||
|
||||
if (step) {
|
||||
switch (step.type) {
|
||||
case StepTypes.USERS:
|
||||
return (
|
||||
<BoardMembershipsStep
|
||||
items={boardMemberships}
|
||||
currentUserIds={currentUserIds}
|
||||
onUserSelect={onUserAdd}
|
||||
onUserDeselect={onUserRemove}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
case StepTypes.LABELS:
|
||||
return (
|
||||
<LabelsStep
|
||||
items={labels}
|
||||
currentIds={currentLabelIds}
|
||||
onSelect={onLabelAdd}
|
||||
onDeselect={onLabelRemove}
|
||||
onCreate={onLabelCreate}
|
||||
onUpdate={onLabelUpdate}
|
||||
onDelete={onLabelDelete}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
case StepTypes.EDIT_DUE_DATE:
|
||||
return (
|
||||
<DueDateEditStep
|
||||
defaultValue={card.dueDate}
|
||||
onUpdate={handleDueDateUpdate}
|
||||
onBack={handleBack}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
case StepTypes.EDIT_TIMER:
|
||||
return (
|
||||
<TimerEditStep
|
||||
defaultValue={card.timer}
|
||||
onUpdate={handleTimerUpdate}
|
||||
onBack={handleBack}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
case StepTypes.MOVE:
|
||||
return (
|
||||
<CardMoveStep
|
||||
projectsToLists={projectsToLists}
|
||||
defaultPath={pick(card, ['projectId', 'boardId', 'listId'])}
|
||||
onMove={onMove}
|
||||
onTransfer={onTransfer}
|
||||
onBoardFetch={onBoardFetch}
|
||||
onBack={handleBack}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
case StepTypes.DELETE:
|
||||
return (
|
||||
<DeleteStep
|
||||
title={t('common.deleteCard', {
|
||||
context: 'title',
|
||||
})}
|
||||
content={t('common.areYouSureYouWantToDeleteThisCard')}
|
||||
buttonContent={t('action.deleteCard')}
|
||||
onConfirm={onDelete}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t('common.cardActions', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
|
||||
{t('action.editTitle', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleUsersClick}>
|
||||
{t('common.members', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleLabelsClick}>
|
||||
{t('common.labels', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditDueDateClick}>
|
||||
{t('action.editDueDate', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditTimerClick}>
|
||||
{t('action.editTimer', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleMoveClick}>
|
||||
{t('action.moveCard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
|
||||
{t('action.deleteCard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ActionsStep.propTypes = {
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
card: PropTypes.object.isRequired,
|
||||
projectsToLists: PropTypes.array.isRequired,
|
||||
boardMemberships: PropTypes.array.isRequired,
|
||||
currentUserIds: PropTypes.array.isRequired,
|
||||
labels: PropTypes.array.isRequired,
|
||||
currentLabelIds: PropTypes.array.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
onNameEdit: PropTypes.func.isRequired,
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onMove: PropTypes.func.isRequired,
|
||||
onTransfer: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onUserAdd: PropTypes.func.isRequired,
|
||||
onUserRemove: PropTypes.func.isRequired,
|
||||
onBoardFetch: PropTypes.func.isRequired,
|
||||
onLabelAdd: PropTypes.func.isRequired,
|
||||
onLabelRemove: PropTypes.func.isRequired,
|
||||
onLabelCreate: PropTypes.func.isRequired,
|
||||
onLabelUpdate: PropTypes.func.isRequired,
|
||||
onLabelDelete: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withPopup(ActionsStep);
|
||||
@@ -1,11 +0,0 @@
|
||||
:global(#app) {
|
||||
.menu {
|
||||
margin: -7px -12px -5px;
|
||||
width: calc(100% + 24px);
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
margin: 0;
|
||||
padding-left: 14px;
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Button, Icon } from 'semantic-ui-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
import { startTimer, stopTimer } from '../../utils/timer';
|
||||
import Paths from '../../constants/Paths';
|
||||
import Tasks from './Tasks';
|
||||
import NameEdit from './NameEdit';
|
||||
import ActionsPopup from './ActionsPopup';
|
||||
import User from '../User';
|
||||
import Label from '../Label';
|
||||
import DueDate from '../DueDate';
|
||||
import Timer from '../Timer';
|
||||
|
||||
import styles from './Card.module.scss';
|
||||
|
||||
const Card = React.memo(
|
||||
({
|
||||
id,
|
||||
index,
|
||||
name,
|
||||
dueDate,
|
||||
timer,
|
||||
coverUrl,
|
||||
boardId,
|
||||
listId,
|
||||
projectId,
|
||||
isPersisted,
|
||||
notificationsTotal,
|
||||
users,
|
||||
labels,
|
||||
tasks,
|
||||
allProjectsToLists,
|
||||
allBoardMemberships,
|
||||
allLabels,
|
||||
canEdit,
|
||||
onUpdate,
|
||||
onMove,
|
||||
onTransfer,
|
||||
onDelete,
|
||||
onUserAdd,
|
||||
onUserRemove,
|
||||
onBoardFetch,
|
||||
onLabelAdd,
|
||||
onLabelRemove,
|
||||
onLabelCreate,
|
||||
onLabelUpdate,
|
||||
onLabelDelete,
|
||||
}) => {
|
||||
const nameEdit = useRef(null);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleToggleTimerClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
onUpdate({
|
||||
timer: timer.startedAt ? stopTimer(timer) : startTimer(timer),
|
||||
});
|
||||
},
|
||||
[timer, onUpdate],
|
||||
);
|
||||
|
||||
const handleNameUpdate = useCallback(
|
||||
(newName) => {
|
||||
onUpdate({
|
||||
name: newName,
|
||||
});
|
||||
},
|
||||
[onUpdate],
|
||||
);
|
||||
|
||||
const handleNameEdit = useCallback(() => {
|
||||
nameEdit.current.open();
|
||||
}, []);
|
||||
|
||||
const contentNode = (
|
||||
<>
|
||||
{coverUrl && <img src={coverUrl} alt="" className={styles.cover} />}
|
||||
<div className={styles.details}>
|
||||
{labels.length > 0 && (
|
||||
<span className={styles.labels}>
|
||||
{labels.map((label) => (
|
||||
<span
|
||||
key={label.id}
|
||||
className={classNames(styles.attachment, styles.attachmentLeft)}
|
||||
>
|
||||
<Label name={label.name} color={label.color} size="tiny" />
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
<div className={styles.name}>{name}</div>
|
||||
{tasks.length > 0 && <Tasks items={tasks} />}
|
||||
{(dueDate || timer || notificationsTotal > 0) && (
|
||||
<span className={styles.attachments}>
|
||||
{notificationsTotal > 0 && (
|
||||
<span
|
||||
className={classNames(
|
||||
styles.attachment,
|
||||
styles.attachmentLeft,
|
||||
styles.notification,
|
||||
)}
|
||||
>
|
||||
{notificationsTotal}
|
||||
</span>
|
||||
)}
|
||||
{dueDate && (
|
||||
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
||||
<DueDate value={dueDate} size="tiny" />
|
||||
</span>
|
||||
)}
|
||||
{timer && (
|
||||
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
||||
<Timer
|
||||
as="span"
|
||||
startedAt={timer.startedAt}
|
||||
total={timer.total}
|
||||
size="tiny"
|
||||
onClick={canEdit ? handleToggleTimerClick : undefined}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{users.length > 0 && (
|
||||
<span className={classNames(styles.attachments, styles.attachmentsRight)}>
|
||||
{users.map((user) => (
|
||||
<span
|
||||
key={user.id}
|
||||
className={classNames(styles.attachment, styles.attachmentRight)}
|
||||
>
|
||||
<User name={user.name} avatarUrl={user.avatarUrl} size="small" />
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Draggable draggableId={`card:${id}`} index={index} isDragDisabled={!isPersisted || !canEdit}>
|
||||
{({ innerRef, draggableProps, dragHandleProps }) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<div {...draggableProps} {...dragHandleProps} ref={innerRef} className={styles.wrapper}>
|
||||
<NameEdit ref={nameEdit} defaultValue={name} onUpdate={handleNameUpdate}>
|
||||
<div className={styles.card}>
|
||||
{isPersisted ? (
|
||||
<>
|
||||
<Link
|
||||
to={Paths.CARDS.replace(':id', id)}
|
||||
className={styles.content}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{contentNode}
|
||||
</Link>
|
||||
{canEdit && (
|
||||
<ActionsPopup
|
||||
card={{
|
||||
dueDate,
|
||||
timer,
|
||||
boardId,
|
||||
listId,
|
||||
projectId,
|
||||
}}
|
||||
projectsToLists={allProjectsToLists}
|
||||
boardMemberships={allBoardMemberships}
|
||||
currentUserIds={users.map((user) => user.id)}
|
||||
labels={allLabels}
|
||||
currentLabelIds={labels.map((label) => label.id)}
|
||||
onNameEdit={handleNameEdit}
|
||||
onUpdate={onUpdate}
|
||||
onMove={onMove}
|
||||
onTransfer={onTransfer}
|
||||
onDelete={onDelete}
|
||||
onUserAdd={onUserAdd}
|
||||
onUserRemove={onUserRemove}
|
||||
onBoardFetch={onBoardFetch}
|
||||
onLabelAdd={onLabelAdd}
|
||||
onLabelRemove={onLabelRemove}
|
||||
onLabelCreate={onLabelCreate}
|
||||
onLabelUpdate={onLabelUpdate}
|
||||
onLabelDelete={onLabelDelete}
|
||||
>
|
||||
<Button className={classNames(styles.actionsButton, styles.target)}>
|
||||
<Icon fitted name="pencil" size="small" />
|
||||
</Button>
|
||||
</ActionsPopup>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className={styles.content}>{contentNode}</span>
|
||||
)}
|
||||
</div>
|
||||
</NameEdit>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Card.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
dueDate: PropTypes.instanceOf(Date),
|
||||
timer: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
coverUrl: PropTypes.string,
|
||||
boardId: PropTypes.string.isRequired,
|
||||
listId: PropTypes.string.isRequired,
|
||||
projectId: PropTypes.string.isRequired,
|
||||
isPersisted: PropTypes.bool.isRequired,
|
||||
notificationsTotal: PropTypes.number.isRequired,
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
users: PropTypes.array.isRequired,
|
||||
labels: PropTypes.array.isRequired,
|
||||
tasks: PropTypes.array.isRequired,
|
||||
allProjectsToLists: PropTypes.array.isRequired,
|
||||
allBoardMemberships: PropTypes.array.isRequired,
|
||||
allLabels: PropTypes.array.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onMove: PropTypes.func.isRequired,
|
||||
onTransfer: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onUserAdd: PropTypes.func.isRequired,
|
||||
onUserRemove: PropTypes.func.isRequired,
|
||||
onBoardFetch: PropTypes.func.isRequired,
|
||||
onLabelAdd: PropTypes.func.isRequired,
|
||||
onLabelRemove: PropTypes.func.isRequired,
|
||||
onLabelCreate: PropTypes.func.isRequired,
|
||||
onLabelUpdate: PropTypes.func.isRequired,
|
||||
onLabelDelete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Card.defaultProps = {
|
||||
dueDate: undefined,
|
||||
timer: undefined,
|
||||
coverUrl: undefined,
|
||||
};
|
||||
|
||||
export default Card;
|
||||
@@ -1,124 +0,0 @@
|
||||
:global(#app) {
|
||||
.actionsButton {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: 3px;
|
||||
box-sizing: content-box;
|
||||
color: #798d99;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
min-height: auto;
|
||||
opacity: 0;
|
||||
outline: none;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
transition: background 85ms ease;
|
||||
width: 20px;
|
||||
|
||||
&:hover {
|
||||
background: #ebeef0;
|
||||
color: #516b7a;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment {
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
margin: 0 0 6px 0;
|
||||
max-width: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.attachmentLeft {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.attachmentRight {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.attachments {
|
||||
display: inline-block;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.attachmentsRight {
|
||||
float: right;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 0 #ccc;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: #f5f6f7;
|
||||
border-bottom-color: rgba(9, 30, 66, 0.25);
|
||||
|
||||
.target {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: block;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
.cover {
|
||||
border-radius: 3px 3px 0 0;
|
||||
margin-bottom: 2px;
|
||||
padding: 2px 2px 0;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.details {
|
||||
padding: 6px 8px 0;
|
||||
}
|
||||
|
||||
.labels {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: #17394d;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
padding-bottom: 6px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.notification {
|
||||
background: #eb5a46;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 0px 6px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
outline: none;
|
||||
text-align: left;
|
||||
transition: background 0.3s ease;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
cursor: auto;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { Button, Form, TextArea } from 'semantic-ui-react';
|
||||
|
||||
import { useClosableForm, useField } from '../../hooks';
|
||||
|
||||
import styles from './NameEdit.module.scss';
|
||||
|
||||
const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) => {
|
||||
const [t] = useTranslation();
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
const [value, handleFieldChange, setValue] = useField(defaultValue);
|
||||
|
||||
const field = useRef(null);
|
||||
|
||||
const open = useCallback(() => {
|
||||
setIsOpened(true);
|
||||
setValue(defaultValue);
|
||||
}, [defaultValue, setValue]);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setIsOpened(false);
|
||||
setValue(null);
|
||||
}, [setValue]);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
const cleanValue = value.trim();
|
||||
|
||||
if (!cleanValue) {
|
||||
field.current.ref.current.select();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cleanValue !== defaultValue) {
|
||||
onUpdate(cleanValue);
|
||||
}
|
||||
|
||||
close();
|
||||
}, [defaultValue, onUpdate, value, close]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
open,
|
||||
close,
|
||||
}),
|
||||
[open, close],
|
||||
);
|
||||
|
||||
const handleFieldKeyDown = useCallback(
|
||||
(event) => {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
|
||||
submit();
|
||||
|
||||
break;
|
||||
case 'Escape':
|
||||
close();
|
||||
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
[close, submit],
|
||||
);
|
||||
|
||||
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
|
||||
close,
|
||||
isOpened,
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
submit();
|
||||
}, [submit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpened) {
|
||||
field.current.ref.current.focus();
|
||||
}
|
||||
}, [isOpened]);
|
||||
|
||||
if (!isOpened) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div className={styles.fieldWrapper}>
|
||||
<TextArea
|
||||
ref={field}
|
||||
as={TextareaAutosize}
|
||||
value={value}
|
||||
minRows={3}
|
||||
maxRows={8}
|
||||
spellCheck={false}
|
||||
className={styles.field}
|
||||
onKeyDown={handleFieldKeyDown}
|
||||
onChange={handleFieldChange}
|
||||
onBlur={handleFieldBlur}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.controls}>
|
||||
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
|
||||
<Button
|
||||
positive
|
||||
content={t('action.save')}
|
||||
className={styles.submitButton}
|
||||
onMouseOver={handleControlMouseOver}
|
||||
onMouseOut={handleControlMouseOut}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
||||
NameEdit.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
defaultValue: PropTypes.string.isRequired,
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(NameEdit);
|
||||
@@ -1,26 +0,0 @@
|
||||
:global(#app) {
|
||||
.field {
|
||||
border: none;
|
||||
margin-bottom: 4px;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.fieldWrapper {
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 0 #ccc;
|
||||
margin-bottom: 8px;
|
||||
min-height: 20px;
|
||||
padding: 6px 8px 2px;
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
margin-bottom: 8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Progress } from 'semantic-ui-react';
|
||||
import { useToggle } from '../../lib/hooks';
|
||||
|
||||
import styles from './Tasks.module.scss';
|
||||
|
||||
const Tasks = React.memo(({ items }) => {
|
||||
const [isOpened, toggleOpened] = useToggle();
|
||||
|
||||
const handleToggleClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
toggleOpened();
|
||||
},
|
||||
[toggleOpened],
|
||||
);
|
||||
|
||||
const completedItems = items.filter((item) => item.isCompleted);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* eslint-disable jsx-a11y/click-events-have-key-events,
|
||||
jsx-a11y/no-static-element-interactions */}
|
||||
<div className={styles.button} onClick={handleToggleClick}>
|
||||
{/* eslint-enable jsx-a11y/click-events-have-key-events,
|
||||
jsx-a11y/no-static-element-interactions */}
|
||||
<span className={styles.progressWrapper}>
|
||||
<Progress
|
||||
autoSuccess
|
||||
value={completedItems.length}
|
||||
total={items.length}
|
||||
color="blue"
|
||||
size="tiny"
|
||||
className={styles.progress}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className={classNames(styles.count, isOpened ? styles.countOpened : styles.countClosed)}
|
||||
>
|
||||
{completedItems.length}/{items.length}
|
||||
</span>
|
||||
</div>
|
||||
{isOpened && (
|
||||
<ul className={styles.tasks}>
|
||||
{items.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className={classNames(styles.task, item.isCompleted && styles.taskCompleted)}
|
||||
>
|
||||
{item.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
Tasks.propTypes = {
|
||||
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
export default Tasks;
|
||||
@@ -1,82 +0,0 @@
|
||||
:global(#app) {
|
||||
.button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
line-height: 0;
|
||||
margin: 0 -8px;
|
||||
outline: none;
|
||||
padding: 0px 8px 8px;
|
||||
width: calc(100% + 16px);
|
||||
}
|
||||
|
||||
.count {
|
||||
color: #888;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
width: 50px;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
.countOpened:after {
|
||||
background: url("data:image/gif;base64,R0lGODlhCwALAJEAAAAAAP///xUVFf///yH5BAEAAAMALAAAAAALAAsAAAIPnI+py+0/hJzz0IruwjsVADs=")
|
||||
no-repeat center right;
|
||||
margin-left: 2px;
|
||||
padding: 6px 6px 0px;
|
||||
}
|
||||
|
||||
.countClosed:after {
|
||||
background: url("data:image/gif;base64,R0lGODlhCwALAJEAAAAAAP///xUVFf///yH5BAEAAAMALAAAAAALAAsAAAIRnC2nKLnT4or00Puy3rx7VQAAOw==")
|
||||
no-repeat center right;
|
||||
margin-left: 2px;
|
||||
padding: 0 6px 6px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.progressWrapper {
|
||||
display: inline-block;
|
||||
padding: 3px 0;
|
||||
vertical-align: top;
|
||||
width: calc(100% - 50px);
|
||||
}
|
||||
|
||||
.task {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
padding-bottom: 6px;
|
||||
padding-left: 14px;
|
||||
word-break: break-all;
|
||||
|
||||
&:before {
|
||||
content: "–";
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.taskCompleted {
|
||||
color: #aaa;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.tasks {
|
||||
color: #333;
|
||||
list-style: none;
|
||||
margin: -2px 0 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import Card from './Card';
|
||||
|
||||
export default Card;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user