Compare commits

..
36 Commits
Author SHA1 Message Date
Daniel Hiller a26c468a9d Add: AI-generated translations
Adding missing translation from the top 10 most spoken languages

* Hindi
* Arabic
* Bengali

* Dutch (because requested)
2023-09-30 02:44:26 +02:00
Daniel Hiller 0f076f4cc4 Add: Translation Bengali (AI) 2023-09-30 02:33:09 +02:00
Daniel Hiller f1865db477 Add: Translation Arabic (AI) 2023-09-30 02:27:51 +02:00
Daniel Hiller b80127e952 Add: Translation Dutch (AI) 2023-09-30 02:18:27 +02:00
Daniel Hiller 5e400c5c4e Add: Translation Hindi (AI) 2023-09-30 02:15:22 +02:00
Daniel Hiller 8367fa4a29 Add: Dutch translation (AI) 2023-09-30 02:04:48 +02:00
Daniel Hiller aae69cb5e4 CI: Add prebuild planka package on release 2023-09-30 01:59:09 +02:00
Daniel Hiller 782c0356c8 Meta: Add missing return 2023-09-23 03:48:32 +02:00
Daniel Hiller 757cdb14a4 Meta: Add Automated installation 2023-09-23 03:47:46 +02:00
Daniel Hiller d896c3183b chore: fix docker dev build 2023-09-14 01:27:29 +02:00
Daniel Hiller 13ee0c3466 chore: fix runs-on name 2023-09-13 22:55:09 +02:00
Daniel Hiller 211c841379 chore: Switch to self hostet action runner 2023-09-13 22:47:08 +02:00
Daniel Hiller 90a30961ff chore: Add dev container, based on latest commit 2023-09-13 22:39:37 +02:00
Daniel Hiller 7cb9774e05 chore: Switch to pnpm package manager 2023-09-13 22:38:37 +02:00
Maksim Eltyshev 875895b331 fix: Fix starting without OIDC environment variables
Closes #503
2023-09-12 01:20:21 +02:00
Maksim Eltyshev 91bc889fed feat: Use environment variables for default admin configuration 2023-09-12 01:12:38 +02:00
Gilberto Vidal a34d8e9753 fix: Fix Turkish translation linting (#504) 2023-09-11 22:21:46 +05:00
gorrilla10101 107cb85ba2 feat: OIDC with PKCE flow (#491) 2023-09-04 20:06:59 +05:00
creador30 9333ef562e feat: Add Turkish translation (#501) 2023-09-04 19:41:11 +05:00
RocketProto 7d28abc7e6 fix: Decouple BASE_URL from Ingress host (#498)
Closes #496
2023-08-27 02:26:45 +05:00
Andrey 5317950b5c fix: Update Russian translation (#497) 2023-08-22 14:34:45 +05:00
Maksim Eltyshev 7501619b7e fix: Fix Czech translation linting
Closes #487
2023-07-31 11:54:50 +02:00
leroyloren 080e29aa03 fix: Update Czech translation (#485) 2023-07-27 04:07:47 +05:00
[object Object] 48c532b2c2 feat: Add edit icon to project name (#457) 2023-07-09 20:49:08 +05:00
Maksim Eltyshev a62815f825 ref: Little refactoring 2023-06-13 12:06:28 +02:00
Maksim Eltyshev 6938232dc4 fix: Keep task edits when textarea loses focus 2023-06-13 12:04:49 +02:00
[object Object] 4c4cfa9c66 fix: Keep comment edits when textarea loses focus (#443)
Closes #441
2023-06-13 14:49:54 +05:00
Maksim Eltyshev ba89d41b19 chore: Update chart version 2023-06-13 00:46:17 +02:00
Maksim Eltyshev 5c435414f7 chore: Update version 2023-06-13 00:25:40 +02:00
Maksim Eltyshev 2d7812a679 fix: Use creation timestamp instead of id for ordering
Closes #465
2023-06-13 00:19:17 +02:00
Maksim Eltyshev d386c82973 fix: Fix saving milliseconds for timestamps 2023-06-12 23:54:57 +02:00
ULiiAn 6c4db967c2 fix: Fix Chinese translation (#464)
Closes #463
2023-06-12 14:51:31 +05:00
Christopher Greaves 932d8225ed feat: Add official Helm Chart (#442)
Closes #192
2023-05-25 20:23:11 +05:00
orbatschow 0d481703da feat: Allow postgres connections that require ssl mode (#409)
Closes #261
2023-03-06 17:54:52 +05:00
Zsh 23e2b4a622 fix: Update Chinese translation (#410) 2023-03-06 17:12:59 +05:00
LuisLiu f4cc51e1c0 feat: Add Chinese translation (#398) 2023-03-06 17:07:00 +05:00
109 changed files with 3447 additions and 270 deletions
@@ -0,0 +1,57 @@
name: Build and publish release package
on:
release:
types: [created]
jobs:
build-and-publish-release-package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Workflow install pnpm
run: npm install pnpm -g
- name: Client install dependencies
run: pnpm install
- name: Server install dependencies
run: pnpm install
- name: Server include into dist
run: mv server/ dist/
- name: Client build production
run: |
npm run build
working-directory: ./client
- name: Client include into dist
run: |
mv build/index.html ../dist/views/index.ejs
mv build/* ../dist/public/
working-directory: ./client
- name: Dist include README.md SECURITY.md LICENSE start.sh
run: mv README.md SECURITY.md LICENSE start.sh dist/
- name: Dist Remove node modules
run: rm -R dist/node_modules
- name: Dist create .zip file
run: |
mv dist/ planka/
zip -r planka-prebuild-${{ github.event.release.tag_name }}.zip planka
- name: Dist upload assets
run: |
gh release upload ${{ github.event.release.tag_name }} planka-prebuild-${{ github.event.release.tag_name }}.zip
env:
GH_TOKEN: ${{ github.token }}
@@ -13,26 +13,26 @@ env:
jobs: jobs:
build-and-push-docker-base-image: build-and-push-docker-base-image:
runs-on: ubuntu-latest runs-on: self-hosted
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v4
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
@@ -0,0 +1,34 @@
name: Build and push Docker DEV image
on:
push:
branches: [master]
jobs:
build-and-push-docker-image-dev:
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- 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:dev
@@ -6,19 +6,19 @@ on:
jobs: jobs:
build-and-push-docker-image: build-and-push-docker-image:
runs-on: ubuntu-latest runs-on: self-hosted
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -32,7 +32,7 @@ jobs:
script: return context.payload.release.tag_name.replace('v', '') script: return context.payload.release.tag_name.replace('v', '')
- name: Build and push - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v4
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
+42
View File
@@ -0,0 +1,42 @@
name: Release Charts
on:
push:
paths:
- "charts/**"
branches:
- master
jobs:
release:
# depending on default permission settings for your org (contents being read-only or read-write for workloads), you will have to add permissions
# see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
permissions:
contents: write
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure Git
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Install Helm
uses: azure/setup-helm@v3
- name: Add repositories
run: |
for dir in $(ls -d charts/*/); do
helm dependency list $dir 2> /dev/null | tail +2 | head -n -1 | awk '{ print "helm repo add " $1 " " $3 }' | while read cmd; do $cmd; done
done
- name: Run chart-releaser for stable
uses: helm/chart-releaser-action@v1.5.0
with:
charts_dir: charts
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+3
View File
@@ -10,3 +10,6 @@ docker-compose.override.yml
# `npm i --package-lock-only` # `npm i --package-lock-only`
pnpm-lock.yaml pnpm-lock.yaml
yarn.lock yarn.lock
# Chart dependencies
**/charts/*.tgz
+4 -2
View File
@@ -5,7 +5,8 @@ WORKDIR /app
COPY server/package.json server/package-lock.json . COPY server/package.json server/package-lock.json .
RUN npm install npm@latest --global \ RUN npm install npm@latest --global \
&& npm clean-install --omit=dev && npm install pnpm --global \
&& pnpm install --prod
FROM node:lts AS client FROM node:lts AS client
@@ -14,7 +15,8 @@ WORKDIR /app
COPY client/package.json client/package-lock.json . COPY client/package.json client/package-lock.json .
RUN npm install npm@latest --global \ RUN npm install npm@latest --global \
&& npm clean-install --omit=dev && npm install pnpm --global \
&& pnpm install --prod
COPY client . COPY client .
RUN DISABLE_ESLINT_PLUGIN=true npm run build RUN DISABLE_ESLINT_PLUGIN=true npm run build
+1
View File
@@ -23,6 +23,7 @@
There are 2 types of installation: There are 2 types of installation:
- [Without Docker](https://docs.planka.cloud/docs/installl-planka/Debian%20&%20Ubuntu) ([for Windows](https://docs.planka.cloud/docs/installl-planka/Windows)) - [Without Docker](https://docs.planka.cloud/docs/installl-planka/Debian%20&%20Ubuntu) ([for Windows](https://docs.planka.cloud/docs/installl-planka/Windows))
- [Dockerized](https://docs.planka.cloud/docs/installl-planka/Docker%20Compose) - [Dockerized](https://docs.planka.cloud/docs/installl-planka/Docker%20Compose)
- [Automated installation](https://github.com/plankanban/planka-installer)
For configuration, please see the [configuration section](https://docs.planka.cloud/docs/category/configuration). For configuration, please see the [configuration section](https://docs.planka.cloud/docs/category/configuration).
+23
View File
@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
+6
View File
@@ -0,0 +1,6 @@
dependencies:
- name: postgresql
repository: https://charts.bitnami.com/bitnami
version: 12.5.1
digest: sha256:01dfb2d07ab6800b4a5a6c81f20f3377a758124b2b96b891d0cd6b4f64cf783b
generated: "2023-05-15T00:54:48.1308917+01:00"
+31
View File
@@ -0,0 +1,31 @@
apiVersion: v2
name: planka
description: A Helm chart to deploy Planka and it's dependencies.
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
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.2
# 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.12.0"
dependencies:
- alias: postgresql
condition: postgresql.enabled
name: postgresql
repository: &bitnami-repo https://charts.bitnami.com/bitnami
version: 12.5.1
+84
View File
@@ -0,0 +1,84 @@
# Planka Helm Chart
[Planka](https://github.com/plankanban/planka) is an OSS alternative to Trello that you can host yourself, and this is a Helm Chart to make it easier to deploy to K8s.
Shoutout to [this issue](https://github.com/plankanban/planka/issues/192) who have been asking for this Helm Chart.
## Issues
By using the Bitnami chart for PostgreSQL, there is an issue where once deployed, if trying to use a different password then it will be ignored as the Persistant Volume (PV) will already exist with the previous password. See warning from Bitnami below:
> **Warning!** Setting a password will be ignored on new installation in the case when previous Posgresql release was deleted through the helm command. In that case, old PVC will have an old password, and setting it through helm won't take effect. Deleting persistent volumes (PVs) will solve the issue. Refer to [issue 2061](https://github.com/bitnami/charts/issues/2061) for more details
If you want to fully uninstall this chart including the data, follow [these steps](https://github.com/bitnami/charts/blob/main/bitnami/postgresql/README.md#uninstalling-the-chart) from the Bitnami Chart's docs.
## Usage
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
helm dependency build
export SECRETKEY=$(openssl rand -hex 64)
helm install planka . --set secretkey=$SECRETKEY
```
> **NOTE:** The command `openssl rand -hex 64` is needed to create a random hexadecimal key for planka. On Windows you can use Git Bash to run that command.
To access Planka you can port forward using the following command:
```bash
kubectl port-forward $POD_NAME 3000:1337
```
### Accessing Externally
To access Planka externally you can use the following configuration
```bash
# HTTP only
helm install planka . --set secretkey=$SECRETKEY \
--set ingress.enabled=true \
--set ingress.hosts[0].host=planka.example.dev \
# HTTPS
helm install planka . --set secretkey=$SECRETKEY \
--set ingress.enabled=true \
--set ingress.hosts[0].host=planka.example.dev \
--set ingress.tls[0].secretName=planka-tls \
--set ingress.tls[0].hosts[0]=planka.example.dev \
```
or create a values.yaml file like:
```yaml
secretkey: "<InsertSecretKey>"
ingress:
enabled: true
hosts:
- host: planka.example.dev
paths:
- path: /
pathType: ImplementationSpecific
# Needed for HTTPS
tls:
- secretName: planka-tls # existing TLS secret in k8s
hosts:
- planka.example.dev
```
```bash
helm install planka . -f values.yaml
```
### Things to consider if production hosting
If you want to host Planka for more than just playing around with, you might want to do the following things:
- Create a `values.yaml` with your config, as this will make applying upgrades much easier in the future.
- Create your `secretkey` once and store it either in a secure vault, or in your `values.yaml` file so it will be the same for upgrading in the future.
- Specify a password for `postgresql.auth.password` as there have been issues with the postgresql chart generating new passwords locking you out of the data you've already stored. (see [this issue](https://github.com/bitnami/charts/issues/2061))
Any questions or concerns, [raise an issue](https://github.com/Chris-Greaves/planka-helm-chart/issues/new).
+22
View File
@@ -0,0 +1,22 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "planka.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "planka.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "planka.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "planka.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://localhost:3000 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 3000:$CONTAINER_PORT
{{- end }}
+62
View File
@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "planka.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "planka.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "planka.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "planka.labels" -}}
helm.sh/chart: {{ include "planka.chart" . }}
{{ include "planka.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "planka.selectorLabels" -}}
app.kubernetes.io/name: {{ include "planka.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "planka.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "planka.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
+97
View File
@@ -0,0 +1,97 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "planka.fullname" . }}
labels:
{{- include "planka.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "planka.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "planka.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "planka.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
volumeMounts:
- mountPath: /app/public/user-avatars
subPath: user-avatars
name: planka
- mountPath: /app/public/project-background-images
subPath: project-background-images
name: planka
- mountPath: /app/private/attachments
subPath: attachments
name: planka
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: planka-postgresql-svcbind-custom-user
key: uri
- name: BASE_URL
{{- if .Values.baseUrl }}
value: {{ .Values.baseUrl }}
{{- else if .Values.ingress.enabled }}
value: {{ printf "https://%s" (first .Values.ingress.hosts).host }}
{{- else }}
value: http://localhost:3000
{{- end }}
- name: SECRET_KEY
value: {{ required "A secret key needs to be generated using 'openssl rand -hex 64' and assigned to secretkey." .Values.secretkey }}
- name: TRUST_PROXY
value: "0"
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
- name: planka
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ .Values.persistence.existingClaim | default (include "planka.fullname" .) }}
{{- else }}
emptyDir: {}
{{- end }}
+28
View File
@@ -0,0 +1,28 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "planka.fullname" . }}
labels:
{{- include "planka.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "planka.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}
+61
View File
@@ -0,0 +1,61 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "planka.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "planka.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+25
View File
@@ -0,0 +1,25 @@
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: {{ include "planka.fullname" . }}
labels:
app.kubernetes.io/name: {{ include "planka.name" . }}
helm.sh/chart: {{ include "planka.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
accessModes:
- {{ .Values.persistence.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- if .Values.persistence.storageClass }}
{{- if (eq "-" .Values.persistence.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: "{{ .Values.persistence.storageClass }}"
{{- end }}
{{- end }}
{{- end }}
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "planka.fullname" . }}
labels:
{{- include "planka.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "planka.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "planka.serviceAccountName" . }}
labels:
{{- include "planka.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "planka.fullname" . }}-test-connection"
labels:
{{- include "planka.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "planka.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never
+111
View File
@@ -0,0 +1,111 @@
# Default values for planka.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: ghcr.io/plankanban/planka
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
# Generate a secret using openssl rand -base64 45
secretkey: ""
# Base url for Planka. Will override `ingress.hosts[0].host`
# Defaults to `http://localhost:3000` if ingress is disabled.
baseUrl: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 1337
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
# Used to set planka BASE_URL if no `baseurl` is provided.
- host: planka.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: planka-tls
# hosts:
# - planka.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
postgresql:
enabled: true
auth:
database: planka
username: planka
password: ""
postgresPassword: ""
replicationPassword: ""
# existingSecret: planka-postgresql
serviceBindings:
enabled: true
## PVC-based data storage configuration
persistence:
enabled: false
# existingClaim: netbox-data
# storageClass: "-"
accessMode: ReadWriteOnce
size: 10Gi
+1 -1
View File
@@ -1 +1 @@
REACT_APP_VERSION=1.10.3 REACT_APP_VERSION=1.12.0
+54
View File
@@ -30,6 +30,7 @@
"react-i18next": "^12.0.0", "react-i18next": "^12.0.0",
"react-input-mask": "^2.0.4", "react-input-mask": "^2.0.4",
"react-markdown": "^8.0.3", "react-markdown": "^8.0.3",
"react-oidc-context": "^2.2.2",
"react-photoswipe-gallery": "^2.2.2", "react-photoswipe-gallery": "^2.2.2",
"react-redux": "^8.0.5", "react-redux": "^8.0.5",
"react-router-dom": "^6.4.3", "react-router-dom": "^6.4.3",
@@ -7189,6 +7190,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/crypto-js": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
"integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==",
"peer": true
},
"node_modules/crypto-random-string": { "node_modules/crypto-random-string": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -17035,6 +17042,19 @@
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
}, },
"node_modules/oidc-client-ts": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.2.4.tgz",
"integrity": "sha512-nOZwIomju+AmXObl5Oq5PjrES/qTt8bLsENJCIydVgi9TEWk7SCkOU6X3RNkY7yfySRM1OJJvDKdREZdmnDT2g==",
"peer": true,
"dependencies": {
"crypto-js": "^4.1.1",
"jwt-decode": "^3.1.2"
},
"engines": {
"node": ">=12.13.0"
}
},
"node_modules/on-finished": { "node_modules/on-finished": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -19276,6 +19296,18 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
}, },
"node_modules/react-oidc-context": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-2.2.2.tgz",
"integrity": "sha512-rke8goKuxxQhSAR11h8wVn56m7kEa1mcAfYDlIycsIgmbZOFzKA0Un0y0RodHZ/M/CG6u0JD1I8RKZHqRJjWnA==",
"engines": {
"node": ">=12.13.0"
},
"peerDependencies": {
"oidc-client-ts": "^2.2.1",
"react": ">=16.8.0"
}
},
"node_modules/react-onclickoutside": { "node_modules/react-onclickoutside": {
"version": "6.12.2", "version": "6.12.2",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz",
@@ -28767,6 +28799,12 @@
"which": "^2.0.1" "which": "^2.0.1"
} }
}, },
"crypto-js": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
"integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==",
"peer": true
},
"crypto-random-string": { "crypto-random-string": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -35933,6 +35971,16 @@
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
}, },
"oidc-client-ts": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.2.4.tgz",
"integrity": "sha512-nOZwIomju+AmXObl5Oq5PjrES/qTt8bLsENJCIydVgi9TEWk7SCkOU6X3RNkY7yfySRM1OJJvDKdREZdmnDT2g==",
"peer": true,
"requires": {
"crypto-js": "^4.1.1",
"jwt-decode": "^3.1.2"
}
},
"on-finished": { "on-finished": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -37371,6 +37419,12 @@
} }
} }
}, },
"react-oidc-context": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-2.2.2.tgz",
"integrity": "sha512-rke8goKuxxQhSAR11h8wVn56m7kEa1mcAfYDlIycsIgmbZOFzKA0Un0y0RodHZ/M/CG6u0JD1I8RKZHqRJjWnA==",
"requires": {}
},
"react-onclickoutside": { "react-onclickoutside": {
"version": "6.12.2", "version": "6.12.2",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz",
+1
View File
@@ -77,6 +77,7 @@
"react-i18next": "^12.0.0", "react-i18next": "^12.0.0",
"react-input-mask": "^2.0.4", "react-input-mask": "^2.0.4",
"react-markdown": "^8.0.3", "react-markdown": "^8.0.3",
"react-oidc-context": "^2.2.2",
"react-photoswipe-gallery": "^2.2.2", "react-photoswipe-gallery": "^2.2.2",
"react-redux": "^8.0.5", "react-redux": "^8.0.5",
"react-router-dom": "^6.4.3", "react-router-dom": "^6.4.3",
+3
View File
@@ -4,6 +4,8 @@ import socket from './socket';
/* Actions */ /* Actions */
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers); const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
const exchangeOidcToken = (accessToken, headers) =>
http.post('/access-tokens/exchange', { token: accessToken }, headers);
const deleteCurrentAccessToken = (headers) => const deleteCurrentAccessToken = (headers) =>
socket.delete('/access-tokens/me', undefined, headers); socket.delete('/access-tokens/me', undefined, headers);
@@ -11,4 +13,5 @@ const deleteCurrentAccessToken = (headers) =>
export default { export default {
createAccessToken, createAccessToken,
deleteCurrentAccessToken, deleteCurrentAccessToken,
exchangeOidcToken,
}; };
+5
View File
@@ -1,4 +1,5 @@
import socket from './socket'; import socket from './socket';
import { transformUser } from './users';
/* Transformers */ /* Transformers */
@@ -13,6 +14,10 @@ const getActivities = (cardId, data, headers) =>
socket.get(`/cards/${cardId}/actions`, data, headers).then((body) => ({ socket.get(`/cards/${cardId}/actions`, data, headers).then((body) => ({
...body, ...body,
items: body.items.map(transformActivity), items: body.items.map(transformActivity),
included: {
...body.included,
users: body.included.users.map(transformUser),
},
})); }));
/* Event handlers */ /* Event handlers */
+35 -3
View File
@@ -1,18 +1,50 @@
import socket from './socket'; import socket from './socket';
/* Transformers */
export const transformBoardMembership = (boardMembership) => ({
...boardMembership,
createdAt: new Date(boardMembership.createdAt),
});
/* Actions */ /* Actions */
const createBoardMembership = (boardId, data, headers) => const createBoardMembership = (boardId, data, headers) =>
socket.post(`/boards/${boardId}/memberships`, data, headers); socket.post(`/boards/${boardId}/memberships`, data, headers).then((body) => ({
...body,
item: transformBoardMembership(body.item),
}));
const updateBoardMembership = (id, data, headers) => const updateBoardMembership = (id, data, headers) =>
socket.patch(`/board-memberships/${id}`, data, headers); socket.patch(`/board-memberships/${id}`, data, headers).then((body) => ({
...body,
item: transformBoardMembership(body.item),
}));
const deleteBoardMembership = (id, headers) => const deleteBoardMembership = (id, headers) =>
socket.delete(`/board-memberships/${id}`, undefined, headers); socket.delete(`/board-memberships/${id}`, undefined, headers).then((body) => ({
...body,
item: transformBoardMembership(body.item),
}));
/* Event handlers */
const makeHandleBoardMembershipCreate = (next) => (body) => {
next({
...body,
item: transformBoardMembership(body.item),
});
};
const makeHandleBoardMembershipUpdate = makeHandleBoardMembershipCreate;
const makeHandleBoardMembershipDelete = makeHandleBoardMembershipCreate;
export default { export default {
createBoardMembership, createBoardMembership,
updateBoardMembership, updateBoardMembership,
deleteBoardMembership, deleteBoardMembership,
makeHandleBoardMembershipCreate,
makeHandleBoardMembershipUpdate,
makeHandleBoardMembershipDelete,
}; };
+11 -1
View File
@@ -1,12 +1,20 @@
import socket from './socket'; import socket from './socket';
import http from './http'; import http from './http';
import { transformUser } from './users';
import { transformBoardMembership } from './board-memberships';
import { transformCard } from './cards'; import { transformCard } from './cards';
import { transformAttachment } from './attachments'; import { transformAttachment } from './attachments';
/* Actions */ /* Actions */
const createBoard = (projectId, data, headers) => const createBoard = (projectId, data, headers) =>
socket.post(`/projects/${projectId}/boards`, data, headers); socket.post(`/projects/${projectId}/boards`, data, headers).then((body) => ({
...body,
included: {
...body.included,
boardMemberships: body.included.boardMemberships.map(transformBoardMembership),
},
}));
const createBoardWithImport = (projectId, data, requestId, headers) => const createBoardWithImport = (projectId, data, requestId, headers) =>
http.post(`/projects/${projectId}/boards?requestId=${requestId}`, data, headers); http.post(`/projects/${projectId}/boards?requestId=${requestId}`, data, headers);
@@ -18,6 +26,8 @@ const getBoard = (id, subscribe, headers) =>
...body, ...body,
included: { included: {
...body.included, ...body.included,
users: body.included.users.map(transformUser),
boardMemberships: body.included.boardMemberships.map(transformBoardMembership),
cards: body.included.cards.map(transformCard), cards: body.included.cards.map(transformCard),
attachments: body.included.attachments.map(transformAttachment), attachments: body.included.attachments.map(transformAttachment),
}, },
+3
View File
@@ -1,6 +1,7 @@
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import socket from './socket'; import socket from './socket';
import { transformUser } from './users';
import { transformCard } from './cards'; import { transformCard } from './cards';
import { transformActivity } from './activities'; import { transformActivity } from './activities';
@@ -19,6 +20,7 @@ const getNotifications = (headers) =>
items: body.items.map(transformNotification), items: body.items.map(transformNotification),
included: { included: {
...omit(body.included, 'actions'), ...omit(body.included, 'actions'),
users: body.included.users.map(transformUser),
cards: body.included.cards.map(transformCard), cards: body.included.cards.map(transformCard),
activities: body.included.actions.map(transformActivity), activities: body.included.actions.map(transformActivity),
}, },
@@ -30,6 +32,7 @@ const getNotification = (id, headers) =>
item: transformNotification(body.item), item: transformNotification(body.item),
included: { included: {
...omit(body.included, 'actions'), ...omit(body.included, 'actions'),
users: body.included.users.map(transformUser),
cards: body.included.cards.map(transformCard), cards: body.included.cards.map(transformCard),
activities: body.included.actions.map(transformActivity), activities: body.included.actions.map(transformActivity),
}, },
+28 -2
View File
@@ -1,14 +1,40 @@
import socket from './socket'; import socket from './socket';
/* Transformers */
export const transformProjectManager = (projectManager) => ({
...projectManager,
createdAt: new Date(projectManager.createdAt),
});
/* Actions */ /* Actions */
const createProjectManager = (projectId, data, headers) => const createProjectManager = (projectId, data, headers) =>
socket.post(`/projects/${projectId}/managers`, data, headers); socket.post(`/projects/${projectId}/managers`, data, headers).then((body) => ({
...body,
item: transformProjectManager(body.item),
}));
const deleteProjectManager = (id, headers) => const deleteProjectManager = (id, headers) =>
socket.delete(`/project-managers/${id}`, undefined, headers); socket.delete(`/project-managers/${id}`, undefined, headers).then((body) => ({
...body,
item: transformProjectManager(body.item),
}));
/* Event handlers */
const makeHandleProjectManagerCreate = (next) => (body) => {
next({
...body,
item: transformProjectManager(body.item),
});
};
const makeHandleProjectManagerDelete = makeHandleProjectManagerCreate;
export default { export default {
createProjectManager, createProjectManager,
deleteProjectManager, deleteProjectManager,
makeHandleProjectManagerCreate,
makeHandleProjectManagerDelete,
}; };
+31 -3
View File
@@ -1,13 +1,41 @@
import http from './http'; import http from './http';
import socket from './socket'; import socket from './socket';
import { transformUser } from './users';
import { transformProjectManager } from './project-managers';
import { transformBoardMembership } from './board-memberships';
/* Actions */ /* Actions */
const getProjects = (headers) => socket.get('/projects', undefined, headers); const getProjects = (headers) =>
socket.get('/projects', undefined, headers).then((body) => ({
...body,
included: {
...body.included,
users: body.included.users.map(transformUser),
projectManagers: body.included.projectManagers.map(transformProjectManager),
boardMemberships: body.included.boardMemberships.map(transformBoardMembership),
},
}));
const createProject = (data, headers) => socket.post('/projects', data, headers); const createProject = (data, headers) =>
socket.post('/projects', data, headers).then((body) => ({
...body,
included: {
...body.included,
projectManagers: body.included.projectManagers.map(transformProjectManager),
},
}));
const getProject = (id, headers) => socket.get(`/projects/${id}`, undefined, headers); const getProject = (id, headers) =>
socket.get(`/projects/${id}`, undefined, headers).then((body) => ({
...body,
included: {
...body.included,
users: body.included.users.map(transformUser),
projectManagers: body.included.projectManagers.map(transformProjectManager),
boardMemberships: body.included.boardMemberships.map(transformBoardMembership),
},
}));
const updateProject = (id, data, headers) => socket.patch(`/projects/${id}`, data, headers); const updateProject = (id, data, headers) => socket.patch(`/projects/${id}`, data, headers);
+70 -10
View File
@@ -1,30 +1,87 @@
import http from './http'; import http from './http';
import socket from './socket'; import socket from './socket';
/* Transformers */
export const transformUser = (user) => ({
...user,
createdAt: new Date(user.createdAt),
});
/* Actions */ /* Actions */
const getUsers = (headers) => socket.get('/users', undefined, headers); const getUsers = (headers) =>
socket.get('/users', undefined, headers).then((body) => ({
...body,
items: body.items.map(transformUser),
}));
const createUser = (data, headers) => socket.post('/users', data, headers); const createUser = (data, headers) =>
socket.post('/users', data, headers).then((body) => ({
...body,
item: transformUser(body.item),
}));
const getUser = (id, headers) => socket.get(`/users/${id}`, undefined, headers); const getUser = (id, headers) =>
socket.get(`/users/${id}`, undefined, headers).then((body) => ({
...body,
item: transformUser(body.item),
}));
const getCurrentUser = (subscribe, headers) => const getCurrentUser = (subscribe, headers) =>
socket.get(`/users/me${subscribe ? '?subscribe=true' : ''}`, undefined, headers); socket.get(`/users/me${subscribe ? '?subscribe=true' : ''}`, undefined, headers).then((body) => ({
...body,
item: transformUser(body.item),
}));
const updateUser = (id, data, headers) => socket.patch(`/users/${id}`, data, headers); const updateUser = (id, data, headers) =>
socket.patch(`/users/${id}`, data, headers).then((body) => ({
...body,
item: transformUser(body.item),
}));
const updateUserEmail = (id, data, headers) => socket.patch(`/users/${id}/email`, data, headers); const updateUserEmail = (id, data, headers) =>
socket.patch(`/users/${id}/email`, data, headers).then((body) => ({
...body,
item: transformUser(body.item),
}));
const updateUserPassword = (id, data, headers) => const updateUserPassword = (id, data, headers) =>
socket.patch(`/users/${id}/password`, data, headers); socket.patch(`/users/${id}/password`, data, headers).then((body) => ({
...body,
item: transformUser(body.item),
}));
const updateUserUsername = (id, data, headers) => const updateUserUsername = (id, data, headers) =>
socket.patch(`/users/${id}/username`, data, headers); socket.patch(`/users/${id}/username`, data, headers).then((body) => ({
...body,
item: transformUser(body.item),
}));
const updateUserAvatar = (id, data, headers) => http.post(`/users/${id}/avatar`, data, headers); const updateUserAvatar = (id, data, headers) =>
http.post(`/users/${id}/avatar`, data, headers).then((body) => ({
...body,
item: transformUser(body.item),
}));
const deleteUser = (id, headers) => socket.delete(`/users/${id}`, undefined, headers); const deleteUser = (id, headers) =>
socket.delete(`/users/${id}`, undefined, headers).then((body) => ({
...body,
item: transformUser(body.item),
}));
/* Event handlers */
const makeHandleUserCreate = (next) => (body) => {
next({
...body,
item: transformUser(body.item),
});
};
const makeHandleUserUpdate = makeHandleUserCreate;
const makeHandleUserDelete = makeHandleUserCreate;
export default { export default {
getUsers, getUsers,
@@ -37,4 +94,7 @@ export default {
updateUserUsername, updateUserUsername,
updateUserAvatar, updateUserAvatar,
deleteUser, deleteUser,
makeHandleUserCreate,
makeHandleUserUpdate,
makeHandleUserDelete,
}; };
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react'; import { Button, Form, TextArea } from 'semantic-ui-react';
import { useClosableForm, useForm } from '../../../hooks'; import { useForm } from '../../../hooks';
import styles from './CommentEdit.module.scss'; import styles from './CommentEdit.module.scss';
@@ -35,12 +35,7 @@ const CommentEdit = React.forwardRef(({ children, defaultData, onUpdate }, ref)
text: data.text.trim(), text: data.text.trim(),
}; };
if (!cleanData.text) { if (cleanData.text && !dequal(cleanData, defaultData)) {
textField.current.ref.current.select();
return;
}
if (!dequal(cleanData, defaultData)) {
onUpdate(cleanData); onUpdate(cleanData);
} }
@@ -65,10 +60,9 @@ const CommentEdit = React.forwardRef(({ children, defaultData, onUpdate }, ref)
[submit], [submit],
); );
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm( const handleFieldBlur = useCallback(() => {
close, submit();
isOpened, }, [submit]);
);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
submit(); submit();
@@ -99,13 +93,7 @@ const CommentEdit = React.forwardRef(({ children, defaultData, onUpdate }, ref)
onBlur={handleFieldBlur} onBlur={handleFieldBlur}
/> />
<div className={styles.controls}> <div className={styles.controls}>
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */} <Button positive content={t('action.save')} />
<Button
positive
content={t('action.save')}
onMouseOver={handleControlMouseOver}
onMouseOut={handleControlMouseOut}
/>
</div> </div>
</Form> </Form>
); );
@@ -100,7 +100,6 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
onChange={setValue} onChange={setValue}
/> />
<div className={styles.controls}> <div className={styles.controls}>
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
<Button positive content={t('action.save')} /> <Button positive content={t('action.save')} />
</div> </div>
</Form> </Form>
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react'; import { Button, Form, TextArea } from 'semantic-ui-react';
import { useClosableForm, useField } from '../../../hooks'; import { useField } from '../../../hooks';
import styles from './NameEdit.module.scss'; import styles from './NameEdit.module.scss';
@@ -28,12 +28,7 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
const submit = useCallback(() => { const submit = useCallback(() => {
const cleanValue = value.trim(); const cleanValue = value.trim();
if (!cleanValue) { if (cleanValue && cleanValue !== defaultValue) {
field.current.ref.current.select();
return;
}
if (cleanValue !== defaultValue) {
onUpdate(cleanValue); onUpdate(cleanValue);
} }
@@ -60,10 +55,9 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
[submit], [submit],
); );
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm( const handleFieldBlur = useCallback(() => {
close, submit();
isOpened, }, [submit]);
);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
submit(); submit();
@@ -93,13 +87,7 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
onBlur={handleFieldBlur} onBlur={handleFieldBlur}
/> />
<div className={styles.controls}> <div className={styles.controls}>
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */} <Button positive content={t('action.save')} />
<Button
positive
content={t('action.save')}
onMouseOver={handleControlMouseOver}
onMouseOut={handleControlMouseOut}
/>
</div> </div>
</Form> </Form>
); );
+17 -9
View File
@@ -2,7 +2,8 @@ import React, { useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Icon, Menu } from 'semantic-ui-react'; import { Button, Icon, Menu } from 'semantic-ui-react';
import { useAuth } from 'react-oidc-context';
import { usePopup } from '../../lib/popup'; import { usePopup } from '../../lib/popup';
import Paths from '../../constants/Paths'; import Paths from '../../constants/Paths';
@@ -29,6 +30,7 @@ const Header = React.memo(
onUserSettingsClick, onUserSettingsClick,
onLogout, onLogout,
}) => { }) => {
const auth = useAuth();
const handleProjectSettingsClick = useCallback(() => { const handleProjectSettingsClick = useCallback(() => {
if (canEditProject) { if (canEditProject) {
onProjectSettingsClick(); onProjectSettingsClick();
@@ -38,6 +40,11 @@ const Header = React.memo(
const NotificationsPopup = usePopup(NotificationsStep, POPUP_PROPS); const NotificationsPopup = usePopup(NotificationsStep, POPUP_PROPS);
const UserPopup = usePopup(UserStep, POPUP_PROPS); const UserPopup = usePopup(UserStep, POPUP_PROPS);
const onFullLogout = () => {
auth.signoutSilent();
onLogout();
};
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
{!project && ( {!project && (
@@ -55,15 +62,16 @@ const Header = React.memo(
> >
<Icon fitted name="arrow left" /> <Icon fitted name="arrow left" />
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item className={classNames(styles.item, styles.title)}>
className={classNames( {project.name}
styles.item, {canEditProject && (
canEditProject && styles.itemHoverable, <Button
styles.title, className={classNames(styles.editButton, styles.target)}
)}
onClick={handleProjectSettingsClick} onClick={handleProjectSettingsClick}
> >
{project.name} <Icon fitted name="pencil" size="small" />
</Button>
)}
</Menu.Item> </Menu.Item>
</Menu.Menu> </Menu.Menu>
)} )}
@@ -87,7 +95,7 @@ const Header = React.memo(
<UserPopup <UserPopup
isLogouting={isLogouting} isLogouting={isLogouting}
onSettingsClick={onUserSettingsClick} onSettingsClick={onUserSettingsClick}
onLogout={onLogout} onLogout={onFullLogout}
> >
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}> <Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
{user.name} {user.name}
@@ -1,4 +1,20 @@
:global(#app) { :global(#app) {
.editButton {
background: transparent;
box-shadow: none;
color: #fff;
font-size: 15px;
line-height: 34px;
margin-left: 8px;
opacity: 0;
padding: 0;
width: 34px;
&:hover {
background: rgba(255, 255, 255, 0.08);
}
}
.item { .item {
cursor: auto; cursor: auto;
user-select: auto; user-select: auto;
@@ -11,6 +27,10 @@
&:hover { &:hover {
background: transparent; background: transparent;
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
.target {
opacity: 1;
}
} }
} }
+5
View File
@@ -1,5 +1,6 @@
import isEmail from 'validator/lib/isEmail'; import isEmail from 'validator/lib/isEmail';
import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useAuth } from 'react-oidc-context';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -48,6 +49,7 @@ const createMessage = (error) => {
const Login = React.memo( const Login = React.memo(
({ defaultData, isSubmitting, error, onAuthenticate, onMessageDismiss }) => { ({ defaultData, isSubmitting, error, onAuthenticate, onMessageDismiss }) => {
const auth = useAuth();
const [t] = useTranslation(); const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting); const wasSubmitting = usePrevious(isSubmitting);
@@ -171,6 +173,9 @@ const Login = React.memo(
disabled={isSubmitting} disabled={isSubmitting}
/> />
</Form> </Form>
<Form.Button type="button" onClick={() => auth.signinRedirect()}>
Log in with SSO
</Form.Button>
</div> </div>
</div> </div>
</Grid.Column> </Grid.Column>
+19
View File
@@ -0,0 +1,19 @@
import { useAuth } from 'react-oidc-context';
import PropTypes from 'prop-types';
import React from 'react';
let isLoggingIn = true;
const OidcLogin = React.memo(({ onAuthenticate }) => {
const auth = useAuth();
if (isLoggingIn && auth.user) {
isLoggingIn = false;
const { user } = auth;
onAuthenticate(user);
}
});
OidcLogin.propTypes = {
onAuthenticate: PropTypes.func.isRequired,
};
export default OidcLogin;
+3
View File
@@ -0,0 +1,3 @@
import OidcLogin from './OidcLogin';
export default OidcLogin;
+15 -3
View File
@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { AuthProvider } from 'react-oidc-context';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { Route, Routes } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import { ReduxRouter } from '../lib/redux-router'; import { ReduxRouter } from '../lib/redux-router';
@@ -13,15 +14,25 @@ import 'react-datepicker/dist/react-datepicker.css';
import 'photoswipe/dist/photoswipe.css'; import 'photoswipe/dist/photoswipe.css';
import 'easymde/dist/easymde.min.css'; import 'easymde/dist/easymde.min.css';
import '../lib/custom-ui/styles.css'; import '../lib/custom-ui/styles.css';
import '../styles.module.scss'; import '../styles.module.scss';
import OidcLoginContainer from '../containers/OidcLoginContainer';
function Root({ store, history }) { function Root({ store, history, config }) {
return ( return (
<AuthProvider
authority={config.authority}
client_id={config.clientId}
redirect_uri={config.redirectUri}
scope={config.scopes}
onSigninCallback={() => {
window.history.replaceState({}, document.title, window.location.pathname);
}}
>
<Provider store={store}> <Provider store={store}>
<ReduxRouter history={history}> <ReduxRouter history={history}>
<Routes> <Routes>
<Route path={Paths.LOGIN} element={<LoginContainer />} /> <Route path={Paths.LOGIN} element={<LoginContainer />} />
<Route path={Paths.OIDC_LOGIN} element={<OidcLoginContainer />} />
<Route path={Paths.ROOT} element={<CoreContainer />} /> <Route path={Paths.ROOT} element={<CoreContainer />} />
<Route path={Paths.PROJECTS} element={<CoreContainer />} /> <Route path={Paths.PROJECTS} element={<CoreContainer />} />
<Route path={Paths.BOARDS} element={<CoreContainer />} /> <Route path={Paths.BOARDS} element={<CoreContainer />} />
@@ -30,13 +41,14 @@ function Root({ store, history }) {
</Routes> </Routes>
</ReduxRouter> </ReduxRouter>
</Provider> </Provider>
</AuthProvider>
); );
} }
Root.propTypes = { Root.propTypes = {
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
store: PropTypes.object.isRequired, store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
config: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
}; };
@@ -1,4 +1,5 @@
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import omit from 'lodash/omit';
import pickBy from 'lodash/pickBy'; import pickBy from 'lodash/pickBy';
import React, { useCallback, useMemo, useRef } from 'react'; import React, { useCallback, useMemo, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -9,7 +10,7 @@ import { useForm } from '../../hooks';
import styles from './UserInformationEdit.module.scss'; import styles from './UserInformationEdit.module.scss';
const UserInformationEdit = React.memo(({ defaultData, onUpdate }) => { const UserInformationEdit = React.memo(({ defaultData, isNameEditable, onUpdate }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [data, handleFieldChange] = useForm(() => ({ const [data, handleFieldChange] = useForm(() => ({
@@ -32,13 +33,17 @@ const UserInformationEdit = React.memo(({ defaultData, onUpdate }) => {
const nameField = useRef(null); const nameField = useRef(null);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
if (isNameEditable) {
if (!cleanData.name) { if (!cleanData.name) {
nameField.current.select(); nameField.current.select();
return; return;
} }
onUpdate(cleanData); onUpdate(cleanData);
}, [onUpdate, cleanData]); } else {
onUpdate(omit(cleanData, 'name'));
}
}, [isNameEditable, onUpdate, cleanData]);
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
@@ -48,6 +53,7 @@ const UserInformationEdit = React.memo(({ defaultData, onUpdate }) => {
ref={nameField} ref={nameField}
name="name" name="name"
value={data.name} value={data.name}
disabled={!isNameEditable}
className={styles.field} className={styles.field}
onChange={handleFieldChange} onChange={handleFieldChange}
/> />
@@ -74,6 +80,7 @@ const UserInformationEdit = React.memo(({ defaultData, onUpdate }) => {
UserInformationEdit.propTypes = { UserInformationEdit.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
isNameEditable: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
}; };
@@ -5,7 +5,8 @@ import { Popup } from '../lib/custom-ui';
import UserInformationEdit from './UserInformationEdit'; import UserInformationEdit from './UserInformationEdit';
const UserInformationEditStep = React.memo(({ defaultData, onUpdate, onBack, onClose }) => { const UserInformationEditStep = React.memo(
({ defaultData, isNameEditable, onUpdate, onBack, onClose }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const handleUpdate = useCallback( const handleUpdate = useCallback(
@@ -24,14 +25,20 @@ const UserInformationEditStep = React.memo(({ defaultData, onUpdate, onBack, onC
})} })}
</Popup.Header> </Popup.Header>
<Popup.Content> <Popup.Content>
<UserInformationEdit defaultData={defaultData} onUpdate={handleUpdate} /> <UserInformationEdit
defaultData={defaultData}
isNameEditable={isNameEditable}
onUpdate={handleUpdate}
/>
</Popup.Content> </Popup.Content>
</> </>
); );
}); },
);
UserInformationEditStep.propTypes = { UserInformationEditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
isNameEditable: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onBack: PropTypes.func, onBack: PropTypes.func,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
@@ -23,6 +23,7 @@ const AccountPane = React.memo(
phone, phone,
organization, organization,
language, language,
isLocked,
isAvatarUpdating, isAvatarUpdating,
usernameUpdateForm, usernameUpdateForm,
emailUpdateForm, emailUpdateForm,
@@ -74,6 +75,7 @@ const AccountPane = React.memo(
phone, phone,
organization, organization,
}} }}
isNameEditable={!isLocked}
onUpdate={onUpdate} onUpdate={onUpdate}
/> />
<Divider horizontal section> <Divider horizontal section>
@@ -102,6 +104,8 @@ const AccountPane = React.memo(
value={language || 'auto'} value={language || 'auto'}
onChange={handleLanguageChange} onChange={handleLanguageChange}
/> />
{!isLocked && (
<>
<Divider horizontal section> <Divider horizontal section>
<Header as="h4"> <Header as="h4">
{t('common.authentication', { {t('common.authentication', {
@@ -159,6 +163,8 @@ const AccountPane = React.memo(
</Button> </Button>
</UserPasswordEditPopup> </UserPasswordEditPopup>
</div> </div>
</>
)}
</Tab.Pane> </Tab.Pane>
); );
}, },
@@ -172,6 +178,7 @@ AccountPane.propTypes = {
phone: PropTypes.string, phone: PropTypes.string,
organization: PropTypes.string, organization: PropTypes.string,
language: PropTypes.string, language: PropTypes.string,
isLocked: PropTypes.bool.isRequired,
isAvatarUpdating: PropTypes.bool.isRequired, isAvatarUpdating: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
usernameUpdateForm: PropTypes.object.isRequired, usernameUpdateForm: PropTypes.object.isRequired,
@@ -16,6 +16,7 @@ const UserSettingsModal = React.memo(
phone, phone,
organization, organization,
language, language,
isLocked,
subscribeToOwnCards, subscribeToOwnCards,
isAvatarUpdating, isAvatarUpdating,
usernameUpdateForm, usernameUpdateForm,
@@ -48,6 +49,7 @@ const UserSettingsModal = React.memo(
phone={phone} phone={phone}
organization={organization} organization={organization}
language={language} language={language}
isLocked={isLocked}
isAvatarUpdating={isAvatarUpdating} isAvatarUpdating={isAvatarUpdating}
usernameUpdateForm={usernameUpdateForm} usernameUpdateForm={usernameUpdateForm}
emailUpdateForm={emailUpdateForm} emailUpdateForm={emailUpdateForm}
@@ -104,6 +106,7 @@ UserSettingsModal.propTypes = {
phone: PropTypes.string, phone: PropTypes.string,
organization: PropTypes.string, organization: PropTypes.string,
language: PropTypes.string, language: PropTypes.string,
isLocked: PropTypes.bool.isRequired,
subscribeToOwnCards: PropTypes.bool.isRequired, subscribeToOwnCards: PropTypes.bool.isRequired,
isAvatarUpdating: PropTypes.bool.isRequired, isAvatarUpdating: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
@@ -64,6 +64,7 @@ const ActionsStep = React.memo(
return ( return (
<UserInformationEditStep <UserInformationEditStep
defaultData={pick(user, ['name', 'phone', 'organization'])} defaultData={pick(user, ['name', 'phone', 'organization'])}
isNameEditable={!user.isLocked}
onUpdate={onUpdate} onUpdate={onUpdate}
onBack={handleBack} onBack={handleBack}
onClose={onClose} onClose={onClose}
@@ -135,6 +136,8 @@ const ActionsStep = React.memo(
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
{!user.isLocked && (
<>
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}> <Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
{t('action.editUsername', { {t('action.editUsername', {
context: 'title', context: 'title',
@@ -155,6 +158,8 @@ const ActionsStep = React.memo(
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
</>
)}
</Menu> </Menu>
</Popup.Content> </Popup.Content>
</> </>
@@ -17,6 +17,7 @@ const Item = React.memo(
organization, organization,
phone, phone,
isAdmin, isAdmin,
isLocked,
emailUpdateForm, emailUpdateForm,
passwordUpdateForm, passwordUpdateForm,
usernameUpdateForm, usernameUpdateForm,
@@ -46,7 +47,7 @@ const Item = React.memo(
<Table.Cell>{username || '-'}</Table.Cell> <Table.Cell>{username || '-'}</Table.Cell>
<Table.Cell>{email}</Table.Cell> <Table.Cell>{email}</Table.Cell>
<Table.Cell> <Table.Cell>
<Radio toggle checked={isAdmin} onChange={handleIsAdminChange} /> <Radio toggle checked={isAdmin} disabled={isLocked} onChange={handleIsAdminChange} />
</Table.Cell> </Table.Cell>
<Table.Cell textAlign="right"> <Table.Cell textAlign="right">
<ActionsPopup <ActionsPopup
@@ -57,6 +58,7 @@ const Item = React.memo(
organization, organization,
phone, phone,
isAdmin, isAdmin,
isLocked,
emailUpdateForm, emailUpdateForm,
passwordUpdateForm, passwordUpdateForm,
usernameUpdateForm, usernameUpdateForm,
@@ -88,6 +90,7 @@ Item.propTypes = {
organization: PropTypes.string, organization: PropTypes.string,
phone: PropTypes.string, phone: PropTypes.string,
isAdmin: PropTypes.bool.isRequired, isAdmin: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
emailUpdateForm: PropTypes.object.isRequired, emailUpdateForm: PropTypes.object.isRequired,
passwordUpdateForm: PropTypes.object.isRequired, passwordUpdateForm: PropTypes.object.isRequired,
@@ -110,6 +110,7 @@ const UsersModal = React.memo(
organization={item.organization} organization={item.organization}
phone={item.phone} phone={item.phone}
isAdmin={item.isAdmin} isAdmin={item.isAdmin}
isLocked={item.isLocked}
emailUpdateForm={item.emailUpdateForm} emailUpdateForm={item.emailUpdateForm}
passwordUpdateForm={item.passwordUpdateForm} passwordUpdateForm={item.passwordUpdateForm}
usernameUpdateForm={item.usernameUpdateForm} usernameUpdateForm={item.usernameUpdateForm}
+2
View File
@@ -2,6 +2,7 @@ import Config from './Config';
const ROOT = `${Config.BASE_PATH}/`; const ROOT = `${Config.BASE_PATH}/`;
const LOGIN = `${Config.BASE_PATH}/login`; const LOGIN = `${Config.BASE_PATH}/login`;
const OIDC_LOGIN = `${Config.BASE_PATH}/oidclogin`;
const PROJECTS = `${Config.BASE_PATH}/projects/:id`; const PROJECTS = `${Config.BASE_PATH}/projects/:id`;
const BOARDS = `${Config.BASE_PATH}/boards/:id`; const BOARDS = `${Config.BASE_PATH}/boards/:id`;
const CARDS = `${Config.BASE_PATH}/cards/:id`; const CARDS = `${Config.BASE_PATH}/cards/:id`;
@@ -12,4 +13,5 @@ export default {
PROJECTS, PROJECTS,
BOARDS, BOARDS,
CARDS, CARDS,
OIDC_LOGIN,
}; };
@@ -0,0 +1,26 @@
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import entryActions from '../entry-actions';
import OidcLogin from '../components/OIDC';
const mapStateToProps = ({
ui: {
authenticateForm: { data: defaultData, isSubmitting, error },
},
}) => ({
defaultData,
isSubmitting,
error,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators(
{
onAuthenticate: entryActions.authenticate,
onMessageDismiss: entryActions.clearAuthenticateError,
},
dispatch,
);
export default connect(mapStateToProps, mapDispatchToProps)(OidcLogin);
@@ -14,6 +14,7 @@ const mapStateToProps = (state) => {
phone, phone,
organization, organization,
language, language,
isLocked,
subscribeToOwnCards, subscribeToOwnCards,
isAvatarUpdating, isAvatarUpdating,
emailUpdateForm, emailUpdateForm,
@@ -29,6 +30,7 @@ const mapStateToProps = (state) => {
phone, phone,
organization, organization,
language, language,
isLocked,
subscribeToOwnCards, subscribeToOwnCards,
isAvatarUpdating, isAvatarUpdating,
emailUpdateForm, emailUpdateForm,
+6 -2
View File
@@ -1,11 +1,15 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import Config from './constants/Config';
import store from './store'; import store from './store';
import history from './history'; import history from './history';
import Root from './components/Root'; import Root from './components/Root';
import './i18n'; import './i18n';
fetch(`${Config.SERVER_BASE_URL}/api/appconfig`).then((response) => {
response.json().then((config) => {
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(Root, { store, history })); root.render(React.createElement(Root, { store, history, config }));
});
});
+233
View File
@@ -0,0 +1,233 @@
export default {
format: {
date: 'M/d/yyyy',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMM d',
longDateTime: "MMMM d 'at' p",
},
translation: {
common: {
aboutPlanka: 'حول بلانكا',
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: 'اللون',
createBoard_title: 'إنشاء لوحة',
createLabel_title: 'إنشاء علامة',
createNewOneOrSelectExistingOne: 'إنشاء واحدة جديدة أو اختيار<br />واحدة موجودة بالفعل',
createProject_title: 'إنشاء مشروع',
createTextFile_title: 'إنشاء ملف نصي',
currentPassword: 'كلمة المرور الحالية',
dangerZone_title: 'المنطقة الخطرة',
date: 'التاريخ',
dueDate_title: 'تاريخ الاستحقاق',
deleteAttachment_title: 'حذف المرفق',
deleteBoard_title: 'حذف اللوحة',
deleteCard_title: 'حذف البطاقة',
deleteComment_title: 'حذف التعليق',
deleteLabel_title: 'حذف العلامة',
deleteList_title: 'حذف القائمة',
deleteProject_title: 'حذف المشروع',
deleteTask_title: 'حذف المهمة',
deleteUser_title: 'حذف المستخدم',
description: 'الوصف',
detectAutomatically: 'الكشف تلقائياً',
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: 'من تريلو',
general: 'عام',
hours: 'ساعات',
importBoard_title: 'استيراد اللوحة',
invalidCurrentPassword: 'كلمة المرور الحالية غير صالحة',
labels: 'العلامات',
language: 'اللغة',
leaveBoard_title: 'مغادرة اللوحة',
leaveProject_title: 'مغادرة المشروع',
list: 'القائمة',
listActions_title: 'إجراءات القائمة',
managers: 'المديرين',
members: 'الأعضاء',
minutes: 'دقائق',
moveCard_title: 'نقل البطاقة',
name: 'الاسم',
newEmail: 'البريد الإلكتروني الجديد',
newPassword: 'كلمة المرور الجديدة',
newUsername: 'اسم المستخدم الجديد',
noConnectionToServer: 'لا يوجد اتصال بالخادم',
noBoards: 'لا توجد لوحات',
noLists: 'لا توجد قوائم',
noProjects: 'لا توجد مشاريع',
notifications: 'الإشعارات',
noUnreadNotifications: 'لا توجد إشعارات غير مقروءة',
openBoard_title: 'فتح اللوحة',
optional_inline: 'اختياري',
organization: 'المنظمة',
phone: 'الهاتف',
preferences: 'التفضيلات',
pressPasteShortcutToAddAttachmentFromClipboard:
'نصيحة: اضغط على Ctrl-V (Cmd-V على ماك) لإضافة مرفق من الحافظة',
project: 'المشروع',
projectNotFound_title: 'المشروع غير موجود',
removeManager_title: 'إزالة المدير',
removeMember_title: 'إزالة العضو',
searchLabels: 'البحث في العلامات...',
searchMembers: 'البحث في الأعضاء...',
searchUsers: 'البحث في المستخدمين...',
seconds: 'ثواني',
selectBoard: 'اختيار اللوحة',
selectList: 'اختيار القائمة',
selectPermissions_title: 'اختيار الصلاحيات',
selectProject: 'اختيار المشروع',
settings: 'الإعدادات',
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: 'حذف المستخدم',
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: 'عرض مرفقات أقل',
start: 'بدء',
stop: 'إيقاف',
subscribe: 'الاشتراك',
unsubscribe: 'إلغاء الاشتراك',
uploadNewAvatar: 'تحميل صورة رمزية جديدة',
uploadNewImage: 'تحميل صورة جديدة',
},
},
};
+11
View File
@@ -0,0 +1,11 @@
import merge from 'lodash/merge';
import login from './login';
import core from './core';
export default {
language: 'ar',
country: 'ar',
name: 'Arabic',
embeddedLocale: merge(login, core),
};
+20
View File
@@ -0,0 +1,20 @@
export default {
translation: {
common: {
emailOrUsername: 'البريد الإلكتروني أو اسم المستخدم',
invalidEmailOrUsername: 'بريد إلكتروني أو اسم مستخدم غير صالح',
invalidPassword: 'كلمة مرور غير صالحة',
logInToPlanka: 'تسجيل الدخول إلى بلانكا',
noInternetConnection: 'لا يوجد اتصال بالإنترنت',
pageNotFound_title: 'الصفحة غير موجودة',
password: 'كلمة المرور',
projectManagement: 'إدارة المشروع',
serverConnectionFailed: 'فشل الاتصال بالخادم',
unknownError: 'خطأ غير معروف، حاول مرة أخرى لاحقًا',
},
action: {
logIn: 'تسجيل الدخول',
},
},
};
+235
View File
@@ -0,0 +1,235 @@
export default {
format: {
date: 'M/d/yyyy',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMM d',
longDateTime: "MMMM d 'তে' p",
},
translation: {
common: {
aboutPlanka: 'প্লাঙ্কা সম্পর্কে',
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: 'রং',
createBoard_title: 'বোর্ড তৈরি করুন',
createLabel_title: 'লেবেল তৈরি করুন',
createNewOneOrSelectExistingOne: 'নতুন তৈরি করুন অথবা<br />বিদ্যমান একটি নির্বাচন করুন',
createProject_title: 'প্রকল্প তৈরি করুন',
createTextFile_title: 'টেক্সট ফাইল তৈরি করুন',
currentPassword: 'বর্তমান পাসওয়ার্ড',
dangerZone_title: 'আপত্তি জোন',
date: 'তারিখ',
dueDate_title: 'নির্ধারণিত তারিখ',
deleteAttachment_title: 'সংযুক্তি মুছে ফেলুন',
deleteBoard_title: 'বোর্ড মুছে ফেলুন',
deleteCard_title: 'কার্ড মুছে ফেলুন',
deleteComment_title: 'মন্তব্য মুছে ফেলুন',
deleteLabel_title: 'লেবেল মুছে ফেলুন',
deleteList_title: 'তালিকা মুছে ফেলুন',
deleteProject_title: 'প্রকল্প মুছে ফেলুন',
deleteTask_title: 'টাস্ক মুছে ফেলুন',
deleteUser_title: 'ব্যবহারকারী মুছে ফেলুন',
description: 'বর্ণনা',
detectAutomatically: 'স্বয়ংক্রিয়ভাবে চেষ্টা করুন',
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: 'ট্রেলো থেকে',
general: 'সাধারণ',
hours: 'ঘণ্টা',
importBoard_title: 'বোর্ড আমদানি করুন',
invalidCurrentPassword: 'বর্তমান পাসওয়ার্ড অবৈধ',
labels: 'লেবেল',
language: 'ভাষা',
leaveBoard_title: 'বোর্ড ত্যাগ করুন',
leaveProject_title: 'প্রকল্প ত্যাগ করুন',
list: 'তালিকা',
listActions_title: 'তালিকা ক্রিয়াকলাপ',
managers: 'ম্যানেজারসমূহ',
members: 'সদস্যগণ',
minutes: 'মিনিট',
moveCard_title: 'কার্ড সরান',
name: 'নাম',
newEmail: 'নতুন ইমেইল',
newPassword: 'নতুন পাসওয়ার্ড',
newUsername: 'নতুন ব্যবহারকারীর নাম',
noConnectionToServer: 'সার্ভারে সংযোগ নেই',
noBoards: 'কোন বোর্ড নেই',
noLists: 'কোন তালিকা নেই',
noProjects: 'কোন প্রকল্প নেই',
notifications: 'বিজ্ঞপ্তি',
noUnreadNotifications: 'কোন অপঠিত বিজ্ঞপ্তি নেই',
openBoard_title: 'বোর্ড খুলুন',
optional_inline: 'ঐচ্ছিক',
organization: 'সংগঠন',
phone: 'ফোন',
preferences: 'পছন্দ',
pressPasteShortcutToAddAttachmentFromClipboard:
'প্রয়োজনীয়: Ctrl-V (Mac উপর Cmd-V) চাপুন যেটাতে ক্লিপবোর্ড থেকে সংযুক্তি যোগ করতে পারবেন',
project: 'প্রকল্প',
projectNotFound_title: 'প্রকল্প পাওয়া যায়নি',
removeManager_title: 'ম্যানেজার সরান',
removeMember_title: 'সদস্য সরান',
searchLabels: 'লেবেল খুঁজুন...',
searchMembers: 'সদস্য খুঁজুন...',
searchUsers: 'ব্যবহারকারী খুঁজুন...',
seconds: 'সেকেন্ড',
selectBoard: 'বোর্ড নির্বাচন করুন',
selectList: 'তালিকা নির্বাচন করুন',
selectPermissions_title: 'অনুমতি নির্বাচন করুন',
selectProject: 'প্রকল্প নির্বাচন করুন',
settings: 'সেটিংস',
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: 'ব্যবহারকারী মুছে ফেলুন',
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: 'কম সংযুক্তি দেখান',
start: 'শুরু',
stop: 'বন্ধ',
subscribe: 'সাবস্ক্রাইব করুন',
unsubscribe: 'সাবস্ক্রাইব বাতিল করুন',
uploadNewAvatar: 'নতুন অবতার আপলোড করুন',
uploadNewImage: 'নতুন চিত্র আপলোড করুন',
},
},
};
+11
View File
@@ -0,0 +1,11 @@
import merge from 'lodash/merge';
import login from './login';
import core from './core';
export default {
language: 'bn',
country: 'bn',
name: 'Bengali',
embeddedLocale: merge(login, core),
};
+20
View File
@@ -0,0 +1,20 @@
export default {
translation: {
common: {
emailOrUsername: 'ইমেইল বা ব্যবহারকারীর নাম',
invalidEmailOrUsername: 'অবৈধ ইমেইল বা ব্যবহারকারীর নাম',
invalidPassword: 'অবৈধ পাসওয়ার্ড',
logInToPlanka: 'Planka-তে লগ ইন করুন',
noInternetConnection: 'ইন্টারনেট সংযোগ নেই',
pageNotFound_title: 'পেজ পাওয়া যায়নি',
password: 'পাসওয়ার্ড',
projectManagement: 'প্রকল্প পরিচালনা',
serverConnectionFailed: 'সার্ভার সংযোগ ব্যর্থ হয়েছে',
unknownError: 'অজানা ত্রুটি, পরে আবার চেষ্টা করুন',
},
action: {
logIn: 'লগ ইন করুন',
},
},
};
+24
View File
@@ -13,6 +13,7 @@ export default {
translation: { translation: {
common: { common: {
aboutPlanka: 'O aplikaci Planka',
account: 'Účet', account: 'Účet',
actions: 'Akce', actions: 'Akce',
addAttachment_title: 'Přidat přílohu', addAttachment_title: 'Přidat přílohu',
@@ -45,6 +46,9 @@ export default {
background: 'Pozadí', background: 'Pozadí',
board: 'Tabule', board: 'Tabule',
boardNotFound_title: 'Tabule nenalezena', boardNotFound_title: 'Tabule nenalezena',
canComment: 'Může komentovat',
canEditContentOfBoard: 'Může upravovat obsah tabule',
canOnlyViewBoard: 'Může zobrazit pouze tabuli',
cardActions_title: 'Akce na kartě', cardActions_title: 'Akce na kartě',
cardNotFound_title: 'Karta nenalezena', cardNotFound_title: 'Karta nenalezena',
cardOrActionAreDeleted: 'Karta nebo akce je smazána', cardOrActionAreDeleted: 'Karta nebo akce je smazána',
@@ -68,14 +72,18 @@ export default {
deleteTask_title: 'Smazat úkol', deleteTask_title: 'Smazat úkol',
deleteUser_title: 'Smazat uživatele', deleteUser_title: 'Smazat uživatele',
description: 'Popis', description: 'Popis',
detectAutomatically: 'Automatická detekce',
dropFileToUpload: 'Přetáhnutím nahraj soubor', dropFileToUpload: 'Přetáhnutím nahraj soubor',
editor: 'Editor',
editAttachment_title: 'Upravit přílohu', editAttachment_title: 'Upravit přílohu',
editAvatar_title: 'Upravit avatar', editAvatar_title: 'Upravit avatar',
editBoard_title: 'Upravit tabuli', editBoard_title: 'Upravit tabuli',
editDueDate_title: 'Upravit Termín do', editDueDate_title: 'Upravit Termín do',
editEmail_title: 'Upravit e-mail', editEmail_title: 'Upravit e-mail',
editInformation_title: 'Upravit informace',
editLabel_title: 'Upravit štítek', editLabel_title: 'Upravit štítek',
editPassword_title: 'Upravit heslo', editPassword_title: 'Upravit heslo',
editPermissions_title: 'Upravit oprávnění',
editStopwatch_title: 'Upravit časovač', editStopwatch_title: 'Upravit časovač',
editUsername_title: 'Upravit uživatelské jméno', editUsername_title: 'Upravit uživatelské jméno',
email: 'E-mail', email: 'E-mail',
@@ -89,10 +97,13 @@ export default {
filterByLabels_title: 'Filtruj podle štítku', filterByLabels_title: 'Filtruj podle štítku',
filterByMembers_title: 'Filtruj podle člena', filterByMembers_title: 'Filtruj podle člena',
fromComputer_title: 'Z počítače', fromComputer_title: 'Z počítače',
fromTrello: 'Z Trella',
general: 'Obecné', general: 'Obecné',
hours: 'Hodiny', hours: 'Hodiny',
importBoard_title: 'Importovat tabuli',
invalidCurrentPassword: 'Neplatné aktuální heslo', invalidCurrentPassword: 'Neplatné aktuální heslo',
labels: 'Štítky', labels: 'Štítky',
language: 'Jazyk',
leaveBoard_title: 'Opustit tabuli', leaveBoard_title: 'Opustit tabuli',
leaveProject_title: 'Opustit projekt', leaveProject_title: 'Opustit projekt',
list: 'Seznam', list: 'Seznam',
@@ -122,15 +133,20 @@ export default {
projectNotFound_title: 'Projekt nenalezen', projectNotFound_title: 'Projekt nenalezen',
removeManager_title: 'Odstranit vedoucího', removeManager_title: 'Odstranit vedoucího',
removeMember_title: 'Odstranit člena', removeMember_title: 'Odstranit člena',
searchLabels: 'Hledat štítky...',
searchMembers: 'Hledat členy...',
searchUsers: 'Hledat uživatele...',
seconds: 'Vteřin', seconds: 'Vteřin',
selectBoard: 'Vybrat tabuli', selectBoard: 'Vybrat tabuli',
selectList: 'Vybrat seznam', selectList: 'Vybrat seznam',
selectPermissions_title: 'Vybrat oprávnění',
selectProject: 'Vybrat projekt', selectProject: 'Vybrat projekt',
settings: 'Nastavení', settings: 'Nastavení',
stopwatch: 'Časovač', stopwatch: 'Časovač',
subscribeToMyOwnCardsByDefault: 'Ve výchozím nastavení odebírat vlastní karty', subscribeToMyOwnCardsByDefault: 'Ve výchozím nastavení odebírat vlastní karty',
taskActions_title: 'Akce na úkolu', taskActions_title: 'Akce na úkolu',
tasks: 'Úkoly', tasks: 'Úkoly',
thereIsNoPreviewAvailableForThisAttachment: 'Pro tuto přílohu není k dispozici žádný náhled',
time: 'Čas', time: 'Čas',
title: 'Titulek', title: 'Titulek',
userActions_title: 'Akce na uživateli', userActions_title: 'Akce na uživateli',
@@ -142,6 +158,8 @@ export default {
username: 'Uživatelské jméno', username: 'Uživatelské jméno',
usernameAlreadyInUse: 'Uživatelské jméno se již používá', usernameAlreadyInUse: 'Uživatelské jméno se již používá',
users: 'Uživatelé', users: 'Uživatelé',
version: 'Verze',
viewer: 'Prohlížeč',
writeComment: 'Napsat komentář...', writeComment: 'Napsat komentář...',
}, },
@@ -153,6 +171,7 @@ export default {
addCard_title: 'Přidat kartu', addCard_title: 'Přidat kartu',
addComment: 'Přidat komentář', addComment: 'Přidat komentář',
addList: 'Přidat seznam', addList: 'Přidat seznam',
addMember: 'Přidat člena',
addMoreDetailedDescription: 'Přidat další detailní popis', addMoreDetailedDescription: 'Přidat další detailní popis',
addTask: 'Přidat úkol', addTask: 'Přidat úkol',
addToCard: 'Přidat na kartu', addToCard: 'Přidat na kartu',
@@ -182,10 +201,14 @@ export default {
editDueDate_title: 'Upravit termín do', editDueDate_title: 'Upravit termín do',
editDescription_title: 'Upravit popis', editDescription_title: 'Upravit popis',
editEmail_title: 'Upravit e-mail', editEmail_title: 'Upravit e-mail',
editInformation_title: 'Upravit informace',
editPassword_title: 'Upravit heslo', editPassword_title: 'Upravit heslo',
editPermissions: 'Upravit oprávnění',
editStopwatch_title: 'Upravit časovač', editStopwatch_title: 'Upravit časovač',
editTitle_title: 'Upravit titulek', editTitle_title: 'Upravit titulek',
editUsername_title: 'Upravit uživatelské jméno', editUsername_title: 'Upravit uživatelské jméno',
hideDetails: 'Skrýt detaily',
import: 'Import',
leaveBoard: 'Opustit tabuli', leaveBoard: 'Opustit tabuli',
leaveProject: 'Opustit projekt', leaveProject: 'Opustit projekt',
logOut_title: 'Odhlásit se', logOut_title: 'Odhlásit se',
@@ -202,6 +225,7 @@ export default {
save: 'Uložit', save: 'Uložit',
showAllAttachments: 'Zozbrazit všechny přílohy ({{hidden}} skryté)', showAllAttachments: 'Zozbrazit všechny přílohy ({{hidden}} skryté)',
showFewerAttachments: 'Zobrazit méně příloh', showFewerAttachments: 'Zobrazit méně příloh',
showDetails: 'Zobrazit detaily',
start: 'Start', start: 'Start',
stop: 'Stop', stop: 'Stop',
subscribe: 'Odebírat', subscribe: 'Odebírat',
+236
View File
@@ -0,0 +1,236 @@
export default {
format: {
date: 'd/M/yyyy',
time: 'A',
dateTime: '$t(format:date), $t(format:time)',
longDate: 'd MMM',
longDateTime: "d MMMM 'को' $t(format:time)",
},
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: 'रंग',
createBoard_title: 'बोर्ड बनाएं',
createLabel_title: 'लेबल बनाएं',
createNewOneOrSelectExistingOne: 'नया बनाएं या<br />मौजूदा चुनें',
createProject_title: 'परियोजना बनाएं',
createTextFile_title: 'टेक्स्ट फ़ाइल बनाएं',
currentPassword: 'वर्तमान पासवर्ड',
dangerZone_title: 'खतरे का क्षेत्र',
date: 'तारीख',
dueDate_title: 'नियत तारीख',
deleteAttachment_title: 'अटैचमेंट हटाएं',
deleteBoard_title: 'बोर्ड हटाएं',
deleteCard_title: 'कार्ड हटाएं',
deleteComment_title: 'टिप्पणी हटाएं',
deleteLabel_title: 'लेबल हटाएं',
deleteList_title: 'सूची हटाएं',
deleteProject_title: 'परियोजना हटाएं',
deleteTask_title: 'कार्य हटाएं',
deleteUser_title: 'उपयोगकर्ता हटाएं',
description: 'विवरण',
detectAutomatically: 'स्वचालित खोजें',
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: 'मैनेजर',
members: 'सदस्य',
minutes: 'मिनट',
moveCard_title: 'कार्ड को ले जाएं',
name: 'नाम',
newEmail: 'नई ईमेल',
newPassword: 'नया पासवर्ड',
newUsername: 'नया उपयोगकर्ता नाम',
noConnectionToServer: 'सर्वर से कोई जुड़ाव नहीं है',
noBoards: 'कोई बोर्ड नहीं',
noLists: 'कोई सूची नहीं',
noProjects: 'कोई परियोजनाएँ नहीं',
notifications: 'सूचनाएँ',
noUnreadNotifications: 'कोई अपठित सूचनाएँ नहीं हैं',
openBoard_title: 'बोर्ड खोलें',
optional_inline: 'वैकल्पिक',
organization: 'संगठन',
phone: 'फ़ोन',
preferences: 'प्राथमिकताएँ',
pressPasteShortcutToAddAttachmentFromClipboard:
'सुझाव: क्लिपबोर्ड से अटैचमेंट जोड़ने के लिए Ctrl-V (Mac पर Cmd-V) दबाएं',
project: 'परियोजना',
projectNotFound_title: 'परियोजना नहीं मिली',
removeManager_title: 'मैनेजर हटाएं',
removeMember_title: 'सदस्य हटाएं',
searchLabels: 'लेबल खोजें...',
searchMembers: 'सदस्य खोजें...',
searchUsers: 'उपयोगकर्ताओं को खोजें...',
seconds: 'सेकंड्स',
selectBoard: 'बोर्ड चुनें',
selectList: 'सूची चुनें',
selectPermissions_title: 'अनुमतियाँ चुनें',
selectProject: 'परियोजना चुनें',
settings: 'सेटिंग्स',
stopwatch: 'स्टॉपवॉच',
subscribeToMyOwnCardsByDefault: 'डिफ़ॉल्ट रूप से मेरे अपने कार्डों की सदस्यता लें',
taskActions_title: 'कार्य क्रियाएँ',
tasks: 'कार्य',
thereIsNoPreviewAvailableForThisAttachment:
'इस अटैचमेंट के लिए कोई पूर्वावलोकन उपलब्ध नहीं है',
time: 'समय',
title: 'शीर्षक',
userActions_title: 'उपयोगकर्ता क्रियाएँ',
userAddedThisCardToList: '<0>{{user}}</0><1> ने इस कार्ड को {{list}} में जोड़ा</1>',
userLeftNewCommentToCard:
'{{user}} ने कार्ड <2>{{card}}</2> पर नई टिप्पणी छोड़ी «{{comment}}»',
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: 'उपयोगकर्ता हटाएं',
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: 'कम अटैचमेंट दिखाएं',
start: 'शुरू करें',
stop: 'रोकें',
subscribe: 'सदस्यता लें',
unsubscribe: 'सदस्यता रद्द करें',
uploadNewAvatar: 'नया अवतार अपलोड करें',
uploadNewImage: 'नई छवि अपलोड करें',
},
},
};
+11
View File
@@ -0,0 +1,11 @@
import merge from 'lodash/merge';
import login from './login';
import core from './core';
export default {
language: 'hi',
country: 'hi',
name: 'Hindi',
embeddedLocale: merge(login, core),
};
+20
View File
@@ -0,0 +1,20 @@
export default {
translation: {
common: {
emailOrUsername: 'ईमेल या उपयोगकर्ता नाम',
invalidEmailOrUsername: 'अवैध ईमेल या उपयोगकर्ता नाम',
invalidPassword: 'अवैध पासवर्ड',
logInToPlanka: 'Planka में लॉग इन करें',
noInternetConnection: 'कोई इंटरनेट कनेक्शन नहीं',
pageNotFound_title: 'पृष्ठ नहीं मिला',
password: 'पासवर्ड',
projectManagement: 'परियोजना प्रबंधन',
serverConnectionFailed: 'सर्वर कनेक्शन विफल हो गया',
unknownError: 'अज्ञात त्रुटि, बाद में पुनः प्रयास करें',
},
action: {
logIn: 'लॉग इन करें',
},
},
};
+6 -1
View File
@@ -1,20 +1,25 @@
import ar from './ar';
import bn from './bn';
import cs from './cs'; import cs from './cs';
import da from './da'; import da from './da';
import de from './de'; import de from './de';
import en from './en'; import en from './en';
import es from './es'; import es from './es';
import fr from './fr'; import fr from './fr';
import hi from './hi';
import it from './it'; import it from './it';
import ja from './ja'; import ja from './ja';
import ko from './ko'; import ko from './ko';
import nl from './nl';
import pl from './pl'; import pl from './pl';
import ru from './ru'; import ru from './ru';
import sk from './sk'; import sk from './sk';
import sv from './sv'; import sv from './sv';
import tr from './tr';
import uz from './uz'; import uz from './uz';
import zh from './zh'; import zh from './zh';
const locales = [cs, da, de, en, es, fr, it, ja, ko, pl, ru, sk, sv, uz, zh]; const locales = [ar, bn, cs, da, de, en, es, fr, hi, it, ja, ko, nl, pl, ru, sk, sv, tr, uz, zh];
export default locales; export default locales;
+236
View File
@@ -0,0 +1,236 @@
export default {
format: {
date: 'd-M-yyyy',
time: 'HH:mm',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd MMM',
longDateTime: "d MMMM 'om' HH:mm",
},
translation: {
common: {
aboutPlanka: 'Over Planka',
account: 'Account',
actions: 'Acties',
addAttachment_title: 'Bijlage Toevoegen',
addComment: 'Opmerking Toevoegen',
addManager_title: 'Manager Toevoegen',
addMember_title: 'Lid Toevoegen',
addUser_title: 'Gebruiker Toevoegen',
administrator: 'Beheerder',
all: 'Alles',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Alle wijzigingen worden automatisch opgeslagen<br />nadat de verbinding is hersteld',
areYouSureYouWantToDeleteThisAttachment: 'Weet u zeker dat u deze bijlage wilt verwijderen?',
areYouSureYouWantToDeleteThisBoard: 'Weet u zeker dat u dit bord wilt verwijderen?',
areYouSureYouWantToDeleteThisCard: 'Weet u zeker dat u deze kaart wilt verwijderen?',
areYouSureYouWantToDeleteThisComment: 'Weet u zeker dat u deze opmerking wilt verwijderen?',
areYouSureYouWantToDeleteThisLabel: 'Weet u zeker dat u deze label wilt verwijderen?',
areYouSureYouWantToDeleteThisList: 'Weet u zeker dat u deze lijst wilt verwijderen?',
areYouSureYouWantToDeleteThisProject: 'Weet u zeker dat u dit project wilt verwijderen?',
areYouSureYouWantToDeleteThisTask: 'Weet u zeker dat u deze taak wilt verwijderen?',
areYouSureYouWantToDeleteThisUser: 'Weet u zeker dat u deze gebruiker wilt verwijderen?',
areYouSureYouWantToLeaveBoard: 'Weet u zeker dat u het bord wilt verlaten?',
areYouSureYouWantToLeaveProject: 'Weet u zeker dat u het project wilt verlaten?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Weet u zeker dat u deze manager uit het project wilt verwijderen?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Weet u zeker dat u dit lid uit het bord wilt verwijderen?',
attachment: 'Bijlage',
attachments: 'Bijlagen',
authentication: 'Authenticatie',
background: 'Achtergrond',
board: 'Bord',
boardNotFound_title: 'Bord Niet Gevonden',
canComment: 'Kan opmerking plaatsen',
canEditContentOfBoard: 'Kan de inhoud van het bord bewerken',
canOnlyViewBoard: 'Kan alleen het bord bekijken',
cardActions_title: 'Kaartacties',
cardNotFound_title: 'Kaart Niet Gevonden',
cardOrActionAreDeleted: 'Kaart of actie zijn verwijderd',
color: 'Kleur',
createBoard_title: 'Bord Aanmaken',
createLabel_title: 'Label Aanmaken',
createNewOneOrSelectExistingOne: 'Maak een nieuwe aan of selecteer<br />een bestaande',
createProject_title: 'Project Aanmaken',
createTextFile_title: 'Tekstbestand Aanmaken',
currentPassword: 'Huidig wachtwoord',
dangerZone_title: 'Gevaarlijke Zone',
date: 'Datum',
dueDate_title: 'Vervaldatum',
deleteAttachment_title: 'Bijlage Verwijderen',
deleteBoard_title: 'Bord Verwijderen',
deleteCard_title: 'Kaart Verwijderen',
deleteComment_title: 'Opmerking Verwijderen',
deleteLabel_title: 'Label Verwijderen',
deleteList_title: 'Lijst Verwijderen',
deleteProject_title: 'Project Verwijderen',
deleteTask_title: 'Taak Verwijderen',
deleteUser_title: 'Gebruiker Verwijderen',
description: 'Beschrijving',
detectAutomatically: 'Automatisch detecteren',
dropFileToUpload: 'Sleep het bestand om te uploaden',
editor: 'Editor',
editAttachment_title: 'Bijlage Bewerken',
editAvatar_title: 'Avatar Bewerken',
editBoard_title: 'Bord Bewerken',
editDueDate_title: 'Vervaldatum Bewerken',
editEmail_title: 'E-mail Bewerken',
editInformation_title: 'Informatie Bewerken',
editLabel_title: 'Label Bewerken',
editPassword_title: 'Wachtwoord Bewerken',
editPermissions_title: 'Rechten Bewerken',
editStopwatch_title: 'Stopwatch Bewerken',
editUsername_title: 'Gebruikersnaam Bewerken',
email: 'E-mail',
emailAlreadyInUse: 'E-mail is al in gebruik',
enterCardTitle: 'Voer de kaarttitel in... [Ctrl+Enter] om automatisch te openen',
enterDescription: 'Voer beschrijving in...',
enterFilename: 'Voer bestandsnaam in',
enterListTitle: 'Voer de lijsttitel in...',
enterProjectTitle: 'Voer de projecttitel in',
enterTaskDescription: 'Voer taakbeschrijving in...',
filterByLabels_title: 'Filteren op Labels',
filterByMembers_title: 'Filteren op Leden',
fromComputer_title: 'Van Computer',
fromTrello: 'Van Trello',
general: 'Algemeen',
hours: 'Uren',
importBoard_title: 'Bord Importeren',
invalidCurrentPassword: 'Ongeldig huidig wachtwoord',
labels: 'Labels',
language: 'Taal',
leaveBoard_title: 'Bord Verlaten',
leaveProject_title: 'Project Verlaten',
list: 'Lijst',
listActions_title: 'Lijstacties',
managers: 'Managers',
members: 'Leden',
minutes: 'Minuten',
moveCard_title: 'Kaart Verplaatsen',
name: 'Naam',
newEmail: 'Nieuwe e-mail',
newPassword: 'Nieuw wachtwoord',
newUsername: 'Nieuwe gebruikersnaam',
noConnectionToServer: 'Geen verbinding met de server',
noBoards: 'Geen borden',
noLists: 'Geen lijsten',
noProjects: 'Geen projecten',
notifications: 'Meldingen',
noUnreadNotifications: 'Geen ongelezen meldingen',
openBoard_title: 'Bord Openen',
optional_inline: 'optioneel',
organization: 'Organisatie',
phone: 'Telefoon',
preferences: 'Voorkeuren',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: druk op Ctrl-V (Cmd-V op Mac) om een bijlage van het klembord toe te voegen',
project: 'Project',
projectNotFound_title: 'Project Niet Gevonden',
removeManager_title: 'Manager Verwijderen',
removeMember_title: 'Lid Verwijderen',
searchLabels: 'Zoek labels...',
searchMembers: 'Zoek leden...',
searchUsers: 'Zoek gebruikers...',
seconds: 'Seconden',
selectBoard: 'Selecteer bord',
selectList: 'Selecteer lijst',
selectPermissions_title: 'Selecteer Rechten',
selectProject: 'Selecteer project',
settings: 'Instellingen',
stopwatch: 'Stopwatch',
subscribeToMyOwnCardsByDefault: 'Standaard abonneren op mijn eigen kaarten',
taskActions_title: 'Taakacties',
tasks: 'Taken',
thereIsNoPreviewAvailableForThisAttachment:
'Er is geen voorbeeld beschikbaar voor deze bijlage',
time: 'Tijd',
title: 'Titel',
userActions_title: 'Gebruikersacties',
userAddedThisCardToList: '<0>{{user}}</0><1> voegde deze kaart toe aan {{list}}</1>',
userLeftNewCommentToCard:
'{{user}} liet een nieuwe opmerking achter «{{comment}}» bij <2>{{card}}</2>',
userMovedCardFromListToList:
'{{user}} verplaatste <2>{{kaart}}</2> van {{fromList}} naar {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> verplaatste deze kaart van {{fromList}} naar {{toList}}</1>',
username: 'Gebruikersnaam',
usernameAlreadyInUse: 'Gebruikersnaam is al in gebruik',
users: 'Gebruikers',
version: 'Versie',
viewer: 'Viewer',
writeComment: 'Schrijf een opmerking...',
},
action: {
addAnotherCard: 'Voeg nog een kaart toe',
addAnotherList: 'Voeg nog een lijst toe',
addAnotherTask: 'Voeg nog een taak toe',
addCard: 'Kaart toevoegen',
addCard_title: 'Kaart Toevoegen',
addComment: 'Opmerking toevoegen',
addList: 'Lijst toevoegen',
addMember: 'Lid toevoegen',
addMoreDetailedDescription: 'Voeg een meer gedetailleerde beschrijving toe',
addTask: 'Taak toevoegen',
addToCard: 'Toevoegen aan kaart',
addUser: 'Gebruiker toevoegen',
createBoard: 'Bord aanmaken',
createFile: 'Bestand aanmaken',
createLabel: 'Label aanmaken',
createNewLabel: 'Nieuw label aanmaken',
createProject: 'Project aanmaken',
delete: 'Verwijderen',
deleteAttachment: 'Bijlage verwijderen',
deleteAvatar: 'Avatar verwijderen',
deleteBoard: 'Bord verwijderen',
deleteCard: 'Kaart verwijderen',
deleteCard_title: 'Kaart Verwijderen',
deleteComment: 'Opmerking verwijderen',
deleteImage: 'Afbeelding verwijderen',
deleteLabel: 'Label verwijderen',
deleteList: 'Lijst verwijderen',
deleteList_title: 'Lijst Verwijderen',
deleteProject: 'Project verwijderen',
deleteProject_title: 'Project Verwijderen',
deleteTask: 'Taak verwijderen',
deleteTask_title: 'Taak Verwijderen',
deleteUser: 'Gebruiker verwijderen',
edit: 'Bewerken',
editDueDate_title: 'Vervaldatum Bewerken',
editDescription_title: 'Beschrijving Bewerken',
editEmail_title: 'E-mail Bewerken',
editInformation_title: 'Informatie Bewerken',
editPassword_title: 'Wachtwoord Bewerken',
editPermissions: 'Rechten Bewerken',
editStopwatch_title: 'Stopwatch Bewerken',
editTitle_title: 'Titel Bewerken',
editUsername_title: 'Gebruikersnaam Bewerken',
hideDetails: 'Details verbergen',
import: 'Importeren',
leaveBoard: 'Bord verlaten',
leaveProject: 'Project verlaten',
logOut_title: 'Uitloggen',
makeCover_title: 'Maak Omslag',
move: 'Verplaatsen',
moveCard_title: 'Kaart Verplaatsen',
remove: 'Verwijderen',
removeBackground: 'Achtergrond verwijderen',
removeCover_title: 'Omslag Verwijderen',
removeFromBoard: 'Verwijderen van bord',
removeFromProject: 'Verwijderen van project',
removeManager: 'Manager verwijderen',
removeMember: 'Lid verwijderen',
save: 'Opslaan',
showAllAttachments: 'Toon alle bijlagen ({{hidden}} verborgen)',
showDetails: 'Details weergeven',
showFewerAttachments: 'Toon minder bijlagen',
start: 'Start',
stop: 'Stop',
subscribe: 'Abonneren',
unsubscribe: 'Afmelden',
uploadNewAvatar: 'Nieuwe avatar uploaden',
uploadNewImage: 'Nieuwe afbeelding uploaden',
},
},
};
+11
View File
@@ -0,0 +1,11 @@
import merge from 'lodash/merge';
import login from './login';
import core from './core';
export default {
language: 'nl',
country: 'nl',
name: 'Dutch',
embeddedLocale: merge(login, core),
};
+20
View File
@@ -0,0 +1,20 @@
export default {
translation: {
common: {
emailOrUsername: 'E-mail of gebruikersnaam',
invalidEmailOrUsername: 'Ongeldige e-mail of gebruikersnaam',
invalidPassword: 'Ongeldig wachtwoord',
logInToPlanka: 'Inloggen op Planka',
noInternetConnection: 'Geen internetverbinding',
pageNotFound_title: 'Pagina Niet Gevonden',
password: 'Wachtwoord',
projectManagement: 'Projectbeheer',
serverConnectionFailed: 'Verbinding met server mislukt',
unknownError: 'Onbekende fout, probeer het later opnieuw',
},
action: {
logIn: 'Inloggen',
},
},
};
+27 -4
View File
@@ -13,10 +13,12 @@ export default {
translation: { translation: {
common: { common: {
aboutPlanka: 'О проекте Planka',
account: 'Учетная запись', account: 'Учетная запись',
actions: 'Действия', actions: 'Действия',
addAttachment: 'Добавление вложения', addAttachment: 'Добавление вложения',
addComment: 'Добавление комментария', addComment: 'Добавление комментария',
addManager_title: 'Добавление менеджера',
addMember: 'Добавление участника', addMember: 'Добавление участника',
addUser: 'Добавление пользователя', addUser: 'Добавление пользователя',
administrator: 'Администратор', administrator: 'Администратор',
@@ -32,11 +34,16 @@ export default {
areYouSureYouWantToDeleteThisProject: 'Вы уверены, что хотите удалить этот проект?', areYouSureYouWantToDeleteThisProject: 'Вы уверены, что хотите удалить этот проект?',
areYouSureYouWantToDeleteThisTask: 'Вы уверены, что хотите удалить эту задачу?', areYouSureYouWantToDeleteThisTask: 'Вы уверены, что хотите удалить эту задачу?',
areYouSureYouWantToDeleteThisUser: 'Вы уверены, что хотите удалить этого пользователя?', areYouSureYouWantToDeleteThisUser: 'Вы уверены, что хотите удалить этого пользователя?',
areYouSureYouWantToRemoveThisMemberFromProject: areYouSureYouWantToLeaveBoard: 'Вы уверены, что хотите покинуть эту доску?',
'Вы уверены, что хотите удалить этого участника из проекта?', areYouSureYouWantToLeaveProject: 'Вы уверены, что хотите покинуть этот проект?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Вы уверены, что хотите удалить этого менеджера из проекта?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Вы уверены, что хотите удалить этого участника из доски?',
attachment: 'Вложение', attachment: 'Вложение',
attachments: 'Вложения', attachments: 'Вложения',
authentication: 'Аутентификация', authentication: 'Аутентификация',
background: 'Фон',
board: 'Доска', board: 'Доска',
boardNotFound: 'Доска не найдена', boardNotFound: 'Доска не найдена',
canComment: 'Может комментировать', canComment: 'Может комментировать',
@@ -53,6 +60,7 @@ export default {
createProject: 'Создание проекта', createProject: 'Создание проекта',
createTextFile: 'Создание текстового файла', createTextFile: 'Создание текстового файла',
currentPassword: 'Текущий пароль', currentPassword: 'Текущий пароль',
dangerZone_title: 'Опасная зона',
date: 'Дата', date: 'Дата',
dueDate: 'Срок', dueDate: 'Срок',
deleteAttachment: 'Удаление вложения', deleteAttachment: 'Удаление вложения',
@@ -74,7 +82,8 @@ export default {
editBackground: 'Изменение фона', editBackground: 'Изменение фона',
editDueDate: 'Изменение срока', editDueDate: 'Изменение срока',
editEmail: 'Изменение e-mail', editEmail: 'Изменение e-mail',
editLabel: 'Изменения метки', editInformation: 'Изменение информации',
editLabel: 'Изменение метки',
editPassword: 'Изменение пароля', editPassword: 'Изменение пароля',
editPermissions: 'Редактирование разрешений', editPermissions: 'Редактирование разрешений',
editStopwatch: 'Изменение секундомера', editStopwatch: 'Изменение секундомера',
@@ -91,12 +100,18 @@ export default {
filterByLabels: 'Фильтр по меткам', filterByLabels: 'Фильтр по меткам',
filterByMembers: 'Фильтр по участникам', filterByMembers: 'Фильтр по участникам',
fromComputer: 'С компьютера', fromComputer: 'С компьютера',
fromTrello: 'Из Trello',
general: 'Основные',
hours: 'Часы', hours: 'Часы',
importBoard: 'Импорт доски',
invalidCurrentPassword: 'Неверный текущий пароль', invalidCurrentPassword: 'Неверный текущий пароль',
labels: 'Метки', labels: 'Метки',
language: 'Язык', language: 'Язык',
leaveBoard: 'Покинуть доску',
leaveProject: 'Покинуть проект',
list: 'Список', list: 'Список',
listActions: 'Действия со списком', listActions: 'Действия со списком',
managers: 'Менеджеры',
members: 'Участники', members: 'Участники',
minutes: 'Минуты', minutes: 'Минуты',
moveCard: 'Перемещение карточки', moveCard: 'Перемещение карточки',
@@ -120,6 +135,7 @@ export default {
project: 'Проект', project: 'Проект',
projectActions: 'Действия с проектом', projectActions: 'Действия с проектом',
projectNotFound: 'Проект не найден', projectNotFound: 'Проект не найден',
removeManager: 'Удалить менеджера',
removeMember: 'Удаление участника', removeMember: 'Удаление участника',
searchLabels: 'Поиск меток...', searchLabels: 'Поиск меток...',
searchMembers: 'Поиск участников...', searchMembers: 'Поиск участников...',
@@ -131,7 +147,7 @@ export default {
selectProject: 'Выберите проект', selectProject: 'Выберите проект',
settings: 'Настройки', settings: 'Настройки',
stopwatch: 'Секундомер', stopwatch: 'Секундомер',
subscribeToMyOwnCardsByDefault: 'По умолчанию подписаться на мои собственные карточки', subscribeToMyOwnCardsByDefault: 'По умолчанию подписываться на мои собственные карточки',
taskActions: 'Действия с задачей', taskActions: 'Действия с задачей',
tasks: 'Задачи', tasks: 'Задачи',
thereIsNoPreviewAvailableForThisAttachment: 'Предпросмотр для этого вложения недоступен', thereIsNoPreviewAvailableForThisAttachment: 'Предпросмотр для этого вложения недоступен',
@@ -147,6 +163,7 @@ export default {
username: 'Имя пользователя', username: 'Имя пользователя',
usernameAlreadyInUse: 'Имя пользователя уже занято', usernameAlreadyInUse: 'Имя пользователя уже занято',
users: 'Пользователи', users: 'Пользователи',
version: 'Версия',
viewer: 'Читатель', viewer: 'Читатель',
writeComment: 'Напишите комментарий...', writeComment: 'Напишите комментарий...',
}, },
@@ -185,6 +202,7 @@ export default {
editDueDate: 'Изменить срок', editDueDate: 'Изменить срок',
editDescription: 'Изменить описание', editDescription: 'Изменить описание',
editEmail: 'Изменить e-mail', editEmail: 'Изменить e-mail',
editInformation: 'Изменить информацию',
editPassword: 'Изменить пароль', editPassword: 'Изменить пароль',
editPermissions: 'Изменить разрешения', editPermissions: 'Изменить разрешения',
editStopwatch: 'Изменить секундомер', editStopwatch: 'Изменить секундомер',
@@ -192,6 +210,9 @@ export default {
editTitle: 'Изменить название', editTitle: 'Изменить название',
editUsername: 'Изменить имя пользователя', editUsername: 'Изменить имя пользователя',
hideDetails: 'Скрыть подробности', hideDetails: 'Скрыть подробности',
import: 'Импорт',
leaveBoard: 'Покинуть доску',
leaveProject: 'Покинуть проект',
logOut: 'Выйти', logOut: 'Выйти',
makeCover: 'Сделать обложкой', makeCover: 'Сделать обложкой',
move: 'Переместить', move: 'Переместить',
@@ -199,7 +220,9 @@ export default {
remove: 'Убрать', remove: 'Убрать',
removeBackground: 'Убрать фон', removeBackground: 'Убрать фон',
removeCover: 'Убрать обложку', removeCover: 'Убрать обложку',
removeFromBoard: 'Удалить из доски',
removeFromProject: 'Удалить из проекта', removeFromProject: 'Удалить из проекта',
removeManager: 'Удалить менеджера',
removeMember: 'Удалить участника', removeMember: 'Удалить участника',
save: 'Сохранить', save: 'Сохранить',
showAllAttachments: 'Показать все вложения ({{hidden}} скрыто)', showAllAttachments: 'Показать все вложения ({{hidden}} скрыто)',
+219
View File
@@ -0,0 +1,219 @@
import dateFns from 'date-fns/locale/tr';
export default {
dateFns,
format: {
date: 'd.MM.yyyy',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd. MMM',
longDateTime: "d. MMMM 'Saat' p",
},
translation: {
common: {
account: 'Hesap',
actions: 'Eylemler',
addAttachment_title: 'Dosya ekle',
addComment: 'Yorum ekle',
addManager_title: 'Yönetici ekle',
addMember_title: 'Üye ekle',
addUser_title: 'Kullanıcı ekle',
administrator: 'Administrator',
all: 'Tümü',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Bağlantı yeniden kurulduğunda tüm değişiklikler kaydedilecektir.',
areYouSureYouWantToDeleteThisAttachment: 'Bu eki silmek istediğinize emin misiniz?',
areYouSureYouWantToDeleteThisBoard: 'Bu panoyu silmek istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisCard: 'Bu kartı silmek istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisComment: 'Bu yorumu silmek istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisLabel: 'Bu etiketi silmek istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisList: 'Bu listeyi silmek istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisProject: 'Bu projeyi silmek istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisTask: 'Bu görevi silmek istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisUser: 'Bu kullanıcıyı silmek istediğinizden emin misiniz?',
areYouSureYouWantToLeaveBoard: 'Panodan ayrılmak istediğinizden emin misiniz?',
areYouSureYouWantToLeaveProject: 'Projeden ayrılmak istediğinizden emin misiniz?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Bu yöneticiyi projeden çıkarmak istediğinizden emin misiniz?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Bu üyeyi panodan çıkarmak istediğinizden emin misiniz?',
attachment: 'ek',
attachments: 'ekler',
authentication: 'kimlik doğrulama',
background: 'arka plan',
board: 'pano',
boardNotFound_title: 'Pano bulunamadı',
cardActions_title: 'Kart İşlemleri',
cardNotFound_title: 'Kart bulunamadı',
cardOrActionAreDeleted: 'Kart veya işlem silindi',
color: 'renk',
createBoard_title: 'Pano Oluştur',
createLabel_title: 'Etiket Oluştur',
createNewOneOrSelectExistingOne: 'Yeni bir tane oluşturun veya mevcut bir tanesini seçin.',
createProject_title: 'Proje Oluştur',
createTextFile_title: 'Metin Dosyası Oluştur',
currentPassword: 'Geçerli şifre',
dangerZone_title: 'Tehlikeli Bölge',
date: 'tarih',
dueDate_title: 'Termin Tarihi',
deleteAttachment_title: 'Eki Sil',
deleteBoard_title: 'Panoyu Sil',
deleteCard_title: 'Kartı Sil',
deleteComment_title: 'Yorumu Sil',
deleteLabel_title: 'Etiketi Sil',
deleteList_title: 'Listeyi Sil',
deleteProject_title: 'Projeyi Sil',
deleteTask_title: 'Görevi Sil',
deleteUser_title: 'Kullanıcıyı Sil',
description: 'açıklama',
detectAutomaically: 'Otomatik olarak algıla',
dropFileToUpload: 'Yüklenecek dosyayı buraya bırakın',
editAttachment_title: 'Eki Düzenle',
editAvatar_title: 'Avatarı Düzenle',
editBoard_title: 'Panoyu Düzenle',
editDueDate_title: 'Son Tarihi Düzenle',
editEmail_title: 'E-posta Adresini Düzenle',
editLabel_title: 'Etiketi Düzenle',
editPassword_title: 'Şifreyi Değiştir',
editStopwatch_title: 'Kronometreyi Düzenle',
editUsername_title: 'Kullanıcı Adını Düzenle',
email: 'e-posta',
emailAlreadyInUse: 'E-posta adresi zaten kullanımda',
enterCardTitle: 'Kart başlığını girin',
enterDescription: 'Açıklamayı girin',
enterFilename: 'Dosya adını girin',
enterListTitle: 'Liste başlığını girin',
enterProjectTitle: 'Proje başlığını girin',
enterTaskDescription: 'Görev açıklamasını girin',
filterByLabels_title: 'Etikete Göre Filtrele',
filterByMembers_title: 'Üyelere göre filtrele',
fromComputer_title: 'Bilgisayardan',
general: 'Genel',
hours: 'saat',
invalidCurrentPassword: 'Mevcut şifre yanlış',
labels: 'etiketler',
language: 'dil',
leaveBoard_title: 'Panodan Ayrıl',
leaveProject_title: 'Projeden Ayrıl',
list: 'Listeler',
listActions_title: 'Görevleri Listele',
managers: 'Yöneticiler',
members: 'Üyeler',
minutes: 'dakika',
moveCard_title: 'Kartı Taşı',
name: 'isim',
newEmail: 'Yeni e-posta adresi',
newPassword: 'Yeni şifre',
newUsername: 'Yeni kullanıcı adı',
noConnectionToServer: 'Sunucuya bağlantı yok',
noBoards: 'Pano yok',
noLists: 'Liste yok',
noProjects: 'Proje yok',
notifications: 'Bildirimler',
noUnreadNotifications: 'Okunmamış bildirim yok',
openBoard_title: 'Açık Pano',
optional_inline: 'İsteğe bağlı',
organization: 'Organizasyon',
phone: 'telefon',
preferences: 'Tercihler',
pressPasteShortcutToAddAttachmentFromClipboard:
'İpucu: Panodan bir ek eklemek için CTRL-V ye (Macte Cmd-V) basın',
project: 'Proje',
projectNotFound_title: 'Proje bulunamadı',
removeManager_title: 'Yöneticiyi Kaldır',
removeMember_title: 'Üyeyi Kaldır',
seconds: 'saniye',
selectBoard: 'Pano Seç',
selectList: 'Liste seç',
selectProject: 'Proje seç',
settings: 'Ayarlar',
stopwatch: 'kronometre',
subscribeToMyOwnCardsByDefault: 'Varsayılan olarak kendi kartlarıma abone ol',
taskActions_title: 'Görev Eylemleri',
tasks: 'Görevler',
thereIsNoPreviewAvailableForThisAttachment: 'Bu ek için önizleme mevcut değil',
time: 'zaman',
title: 'başlık',
userActions_title: 'Kullanıcı İşlemleri',
userAddedThisCardToList: '<0>{{user}}</0><1> bu kartı {{list}</1> listesine ekledi',
userLeftNewCommentToCard:
'{{user}} yeni bir yorum yazdı: <2>{{card}</2> kartına «{{comment}}»',
userMovedCardFromListToList:
'{{user}}, <2>{{card}></2> kartını {{fromList}} listesinden {{toList}} listesine taşıdı',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> bu kartı {{fromList}} konumundan {{toList}}</1> konumuna taşıdı',
username: 'kullanıcı adı',
usernameAlreadyInUse: 'Kullanıcı adı zaten kullanımda',
users: 'kullanıcı',
writeComment: 'Yorum yazın',
},
action: {
addAnotherCard: 'Başka bir kart ekle',
addAnotherList: 'Başka bir liste ekle',
addAnotherTask: 'Başka bir görev ekle',
addCard: 'Kart ekle',
addCard_title: 'Kart Ekle',
addComment: 'Yorum ekle',
addList: 'Liste ekle',
addMoreDetailedDescription: 'Ayrıntılı bir açıklama ekleyin',
addTask: 'Görev ekle',
addToCard: 'Karta ekle',
addUser: 'Kullanıcı ekle',
createBoard: 'Pano oluştur',
createFile: 'Dosya oluştur',
createLabel: 'Etiket Oluştur',
createNewLabel: 'Yeni etiket oluştur',
createProject: 'Proje oluştur',
delete: 'Sil',
deleteAttachment: 'Eki sil',
deleteAvatar: 'Avatarı sil',
deleteBoard: 'Panoyu Sil',
deleteCard: 'Kartı sil',
deleteCard_title: 'Kartı Sil',
deleteComment: 'Yorumu sil',
deleteImage: 'Resmi sil',
deleteLabel: 'Etiketi sil',
deleteList: 'Listeyi sil',
deleteList_title: 'Listeyi Sil',
deleteProject: 'Projeyi sil',
deleteProject_title: 'Projeyi Sil',
deleteTask: 'Görevi sil',
deleteTask_title: 'Görevi Sil',
deleteUser: 'Kullanıcıyı sil',
edit: 'Düzenle',
editDueDate_title: 'Son Tarihi Düzenle',
editDescription_title: 'Açıklamayı Düzenle',
editEmail_title: 'E-posta Adresini Düzenle',
editPassword_title: 'Şifreyi Değiştir',
editStopwatch_title: 'Kronometreyi Düzenle',
editTitle_title: 'Başlığı düzenle',
editUsername_title: 'Kullanıcı Adını Düzenle',
hideDetails: 'Ayrıntıları gizle',
leaveBoard: 'Panodan Ayrıl',
leaveProject: 'Projeden ayrıl',
logOut_title: 'Çıkış',
makeCover_title: 'Önizleme olarak ayarla',
move: 'Taşı',
moveCard_title: 'Kartı Taşı',
remove: 'Sil',
removeBackground: 'Arka planı kaldır',
removeCover_title: 'Önizlemeyi Kaldır',
removeFromBoard: 'Panodan kaldır',
removeFromProject: 'Projeden kaldır',
removeManager: 'Yöneticiyi kaldır',
removeMember: 'Üyeyi kaldır',
save: 'Kaydet',
showAllAttachments: 'Tüm ekleri göster ({{hidden}} gizli)',
showDetails: 'Ayrıntıları göster',
showFewerAttachments: 'Daha az ek göster',
start: 'başlat',
stop: 'dur',
subscribe: 'Abone ol',
unsubscribe: 'Abonelikten çık',
uploadNewAvatar: 'Yeni avatar yükle',
uploadNewImage: 'Yeni resim yükle',
},
},
};
+8
View File
@@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'tr',
country: 'tr',
name: 'Türkçe',
embeddedLocale: login,
};
+20
View File
@@ -0,0 +1,20 @@
export default {
translation: {
common: {
emailOrUsername: 'E-posta adresi veya Kullanıcı adı',
invalidEmailOrUsername: 'Geçersiz e-posta adresi veya kullanıcı adı',
invalidPassword: 'Hatalı Şifre',
logInToPlanka: 'Giriş Yap',
noInternetConnection: 'Internet bağlantısı yok',
pageNotFound_title: 'Sayfa bulunamadı',
password: 'Şifre',
projectManagement: 'Proje Yönetimi',
serverConnectionFailed: 'Sunucu bağlantı hatası',
unknownError: 'Bilinmeyen Hata, daha sonra tekrar deneyin',
},
action: {
logIn: 'Giriş Yap',
},
},
};
+6 -6
View File
@@ -53,7 +53,7 @@ export default {
dueDate_title: '截止日期', dueDate_title: '截止日期',
deleteAttachment_title: '删除附件', deleteAttachment_title: '删除附件',
deleteBoard_title: '删除面板', deleteBoard_title: '删除面板',
deleteCard_title: '删除擦片', deleteCard_title: '删除卡片',
deleteComment_title: '删除评论', deleteComment_title: '删除评论',
deleteLabel_title: '删除标签', deleteLabel_title: '删除标签',
deleteList_title: '删除列表', deleteList_title: '删除列表',
@@ -81,7 +81,7 @@ export default {
enterTaskDescription: '输入任务描述...', enterTaskDescription: '输入任务描述...',
filterByLabels_title: '通过标签筛选', filterByLabels_title: '通过标签筛选',
filterByMembers_title: '通过成员筛选', filterByMembers_title: '通过成员筛选',
fromComputer_title: '从电脑', fromComputer_title: '从本机',
general: '全体', general: '全体',
hours: '小时', hours: '小时',
invalidCurrentPassword: '当前密码错误', invalidCurrentPassword: '当前密码错误',
@@ -110,7 +110,7 @@ export default {
phone: '电话', phone: '电话',
preferences: '偏好', preferences: '偏好',
pressPasteShortcutToAddAttachmentFromClipboard: pressPasteShortcutToAddAttachmentFromClipboard:
'提示: 点击 Ctrl-V (Mac: Cmd-V) 从剪切板添加附件', '提示: 按下 Ctrl-V (Mac: Cmd-V) 从剪切板添加附件',
project: '项目', project: '项目',
projectNotFound_title: '项目未找到', projectNotFound_title: '项目未找到',
removeManager_title: '删除管理员', removeManager_title: '删除管理员',
@@ -134,7 +134,7 @@ export default {
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> 将该卡片从 {{fromList}} 移动到 {{toList}}</1>', '<0>{{user}}</0><1> 将该卡片从 {{fromList}} 移动到 {{toList}}</1>',
username: '用户名', username: '用户名',
usernameAlreadyInUse: '用户名已占用', usernameAlreadyInUse: '用户名已被占用',
users: '用户', users: '用户',
writeComment: '编写评论...', writeComment: '编写评论...',
}, },
@@ -149,7 +149,7 @@ export default {
addList: '添加列表', addList: '添加列表',
addMoreDetailedDescription: '添加更多详细描述', addMoreDetailedDescription: '添加更多详细描述',
addTask: '添加任务', addTask: '添加任务',
addToCard: '添加擦篇', addToCard: '添加到卡片',
addUser: '添加用户', addUser: '添加用户',
createBoard: '创建面板', createBoard: '创建面板',
createFile: '创建文件', createFile: '创建文件',
@@ -177,7 +177,7 @@ export default {
editDescription_title: '编辑描述', editDescription_title: '编辑描述',
editEmail_title: '编辑邮箱', editEmail_title: '编辑邮箱',
editPassword_title: '编辑密码', editPassword_title: '编辑密码',
editStopwatch_title: '编辑实践', editStopwatch_title: '编辑时间',
editTitle_title: '编辑标题', editTitle_title: '编辑标题',
editUsername_title: '编辑用户名', editUsername_title: '编辑用户名',
leaveBoard: '离开面板', leaveBoard: '离开面板',
+3
View File
@@ -12,6 +12,9 @@ export default class extends BaseModel {
coverUrl: attr(), coverUrl: attr(),
image: attr(), image: attr(),
name: attr(), name: attr(),
createdAt: attr({
getDefault: () => new Date(),
}),
cardId: fk({ cardId: fk({
to: 'Card', to: 'Card',
as: 'card', as: 'card',
+1 -1
View File
@@ -172,7 +172,7 @@ export default class extends BaseModel {
} }
getOrderedMembershipsQuerySet() { getOrderedMembershipsQuerySet() {
return this.memberships.orderBy('id'); return this.memberships.orderBy('createdAt');
} }
getOrderedLabelsQuerySet() { getOrderedLabelsQuerySet() {
+3
View File
@@ -10,6 +10,9 @@ export default class extends BaseModel {
id: attr(), id: attr(),
role: attr(), role: attr(),
canComment: attr(), canComment: attr(),
createdAt: attr({
getDefault: () => new Date(),
}),
boardId: fk({ boardId: fk({
to: 'Board', to: 'Board',
as: 'board', as: 'board',
+2 -2
View File
@@ -244,7 +244,7 @@ export default class extends BaseModel {
} }
getOrderedAttachmentsQuerySet() { getOrderedAttachmentsQuerySet() {
return this.attachments.orderBy('id', false); return this.attachments.orderBy('createdAt', false);
} }
getFilteredOrderedInCardActivitiesQuerySet() { getFilteredOrderedInCardActivitiesQuerySet() {
@@ -256,7 +256,7 @@ export default class extends BaseModel {
filter.type = ActivityTypes.COMMENT_CARD; filter.type = ActivityTypes.COMMENT_CARD;
} }
return this.activities.filter(filter).orderBy('id', false); return this.activities.filter(filter).orderBy('createdAt', false);
} }
getUnreadNotificationsQuerySet() { getUnreadNotificationsQuerySet() {
+1 -1
View File
@@ -137,7 +137,7 @@ export default class extends BaseModel {
} }
getOrderedManagersQuerySet() { getOrderedManagersQuerySet() {
return this.managers.orderBy('id'); return this.managers.orderBy('createdAt');
} }
getOrderedBoardsQuerySet() { getOrderedBoardsQuerySet() {
+3
View File
@@ -8,6 +8,9 @@ export default class extends BaseModel {
static fields = { static fields = {
id: attr(), id: attr(),
createdAt: attr({
getDefault: () => new Date(),
}),
projectId: fk({ projectId: fk({
to: 'Project', to: 'Project',
as: 'project', as: 'project',
+10 -4
View File
@@ -43,10 +43,16 @@ export default class extends BaseModel {
organization: attr(), organization: attr(),
language: attr(), language: attr(),
subscribeToOwnCards: attr(), subscribeToOwnCards: attr(),
createdAt: attr({
getDefault: () => new Date(),
}),
deletedAt: attr(), deletedAt: attr(),
isAdmin: attr({ isAdmin: attr({
getDefault: () => false, getDefault: () => false,
}), }),
isLocked: attr({
getDefault: () => false,
}),
isAvatarUpdating: attr({ isAvatarUpdating: attr({
getDefault: () => false, getDefault: () => false,
}), }),
@@ -293,15 +299,15 @@ export default class extends BaseModel {
static getOrderedUndeletedQuerySet() { static getOrderedUndeletedQuerySet() {
return this.filter({ return this.filter({
deletedAt: null, deletedAt: null,
}).orderBy('id'); }).orderBy('createdAt');
} }
getOrderedProjectManagersQuerySet() { getOrderedProjectManagersQuerySet() {
return this.projectManagers.orderBy('id'); return this.projectManagers.orderBy('createdAt');
} }
getOrderedBoardMembershipsQuerySet() { getOrderedBoardMembershipsQuerySet() {
return this.boardMemberships.orderBy('id'); return this.boardMemberships.orderBy('createdAt');
} }
getOrderedUnreadNotificationsQuerySet() { getOrderedUnreadNotificationsQuerySet() {
@@ -309,7 +315,7 @@ export default class extends BaseModel {
.filter({ .filter({
isRead: false, isRead: false,
}) })
.orderBy('id', false); .orderBy('createdAt', false);
} }
getOrderedAvailableProjectsModelArray() { getOrderedAvailableProjectsModelArray() {
+16 -16
View File
@@ -16,17 +16,17 @@ const createSocketEventsChannel = () =>
emit(entryActions.handleSocketReconnect()); emit(entryActions.handleSocketReconnect());
}; };
const handleUserCreate = ({ item }) => { const handleUserCreate = api.makeHandleUserCreate(({ item }) => {
emit(entryActions.handleUserCreate(item)); emit(entryActions.handleUserCreate(item));
}; });
const handleUserUpdate = ({ item }) => { const handleUserUpdate = api.makeHandleUserUpdate(({ item }) => {
emit(entryActions.handleUserUpdate(item)); emit(entryActions.handleUserUpdate(item));
}; });
const handleUserDelete = ({ item }) => { const handleUserDelete = api.makeHandleUserDelete(({ item }) => {
emit(entryActions.handleUserDelete(item)); emit(entryActions.handleUserDelete(item));
}; });
const handleProjectCreate = ({ item }) => { const handleProjectCreate = ({ item }) => {
emit(entryActions.handleProjectCreate(item)); emit(entryActions.handleProjectCreate(item));
@@ -40,13 +40,13 @@ const createSocketEventsChannel = () =>
emit(entryActions.handleProjectDelete(item)); emit(entryActions.handleProjectDelete(item));
}; };
const handleProjectManagerCreate = ({ item }) => { const handleProjectManagerCreate = api.makeHandleProjectManagerCreate(({ item }) => {
emit(entryActions.handleProjectManagerCreate(item)); emit(entryActions.handleProjectManagerCreate(item));
}; });
const handleProjectManagerDelete = ({ item }) => { const handleProjectManagerDelete = api.makeHandleProjectManagerDelete(({ item }) => {
emit(entryActions.handleProjectManagerDelete(item)); emit(entryActions.handleProjectManagerDelete(item));
}; });
const handleBoardCreate = ({ item, requestId }) => { const handleBoardCreate = ({ item, requestId }) => {
emit(entryActions.handleBoardCreate(item, requestId)); emit(entryActions.handleBoardCreate(item, requestId));
@@ -60,17 +60,17 @@ const createSocketEventsChannel = () =>
emit(entryActions.handleBoardDelete(item)); emit(entryActions.handleBoardDelete(item));
}; };
const handleBoardMembershipCreate = ({ item }) => { const handleBoardMembershipCreate = api.makeHandleBoardMembershipCreate(({ item }) => {
emit(entryActions.handleBoardMembershipCreate(item)); emit(entryActions.handleBoardMembershipCreate(item));
}; });
const handleBoardMembershipUpdate = ({ item }) => { const handleBoardMembershipUpdate = api.makeHandleBoardMembershipUpdate(({ item }) => {
emit(entryActions.handleBoardMembershipUpdate(item)); emit(entryActions.handleBoardMembershipUpdate(item));
}; });
const handleBoardMembershipDelete = ({ item }) => { const handleBoardMembershipDelete = api.makeHandleBoardMembershipDelete(({ item }) => {
emit(entryActions.handleBoardMembershipDelete(item)); emit(entryActions.handleBoardMembershipDelete(item));
}; });
const handleListCreate = ({ item }) => { const handleListCreate = ({ item }) => {
emit(entryActions.handleListCreate(item)); emit(entryActions.handleListCreate(item));
+5 -1
View File
@@ -7,9 +7,13 @@ import { setAccessToken } from '../../../utils/access-token-storage';
export function* authenticate(data) { export function* authenticate(data) {
yield put(actions.authenticate(data)); yield put(actions.authenticate(data));
let accessToken; let accessToken = data.access_token;
try { try {
if (accessToken) {
({ item: accessToken } = yield call(api.exchangeOidcToken, accessToken));
} else {
({ item: accessToken } = yield call(api.createAccessToken, data)); ({ item: accessToken } = yield call(api.createAccessToken, data));
}
} catch (error) { } catch (error) {
yield put(actions.authenticate.failure(error)); yield put(actions.authenticate.failure(error));
return; return;
+14
View File
@@ -23,6 +23,20 @@ services:
- TRUST_PROXY=0 - TRUST_PROXY=0
- DATABASE_URL=postgresql://postgres@postgres/planka - DATABASE_URL=postgresql://postgres@postgres/planka
- SECRET_KEY=notsecretkey - SECRET_KEY=notsecretkey
# Can be removed after installation
- 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
# related: https://github.com/knex/knex/issues/2354
# As knex does not pass query parameters from the connection string we
# have to use environment variables in order to pass the desired values, e.g.
# - PGSSLMODE=<value>
# Configure knex to accept SSL certificates
# - KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
depends_on: depends_on:
- postgres - postgres
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "planka", "name": "planka",
"version": "1.11.0", "version": "1.12.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "planka", "name": "planka",
"version": "1.11.0", "version": "1.12.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "planka", "name": "planka",
"version": "1.11.0", "version": "1.12.0",
"private": true, "private": true,
"homepage": "https://plankanban.github.io/planka", "homepage": "https://plankanban.github.io/planka",
"repository": { "repository": {
+15
View File
@@ -4,11 +4,26 @@ BASE_URL=http://localhost:1337
DATABASE_URL=postgresql://postgres@localhost/planka DATABASE_URL=postgresql://postgres@localhost/planka
SECRET_KEY=notsecretkey SECRET_KEY=notsecretkey
## Can be removed after installation
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
## Optional ## Optional
# TRUST_PROXY=0 # TRUST_PROXY=0
# TOKEN_EXPIRES_IN=365 # In days # TOKEN_EXPIRES_IN=365 # In days
# related: https://github.com/knex/knex/issues/2354
# As knex does not pass query parameters from the connection string we
# have to use environment variables in order to pass the desired values, e.g.
# PGSSLMODE=<value>
# Configure knex to accept SSL certificates
# KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
## Do not edit this ## Do not edit this
TZ=UTC TZ=UTC
@@ -6,7 +6,7 @@ module.exports = {
accessToken, accessToken,
deletedAt: null, deletedAt: null,
}).set({ }).set({
deletedAt: new Date().toUTCString(), deletedAt: new Date().toISOString(),
}); });
return { return {
@@ -0,0 +1,177 @@
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const openidClient = require('openid-client');
const { getRemoteAddress } = require('../../../utils/remoteAddress');
const Errors = {
INVALID_TOKEN: {
invalidToken: 'Access Token is invalid',
},
MISSING_VALUES: {
missingValues:
'Unable to retrieve required values. Verify the access token or UserInfo endpoint has email, username and name claims',
},
};
const jwks = jwksClient({
jwksUri: sails.config.custom.oidcJwksUri,
requestHeaders: {}, // Optional
timeout: 30000, // Defaults to 30s
});
const getJwtVerificationOptions = () => {
const options = {};
if (sails.config.custom.oidcIssuer) {
options.issuer = sails.config.custom.oidcIssuer;
}
if (sails.config.custom.oidcAudience) {
options.audience = sails.config.custom.oidcAudience;
}
return options;
};
const validateAndDecodeToken = async (accessToken, options) => {
const keys = await jwks.getSigningKeys();
let validToken = {};
const isTokenValid = keys.some((signingKey) => {
try {
const key = signingKey.getPublicKey();
validToken = jwt.verify(accessToken, key, options);
return 'true';
} catch (error) {
sails.log.error(error);
}
return false;
});
if (!isTokenValid) {
const tokenForLogging = jwt.decode(accessToken);
const remoteAddress = getRemoteAddress(this.req);
sails.log.warn(
`invalid token: sub: "${tokenForLogging.sub}" issuer: "${tokenForLogging.iss}" audience: "${tokenForLogging.aud}" exp: ${tokenForLogging.exp} (IP: ${remoteAddress})`,
);
throw Errors.INVALID_TOKEN;
}
return validToken;
};
const getUserInfo = async (accessToken, options) => {
if (sails.config.custom.oidcSkipUserInfo) {
return {};
}
const issuer = await openidClient.Issuer.discover(options.issuer);
const oidcClient = new issuer.Client({
client_id: 'irrelevant',
});
const userInfo = await oidcClient.userinfo(accessToken);
return userInfo;
};
const mergeUserData = (validToken, userInfo) => {
const oidcUser = { ...validToken, ...userInfo };
return oidcUser;
};
const getOrCreateUser = async (newUser) => {
const user = await User.findOne({
where: {
username: newUser.username,
},
});
if (user) {
return user;
}
return User.create(newUser).fetch();
};
module.exports = {
inputs: {
token: {
type: 'string',
required: true,
},
},
exits: {
invalidToken: {
responseType: 'unauthorized',
},
missingValues: {
responseType: 'unauthorized',
},
},
async fn(inputs) {
const options = getJwtVerificationOptions();
const validToken = await validateAndDecodeToken(inputs.token, options);
const userInfo = await getUserInfo(inputs.token, options);
const oidcUser = mergeUserData(validToken, userInfo);
const now = new Date();
let isAdmin = false;
if (sails.config.custom.oidcAdminRoles.includes('*')) isAdmin = true;
else if (Array.isArray(oidcUser[sails.config.custom.oidcRolesAttribute])) {
const userRoles = new Set(oidcUser[sails.config.custom.oidcRolesAttribute]);
isAdmin = sails.config.custom.oidcAdminRoles.findIndex((role) => userRoles.has(role)) > -1;
}
const newUser = {
email: oidcUser.email,
isAdmin,
name: oidcUser.name,
username: oidcUser.preferred_username,
subscribeToOwnCards: false,
createdAt: now,
updatedAt: now,
locked: true,
};
if (!newUser.email || !newUser.username || !newUser.name) {
sails.log.error(Errors.MISSING_VALUES.missingValues);
throw Errors.MISSING_VALUES;
}
const identityProviderUser = await IdentityProviderUser.findOne({
where: {
issuer: oidcUser.iss,
sub: oidcUser.sub,
},
}).populate('userId');
let user = identityProviderUser ? identityProviderUser.userId : {};
if (!identityProviderUser) {
user = await getOrCreateUser(newUser);
await IdentityProviderUser.create({
issuer: oidcUser.iss,
sub: oidcUser.sub,
userId: user.id,
});
}
const controlledFields = ['email', 'password', 'isAdmin', 'name', 'username'];
const updateFields = {};
controlledFields.forEach((field) => {
if (user[field] !== newUser[field]) {
updateFields[field] = newUser[field];
}
});
if (Object.keys(updateFields).length > 0) {
updateFields.updatedAt = now;
await User.updateOne({ id: user.id }).set(updateFields);
}
const plankaToken = sails.helpers.utils.createToken(user.id);
const remoteAddress = getRemoteAddress(this.req);
await Session.create({
accessToken: plankaToken,
remoteAddress,
userId: user.id,
userAgent: this.req.headers['user-agent'],
});
return {
item: plankaToken,
};
},
};
+11
View File
@@ -0,0 +1,11 @@
module.exports = {
async fn() {
const config = {
authority: sails.config.custom.oidcIssuer,
clientId: sails.config.custom.oidcClientId,
redirectUri: sails.config.custom.oidcredirectUri,
scopes: sails.config.custom.oidcScopes,
};
return config;
},
};
+4
View File
@@ -26,6 +26,10 @@ module.exports = {
throw Errors.USER_NOT_FOUND; throw Errors.USER_NOT_FOUND;
} }
if (user.email === sails.config.custom.defaultAdminEmail) {
throw Errors.USER_NOT_FOUND; // Forbidden
}
user = await sails.helpers.users.deleteOne.with({ user = await sails.helpers.users.deleteOne.with({
record: user, record: user,
request: this.req, request: this.req,
@@ -59,6 +59,10 @@ module.exports = {
throw Errors.USER_NOT_FOUND; throw Errors.USER_NOT_FOUND;
} }
if (user.email === sails.config.custom.defaultAdminEmail) {
throw Errors.USER_NOT_FOUND; // Forbidden
}
if ( if (
inputs.id === currentUser.id && inputs.id === currentUser.id &&
!bcrypt.compareSync(inputs.currentPassword, user.password) !bcrypt.compareSync(inputs.currentPassword, user.password)
@@ -58,6 +58,10 @@ module.exports = {
throw Errors.USER_NOT_FOUND; throw Errors.USER_NOT_FOUND;
} }
if (user.email === sails.config.custom.defaultAdminEmail) {
throw Errors.USER_NOT_FOUND; // Forbidden
}
if ( if (
inputs.id === currentUser.id && inputs.id === currentUser.id &&
!bcrypt.compareSync(inputs.currentPassword, user.password) !bcrypt.compareSync(inputs.currentPassword, user.password)
@@ -61,6 +61,10 @@ module.exports = {
throw Errors.USER_NOT_FOUND; throw Errors.USER_NOT_FOUND;
} }
if (user.email === sails.config.custom.defaultAdminEmail) {
throw Errors.USER_NOT_FOUND; // Forbidden
}
if ( if (
inputs.id === currentUser.id && inputs.id === currentUser.id &&
!bcrypt.compareSync(inputs.currentPassword, user.password) !bcrypt.compareSync(inputs.currentPassword, user.password)
+7
View File
@@ -67,6 +67,13 @@ module.exports = {
throw Errors.USER_NOT_FOUND; throw Errors.USER_NOT_FOUND;
} }
if (user.email === sails.config.custom.defaultAdminEmail) {
/* eslint-disable no-param-reassign */
delete inputs.isAdmin;
delete inputs.name;
/* eslint-enable no-param-reassign */
}
const values = { const values = {
..._.pick(inputs, [ ..._.pick(inputs, [
'isAdmin', 'isAdmin',
+1 -1
View File
@@ -30,7 +30,7 @@ module.exports = {
id: inputs.record.id, id: inputs.record.id,
deletedAt: null, deletedAt: null,
}).set({ }).set({
deletedAt: new Date().toUTCString(), deletedAt: new Date().toISOString(),
}); });
if (user) { if (user) {
+1 -1
View File
@@ -64,7 +64,7 @@ module.exports = {
if (!_.isUndefined(values.password)) { if (!_.isUndefined(values.password)) {
Object.assign(values, { Object.assign(values, {
password: bcrypt.hashSync(values.password, 10), password: bcrypt.hashSync(values.password, 10),
passwordChangedAt: new Date().toUTCString(), passwordChangedAt: new Date().toISOString(),
}); });
if (Object.keys(values).length === 1) { if (Object.keys(values).length === 1) {
+40
View File
@@ -0,0 +1,40 @@
/**
* ProjectManager.js
*
* @description :: A model definition represents a database table/collection.
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
*/
module.exports = {
attributes: {
issuer: {
type: 'string',
isNotEmptyString: true,
allowNull: true,
},
sub: {
type: 'string',
isNotEmptyString: true,
allowNull: true,
},
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
userId: {
model: 'User',
required: true,
columnName: 'user_id',
},
},
tableName: 'identity_provider_user',
};
+9 -1
View File
@@ -18,7 +18,6 @@ module.exports = {
}, },
password: { password: {
type: 'string', type: 'string',
required: true,
}, },
isAdmin: { isAdmin: {
type: 'boolean', type: 'boolean',
@@ -68,6 +67,10 @@ module.exports = {
type: 'ref', type: 'ref',
columnName: 'password_changed_at', columnName: 'password_changed_at',
}, },
locked: {
type: 'boolean',
columnName: 'locked',
},
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗ // ║╣ ║║║╠╩╗║╣ ║║╚═╗
@@ -97,6 +100,10 @@ module.exports = {
via: 'userId', via: 'userId',
through: 'CardMembership', through: 'CardMembership',
}, },
identityProviders: {
collection: 'IdentityProviderUser',
via: 'userId',
},
}, },
tableName: 'user_account', tableName: 'user_account',
@@ -107,6 +114,7 @@ module.exports = {
avatarUrl: avatarUrl:
this.avatar && this.avatar &&
`${sails.config.custom.userAvatarsUrl}/${this.avatar.dirname}/square-100.${this.avatar.extension}`, `${sails.config.custom.userAvatarsUrl}/${this.avatar.dirname}/square-100.${this.avatar.extension}`,
isLocked: this.email === sails.config.custom.defaultAdminEmail,
}; };
}, },
}; };
+12
View File
@@ -30,4 +30,16 @@ module.exports.custom = {
attachmentsPath: path.join(sails.config.appPath, 'private', 'attachments'), attachmentsPath: path.join(sails.config.appPath, 'private', 'attachments'),
attachmentsUrl: `${process.env.BASE_URL}/attachments`, attachmentsUrl: `${process.env.BASE_URL}/attachments`,
oidcIssuer: process.env.OIDC_ISSUER,
oidcAudience: process.env.OIDC_AUDIENCE,
oidcClientId: process.env.OIDC_CLIENT_ID,
oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups',
oidcAdminRoles: process.env.OIDC_ADMIN_ROLES ? process.env.OIDC_ADMIN_ROLES.split(',') : [],
oidcredirectUri: process.env.OIDC_REDIRECT_URI,
oidcJwksUri: process.env.OIDC_JWKS_URI,
oidcScopes: process.env.OIDC_SCOPES || 'openid profile email',
oidcSkipUserInfo: process.env.OIDC_SKIP_USER_INFO === 'true',
defaultAdminEmail: process.env.DEFAULT_ADMIN_EMAIL,
}; };
+2 -2
View File
@@ -80,13 +80,13 @@ module.exports.models = {
}, },
beforeCreate(valuesToSet, proceed) { beforeCreate(valuesToSet, proceed) {
valuesToSet.createdAt = new Date().toUTCString(); // eslint-disable-line no-param-reassign valuesToSet.createdAt = new Date().toISOString(); // eslint-disable-line no-param-reassign
proceed(); proceed();
}, },
beforeUpdate(valuesToSet, proceed) { beforeUpdate(valuesToSet, proceed) {
valuesToSet.updatedAt = new Date().toUTCString(); // eslint-disable-line no-param-reassign valuesToSet.updatedAt = new Date().toISOString(); // eslint-disable-line no-param-reassign
proceed(); proceed();
}, },
+2
View File
@@ -24,4 +24,6 @@ module.exports.policies = {
'projects/create': ['is-authenticated', 'is-admin'], 'projects/create': ['is-authenticated', 'is-admin'],
'access-tokens/create': true, 'access-tokens/create': true,
'access-tokens/exchange': true,
'appconfig/index': true,
}; };

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