Compare commits

..
73 Commits
Author SHA1 Message Date
meltyshev 2944206fac Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-09-16 19:24:49 +00:00
Maksim Eltyshev 9fadb8978c Updates 2024-09-16 00:30:04 +02:00
meltyshev 1eba6917a5 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-08-14 19:42:52 +00:00
meltyshev 5b0902ab3e Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-08-12 21:24:23 +00:00
meltyshev 50b1275d08 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-07-21 17:46:48 +00:00
meltyshev 02d2a5d278 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-07-17 12:03:31 +00:00
meltyshev 8fd4394b8e Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-07-15 14:25:01 +00:00
meltyshev 2bee32b79b Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-06-21 10:11:46 +00:00
meltyshev 31860d164b Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-06-14 15:19:10 +00:00
meltyshev 6f8ba52593 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-06-14 14:42:57 +00:00
meltyshev 165e5c6e86 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-06-12 07:08:47 +00:00
meltyshev aeff1eb5d0 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-06-11 23:04:52 +00:00
meltyshev 5eba8b7fda Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-06-01 23:37:00 +00:00
meltyshev fa3bb3e803 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-05-21 21:32:33 +00:00
meltyshev 22ad2360a5 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-05-21 11:58:59 +00:00
Maksim Eltyshev c83244da31 chore: Restore index.yaml
Closes #763
2024-05-21 13:36:41 +02:00
Maksim Eltyshev 2a6740a1f0 Updates 2024-05-16 21:13:16 +02:00
meltyshev c32baac2c5 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-05-16 19:07:01 +00:00
meltyshev 4d4f5a1f89 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-05-03 10:46:36 +00:00
meltyshev 773e99e36e Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-04-23 13:57:47 +00:00
meltyshev c55e811ba2 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-04-22 21:27:02 +00:00
meltyshev c63d5bea8c Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-04-12 10:11:23 +00:00
meltyshev 2e73b08ba2 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-04-10 13:57:48 +00:00
meltyshev d58674c56a Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-04-10 10:56:47 +00:00
meltyshev 0c7e8dc634 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-04-05 20:49:30 +00:00
meltyshev a750f5c7b6 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-04-05 08:39:10 +00:00
meltyshev 759eb50b49 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-04-03 18:59:47 +00:00
meltyshev 0010051149 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-03-26 13:09:27 +00:00
Maksim Eltyshev 915cce046e Updates 2024-03-22 14:27:49 +01:00
meltyshev 467c837589 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-03-21 23:53:16 +00:00
Maksim Eltyshev b2c7261cd3 Updates 2024-03-19 22:11:17 +01:00
meltyshev 9c29656f23 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-03-12 19:44:55 +00:00
Maksim Eltyshev c17ca9d5e8 Updates 2024-02-05 13:32:34 +01:00
meltyshev c08ac75512 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-01-31 23:35:05 +00:00
meltyshev 13e15a3954 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-01-31 14:59:27 +00:00
meltyshev 1db2271e77 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2024-01-18 10:02:34 +00:00
meltyshev 9a9596db90 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2023-12-20 12:49:57 +00:00
meltyshev a640c10270 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2023-12-11 00:18:34 +01:00
meltyshev 8c3c53d3e0 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2023-11-20 13:16:38 +00:00
meltyshev 913d79fabc Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2023-11-17 21:13:26 +00:00
meltyshev ec74e38392 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2023-10-25 21:42:11 +00:00
daniel-hiller f5ef07f493 Update index.yaml
Signed-off-by: daniel-hiller <daniel-hiller@users.noreply.github.com>
2023-10-22 20:49:11 +00:00
meltyshev 55b31459bf Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2023-10-20 20:39:08 +00:00
meltyshev 5b87537262 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2023-10-19 21:09:29 +00:00
meltyshev fb7dafba9b Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2023-10-19 14:08:16 +00:00
meltyshev 67fbd23cc4 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2023-10-18 21:34:51 +00:00
meltyshev b62f3adf3a Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2023-10-17 20:31:54 +00:00
meltyshev 0a57440fe2 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2023-08-26 21:27:13 +00:00
meltyshev afe6da7599 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2023-06-12 22:46:47 +00:00
meltyshev 0114af1128 Update index.yaml
Signed-off-by: meltyshev <meltyshev@users.noreply.github.com>
2023-05-25 15:23:55 +00:00
Maksim Eltyshev 0cbf36f9e0 Updates 2023-02-27 19:38:16 +01:00
Maksim Eltyshev a9f1546741 Updates 2022-12-26 23:13:30 +01:00
Maksim Eltyshev 76ca466725 Updates 2022-12-24 02:38:46 +01:00
Maksim Eltyshev b5a6179af7 Updates 2022-12-21 23:54:41 +01:00
Maksim Eltyshev 0262213f8d Updates 2022-11-20 16:03:56 +01:00
Maksim Eltyshev a065c328f8 Updates 2022-11-07 18:02:53 +01:00
Maksim Eltyshev 968708cd41 Updates 2022-10-04 00:50:05 +02:00
Maksim Eltyshev 89411d4437 Updates 2022-09-28 18:51:40 +02:00
Maksim Eltyshev ce57fbc410 Updates 2022-09-16 15:02:14 +05:00
Maksim Eltyshev 47eb32d841 Updates 2022-09-03 23:00:15 +05:00
Maksim Eltyshev 8b5fdd05e1 Updates 2022-09-02 18:01:43 +05:00
Maksim Eltyshev ea742e2682 Updates 2022-09-02 17:53:09 +05:00
Maksim Eltyshev 809ace238e Updates 2022-08-26 19:21:52 +02:00
Maksim Eltyshev c787454796 Updates 2022-08-26 02:50:45 +02:00
Maksim Eltyshev 6a0c3c32d9 Updates 2022-08-25 14:32:00 +02:00
Maksim Eltyshev 4ca45d33be Updates 2022-08-19 14:52:47 +02:00
Maksim Eltyshev ba160b9028 Updates 2022-08-17 02:35:46 +02:00
Maksim Eltyshev 8a26e41e07 Updates 2022-08-01 00:23:11 +02:00
Maksim Eltyshev 238456f931 Updates 2022-07-29 21:23:54 +02:00
Maksim Eltyshev ac1cf9c070 Updates 2022-07-21 22:04:33 +02:00
Maksim Eltyshev 69ab041b2b Updates 2022-07-21 21:03:49 +02:00
Maksim Eltyshev fc7bc7d74c Updates 2022-07-05 19:43:10 +02:00
Maksim Eltyshev 01c1afb329 Updates 2020-06-13 01:43:50 +05:00
898 changed files with 3337 additions and 142745 deletions
-29
View File
@@ -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
View File
@@ -1 +0,0 @@
client/src/lib/custom-ui/styles.css linguist-vendored
-13
View File
@@ -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
View File
@@ -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
-4
View File
@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
-5
View File
@@ -1,5 +0,0 @@
{
"recommendations": [
"dbaeumer.vscode-eslint"
]
}
-18
View File
@@ -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"
}
}
-76
View File
@@ -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
-49
View File
@@ -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
View File
@@ -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"]
-27
View File
@@ -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}
-21
View File
@@ -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.
-305
View File
@@ -1,305 +0,0 @@
# Planka
![David (path)](https://img.shields.io/github/package-json/v/plankanban/planka) ![Docker Pulls](https://img.shields.io/docker/pulls/meltyshev/planka) ![GitHub](https://img.shields.io/github/license/plankanban/planka)
A Trello-like kanban board built with React and Redux.
![](https://raw.githubusercontent.com/plankanban/planka/master/demo.gif)
[**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://d207aa93qlcgug.cloudfront.net/1.95.5.qa/img/nav/docker-logo-loggedout.png)](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's already available in Planka, or you can also 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
View File
@@ -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!
+80
View File
@@ -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"
]
}
-23
View File
@@ -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
View File
@@ -1 +0,0 @@
# Planka client
-56
View File
@@ -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;
};
-40648
View File
File diff suppressed because it is too large Load Diff
-114
View File
@@ -1,114 +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-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-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"
}
}
-44
View File
@@ -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>
-79
View File
@@ -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,
};
-99
View File
@@ -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,
};
-138
View File
@@ -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,
};
-145
View File
@@ -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,
};
-99
View File
@@ -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,
};
-75
View File
@@ -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,
};
-55
View File
@@ -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,
};
-39
View File
@@ -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,
};
-183
View File
@@ -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,
};
-99
View File
@@ -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,
};
-32
View File
@@ -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,
};
-18
View File
@@ -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,
};
-46
View File
@@ -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,
};
-110
View File
@@ -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,
};
-125
View File
@@ -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,
};
-43
View File
@@ -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,
};
-58
View File
@@ -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,
};
-99
View File
@@ -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,
};
-310
View File
@@ -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,
};
-14
View File
@@ -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,
};
-36
View File
@@ -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,
};
-51
View File
@@ -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,
};
-18
View File
@@ -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,
};
-34
View File
@@ -1,34 +0,0 @@
import socket from './socket';
import http from './http';
import { transformCard } from './cards';
import { transformAttachment } from './attachments';
/* Actions */
const createBoard = (projectId, data, headers) =>
socket.post(`/projects/${projectId}/boards`, data, headers);
const createBoardWithImport = (projectId, data, requestId, headers) =>
http.post(`/projects/${projectId}/boards?requestId=${requestId}`, 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,
createBoardWithImport,
getBoard,
updateBoard,
deleteBoard,
};
-14
View File
@@ -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,
};
-14
View File
@@ -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,
};
-94
View File
@@ -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,
};
-28
View File
@@ -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,
};
-37
View File
@@ -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;
-39
View File
@@ -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,
};
-16
View File
@@ -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,
};
-16
View File
@@ -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,
};
-61
View File
@@ -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,
};
-14
View File
@@ -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,
};
-26
View File
@@ -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,
};
-40
View File
@@ -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;
-15
View File
@@ -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,
};
-39
View File
@@ -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;
@@ -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;
@@ -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: 60vh;
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;
-25
View File
@@ -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,116 +0,0 @@
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Icon } from 'semantic-ui-react';
import { useDidUpdate, useToggle } from '../../../lib/hooks';
import { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui';
import { useForm, useSteps } from '../../../hooks';
import ImportStep from './ImportStep';
import styles from './AddPopup.module.scss';
const StepTypes = {
IMPORT: 'IMPORT',
};
const AddStep = React.memo(({ onCreate, onClose }) => {
const [t] = useTranslation();
const [data, handleFieldChange, setData] = useForm({
name: '',
import: null,
});
const [step, openStep, handleBack] = useSteps();
const [focusNameFieldState, focusNameField] = useToggle();
const nameField = useRef(null);
const handleImportSelect = useCallback(
(nextImport) => {
setData((prevData) => ({
...prevData,
import: nextImport,
}));
},
[setData],
);
const handleImportBack = useCallback(() => {
handleBack();
focusNameField();
}, [handleBack, focusNameField]);
const handleSubmit = useCallback(() => {
const cleanData = {
...data,
type: 'kanban',
name: data.name.trim(),
};
if (!cleanData.name) {
nameField.current.select();
return;
}
onCreate(cleanData);
onClose();
}, [onClose, data, onCreate]);
const handleImportClick = useCallback(() => {
openStep(StepTypes.IMPORT);
}, [openStep]);
useEffect(() => {
nameField.current.focus();
}, []);
useDidUpdate(() => {
nameField.current.focus();
}, [focusNameFieldState]);
if (step && step.type === StepTypes.IMPORT) {
return <ImportStep onSelect={handleImportSelect} onBack={handleImportBack} />;
}
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}
/>
<div className={styles.controls}>
<Button positive content={t('action.createBoard')} className={styles.createButton} />
<Button type="button" className={styles.importButton} onClick={handleImportClick}>
<Icon
name={data.import ? data.import.type : 'arrow down'}
className={styles.importButtonIcon}
/>
{data.import ? data.import.file.name : t('action.import')}
</Button>
</div>
</Form>
</Popup.Content>
</>
);
});
AddStep.propTypes = {
onCreate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(AddStep);
@@ -1,42 +0,0 @@
:global(#app) {
.controls {
display: flex;
max-width: 280px;
@media only screen and (max-width: 767px) {
max-width: 226px;
}
}
.createButton {
white-space: nowrap;
}
.field {
margin-bottom: 8px;
}
.importButton {
background: transparent;
box-shadow: none;
color: #6b808c;
font-weight: normal;
margin-left: auto;
margin-right: 0;
overflow: hidden;
text-align: left;
text-decoration: underline;
text-overflow: ellipsis;
transition: none;
white-space: nowrap;
&:hover {
background: rgba(9, 30, 66, 0.08);
color: #092d42;
}
}
.importButtonIcon {
text-decoration: none;
}
}
@@ -1,51 +0,0 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { FilePicker, Popup } from '../../../lib/custom-ui';
import styles from './ImportStep.module.scss';
const ImportStep = React.memo(({ onSelect, onBack }) => {
const [t] = useTranslation();
const handleFileSelect = useCallback(
(type, file) => {
onSelect({
type,
file,
});
onBack();
},
[onSelect, onBack],
);
return (
<>
<Popup.Header onBack={onBack}>
{t('common.importBoard', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<FilePicker onSelect={(file) => handleFileSelect('trello', file)} accept=".json">
<Button
fluid
type="button"
icon="trello"
content={t('common.fromTrello')}
className={styles.button}
/>
</FilePicker>
</Popup.Content>
</>
);
});
ImportStep.propTypes = {
onSelect: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
export default ImportStep;
@@ -1,17 +0,0 @@
:global(#app) {
.button {
background: transparent;
box-shadow: none;
color: #6b808c;
font-weight: normal;
margin-top: 8px;
padding: 6px 11px;
text-align: left;
transition: none;
&:hover {
background: rgba(9, 30, 66, 0.08);
color: #092d42;
}
}
}
@@ -1,3 +0,0 @@
import AddPopup from './AddPopup';
export default AddPopup;
-138
View File
@@ -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,115 +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: rgba(255, 255, 255, 0.72);
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);
}
.link {
color: #fff;
font-weight: bold;
}
}
.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;
}
}
-107
View File
@@ -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;
}
}
-3
View File
@@ -1,3 +0,0 @@
import Boards from './Boards';
export default Boards;
-248
View File
@@ -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;
}
}
-253
View File
@@ -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;
-124
View File
@@ -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;
}
}
-126
View File
@@ -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;
}
}

Some files were not shown because too many files have changed in this diff Show More