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

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