Compare 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
234 changed files with 34897 additions and 18602 deletions
@@ -40,5 +40,5 @@ jobs:
build-args: ALPINE_VERSION=${{ env.ALPINE_VERSION }} build-args: ALPINE_VERSION=${{ env.ALPINE_VERSION }}
push: true push: true
tags: | tags: |
ghcr.io/${{ github.repository }}:base-latest ghcr.io/plankanban/planka:base-latest
ghcr.io/${{ github.repository }}:base-${{ env.ALPINE_VERSION }} ghcr.io/plankanban/planka:base-${{ env.ALPINE_VERSION }}
@@ -1,20 +1,11 @@
# https://docs.docker.com/build/ci/github-actions/multi-platform/
name: Build and push Docker DEV image name: Build and push Docker DEV image
on: on:
push: push:
paths-ignore:
- '.github/**'
- 'charts/**'
- 'docker-*.sh'
- '*.md'
branches: [master] branches: [master]
workflow_dispatch:
env:
REGISTRY_IMAGE: ghcr.io/${{ github.repository }}
jobs: jobs:
build: build-and-push-docker-image-dev:
runs-on: self-hosted runs-on: self-hosted
steps: steps:
- name: Checkout - name: Checkout
@@ -33,21 +24,11 @@ jobs:
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
type=raw,value=dev
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 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
push: true push: true
tags: ${{ steps.metadata.outputs.tags }} tags: |
labels: ${{ steps.metadata.outputs.labels }} ghcr.io/plankanban/planka:dev
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -31,23 +31,12 @@ jobs:
result-encoding: string result-encoding: string
script: return context.payload.release.tag_name.replace('v', '') script: return context.payload.release.tag_name.replace('v', '')
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v5
with:
images: |
name=ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ steps.set-version.outputs.result }}
type=raw,value=latest
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 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
push: true push: true
tags: ${{ steps.metadata.outputs.tags }} tags: |
labels: ${{ steps.metadata.outputs.labels }} ghcr.io/plankanban/planka:latest
cache-from: type=gha ghcr.io/plankanban/planka:${{ steps.set-version.outputs.result }}
cache-to: type=gha,mode=max
+1 -2
View File
@@ -35,9 +35,8 @@ jobs:
done done
- name: Run chart-releaser for stable - name: Run chart-releaser for stable
uses: helm/chart-releaser-action@v1.6.0 uses: helm/chart-releaser-action@v1.5.0
with: with:
charts_dir: charts charts_dir: charts
mark_as_latest: false
env: env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+6 -19
View File
@@ -1,44 +1,35 @@
FROM node:18-alpine as server-dependencies FROM ghcr.io/plankanban/planka:base-latest as server-dependencies
RUN apk -U upgrade \
&& apk add build-base python3 \
--no-cache
WORKDIR /app 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 install pnpm --global \ && npm install pnpm --global \
&& pnpm import \
&& pnpm install --prod && pnpm install --prod
FROM node:lts AS client FROM node:lts AS client
WORKDIR /app 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 install pnpm --global \ && npm install pnpm --global \
&& pnpm import \
&& pnpm install --prod && pnpm install --prod
COPY client . COPY client .
RUN DISABLE_ESLINT_PLUGIN=true npm run build RUN DISABLE_ESLINT_PLUGIN=true npm run build
FROM node:18-alpine FROM ghcr.io/plankanban/planka:base-latest
RUN apk -U upgrade \ RUN apk del vips-dependencies --purge
&& apk add bash \
--no-cache
USER node USER node
WORKDIR /app WORKDIR /app
COPY --chown=node:node start.sh . COPY --chown=node:node start.sh .
COPY --chown=node:node server . COPY --chown=node:node server .
COPY --chown=node:node healthcheck.js .
RUN mv .env.sample .env RUN mv .env.sample .env
@@ -53,8 +44,4 @@ VOLUME /app/private/attachments
EXPOSE 1337 EXPOSE 1337
HEALTHCHECK --interval=10s --timeout=2s --start-period=15s \ CMD ["./start.sh"]
CMD node ./healthcheck.js
CMD [ "bash", "start.sh" ]
+17 -12
View File
@@ -1,22 +1,27 @@
FROM node:18-alpine FROM node:lts-alpine
ARG VIPS_VERSION=8.14.5 ARG ALPINE_VERSION=3.16
ARG VIPS_VERSION=8.13.3
RUN apk -U upgrade \ RUN apk -U upgrade \
&& apk add \ && apk add \
bash pkgconf \ bash giflib glib lcms2 libexif \
libjpeg-turbo libexif librsvg cgif tiff libspng libimagequant \ libgsf libjpeg-turbo libpng librsvg libwebp \
orc pango tiff \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
--no-cache \ --no-cache \
&& apk add \ && apk add \
build-base gobject-introspection-dev meson \ build-base giflib-dev glib-dev lcms2-dev libexif-dev \
libjpeg-turbo-dev libexif-dev librsvg-dev cgif-dev tiff-dev libspng-dev libimagequant-dev \ libgsf-dev libjpeg-turbo-dev libpng-dev librsvg-dev libwebp-dev \
orc-dev pango-dev tiff-dev \
--virtual vips-dependencies \ --virtual vips-dependencies \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
--no-cache \ --no-cache \
&& wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz | tar xJC /tmp \ && wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.gz | tar xzC /tmp \
&& cd /tmp/vips-${VIPS_VERSION} \ && cd /tmp/vips-${VIPS_VERSION} \
&& meson setup build-dir \ && ./configure \
&& cd build-dir \ && make \
&& ninja \ && make install-strip \
&& ninja test \
&& ninja install \
&& rm -rf /tmp/vips-${VIPS_VERSION} && rm -rf /tmp/vips-${VIPS_VERSION}
+10 -12
View File
@@ -1,5 +1,5 @@
# Planka # Planka
#### Elegant open source project tracking. #### Elegant open source project tracking
![David (path)](https://img.shields.io/github/package-json/v/plankanban/planka) ![Docker Pulls](https://img.shields.io/docker/pulls/meltyshev/planka) ![GitHub](https://img.shields.io/github/license/plankanban/planka) ![David (path)](https://img.shields.io/github/package-json/v/plankanban/planka) ![Docker Pulls](https://img.shields.io/docker/pulls/meltyshev/planka) ![GitHub](https://img.shields.io/github/license/plankanban/planka)
@@ -10,18 +10,20 @@
## Features ## Features
- Create projects, boards, lists, cards, labels and tasks - Create projects, boards, lists, cards, labels and tasks
- Add card members, track time, set due dates, add attachments, write comments - Add card members, track time, set a due date, add attachments, write comments
- Markdown support in card description and comments - Markdown support in a card description and comment
- Filter by members and labels - Filter by members and labels
- Customize project backgrounds - Customize project background
- Real-time updates - Real-time updates
- Internal notifications - User notifications
- Multiple interface languages - Internationalization
- Single sign-on via OpenID Connect
## How to deploy Planka ## How to deploy Planka
There are many ways to install Planka, [check them out](https://docs.planka.cloud/docs/intro). 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))
- [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).
@@ -45,7 +47,3 @@ See the [development section](https://docs.planka.cloud/docs/Development).
## License ## License
Planka is [AGPL-3.0 licensed](https://github.com/plankanban/planka/blob/master/LICENSE). Planka is [AGPL-3.0 licensed](https://github.com/plankanban/planka/blob/master/LICENSE).
## Contributors
[![](https://contrib.rocks/image?repo=plankanban/planka)](https://github.com/plankanban/planka/graphs/contributors)
+7 -7
View File
@@ -15,17 +15,17 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # 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. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.29 version: 0.1.2
# This is the version number of the application being deployed. This version number should be # 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 # 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. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
appVersion: "1.17.2" appVersion: "1.12.0"
dependencies: dependencies:
- alias: postgresql - alias: postgresql
condition: postgresql.enabled condition: postgresql.enabled
name: postgresql name: postgresql
repository: &bitnami-repo https://charts.bitnami.com/bitnami repository: &bitnami-repo https://charts.bitnami.com/bitnami
version: 12.5.1 version: 12.5.1
+1 -24
View File
@@ -21,11 +21,7 @@ git clone https://github.com/Chris-Greaves/planka-helm-chart.git
cd planka-helm-chart cd planka-helm-chart
helm dependency build helm dependency build
export SECRETKEY=$(openssl rand -hex 64) export SECRETKEY=$(openssl rand -hex 64)
helm install planka . --set secretkey=$SECRETKEY \ helm install planka . --set secretkey=$SECRETKEY
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo"
``` ```
> **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. > **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.
@@ -43,19 +39,11 @@ To access Planka externally you can use the following configuration
```bash ```bash
# HTTP only # HTTP only
helm install planka . --set secretkey=$SECRETKEY \ helm install planka . --set secretkey=$SECRETKEY \
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo" \
--set ingress.enabled=true \ --set ingress.enabled=true \
--set ingress.hosts[0].host=planka.example.dev \ --set ingress.hosts[0].host=planka.example.dev \
# HTTPS # HTTPS
helm install planka . --set secretkey=$SECRETKEY \ helm install planka . --set secretkey=$SECRETKEY \
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo" \
--set ingress.enabled=true \ --set ingress.enabled=true \
--set ingress.hosts[0].host=planka.example.dev \ --set ingress.hosts[0].host=planka.example.dev \
--set ingress.tls[0].secretName=planka-tls \ --set ingress.tls[0].secretName=planka-tls \
@@ -66,17 +54,6 @@ or create a values.yaml file like:
```yaml ```yaml
secretkey: "<InsertSecretKey>" secretkey: "<InsertSecretKey>"
# The admin section needs to be present for new instances of Planka, after the first start you can remove the lines starting with admin_. If you want the admin user to be unchangeable admin_email: has to stay
# After changing the config you have to run ```helm upgrade planka . -f values.yaml```
# Admin user
admin_email: "demo@demo.demo" # Do not remove if you want to prevent this user from being edited/deleted
admin_password: "demo"
admin_name: "Demo Demo"
admin_username: "demo"
# Admin user
# Ingress
ingress: ingress:
enabled: true enabled: true
hosts: hosts:
+1 -54
View File
@@ -35,7 +35,7 @@ spec:
imagePullPolicy: {{ .Values.image.pullPolicy }} imagePullPolicy: {{ .Values.image.pullPolicy }}
ports: ports:
- name: http - name: http
containerPort: {{ .Values.service.containerPort | default 1337 }} containerPort: {{ .Values.service.port }}
protocol: TCP protocol: TCP
livenessProbe: livenessProbe:
httpGet: httpGet:
@@ -55,24 +55,14 @@ spec:
- mountPath: /app/private/attachments - mountPath: /app/private/attachments
subPath: attachments subPath: attachments
name: planka name: planka
{{- if .Values.securityContext.readOnlyRootFilesystem }}
- mountPath: /app/logs
subPath: app-logs
name: emptydir
{{- end }}
resources: resources:
{{- toYaml .Values.resources | nindent 12 }} {{- toYaml .Values.resources | nindent 12 }}
env: env:
{{- if not .Values.postgresql.enabled }}
- name: DATABASE_URL
value: {{ required "If the included postgresql deployment is disabled you need to define a Database URL in 'dburl'" .Values.dburl }}
{{- else }}
- name: DATABASE_URL - name: DATABASE_URL
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: planka-postgresql-svcbind-custom-user name: planka-postgresql-svcbind-custom-user
key: uri key: uri
{{- end }}
- name: BASE_URL - name: BASE_URL
{{- if .Values.baseUrl }} {{- if .Values.baseUrl }}
value: {{ .Values.baseUrl }} value: {{ .Values.baseUrl }}
@@ -85,45 +75,6 @@ spec:
value: {{ required "A secret key needs to be generated using 'openssl rand -hex 64' and assigned to secretkey." .Values.secretkey }} value: {{ required "A secret key needs to be generated using 'openssl rand -hex 64' and assigned to secretkey." .Values.secretkey }}
- name: TRUST_PROXY - name: TRUST_PROXY
value: "0" value: "0"
- name: DEFAULT_ADMIN_EMAIL
value: {{ .Values.admin_email }}
- name: DEFAULT_ADMIN_PASSWORD
value: {{ .Values.admin_password }}
- name: DEFAULT_ADMIN_NAME
value: {{ .Values.admin_name }}
- name: DEFAULT_ADMIN_USERNAME
value: {{ .Values.admin_username }}
{{ range $k, $v := .Values.env }}
- name: {{ $k | quote }}
value: {{ $v | quote }}
{{- end }}
{{- if .Values.oidc.enabled }}
{{- $secretName := default (printf "%s-oidc" (include "planka.fullname" .)) .Values.oidc.existingSecret }}
- name: OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
key: clientId
name: {{ $secretName }}
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
key: clientSecret
name: {{ $secretName }}
- name: OIDC_ISSUER
value: {{ required "issuerUrl is required when configuring OIDC" .Values.oidc.issuerUrl | quote }}
- name: OIDC_SCOPES
value: {{ join " " .Values.oidc.scopes | default "openid profile email" | quote }}
{{- if .Values.oidc.admin.roles }}
- name: OIDC_ADMIN_ROLES
value: {{ join "," .Values.oidc.admin.roles | quote }}
{{- end }}
- name: OIDC_ROLES_ATTRIBUTE
value: {{ .Values.oidc.admin.rolesAttribute | default "groups" | quote }}
{{- if .Values.oidc.admin.ignoreRoles }}
- name: OIDC_IGNORE_ROLES
value: {{ .Values.oidc.admin.ignoreRoles | quote }}
{{- end }}
{{- end }}
{{- with .Values.nodeSelector }} {{- with .Values.nodeSelector }}
nodeSelector: nodeSelector:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
@@ -144,7 +95,3 @@ spec:
{{- else }} {{- else }}
emptyDir: {} emptyDir: {}
{{- end }} {{- end }}
{{- if .Values.securityContext.readOnlyRootFilesystem }}
- name: emptydir
emptyDir: {}
{{- end }}
+3 -7
View File
@@ -1,5 +1,5 @@
{{- if .Values.autoscaling.enabled }} {{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2 apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler kind: HorizontalPodAutoscaler
metadata: metadata:
name: {{ include "planka.fullname" . }} name: {{ include "planka.fullname" . }}
@@ -17,16 +17,12 @@ spec:
- type: Resource - type: Resource
resource: resource:
name: cpu name: cpu
target: targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }} {{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource - type: Resource
resource: resource:
name: memory name: memory
target: targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }} {{- end }}
{{- end }} {{- end }}
-17
View File
@@ -1,17 +0,0 @@
{{- if .Values.oidc.enabled }}
{{- if eq (and (not (empty .Values.oidc.clientId)) (not (empty .Values.oidc.clientSecret))) (not (empty .Values.oidc.existingSecret)) -}}
{{- fail "Either specify inline `clientId` and `clientSecret` or refer to them via `existingSecret`" -}}
{{- end }}
{{- if (and (and (not (empty .Values.oidc.clientId)) (not (empty .Values.oidc.clientSecret))) (empty .Values.oidc.existingSecret)) -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "planka.fullname" . }}-oidc
labels:
{{- include "planka.labels" . | nindent 4 }}
type: Opaque
data:
clientId: {{ .Values.oidc.clientId | b64enc | quote }}
clientSecret: {{ .Values.oidc.clientSecret | b64enc | quote }}
{{- end }}
{{- end }}
+1 -72
View File
@@ -46,10 +46,6 @@ securityContext: {}
service: service:
type: ClusterIP type: ClusterIP
port: 1337 port: 1337
## @param service.containerPort Planka HTTP container port
## If empty will default to 1337
##
containerPort: 1337
ingress: ingress:
enabled: false enabled: false
@@ -105,78 +101,11 @@ postgresql:
serviceBindings: serviceBindings:
enabled: true enabled: true
## Set this if you disable the built-in postgresql deployment
dburl:
## PVC-based data storage configuration ## PVC-based data storage configuration
persistence: persistence:
enabled: false enabled: false
# existingClaim: netbox-data # existingClaim: netbox-data
# storageClass: "-" # storageClass: "-"
accessMode: ReadWriteOnce accessMode: ReadWriteOnce
size: 10Gi size: 10Gi
## OpenID Identity Management configuration
##
## Example:
## ---------------
## oidc:
## enabled: true
## clientId: sxxaAIAxVXlCxTmc1YLHBbQr8NL8MqLI2DUbt42d
## clientSecret: om4RTMRVHRszU7bqxB7RZNkHIzA8e4sGYWxeCwIMYQXPwEBWe4SY5a0wwCe9ltB3zrq5f0dnFnp34cEHD7QSMHsKvV9AiV5Z7eqDraMnv0I8IFivmuV5wovAECAYreSI
## issuerUrl: https://auth.local/application/o/planka/
## admin:
## roles:
## - planka-admin
##
## ---------------
## NOTE: A minimal configuration requires setting `clientId`, `clientSecret` and `issuerUrl`. (plus `admin.roles` for administrators)
## ref: https://docs.planka.cloud/docs/Configuration/OIDC
##
oidc:
## @param oidc.enabled Enable single sign-on (SSO) with OpenID Connect (OIDC)
##
enabled: false
## OIDC credentials
## @param oidc.clientId A string unique to the provider that identifies your app.
## @param oidc.clientSecret A secret string that the provider uses to confirm ownership of a client ID.
##
## NOTE: Either specify inline `clientId` and `clientSecret` or refer to them via `existingSecret`
##
clientId: ""
clientSecret: ""
## @param oidc.existingSecret Name of an existing secret containing OIDC credentials
## NOTE: Must contain key `clientId` and `clientSecret`
## NOTE: When it's set, the `clientId` and `clientSecret` parameters are ignored
##
existingSecret: ""
## @param oidc.issuerUrl The OpenID connect metadata document endpoint
##
issuerUrl: ""
## @param oidc.scopes A list of scopes required for OIDC client.
## If empty will default to `openid`, `profile` and `email`
## NOTE: Planka needs the email and name claims
##
scopes: []
## Admin permissions configuration
admin:
## @param oidc.admin.ignoreRoles If set to true, the admin roles will be ignored.
## It is useful if you want to use OIDC for authentication but not for authorization.
## If empty will default to `false`
##
ignoreRoles: false
## @param oidc.admin.rolesAttribute The name of a custom group claim that you have configured in your OIDC provider
## If empty will default to `groups`
##
rolesAttribute: groups
## @param oidc.admin.roles The names of the admin groups
##
roles: []
# - planka-admin
+1 -1
View File
@@ -1 +1 @@
REACT_APP_VERSION=1.17.2 REACT_APP_VERSION=1.12.0
+22036 -9915
View File
File diff suppressed because it is too large Load Diff
+31 -39
View File
@@ -49,74 +49,66 @@
"**/*.test.js" "**/*.test.js"
] ]
} }
],
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
] ]
} }
}, },
"dependencies": { "dependencies": {
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"classnames": "^2.5.1", "classnames": "^2.3.2",
"date-fns": "^2.30.0", "date-fns": "^2.29.3",
"dequal": "^2.0.3", "dequal": "^2.0.3",
"easymde": "^2.18.0", "easymde": "^2.18.0",
"history": "^5.3.0", "history": "^5.3.0",
"i18next": "^23.11.2", "i18next": "^22.0.6",
"i18next-browser-languagedetector": "^7.2.1", "i18next-browser-languagedetector": "^7.0.1",
"initials": "^3.1.2", "initials": "^3.1.2",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.1",
"jwt-decode": "^4.0.0", "jwt-decode": "^3.1.2",
"linkify-react": "^4.1.3",
"linkifyjs": "^4.1.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"nanoid": "^5.0.7", "node-sass": "^8.0.0",
"node-sass": "^9.0.0", "photoswipe": "^5.3.3",
"photoswipe": "^5.4.3",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-app-rewired": "^2.2.1", "react-app-rewired": "^2.2.1",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
"react-datepicker": "^4.25.0", "react-datepicker": "^4.8.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-i18next": "^13.5.0", "react-i18next": "^12.0.0",
"react-input-mask": "^2.0.4", "react-input-mask": "^2.0.4",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.3",
"react-photoswipe-gallery": "^2.2.7", "react-oidc-context": "^2.2.2",
"react-redux": "^8.1.3", "react-photoswipe-gallery": "^2.2.2",
"react-router-dom": "^6.22.3", "react-redux": "^8.0.5",
"react-router-dom": "^6.4.3",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-simplemde-editor": "^5.2.0", "react-simplemde-editor": "^5.2.0",
"react-textarea-autosize": "^8.5.3", "react-textarea-autosize": "^8.4.0",
"redux": "^4.2.1", "redux": "^4.2.0",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-orm": "^0.16.2", "redux-orm": "^0.16.2",
"redux-saga": "^1.3.0", "redux-saga": "^1.2.1",
"remark-breaks": "^4.0.0", "remark-breaks": "^3.0.2",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"reselect": "^4.1.8", "reselect": "^4.1.7",
"sails.io.js": "^1.2.1", "sails.io.js": "^1.2.1",
"semantic-ui-react": "^2.1.5", "semantic-ui-react": "^2.1.3",
"socket.io-client": "^2.5.0", "socket.io-client": "^2.5.0",
"validator": "^13.11.0", "validator": "^13.7.0",
"whatwg-fetch": "^3.6.20", "whatwg-fetch": "^3.6.2",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^6.4.2", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.3.1", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^13.5.0",
"babel-preset-airbnb": "^5.0.0", "babel-preset-airbnb": "^5.0.0",
"chai": "^4.4.1", "chai": "^4.3.7",
"eslint": "^8.57.0", "eslint": "^8.28.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.34.1", "eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"react-test-renderer": "^18.2.0" "react-test-renderer": "^18.2.0"
} }
+1 -43
View File
@@ -23,14 +23,10 @@ createCard.failure = (localId, error) => ({
}, },
}); });
const handleCardCreate = (card, cardMemberships, cardLabels, tasks, attachments) => ({ const handleCardCreate = (card) => ({
type: ActionTypes.CARD_CREATE_HANDLE, type: ActionTypes.CARD_CREATE_HANDLE,
payload: { payload: {
card, card,
cardMemberships,
cardLabels,
tasks,
attachments,
}, },
}); });
@@ -64,34 +60,6 @@ const handleCardUpdate = (card) => ({
}, },
}); });
const duplicateCard = (id, card, taskIds) => ({
type: ActionTypes.CARD_DUPLICATE,
payload: {
id,
card,
taskIds,
},
});
duplicateCard.success = (localId, card, cardMemberships, cardLabels, tasks) => ({
type: ActionTypes.CARD_DUPLICATE__SUCCESS,
payload: {
localId,
card,
cardMemberships,
cardLabels,
tasks,
},
});
duplicateCard.failure = (id, error) => ({
type: ActionTypes.CARD_DUPLICATE__FAILURE,
payload: {
id,
error,
},
});
const deleteCard = (id) => ({ const deleteCard = (id) => ({
type: ActionTypes.CARD_DELETE, type: ActionTypes.CARD_DELETE,
payload: { payload: {
@@ -121,21 +89,11 @@ const handleCardDelete = (card) => ({
}, },
}); });
const filterText = (boardId, text) => ({
type: ActionTypes.TEXT_FILTER_IN_CURRENT_BOARD,
payload: {
boardId,
text,
},
});
export default { export default {
createCard, createCard,
handleCardCreate, handleCardCreate,
updateCard, updateCard,
handleCardUpdate, handleCardUpdate,
duplicateCard,
deleteCard, deleteCard,
handleCardDelete, handleCardDelete,
filterText,
}; };
+2 -12
View File
@@ -39,19 +39,9 @@ const initializeCore = (
}, },
}); });
// TODO: with success? const logout = () => ({
initializeCore.fetchConfig = (config) => ({
type: ActionTypes.CORE_INITIALIZE__CONFIG_FETCH,
payload: {
config,
},
});
const logout = (invalidateAccessToken) => ({
type: ActionTypes.LOGOUT, type: ActionTypes.LOGOUT,
payload: { payload: {},
invalidateAccessToken,
},
}); });
logout.invalidateAccessToken = () => ({ logout.invalidateAccessToken = () => ({
-34
View File
@@ -60,38 +60,6 @@ const handleListUpdate = (list) => ({
}, },
}); });
const sortList = (id, data) => ({
type: ActionTypes.LIST_SORT,
payload: {
id,
data,
},
});
sortList.success = (list, cards) => ({
type: ActionTypes.LIST_SORT__SUCCESS,
payload: {
list,
cards,
},
});
sortList.failure = (id, error) => ({
type: ActionTypes.LIST_SORT__FAILURE,
payload: {
id,
error,
},
});
const handleListSort = (list, cards) => ({
type: ActionTypes.LIST_SORT_HANDLE,
payload: {
list,
cards,
},
});
const deleteList = (id) => ({ const deleteList = (id) => ({
type: ActionTypes.LIST_DELETE, type: ActionTypes.LIST_DELETE,
payload: { payload: {
@@ -126,8 +94,6 @@ export default {
handleListCreate, handleListCreate,
updateList, updateList,
handleListUpdate, handleListUpdate,
sortList,
handleListSort,
deleteList, deleteList,
handleListDelete, handleListDelete,
}; };
-28
View File
@@ -1,12 +1,5 @@
import ActionTypes from '../constants/ActionTypes'; import ActionTypes from '../constants/ActionTypes';
const initializeLogin = (config) => ({
type: ActionTypes.LOGIN_INITIALIZE,
payload: {
config,
},
});
const authenticate = (data) => ({ const authenticate = (data) => ({
type: ActionTypes.AUTHENTICATE, type: ActionTypes.AUTHENTICATE,
payload: { payload: {
@@ -28,33 +21,12 @@ authenticate.failure = (error) => ({
}, },
}); });
const authenticateUsingOidc = () => ({
type: ActionTypes.USING_OIDC_AUTHENTICATE,
payload: {},
});
authenticateUsingOidc.success = (accessToken) => ({
type: ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS,
payload: {
accessToken,
},
});
authenticateUsingOidc.failure = (error) => ({
type: ActionTypes.USING_OIDC_AUTHENTICATE__FAILURE,
payload: {
error,
},
});
const clearAuthenticateError = () => ({ const clearAuthenticateError = () => ({
type: ActionTypes.AUTHENTICATE_ERROR_CLEAR, type: ActionTypes.AUTHENTICATE_ERROR_CLEAR,
payload: {}, payload: {},
}); });
export default { export default {
initializeLogin,
authenticate, authenticate,
authenticateUsingOidc,
clearAuthenticateError, clearAuthenticateError,
}; };
+3 -4
View File
@@ -4,15 +4,14 @@ 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) =>
const exchangeForAccessTokenUsingOidc = (data, headers) => http.post('/access-tokens/exchange', { token: accessToken }, headers);
http.post('/access-tokens/exchange-using-oidc', data, headers);
const deleteCurrentAccessToken = (headers) => const deleteCurrentAccessToken = (headers) =>
socket.delete('/access-tokens/me', undefined, headers); socket.delete('/access-tokens/me', undefined, headers);
export default { export default {
createAccessToken, createAccessToken,
exchangeForAccessTokenUsingOidc,
deleteCurrentAccessToken, deleteCurrentAccessToken,
exchangeOidcToken,
}; };
-7
View File
@@ -57,12 +57,6 @@ const updateCard = (id, data, headers) =>
item: transformCard(body.item), item: transformCard(body.item),
})); }));
const duplicateCard = (id, data, headers) =>
socket.post(`/cards/${id}/duplicate`, data, headers).then((body) => ({
...body,
item: transformCard(body.item),
}));
const deleteCard = (id, headers) => const deleteCard = (id, headers) =>
socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({ socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({
...body, ...body,
@@ -87,7 +81,6 @@ export default {
getCard, getCard,
updateCard, updateCard,
deleteCard, deleteCard,
duplicateCard,
makeHandleCardCreate, makeHandleCardCreate,
makeHandleCardUpdate, makeHandleCardUpdate,
makeHandleCardDelete, makeHandleCardDelete,
+5 -7
View File
@@ -5,15 +5,13 @@ import Config from '../constants/Config';
const http = {}; const http = {};
// TODO: add all methods // TODO: add all methods
['GET', 'POST'].forEach((method) => { ['POST'].forEach((method) => {
http[method.toLowerCase()] = (url, data, headers) => { http[method.toLowerCase()] = (url, data, headers) => {
const formData = const formData = Object.keys(data).reduce((result, key) => {
data && result.append(key, data[key]);
Object.keys(data).reduce((result, key) => {
result.append(key, data[key]);
return result; return result;
}, new FormData()); }, new FormData());
return fetch(`${Config.SERVER_BASE_URL}/api${url}`, { return fetch(`${Config.SERVER_BASE_URL}/api${url}`, {
method, method,
-2
View File
@@ -1,6 +1,5 @@
import http from './http'; import http from './http';
import socket from './socket'; import socket from './socket';
import root from './root';
import accessTokens from './access-tokens'; import accessTokens from './access-tokens';
import users from './users'; import users from './users';
import projects from './projects'; import projects from './projects';
@@ -21,7 +20,6 @@ import notifications from './notifications';
export { http, socket }; export { http, socket };
export default { export default {
...root,
...accessTokens, ...accessTokens,
...users, ...users,
...projects, ...projects,
-24
View File
@@ -1,5 +1,4 @@
import socket from './socket'; import socket from './socket';
import { transformCard } from './cards';
/* Actions */ /* Actions */
@@ -8,33 +7,10 @@ const createList = (boardId, data, headers) =>
const updateList = (id, data, headers) => socket.patch(`/lists/${id}`, data, headers); const updateList = (id, data, headers) => socket.patch(`/lists/${id}`, data, headers);
const sortList = (id, data, headers) =>
socket.post(`/lists/${id}/sort`, data, headers).then((body) => ({
...body,
included: {
...body.included,
cards: body.included.cards.map(transformCard),
},
}));
const deleteList = (id, headers) => socket.delete(`/lists/${id}`, undefined, headers); const deleteList = (id, headers) => socket.delete(`/lists/${id}`, undefined, headers);
/* Event handlers */
const makeHandleListSort = (next) => (body) => {
next({
...body,
included: {
...body.included,
cards: body.included.cards.map(transformCard),
},
});
};
export default { export default {
createList, createList,
updateList, updateList,
sortList,
deleteList, deleteList,
makeHandleListSort,
}; };
-9
View File
@@ -1,9 +0,0 @@
import http from './http';
/* Actions */
const getConfig = (headers) => http.get('/config', undefined, headers);
export default {
getConfig,
};
@@ -13,7 +13,6 @@ const BoardActions = React.memo(
labels, labels,
filterUsers, filterUsers,
filterLabels, filterLabels,
filterText,
allUsers, allUsers,
canEdit, canEdit,
canEditMemberships, canEditMemberships,
@@ -28,7 +27,6 @@ const BoardActions = React.memo(
onLabelUpdate, onLabelUpdate,
onLabelMove, onLabelMove,
onLabelDelete, onLabelDelete,
onTextFilterUpdate,
}) => { }) => {
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
@@ -48,7 +46,6 @@ const BoardActions = React.memo(
<Filters <Filters
users={filterUsers} users={filterUsers}
labels={filterLabels} labels={filterLabels}
filterText={filterText}
allBoardMemberships={memberships} allBoardMemberships={memberships}
allLabels={labels} allLabels={labels}
canEdit={canEdit} canEdit={canEdit}
@@ -60,7 +57,6 @@ const BoardActions = React.memo(
onLabelUpdate={onLabelUpdate} onLabelUpdate={onLabelUpdate}
onLabelMove={onLabelMove} onLabelMove={onLabelMove}
onLabelDelete={onLabelDelete} onLabelDelete={onLabelDelete}
onTextFilterUpdate={onTextFilterUpdate}
/> />
</div> </div>
</div> </div>
@@ -75,7 +71,6 @@ BoardActions.propTypes = {
labels: PropTypes.array.isRequired, labels: PropTypes.array.isRequired,
filterUsers: PropTypes.array.isRequired, filterUsers: PropTypes.array.isRequired,
filterLabels: PropTypes.array.isRequired, filterLabels: PropTypes.array.isRequired,
filterText: PropTypes.string.isRequired,
allUsers: PropTypes.array.isRequired, allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
canEdit: PropTypes.bool.isRequired, canEdit: PropTypes.bool.isRequired,
@@ -91,7 +86,6 @@ BoardActions.propTypes = {
onLabelUpdate: PropTypes.func.isRequired, onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired, onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired, onLabelDelete: PropTypes.func.isRequired,
onTextFilterUpdate: PropTypes.func.isRequired,
}; };
export default BoardActions; export default BoardActions;
@@ -1,7 +1,5 @@
:global(#app) { :global(#app) {
.action { .action {
align-items: center;
display: flex;
flex: 0 0 auto; flex: 0 0 auto;
margin-right: 20px; margin-right: 20px;
} }
+1 -65
View File
@@ -1,10 +1,7 @@
import React, { useCallback, useRef, useState } from 'react'; import React, { useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon } from 'semantic-ui-react';
import { usePopup } from '../../lib/popup'; import { usePopup } from '../../lib/popup';
import { Input } from '../../lib/custom-ui';
import User from '../User'; import User from '../User';
import Label from '../Label'; import Label from '../Label';
@@ -17,7 +14,6 @@ const Filters = React.memo(
({ ({
users, users,
labels, labels,
filterText,
allBoardMemberships, allBoardMemberships,
allLabels, allLabels,
canEdit, canEdit,
@@ -29,17 +25,8 @@ const Filters = React.memo(
onLabelUpdate, onLabelUpdate,
onLabelMove, onLabelMove,
onLabelDelete, onLabelDelete,
onTextFilterUpdate,
}) => { }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [isSearchFocused, setIsSearchFocused] = useState(false);
const searchFieldRef = useRef(null);
const cancelSearch = useCallback(() => {
onTextFilterUpdate('');
searchFieldRef.current.blur();
}, [onTextFilterUpdate]);
const handleRemoveUserClick = useCallback( const handleRemoveUserClick = useCallback(
(id) => { (id) => {
@@ -55,39 +42,9 @@ const Filters = React.memo(
[onLabelRemove], [onLabelRemove],
); );
const handleSearchChange = useCallback(
(_, { value }) => {
onTextFilterUpdate(value);
},
[onTextFilterUpdate],
);
const handleSearchFocus = useCallback(() => {
setIsSearchFocused(true);
}, []);
const handleSearchKeyDown = useCallback(
(event) => {
if (event.key === 'Escape') {
cancelSearch();
}
},
[cancelSearch],
);
const handleSearchBlur = useCallback(() => {
setIsSearchFocused(false);
}, []);
const handleCancelSearchClick = useCallback(() => {
cancelSearch();
}, [cancelSearch]);
const BoardMembershipsPopup = usePopup(BoardMembershipsStep); const BoardMembershipsPopup = usePopup(BoardMembershipsStep);
const LabelsPopup = usePopup(LabelsStep); const LabelsPopup = usePopup(LabelsStep);
const isSearchActive = filterText || isSearchFocused;
return ( return (
<> <>
<span className={styles.filter}> <span className={styles.filter}>
@@ -143,25 +100,6 @@ const Filters = React.memo(
</span> </span>
))} ))}
</span> </span>
<span className={styles.filter}>
<Input
ref={searchFieldRef}
value={filterText}
placeholder={t('common.searchCards')}
icon={
isSearchActive ? (
<Icon link name="cancel" onClick={handleCancelSearchClick} />
) : (
'search'
)
}
className={classNames(styles.search, !isSearchActive && styles.searchInactive)}
onFocus={handleSearchFocus}
onKeyDown={handleSearchKeyDown}
onChange={handleSearchChange}
onBlur={handleSearchBlur}
/>
</span>
</> </>
); );
}, },
@@ -171,7 +109,6 @@ Filters.propTypes = {
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired, users: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired, labels: PropTypes.array.isRequired,
filterText: PropTypes.string.isRequired,
allBoardMemberships: PropTypes.array.isRequired, allBoardMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired, allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
@@ -184,7 +121,6 @@ Filters.propTypes = {
onLabelUpdate: PropTypes.func.isRequired, onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired, onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired, onLabelDelete: PropTypes.func.isRequired,
onTextFilterUpdate: PropTypes.func.isRequired,
}; };
export default Filters; export default Filters;
@@ -43,32 +43,4 @@
line-height: 20px; line-height: 20px;
padding: 2px 12px; padding: 2px 12px;
} }
.search {
height: 30px;
margin: 0 12px;
transition: width 0.2s ease;
width: 280px;
input {
font-size: 13px;
}
}
.searchInactive {
color: #fff;
height: 24px;
width: 220px;
input {
background: rgba(0, 0, 0, 0.24);
border: none;
color: #fff !important;
font-size: 12px;
&::placeholder {
color: #fff;
}
}
}
} }
@@ -4,6 +4,7 @@
max-height: 60vh; max-height: 60vh;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin;
width: 100%; width: 100%;
&::-webkit-scrollbar { &::-webkit-scrollbar {
@@ -85,6 +85,7 @@
height: 56px; height: 56px;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
scrollbar-width: thin;
&:hover { &:hover {
height: 38px; height: 38px;
@@ -36,7 +36,6 @@ const ActionsStep = React.memo(
onUpdate, onUpdate,
onMove, onMove,
onTransfer, onTransfer,
onDuplicate,
onDelete, onDelete,
onUserAdd, onUserAdd,
onUserRemove, onUserRemove,
@@ -77,11 +76,6 @@ const ActionsStep = React.memo(
openStep(StepTypes.MOVE); openStep(StepTypes.MOVE);
}, [openStep]); }, [openStep]);
const handleDuplicateClick = useCallback(() => {
onDuplicate();
onClose();
}, [onDuplicate, onClose]);
const handleDeleteClick = useCallback(() => { const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE); openStep(StepTypes.DELETE);
}, [openStep]); }, [openStep]);
@@ -213,11 +207,6 @@ const ActionsStep = React.memo(
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDuplicateClick}>
{t('action.duplicateCard', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}> <Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteCard', { {t('action.deleteCard', {
context: 'title', context: 'title',
@@ -243,7 +232,6 @@ ActionsStep.propTypes = {
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired, onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired, onTransfer: PropTypes.func.isRequired,
onDuplicate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired, onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired, onUserRemove: PropTypes.func.isRequired,
-3
View File
@@ -41,7 +41,6 @@ const Card = React.memo(
onUpdate, onUpdate,
onMove, onMove,
onTransfer, onTransfer,
onDuplicate,
onDelete, onDelete,
onUserAdd, onUserAdd,
onUserRemove, onUserRemove,
@@ -186,7 +185,6 @@ const Card = React.memo(
onUpdate={onUpdate} onUpdate={onUpdate}
onMove={onMove} onMove={onMove}
onTransfer={onTransfer} onTransfer={onTransfer}
onDuplicate={onDuplicate}
onDelete={onDelete} onDelete={onDelete}
onUserAdd={onUserAdd} onUserAdd={onUserAdd}
onUserRemove={onUserRemove} onUserRemove={onUserRemove}
@@ -240,7 +238,6 @@ Card.propTypes = {
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired, onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired, onTransfer: PropTypes.func.isRequired,
onDuplicate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired, onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired, onUserRemove: PropTypes.func.isRequired,
+1 -3
View File
@@ -4,8 +4,6 @@ import classNames from 'classnames';
import { Progress } from 'semantic-ui-react'; import { Progress } from 'semantic-ui-react';
import { useToggle } from '../../lib/hooks'; import { useToggle } from '../../lib/hooks';
import Linkify from '../Linkify';
import styles from './Tasks.module.scss'; import styles from './Tasks.module.scss';
const Tasks = React.memo(({ items }) => { const Tasks = React.memo(({ items }) => {
@@ -50,7 +48,7 @@ const Tasks = React.memo(({ items }) => {
key={item.id} key={item.id}
className={classNames(styles.task, item.isCompleted && styles.taskCompleted)} className={classNames(styles.task, item.isCompleted && styles.taskCompleted)}
> >
<Linkify linkStopPropagation>{item.name}</Linkify> {item.name}
</li> </li>
))} ))}
</ul> </ul>
@@ -55,10 +55,8 @@
display: block; display: block;
font-size: 12px; font-size: 12px;
line-height: 14px; line-height: 14px;
overflow: hidden;
padding-bottom: 6px; padding-bottom: 6px;
padding-left: 14px; padding-left: 14px;
text-overflow: ellipsis;
&:before { &:before {
content: "–"; content: "–";
@@ -4,7 +4,6 @@ import classNames from 'classnames';
import { useTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';
import { Comment } from 'semantic-ui-react'; import { Comment } from 'semantic-ui-react';
import getDateFormat from '../../../utils/get-date-format';
import { ActivityTypes } from '../../../constants/Enums'; import { ActivityTypes } from '../../../constants/Enums';
import ItemComment from './ItemComment'; import ItemComment from './ItemComment';
import User from '../../User'; import User from '../../User';
@@ -67,7 +66,7 @@ const Item = React.memo(({ type, data, createdAt, user }) => {
<div className={classNames(styles.content)}> <div className={classNames(styles.content)}>
<div>{contentNode}</div> <div>{contentNode}</div>
<span className={styles.date}> <span className={styles.date}>
{t(`format:${getDateFormat(createdAt)}`, { {t('format:longDateTime', {
postProcess: 'formatDate', postProcess: 'formatDate',
value: createdAt, value: createdAt,
})} })}
@@ -6,7 +6,6 @@ import { Comment } from 'semantic-ui-react';
import { usePopup } from '../../../lib/popup'; import { usePopup } from '../../../lib/popup';
import { Markdown } from '../../../lib/custom-ui'; import { Markdown } from '../../../lib/custom-ui';
import getDateFormat from '../../../utils/get-date-format';
import CommentEdit from './CommentEdit'; import CommentEdit from './CommentEdit';
import User from '../../User'; import User from '../../User';
import DeleteStep from '../../DeleteStep'; import DeleteStep from '../../DeleteStep';
@@ -34,7 +33,7 @@ const ItemComment = React.memo(
<div className={styles.title}> <div className={styles.title}>
<span className={styles.author}>{user.name}</span> <span className={styles.author}>{user.name}</span>
<span className={styles.date}> <span className={styles.date}>
{t(`format:${getDateFormat(createdAt)}`, { {t('format:longDateTime', {
postProcess: 'formatDate', postProcess: 'formatDate',
value: createdAt, value: createdAt,
})} })}
@@ -94,7 +94,7 @@ const AttachmentAddZone = React.memo(({ children, onCreate }) => {
return ( return (
<> <>
{/* eslint-disable-next-line react/jsx-props-no-spreading */} {/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div {...getRootProps()}> <div {...getRootProps()} className={styles.wrapper}>
{isDragActive && <div className={styles.dropzone}>{t('common.dropFileToUpload')}</div>} {isDragActive && <div className={styles.dropzone}>{t('common.dropFileToUpload')}</div>}
{children} {children}
{/* eslint-disable-next-line react/jsx-props-no-spreading */} {/* eslint-disable-next-line react/jsx-props-no-spreading */}
@@ -12,4 +12,8 @@
width: 100%; width: 100%;
z-index: 2001; z-index: 2001;
} }
.wrapper {
overflow: hidden;
}
} }
+1 -15
View File
@@ -55,7 +55,6 @@ const CardModal = React.memo(
onUpdate, onUpdate,
onMove, onMove,
onTransfer, onTransfer,
onDuplicate,
onDelete, onDelete,
onUserAdd, onUserAdd,
onUserRemove, onUserRemove,
@@ -141,11 +140,6 @@ const CardModal = React.memo(
}); });
}, [isSubscribed, onUpdate]); }, [isSubscribed, onUpdate]);
const handleDuplicateClick = useCallback(() => {
onDuplicate();
onClose();
}, [onDuplicate, onClose]);
const handleGalleryOpen = useCallback(() => { const handleGalleryOpen = useCallback(() => {
isGalleryOpened.current = true; isGalleryOpened.current = true;
}, []); }, []);
@@ -223,7 +217,6 @@ const CardModal = React.memo(
onUserSelect={onUserAdd} onUserSelect={onUserAdd}
onUserDeselect={onUserRemove} onUserDeselect={onUserRemove}
> >
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<button <button
type="button" type="button"
className={classNames(styles.attachment, styles.dueDate)} className={classNames(styles.attachment, styles.dueDate)}
@@ -273,7 +266,6 @@ const CardModal = React.memo(
onMove={onLabelMove} onMove={onLabelMove}
onDelete={onLabelDelete} onDelete={onLabelDelete}
> >
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<button <button
type="button" type="button"
className={classNames(styles.attachment, styles.dueDate)} className={classNames(styles.attachment, styles.dueDate)}
@@ -322,11 +314,10 @@ const CardModal = React.memo(
)} )}
</span> </span>
{canEdit && ( {canEdit && (
// eslint-disable-next-line jsx-a11y/control-has-associated-label
<button <button
onClick={handleToggleStopwatchClick}
type="button" type="button"
className={classNames(styles.attachment, styles.dueDate)} className={classNames(styles.attachment, styles.dueDate)}
onClick={handleToggleStopwatchClick}
> >
<Icon <Icon
name={stopwatch.startedAt ? 'pause' : 'play'} name={stopwatch.startedAt ? 'pause' : 'play'}
@@ -502,10 +493,6 @@ const CardModal = React.memo(
{t('action.move')} {t('action.move')}
</Button> </Button>
</CardMovePopup> </CardMovePopup>
<Button fluid className={styles.actionButton} onClick={handleDuplicateClick}>
<Icon name="copy outline" className={styles.actionIcon} />
{t('action.duplicate')}
</Button>
<DeletePopup <DeletePopup
title="common.deleteCard" title="common.deleteCard"
content="common.areYouSureYouWantToDeleteThisCard" content="common.areYouSureYouWantToDeleteThisCard"
@@ -565,7 +552,6 @@ CardModal.propTypes = {
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired, onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired, onTransfer: PropTypes.func.isRequired,
onDuplicate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired, onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired, onUserRemove: PropTypes.func.isRequired,
@@ -8,7 +8,6 @@ import { usePopup } from '../../../lib/popup';
import NameEdit from './NameEdit'; import NameEdit from './NameEdit';
import ActionsStep from './ActionsStep'; import ActionsStep from './ActionsStep';
import Linkify from '../../Linkify';
import styles from './Item.module.scss'; import styles from './Item.module.scss';
@@ -66,7 +65,7 @@ const Item = React.memo(
onClick={handleClick} onClick={handleClick}
> >
<span className={classNames(styles.task, isCompleted && styles.taskCompleted)}> <span className={classNames(styles.task, isCompleted && styles.taskCompleted)}>
<Linkify linkStopPropagation>{name}</Linkify> {name}
</span> </span>
</span> </span>
{isPersisted && canEdit && ( {isPersisted && canEdit && (
@@ -48,21 +48,14 @@ const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onMove, onDelete
return ( return (
<> <>
{items.length > 0 && ( {items.length > 0 && (
<> <Progress
<span className={styles.progressWrapper}> autoSuccess
<Progress value={completedItems.length}
autoSuccess total={items.length}
value={completedItems.length} color="blue"
total={items.length} size="tiny"
color="blue" className={styles.progress}
size="tiny" />
className={styles.progress}
/>
</span>
<span className={styles.count}>
{completedItems.length}/{items.length}
</span>
</>
)} )}
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}> <DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="tasks" type={DroppableTypes.TASK}> <Droppable droppableId="tasks" type={DroppableTypes.TASK}>
@@ -3,23 +3,6 @@
margin: 0 0 16px; margin: 0 0 16px;
} }
.progressWrapper {
display: inline-block;
padding: 3px 0;
vertical-align: top;
width: calc(100% - 50px);
}
.count {
color: #8c8c8c;
display: inline-block;
font-size: 14px;
line-height: 14px;
text-align: right;
vertical-align: top;
width: 50px;
}
.taskButton { .taskButton {
background: transparent; background: transparent;
border: none; border: none;
+2 -16
View File
@@ -4,8 +4,6 @@ import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import getDateFormat from '../../utils/get-date-format';
import styles from './DueDate.module.scss'; import styles from './DueDate.module.scss';
const SIZES = { const SIZES = {
@@ -14,27 +12,15 @@ const SIZES = {
MEDIUM: 'medium', MEDIUM: 'medium',
}; };
const LONG_DATE_FORMAT_BY_SIZE = { const FORMATS = {
tiny: 'longDate', tiny: 'longDate',
small: 'longDate', small: 'longDate',
medium: 'longDateTime', medium: 'longDateTime',
}; };
const FULL_DATE_FORMAT_BY_SIZE = {
tiny: 'fullDate',
small: 'fullDate',
medium: 'fullDateTime',
};
const DueDate = React.memo(({ value, size, isDisabled, onClick }) => { const DueDate = React.memo(({ value, size, isDisabled, onClick }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const dateFormat = getDateFormat(
value,
LONG_DATE_FORMAT_BY_SIZE[size],
FULL_DATE_FORMAT_BY_SIZE[size],
);
const contentNode = ( const contentNode = (
<span <span
className={classNames( className={classNames(
@@ -43,7 +29,7 @@ const DueDate = React.memo(({ value, size, isDisabled, onClick }) => {
onClick && styles.wrapperHoverable, onClick && styles.wrapperHoverable,
)} )}
> >
{t(`format:${dateFormat}`, { {t(`format:${FORMATS[size]}`, {
value, value,
postProcess: 'formatDate', postProcess: 'formatDate',
})} })}
+9 -4
View File
@@ -3,11 +3,11 @@ 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 { Button, 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';
import NotificationsStep from './NotificationsStep'; import NotificationsStep from './NotificationsStep';
import User from '../User';
import UserStep from '../UserStep'; import UserStep from '../UserStep';
import styles from './Header.module.scss'; import styles from './Header.module.scss';
@@ -30,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();
@@ -39,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 && (
@@ -89,11 +95,10 @@ 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)}>
<span className={styles.userName}>{user.name}</span> {user.name}
<User name={user.name} avatarUrl={user.avatarUrl} size="small" />
</Menu.Item> </Menu.Item>
</UserPopup> </UserPopup>
</Menu.Menu> </Menu.Menu>
@@ -86,10 +86,6 @@
font-weight: bold; font-weight: bold;
} }
.userName {
margin-right: 10px;
}
.wrapper { .wrapper {
background: rgba(0, 0, 0, 0.24); background: rgba(0, 0, 0, 0.24);
display: flex; display: flex;
@@ -49,6 +49,7 @@
max-height: 60vh; max-height: 60vh;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 5px; width: 5px;
@@ -25,6 +25,7 @@
max-height: 60vh; max-height: 60vh;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 5px; width: 5px;
-68
View File
@@ -1,68 +0,0 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import LinkifyReact from 'linkify-react';
import history from '../history';
const Linkify = React.memo(({ children, linkStopPropagation, ...props }) => {
const handleLinkClick = useCallback(
(event) => {
if (linkStopPropagation) {
event.stopPropagation();
}
if (!event.target.getAttribute('target')) {
event.preventDefault();
history.push(event.target.href);
}
},
[linkStopPropagation],
);
const linkRenderer = useCallback(
({ attributes: { href, ...linkProps }, content }) => {
let url;
try {
url = new URL(href, window.location);
} catch (error) {} // eslint-disable-line no-empty
const isSameSite = !!url && url.origin === window.location.origin;
return (
<a
{...linkProps} // eslint-disable-line react/jsx-props-no-spreading
href={href}
target={isSameSite ? undefined : '_blank'}
rel={isSameSite ? undefined : 'noreferrer'}
onClick={handleLinkClick}
>
{isSameSite ? url.pathname : content}
</a>
);
},
[handleLinkClick],
);
return (
<LinkifyReact
{...props} // eslint-disable-line react/jsx-props-no-spreading
options={{
defaultProtocol: 'https',
render: linkRenderer,
}}
>
{children}
</LinkifyReact>
);
});
Linkify.propTypes = {
children: PropTypes.string.isRequired,
linkStopPropagation: PropTypes.bool,
};
Linkify.defaultProps = {
linkStopPropagation: false,
};
export default Linkify;
+11 -40
View File
@@ -5,17 +5,15 @@ import { Menu } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui'; import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks'; import { useSteps } from '../../hooks';
import ListSortStep from '../ListSortStep';
import DeleteStep from '../DeleteStep'; import DeleteStep from '../DeleteStep';
import styles from './ActionsStep.module.scss'; import styles from './ActionsStep.module.scss';
const StepTypes = { const StepTypes = {
DELETE: 'DELETE', DELETE: 'DELETE',
SORT: 'SORT',
}; };
const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onSort, onDelete, onClose }) => { const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps(); const [step, openStep, handleBack] = useSteps();
@@ -29,41 +27,20 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onSort, onDelete, onClo
onClose(); onClose();
}, [onCardAdd, onClose]); }, [onCardAdd, onClose]);
const handleSortClick = useCallback(() => {
openStep(StepTypes.SORT);
}, [openStep]);
const handleDeleteClick = useCallback(() => { const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE); openStep(StepTypes.DELETE);
}, [openStep]); }, [openStep]);
const handleSortTypeSelect = useCallback( if (step && step.type === StepTypes.DELETE) {
(type) => { return (
onSort({ <DeleteStep
type, title="common.deleteList"
}); content="common.areYouSureYouWantToDeleteThisList"
buttonContent="action.deleteList"
onClose(); onConfirm={onDelete}
}, onBack={handleBack}
[onSort, onClose], />
); );
if (step && step.type) {
switch (step.type) {
case StepTypes.SORT:
return <ListSortStep onTypeSelect={handleSortTypeSelect} onBack={handleBack} />;
case StepTypes.DELETE:
return (
<DeleteStep
title="common.deleteList"
content="common.areYouSureYouWantToDeleteThisList"
buttonContent="action.deleteList"
onConfirm={onDelete}
onBack={handleBack}
/>
);
default:
}
} }
return ( return (
@@ -85,11 +62,6 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onSort, onDelete, onClo
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleSortClick}>
{t('action.sortList', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}> <Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteList', { {t('action.deleteList', {
context: 'title', context: 'title',
@@ -105,7 +77,6 @@ ActionsStep.propTypes = {
onNameEdit: PropTypes.func.isRequired, onNameEdit: PropTypes.func.isRequired,
onCardAdd: PropTypes.func.isRequired, onCardAdd: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
+1 -14
View File
@@ -16,18 +16,7 @@ import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-ic
import styles from './List.module.scss'; import styles from './List.module.scss';
const List = React.memo( const List = React.memo(
({ ({ id, index, name, isPersisted, cardIds, canEdit, onUpdate, onDelete, onCardCreate }) => {
id,
index,
name,
isPersisted,
cardIds,
canEdit,
onUpdate,
onDelete,
onSort,
onCardCreate,
}) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [isAddCardOpened, setIsAddCardOpened] = useState(false); const [isAddCardOpened, setIsAddCardOpened] = useState(false);
@@ -125,7 +114,6 @@ const List = React.memo(
onNameEdit={handleNameEdit} onNameEdit={handleNameEdit}
onCardAdd={handleCardAdd} onCardAdd={handleCardAdd}
onDelete={onDelete} onDelete={onDelete}
onSort={onSort}
> >
<Button className={classNames(styles.headerButton, styles.target)}> <Button className={classNames(styles.headerButton, styles.target)}>
<Icon fitted name="pencil" size="small" /> <Icon fitted name="pencil" size="small" />
@@ -171,7 +159,6 @@ List.propTypes = {
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool.isRequired, canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onCardCreate: PropTypes.func.isRequired, onCardCreate: PropTypes.func.isRequired,
}; };
@@ -43,6 +43,7 @@
max-height: calc(100vh - 268px); max-height: calc(100vh - 268px);
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin;
width: 290px; width: 290px;
&:hover { &:hover {
@@ -1,61 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { ListSortTypes } from '../../constants/Enums';
import styles from './ListSortStep.module.scss';
const ListSortStep = React.memo(({ onTypeSelect, onBack }) => {
const [t] = useTranslation();
return (
<>
<Popup.Header onBack={onBack}>
{t('common.sortList', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item
className={styles.menuItem}
onClick={() => onTypeSelect(ListSortTypes.NAME_ASC)}
>
{t('common.title')}
</Menu.Item>
<Menu.Item
className={styles.menuItem}
onClick={() => onTypeSelect(ListSortTypes.DUE_DATE_ASC)}
>
{t('common.dueDate')}
</Menu.Item>
<Menu.Item
className={styles.menuItem}
onClick={() => onTypeSelect(ListSortTypes.CREATED_AT_ASC)}
>
{t('common.oldestFirst')}
</Menu.Item>
<Menu.Item
className={styles.menuItem}
onClick={() => onTypeSelect(ListSortTypes.CREATED_AT_DESC)}
>
{t('common.newestFirst')}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
});
ListSortStep.propTypes = {
onTypeSelect: PropTypes.func.isRequired,
onBack: PropTypes.func,
};
ListSortStep.defaultProps = {
onBack: undefined,
};
export default ListSortStep;
@@ -1,11 +0,0 @@
:global(#app) {
.menu {
margin: -7px -12px -5px;
width: calc(100% + 24px);
}
.menuItem {
margin: 0;
padding-left: 14px;
}
}
@@ -1,3 +0,0 @@
import ListSortStep from './ListSortStep';
export default ListSortStep;
+44 -88
View File
@@ -1,9 +1,10 @@
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';
import { Button, Form, Grid, Header, Message } from 'semantic-ui-react'; import { Form, Grid, Header, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks'; import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
import { Input } from '../../lib/custom-ui'; import { Input } from '../../lib/custom-ui';
@@ -28,21 +29,6 @@ const createMessage = (error) => {
type: 'error', type: 'error',
content: 'common.invalidPassword', content: 'common.invalidPassword',
}; };
case 'Use single sign-on':
return {
type: 'error',
content: 'common.useSingleSignOn',
};
case 'Email already in use':
return {
type: 'error',
content: 'common.emailAlreadyInUse',
};
case 'Username already in use':
return {
type: 'error',
content: 'common.usernameAlreadyInUse',
};
case 'Failed to fetch': case 'Failed to fetch':
return { return {
type: 'warning', type: 'warning',
@@ -62,17 +48,8 @@ const createMessage = (error) => {
}; };
const Login = React.memo( const Login = React.memo(
({ ({ defaultData, isSubmitting, error, onAuthenticate, onMessageDismiss }) => {
defaultData, const auth = useAuth();
isSubmitting,
isSubmittingUsingOidc,
error,
withOidc,
isOidcEnforced,
onAuthenticate,
onAuthenticateUsingOidc,
onMessageDismiss,
}) => {
const [t] = useTranslation(); const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting); const wasSubmitting = usePrevious(isSubmitting);
@@ -108,10 +85,8 @@ const Login = React.memo(
}, [onAuthenticate, data]); }, [onAuthenticate, data]);
useEffect(() => { useEffect(() => {
if (!isOidcEnforced) { emailOrUsernameField.current.focus();
emailOrUsernameField.current.focus(); }, []);
}
}, [isOidcEnforced]);
useEffect(() => { useEffect(() => {
if (wasSubmitting && !isSubmitting && error) { if (wasSubmitting && !isSubmitting && error) {
@@ -162,58 +137,45 @@ const Login = React.memo(
onDismiss={onMessageDismiss} onDismiss={onMessageDismiss}
/> />
)} )}
{!isOidcEnforced && ( <Form size="large" onSubmit={handleSubmit}>
<Form size="large" onSubmit={handleSubmit}> <div className={styles.inputWrapper}>
<div className={styles.inputWrapper}> <div className={styles.inputLabel}>{t('common.emailOrUsername')}</div>
<div className={styles.inputLabel}>{t('common.emailOrUsername')}</div> <Input
<Input fluid
fluid ref={emailOrUsernameField}
ref={emailOrUsernameField} name="emailOrUsername"
name="emailOrUsername" value={data.emailOrUsername}
value={data.emailOrUsername} readOnly={isSubmitting}
readOnly={isSubmitting} className={styles.input}
className={styles.input} onChange={handleFieldChange}
onChange={handleFieldChange}
/>
</div>
<div className={styles.inputWrapper}>
<div className={styles.inputLabel}>{t('common.password')}</div>
<Input.Password
fluid
ref={passwordField}
name="password"
value={data.password}
readOnly={isSubmitting}
className={styles.input}
onChange={handleFieldChange}
/>
</div>
<Form.Button
primary
size="large"
icon="right arrow"
labelPosition="right"
content={t('action.logIn')}
floated="right"
loading={isSubmitting}
disabled={isSubmitting || isSubmittingUsingOidc}
/> />
</Form> </div>
)} <div className={styles.inputWrapper}>
{withOidc && ( <div className={styles.inputLabel}>{t('common.password')}</div>
<Button <Input.Password
type="button" fluid
fluid={isOidcEnforced} ref={passwordField}
primary={isOidcEnforced} name="password"
size={isOidcEnforced ? 'large' : undefined} value={data.password}
icon={isOidcEnforced ? 'right arrow' : undefined} readOnly={isSubmitting}
labelPosition={isOidcEnforced ? 'right' : undefined} className={styles.input}
content={t('action.logInWithSSO')} onChange={handleFieldChange}
loading={isSubmittingUsingOidc} />
disabled={isSubmitting || isSubmittingUsingOidc} </div>
onClick={onAuthenticateUsingOidc} <Form.Button
primary
size="large"
icon="right arrow"
labelPosition="right"
content={t('action.logIn')}
floated="right"
loading={isSubmitting}
disabled={isSubmitting}
/> />
)} </Form>
<Form.Button type="button" onClick={() => auth.signinRedirect()}>
Log in with SSO
</Form.Button>
</div> </div>
</div> </div>
</Grid.Column> </Grid.Column>
@@ -244,16 +206,10 @@ const Login = React.memo(
); );
Login.propTypes = { Login.propTypes = {
/* eslint-disable react/forbid-prop-types */ defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
defaultData: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */
isSubmitting: PropTypes.bool.isRequired, isSubmitting: PropTypes.bool.isRequired,
isSubmittingUsingOidc: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
withOidc: PropTypes.bool.isRequired,
isOidcEnforced: PropTypes.bool.isRequired,
onAuthenticate: PropTypes.func.isRequired, onAuthenticate: PropTypes.func.isRequired,
onAuthenticateUsingOidc: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired, onMessageDismiss: PropTypes.func.isRequired,
}; };
-19
View File
@@ -1,19 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Loader } from 'semantic-ui-react';
import LoginContainer from '../containers/LoginContainer';
const LoginWrapper = React.memo(({ isInitializing }) => {
if (isInitializing) {
return <Loader active size="massive" />;
}
return <LoginContainer />;
});
LoginWrapper.propTypes = {
isInitializing: PropTypes.bool.isRequired,
};
export default LoginWrapper;
@@ -3,7 +3,6 @@ import React, { useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react'; import { Button } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks'; import { useSteps } from '../../hooks';
import User from '../User'; import User from '../User';
@@ -20,7 +19,6 @@ const ActionsStep = React.memo(
({ ({
membership, membership,
permissionsSelectStep, permissionsSelectStep,
title,
leaveButtonContent, leaveButtonContent,
leaveConfirmationTitle, leaveConfirmationTitle,
leaveConfirmationContent, leaveConfirmationContent,
@@ -33,7 +31,6 @@ const ActionsStep = React.memo(
canLeave, canLeave,
onUpdate, onUpdate,
onDelete, onDelete,
onBack,
onClose, onClose,
}) => { }) => {
const [t] = useTranslation(); const [t] = useTranslation();
@@ -56,11 +53,6 @@ const ActionsStep = React.memo(
[onUpdate], [onUpdate],
); );
const handleDeleteConfirm = useCallback(() => {
onDelete();
onClose();
}, [onDelete, onClose]);
if (step) { if (step) {
switch (step.type) { switch (step.type) {
case StepTypes.EDIT_PERMISSIONS: { case StepTypes.EDIT_PERMISSIONS: {
@@ -89,7 +81,7 @@ const ActionsStep = React.memo(
? leaveConfirmationButtonContent ? leaveConfirmationButtonContent
: deleteConfirmationButtonContent : deleteConfirmationButtonContent
} }
onConfirm={handleDeleteConfirm} onConfirm={onDelete}
onBack={handleBack} onBack={handleBack}
/> />
); );
@@ -97,7 +89,7 @@ const ActionsStep = React.memo(
} }
} }
const contentNode = ( return (
<> <>
<span className={styles.user}> <span className={styles.user}>
<User name={membership.user.name} avatarUrl={membership.user.avatarUrl} size="large" /> <User name={membership.user.name} avatarUrl={membership.user.avatarUrl} size="large" />
@@ -133,26 +125,12 @@ const ActionsStep = React.memo(
)} )}
</> </>
); );
return onBack ? (
<>
<Popup.Header onBack={onBack}>
{t(title, {
context: 'title',
})}
</Popup.Header>
<Popup.Content>{contentNode}</Popup.Content>
</>
) : (
contentNode
);
}, },
); );
ActionsStep.propTypes = { ActionsStep.propTypes = {
membership: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types membership: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
permissionsSelectStep: PropTypes.elementType, permissionsSelectStep: PropTypes.elementType,
title: PropTypes.string,
leaveButtonContent: PropTypes.string, leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string, leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string, leaveConfirmationContent: PropTypes.string,
@@ -165,13 +143,11 @@ ActionsStep.propTypes = {
canLeave: PropTypes.bool.isRequired, canLeave: PropTypes.bool.isRequired,
onUpdate: PropTypes.func, onUpdate: PropTypes.func,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
ActionsStep.defaultProps = { ActionsStep.defaultProps = {
permissionsSelectStep: undefined, permissionsSelectStep: undefined,
title: 'common.memberActions',
leaveButtonContent: 'action.leaveBoard', leaveButtonContent: 'action.leaveBoard',
leaveConfirmationTitle: 'common.leaveBoard', leaveConfirmationTitle: 'common.leaveBoard',
leaveConfirmationContent: 'common.areYouSureYouWantToLeaveBoard', leaveConfirmationContent: 'common.areYouSureYouWantToLeaveBoard',
@@ -181,7 +157,6 @@ ActionsStep.defaultProps = {
deleteConfirmationContent: 'common.areYouSureYouWantToRemoveThisMemberFromBoard', deleteConfirmationContent: 'common.areYouSureYouWantToRemoveThisMemberFromBoard',
deleteConfirmationButtonContent: 'action.removeMember', deleteConfirmationButtonContent: 'action.removeMember',
onUpdate: undefined, onUpdate: undefined,
onBack: undefined,
}; };
export default ActionsStep; export default ActionsStep;
@@ -4,6 +4,7 @@
max-height: 60vh; max-height: 60vh;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 5px; width: 5px;
@@ -5,21 +5,16 @@ import { usePopup } from '../../lib/popup';
import AddStep from './AddStep'; import AddStep from './AddStep';
import ActionsStep from './ActionsStep'; import ActionsStep from './ActionsStep';
import MembershipsStep from './MembershipsStep';
import User from '../User'; import User from '../User';
import styles from './Memberships.module.scss'; import styles from './Memberships.module.scss';
const MAX_MEMBERS = 6;
const Memberships = React.memo( const Memberships = React.memo(
({ ({
items, items,
allUsers, allUsers,
permissionsSelectStep, permissionsSelectStep,
title,
addTitle, addTitle,
actionsTitle,
leaveButtonContent, leaveButtonContent,
leaveConfirmationTitle, leaveConfirmationTitle,
leaveConfirmationContent, leaveConfirmationContent,
@@ -36,14 +31,11 @@ const Memberships = React.memo(
}) => { }) => {
const AddPopup = usePopup(AddStep); const AddPopup = usePopup(AddStep);
const ActionsPopup = usePopup(ActionsStep); const ActionsPopup = usePopup(ActionsStep);
const MembershipsPopup = usePopup(MembershipsStep);
const remainMembersCount = items.length - MAX_MEMBERS;
return ( return (
<> <>
<span className={styles.users}> <span className={styles.users}>
{items.slice(0, MAX_MEMBERS).map((item) => ( {items.map((item) => (
<span key={item.id} className={styles.user}> <span key={item.id} className={styles.user}>
<ActionsPopup <ActionsPopup
membership={item} membership={item}
@@ -71,30 +63,6 @@ const Memberships = React.memo(
</span> </span>
))} ))}
</span> </span>
{remainMembersCount > 0 && (
<MembershipsPopup
items={items}
permissionsSelectStep={permissionsSelectStep}
title={title}
actionsTitle={actionsTitle}
leaveButtonContent={leaveButtonContent}
leaveConfirmationTitle={leaveConfirmationTitle}
leaveConfirmationContent={leaveConfirmationContent}
leaveConfirmationButtonContent={leaveConfirmationButtonContent}
deleteButtonContent={deleteButtonContent}
deleteConfirmationTitle={deleteConfirmationTitle}
deleteConfirmationContent={deleteConfirmationContent}
deleteConfirmationButtonContent={deleteConfirmationButtonContent}
canEdit={canEdit}
canLeave={items.length > 1 || canLeaveIfLast}
onUpdate={onUpdate}
onDelete={onDelete}
>
<Button icon className={styles.addUser}>
+{remainMembersCount < 99 ? remainMembersCount : 99}
</Button>
</MembershipsPopup>
)}
{canEdit && ( {canEdit && (
<AddPopup <AddPopup
users={allUsers} users={allUsers}
@@ -117,9 +85,7 @@ Memberships.propTypes = {
allUsers: PropTypes.array.isRequired, allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
permissionsSelectStep: PropTypes.elementType, permissionsSelectStep: PropTypes.elementType,
title: PropTypes.string,
addTitle: PropTypes.string, addTitle: PropTypes.string,
actionsTitle: PropTypes.string,
leaveButtonContent: PropTypes.string, leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string, leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string, leaveConfirmationContent: PropTypes.string,
@@ -137,9 +103,7 @@ Memberships.propTypes = {
Memberships.defaultProps = { Memberships.defaultProps = {
permissionsSelectStep: undefined, permissionsSelectStep: undefined,
title: undefined,
addTitle: undefined, addTitle: undefined,
actionsTitle: undefined,
leaveButtonContent: undefined, leaveButtonContent: undefined,
leaveConfirmationTitle: undefined, leaveConfirmationTitle: undefined,
leaveConfirmationContent: undefined, leaveConfirmationContent: undefined,
@@ -1,121 +0,0 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useSteps } from '../../hooks';
import ActionsStep from './ActionsStep';
import BoardMembershipsStep from '../BoardMembershipsStep';
const StepTypes = {
EDIT: 'EDIT',
};
const MembershipsStep = React.memo(
({
items,
permissionsSelectStep,
title,
actionsTitle,
leaveButtonContent,
leaveConfirmationTitle,
leaveConfirmationContent,
leaveConfirmationButtonContent,
deleteButtonContent,
deleteConfirmationTitle,
deleteConfirmationContent,
deleteConfirmationButtonContent,
canEdit,
canLeave,
onUpdate,
onDelete,
onClose,
}) => {
const [step, openStep, handleBack] = useSteps();
const handleUserSelect = useCallback(
(userId) => {
openStep(StepTypes.EDIT, {
userId,
});
},
[openStep],
);
if (step && step.type === StepTypes.EDIT) {
const currentItem = items.find((item) => item.userId === step.params.userId);
if (currentItem) {
return (
<ActionsStep
membership={currentItem}
permissionsSelectStep={permissionsSelectStep}
title={actionsTitle}
leaveButtonContent={leaveButtonContent}
leaveConfirmationTitle={leaveConfirmationTitle}
leaveConfirmationContent={leaveConfirmationContent}
leaveConfirmationButtonContent={leaveConfirmationButtonContent}
deleteButtonContent={deleteButtonContent}
deleteConfirmationTitle={deleteConfirmationTitle}
deleteConfirmationContent={deleteConfirmationContent}
deleteConfirmationButtonContent={deleteConfirmationButtonContent}
canEdit={canEdit}
canLeave={canLeave}
onUpdate={(data) => onUpdate(currentItem.id, data)}
onDelete={() => onDelete(currentItem.id)}
onBack={handleBack}
onClose={onClose}
/>
);
}
openStep(null);
}
return (
// FIXME: hack
<BoardMembershipsStep
items={items}
currentUserIds={[]}
title={title}
onUserSelect={handleUserSelect}
onUserDeselect={() => {}}
/>
);
},
);
MembershipsStep.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
permissionsSelectStep: PropTypes.elementType,
title: PropTypes.string,
actionsTitle: PropTypes.string,
leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string,
leaveConfirmationButtonContent: PropTypes.string,
deleteButtonContent: PropTypes.string,
deleteConfirmationTitle: PropTypes.string,
deleteConfirmationContent: PropTypes.string,
deleteConfirmationButtonContent: PropTypes.string,
canEdit: PropTypes.bool.isRequired,
canLeave: PropTypes.bool.isRequired,
onUpdate: PropTypes.func,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
MembershipsStep.defaultProps = {
permissionsSelectStep: undefined,
title: undefined,
actionsTitle: undefined,
leaveButtonContent: undefined,
leaveConfirmationTitle: undefined,
leaveConfirmationContent: undefined,
leaveConfirmationButtonContent: undefined,
deleteButtonContent: undefined,
deleteConfirmationTitle: undefined,
deleteConfirmationContent: undefined,
deleteConfirmationButtonContent: undefined,
onUpdate: undefined,
};
export default MembershipsStep;
+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;
@@ -12,9 +12,7 @@ const ManagersPane = React.memo(({ items, allUsers, onCreate, onDelete }) => {
<Memberships <Memberships
items={items} items={items}
allUsers={allUsers} allUsers={allUsers}
title="common.managers"
addTitle="common.addManager" addTitle="common.addManager"
actionsTitle="common.managerActions"
leaveButtonContent="action.leaveProject" leaveButtonContent="action.leaveProject"
leaveConfirmationTitle="common.leaveProject" leaveConfirmationTitle="common.leaveProject"
leaveConfirmationContent="common.areYouSureYouWantToLeaveProject" leaveConfirmationContent="common.areYouSureYouWantToLeaveProject"
+28 -16
View File
@@ -1,11 +1,12 @@
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';
import Paths from '../constants/Paths'; import Paths from '../constants/Paths';
import LoginWrapperContainer from '../containers/LoginWrapperContainer'; import LoginContainer from '../containers/LoginContainer';
import CoreContainer from '../containers/CoreContainer'; import CoreContainer from '../containers/CoreContainer';
import NotFound from './NotFound'; import NotFound from './NotFound';
@@ -14,29 +15,40 @@ 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 (
<Provider store={store}> <AuthProvider
<ReduxRouter history={history}> authority={config.authority}
<Routes> client_id={config.clientId}
<Route path={Paths.LOGIN} element={<LoginWrapperContainer />} /> redirect_uri={config.redirectUri}
<Route path={Paths.OIDC_CALLBACK} element={<LoginWrapperContainer />} /> scope={config.scopes}
<Route path={Paths.ROOT} element={<CoreContainer />} /> onSigninCallback={() => {
<Route path={Paths.PROJECTS} element={<CoreContainer />} /> window.history.replaceState({}, document.title, window.location.pathname);
<Route path={Paths.BOARDS} element={<CoreContainer />} /> }}
<Route path={Paths.CARDS} element={<CoreContainer />} /> >
<Route path="*" element={<NotFound />} /> <Provider store={store}>
</Routes> <ReduxRouter history={history}>
</ReduxRouter> <Routes>
</Provider> <Route path={Paths.LOGIN} element={<LoginContainer />} />
<Route path={Paths.OIDC_LOGIN} element={<OidcLoginContainer />} />
<Route path={Paths.ROOT} element={<CoreContainer />} />
<Route path={Paths.PROJECTS} element={<CoreContainer />} />
<Route path={Paths.BOARDS} element={<CoreContainer />} />
<Route path={Paths.CARDS} element={<CoreContainer />} />
<Route path="*" element={<NotFound />} />
</Routes>
</ReduxRouter>
</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 */
}; };
@@ -24,7 +24,6 @@ const AccountPane = React.memo(
organization, organization,
language, language,
isLocked, isLocked,
isUsernameLocked,
isAvatarUpdating, isAvatarUpdating,
usernameUpdateForm, usernameUpdateForm,
emailUpdateForm, emailUpdateForm,
@@ -105,7 +104,7 @@ const AccountPane = React.memo(
value={language || 'auto'} value={language || 'auto'}
onChange={handleLanguageChange} onChange={handleLanguageChange}
/> />
{(!isLocked || !isUsernameLocked) && ( {!isLocked && (
<> <>
<Divider horizontal section> <Divider horizontal section>
<Header as="h4"> <Header as="h4">
@@ -114,62 +113,56 @@ const AccountPane = React.memo(
})} })}
</Header> </Header>
</Divider> </Divider>
{!isUsernameLocked && ( <div className={styles.action}>
<div className={styles.action}> <UserUsernameEditPopup
<UserUsernameEditPopup usePasswordConfirmation
defaultData={usernameUpdateForm.data} defaultData={usernameUpdateForm.data}
username={username} username={username}
isSubmitting={usernameUpdateForm.isSubmitting} isSubmitting={usernameUpdateForm.isSubmitting}
error={usernameUpdateForm.error} error={usernameUpdateForm.error}
usePasswordConfirmation={!isLocked} // FIXME: hack onUpdate={onUsernameUpdate}
onUpdate={onUsernameUpdate} onMessageDismiss={onUsernameUpdateMessageDismiss}
onMessageDismiss={onUsernameUpdateMessageDismiss} >
> <Button className={styles.actionButton}>
<Button className={styles.actionButton}> {t('action.editUsername', {
{t('action.editUsername', { context: 'title',
context: 'title', })}
})} </Button>
</Button> </UserUsernameEditPopup>
</UserUsernameEditPopup> </div>
</div> <div className={styles.action}>
)} <UserEmailEditPopup
{!isLocked && ( usePasswordConfirmation
<> defaultData={emailUpdateForm.data}
<div className={styles.action}> email={email}
<UserEmailEditPopup isSubmitting={emailUpdateForm.isSubmitting}
usePasswordConfirmation error={emailUpdateForm.error}
defaultData={emailUpdateForm.data} onUpdate={onEmailUpdate}
email={email} onMessageDismiss={onEmailUpdateMessageDismiss}
isSubmitting={emailUpdateForm.isSubmitting} >
error={emailUpdateForm.error} <Button className={styles.actionButton}>
onUpdate={onEmailUpdate} {t('action.editEmail', {
onMessageDismiss={onEmailUpdateMessageDismiss} context: 'title',
> })}
<Button className={styles.actionButton}> </Button>
{t('action.editEmail', { </UserEmailEditPopup>
context: 'title', </div>
})} <div className={styles.action}>
</Button> <UserPasswordEditPopup
</UserEmailEditPopup> usePasswordConfirmation
</div> defaultData={passwordUpdateForm.data}
<div className={styles.action}> isSubmitting={passwordUpdateForm.isSubmitting}
<UserPasswordEditPopup error={passwordUpdateForm.error}
usePasswordConfirmation onUpdate={onPasswordUpdate}
defaultData={passwordUpdateForm.data} onMessageDismiss={onPasswordUpdateMessageDismiss}
isSubmitting={passwordUpdateForm.isSubmitting} >
error={passwordUpdateForm.error} <Button className={styles.actionButton}>
onUpdate={onPasswordUpdate} {t('action.editPassword', {
onMessageDismiss={onPasswordUpdateMessageDismiss} context: 'title',
> })}
<Button className={styles.actionButton}> </Button>
{t('action.editPassword', { </UserPasswordEditPopup>
context: 'title', </div>
})}
</Button>
</UserPasswordEditPopup>
</div>
</>
)}
</> </>
)} )}
</Tab.Pane> </Tab.Pane>
@@ -186,7 +179,6 @@ AccountPane.propTypes = {
organization: PropTypes.string, organization: PropTypes.string,
language: PropTypes.string, language: PropTypes.string,
isLocked: PropTypes.bool.isRequired, isLocked: PropTypes.bool.isRequired,
isUsernameLocked: 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,9 +16,8 @@ const UserSettingsModal = React.memo(
phone, phone,
organization, organization,
language, language,
subscribeToOwnCards,
isLocked, isLocked,
isUsernameLocked, subscribeToOwnCards,
isAvatarUpdating, isAvatarUpdating,
usernameUpdateForm, usernameUpdateForm,
emailUpdateForm, emailUpdateForm,
@@ -51,7 +50,6 @@ const UserSettingsModal = React.memo(
organization={organization} organization={organization}
language={language} language={language}
isLocked={isLocked} isLocked={isLocked}
isUsernameLocked={isUsernameLocked}
isAvatarUpdating={isAvatarUpdating} isAvatarUpdating={isAvatarUpdating}
usernameUpdateForm={usernameUpdateForm} usernameUpdateForm={usernameUpdateForm}
emailUpdateForm={emailUpdateForm} emailUpdateForm={emailUpdateForm}
@@ -108,9 +106,8 @@ UserSettingsModal.propTypes = {
phone: PropTypes.string, phone: PropTypes.string,
organization: PropTypes.string, organization: PropTypes.string,
language: PropTypes.string, language: PropTypes.string,
subscribeToOwnCards: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired, isLocked: PropTypes.bool.isRequired,
isUsernameLocked: 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 */
usernameUpdateForm: PropTypes.object.isRequired, usernameUpdateForm: PropTypes.object.isRequired,
@@ -136,15 +136,13 @@ const ActionsStep = React.memo(
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
{!user.isUsernameLocked && (
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
{t('action.editUsername', {
context: 'title',
})}
</Menu.Item>
)}
{!user.isLocked && ( {!user.isLocked && (
<> <>
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
{t('action.editUsername', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditEmailClick}> <Menu.Item className={styles.menuItem} onClick={handleEditEmailClick}>
{t('action.editEmail', { {t('action.editEmail', {
context: 'title', context: 'title',
@@ -155,15 +153,13 @@ const ActionsStep = React.memo(
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteUser', {
context: 'title',
})}
</Menu.Item>
</> </>
)} )}
{!user.isDeletionLocked && (
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteUser', {
context: 'title',
})}
</Menu.Item>
)}
</Menu> </Menu>
</Popup.Content> </Popup.Content>
</> </>
@@ -18,9 +18,6 @@ const Item = React.memo(
phone, phone,
isAdmin, isAdmin,
isLocked, isLocked,
isRoleLocked,
isUsernameLocked,
isDeletionLocked,
emailUpdateForm, emailUpdateForm,
passwordUpdateForm, passwordUpdateForm,
usernameUpdateForm, usernameUpdateForm,
@@ -50,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} disabled={isRoleLocked} onChange={handleIsAdminChange} /> <Radio toggle checked={isAdmin} disabled={isLocked} onChange={handleIsAdminChange} />
</Table.Cell> </Table.Cell>
<Table.Cell textAlign="right"> <Table.Cell textAlign="right">
<ActionsPopup <ActionsPopup
@@ -62,8 +59,6 @@ const Item = React.memo(
phone, phone,
isAdmin, isAdmin,
isLocked, isLocked,
isUsernameLocked,
isDeletionLocked,
emailUpdateForm, emailUpdateForm,
passwordUpdateForm, passwordUpdateForm,
usernameUpdateForm, usernameUpdateForm,
@@ -96,9 +91,6 @@ Item.propTypes = {
phone: PropTypes.string, phone: PropTypes.string,
isAdmin: PropTypes.bool.isRequired, isAdmin: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired, isLocked: PropTypes.bool.isRequired,
isRoleLocked: PropTypes.bool.isRequired,
isUsernameLocked: PropTypes.bool.isRequired,
isDeletionLocked: 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,
@@ -10,7 +10,6 @@ import Item from './Item';
const UsersModal = React.memo( const UsersModal = React.memo(
({ ({
items, items,
canAdd,
onUpdate, onUpdate,
onUsernameUpdate, onUsernameUpdate,
onUsernameUpdateMessageDismiss, onUsernameUpdateMessageDismiss,
@@ -112,9 +111,6 @@ const UsersModal = React.memo(
phone={item.phone} phone={item.phone}
isAdmin={item.isAdmin} isAdmin={item.isAdmin}
isLocked={item.isLocked} isLocked={item.isLocked}
isRoleLocked={item.isRoleLocked}
isUsernameLocked={item.isUsernameLocked}
isDeletionLocked={item.isDeletionLocked}
emailUpdateForm={item.emailUpdateForm} emailUpdateForm={item.emailUpdateForm}
passwordUpdateForm={item.passwordUpdateForm} passwordUpdateForm={item.passwordUpdateForm}
usernameUpdateForm={item.usernameUpdateForm} usernameUpdateForm={item.usernameUpdateForm}
@@ -131,13 +127,11 @@ const UsersModal = React.memo(
</Table.Body> </Table.Body>
</Table> </Table>
</Modal.Content> </Modal.Content>
{canAdd && ( <Modal.Actions>
<Modal.Actions> <UserAddPopupContainer>
<UserAddPopupContainer> <Button positive content={t('action.addUser')} />
<Button positive content={t('action.addUser')} /> </UserAddPopupContainer>
</UserAddPopupContainer> </Modal.Actions>
</Modal.Actions>
)}
</Modal> </Modal>
); );
}, },
@@ -145,7 +139,6 @@ const UsersModal = React.memo(
UsersModal.propTypes = { UsersModal.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canAdd: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onUsernameUpdate: PropTypes.func.isRequired, onUsernameUpdate: PropTypes.func.isRequired,
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired, onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
-13
View File
@@ -12,19 +12,14 @@ export default {
/* Login */ /* Login */
LOGIN_INITIALIZE: 'LOGIN_INITIALIZE',
AUTHENTICATE: 'AUTHENTICATE', AUTHENTICATE: 'AUTHENTICATE',
AUTHENTICATE__SUCCESS: 'AUTHENTICATE__SUCCESS', AUTHENTICATE__SUCCESS: 'AUTHENTICATE__SUCCESS',
AUTHENTICATE__FAILURE: 'AUTHENTICATE__FAILURE', AUTHENTICATE__FAILURE: 'AUTHENTICATE__FAILURE',
USING_OIDC_AUTHENTICATE: 'USING_OIDC_AUTHENTICATE',
USING_OIDC_AUTHENTICATE__SUCCESS: 'USING_OIDC_AUTHENTICATE__SUCCESS',
USING_OIDC_AUTHENTICATE__FAILURE: 'USING_OIDC_AUTHENTICATE__FAILURE',
AUTHENTICATE_ERROR_CLEAR: 'AUTHENTICATE_ERROR_CLEAR', AUTHENTICATE_ERROR_CLEAR: 'AUTHENTICATE_ERROR_CLEAR',
/* Core */ /* Core */
CORE_INITIALIZE: 'CORE_INITIALIZE', CORE_INITIALIZE: 'CORE_INITIALIZE',
CORE_INITIALIZE__CONFIG_FETCH: 'CORE_INITIALIZE__CONFIG_FETCH',
LOGOUT: 'LOGOUT', LOGOUT: 'LOGOUT',
LOGOUT__ACCESS_TOKEN_INVALIDATE: 'LOGOUT__ACCESS_TOKEN_INVALIDATE', LOGOUT__ACCESS_TOKEN_INVALIDATE: 'LOGOUT__ACCESS_TOKEN_INVALIDATE',
@@ -173,10 +168,6 @@ export default {
LIST_UPDATE__SUCCESS: 'LIST_UPDATE__SUCCESS', LIST_UPDATE__SUCCESS: 'LIST_UPDATE__SUCCESS',
LIST_UPDATE__FAILURE: 'LIST_UPDATE__FAILURE', LIST_UPDATE__FAILURE: 'LIST_UPDATE__FAILURE',
LIST_UPDATE_HANDLE: 'LIST_UPDATE_HANDLE', LIST_UPDATE_HANDLE: 'LIST_UPDATE_HANDLE',
LIST_SORT: 'LIST_SORT',
LIST_SORT__SUCCESS: 'LIST_SORT__SUCCESS',
LIST_SORT__FAILURE: 'LIST_SORT__FAILURE',
LIST_SORT_HANDLE: 'LIST_SORT_HANDLE',
LIST_DELETE: 'LIST_DELETE', LIST_DELETE: 'LIST_DELETE',
LIST_DELETE__SUCCESS: 'LIST_DELETE__SUCCESS', LIST_DELETE__SUCCESS: 'LIST_DELETE__SUCCESS',
LIST_DELETE__FAILURE: 'LIST_DELETE__FAILURE', LIST_DELETE__FAILURE: 'LIST_DELETE__FAILURE',
@@ -198,14 +189,10 @@ export default {
CARD_TRANSFER: 'CARD_TRANSFER', CARD_TRANSFER: 'CARD_TRANSFER',
CARD_TRANSFER__SUCCESS: 'CARD_TRANSFER__SUCCESS', CARD_TRANSFER__SUCCESS: 'CARD_TRANSFER__SUCCESS',
CARD_TRANSFER__FAILURE: 'CARD_TRANSFER__FAILURE', CARD_TRANSFER__FAILURE: 'CARD_TRANSFER__FAILURE',
CARD_DUPLICATE: 'CARD_DUPLICATE',
CARD_DUPLICATE__SUCCESS: 'CARD_DUPLICATE__SUCCESS',
CARD_DUPLICATE__FAILURE: 'CARD_DUPLICATE__FAILURE',
CARD_DELETE: 'CARD_DELETE', CARD_DELETE: 'CARD_DELETE',
CARD_DELETE__SUCCESS: 'CARD_DELETE__SUCCESS', CARD_DELETE__SUCCESS: 'CARD_DELETE__SUCCESS',
CARD_DELETE__FAILURE: 'CARD_DELETE__FAILURE', CARD_DELETE__FAILURE: 'CARD_DELETE__FAILURE',
CARD_DELETE_HANDLE: 'CARD_DELETE_HANDLE', CARD_DELETE_HANDLE: 'CARD_DELETE_HANDLE',
TEXT_FILTER_IN_CURRENT_BOARD: 'TEXT_FILTER_IN_CURRENT_BOARD',
/* Tasks */ /* Tasks */
+1 -6
View File
@@ -11,11 +11,11 @@ export default {
/* Login */ /* Login */
AUTHENTICATE: `${PREFIX}/AUTHENTICATE`, AUTHENTICATE: `${PREFIX}/AUTHENTICATE`,
USING_OIDC_AUTHENTICATE: `${PREFIX}/USING_OIDC_AUTHENTICATE`,
AUTHENTICATE_ERROR_CLEAR: `${PREFIX}/AUTHENTICATE_ERROR_CLEAR`, AUTHENTICATE_ERROR_CLEAR: `${PREFIX}/AUTHENTICATE_ERROR_CLEAR`,
/* Core */ /* Core */
CORE_INITIALIZE: `${PREFIX}/CORE_INITIALIZE`,
LOGOUT: `${PREFIX}/LOGOUT`, LOGOUT: `${PREFIX}/LOGOUT`,
/* Modals */ /* Modals */
@@ -120,8 +120,6 @@ export default {
LIST_MOVE: `${PREFIX}/LIST_MOVE`, LIST_MOVE: `${PREFIX}/LIST_MOVE`,
LIST_DELETE: `${PREFIX}/LIST_DELETE`, LIST_DELETE: `${PREFIX}/LIST_DELETE`,
LIST_DELETE_HANDLE: `${PREFIX}/LIST_DELETE_HANDLE`, LIST_DELETE_HANDLE: `${PREFIX}/LIST_DELETE_HANDLE`,
LIST_SORT: `${PREFIX}/LIST_SORT`,
LIST_SORT_HANDLE: `${PREFIX}/LIST_SORT_HANDLE`,
/* Cards */ /* Cards */
@@ -134,12 +132,9 @@ export default {
CURRENT_CARD_MOVE: `${PREFIX}/CURRENT_CARD_MOVE`, CURRENT_CARD_MOVE: `${PREFIX}/CURRENT_CARD_MOVE`,
CARD_TRANSFER: `${PREFIX}/CARD_TRANSFER`, CARD_TRANSFER: `${PREFIX}/CARD_TRANSFER`,
CURRENT_CARD_TRANSFER: `${PREFIX}/CURRENT_CARD_TRANSFER`, CURRENT_CARD_TRANSFER: `${PREFIX}/CURRENT_CARD_TRANSFER`,
CARD_DUPLICATE: `${PREFIX}/CARD_DUPLICATE`,
CURRENT_CARD_DUPLICATE: `${PREFIX}/CURRENT_CARD_DUPLICATE`,
CARD_DELETE: `${PREFIX}/CARD_DELETE`, CARD_DELETE: `${PREFIX}/CARD_DELETE`,
CURRENT_CARD_DELETE: `${PREFIX}/CURRENT_CARD_DELETE`, CURRENT_CARD_DELETE: `${PREFIX}/CURRENT_CARD_DELETE`,
CARD_DELETE_HANDLE: `${PREFIX}/CARD_DELETE_HANDLE`, CARD_DELETE_HANDLE: `${PREFIX}/CARD_DELETE_HANDLE`,
TEXT_FILTER_IN_CURRENT_BOARD: `${PREFIX}/FILTER_TEXT_HANDLE`,
/* Tasks */ /* Tasks */
-7
View File
@@ -8,13 +8,6 @@ export const BoardMembershipRoles = {
VIEWER: 'viewer', VIEWER: 'viewer',
}; };
export const ListSortTypes = {
NAME_ASC: 'name_asc',
DUE_DATE_ASC: 'dueDate_asc',
CREATED_AT_ASC: 'createdAt_asc',
CREATED_AT_DESC: 'createdAt_desc',
};
export const ActivityTypes = { export const ActivityTypes = {
CREATE_CARD: 'createCard', CREATE_CARD: 'createCard',
MOVE_CARD: 'moveCard', MOVE_CARD: 'moveCard',
+2 -2
View File
@@ -2,7 +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_CALLBACK = `${Config.BASE_PATH}/oidc-callback`; 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`;
@@ -10,8 +10,8 @@ const CARDS = `${Config.BASE_PATH}/cards/:id`;
export default { export default {
ROOT, ROOT,
LOGIN, LOGIN,
OIDC_CALLBACK,
PROJECTS, PROJECTS,
BOARDS, BOARDS,
CARDS, CARDS,
OIDC_LOGIN,
}; };
@@ -13,7 +13,6 @@ const mapStateToProps = (state) => {
const labels = selectors.selectLabelsForCurrentBoard(state); const labels = selectors.selectLabelsForCurrentBoard(state);
const filterUsers = selectors.selectFilterUsersForCurrentBoard(state); const filterUsers = selectors.selectFilterUsersForCurrentBoard(state);
const filterLabels = selectors.selectFilterLabelsForCurrentBoard(state); const filterLabels = selectors.selectFilterLabelsForCurrentBoard(state);
const filterText = selectors.selectFilterTextForCurrentBoard(state);
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const isCurrentUserEditor = const isCurrentUserEditor =
@@ -24,7 +23,6 @@ const mapStateToProps = (state) => {
labels, labels,
filterUsers, filterUsers,
filterLabels, filterLabels,
filterText,
allUsers, allUsers,
canEdit: isCurrentUserEditor, canEdit: isCurrentUserEditor,
canEditMemberships: isCurrentUserManager, canEditMemberships: isCurrentUserManager,
@@ -45,7 +43,6 @@ const mapDispatchToProps = (dispatch) =>
onLabelUpdate: entryActions.updateLabel, onLabelUpdate: entryActions.updateLabel,
onLabelMove: entryActions.moveLabel, onLabelMove: entryActions.moveLabel,
onLabelDelete: entryActions.deleteLabel, onLabelDelete: entryActions.deleteLabel,
onTextFilterUpdate: entryActions.filterText,
}, },
dispatch, dispatch,
); );
-1
View File
@@ -62,7 +62,6 @@ const mapDispatchToProps = (dispatch, { id }) =>
onUpdate: (data) => entryActions.updateCard(id, data), onUpdate: (data) => entryActions.updateCard(id, data),
onMove: (listId, index) => entryActions.moveCard(id, listId, index), onMove: (listId, index) => entryActions.moveCard(id, listId, index),
onTransfer: (boardId, listId) => entryActions.transferCard(id, boardId, listId), onTransfer: (boardId, listId) => entryActions.transferCard(id, boardId, listId),
onDuplicate: () => entryActions.duplicateCard(id),
onDelete: () => entryActions.deleteCard(id), onDelete: () => entryActions.deleteCard(id),
onUserAdd: (userId) => entryActions.addUserToCard(userId, id), onUserAdd: (userId) => entryActions.addUserToCard(userId, id),
onUserRemove: (userId) => entryActions.removeUserFromCard(userId, id), onUserRemove: (userId) => entryActions.removeUserFromCard(userId, id),
@@ -78,7 +78,6 @@ const mapDispatchToProps = (dispatch) =>
onUpdate: entryActions.updateCurrentCard, onUpdate: entryActions.updateCurrentCard,
onMove: entryActions.moveCurrentCard, onMove: entryActions.moveCurrentCard,
onTransfer: entryActions.transferCurrentCard, onTransfer: entryActions.transferCurrentCard,
onDuplicate: entryActions.duplicateCurrentCard,
onDelete: entryActions.deleteCurrentCard, onDelete: entryActions.deleteCurrentCard,
onUserAdd: entryActions.addUserToCurrentCard, onUserAdd: entryActions.addUserToCurrentCard,
onUserRemove: entryActions.removeUserFromCurrentCard, onUserRemove: entryActions.removeUserFromCurrentCard,
+2 -2
View File
@@ -4,18 +4,18 @@ import selectors from '../selectors';
import Core from '../components/Core'; import Core from '../components/Core';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const isInitializing = selectors.selectIsInitializing(state); const isCoreInitializing = selectors.selectIsCoreInitializing(state);
const isSocketDisconnected = selectors.selectIsSocketDisconnected(state); const isSocketDisconnected = selectors.selectIsSocketDisconnected(state);
const currentModal = selectors.selectCurrentModal(state); const currentModal = selectors.selectCurrentModal(state);
const currentProject = selectors.selectCurrentProject(state); const currentProject = selectors.selectCurrentProject(state);
const currentBoard = selectors.selectCurrentBoard(state); const currentBoard = selectors.selectCurrentBoard(state);
return { return {
isInitializing,
isSocketDisconnected, isSocketDisconnected,
currentModal, currentModal,
currentProject, currentProject,
currentBoard, currentBoard,
isInitializing: isCoreInitializing,
}; };
}; };
-1
View File
@@ -33,7 +33,6 @@ const mapDispatchToProps = (dispatch, { id }) =>
bindActionCreators( bindActionCreators(
{ {
onUpdate: (data) => entryActions.updateList(id, data), onUpdate: (data) => entryActions.updateList(id, data),
onSort: (data) => entryActions.sortList(id, data),
onDelete: () => entryActions.deleteList(id), onDelete: () => entryActions.deleteList(id),
onCardCreate: (data, autoOpen) => entryActions.createCard(id, data, autoOpen), onCardCreate: (data, autoOpen) => entryActions.createCard(id, data, autoOpen),
}, },
+9 -20
View File
@@ -1,34 +1,23 @@
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import selectors from '../selectors';
import entryActions from '../entry-actions'; import entryActions from '../entry-actions';
import Login from '../components/Login'; import Login from '../components/Login';
const mapStateToProps = (state) => { const mapStateToProps = ({
const oidcConfig = selectors.selectOidcConfig(state); ui: {
authenticateForm: { data: defaultData, isSubmitting, error },
const { },
ui: { }) => ({
authenticateForm: { data: defaultData, isSubmitting, isSubmittingUsingOidc, error }, defaultData,
}, isSubmitting,
} = state; error,
});
return {
defaultData,
isSubmitting,
isSubmittingUsingOidc,
error,
withOidc: !!oidcConfig,
isOidcEnforced: !!oidcConfig && oidcConfig.isEnforced,
};
};
const mapDispatchToProps = (dispatch) => const mapDispatchToProps = (dispatch) =>
bindActionCreators( bindActionCreators(
{ {
onAuthenticate: entryActions.authenticate, onAuthenticate: entryActions.authenticate,
onAuthenticateUsingOidc: entryActions.authenticateUsingOidc,
onMessageDismiss: entryActions.clearAuthenticateError, onMessageDismiss: entryActions.clearAuthenticateError,
}, },
dispatch, dispatch,
@@ -1,14 +0,0 @@
import { connect } from 'react-redux';
import selectors from '../selectors';
import LoginWrapper from '../components/LoginWrapper';
const mapStateToProps = (state) => {
const isInitializing = selectors.selectIsInitializing(state);
return {
isInitializing,
};
};
export default connect(mapStateToProps)(LoginWrapper);
@@ -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,9 +14,8 @@ const mapStateToProps = (state) => {
phone, phone,
organization, organization,
language, language,
subscribeToOwnCards,
isLocked, isLocked,
isUsernameLocked, subscribeToOwnCards,
isAvatarUpdating, isAvatarUpdating,
emailUpdateForm, emailUpdateForm,
passwordUpdateForm, passwordUpdateForm,
@@ -31,9 +30,8 @@ const mapStateToProps = (state) => {
phone, phone,
organization, organization,
language, language,
subscribeToOwnCards,
isLocked, isLocked,
isUsernameLocked, subscribeToOwnCards,
isAvatarUpdating, isAvatarUpdating,
emailUpdateForm, emailUpdateForm,
passwordUpdateForm, passwordUpdateForm,
@@ -6,12 +6,10 @@ import entryActions from '../entry-actions';
import UsersModal from '../components/UsersModal'; import UsersModal from '../components/UsersModal';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const oidcConfig = selectors.selectOidcConfig(state);
const users = selectors.selectUsersExceptCurrent(state); const users = selectors.selectUsersExceptCurrent(state);
return { return {
items: users, items: users,
canAdd: !oidcConfig || !oidcConfig.isEnforced,
}; };
}; };
+4 -26
View File
@@ -38,7 +38,7 @@ const handleCardUpdate = (card) => ({
}, },
}); });
const moveCard = (id, listId, index) => ({ const moveCard = (id, listId, index = 0) => ({
type: EntryActionTypes.CARD_MOVE, type: EntryActionTypes.CARD_MOVE,
payload: { payload: {
id, id,
@@ -47,7 +47,7 @@ const moveCard = (id, listId, index) => ({
}, },
}); });
const moveCurrentCard = (listId, index) => ({ const moveCurrentCard = (listId, index = 0) => ({
type: EntryActionTypes.CURRENT_CARD_MOVE, type: EntryActionTypes.CURRENT_CARD_MOVE,
payload: { payload: {
listId, listId,
@@ -55,7 +55,7 @@ const moveCurrentCard = (listId, index) => ({
}, },
}); });
const transferCard = (id, boardId, listId, index) => ({ const transferCard = (id, boardId, listId, index = 0) => ({
type: EntryActionTypes.CARD_TRANSFER, type: EntryActionTypes.CARD_TRANSFER,
payload: { payload: {
id, id,
@@ -65,7 +65,7 @@ const transferCard = (id, boardId, listId, index) => ({
}, },
}); });
const transferCurrentCard = (boardId, listId, index) => ({ const transferCurrentCard = (boardId, listId, index = 0) => ({
type: EntryActionTypes.CURRENT_CARD_TRANSFER, type: EntryActionTypes.CURRENT_CARD_TRANSFER,
payload: { payload: {
boardId, boardId,
@@ -74,18 +74,6 @@ const transferCurrentCard = (boardId, listId, index) => ({
}, },
}); });
const duplicateCard = (id) => ({
type: EntryActionTypes.CARD_DUPLICATE,
payload: {
id,
},
});
const duplicateCurrentCard = () => ({
type: EntryActionTypes.CURRENT_CARD_DUPLICATE,
payload: {},
});
const deleteCard = (id) => ({ const deleteCard = (id) => ({
type: EntryActionTypes.CARD_DELETE, type: EntryActionTypes.CARD_DELETE,
payload: { payload: {
@@ -105,13 +93,6 @@ const handleCardDelete = (card) => ({
}, },
}); });
const filterText = (text) => ({
type: EntryActionTypes.TEXT_FILTER_IN_CURRENT_BOARD,
payload: {
text,
},
});
export default { export default {
createCard, createCard,
handleCardCreate, handleCardCreate,
@@ -122,10 +103,7 @@ export default {
moveCurrentCard, moveCurrentCard,
transferCard, transferCard,
transferCurrentCard, transferCurrentCard,
duplicateCard,
duplicateCurrentCard,
deleteCard, deleteCard,
deleteCurrentCard, deleteCurrentCard,
handleCardDelete, handleCardDelete,
filterText,
}; };
+8 -4
View File
@@ -1,12 +1,16 @@
import EntryActionTypes from '../constants/EntryActionTypes'; import EntryActionTypes from '../constants/EntryActionTypes';
const logout = (invalidateAccessToken) => ({ const initializeCore = () => ({
type: EntryActionTypes.CORE_INITIALIZE,
payload: {},
});
const logout = () => ({
type: EntryActionTypes.LOGOUT, type: EntryActionTypes.LOGOUT,
payload: { payload: {},
invalidateAccessToken,
},
}); });
export default { export default {
initializeCore,
logout, logout,
}; };
-20
View File
@@ -37,24 +37,6 @@ const moveList = (id, index) => ({
}, },
}); });
const sortList = (id, data) => {
return {
type: EntryActionTypes.LIST_SORT,
payload: {
id,
data,
},
};
};
const handleListSort = (list, cards) => ({
type: EntryActionTypes.LIST_SORT_HANDLE,
payload: {
list,
cards,
},
});
const deleteList = (id) => ({ const deleteList = (id) => ({
type: EntryActionTypes.LIST_DELETE, type: EntryActionTypes.LIST_DELETE,
payload: { payload: {
@@ -75,8 +57,6 @@ export default {
updateList, updateList,
handleListUpdate, handleListUpdate,
moveList, moveList,
sortList,
handleListSort,
deleteList, deleteList,
handleListDelete, handleListDelete,
}; };
-6
View File
@@ -7,11 +7,6 @@ const authenticate = (data) => ({
}, },
}); });
const authenticateUsingOidc = () => ({
type: EntryActionTypes.USING_OIDC_AUTHENTICATE,
payload: {},
});
const clearAuthenticateError = () => ({ const clearAuthenticateError = () => ({
type: EntryActionTypes.AUTHENTICATE_ERROR_CLEAR, type: EntryActionTypes.AUTHENTICATE_ERROR_CLEAR,
payload: {}, payload: {},
@@ -19,6 +14,5 @@ const clearAuthenticateError = () => ({
export default { export default {
authenticate, authenticate,
authenticateUsingOidc,
clearAuthenticateError, clearAuthenticateError,
}; };
+7 -3
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';
const root = ReactDOM.createRoot(document.getElementById('root')); fetch(`${Config.SERVER_BASE_URL}/api/appconfig`).then((response) => {
root.render(React.createElement(Root, { store, history })); response.json().then((config) => {
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(Root, { store, history, config }));
});
});
@@ -9,6 +9,4 @@ export default class Input extends SemanticUIInput {
static Mask = InputMask; static Mask = InputMask;
focus = (options) => this.inputRef.current.focus(options); focus = (options) => this.inputRef.current.focus(options);
blur = () => this.inputRef.current.blur();
} }
+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: 'লগ ইন করুন',
},
},
};
+14 -25
View File
@@ -9,8 +9,6 @@ export default {
dateTime: '$t(format:date) $t(format:time)', dateTime: '$t(format:date) $t(format:time)',
longDate: 'd MMM', longDate: 'd MMM',
longDateTime: "d MMMM 'v' p", longDateTime: "d MMMM 'v' p",
fullDate: 'd MMM, y',
fullDateTime: "d MMMM, y 'v' p",
}, },
translation: { translation: {
@@ -26,7 +24,7 @@ export default {
administrator: 'Administrátor', administrator: 'Administrátor',
all: 'Vše', all: 'Vše',
allChangesWillBeAutomaticallySavedAfterConnectionRestored: allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Všechny změny budou automaticky uloženy<br />po obnově spojení.', 'Všechny změny budou automaticky uloženy<br />po obnově spojení',
areYouSureYouWantToDeleteThisAttachment: 'Opravdu chcete smazat tuto přílohu?', areYouSureYouWantToDeleteThisAttachment: 'Opravdu chcete smazat tuto přílohu?',
areYouSureYouWantToDeleteThisBoard: 'Opravdu chcete smazat tuto tabuli?', areYouSureYouWantToDeleteThisBoard: 'Opravdu chcete smazat tuto tabuli?',
areYouSureYouWantToDeleteThisCard: 'Opravdu chcete smazat tuto kartu?', areYouSureYouWantToDeleteThisCard: 'Opravdu chcete smazat tuto kartu?',
@@ -39,9 +37,9 @@ export default {
areYouSureYouWantToLeaveBoard: 'Opravdu chcete opustit tuto tabuli?', areYouSureYouWantToLeaveBoard: 'Opravdu chcete opustit tuto tabuli?',
areYouSureYouWantToLeaveProject: 'Opravdu chcete opustit projekt?', areYouSureYouWantToLeaveProject: 'Opravdu chcete opustit projekt?',
areYouSureYouWantToRemoveThisManagerFromProject: areYouSureYouWantToRemoveThisManagerFromProject:
'Jste si jisti, že chcete tohoto správce z projektu odebrat?', 'Are you sure you want to remove this manager from the project?',
areYouSureYouWantToRemoveThisMemberFromBoard: areYouSureYouWantToRemoveThisMemberFromBoard:
'Jste si jisti, že chcete tohoto člena odebrat z tabule?', 'Opravdu chcete odstranit tohoto člena z tabule?',
attachment: 'Příloha', attachment: 'Příloha',
attachments: 'Přílohy', attachments: 'Přílohy',
authentication: 'Ověření', authentication: 'Ověření',
@@ -49,23 +47,21 @@ export default {
board: 'Tabule', board: 'Tabule',
boardNotFound_title: 'Tabule nenalezena', boardNotFound_title: 'Tabule nenalezena',
canComment: 'Může komentovat', canComment: 'Může komentovat',
canEditContentOfBoard: 'Může upravovat obsah tabule.', canEditContentOfBoard: 'Může upravovat obsah tabule',
canOnlyViewBoard: 'Může zobrazit pouze tabuli.', 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',
color: 'Barva', color: 'Barva',
copy_inline: 'copy',
createBoard_title: 'Vytvořit tabuli', createBoard_title: 'Vytvořit tabuli',
createLabel_title: 'Vytvořit štítek', createLabel_title: 'Vytvořit štítek',
createNewOneOrSelectExistingOne: 'Vytvořit nový nebo vyberte<br />již existující.', createNewOneOrSelectExistingOne: 'Vytvořit nový nebo vyberte<br />již existující',
createProject_title: 'Vytvořit projekt', createProject_title: 'Vytvořit projekt',
createTextFile_title: 'Vytvořit textový soubor', createTextFile_title: 'Vytvořit textový soubor',
currentPassword: 'Aktuální heslo', currentPassword: 'Aktuální heslo',
dangerZone_title: 'Nebezpečná zóna', dangerZone_title: 'Nebezpečná zóna',
date: 'Datum', date: 'Datum',
dueDate: 'Termín', dueDate_title: 'Termín do',
dueDate_title: 'Termín',
deleteAttachment_title: 'Smazat přílohu', deleteAttachment_title: 'Smazat přílohu',
deleteBoard_title: 'Smazat tabuli', deleteBoard_title: 'Smazat tabuli',
deleteCard_title: 'Smazat kartu', deleteCard_title: 'Smazat kartu',
@@ -82,7 +78,7 @@ export default {
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', editDueDate_title: 'Upravit Termín do',
editEmail_title: 'Upravit e-mail', editEmail_title: 'Upravit e-mail',
editInformation_title: 'Upravit informace', editInformation_title: 'Upravit informace',
editLabel_title: 'Upravit štítek', editLabel_title: 'Upravit štítek',
@@ -117,7 +113,6 @@ export default {
minutes: 'Minuty', minutes: 'Minuty',
moveCard_title: 'Přesunout kartu', moveCard_title: 'Přesunout kartu',
name: 'Jméno', name: 'Jméno',
newestFirst: 'Nejnovější',
newEmail: 'Nový e-mail', newEmail: 'Nový e-mail',
newPassword: 'Nové heslo', newPassword: 'Nové heslo',
newUsername: 'Nové uživatelské jméno', newUsername: 'Nové uživatelské jméno',
@@ -127,14 +122,13 @@ export default {
noProjects: 'Žádné projekty', noProjects: 'Žádné projekty',
notifications: 'Oznámení', notifications: 'Oznámení',
noUnreadNotifications: 'Žádné nepřečtené oznámení', noUnreadNotifications: 'Žádné nepřečtené oznámení',
oldestFirst: 'Nejstarší',
openBoard_title: 'Otevřít tabuli', openBoard_title: 'Otevřít tabuli',
optional_inline: 'volitelné', optional_inline: 'volitelné',
organization: 'Společnost', organization: 'Společnost',
phone: 'Telefon', phone: 'Telefon',
preferences: 'Volby', preferences: 'Volby',
pressPasteShortcutToAddAttachmentFromClipboard: pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: stisknutím Ctrl-V (Cmd-V na Mac) přidáte přílohu ze schránky.', 'Tip: dejte Ctrl-V (Cmd-V na Mac) pro vložení přílohy ze schránky',
project: 'Projekt', project: 'Projekt',
projectNotFound_title: 'Projekt nenalezen', projectNotFound_title: 'Projekt nenalezen',
removeManager_title: 'Odstranit vedoucího', removeManager_title: 'Odstranit vedoucího',
@@ -142,19 +136,17 @@ export default {
searchLabels: 'Hledat štítky...', searchLabels: 'Hledat štítky...',
searchMembers: 'Hledat členy...', searchMembers: 'Hledat členy...',
searchUsers: 'Hledat uživatele...', searchUsers: 'Hledat uživatele...',
searchCards: 'Hledat karty...',
seconds: 'Vteřin', seconds: 'Vteřin',
selectBoard: 'Vybrat tabuli', selectBoard: 'Vybrat tabuli',
selectList: 'Vybrat seznam', selectList: 'Vybrat seznam',
selectPermissions_title: 'Vybrat oprávnění', selectPermissions_title: 'Vybrat oprávnění',
selectProject: 'Vybrat projekt', selectProject: 'Vybrat projekt',
settings: 'Nastavení', settings: 'Nastavení',
sortList_title: 'Řadit podle',
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.', 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',
@@ -205,10 +197,8 @@ export default {
deleteTask: 'Smazat úkol', deleteTask: 'Smazat úkol',
deleteTask_title: 'Smazat úkol', deleteTask_title: 'Smazat úkol',
deleteUser: 'Smazat uživatele', deleteUser: 'Smazat uživatele',
duplicate: 'Duplikovat',
duplicateCard_title: 'Duplikovat kartu',
edit: 'Upravit', edit: 'Upravit',
editDueDate_title: 'Upravit termín', 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', editInformation_title: 'Upravit informace',
@@ -234,14 +224,13 @@ export default {
removeMember: 'Odstranit člena', removeMember: 'Odstranit člena',
save: 'Uložit', save: 'Uložit',
showAllAttachments: 'Zozbrazit všechny přílohy ({{hidden}} skryté)', showAllAttachments: 'Zozbrazit všechny přílohy ({{hidden}} skryté)',
showDetails: 'Zobrazit detaily',
showFewerAttachments: 'Zobrazit méně příloh', showFewerAttachments: 'Zobrazit méně příloh',
sortList_title: 'Řadit podle', showDetails: 'Zobrazit detaily',
start: 'Start', start: 'Start',
stop: 'Stop', stop: 'Stop',
subscribe: 'Odebírat', subscribe: 'Odebírat',
unsubscribe: 'Neodebírat', unsubscribe: 'Neodebírat',
uploadNewAvatar: 'Nahrát nový avatar', uploadNewAvatar: 'Nahrát nového avatara',
uploadNewImage: 'Nahrát nový obrázek', uploadNewImage: 'Nahrát nový obrázek',
}, },
}, },
+2 -2
View File
@@ -1,8 +1,8 @@
export default { export default {
translation: { translation: {
common: { common: {
emailOrUsername: 'E-mail nebo uživatelské jméno', emailOrUsername: 'Email nebo uživatelské jméno',
invalidEmailOrUsername: 'Nesprávný e-mail nebo uživatelské jméno', invalidEmailOrUsername: 'Nesprávný email nebo uživatelské jméno',
invalidPassword: 'Nesprávné heslo', invalidPassword: 'Nesprávné heslo',
logInToPlanka: 'Přihlásit se do Planka', logInToPlanka: 'Přihlásit se do Planka',
noInternetConnection: 'Bez připojení k internetu', noInternetConnection: 'Bez připojení k internetu',
+5 -7
View File
@@ -9,8 +9,6 @@ export default {
dateTime: '$t(format:date) $t(format:time)', dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMM d', longDate: 'MMM d',
longDateTime: "MMMM d 'at' p", longDateTime: "MMMM d 'at' p",
fullDate: 'MMM d, y',
fullDateTime: "MMMM d, y 'a' p",
}, },
translation: { translation: {
@@ -25,7 +23,7 @@ export default {
administrator: 'Administrator', administrator: 'Administrator',
all: 'Alle', all: 'Alle',
allChangesWillBeAutomaticallySavedAfterConnectionRestored: allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Alle ændringer vil automatisk blive gemt<br />ved genforbindelse.', 'Alle ændringer vil automatisk blive gemt<br />ved genforbindelse',
areYouSureYouWantToDeleteThisAttachment: 'Vil du slette den vedhæftede fil?', areYouSureYouWantToDeleteThisAttachment: 'Vil du slette den vedhæftede fil?',
areYouSureYouWantToDeleteThisBoard: 'Vil du slette dette bræt?', areYouSureYouWantToDeleteThisBoard: 'Vil du slette dette bræt?',
areYouSureYouWantToDeleteThisCard: 'Vil du slette dette kort?', areYouSureYouWantToDeleteThisCard: 'Vil du slette dette kort?',
@@ -48,11 +46,11 @@ export default {
boardNotFound_title: 'Bræt ikke fundet', boardNotFound_title: 'Bræt ikke fundet',
cardActions_title: 'Korthandlinger', cardActions_title: 'Korthandlinger',
cardNotFound_title: 'Kort ikke fundet', cardNotFound_title: 'Kort ikke fundet',
cardOrActionAreDeleted: 'Kort eller handling er slettet.', cardOrActionAreDeleted: 'Kort eller handling er slettet',
color: 'Farve', color: 'Farve',
createBoard_title: 'Opret bræt', createBoard_title: 'Opret bræt',
createLabel_title: 'Opret mærkat', createLabel_title: 'Opret mærkat',
createNewOneOrSelectExistingOne: 'Lav en ny eller marker<br />en eksisterende.', createNewOneOrSelectExistingOne: 'Lav en ny eller marker<br />en eksisterende',
createProject_title: 'Opret projekt', createProject_title: 'Opret projekt',
createTextFile_title: 'Opret tekstfil', createTextFile_title: 'Opret tekstfil',
currentPassword: 'Nuværende løsen', currentPassword: 'Nuværende løsen',
@@ -111,14 +109,14 @@ export default {
noLists: 'Ingen lister', noLists: 'Ingen lister',
noProjects: 'Ingen projekter', noProjects: 'Ingen projekter',
notifications: 'Notifikationer', notifications: 'Notifikationer',
noUnreadNotifications: 'Ingen ulæste notifikationer.', noUnreadNotifications: 'Ingen ulæste notifikationer',
openBoard_title: 'Åben bræt', openBoard_title: 'Åben bræt',
optional_inline: 'valgfri', optional_inline: 'valgfri',
organization: 'Organisation', organization: 'Organisation',
phone: 'Telefon', phone: 'Telefon',
preferences: 'Præferencer', preferences: 'Præferencer',
pressPasteShortcutToAddAttachmentFromClipboard: pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: Tryk Ctrl-V (Cmd-V på Mac) for at vedhæfte direkte fra udklipsholder.', 'Tip: Tryk Ctrl-V (Cmd-V på Mac) for at vedhæfte direkte fra udklipsholder',
project: 'Projekt', project: 'Projekt',
projectNotFound_title: 'Projekt ej fundet', projectNotFound_title: 'Projekt ej fundet',
removeManager_title: 'Fjern projektleder', removeManager_title: 'Fjern projektleder',

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