Compare commits

..
56 Commits
Author SHA1 Message Date
Maksim Eltyshev 8d74cc1732 chore: Update version 2024-07-15 16:24:31 +02:00
Emmanuel Guyot ea94fe8bf3 fix: Order users by name (#806) 2024-07-15 15:26:26 +02:00
Αλέξανδρος 6052f8999f fix: Fix application crashing when webhook is unavailable (#815) 2024-07-15 14:40:33 +02:00
ItzAndriss 67d1b1c194 fix: Update Hungarian translation (#823) 2024-07-15 14:25:31 +02:00
Stephan Michard b3cb8405f5 fix: Update German translation (#821) 2024-07-15 14:19:06 +02:00
Maksim Eltyshev 7acaec6d04 fix: Fix incorrect related data when transferring card
Closes #431, closes #803
2024-07-15 14:01:06 +02:00
Maksim Eltyshev 72896a66b3 chore: Update version 2024-06-21 12:11:15 +02:00
Maksim Eltyshev 4be4783251 fix: Fix icon spacing for action buttons 2024-06-21 12:01:46 +02:00
HannesOberreiter 5c6c6a0bcf feat: Add copy link action to card modal (#804) 2024-06-21 11:48:36 +02:00
HannesOberreiter ba178d66fa docs: Add types to webhook function (#796) 2024-06-18 15:41:12 +02:00
HannesOberreiter 270ce29f87 feat: Add custom user agent with base url to webhook (#799) 2024-06-18 15:36:51 +02:00
HannesOberreiter de1ed3c784 ref: Define all possible event types in webhook module (#795) 2024-06-18 15:32:39 +02:00
Maksim Eltyshev caf8790f28 chore: Update version 2024-06-14 17:18:42 +02:00
Sarah Soo 81436b373d feat: Ability to specify existing secrets for values in Helm (#791)
Closes #790
2024-06-14 16:41:10 +02:00
NathanVss cc1e886a31 feat: Ability to allow everyone to create projects (#787) 2024-06-14 16:38:06 +02:00
Maksim Eltyshev f0e9fe7904 chore: Update version 2024-06-12 09:06:20 +02:00
Gavin Mogan 5fcea5d651 fix: Handle WEBHOOKS env variable being unset (#785)
Closes #784
2024-06-12 08:59:36 +02:00
Maksim Eltyshev a963d85a66 chore: Update version 2024-06-12 01:02:55 +02:00
Samuel 04b42decef meta: Add issue templates (#782) 2024-06-12 00:59:37 +02:00
Maksim Eltyshev c065566c15 feat: Webhooks configuration, all events support, refactoring 2024-06-12 00:51:36 +02:00
HannesOberreiter 3779bdb053 feat: Events via webhook (#771)
Closes #215, closes #656
2024-06-06 20:22:14 +02:00
HannesOberreiter 4124ab17d2 fix: Focus end of field when editing (#779) 2024-06-06 20:06:16 +02:00
Maksim Eltyshev 0e3bc92a61 chore: Update version 2024-06-02 01:36:29 +02:00
Maksim Eltyshev b8d7e713b9 chore: Update dependencies 2024-06-02 01:34:03 +02:00
Emmanuel Guyot 017eee8302 fix: Use server base path for socket connection (#772)
Closes #768
2024-05-23 23:59:44 +02:00
Mitch Harvey 1b28892cf5 fix: Fix Helm template for dburl secret (#770) 2024-05-21 23:32:07 +02:00
Maksim Eltyshev 013ea1b869 chore: Update version 2024-05-21 13:58:28 +02:00
Blyamur 4975fa2eeb fix: Update Russian translation (#767) 2024-05-21 13:51:57 +02:00
Blyamur 3e28ad87c7 fix: Update Russian translation (#766) 2024-05-21 13:51:34 +02:00
Smiley3112 2d6666d693 feat: Add SMTP_NAME environment variable (#761)
Closes #758
2024-05-18 15:02:21 +02:00
Maksim Eltyshev 4cbd2f85c7 chore: Update version 2024-05-16 21:06:30 +02:00
Maksim Eltyshev fae9de4e1c fix: Prevent loading fonts from third-party resources
Closes #538, closes #712, closes #742, closes #755
2024-05-16 21:02:58 +02:00
IT Creativity + Art Team 89c1ed71e1 feat: Add Bulgarian translation (#753) 2024-05-13 13:05:20 +02:00
Mitch Harvey 1725f3cb8a feat: Ability to specify existing secret as database url (#751)
Closes #750
2024-05-13 11:07:57 +02:00
Christopher Greaves f7be49e402 docs: Update Kubernetes README (#740) 2024-05-03 21:04:34 +02:00
Maksim Eltyshev c8d732d936 chore: Update version 2024-05-03 12:44:51 +02:00
Maksim Eltyshev 5a32b6327c fix: Fix actions with members
Closes #737
2024-05-03 12:42:51 +02:00
Emmanuel Guyot 8488105810 feat: Search in card descriptions (#729) 2024-04-26 21:52:57 +02:00
Niccolò Pedrini 57ebfa51d8 fix: Update Italian translation (#727) 2024-04-24 18:49:27 +02:00
Maksim Eltyshev 40170fdf90 chore: Update version 2024-04-23 15:57:14 +02:00
leroyloren 1629f62d09 fix: Update Czech translation (#725) 2024-04-23 15:53:10 +02:00
Maksim Eltyshev fff0552081 chore: Update dependencies
Closes #726
2024-04-23 15:45:47 +02:00
Maksim Eltyshev e792cb26b1 chore: Update version 2024-04-22 23:25:09 +02:00
Maksim Eltyshev 7ee2d76be4 feat: Display avatar next to user name in top bar 2024-04-22 23:23:36 +02:00
Emmanuel Guyot eb56b2147b feat: Filter cards by keyword with advanced capabilities (#713)
Closes #706
2024-04-22 23:15:31 +02:00
Maksim Eltyshev 8747aa59de fix: Fix title of sort menu item 2024-04-22 22:02:51 +02:00
Samuel 934dcdf39b feat: Sort cards within list (#717)
Closes #390
2024-04-22 21:56:07 +02:00
Felipe 2c84316fe0 fix: Limit amount of displayed board members (#715)
Closes #617
2024-04-22 15:28:57 +02:00
Daniel Hiller 03825c3bed CI: Fix dev Build 2024-04-16 03:43:54 +02:00
ItzAndriss 200ea3bbc0 feat: Add Hungarian translation (#590) 2024-04-15 18:30:18 +02:00
Robson Ventura 5b4b0cd635 feat: Development environment with docker compose (#709) 2024-04-15 16:22:51 +02:00
Gavin Mogan a1323cd516 feat: Set labels when building docker image 2024-04-15 16:19:53 +02:00
Maksim Eltyshev 92f75789fa chore: Update version 2024-04-12 12:09:22 +02:00
Maksim Eltyshev 8a8c1fee0c fix: Fix error output when sending email or message to Slack 2024-04-12 12:07:19 +02:00
Maksim Eltyshev 16499052f7 chore: Update version 2024-04-10 15:57:15 +02:00
HannesOberreiter cc32daa009 feat: Display clickable links in tasks (#694)
Closes #330
2024-04-10 15:53:05 +02:00
199 changed files with 19914 additions and 15408 deletions
+53
View File
@@ -0,0 +1,53 @@
name: "🐛 Bug Report"
description: Report a bug found while using Planka
title: "[Bug]: "
labels: ["Type: Bug", "Status: Triage"]
body:
- type: dropdown
id: issue-type
attributes:
label: Where is the problem occurring?
description: Select the part of the application where you encountered the issue.
options:
- "I encountered the problem while using the application (Frontend)"
- "I encountered the problem while interacting with the server (Backend)"
- "I'm not sure"
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Brave
- Chrome
- Firefox
- Microsoft Edge
- Safari
- Other
- type: textarea
id: current-behavior
attributes:
label: Current behaviour
description: A description of what is currently happening, including screenshots and other useful information (**DO NOT INCLUDE PRIVATE INFORMATION**).
placeholder: Currently...
validations:
required: true
- type: textarea
id: desired-behavior
attributes:
label: Desired behaviour
description: A clear description of what you think should happen.
placeholder: In this situation, I expected ...
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: Clearly describe which steps or actions you have taken to arrive at the problem. If you have some experience with the code, please link to the specific pieces of code.
placeholder: I did X, then Y, before arriving at Z, when ERROR ...
validations:
required: true
- type: textarea
id: other
attributes:
label: Other information
description: Any other details?
@@ -0,0 +1,33 @@
name: "✨ Feature Request"
description: Suggest a feature or enhancement to improve Planka.
labels: ["Type: Idea"]
body:
- type: dropdown
id: idea-type
attributes:
label: Is this a feature for the backend or frontend?
multiple: true
options:
- Backend
- Frontend
validations:
required: true
- type: textarea
id: feature
attributes:
label: What would you like?
description: A clear description of the feature or enhancement wanted.
placeholder: I'd like to be able to...
validations:
required: true
- type: textarea
id: reason
attributes:
label: Why is this needed?
description: A clear description of why this would be useful to have.
placeholder: I want this because...
- type: textarea
id: other
attributes:
label: Other information
placeholder: Any other details?
@@ -40,5 +40,5 @@ jobs:
build-args: ALPINE_VERSION=${{ env.ALPINE_VERSION }}
push: true
tags: |
ghcr.io/plankanban/planka:base-latest
ghcr.io/plankanban/planka:base-${{ env.ALPINE_VERSION }}
ghcr.io/${{ github.repository }}:base-latest
ghcr.io/${{ github.repository }}:base-${{ env.ALPINE_VERSION }}
@@ -9,99 +9,45 @@ on:
- 'docker-*.sh'
- '*.md'
branches: [master]
workflow_dispatch:
env:
REGISTRY_IMAGE: ghcr.io/plankanban/planka
REGISTRY_IMAGE: ghcr.io/${{ github.repository }}
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: [self-hosted, x64]
platform: linux/amd64
- os: [self-hosted, arm64]
platform: linux/arm64
- os: [self-hosted, arm64]
platform: linux/arm/v7
runs-on: ${{ matrix.os }}
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v5
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
type=raw,value=dev
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v3
with:
name: digests
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: [self-hosted]
needs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v3
with:
name: digests
path: /tmp/digests
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
rerun-failed-jobs:
runs-on: [self-hosted]
needs: [ build, merge]
if: failure()
steps:
- name: Rerun failed jobs in the current workflow
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh run rerun ${{ github.run_id }} --failed
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -31,12 +31,23 @@ jobs:
result-encoding: string
script: return context.payload.release.tag_name.replace('v', '')
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v5
with:
images: |
name=ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ steps.set-version.outputs.result }}
type=raw,value=latest
- name: Build and push
uses: docker/build-push-action@v4
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 }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
Executable → Regular
-3
View File
@@ -1,4 +1 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
+2 -2
View File
@@ -15,13 +15,13 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.24
version: 0.2.3
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.2"
appVersion: "1.19.2"
dependencies:
- alias: postgresql
+6 -2
View File
@@ -14,11 +14,15 @@ If you want to fully uninstall this chart including the data, follow [these step
## Usage
If you just want to spin up an instance using help, please see [these docs](https://docs.planka.cloud/docs/installation/kubernetes/helm_chart/). If you want to make changes to the chart locally, and deploy them, see the below section.
## Local Building and Using the Chart
The basic usage of the chart can be found below:
```bash
git clone https://github.com/Chris-Greaves/planka-helm-chart.git
cd planka-helm-chart
git clone https://github.com/plankanban/planka.git
cd planka/charts/planka
helm dependency build
export SECRETKEY=$(openssl rand -hex 64)
helm install planka . --set secretkey=$SECRETKEY \
+31 -3
View File
@@ -64,8 +64,16 @@ spec:
{{- toYaml .Values.resources | nindent 12 }}
env:
{{- if not .Values.postgresql.enabled }}
{{- if .Values.existingDburlSecret }}
- name: DATABASE_URL
value: {{ required "If the included postgresql deployment is disabled you need to define a Database URL in 'dburl'" .Values.dburl }}
valueFrom:
secretKeyRef:
name: {{ .Values.existingDburlSecret }}
key: uri
{{- else }}
- name: DATABASE_URL
value: {{ required "If the included postgresql deployment is disabled you need to provide an existing secret in .Values.existingDburlSecret or define a Database URL in 'dburl'" .Values.dburl }}
{{- end }}
{{- else }}
- name: DATABASE_URL
valueFrom:
@@ -82,17 +90,37 @@ spec:
value: http://localhost:3000
{{- end }}
- name: SECRET_KEY
{{- if .Values.existingSecretkeySecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.existingSecretkeySecret }}
key: key
{{- else }}
value: {{ required "A secret key needs to be generated using 'openssl rand -hex 64' and assigned to secretkey." .Values.secretkey }}
{{- end }}
- name: TRUST_PROXY
value: "0"
- name: DEFAULT_ADMIN_EMAIL
value: {{ .Values.admin_email }}
- name: DEFAULT_ADMIN_PASSWORD
value: {{ .Values.admin_password }}
- name: DEFAULT_ADMIN_NAME
value: {{ .Values.admin_name }}
{{- if .Values.existingAdminCredsSecret }}
- name: DEFAULT_ADMIN_USERNAME
valueFrom:
secretKeyRef:
name: {{ .Values.existingAdminCredsSecret }}
key: username
- name: DEFAULT_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.existingAdminCredsSecret }}
key: password
{{- else }}
- name: DEFAULT_ADMIN_USERNAME
value: {{ .Values.admin_username }}
- name: DEFAULT_ADMIN_PASSWORD
value: {{ .Values.admin_password }}
{{- end }}
{{ range $k, $v := .Values.env }}
- name: {{ $k | quote }}
value: {{ $v | quote }}
+17 -1
View File
@@ -17,6 +17,16 @@ fullnameOverride: ""
# Generate a secret using openssl rand -base64 45
secretkey: ""
## @param existingSecretkeySecret Name of an existing secret containing the session key string
## NOTE: Must contain key `key`
## NOTE: When it's set, the secretkey parameter is ignored
existingSecretkeySecret: ""
## @param existingAdminCredsSecret Name of an existing secret containing the admin username and password
## NOTE: Must contain keys `username` and `password`
## NOTE: When it's set, the `admin_username` and `admin_password` parameters are ignored
existingAdminCredsSecret: ""
# Base url for Planka. Will override `ingress.hosts[0].host`
# Defaults to `http://localhost:3000` if ingress is disabled.
baseUrl: ""
@@ -105,9 +115,15 @@ postgresql:
serviceBindings:
enabled: true
## Set this if you disable the built-in postgresql deployment
## Set this or existingDburlSecret if you disable the built-in postgresql deployment
dburl:
## @param existingDburlSecret Name of an existing secret containing a DBurl connection string
## NOTE: Must contain key `uri`
## NOTE: When it's set, the `dburl` parameter is ignored
##
existingDburlSecret: ""
## PVC-based data storage configuration
persistence:
enabled: false
+1 -1
View File
@@ -1 +1 @@
REACT_APP_VERSION=1.16.2
REACT_APP_VERSION=1.19.2
+9461 -3572
View File
File diff suppressed because it is too large Load Diff
+22 -20
View File
@@ -60,62 +60,64 @@
},
"dependencies": {
"@juggle/resize-observer": "^3.4.0",
"classnames": "^2.3.2",
"classnames": "^2.5.1",
"date-fns": "^2.30.0",
"dequal": "^2.0.3",
"easymde": "^2.18.0",
"history": "^5.3.0",
"i18next": "^23.7.6",
"i18next-browser-languagedetector": "^7.2.0",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^8.0.0",
"initials": "^3.1.2",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"linkify-react": "^4.1.3",
"linkifyjs": "^4.1.3",
"lodash": "^4.17.21",
"nanoid": "^5.0.3",
"nanoid": "^5.0.7",
"node-sass": "^9.0.0",
"photoswipe": "^5.4.2",
"photoswipe": "^5.4.4",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-app-rewired": "^2.2.1",
"react-beautiful-dnd": "^13.1.1",
"react-datepicker": "^4.21.0",
"react-datepicker": "^4.25.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-i18next": "^13.5.0",
"react-i18next": "^14.1.2",
"react-input-mask": "^2.0.4",
"react-markdown": "^8.0.7",
"react-photoswipe-gallery": "^2.2.7",
"react-redux": "^8.1.3",
"react-router-dom": "^6.19.0",
"react-router-dom": "^6.23.1",
"react-scripts": "5.0.1",
"react-simplemde-editor": "^5.2.0",
"react-textarea-autosize": "^8.5.3",
"redux": "^4.2.1",
"redux-logger": "^3.0.6",
"redux-orm": "^0.16.2",
"redux-saga": "^1.2.3",
"redux-saga": "^1.3.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^3.0.1",
"reselect": "^4.1.8",
"sails.io.js": "^1.2.1",
"semantic-ui-react": "^2.1.4",
"semantic-ui-react": "^2.1.5",
"socket.io-client": "^2.5.0",
"validator": "^13.11.0",
"whatwg-fetch": "^3.6.19",
"validator": "^13.12.0",
"whatwg-fetch": "^3.6.20",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.1.0",
"@testing-library/user-event": "^14.5.1",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@testing-library/user-event": "^14.5.2",
"babel-preset-airbnb": "^5.0.0",
"chai": "^4.3.10",
"eslint": "^8.53.0",
"chai": "^4.4.1",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react": "^7.34.2",
"eslint-plugin-react-hooks": "^4.6.2",
"react-test-renderer": "^18.2.0"
}
}
+15 -1
View File
@@ -57,10 +57,15 @@ updateCard.failure = (id, error) => ({
},
});
const handleCardUpdate = (card) => ({
const handleCardUpdate = (card, isFetched, cardMemberships, cardLabels, tasks, attachments) => ({
type: ActionTypes.CARD_UPDATE_HANDLE,
payload: {
card,
isFetched,
cardMemberships,
cardLabels,
tasks,
attachments,
},
});
@@ -121,6 +126,14 @@ const handleCardDelete = (card) => ({
},
});
const filterText = (boardId, text) => ({
type: ActionTypes.TEXT_FILTER_IN_CURRENT_BOARD,
payload: {
boardId,
text,
},
});
export default {
createCard,
handleCardCreate,
@@ -129,4 +142,5 @@ export default {
duplicateCard,
deleteCard,
handleCardDelete,
filterText,
};
+34
View File
@@ -60,6 +60,38 @@ const handleListUpdate = (list) => ({
},
});
const sortList = (id, data) => ({
type: ActionTypes.LIST_SORT,
payload: {
id,
data,
},
});
sortList.success = (list, cards) => ({
type: ActionTypes.LIST_SORT__SUCCESS,
payload: {
list,
cards,
},
});
sortList.failure = (id, error) => ({
type: ActionTypes.LIST_SORT__FAILURE,
payload: {
id,
error,
},
});
const handleListSort = (list, cards) => ({
type: ActionTypes.LIST_SORT_HANDLE,
payload: {
list,
cards,
},
});
const deleteList = (id) => ({
type: ActionTypes.LIST_DELETE,
payload: {
@@ -94,6 +126,8 @@ export default {
handleListCreate,
updateList,
handleListUpdate,
sortList,
handleListSort,
deleteList,
handleListDelete,
};
+24
View File
@@ -1,4 +1,5 @@
import socket from './socket';
import { transformCard } from './cards';
/* Actions */
@@ -7,10 +8,33 @@ const createList = (boardId, data, headers) =>
const updateList = (id, data, headers) => socket.patch(`/lists/${id}`, data, headers);
const sortList = (id, data, headers) =>
socket.post(`/lists/${id}/sort`, data, headers).then((body) => ({
...body,
included: {
...body.included,
cards: body.included.cards.map(transformCard),
},
}));
const deleteList = (id, headers) => socket.delete(`/lists/${id}`, undefined, headers);
/* Event handlers */
const makeHandleListSort = (next) => (body) => {
next({
...body,
included: {
...body.included,
cards: body.included.cards.map(transformCard),
},
});
};
export default {
createList,
updateList,
sortList,
deleteList,
makeHandleListSort,
};
+1 -1
View File
@@ -13,7 +13,7 @@ io.sails.environment = process.env.NODE_ENV;
const { socket } = io;
socket.path = `${Config.BASE_PATH}/socket.io`;
socket.path = `${Config.SERVER_BASE_PATH}/socket.io`;
socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => {
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,6 +13,7 @@ const BoardActions = React.memo(
labels,
filterUsers,
filterLabels,
filterText,
allUsers,
canEdit,
canEditMemberships,
@@ -27,6 +28,7 @@ const BoardActions = React.memo(
onLabelUpdate,
onLabelMove,
onLabelDelete,
onTextFilterUpdate,
}) => {
return (
<div className={styles.wrapper}>
@@ -46,6 +48,7 @@ const BoardActions = React.memo(
<Filters
users={filterUsers}
labels={filterLabels}
filterText={filterText}
allBoardMemberships={memberships}
allLabels={labels}
canEdit={canEdit}
@@ -57,6 +60,7 @@ const BoardActions = React.memo(
onLabelUpdate={onLabelUpdate}
onLabelMove={onLabelMove}
onLabelDelete={onLabelDelete}
onTextFilterUpdate={onTextFilterUpdate}
/>
</div>
</div>
@@ -71,6 +75,7 @@ BoardActions.propTypes = {
labels: PropTypes.array.isRequired,
filterUsers: PropTypes.array.isRequired,
filterLabels: PropTypes.array.isRequired,
filterText: PropTypes.string.isRequired,
allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
canEdit: PropTypes.bool.isRequired,
@@ -86,6 +91,7 @@ BoardActions.propTypes = {
onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
onTextFilterUpdate: PropTypes.func.isRequired,
};
export default BoardActions;
@@ -1,5 +1,7 @@
:global(#app) {
.action {
align-items: center;
display: flex;
flex: 0 0 auto;
margin-right: 20px;
}
+65 -1
View File
@@ -1,7 +1,10 @@
import React, { useCallback } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Icon } from 'semantic-ui-react';
import { usePopup } from '../../lib/popup';
import { Input } from '../../lib/custom-ui';
import User from '../User';
import Label from '../Label';
@@ -14,6 +17,7 @@ const Filters = React.memo(
({
users,
labels,
filterText,
allBoardMemberships,
allLabels,
canEdit,
@@ -25,8 +29,17 @@ const Filters = React.memo(
onLabelUpdate,
onLabelMove,
onLabelDelete,
onTextFilterUpdate,
}) => {
const [t] = useTranslation();
const [isSearchFocused, setIsSearchFocused] = useState(false);
const searchFieldRef = useRef(null);
const cancelSearch = useCallback(() => {
onTextFilterUpdate('');
searchFieldRef.current.blur();
}, [onTextFilterUpdate]);
const handleRemoveUserClick = useCallback(
(id) => {
@@ -42,9 +55,39 @@ const Filters = React.memo(
[onLabelRemove],
);
const handleSearchChange = useCallback(
(_, { value }) => {
onTextFilterUpdate(value);
},
[onTextFilterUpdate],
);
const handleSearchFocus = useCallback(() => {
setIsSearchFocused(true);
}, []);
const handleSearchKeyDown = useCallback(
(event) => {
if (event.key === 'Escape') {
cancelSearch();
}
},
[cancelSearch],
);
const handleSearchBlur = useCallback(() => {
setIsSearchFocused(false);
}, []);
const handleCancelSearchClick = useCallback(() => {
cancelSearch();
}, [cancelSearch]);
const BoardMembershipsPopup = usePopup(BoardMembershipsStep);
const LabelsPopup = usePopup(LabelsStep);
const isSearchActive = filterText || isSearchFocused;
return (
<>
<span className={styles.filter}>
@@ -100,6 +143,25 @@ const Filters = React.memo(
</span>
))}
</span>
<span className={styles.filter}>
<Input
ref={searchFieldRef}
value={filterText}
placeholder={t('common.searchCards')}
icon={
isSearchActive ? (
<Icon link name="cancel" onClick={handleCancelSearchClick} />
) : (
'search'
)
}
className={classNames(styles.search, !isSearchActive && styles.searchInactive)}
onFocus={handleSearchFocus}
onKeyDown={handleSearchKeyDown}
onChange={handleSearchChange}
onBlur={handleSearchBlur}
/>
</span>
</>
);
},
@@ -109,6 +171,7 @@ Filters.propTypes = {
/* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
filterText: PropTypes.string.isRequired,
allBoardMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
@@ -121,6 +184,7 @@ Filters.propTypes = {
onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
onTextFilterUpdate: PropTypes.func.isRequired,
};
export default Filters;
@@ -43,4 +43,32 @@
line-height: 20px;
padding: 2px 12px;
}
.search {
height: 30px;
margin: 0 12px;
transition: width 0.2s ease;
width: 280px;
input {
font-size: 13px;
}
}
.searchInactive {
color: #fff;
height: 24px;
width: 220px;
input {
background: rgba(0, 0, 0, 0.24);
border: none;
color: #fff !important;
font-size: 12px;
&::placeholder {
color: #fff;
}
}
}
}
+2 -1
View File
@@ -5,6 +5,7 @@ import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { useClosableForm, useField } from '../../hooks';
import { focusEnd } from '../../utils/element-helpers';
import styles from './NameEdit.module.scss';
@@ -79,7 +80,7 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
useEffect(() => {
if (isOpened) {
field.current.ref.current.focus();
focusEnd(field.current.ref.current);
}
}, [isOpened]);
+3 -1
View File
@@ -4,6 +4,8 @@ import classNames from 'classnames';
import { Progress } from 'semantic-ui-react';
import { useToggle } from '../../lib/hooks';
import Linkify from '../Linkify';
import styles from './Tasks.module.scss';
const Tasks = React.memo(({ items }) => {
@@ -48,7 +50,7 @@ const Tasks = React.memo(({ items }) => {
key={item.id}
className={classNames(styles.task, item.isCompleted && styles.taskCompleted)}
>
{item.name}
<Linkify linkStopPropagation>{item.name}</Linkify>
</li>
))}
</ul>
@@ -55,8 +55,10 @@
display: block;
font-size: 12px;
line-height: 14px;
overflow: hidden;
padding-bottom: 6px;
padding-left: 14px;
text-overflow: ellipsis;
&:before {
content: "–";
@@ -6,6 +6,7 @@ import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { useForm } from '../../../hooks';
import { focusEnd } from '../../../utils/element-helpers';
import styles from './CommentEdit.module.scss';
@@ -70,7 +71,7 @@ const CommentEdit = React.forwardRef(({ children, defaultData, onUpdate }, ref)
useEffect(() => {
if (isOpened) {
textField.current.ref.current.focus();
focusEnd(textField.current.ref.current);
}
}, [isOpened]);
+18 -1
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useRef } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
@@ -81,6 +81,7 @@ const CardModal = React.memo(
onClose,
}) => {
const [t] = useTranslation();
const [isLinkCopied, setIsLinkCopied] = useState(false);
const isGalleryOpened = useRef(false);
@@ -146,6 +147,14 @@ const CardModal = React.memo(
onClose();
}, [onDuplicate, onClose]);
const handleCopyLinkClick = useCallback(() => {
navigator.clipboard.writeText(window.location.href);
setIsLinkCopied(true);
setTimeout(() => {
setIsLinkCopied(false);
}, 5000);
}, []);
const handleGalleryOpen = useCallback(() => {
isGalleryOpened.current = true;
}, []);
@@ -506,6 +515,14 @@ const CardModal = React.memo(
<Icon name="copy outline" className={styles.actionIcon} />
{t('action.duplicate')}
</Button>
<Button fluid className={styles.actionButton} onClick={handleCopyLinkClick}>
<Icon name={isLinkCopied ? 'linkify' : 'unlink'} className={styles.actionIcon} />
{isLinkCopied
? t('common.linkIsCopied')
: t('action.copyLink', {
context: 'title',
})}
</Button>
<DeletePopup
title="common.deleteCard"
content="common.areYouSureYouWantToDeleteThisCard"
@@ -20,7 +20,6 @@
.actionIcon {
color: #17394d;
display: inline;
margin-right: 8px;
}
@@ -57,6 +57,7 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
const mdEditorOptions = useMemo(
() => ({
autoDownloadFontAwesome: false,
autofocus: true,
spellChecker: false,
status: false,
@@ -8,6 +8,7 @@ import { usePopup } from '../../../lib/popup';
import NameEdit from './NameEdit';
import ActionsStep from './ActionsStep';
import Linkify from '../../Linkify';
import styles from './Item.module.scss';
@@ -65,7 +66,7 @@ const Item = React.memo(
onClick={handleClick}
>
<span className={classNames(styles.task, isCompleted && styles.taskCompleted)}>
{name}
<Linkify linkStopPropagation>{name}</Linkify>
</span>
</span>
{isPersisted && canEdit && (
@@ -5,6 +5,7 @@ import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { useField } from '../../../hooks';
import { focusEnd } from '../../../utils/element-helpers';
import styles from './NameEdit.module.scss';
@@ -65,7 +66,7 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
useEffect(() => {
if (isOpened) {
field.current.ref.current.focus();
focusEnd(field.current.ref.current);
}
}, [isOpened]);
+3 -1
View File
@@ -7,6 +7,7 @@ import { usePopup } from '../../lib/popup';
import Paths from '../../constants/Paths';
import NotificationsStep from './NotificationsStep';
import User from '../User';
import UserStep from '../UserStep';
import styles from './Header.module.scss';
@@ -91,7 +92,8 @@ const Header = React.memo(
onLogout={onLogout}
>
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
{user.name}
<span className={styles.userName}>{user.name}</span>
<User name={user.name} avatarUrl={user.avatarUrl} size="small" />
</Menu.Item>
</UserPopup>
</Menu.Menu>
@@ -86,6 +86,10 @@
font-weight: bold;
}
.userName {
margin-right: 10px;
}
.wrapper {
background: rgba(0, 0, 0, 0.24);
display: flex;
+68
View File
@@ -0,0 +1,68 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import LinkifyReact from 'linkify-react';
import history from '../history';
const Linkify = React.memo(({ children, linkStopPropagation, ...props }) => {
const handleLinkClick = useCallback(
(event) => {
if (linkStopPropagation) {
event.stopPropagation();
}
if (!event.target.getAttribute('target')) {
event.preventDefault();
history.push(event.target.href);
}
},
[linkStopPropagation],
);
const linkRenderer = useCallback(
({ attributes: { href, ...linkProps }, content }) => {
let url;
try {
url = new URL(href, window.location);
} catch (error) {} // eslint-disable-line no-empty
const isSameSite = !!url && url.origin === window.location.origin;
return (
<a
{...linkProps} // eslint-disable-line react/jsx-props-no-spreading
href={href}
target={isSameSite ? undefined : '_blank'}
rel={isSameSite ? undefined : 'noreferrer'}
onClick={handleLinkClick}
>
{isSameSite ? url.pathname : content}
</a>
);
},
[handleLinkClick],
);
return (
<LinkifyReact
{...props} // eslint-disable-line react/jsx-props-no-spreading
options={{
defaultProtocol: 'https',
render: linkRenderer,
}}
>
{children}
</LinkifyReact>
);
});
Linkify.propTypes = {
children: PropTypes.string.isRequired,
linkStopPropagation: PropTypes.bool,
};
Linkify.defaultProps = {
linkStopPropagation: false,
};
export default Linkify;
+31 -2
View File
@@ -5,15 +5,17 @@ import { Menu } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks';
import ListSortStep from '../ListSortStep';
import DeleteStep from '../DeleteStep';
import styles from './ActionsStep.module.scss';
const StepTypes = {
DELETE: 'DELETE',
SORT: 'SORT',
};
const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) => {
const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onSort, onDelete, onClose }) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
@@ -27,11 +29,30 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
onClose();
}, [onCardAdd, onClose]);
const handleSortClick = useCallback(() => {
openStep(StepTypes.SORT);
}, [openStep]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
if (step && step.type === StepTypes.DELETE) {
const handleSortTypeSelect = useCallback(
(type) => {
onSort({
type,
});
onClose();
},
[onSort, onClose],
);
if (step && step.type) {
switch (step.type) {
case StepTypes.SORT:
return <ListSortStep onTypeSelect={handleSortTypeSelect} onBack={handleBack} />;
case StepTypes.DELETE:
return (
<DeleteStep
title="common.deleteList"
@@ -41,6 +62,8 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
onBack={handleBack}
/>
);
default:
}
}
return (
@@ -62,6 +85,11 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleSortClick}>
{t('action.sortList', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteList', {
context: 'title',
@@ -77,6 +105,7 @@ ActionsStep.propTypes = {
onNameEdit: PropTypes.func.isRequired,
onCardAdd: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
+14 -1
View File
@@ -16,7 +16,18 @@ import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-ic
import styles from './List.module.scss';
const List = React.memo(
({ id, index, name, isPersisted, cardIds, canEdit, onUpdate, onDelete, onCardCreate }) => {
({
id,
index,
name,
isPersisted,
cardIds,
canEdit,
onUpdate,
onDelete,
onSort,
onCardCreate,
}) => {
const [t] = useTranslation();
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
@@ -114,6 +125,7 @@ const List = React.memo(
onNameEdit={handleNameEdit}
onCardAdd={handleCardAdd}
onDelete={onDelete}
onSort={onSort}
>
<Button className={classNames(styles.headerButton, styles.target)}>
<Icon fitted name="pencil" size="small" />
@@ -159,6 +171,7 @@ List.propTypes = {
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onCardCreate: PropTypes.func.isRequired,
};
+2 -1
View File
@@ -4,6 +4,7 @@ import TextareaAutosize from 'react-textarea-autosize';
import { TextArea } from 'semantic-ui-react';
import { useField } from '../../hooks';
import { focusEnd } from '../../utils/element-helpers';
import styles from './NameEdit.module.scss';
@@ -71,7 +72,7 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
useEffect(() => {
if (isOpened) {
field.current.ref.current.select();
focusEnd(field.current.ref.current);
}
}, [isOpened]);
@@ -0,0 +1,61 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { ListSortTypes } from '../../constants/Enums';
import styles from './ListSortStep.module.scss';
const ListSortStep = React.memo(({ onTypeSelect, onBack }) => {
const [t] = useTranslation();
return (
<>
<Popup.Header onBack={onBack}>
{t('common.sortList', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item
className={styles.menuItem}
onClick={() => onTypeSelect(ListSortTypes.NAME_ASC)}
>
{t('common.title')}
</Menu.Item>
<Menu.Item
className={styles.menuItem}
onClick={() => onTypeSelect(ListSortTypes.DUE_DATE_ASC)}
>
{t('common.dueDate')}
</Menu.Item>
<Menu.Item
className={styles.menuItem}
onClick={() => onTypeSelect(ListSortTypes.CREATED_AT_ASC)}
>
{t('common.oldestFirst')}
</Menu.Item>
<Menu.Item
className={styles.menuItem}
onClick={() => onTypeSelect(ListSortTypes.CREATED_AT_DESC)}
>
{t('common.newestFirst')}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
});
ListSortStep.propTypes = {
onTypeSelect: PropTypes.func.isRequired,
onBack: PropTypes.func,
};
ListSortStep.defaultProps = {
onBack: undefined,
};
export default ListSortStep;
@@ -0,0 +1,11 @@
:global(#app) {
.menu {
margin: -7px -12px -5px;
width: calc(100% + 24px);
}
.menuItem {
margin: 0;
padding-left: 14px;
}
}
@@ -0,0 +1,3 @@
import ListSortStep from './ListSortStep';
export default ListSortStep;
@@ -3,6 +3,7 @@ import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks';
import User from '../User';
@@ -19,6 +20,7 @@ const ActionsStep = React.memo(
({
membership,
permissionsSelectStep,
title,
leaveButtonContent,
leaveConfirmationTitle,
leaveConfirmationContent,
@@ -31,6 +33,7 @@ const ActionsStep = React.memo(
canLeave,
onUpdate,
onDelete,
onBack,
onClose,
}) => {
const [t] = useTranslation();
@@ -53,6 +56,11 @@ const ActionsStep = React.memo(
[onUpdate],
);
const handleDeleteConfirm = useCallback(() => {
onDelete();
onClose();
}, [onDelete, onClose]);
if (step) {
switch (step.type) {
case StepTypes.EDIT_PERMISSIONS: {
@@ -81,7 +89,7 @@ const ActionsStep = React.memo(
? leaveConfirmationButtonContent
: deleteConfirmationButtonContent
}
onConfirm={onDelete}
onConfirm={handleDeleteConfirm}
onBack={handleBack}
/>
);
@@ -89,7 +97,7 @@ const ActionsStep = React.memo(
}
}
return (
const contentNode = (
<>
<span className={styles.user}>
<User name={membership.user.name} avatarUrl={membership.user.avatarUrl} size="large" />
@@ -125,12 +133,26 @@ const ActionsStep = React.memo(
)}
</>
);
return onBack ? (
<>
<Popup.Header onBack={onBack}>
{t(title, {
context: 'title',
})}
</Popup.Header>
<Popup.Content>{contentNode}</Popup.Content>
</>
) : (
contentNode
);
},
);
ActionsStep.propTypes = {
membership: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
permissionsSelectStep: PropTypes.elementType,
title: PropTypes.string,
leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string,
@@ -143,11 +165,13 @@ ActionsStep.propTypes = {
canLeave: PropTypes.bool.isRequired,
onUpdate: PropTypes.func,
onDelete: PropTypes.func.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
ActionsStep.defaultProps = {
permissionsSelectStep: undefined,
title: 'common.memberActions',
leaveButtonContent: 'action.leaveBoard',
leaveConfirmationTitle: 'common.leaveBoard',
leaveConfirmationContent: 'common.areYouSureYouWantToLeaveBoard',
@@ -157,6 +181,7 @@ ActionsStep.defaultProps = {
deleteConfirmationContent: 'common.areYouSureYouWantToRemoveThisMemberFromBoard',
deleteConfirmationButtonContent: 'action.removeMember',
onUpdate: undefined,
onBack: undefined,
};
export default ActionsStep;
@@ -5,16 +5,21 @@ import { usePopup } from '../../lib/popup';
import AddStep from './AddStep';
import ActionsStep from './ActionsStep';
import MembershipsStep from './MembershipsStep';
import User from '../User';
import styles from './Memberships.module.scss';
const MAX_MEMBERS = 6;
const Memberships = React.memo(
({
items,
allUsers,
permissionsSelectStep,
title,
addTitle,
actionsTitle,
leaveButtonContent,
leaveConfirmationTitle,
leaveConfirmationContent,
@@ -31,11 +36,14 @@ const Memberships = React.memo(
}) => {
const AddPopup = usePopup(AddStep);
const ActionsPopup = usePopup(ActionsStep);
const MembershipsPopup = usePopup(MembershipsStep);
const remainMembersCount = items.length - MAX_MEMBERS;
return (
<>
<span className={styles.users}>
{items.map((item) => (
{items.slice(0, MAX_MEMBERS).map((item) => (
<span key={item.id} className={styles.user}>
<ActionsPopup
membership={item}
@@ -63,6 +71,30 @@ const Memberships = React.memo(
</span>
))}
</span>
{remainMembersCount > 0 && (
<MembershipsPopup
items={items}
permissionsSelectStep={permissionsSelectStep}
title={title}
actionsTitle={actionsTitle}
leaveButtonContent={leaveButtonContent}
leaveConfirmationTitle={leaveConfirmationTitle}
leaveConfirmationContent={leaveConfirmationContent}
leaveConfirmationButtonContent={leaveConfirmationButtonContent}
deleteButtonContent={deleteButtonContent}
deleteConfirmationTitle={deleteConfirmationTitle}
deleteConfirmationContent={deleteConfirmationContent}
deleteConfirmationButtonContent={deleteConfirmationButtonContent}
canEdit={canEdit}
canLeave={items.length > 1 || canLeaveIfLast}
onUpdate={onUpdate}
onDelete={onDelete}
>
<Button icon className={styles.addUser}>
+{remainMembersCount < 99 ? remainMembersCount : 99}
</Button>
</MembershipsPopup>
)}
{canEdit && (
<AddPopup
users={allUsers}
@@ -85,7 +117,9 @@ Memberships.propTypes = {
allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
permissionsSelectStep: PropTypes.elementType,
title: PropTypes.string,
addTitle: PropTypes.string,
actionsTitle: PropTypes.string,
leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string,
@@ -103,7 +137,9 @@ Memberships.propTypes = {
Memberships.defaultProps = {
permissionsSelectStep: undefined,
title: undefined,
addTitle: undefined,
actionsTitle: undefined,
leaveButtonContent: undefined,
leaveConfirmationTitle: undefined,
leaveConfirmationContent: undefined,
@@ -0,0 +1,121 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useSteps } from '../../hooks';
import ActionsStep from './ActionsStep';
import BoardMembershipsStep from '../BoardMembershipsStep';
const StepTypes = {
EDIT: 'EDIT',
};
const MembershipsStep = React.memo(
({
items,
permissionsSelectStep,
title,
actionsTitle,
leaveButtonContent,
leaveConfirmationTitle,
leaveConfirmationContent,
leaveConfirmationButtonContent,
deleteButtonContent,
deleteConfirmationTitle,
deleteConfirmationContent,
deleteConfirmationButtonContent,
canEdit,
canLeave,
onUpdate,
onDelete,
onClose,
}) => {
const [step, openStep, handleBack] = useSteps();
const handleUserSelect = useCallback(
(userId) => {
openStep(StepTypes.EDIT, {
userId,
});
},
[openStep],
);
if (step && step.type === StepTypes.EDIT) {
const currentItem = items.find((item) => item.userId === step.params.userId);
if (currentItem) {
return (
<ActionsStep
membership={currentItem}
permissionsSelectStep={permissionsSelectStep}
title={actionsTitle}
leaveButtonContent={leaveButtonContent}
leaveConfirmationTitle={leaveConfirmationTitle}
leaveConfirmationContent={leaveConfirmationContent}
leaveConfirmationButtonContent={leaveConfirmationButtonContent}
deleteButtonContent={deleteButtonContent}
deleteConfirmationTitle={deleteConfirmationTitle}
deleteConfirmationContent={deleteConfirmationContent}
deleteConfirmationButtonContent={deleteConfirmationButtonContent}
canEdit={canEdit}
canLeave={canLeave}
onUpdate={(data) => onUpdate(currentItem.id, data)}
onDelete={() => onDelete(currentItem.id)}
onBack={handleBack}
onClose={onClose}
/>
);
}
openStep(null);
}
return (
// FIXME: hack
<BoardMembershipsStep
items={items}
currentUserIds={[]}
title={title}
onUserSelect={handleUserSelect}
onUserDeselect={() => {}}
/>
);
},
);
MembershipsStep.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
permissionsSelectStep: PropTypes.elementType,
title: PropTypes.string,
actionsTitle: PropTypes.string,
leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string,
leaveConfirmationButtonContent: PropTypes.string,
deleteButtonContent: PropTypes.string,
deleteConfirmationTitle: PropTypes.string,
deleteConfirmationContent: PropTypes.string,
deleteConfirmationButtonContent: PropTypes.string,
canEdit: PropTypes.bool.isRequired,
canLeave: PropTypes.bool.isRequired,
onUpdate: PropTypes.func,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
MembershipsStep.defaultProps = {
permissionsSelectStep: undefined,
title: undefined,
actionsTitle: undefined,
leaveButtonContent: undefined,
leaveConfirmationTitle: undefined,
leaveConfirmationContent: undefined,
leaveConfirmationButtonContent: undefined,
deleteButtonContent: undefined,
deleteConfirmationTitle: undefined,
deleteConfirmationContent: undefined,
deleteConfirmationButtonContent: undefined,
onUpdate: undefined,
};
export default MembershipsStep;
@@ -12,7 +12,9 @@ const ManagersPane = React.memo(({ items, allUsers, onCreate, onDelete }) => {
<Memberships
items={items}
allUsers={allUsers}
title="common.managers"
addTitle="common.addManager"
actionsTitle="common.managerActions"
leaveButtonContent="action.leaveProject"
leaveConfirmationTitle="common.leaveProject"
leaveConfirmationContent="common.areYouSureYouWantToLeaveProject"
+1
View File
@@ -13,6 +13,7 @@ import 'react-datepicker/dist/react-datepicker.css';
import 'photoswipe/dist/photoswipe.css';
import 'easymde/dist/easymde.min.css';
import '../lib/custom-ui/styles.css';
import '../assets/css/font-awesome.css';
import '../styles.module.scss';
function Root({ store, history }) {
+5 -3
View File
@@ -173,6 +173,10 @@ export default {
LIST_UPDATE__SUCCESS: 'LIST_UPDATE__SUCCESS',
LIST_UPDATE__FAILURE: 'LIST_UPDATE__FAILURE',
LIST_UPDATE_HANDLE: 'LIST_UPDATE_HANDLE',
LIST_SORT: 'LIST_SORT',
LIST_SORT__SUCCESS: 'LIST_SORT__SUCCESS',
LIST_SORT__FAILURE: 'LIST_SORT__FAILURE',
LIST_SORT_HANDLE: 'LIST_SORT_HANDLE',
LIST_DELETE: 'LIST_DELETE',
LIST_DELETE__SUCCESS: 'LIST_DELETE__SUCCESS',
LIST_DELETE__FAILURE: 'LIST_DELETE__FAILURE',
@@ -191,9 +195,6 @@ export default {
CARD_UPDATE__SUCCESS: 'CARD_UPDATE__SUCCESS',
CARD_UPDATE__FAILURE: 'CARD_UPDATE__FAILURE',
CARD_UPDATE_HANDLE: 'CARD_UPDATE_HANDLE',
CARD_TRANSFER: 'CARD_TRANSFER',
CARD_TRANSFER__SUCCESS: 'CARD_TRANSFER__SUCCESS',
CARD_TRANSFER__FAILURE: 'CARD_TRANSFER__FAILURE',
CARD_DUPLICATE: 'CARD_DUPLICATE',
CARD_DUPLICATE__SUCCESS: 'CARD_DUPLICATE__SUCCESS',
CARD_DUPLICATE__FAILURE: 'CARD_DUPLICATE__FAILURE',
@@ -201,6 +202,7 @@ export default {
CARD_DELETE__SUCCESS: 'CARD_DELETE__SUCCESS',
CARD_DELETE__FAILURE: 'CARD_DELETE__FAILURE',
CARD_DELETE_HANDLE: 'CARD_DELETE_HANDLE',
TEXT_FILTER_IN_CURRENT_BOARD: 'TEXT_FILTER_IN_CURRENT_BOARD',
/* Tasks */
+2
View File
@@ -6,6 +6,7 @@ const BASE_PATH = BASE_URL.replace(/^.*\/\/[^/]*(.*)[^?#]*.*$/, '$1');
const SERVER_BASE_URL =
process.env.REACT_APP_SERVER_BASE_URL ||
(process.env.NODE_ENV === 'production' ? BASE_URL : 'http://localhost:1337');
const SERVER_BASE_PATH = SERVER_BASE_URL.replace(/^.*\/\/[^/]*(.*)[^?#]*.*$/, '$1');
const SERVER_HOST_NAME = SERVER_BASE_URL.replace(/^(.*\/\/[^/?#]*).*$/, '$1');
@@ -20,6 +21,7 @@ export default {
VERSION,
BASE_PATH,
SERVER_BASE_URL,
SERVER_BASE_PATH,
SERVER_HOST_NAME,
ACCESS_TOKEN_KEY,
ACCESS_TOKEN_VERSION_KEY,
+3
View File
@@ -120,6 +120,8 @@ export default {
LIST_MOVE: `${PREFIX}/LIST_MOVE`,
LIST_DELETE: `${PREFIX}/LIST_DELETE`,
LIST_DELETE_HANDLE: `${PREFIX}/LIST_DELETE_HANDLE`,
LIST_SORT: `${PREFIX}/LIST_SORT`,
LIST_SORT_HANDLE: `${PREFIX}/LIST_SORT_HANDLE`,
/* Cards */
@@ -137,6 +139,7 @@ export default {
CARD_DELETE: `${PREFIX}/CARD_DELETE`,
CURRENT_CARD_DELETE: `${PREFIX}/CURRENT_CARD_DELETE`,
CARD_DELETE_HANDLE: `${PREFIX}/CARD_DELETE_HANDLE`,
TEXT_FILTER_IN_CURRENT_BOARD: `${PREFIX}/FILTER_TEXT_HANDLE`,
/* Tasks */
+7
View File
@@ -8,6 +8,13 @@ export const BoardMembershipRoles = {
VIEWER: 'viewer',
};
export const ListSortTypes = {
NAME_ASC: 'name_asc',
DUE_DATE_ASC: 'dueDate_asc',
CREATED_AT_ASC: 'createdAt_asc',
CREATED_AT_DESC: 'createdAt_desc',
};
export const ActivityTypes = {
CREATE_CARD: 'createCard',
MOVE_CARD: 'moveCard',
@@ -13,6 +13,7 @@ const mapStateToProps = (state) => {
const labels = selectors.selectLabelsForCurrentBoard(state);
const filterUsers = selectors.selectFilterUsersForCurrentBoard(state);
const filterLabels = selectors.selectFilterLabelsForCurrentBoard(state);
const filterText = selectors.selectFilterTextForCurrentBoard(state);
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const isCurrentUserEditor =
@@ -23,6 +24,7 @@ const mapStateToProps = (state) => {
labels,
filterUsers,
filterLabels,
filterText,
allUsers,
canEdit: isCurrentUserEditor,
canEditMemberships: isCurrentUserManager,
@@ -43,6 +45,7 @@ const mapDispatchToProps = (dispatch) =>
onLabelUpdate: entryActions.updateLabel,
onLabelMove: entryActions.moveLabel,
onLabelDelete: entryActions.deleteLabel,
onTextFilterUpdate: entryActions.filterText,
},
dispatch,
);
+1
View File
@@ -33,6 +33,7 @@ const mapDispatchToProps = (dispatch, { id }) =>
bindActionCreators(
{
onUpdate: (data) => entryActions.updateList(id, data),
onSort: (data) => entryActions.sortList(id, data),
onDelete: () => entryActions.deleteList(id),
onCardCreate: (data, autoOpen) => entryActions.createCard(id, data, autoOpen),
},
+1 -1
View File
@@ -20,7 +20,7 @@ const mapStateToProps = (state) => {
isSubmittingUsingOidc,
error,
withOidc: !!oidcConfig,
isOidcEnforced: oidcConfig && oidcConfig.isEnforced,
isOidcEnforced: !!oidcConfig && oidcConfig.isEnforced,
};
};
+2 -1
View File
@@ -6,12 +6,13 @@ import entryActions from '../entry-actions';
import Projects from '../components/Projects';
const mapStateToProps = (state) => {
const { allowAllToCreateProjects } = selectors.selectConfig(state);
const { isAdmin } = selectors.selectCurrentUser(state);
const projects = selectors.selectProjectsForCurrentUser(state);
return {
items: projects,
canAdd: isAdmin,
canAdd: allowAllToCreateProjects || isAdmin,
};
};
+8
View File
@@ -105,6 +105,13 @@ const handleCardDelete = (card) => ({
},
});
const filterText = (text) => ({
type: EntryActionTypes.TEXT_FILTER_IN_CURRENT_BOARD,
payload: {
text,
},
});
export default {
createCard,
handleCardCreate,
@@ -120,4 +127,5 @@ export default {
deleteCard,
deleteCurrentCard,
handleCardDelete,
filterText,
};
+20
View File
@@ -37,6 +37,24 @@ const moveList = (id, index) => ({
},
});
const sortList = (id, data) => {
return {
type: EntryActionTypes.LIST_SORT,
payload: {
id,
data,
},
};
};
const handleListSort = (list, cards) => ({
type: EntryActionTypes.LIST_SORT_HANDLE,
payload: {
list,
cards,
},
});
const deleteList = (id) => ({
type: EntryActionTypes.LIST_DELETE,
payload: {
@@ -57,6 +75,8 @@ export default {
updateList,
handleListUpdate,
moveList,
sortList,
handleListSort,
deleteList,
handleListDelete,
};
@@ -9,4 +9,6 @@ export default class Input extends SemanticUIInput {
static Mask = InputMask;
focus = (options) => this.inputRef.current.focus(options);
blur = () => this.inputRef.current.blur();
}
+252
View File
@@ -0,0 +1,252 @@
import dateFns from 'date-fns/locale/bg';
export default {
dateFns,
format: {
date: 'd.MM.yyyy',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd. MMM',
longDateTime: "d. MMMM 'в' p",
fullDate: 'd. MMM. y',
fullDateTime: "d. MMMM. y 'в' p",
},
translation: {
common: {
aboutPlanka: 'За Planka',
account: 'Акаунт',
actions: 'Действия',
addAttachment_title: 'Добавяне на прикачен файл',
addComment: 'Добавяне на коментар',
addManager_title: 'Добавяне на мениджър',
addMember_title: 'Добавяне на член',
addUser_title: 'Добавяне на потребител',
administrator: 'Администратор',
all: 'Всички',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Всички промени ще бъдат автоматично запазени<br />след възстановяване на връзката.',
areYouSureYouWantToDeleteThisAttachment:
'Сигурни ли сте, че искате да изтриете този прикачен файл?',
areYouSureYouWantToDeleteThisBoard: 'Сигурни ли сте, че искате да изтриете това табло?',
areYouSureYouWantToDeleteThisCard: 'Сигурни ли сте, че искате да изтриете тази карта?',
areYouSureYouWantToDeleteThisComment: 'Сигурни ли сте, че искате да изтриете този коментар?',
areYouSureYouWantToDeleteThisLabel: 'Сигурни ли сте, че искате да изтриете този етикет?',
areYouSureYouWantToDeleteThisList: 'Сигурни ли сте, че искате да изтриете този списък?',
areYouSureYouWantToDeleteThisProject: 'Сигурни ли сте, че искате да изтриете този проект?',
areYouSureYouWantToDeleteThisTask: 'Сигурни ли сте, че искате да изтриете тази задача?',
areYouSureYouWantToDeleteThisUser: 'Сигурни ли сте, че искате да изтриете този потребител?',
areYouSureYouWantToLeaveBoard: 'Сигурни ли сте, че искате да напуснете таблото?',
areYouSureYouWantToLeaveProject: 'Сигурни ли сте, че искате да напуснете проекта?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Сигурни ли сте, че искате да премахнете този мениджър от проекта?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Сигурни ли сте, че искате да премахнете този член от таблото?',
attachment: 'Прикачен файл',
attachments: 'Прикачени файлове',
authentication: 'Удостоверяване',
background: 'Фон',
board: 'Табло',
boardNotFound_title: 'Таблото не е намерено',
canComment: 'може да коментира',
canEditContentOfBoard: 'може да редактира съдържание на таблото',
canOnlyViewBoard: 'може само да преглежда таблото',
cardActions_title: 'Действия с карта',
cardNotFound_title: 'Картата не може да бъде открита',
cardOrActionAreDeleted: 'Картата или действието са изтрити.',
color: 'Цвят',
copy_inline: 'копирай',
createBoard_title: 'Създаване на табло',
createLabel_title: 'Създаване на етикет',
createNewOneOrSelectExistingOne: 'Създайте нов или изберете<br/>съществуващ.',
createProject_title: 'Създаване на проект',
createTextFile_title: 'Създаване на текстов файл',
currentPassword: 'Текуща парола',
dangerZone_title: 'Опасна зона',
date: 'Дата',
dueDate: 'Дата на падежа',
dueDate_title: 'Краен срок',
deleteAttachment_title: 'Изтриване на прикачен файл',
deleteBoard_title: 'Изтриване на табло',
deleteCard_title: 'Изтриване на карта',
deleteComment_title: 'Изтриване на коментар',
deleteLabel_title: 'Изтриване на етикета',
deleteList_title: 'Изтриване на списък',
deleteProject_title: 'Изтриване на проект',
deleteTask_title: 'Изтриване на задача',
deleteUser_title: 'Изтриване на потребител',
description: 'Описание',
detectAutomatically: 'Aвтоматично откриване',
dropFileToUpload: 'Пуснете файл за качване',
editor: 'Редактор',
editAttachment_title: 'Редактирай прикачения файл',
editAvatar_title: 'Редактиране на аватар',
editBoard_title: 'Редактиране на табло',
editDueDate_title: 'Редактиране на дата на падеж',
editEmail_title: 'Редактиране на имейл',
editInformation_title: 'Редактиране на информация',
editLabel_title: 'Редактиране на табло',
editPassword_title: 'Редактиране на парола',
editPermissions_title: 'Редактиране на права',
editStopwatch_title: 'Редактиране на Хронометър',
editUsername_title: 'Редактиране на потребителско име',
email: 'Имейл',
emailAlreadyInUse: 'Имейлът вече се използва',
enterCardTitle: 'Въведете заглавие на картата... [Ctrl+Enter] за автоматично отваряне.',
enterDescription: 'Въведете описание...',
enterFilename: 'Въведете име на файла',
enterListTitle: 'Въведете заглавие на списък...',
enterProjectTitle: 'Въведете заглавие на проекта',
enterTaskDescription: 'Въведете описание на задачата...',
filterByLabels_title: 'Филтриране по етикети',
filterByMembers_title: 'Филтриране по членове',
fromComputer_title: 'От компютър',
fromTrello: 'От Trello',
general: 'Общ',
hours: 'Часове',
importBoard_title: 'Импортиране на табло',
invalidCurrentPassword: 'Невалидна текуща парола',
labels: 'Етикети',
language: 'Език',
leaveBoard_title: 'Напуснете таблото',
leaveProject_title: 'Напуснете проекта',
list: 'Списък',
listActions_title: 'Списък с действия',
managers: 'Мениджъри',
managerActions_title: 'Действия с мениджъри',
members: 'Членове',
memberActions_title: 'Действия с членове',
minutes: 'Минути',
moveCard_title: 'Преместване на карта',
name: 'Име',
newestFirst: 'Първо най-новите',
newEmail: 'Нов имейл',
newPassword: 'Нова парола',
newUsername: 'Ново потребителско име',
noConnectionToServer: 'Няма връзка със сървъра',
noBoards: 'Няма табла',
noLists: 'Няма списъци',
noProjects: 'Няма проекти',
notifications: 'Уведомления',
noUnreadNotifications: 'Няма непрочетени известия.',
oldestFirst: 'Първо най-старите',
openBoard_title: 'Отворете табло',
optional_inline: 'по желание',
organization: 'Организация',
phone: 'Телефон',
preferences: 'Предпочитания',
pressPasteShortcutToAddAttachmentFromClipboard:
'Съвет: натиснете Ctrl-V (Cmd-V на Mac), за да добавите прикачен файл от клипборда',
project: 'Проект',
projectNotFound_title: 'Проектът не е намерен',
removeManager_title: 'Премахване на мениджър',
removeMember_title: 'Премахване на член',
searchLabels: 'Търсене на етикети...',
searchMembers: 'Търсене на членове...',
searchUsers: 'Търсене на потребители...',
searchCards: 'Търсене на карти...',
seconds: 'секунди',
selectBoard: 'Изберете табло',
selectList: 'Изберете списък',
selectPermissions_title: 'Изберете правата',
selectProject: 'Изберете проект',
settings: 'Настройки',
sortList_title: 'Сортиране на списък',
stopwatch: 'Хронометър',
subscribeToMyOwnCardsByDefault: 'Абонирайте се за собствените си карти по подразбиране',
taskActions_title: 'Действия със задачи',
tasks: 'Задачи',
thereIsNoPreviewAvailableForThisAttachment: 'Няма наличен преглед за този прикачен файл.',
time: 'Време',
title: 'Заглавие',
userActions_title: 'Потребителски действия',
userAddedThisCardToList: '<0>{{user}}</0><1> добави тази карта в {{list}}</1>',
userLeftNewCommentToCard: '{{user}} остави нов коментар «{{comment}}» в <2>{{card}}</2>',
userMovedCardFromListToList:
'{{user}} премести <2>{{card}}</2> от {{fromList}} към {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> премести тази карта от {{fromList}} към {{toList}}</1>',
username: 'Потребителско име',
usernameAlreadyInUse: 'Потребителското име вече се използва',
users: 'Потребители',
version: 'Версия',
viewer: 'Зрител',
writeComment: 'Напишете коментар...',
},
action: {
addAnotherCard: 'Добавяне на друга карта',
addAnotherList: 'Добавяне на друг списък',
addAnotherTask: 'Добавяне на друга задача',
addCard: 'Добавяне на карта',
addCard_title: 'Добавяне на карта',
addComment: 'Добавяне на коментар',
addList: 'Добавяне на списък',
addMember: 'Добавяне на член',
addMoreDetailedDescription: 'Добавяне на по-подробно описание',
addTask: 'Добавяне на задача',
addToCard: 'Добавяне към карта',
addUser: 'Добавяне на потребител',
createBoard: 'Създаване на табло',
createFile: 'Създаване на файл',
createLabel: 'Създаване на етикет',
createNewLabel: 'Създаване на нов етикет',
createProject: 'Създаване на проект',
delete: 'Изтриване',
deleteAttachment: 'Изтриване на прикачения файл',
deleteAvatar: 'Изтриване на аватар',
deleteBoard: 'Изтриване на табло',
deleteCard: 'Изтриване на карта',
deleteCard_title: 'Изтриване на карта',
deleteComment: 'Изтриване на коментара',
deleteImage: 'Изтриване на изображение',
deleteLabel: 'Изтриване на етикета',
deleteList: 'Изтриване на списък',
deleteList_title: 'Изтриване на списък',
deleteProject: 'Изтриване на проект',
deleteProject_title: 'Изтриване на проект',
deleteTask: 'Изтриване на задача',
deleteTask_title: 'Изтриване на задача',
deleteUser: 'Изтриване на потребител',
duplicate: 'Направи дубликат',
duplicateCard_title: 'Дублирана карта',
edit: 'Редактиране',
editDueDate_title: 'Редактиране на краен срок',
editDescription_title: 'Редактиране на описание',
editEmail_title: 'Редактиране на имейл',
editInformation_title: 'Редактиране на информация',
editPassword_title: 'Редактиране на парола',
editPermissions: 'Разрешения за редактиране',
editStopwatch_title: 'Редактиране на хронометър',
editTitle_title: 'Редактиране на заглавието',
editUsername_title: 'Редактиране на потребителско име',
hideDetails: 'Скриване на детайлите',
import: 'Импортиране',
leaveBoard: 'Напускане на дъската',
leaveProject: 'Напускане на проекта',
logOut_title: 'Излезте',
makeCover_title: 'Създаване на корица',
move: 'Преместване',
moveCard_title: 'Преместване на карта',
remove: 'Премахване',
removeBackground: 'Премахване на фона',
removeCover_title: 'Премахване на корицата',
removeFromBoard: 'Премахване от борда',
removeFromProject: 'Премахване от проекта',
removeManager: 'Премахване на мениджър',
removeMember: 'Премахване на член',
save: 'Запазване',
showAllAttachments: 'Показване на всички прикачени файлове ({{hidden}} скрити)',
showDetails: 'Показване на подробности',
showFewerAttachments: 'Показване на по-малко прикачени файлове',
sortList_title: 'Списък за сортиране',
start: 'Старт',
stop: 'Стоп',
subscribe: 'Абонирайте се',
unsubscribe: 'Отписване',
uploadNewAvatar: 'Качване на нов аватар',
uploadNewImage: 'Качване на ново изображение',
},
},
};
+8
View File
@@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'bg',
country: 'bg',
name: 'Български',
embeddedLocale: login,
};
+22
View File
@@ -0,0 +1,22 @@
export default {
translation: {
common: {
emailOrUsername: 'Имейл или потребителско име',
invalidEmailOrUsername: 'Невалиден имейл или потребителско име',
invalidPassword: 'Невалидна парола',
logInToPlanka: 'Влезте в Planka',
noInternetConnection: 'Няма интернет връзка',
pageNotFound_title: 'Страницата не е намерена',
password: 'Парола',
projectManagement: 'Управление на проекти',
serverConnectionFailed: 'Неуспешна връзка със сървъра',
unknownError: 'Неизвестна грешка, опитайте отново по-късно',
useSingleSignOn: 'Използване на single sign-on',
},
action: {
logIn: 'Вход',
logInWithSSO: 'Вход чрез SSO',
},
},
};
+9 -3
View File
@@ -64,7 +64,8 @@ export default {
currentPassword: 'Aktuální heslo',
dangerZone_title: 'Nebezpečná zóna',
date: 'Datum',
dueDate_title: 'Termín do',
dueDate: 'Termín',
dueDate_title: 'Termín',
deleteAttachment_title: 'Smazat přílohu',
deleteBoard_title: 'Smazat tabuli',
deleteCard_title: 'Smazat kartu',
@@ -81,7 +82,7 @@ export default {
editAttachment_title: 'Upravit přílohu',
editAvatar_title: 'Upravit avatar',
editBoard_title: 'Upravit tabuli',
editDueDate_title: 'Upravit Termín do',
editDueDate_title: 'Upravit termín',
editEmail_title: 'Upravit e-mail',
editInformation_title: 'Upravit informace',
editLabel_title: 'Upravit štítek',
@@ -116,6 +117,7 @@ export default {
minutes: 'Minuty',
moveCard_title: 'Přesunout kartu',
name: 'Jméno',
newestFirst: 'Nejnovější',
newEmail: 'Nový e-mail',
newPassword: 'Nové heslo',
newUsername: 'Nové uživatelské jméno',
@@ -125,6 +127,7 @@ export default {
noProjects: 'Žádné projekty',
notifications: 'Oznámení',
noUnreadNotifications: 'Žádné nepřečtené oznámení',
oldestFirst: 'Nejstarší',
openBoard_title: 'Otevřít tabuli',
optional_inline: 'volitelné',
organization: 'Společnost',
@@ -139,12 +142,14 @@ export default {
searchLabels: 'Hledat štítky...',
searchMembers: 'Hledat členy...',
searchUsers: 'Hledat uživatele...',
searchCards: 'Hledat karty...',
seconds: 'Vteřin',
selectBoard: 'Vybrat tabuli',
selectList: 'Vybrat seznam',
selectPermissions_title: 'Vybrat oprávnění',
selectProject: 'Vybrat projekt',
settings: 'Nastavení',
sortList_title: 'Řadit podle',
stopwatch: 'Časovač',
subscribeToMyOwnCardsByDefault: 'Ve výchozím nastavení odebírat vlastní karty',
taskActions_title: 'Akce na úkolu',
@@ -203,7 +208,7 @@ export default {
duplicate: 'Duplikovat',
duplicateCard_title: 'Duplikovat kartu',
edit: 'Upravit',
editDueDate_title: 'Upravit termín do',
editDueDate_title: 'Upravit termín',
editDescription_title: 'Upravit popis',
editEmail_title: 'Upravit e-mail',
editInformation_title: 'Upravit informace',
@@ -231,6 +236,7 @@ export default {
showAllAttachments: 'Zozbrazit všechny přílohy ({{hidden}} skryté)',
showDetails: 'Zobrazit detaily',
showFewerAttachments: 'Zobrazit méně příloh',
sortList_title: 'Řadit podle',
start: 'Start',
stop: 'Stop',
subscribe: 'Odebírat',
+2 -2
View File
@@ -1,8 +1,8 @@
export default {
translation: {
common: {
emailOrUsername: 'Email nebo uživatelské jméno',
invalidEmailOrUsername: 'Nesprávný email nebo uživatelské jméno',
emailOrUsername: 'E-mail nebo uživatelské jméno',
invalidEmailOrUsername: 'Nesprávný e-mail nebo uživatelské jméno',
invalidPassword: 'Nesprávné heslo',
logInToPlanka: 'Přihlásit se do Planka',
noInternetConnection: 'Bez připojení k internetu',
+4
View File
@@ -104,6 +104,7 @@ export default {
language: 'Sprache',
leaveBoard_title: 'Board verlassen',
leaveProject_title: 'Projekt verlassen',
linkIsCopied: 'Link kopiert',
list: 'Listen',
listActions_title: 'Aufgaben auflisten',
managers: 'Manager',
@@ -169,6 +170,7 @@ export default {
addTask: 'Aufgabe hinzufügen',
addToCard: 'Zu Karte hinzufügen',
addUser: 'Benutzer hinzufügen',
copyLink_title: 'Link kopieren',
createBoard: 'Board erstellen',
createFile: 'Datei erstellen',
createLabel: 'Label erstellen',
@@ -190,6 +192,8 @@ export default {
deleteTask: 'Aufgabe löschen',
deleteTask_title: 'Aufgabe löschen',
deleteUser: 'Benutzer löschen',
duplicate: 'Kopieren',
duplicateCard_title: 'Karte kopieren',
edit: 'Bearbeiten',
editDueDate_title: 'Fälligkeitsdatum bearbeiten',
editDescription_title: 'Beschreibung ändern',
+10
View File
@@ -60,6 +60,7 @@ export default {
currentPassword: 'Current password',
dangerZone_title: 'Danger Zone',
date: 'Date',
dueDate: 'Due date',
dueDate_title: 'Due Date',
deleteAttachment_title: 'Delete Attachment',
deleteBoard_title: 'Delete Board',
@@ -105,13 +106,17 @@ export default {
language: 'Language',
leaveBoard_title: 'Leave Board',
leaveProject_title: 'Leave Project',
linkIsCopied: 'Link is copied',
list: 'List',
listActions_title: 'List Actions',
managers: 'Managers',
managerActions_title: 'Manager Actions',
members: 'Members',
memberActions_title: 'Member Actions',
minutes: 'Minutes',
moveCard_title: 'Move Card',
name: 'Name',
newestFirst: 'Newest first',
newEmail: 'New e-mail',
newPassword: 'New password',
newUsername: 'New username',
@@ -121,6 +126,7 @@ export default {
noProjects: 'No projects',
notifications: 'Notifications',
noUnreadNotifications: 'No unread notifications.',
oldestFirst: 'Oldest first',
openBoard_title: 'Open Board',
optional_inline: 'optional',
organization: 'Organization',
@@ -135,12 +141,14 @@ export default {
searchLabels: 'Search labels...',
searchMembers: 'Search members...',
searchUsers: 'Search users...',
searchCards: 'Search cards...',
seconds: 'Seconds',
selectBoard: 'Select board',
selectList: 'Select list',
selectPermissions_title: 'Select Permissions',
selectProject: 'Select project',
settings: 'Settings',
sortList_title: 'Sort List',
stopwatch: 'Stopwatch',
subscribeToMyOwnCardsByDefault: 'Subscribe to my own cards by default',
taskActions_title: 'Task Actions',
@@ -176,6 +184,7 @@ export default {
addTask: 'Add task',
addToCard: 'Add to card',
addUser: 'Add user',
copyLink_title: 'Copy Link',
createBoard: 'Create board',
createFile: 'Create file',
createLabel: 'Create label',
@@ -228,6 +237,7 @@ export default {
showAllAttachments: 'Show all attachments ({{hidden}} hidden)',
showDetails: 'Show details',
showFewerAttachments: 'Show fewer attachments',
sortList_title: 'Sort List',
start: 'Start',
stop: 'Stop',
subscribe: 'Subscribe',
+4
View File
@@ -111,6 +111,10 @@ export default {
project: 'Projet',
projectNotFound_title: 'Projet introuvable',
removeMember_title: 'Supprimer le membre',
searchLabels: 'Chercher une étiquette...',
searchMembers: 'Chercher un membre...',
searchUsers: 'Chercher un utilisateur...',
searchCards: 'Chercher une carte...',
seconds: 'Secondes',
selectBoard: 'Sélectionner une carte',
selectList: 'Sélectionner une liste',
+254
View File
@@ -0,0 +1,254 @@
import dateFns from 'date-fns/locale/hu';
export default {
dateFns,
format: {
date: 'yyyy.M.d.',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMM. d.',
longDateTime: 'MMMM. d. p',
fullDate: 'y. MMM d.',
fullDateTime: 'y. MMMM d. p',
},
translation: {
common: {
aboutPlanka: 'Plankáról',
account: 'Fiók',
actions: 'Műveletek',
addAttachment_title: 'Melléklet hozzáadása',
addComment: 'Megjegyzés hozzáadása',
addManager_title: 'Menedzser hozzáadása',
addMember_title: 'Tag létrehozása',
addUser_title: 'Felhasználó hozzáadása',
administrator: 'Rendszergazda',
all: 'Összes',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Az összes változás automatikusan mentésre kerül a kapcsolat helyreállása után.',
areYouSureYouWantToDeleteThisAttachment: 'Biztosan törölni szeretné ezt a mellékletet?',
areYouSureYouWantToDeleteThisBoard: 'Biztosan törölni szeretné ezt a táblát?',
areYouSureYouWantToDeleteThisCard: 'Biztosan törölni szeretné ezt a kártyát?',
areYouSureYouWantToDeleteThisComment: 'Biztosan törölni szeretné ezt a megjegyzést?',
areYouSureYouWantToDeleteThisLabel: 'Biztosan törölni szeretné ezt a címkét?',
areYouSureYouWantToDeleteThisList: 'Biztosan törölni szeretné ezt a listát?',
areYouSureYouWantToDeleteThisProject: 'Biztosan törölni szeretné ezt a projektet?',
areYouSureYouWantToDeleteThisTask: 'Biztosan törölni szeretné ezt a feladatot?',
areYouSureYouWantToDeleteThisUser: 'Biztosan törölni szeretné ezt a felhasználót?',
areYouSureYouWantToLeaveBoard: 'Biztosan el akarja hagyni a táblát?',
areYouSureYouWantToLeaveProject: 'Biztosan el akarja hagyni a projektet?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Biztosan eltávolítja ezt a menedzsert a projektből?',
areYouSureYouWantToRemoveThisMemberFromBoard: 'Biztosan eltávolítja ezt a tagot a tábláról?',
attachment: 'Melléklet',
attachments: 'Mellékletek',
authentication: 'Hitelesítés',
background: 'Háttér',
board: 'Tábla',
boardNotFound_title: 'Tábla nem található',
canComment: 'Hozzászólhat',
canEditContentOfBoard: 'Szerkesztheti a tábla tartalmát.',
canOnlyViewBoard: 'Csak megtekintheti a táblát.',
cardActions_title: 'Kártya műveletek',
cardNotFound_title: 'Kártya nem található',
cardOrActionAreDeleted: 'A kártya vagy művelet törölve lett.',
color: 'Szín',
copy_inline: 'másolás',
createBoard_title: 'Tábla létrehozása',
createLabel_title: 'Címke létrehozása',
createNewOneOrSelectExistingOne: 'Hozzon létre egy újat, vagy válasszon ki egy meglévőt.',
createProject_title: 'Projekt létrehozása',
createTextFile_title: 'Szövegfájl létrehozása',
currentPassword: 'Jelenlegi jelszó',
dangerZone_title: 'Veszélyzóna',
date: 'Dátum',
dueDate: 'Határidő',
dueDate_title: 'Esedékesség dátuma',
deleteAttachment_title: 'Melléklet törlése',
deleteBoard_title: 'Tábla törlése',
deleteCard_title: 'Kártya törlése',
deleteComment_title: 'Megjegyzés törlése',
deleteLabel_title: 'Címke törlése',
deleteList_title: 'Lista törlése',
deleteProject_title: 'Projekt törlése',
deleteTask_title: 'Feladat törlése',
deleteUser_title: 'Felhasználó törlése',
description: 'Leírás',
detectAutomatically: 'Automatikus érzékelés',
dropFileToUpload: 'Dobja ide a fájlt a feltöltéshez',
editor: 'Szerkesztő',
editAttachment_title: 'Melléklet szerkesztése',
editAvatar_title: 'Avatar szerkesztése',
editBoard_title: 'Tábla szerkesztése',
editDueDate_title: 'Esedékesség dátumának szerkesztése',
editEmail_title: 'E-mail szerkesztése',
editInformation_title: 'Információ szerkesztése',
editLabel_title: 'Címke szerkesztése',
editPassword_title: 'Jelszó szerkesztése',
editPermissions_title: 'Jogosultságok szerkesztése',
editStopwatch_title: 'Stopper szerkesztése',
editUsername_title: 'Felhasználónév szerkesztése',
email: 'E-mail',
emailAlreadyInUse: 'Az e-mail cím már használatban van',
enterCardTitle: 'Adja meg a kártya címét... [Ctrl+Enter] az automatikus megnyitáshoz.',
enterDescription: 'Adja meg a leírást...',
enterFilename: 'Adja meg a fájl nevét',
enterListTitle: 'Adja meg a lista címét...',
enterProjectTitle: 'Adja meg a projekt címét',
enterTaskDescription: 'Adja meg a feladat leírását...',
filterByLabels_title: 'Szűrés címkék alapján',
filterByMembers_title: 'Szűrés tagok alapján',
fromComputer_title: 'Számítógépről',
fromTrello: 'Trello-ról',
general: 'Általános',
hours: 'Órák',
importBoard_title: 'Tábla importálása',
invalidCurrentPassword: 'Érvénytelen jelenlegi jelszó',
labels: 'Címkék',
language: 'Nyelv',
leaveBoard_title: 'Tábla elhagyása',
leaveProject_title: 'Projekt elhagyása',
linkIsCopied: 'Link másolva',
list: 'Lista',
listActions_title: 'Lista műveletek',
managers: 'Menedzserek',
managerActions_title: 'Menedzseri műveletek',
members: 'Tagok',
memberActions_title: 'Tag műveletek',
minutes: 'Percek',
moveCard_title: 'Kártya áthelyezése',
name: 'Név',
newestFirst: 'Újabbak előre',
newEmail: 'Új e-mail',
newPassword: 'Új jelszó',
newUsername: 'Új felhasználónév',
noConnectionToServer: 'Nincs kapcsolat a szerverrel',
noBoards: 'Nincsenek táblák',
noLists: 'Nincsenek listák',
noProjects: 'Nincsenek projektek',
notifications: 'Értesítések',
noUnreadNotifications: 'Nincsenek olvasatlan értesítések.',
oldestFirst: 'Régebbi előre',
openBoard_title: 'Tábla megnyitása',
optional_inline: 'opcionális',
organization: 'Szervezet',
phone: 'Telefon',
preferences: 'Beállítások',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tipp: nyomja meg a Ctrl-V (Cmd-V a Mac-en) billentyűkombinációt a vágólapról történő melléklet hozzáadásához.',
project: 'Projekt',
projectNotFound_title: 'Projekt nem található',
removeManager_title: 'Menedzser eltávolítása',
removeMember_title: 'Tag eltávolítása',
searchLabels: 'Címkék keresése...',
searchMembers: 'Tagok keresése...',
searchUsers: 'Felhasználók keresése...',
searchCards: 'Kártyák keresése..',
seconds: 'Másodpercek',
selectBoard: 'Válassza ki a táblát',
selectList: 'Válassza ki a listát',
selectPermissions_title: 'Jogosultságok kiválasztása',
selectProject: 'Válassza ki a projektet',
settings: 'Beállítások',
sortList_title: 'Rendezés listában',
stopwatch: 'Stopper',
subscribeToMyOwnCardsByDefault: 'Alapértelmezés szerint feliratkozás a saját kártyáimra',
taskActions_title: 'Feladat műveletek',
tasks: 'Feladatok',
thereIsNoPreviewAvailableForThisAttachment: 'Nincs elérhető előnézet ehhez a melléklethez.',
time: 'Idő',
title: 'Cím',
userActions_title: 'Felhasználói műveletek',
userAddedThisCardToList:
'<0>{{user}}</0><1> hozzáadta ezt a kártyát a következőhöz: {{list}}</1>',
userLeftNewCommentToCard:
'{{user}} új kommentet hagyott itt: «{{comment}}» a következő kártyán: <2>{{card}}</2>',
userMovedCardFromListToList:
'{{user}} áthelyezte ezt a kártyát innen: {{fromList}} ide: {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> áthelyezte ezt a kártyát innen: {{fromList}} ide: {{toList}}</1>',
username: 'Felhasználónév',
usernameAlreadyInUse: 'A felhasználónév már használatban van',
users: 'Felhasználók',
version: 'Verzió',
viewer: 'Néző',
writeComment: 'Írjon egy megjegyzést...',
},
action: {
addAnotherCard: 'Új kártya hozzáadása',
addAnotherList: 'Új lista hozzáadása',
addAnotherTask: 'Új feladat hozzáadása',
addCard: 'Kártya hozzáadása',
addCard_title: 'Kártya hozzáadása',
addComment: 'Megjegyzés hozzáadása',
addList: 'Lista hozzáadása',
addMember: 'Tag hozzáadása',
addMoreDetailedDescription: 'További részletes leírás hozzáadása',
addTask: 'Feladat hozzáadása',
addToCard: 'Kártyához hozzáadása',
addUser: 'Felhasználó hozzáadása',
copyLink_title: 'Link másolása',
createBoard: 'Tábla létrehozása',
createFile: 'Fájl létrehozása',
createLabel: 'Címke létrehozása',
createNewLabel: 'Új címke létrehozása',
createProject: 'Projekt létrehozása',
delete: 'Törlés',
deleteAttachment: 'Melléklet törlése',
deleteAvatar: 'Avatar törlése',
deleteBoard: 'Tábla törlése',
deleteCard: 'Kártya törlése',
deleteCard_title: 'Kártya törlése',
deleteComment: 'Megjegyzés törlése',
deleteImage: 'Kép törlése',
deleteLabel: 'Címke törlése',
deleteList: 'Lista törlése',
deleteList_title: 'Lista törlése',
deleteProject: 'Projekt törlése',
deleteProject_title: 'Projekt törlése',
deleteTask: 'Feladat törlése',
deleteTask_title: 'Feladat törlése',
deleteUser: 'Felhasználó törlése',
duplicate: 'Másolás',
duplicateCard_title: 'Kártya másolása',
edit: 'Szerkesztés',
editDueDate_title: 'Esedékesség dátumának szerkesztése',
editDescription_title: 'Leírás szerkesztése',
editEmail_title: 'E-mail szerkesztése',
editInformation_title: 'Információ szerkesztése',
editPassword_title: 'Jelszó szerkesztése',
editPermissions: 'Jogosultságok szerkesztése',
editStopwatch_title: 'Stopper szerkesztése',
editTitle_title: 'Cím szerkesztése',
editUsername_title: 'Felhasználónév szerkesztése',
hideDetails: 'Részletek elrejtése',
import: 'Importálás',
leaveBoard: 'Tábla elhagyása',
leaveProject: 'Projekt elhagyása',
logOut_title: 'Kijelentkezés',
makeCover_title: 'Borító készítése',
move: 'Áthelyezés',
moveCard_title: 'Kártya áthelyezése',
remove: 'Eltávolítás',
removeBackground: 'Háttér eltávolítása',
removeCover_title: 'Borító eltávolítása',
removeFromBoard: 'Eltávolítás a tábláról',
removeFromProject: 'Eltávolítás a projektről',
removeManager: 'Menedzser eltávolítása',
removeMember: 'Tag eltávolítása',
save: 'Mentés',
showAllAttachments: 'Összes melléklet megjelenítése ({{hidden}} rejtve)',
showDetails: 'Részletek megjelenítése',
showFewerAttachments: 'Kevesebb melléklet megjelenítése',
sortList_title: 'Rendezés listában',
start: 'Indítás',
stop: 'Megállítás',
subscribe: 'Feliratkozás',
unsubscribe: 'Leiratkozás',
uploadNewAvatar: 'Új avatar feltöltése',
uploadNewImage: 'Új kép feltöltése',
},
},
};
+8
View File
@@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'hu',
country: 'hu',
name: 'Magyar',
embeddedLocale: login,
};
+22
View File
@@ -0,0 +1,22 @@
export default {
translation: {
common: {
emailOrUsername: 'E-mail vagy felhasználó',
invalidEmailOrUsername: 'Érvénytelen e-mail vagy felhasználó',
invalidPassword: 'Érvénytelen jelszó',
logInToPlanka: 'Plankába belépés',
noInternetConnection: 'Nincs internet kapcsolat',
pageNotFound_title: 'Az oldal nem található',
password: 'Jelszó',
projectManagement: 'Projektmenedzsment',
serverConnectionFailed: 'A szerverkapcsolat sikertelen',
unknownError: 'Ismeretlen hiba, próbáld meg később újra',
useSingleSignOn: 'Egyszeri bejelentkezés használata',
},
action: {
logIn: 'Belépés',
logInWithSSO: 'Belépés SSO-val',
},
},
};
+4
View File
@@ -1,9 +1,11 @@
import bg from './bg';
import cs from './cs';
import da from './da';
import de from './de';
import en from './en';
import es from './es';
import fr from './fr';
import hu from './hu';
import id from './id';
import it from './it';
import ja from './ja';
@@ -21,12 +23,14 @@ import uz from './uz';
import zh from './zh';
const locales = [
bg,
cs,
da,
de,
en,
es,
fr,
hu,
id,
it,
ja,
+6
View File
@@ -59,6 +59,7 @@ export default {
currentPassword: 'Password corrente',
dangerZone_title: 'Zona pericolosa',
date: 'Data',
dueDate: 'Data di scadenza',
dueDate_title: 'Data di scadenza',
deleteAttachment_title: 'Elimina Allegato',
deleteBoard_title: 'Elimina Bacheca',
@@ -111,6 +112,7 @@ export default {
minutes: 'Minuti',
moveCard_title: 'Sposta card',
name: 'Nome',
newestFirst: 'Dal più recente',
newEmail: 'Nuova e-mail',
newPassword: 'Nuova password',
newUsername: 'Nuovo username',
@@ -120,6 +122,7 @@ export default {
noProjects: 'No progetti',
notifications: 'Notifiche',
noUnreadNotifications: 'Nessuna notifica non letta.',
oldestFirst: 'Dal meno recente',
openBoard_title: 'Bacheca aperta',
optional_inline: 'opzionale',
organization: 'Organizazzione',
@@ -134,12 +137,14 @@ export default {
searchLabels: 'Cerca etichette...',
searchMembers: 'Cerca membri...',
searchUsers: 'Cerca utenti...',
searchCards: 'Cerca cards...',
seconds: 'Secondi',
selectBoard: 'Seleziona bacheca',
selectList: 'Seleziona lista',
selectPermissions_title: 'Seleziona Permessi',
selectProject: 'Seleziona progetto',
settings: 'Impostazioni',
sortList_title: 'Ordina',
stopwatch: 'Timer',
subscribeToMyOwnCardsByDefault: 'Abbonati alle mie card per impostazione predefinita',
taskActions_title: 'Task Azioni',
@@ -227,6 +232,7 @@ export default {
showAllAttachments: 'Mostra tutti gli allegati ({{hidden}} hidden)',
showDetails: 'Mostra dettagli',
showFewerAttachments: 'Mostra meno allegati',
sortList_title: 'Ordina',
start: 'Inizio',
stop: 'Ferma',
subscribe: 'Iscriviti',
+2
View File
@@ -142,6 +142,7 @@ export default {
searchLabels: 'Поиск меток...',
searchMembers: 'Поиск участников...',
searchUsers: 'Поиск пользователей...',
searchCards: 'Поиск карточек...',
seconds: 'Секунды',
selectBoard: 'Выберите доску',
selectList: 'Выберите список',
@@ -232,6 +233,7 @@ export default {
showAllAttachments: 'Показать все вложения ({{hidden}} скрыто)',
showDetails: 'Показать подробности',
showFewerAttachments: 'Показать меньше вложений',
sortList_title: 'Сортировать список',
start: 'Начать',
stop: 'Остановить',
subscribe: 'Подписаться',
+1
View File
@@ -28,6 +28,7 @@ export default class extends BaseModel {
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
if (payload.attachments) {
payload.attachments.forEach((attachment) => {
Attachment.upsert(attachment);
+53 -3
View File
@@ -1,8 +1,12 @@
import orderBy from 'lodash/orderBy';
import { attr, fk, many } from 'redux-orm';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
import User from './User';
import Label from './Label';
export default class extends BaseModel {
static modelName = 'Board';
@@ -25,6 +29,9 @@ export default class extends BaseModel {
}),
filterUsers: many('User', 'filterBoards'),
filterLabels: many('Label', 'filterBoards'),
filterText: attr({
getDefault: () => '',
}),
};
static reducer({ type, payload }, Board) {
@@ -167,12 +174,49 @@ export default class extends BaseModel {
Board.withId(payload.boardId).filterLabels.remove(payload.id);
break;
default:
case ActionTypes.TEXT_FILTER_IN_CURRENT_BOARD: {
const board = Board.withId(payload.boardId);
let filterText = payload.text;
const posSpace = filterText.indexOf(' ');
// Shortcut to user filters
const posAT = filterText.indexOf('@');
if (posAT >= 0 && posSpace > 0 && posAT < posSpace) {
const userId = User.findUsersFromText(
filterText.substring(posAT + 1, posSpace),
board.memberships.toModelArray().map((membership) => membership.user),
);
if (
userId &&
board.filterUsers.toModelArray().filter((user) => user.id === userId).length === 0
) {
board.filterUsers.add(userId);
filterText = filterText.substring(0, posAT);
}
}
getOrderedMembershipsQuerySet() {
return this.memberships.orderBy('createdAt');
// Shortcut to label filters
const posSharp = filterText.indexOf('#');
if (posSharp >= 0 && posSpace > 0 && posSharp < posSpace) {
const labelId = Label.findLabelsFromText(
filterText.substring(posSharp + 1, posSpace),
board.labels.toModelArray(),
);
if (
labelId &&
board.filterLabels.toModelArray().filter((label) => label.id === labelId).length === 0
) {
board.filterLabels.add(labelId);
filterText = filterText.substring(0, posSharp);
}
}
board.update({ filterText });
break;
}
default:
}
}
getOrderedLabelsQuerySet() {
@@ -183,6 +227,12 @@ export default class extends BaseModel {
return this.lists.orderBy('position');
}
getOrderedMembershipsModelArray() {
return orderBy(this.memberships.toModelArray(), (boardMembershipModel) =>
boardMembershipModel.user.name.toLocaleLowerCase(),
);
}
getMembershipModelForUser(userId) {
return this.memberships
.filter({
+47 -9
View File
@@ -14,6 +14,11 @@ export default class extends BaseModel {
position: attr(),
name: attr(),
description: attr(),
creatorUserId: oneToOne({
to: 'User',
as: 'creatorUser',
relatedName: 'ownCards',
}),
dueDate: attr(),
stopwatch: attr(),
isSubscribed: attr({
@@ -164,10 +169,17 @@ export default class extends BaseModel {
Card.withId(payload.cardLabel.cardId).labels.remove(payload.cardLabel.labelId);
} catch {} // eslint-disable-line no-empty
break;
case ActionTypes.LIST_SORT__SUCCESS:
case ActionTypes.LIST_SORT_HANDLE:
case ActionTypes.NOTIFICATION_CREATE_HANDLE:
payload.cards.forEach((card) => {
Card.upsert(card);
});
break;
case ActionTypes.CARD_CREATE:
case ActionTypes.CARD_UPDATE__SUCCESS:
case ActionTypes.CARD_UPDATE_HANDLE:
Card.upsert(payload.card);
break;
@@ -189,8 +201,40 @@ export default class extends BaseModel {
break;
}
case ActionTypes.CARD_UPDATE:
Card.withId(payload.id).update(payload.data);
case ActionTypes.CARD_UPDATE: {
const cardModel = Card.withId(payload.id);
// TODO: introduce separate action?
if (payload.data.boardId && payload.data.boardId !== cardModel.boardId) {
cardModel.deleteWithRelated();
} else {
cardModel.update(payload.data);
}
break;
}
case ActionTypes.CARD_UPDATE_HANDLE:
if (payload.isFetched) {
const cardModel = Card.withId(payload.card.id);
if (cardModel) {
cardModel.deleteWithRelated();
}
}
Card.upsert(payload.card);
if (payload.cardMemberships) {
payload.cardMemberships.forEach(({ cardId, userId }) => {
Card.withId(cardId).users.add(userId);
});
}
if (payload.cardLabels) {
payload.cardLabels.forEach(({ cardId, labelId }) => {
Card.withId(cardId).labels.add(labelId);
});
}
break;
case ActionTypes.CARD_DUPLICATE: {
@@ -282,12 +326,6 @@ export default class extends BaseModel {
break;
}
case ActionTypes.NOTIFICATION_CREATE_HANDLE:
payload.cards.forEach((card) => {
Card.upsert(card);
});
break;
default:
}
}
+12
View File
@@ -80,4 +80,16 @@ export default class extends BaseModel {
default:
}
}
static findLabelsFromText(filterText, labels) {
const selectLabel = filterText.toLocaleLowerCase();
const matchingLabels = labels.filter((label) =>
label.name ? label.name.toLocaleLowerCase().startsWith(selectLabel) : false,
);
if (matchingLabels.length === 1) {
// Appens the user to the filter
return matchingLabels[0].id;
}
return null;
}
}
+38
View File
@@ -1,6 +1,7 @@
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
import User from './User';
import ActionTypes from '../constants/ActionTypes';
export default class extends BaseModel {
@@ -50,6 +51,8 @@ export default class extends BaseModel {
case ActionTypes.LIST_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE__SUCCESS:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.LIST_SORT__SUCCESS:
case ActionTypes.LIST_SORT_HANDLE:
List.upsert(payload.list);
break;
@@ -87,6 +90,41 @@ export default class extends BaseModel {
getFilteredOrderedCardsModelArray() {
let cardModels = this.getOrderedCardsQuerySet().toModelArray();
const { filterText } = this.board;
if (filterText !== '') {
let re = null;
const posSpace = filterText.indexOf(' ');
if (filterText.startsWith('/')) {
re = new RegExp(filterText.substring(1), 'i');
}
let doRegularSearch = true;
if (re) {
cardModels = cardModels.filter(
(cardModel) => re.test(cardModel.name) || re.test(cardModel?.description),
);
doRegularSearch = false;
} else if (filterText.startsWith('!') && posSpace > 0) {
const creatorUserId = User.findUsersFromText(
filterText.substring(1, posSpace),
this.board.memberships.toModelArray().map((membership) => membership.user),
);
if (creatorUserId != null) {
doRegularSearch = false;
cardModels = cardModels.filter((cardModel) => cardModel.creatorUser.id === creatorUserId);
}
}
if (doRegularSearch) {
const lowerCasedFilter = filterText.toLocaleLowerCase();
cardModels = cardModels.filter(
(cardModel) =>
cardModel.name.toLocaleLowerCase().indexOf(lowerCasedFilter) >= 0 ||
cardModel.description?.toLocaleLowerCase().indexOf(lowerCasedFilter) >= 0,
);
}
}
const filterUserIds = this.board.filterUsers.toRefArray().map((user) => user.id);
const filterLabelIds = this.board.filterLabels.toRefArray().map((label) => label.id);
+1
View File
@@ -27,6 +27,7 @@ export default class extends BaseModel {
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
if (payload.tasks) {
payload.tasks.forEach((task) => {
Task.upsert(task);
+15 -1
View File
@@ -298,7 +298,7 @@ export default class extends BaseModel {
static getOrderedUndeletedQuerySet() {
return this.filter({
deletedAt: null,
}).orderBy('createdAt');
}).orderBy((user) => user.name.toLocaleLowerCase());
}
getOrderedProjectManagersQuerySet() {
@@ -359,4 +359,18 @@ export default class extends BaseModel {
},
);
}
static findUsersFromText(filterText, users) {
const selectUser = filterText.toLocaleLowerCase();
const matchingUsers = users.filter(
(user) =>
user.name.toLocaleLowerCase().startsWith(selectUser) ||
user.username.toLocaleLowerCase().startsWith(selectUser),
);
if (matchingUsers.length === 1) {
// Appens the user to the filter
return matchingUsers[0].id;
}
return null;
}
}
+30 -2
View File
@@ -81,9 +81,30 @@ export function* updateCurrentCard(data) {
yield call(updateCard, cardId, data);
}
// TODO: handle card transfer
export function* handleCardUpdate(card) {
yield put(actions.handleCardUpdate(card));
let fetch = false;
if (card.boardId) {
const prevCard = yield select(selectors.selectCardById, card.id);
fetch = !prevCard || prevCard.boardId !== card.boardId;
}
let cardMemberships;
let cardLabels;
let tasks;
let attachments;
if (fetch) {
try {
({
item: card, // eslint-disable-line no-param-reassign
included: { cardMemberships, cardLabels, tasks, attachments },
} = yield call(request, api.getCard, card.id));
} catch (error) {
fetch = false;
}
}
yield put(actions.handleCardUpdate(card, fetch, cardMemberships, cardLabels, tasks, attachments));
}
export function* moveCard(id, listId, index = 0) {
@@ -207,6 +228,12 @@ export function* handleCardDelete(card) {
yield put(actions.handleCardDelete(card));
}
export function* handleTextFilter(text) {
const { boardId } = yield select(selectors.selectPath);
yield put(actions.filterText(boardId, text));
}
export default {
createCard,
handleCardCreate,
@@ -222,4 +249,5 @@ export default {
deleteCard,
deleteCurrentCard,
handleCardDelete,
handleTextFilter,
};
+26
View File
@@ -70,6 +70,30 @@ export function* moveList(id, index) {
});
}
// TODO: sort locally
export function* sortList(id, data) {
yield put(actions.sortList(id, data));
let list;
let cards;
try {
({
item: list,
included: { cards },
} = yield call(request, api.sortList, id, data));
} catch (error) {
yield put(actions.sortList.failure(id, error));
return;
}
yield put(actions.sortList.success(list, cards));
}
export function* handleListSort(list, cards) {
yield put(actions.handleListSort(list, cards));
}
export function* deleteList(id) {
yield put(actions.deleteList(id));
@@ -95,6 +119,8 @@ export default {
updateList,
handleListUpdate,
moveList,
sortList,
handleListSort,
deleteList,
handleListDelete,
};
+3
View File
@@ -39,5 +39,8 @@ export default function* cardsWatchers() {
takeEvery(EntryActionTypes.CARD_DELETE_HANDLE, ({ payload: { card } }) =>
services.handleCardDelete(card),
),
takeEvery(EntryActionTypes.TEXT_FILTER_IN_CURRENT_BOARD, ({ payload: { text } }) =>
services.handleTextFilter(text),
),
]);
}
+6
View File
@@ -20,6 +20,12 @@ export default function* listsWatchers() {
takeEvery(EntryActionTypes.LIST_MOVE, ({ payload: { id, index } }) =>
services.moveList(id, index),
),
takeEvery(EntryActionTypes.LIST_SORT, ({ payload: { id, data } }) =>
services.sortList(id, data),
),
takeEvery(EntryActionTypes.LIST_SORT_HANDLE, ({ payload: { list, cards } }) =>
services.handleListSort(list, cards),
),
takeEvery(EntryActionTypes.LIST_DELETE, ({ payload: { id } }) => services.deleteList(id)),
takeEvery(EntryActionTypes.LIST_DELETE_HANDLE, ({ payload: { list } }) =>
services.handleListDelete(list),
+6
View File
@@ -84,6 +84,10 @@ const createSocketEventsChannel = () =>
emit(entryActions.handleListUpdate(item));
};
const handleListSort = api.makeHandleListSort(({ item, included: { cards } }) => {
emit(entryActions.handleListSort(item, cards));
});
const handleListDelete = ({ item }) => {
emit(entryActions.handleListDelete(item));
};
@@ -198,6 +202,7 @@ const createSocketEventsChannel = () =>
socket.on('listCreate', handleListCreate);
socket.on('listUpdate', handleListUpdate);
socket.on('listSort', handleListSort);
socket.on('listDelete', handleListDelete);
socket.on('labelCreate', handleLabelCreate);
@@ -256,6 +261,7 @@ const createSocketEventsChannel = () =>
socket.off('listCreate', handleListCreate);
socket.off('listUpdate', handleListUpdate);
socket.off('listSort', handleListSort);
socket.off('listDelete', handleListDelete);
socket.off('labelCreate', handleLabelCreate);
+20 -4
View File
@@ -55,10 +55,7 @@ export const selectMembershipsForCurrentBoard = createSelector(
return boardModel;
}
return boardModel
.getOrderedMembershipsQuerySet()
.toModelArray()
.map((boardMembershipModel) => ({
return boardModel.getOrderedMembershipsModelArray().map((boardMembershipModel) => ({
...boardMembershipModel.ref,
isPersisted: !isLocalId(boardMembershipModel.id),
user: {
@@ -175,6 +172,24 @@ export const selectFilterLabelsForCurrentBoard = createSelector(
},
);
export const selectFilterTextForCurrentBoard = createSelector(
orm,
(state) => selectPath(state).boardId,
({ Board }, id) => {
if (!id) {
return id;
}
const boardModel = Board.withId(id);
if (!boardModel) {
return boardModel;
}
return boardModel.filterText;
},
);
export const selectIsBoardWithIdExists = createSelector(
orm,
(_, id) => id,
@@ -191,5 +206,6 @@ export default {
selectListIdsForCurrentBoard,
selectFilterUsersForCurrentBoard,
selectFilterLabelsForCurrentBoard,
selectFilterTextForCurrentBoard,
selectIsBoardWithIdExists,
};
+5
View File
@@ -0,0 +1,5 @@
// eslint-disable-next-line import/prefer-default-export
export const focusEnd = (element) => {
element.focus();
element.setSelectionRange(element.value.length + 1, element.value.length + 1);
};
+18
View File
@@ -0,0 +1,18 @@
FROM node:18-alpine as server-dependencies
RUN apk -U upgrade \
&& apk add build-base python3 \
--no-cache
WORKDIR /app/client
COPY package.json package-lock.json /app/client/
RUN npm install npm@latest --global \
&& npm install pnpm --global \
&& pnpm import \
&& pnpm install
WORKDIR /app/
COPY ../../package.json ../../package-lock.json /app/
RUN pnpm import \
&& pnpm install
+14
View File
@@ -0,0 +1,14 @@
FROM node:18-alpine as server-dependencies
RUN apk -U upgrade \
&& apk add build-base python3 \
--no-cache
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install npm@latest --global \
&& npm install pnpm --global \
&& pnpm import \
&& pnpm install
+47
View File
@@ -0,0 +1,47 @@
user nginx;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
location /api/ {
proxy_pass http://server:1337;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location /socket.io {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_set_header Host $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_pass http://server:1337/socket.io;
}
location / {
proxy_pass http://client:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
}
+86 -30
View File
@@ -1,18 +1,16 @@
version: '3'
version: '3.8'
services:
planka:
image: ghcr.io/plankanban/planka:master
restart: on-failure
server:
build:
context: ./server
dockerfile: ../config/development/Dockerfile.server
volumes:
- user-avatars:/app/public/user-avatars
- project-background-images:/app/public/project-background-images
- attachments:/app/private/attachments
ports:
- 3000:1337
- ./server:/app
- /app/node_modules
environment:
- BASE_URL=http://localhost:3000
- DATABASE_URL=postgresql://postgres@postgres/planka
- NODE_ENV=development
- DATABASE_URL=postgresql://user:password@postgres:5432/planka_db
- SECRET_KEY=notsecretkey
# - TRUST_PROXY=0
@@ -26,18 +24,7 @@ services:
# Configure knex to accept SSL certificates
# - KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
# - DEFAULT_ADMIN_EMAIL=demo@demo.demo # Do not remove if you want to prevent this user from being edited/deleted
# - DEFAULT_ADMIN_PASSWORD=demo
# - DEFAULT_ADMIN_NAME=Demo Demo
# - DEFAULT_ADMIN_USERNAME=demo
# Email Notifications (https://nodemailer.com/smtp/)
# - SMTP_HOST=
# - SMTP_PORT=587
# - SMTP_SECURE=true
# - SMTP_USER=
# - SMTP_PASSWORD=
# - SMTP_FROM="Demo Demo" <demo@demo.demo>
# - ALLOW_ALL_TO_CREATE_PROJECTS=true
# - OIDC_ISSUER=
# - OIDC_CLIENT_ID=
@@ -51,26 +38,95 @@ services:
# - OIDC_IGNORE_USERNAME=true
# - OIDC_IGNORE_ROLES=true
# - OIDC_ENFORCED=true
# Email Notifications (https://nodemailer.com/smtp/)
# - SMTP_HOST=
# - SMTP_PORT=587
# - SMTP_NAME=
# - SMTP_SECURE=true
# - SMTP_USER=
# - SMTP_PASSWORD=
# - SMTP_FROM="Demo Demo" <demo@demo.demo>
# Optional fields: accessToken, events, excludedEvents
# - |
# WEBHOOKS=[{
# "url": "http://localhost:3001",
# "accessToken": "notaccesstoken",
# "events": ["cardCreate", "cardUpdate", "cardDelete"],
# "excludedEvents": ["notificationCreate", "notificationUpdate"]
# }]
# - SLACK_BOT_TOKEN=
# - SLACK_CHANNEL_ID=
working_dir: /app
command: ["sh", "-c", "npm run start"]
depends_on:
postgres:
condition: service_healthy
client:
build:
context: ./client
dockerfile: ../config/development/Dockerfile.client
volumes:
- ./client:/app/client
- /app/client/node_modules
- /app/node_modules
environment:
- NODE_ENV=development
- CHOKIDAR_USEPOLLING=true
- BASE_URL=http://localhost:3000
- REACT_APP_SERVER_BASE_URL=http://localhost:3000
working_dir: /app/client
command: npm start
init-db:
build:
context: ./server
dockerfile: ../config/development/Dockerfile.server
environment:
- DATABASE_URL=postgresql://user:password@postgres:5432/planka_db
# - DEFAULT_ADMIN_EMAIL=demo@demo.demo # Do not remove if you want to prevent this user from being edited/deleted
# - DEFAULT_ADMIN_PASSWORD=demo
# - DEFAULT_ADMIN_NAME=Demo Demo
# - DEFAULT_ADMIN_USERNAME=demo
working_dir: /app
command: ["sh", "-c", "npm run db:init"]
volumes:
- ./server:/app
- /app/node_modules
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:14-alpine
restart: on-failure
image: postgres:latest
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=planka
- POSTGRES_HOST_AUTH_METHOD=trust
POSTGRES_DB: planka_db
POSTGRES_USER: user
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d planka"]
interval: 10s
timeout: 5s
retries: 5
proxy:
image: nginx:alpine
ports:
- "3000:80"
volumes:
- ./config/development/nginx.conf:/etc/nginx/nginx.conf
depends_on:
- server
- client
volumes:
user-avatars:
project-background-images:
attachments:
db-data:
+12
View File
@@ -31,6 +31,8 @@ services:
# - DEFAULT_ADMIN_NAME=Demo Demo
# - DEFAULT_ADMIN_USERNAME=demo
# - ALLOW_ALL_TO_CREATE_PROJECTS=true
# - OIDC_ISSUER=
# - OIDC_CLIENT_ID=
# - OIDC_CLIENT_SECRET=
@@ -47,11 +49,21 @@ services:
# Email Notifications (https://nodemailer.com/smtp/)
# - SMTP_HOST=
# - SMTP_PORT=587
# - SMTP_NAME=
# - SMTP_SECURE=true
# - SMTP_USER=
# - SMTP_PASSWORD=
# - SMTP_FROM="Demo Demo" <demo@demo.demo>
# Optional fields: accessToken, events, excludedEvents
# - |
# WEBHOOKS=[{
# "url": "http://localhost:3001",
# "accessToken": "notaccesstoken",
# "events": ["cardCreate", "cardUpdate", "cardDelete"],
# "excludedEvents": ["notificationCreate", "notificationUpdate"]
# }]
# - SLACK_BOT_TOKEN=
# - SLACK_CHANNEL_ID=
depends_on:
+262 -2267
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "planka",
"version": "1.16.2",
"version": "1.19.2",
"private": true,
"homepage": "https://plankanban.github.io/planka",
"repository": {
@@ -18,7 +18,7 @@
"docker:build:base": "docker build -t ghcr.io/plankanban/planka:base-local -f Dockerfile.base .",
"postinstall": "(cd server && npm i && cd ../client && npm i)",
"lint": "npm run server:lint && npm run client:lint",
"prepare": "husky install",
"prepare": "husky",
"server:console": "npm run console --prefix server",
"server:db:init": "npm run db:init --prefix server",
"server:db:migrate": "npm run db:migrate --prefix server",
@@ -57,13 +57,13 @@
},
"dependencies": {
"concurrently": "^8.2.2",
"husky": "^8.0.3",
"lint-staged": "^15.1.0"
"husky": "^9.0.1",
"lint-staged": "^15.2.5"
},
"devDependencies": {
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"prettier": "^3.1.0"
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"prettier": "^3.3.0"
}
}
+10
View File
@@ -22,6 +22,8 @@ SECRET_KEY=notsecretkey
# DEFAULT_ADMIN_NAME=Demo Demo
# DEFAULT_ADMIN_USERNAME=demo
# ALLOW_ALL_TO_CREATE_PROJECTS=true
# OIDC_ISSUER=
# OIDC_CLIENT_ID=
# OIDC_CLIENT_SECRET=
@@ -43,6 +45,14 @@ SECRET_KEY=notsecretkey
# SMTP_PASSWORD=
# SMTP_FROM="Demo Demo" <demo@demo.demo>
# Optional fields: accessToken, events, excludedEvents
# WEBHOOKS='[{
# "url": "http://localhost:3001",
# "accessToken": "notaccesstoken",
# "events": ["cardCreate", "cardUpdate", "cardDelete"],
# "excludedEvents": ["notificationCreate", "notificationUpdate"]
# }]'
# SLACK_BOT_TOKEN=
# SLACK_CHANNEL_ID=
+5 -2
View File
@@ -44,12 +44,12 @@ module.exports = {
async fn(inputs, exits) {
const { currentUser } = this.req;
const { card } = await sails.helpers.cards
const { card, list, board, project } = await sails.helpers.cards
.getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
const boardMembership = await BoardMembership.findOne({
boardId: card.boardId,
boardId: board.id,
userId: currentUser.id,
});
@@ -83,6 +83,9 @@ module.exports = {
const fileData = await sails.helpers.attachments.processUploadedFile(file);
const attachment = await sails.helpers.attachments.createOne.with({
project,
board,
list,
values: {
...fileData,
card,

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