Compare commits

..

6 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)
2 years ago
Daniel Hiller 0f076f4cc4 Add: Translation Bengali (AI) 2 years ago
Daniel Hiller f1865db477 Add: Translation Arabic (AI) 2 years ago
Daniel Hiller b80127e952 Add: Translation Dutch (AI) 2 years ago
Daniel Hiller 5e400c5c4e Add: Translation Hindi (AI) 2 years ago
Daniel Hiller 8367fa4a29 Add: Dutch translation (AI) 2 years ago

@ -1,53 +0,0 @@
name: "🐛 Bug Report"
description: Report a bug found while using Planka
title: "[Bug]: "
labels: ["Type: Bug", "Status: Triage"]
body:
- type: dropdown
id: issue-type
attributes:
label: Where is the problem occurring?
description: Select the part of the application where you encountered the issue.
options:
- "I encountered the problem while using the application (Frontend)"
- "I encountered the problem while interacting with the server (Backend)"
- "I'm not sure"
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Brave
- Chrome
- Firefox
- Microsoft Edge
- Safari
- Other
- type: textarea
id: current-behavior
attributes:
label: Current behaviour
description: A description of what is currently happening, including screenshots and other useful information (**DO NOT INCLUDE PRIVATE INFORMATION**).
placeholder: Currently...
validations:
required: true
- type: textarea
id: desired-behavior
attributes:
label: Desired behaviour
description: A clear description of what you think should happen.
placeholder: In this situation, I expected ...
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: Clearly describe which steps or actions you have taken to arrive at the problem. If you have some experience with the code, please link to the specific pieces of code.
placeholder: I did X, then Y, before arriving at Z, when ERROR ...
validations:
required: true
- type: textarea
id: other
attributes:
label: Other information
description: Any other details?

@ -1,33 +0,0 @@
name: "✨ Feature Request"
description: Suggest a feature or enhancement to improve Planka.
labels: ["Type: Idea"]
body:
- type: dropdown
id: idea-type
attributes:
label: Is this a feature for the backend or frontend?
multiple: true
options:
- Backend
- Frontend
validations:
required: true
- type: textarea
id: feature
attributes:
label: What would you like?
description: A clear description of the feature or enhancement wanted.
placeholder: I'd like to be able to...
validations:
required: true
- type: textarea
id: reason
attributes:
label: Why is this needed?
description: A clear description of why this would be useful to have.
placeholder: I want this because...
- type: textarea
id: other
attributes:
label: Other information
placeholder: Any other details?

@ -40,5 +40,5 @@ jobs:
build-args: ALPINE_VERSION=${{ env.ALPINE_VERSION }} build-args: ALPINE_VERSION=${{ env.ALPINE_VERSION }}
push: true push: true
tags: | tags: |
ghcr.io/${{ github.repository }}:base-latest ghcr.io/plankanban/planka:base-latest
ghcr.io/${{ github.repository }}:base-${{ env.ALPINE_VERSION }} ghcr.io/plankanban/planka:base-${{ env.ALPINE_VERSION }}

@ -1,20 +1,11 @@
# https://docs.docker.com/build/ci/github-actions/multi-platform/
name: Build and push Docker DEV image name: Build and push Docker DEV image
on: on:
push: push:
paths-ignore:
- '.github/**'
- 'charts/**'
- 'docker-*.sh'
- '*.md'
branches: [master] branches: [master]
workflow_dispatch:
env:
REGISTRY_IMAGE: ghcr.io/${{ github.repository }}
jobs: jobs:
build: build-and-push-docker-image-dev:
runs-on: self-hosted runs-on: self-hosted
steps: steps:
- name: Checkout - name: Checkout
@ -33,21 +24,11 @@ jobs:
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
type=raw,value=dev
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true push: true
tags: ${{ steps.metadata.outputs.tags }} tags: |
labels: ${{ steps.metadata.outputs.labels }} ghcr.io/plankanban/planka:dev
cache-from: type=gha
cache-to: type=gha,mode=max

@ -31,23 +31,12 @@ jobs:
result-encoding: string result-encoding: string
script: return context.payload.release.tag_name.replace('v', '') script: return context.payload.release.tag_name.replace('v', '')
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v5
with:
images: |
name=ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ steps.set-version.outputs.result }}
type=raw,value=latest
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true push: true
tags: ${{ steps.metadata.outputs.tags }} tags: |
labels: ${{ steps.metadata.outputs.labels }} ghcr.io/plankanban/planka:latest
cache-from: type=gha ghcr.io/plankanban/planka:${{ steps.set-version.outputs.result }}
cache-to: type=gha,mode=max

@ -35,9 +35,8 @@ jobs:
done done
- name: Run chart-releaser for stable - name: Run chart-releaser for stable
uses: helm/chart-releaser-action@v1.6.0 uses: helm/chart-releaser-action@v1.5.0
with: with:
charts_dir: charts charts_dir: charts
mark_as_latest: false
env: env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

@ -1,2 +1,4 @@
#!/bin/sh #!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged npx lint-staged

@ -1,44 +1,35 @@
FROM node:18-alpine as server-dependencies FROM ghcr.io/plankanban/planka:base-latest as server-dependencies
RUN apk -U upgrade \
&& apk add build-base python3 \
--no-cache
WORKDIR /app WORKDIR /app
COPY server/package.json server/package-lock.json ./ COPY server/package.json server/package-lock.json .
RUN npm install npm@latest --global \ RUN npm install npm@latest --global \
&& npm install pnpm --global \ && npm install pnpm --global \
&& pnpm import \
&& pnpm install --prod && pnpm install --prod
FROM node:lts AS client FROM node:lts AS client
WORKDIR /app WORKDIR /app
COPY client/package.json client/package-lock.json ./ COPY client/package.json client/package-lock.json .
RUN npm install npm@latest --global \ RUN npm install npm@latest --global \
&& npm install pnpm --global \ && npm install pnpm --global \
&& pnpm import \
&& pnpm install --prod && pnpm install --prod
COPY client . COPY client .
RUN DISABLE_ESLINT_PLUGIN=true npm run build RUN DISABLE_ESLINT_PLUGIN=true npm run build
FROM node:18-alpine FROM ghcr.io/plankanban/planka:base-latest
RUN apk -U upgrade \ RUN apk del vips-dependencies --purge
&& apk add bash \
--no-cache
USER node USER node
WORKDIR /app WORKDIR /app
COPY --chown=node:node start.sh . COPY --chown=node:node start.sh .
COPY --chown=node:node server . COPY --chown=node:node server .
COPY --chown=node:node healthcheck.js .
RUN mv .env.sample .env RUN mv .env.sample .env
@ -53,8 +44,4 @@ VOLUME /app/private/attachments
EXPOSE 1337 EXPOSE 1337
HEALTHCHECK --interval=10s --timeout=2s --start-period=15s \ CMD ["./start.sh"]
CMD node ./healthcheck.js
CMD [ "bash", "start.sh" ]

@ -1,22 +1,27 @@
FROM node:18-alpine FROM node:lts-alpine
ARG VIPS_VERSION=8.14.5 ARG ALPINE_VERSION=3.16
ARG VIPS_VERSION=8.13.3
RUN apk -U upgrade \ RUN apk -U upgrade \
&& apk add \ && apk add \
bash pkgconf \ bash giflib glib lcms2 libexif \
libjpeg-turbo libexif librsvg cgif tiff libspng libimagequant \ libgsf libjpeg-turbo libpng librsvg libwebp \
orc pango tiff \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
--no-cache \ --no-cache \
&& apk add \ && apk add \
build-base gobject-introspection-dev meson \ build-base giflib-dev glib-dev lcms2-dev libexif-dev \
libjpeg-turbo-dev libexif-dev librsvg-dev cgif-dev tiff-dev libspng-dev libimagequant-dev \ libgsf-dev libjpeg-turbo-dev libpng-dev librsvg-dev libwebp-dev \
orc-dev pango-dev tiff-dev \
--virtual vips-dependencies \ --virtual vips-dependencies \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
--no-cache \ --no-cache \
&& wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz | tar xJC /tmp \ && wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.gz | tar xzC /tmp \
&& cd /tmp/vips-${VIPS_VERSION} \ && cd /tmp/vips-${VIPS_VERSION} \
&& meson setup build-dir \ && ./configure \
&& cd build-dir \ && make \
&& ninja \ && make install-strip \
&& ninja test \
&& ninja install \
&& rm -rf /tmp/vips-${VIPS_VERSION} && rm -rf /tmp/vips-${VIPS_VERSION}

@ -1,7 +1,7 @@
# Planka # Planka
#### Elegant open source project tracking. #### Elegant open source project tracking
![David (path)](https://img.shields.io/github/package-json/v/plankanban/planka) ![Docker Pulls](https://img.shields.io/badge/docker_pulls-4M%2B-%23066da5) ![GitHub](https://img.shields.io/github/license/plankanban/planka) ![David (path)](https://img.shields.io/github/package-json/v/plankanban/planka) ![Docker Pulls](https://img.shields.io/docker/pulls/meltyshev/planka) ![GitHub](https://img.shields.io/github/license/plankanban/planka)
![](https://raw.githubusercontent.com/plankanban/planka/master/demo.gif) ![](https://raw.githubusercontent.com/plankanban/planka/master/demo.gif)
@ -10,18 +10,20 @@
## Features ## Features
- Create projects, boards, lists, cards, labels and tasks - Create projects, boards, lists, cards, labels and tasks
- Add card members, track time, set due dates, add attachments, write comments - Add card members, track time, set a due date, add attachments, write comments
- Markdown support in card description and comments - Markdown support in a card description and comment
- Filter by members and labels - Filter by members and labels
- Customize project backgrounds - Customize project background
- Real-time updates - Real-time updates
- Internal notifications - User notifications
- Multiple interface languages - Internationalization
- Single sign-on via OpenID Connect
## How to deploy Planka ## How to deploy Planka
There are many ways to install Planka, [check them out](https://docs.planka.cloud/docs/intro). There are 2 types of installation:
- [Without Docker](https://docs.planka.cloud/docs/installl-planka/Debian%20&%20Ubuntu) ([for Windows](https://docs.planka.cloud/docs/installl-planka/Windows))
- [Dockerized](https://docs.planka.cloud/docs/installl-planka/Docker%20Compose)
- [Automated installation](https://github.com/plankanban/planka-installer)
For configuration, please see the [configuration section](https://docs.planka.cloud/docs/category/configuration). For configuration, please see the [configuration section](https://docs.planka.cloud/docs/category/configuration).
@ -45,7 +47,3 @@ See the [development section](https://docs.planka.cloud/docs/Development).
## License ## License
Planka is [AGPL-3.0 licensed](https://github.com/plankanban/planka/blob/master/LICENSE). Planka is [AGPL-3.0 licensed](https://github.com/plankanban/planka/blob/master/LICENSE).
## Contributors
[![](https://contrib.rocks/image?repo=plankanban/planka)](https://github.com/plankanban/planka/graphs/contributors)

@ -15,16 +15,16 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.2.8 version: 0.1.2
# This is the version number of the application being deployed. This version number should be # This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
appVersion: "1.22.0" appVersion: "1.12.0"
dependencies: dependencies:
- alias: postgresql - alias: postgresql
condition: postgresql.enabled condition: postgresql.enabled
name: postgresql name: postgresql
repository: &bitnami-repo https://charts.bitnami.com/bitnami repository: &bitnami-repo https://charts.bitnami.com/bitnami

@ -14,22 +14,14 @@ If you want to fully uninstall this chart including the data, follow [these step
## Usage ## Usage
If you just want to spin up an instance using help, please see [these docs](https://docs.planka.cloud/docs/installation/kubernetes/helm_chart/). If you want to make changes to the chart locally, and deploy them, see the below section.
## Local Building and Using the Chart
The basic usage of the chart can be found below: The basic usage of the chart can be found below:
```bash ```bash
git clone https://github.com/plankanban/planka.git git clone https://github.com/Chris-Greaves/planka-helm-chart.git
cd planka/charts/planka cd planka-helm-chart
helm dependency build helm dependency build
export SECRETKEY=$(openssl rand -hex 64) export SECRETKEY=$(openssl rand -hex 64)
helm install planka . --set secretkey=$SECRETKEY \ helm install planka . --set secretkey=$SECRETKEY
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo"
``` ```
> **NOTE:** The command `openssl rand -hex 64` is needed to create a random hexadecimal key for planka. On Windows you can use Git Bash to run that command. > **NOTE:** The command `openssl rand -hex 64` is needed to create a random hexadecimal key for planka. On Windows you can use Git Bash to run that command.
@ -47,19 +39,11 @@ To access Planka externally you can use the following configuration
```bash ```bash
# HTTP only # HTTP only
helm install planka . --set secretkey=$SECRETKEY \ helm install planka . --set secretkey=$SECRETKEY \
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo" \
--set ingress.enabled=true \ --set ingress.enabled=true \
--set ingress.hosts[0].host=planka.example.dev \ --set ingress.hosts[0].host=planka.example.dev \
# HTTPS # HTTPS
helm install planka . --set secretkey=$SECRETKEY \ helm install planka . --set secretkey=$SECRETKEY \
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo" \
--set ingress.enabled=true \ --set ingress.enabled=true \
--set ingress.hosts[0].host=planka.example.dev \ --set ingress.hosts[0].host=planka.example.dev \
--set ingress.tls[0].secretName=planka-tls \ --set ingress.tls[0].secretName=planka-tls \
@ -70,17 +54,6 @@ or create a values.yaml file like:
```yaml ```yaml
secretkey: "<InsertSecretKey>" secretkey: "<InsertSecretKey>"
# The admin section needs to be present for new instances of Planka, after the first start you can remove the lines starting with admin_. If you want the admin user to be unchangeable admin_email: has to stay
# After changing the config you have to run ```helm upgrade planka . -f values.yaml```
# Admin user
admin_email: "demo@demo.demo" # Do not remove if you want to prevent this user from being edited/deleted
admin_password: "demo"
admin_name: "Demo Demo"
admin_username: "demo"
# Admin user
# Ingress
ingress: ingress:
enabled: true enabled: true
hosts: hosts:

@ -11,10 +11,6 @@ spec:
selector: selector:
matchLabels: matchLabels:
{{- include "planka.selectorLabels" . | nindent 6 }} {{- include "planka.selectorLabels" . | nindent 6 }}
{{- if .Values.persistence.enabled }}
strategy:
type: Recreate
{{- end }}
template: template:
metadata: metadata:
{{- with .Values.podAnnotations }} {{- with .Values.podAnnotations }}
@ -39,7 +35,7 @@ spec:
imagePullPolicy: {{ .Values.image.pullPolicy }} imagePullPolicy: {{ .Values.image.pullPolicy }}
ports: ports:
- name: http - name: http
containerPort: {{ .Values.service.containerPort | default 1337 }} containerPort: {{ .Values.service.port }}
protocol: TCP protocol: TCP
livenessProbe: livenessProbe:
httpGet: httpGet:
@ -59,32 +55,14 @@ spec:
- mountPath: /app/private/attachments - mountPath: /app/private/attachments
subPath: attachments subPath: attachments
name: planka name: planka
{{- if .Values.securityContext.readOnlyRootFilesystem }}
- mountPath: /app/logs
subPath: app-logs
name: emptydir
{{- end }}
resources: resources:
{{- toYaml .Values.resources | nindent 12 }} {{- toYaml .Values.resources | nindent 12 }}
env: env:
{{- if not .Values.postgresql.enabled }}
{{- if .Values.existingDburlSecret }}
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ .Values.existingDburlSecret }}
key: uri
{{- else }}
- name: DATABASE_URL
value: {{ required "If the included postgresql deployment is disabled you need to provide an existing secret in .Values.existingDburlSecret or define a Database URL in 'dburl'" .Values.dburl }}
{{- end }}
{{- else }}
- name: DATABASE_URL - name: DATABASE_URL
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: planka-postgresql-svcbind-custom-user name: planka-postgresql-svcbind-custom-user
key: uri key: uri
{{- end }}
- name: BASE_URL - name: BASE_URL
{{- if .Values.baseUrl }} {{- if .Values.baseUrl }}
value: {{ .Values.baseUrl }} value: {{ .Values.baseUrl }}
@ -94,68 +72,9 @@ spec:
value: http://localhost:3000 value: http://localhost:3000
{{- end }} {{- end }}
- name: SECRET_KEY - name: SECRET_KEY
{{- if .Values.existingSecretkeySecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.existingSecretkeySecret }}
key: key
{{- else }}
value: {{ required "A secret key needs to be generated using 'openssl rand -hex 64' and assigned to secretkey." .Values.secretkey }} value: {{ required "A secret key needs to be generated using 'openssl rand -hex 64' and assigned to secretkey." .Values.secretkey }}
{{- end }}
- name: TRUST_PROXY - name: TRUST_PROXY
value: "0" value: "0"
- name: DEFAULT_ADMIN_EMAIL
value: {{ .Values.admin_email }}
- name: DEFAULT_ADMIN_NAME
value: {{ .Values.admin_name }}
{{- if .Values.existingAdminCredsSecret }}
- name: DEFAULT_ADMIN_USERNAME
valueFrom:
secretKeyRef:
name: {{ .Values.existingAdminCredsSecret }}
key: username
- name: DEFAULT_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.existingAdminCredsSecret }}
key: password
{{- else }}
- name: DEFAULT_ADMIN_USERNAME
value: {{ .Values.admin_username }}
- name: DEFAULT_ADMIN_PASSWORD
value: {{ .Values.admin_password }}
{{- end }}
{{ range $k, $v := .Values.env }}
- name: {{ $k | quote }}
value: {{ $v | quote }}
{{- end }}
{{- if .Values.oidc.enabled }}
{{- $secretName := default (printf "%s-oidc" (include "planka.fullname" .)) .Values.oidc.existingSecret }}
- name: OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
key: clientId
name: {{ $secretName }}
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
key: clientSecret
name: {{ $secretName }}
- name: OIDC_ISSUER
value: {{ required "issuerUrl is required when configuring OIDC" .Values.oidc.issuerUrl | quote }}
- name: OIDC_SCOPES
value: {{ join " " .Values.oidc.scopes | default "openid profile email" | quote }}
{{- if .Values.oidc.admin.roles }}
- name: OIDC_ADMIN_ROLES
value: {{ join "," .Values.oidc.admin.roles | quote }}
{{- end }}
- name: OIDC_ROLES_ATTRIBUTE
value: {{ .Values.oidc.admin.rolesAttribute | default "groups" | quote }}
{{- if .Values.oidc.admin.ignoreRoles }}
- name: OIDC_IGNORE_ROLES
value: {{ .Values.oidc.admin.ignoreRoles | quote }}
{{- end }}
{{- end }}
{{- with .Values.nodeSelector }} {{- with .Values.nodeSelector }}
nodeSelector: nodeSelector:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
@ -176,7 +95,3 @@ spec:
{{- else }} {{- else }}
emptyDir: {} emptyDir: {}
{{- end }} {{- end }}
{{- if .Values.securityContext.readOnlyRootFilesystem }}
- name: emptydir
emptyDir: {}
{{- end }}

@ -1,5 +1,5 @@
{{- if .Values.autoscaling.enabled }} {{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2 apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler kind: HorizontalPodAutoscaler
metadata: metadata:
name: {{ include "planka.fullname" . }} name: {{ include "planka.fullname" . }}
@ -17,16 +17,12 @@ spec:
- type: Resource - type: Resource
resource: resource:
name: cpu name: cpu
target: targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }} {{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource - type: Resource
resource: resource:
name: memory name: memory
target: targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }} {{- end }}
{{- end }} {{- end }}

@ -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 }}

@ -17,16 +17,6 @@ fullnameOverride: ""
# Generate a secret using openssl rand -base64 45 # Generate a secret using openssl rand -base64 45
secretkey: "" secretkey: ""
## @param existingSecretkeySecret Name of an existing secret containing the session key string
## NOTE: Must contain key `key`
## NOTE: When it's set, the secretkey parameter is ignored
existingSecretkeySecret: ""
## @param existingAdminCredsSecret Name of an existing secret containing the admin username and password
## NOTE: Must contain keys `username` and `password`
## NOTE: When it's set, the `admin_username` and `admin_password` parameters are ignored
existingAdminCredsSecret: ""
# Base url for Planka. Will override `ingress.hosts[0].host` # Base url for Planka. Will override `ingress.hosts[0].host`
# Defaults to `http://localhost:3000` if ingress is disabled. # Defaults to `http://localhost:3000` if ingress is disabled.
baseUrl: "" baseUrl: ""
@ -56,10 +46,6 @@ securityContext: {}
service: service:
type: ClusterIP type: ClusterIP
port: 1337 port: 1337
## @param service.containerPort Planka HTTP container port
## If empty will default to 1337
##
containerPort: 1337
ingress: ingress:
enabled: false enabled: false
@ -115,84 +101,11 @@ postgresql:
serviceBindings: serviceBindings:
enabled: true enabled: true
## Set this or existingDburlSecret if you disable the built-in postgresql deployment
dburl:
## @param existingDburlSecret Name of an existing secret containing a DBurl connection string
## NOTE: Must contain key `uri`
## NOTE: When it's set, the `dburl` parameter is ignored
##
existingDburlSecret: ""
## PVC-based data storage configuration ## PVC-based data storage configuration
persistence: persistence:
enabled: false enabled: false
# existingClaim: netbox-data # existingClaim: netbox-data
# storageClass: "-" # storageClass: "-"
accessMode: ReadWriteOnce accessMode: ReadWriteOnce
size: 10Gi size: 10Gi
## OpenID Identity Management configuration
##
## Example:
## ---------------
## oidc:
## enabled: true
## clientId: sxxaAIAxVXlCxTmc1YLHBbQr8NL8MqLI2DUbt42d
## clientSecret: om4RTMRVHRszU7bqxB7RZNkHIzA8e4sGYWxeCwIMYQXPwEBWe4SY5a0wwCe9ltB3zrq5f0dnFnp34cEHD7QSMHsKvV9AiV5Z7eqDraMnv0I8IFivmuV5wovAECAYreSI
## issuerUrl: https://auth.local/application/o/planka/
## admin:
## roles:
## - planka-admin
##
## ---------------
## NOTE: A minimal configuration requires setting `clientId`, `clientSecret` and `issuerUrl`. (plus `admin.roles` for administrators)
## ref: https://docs.planka.cloud/docs/Configuration/OIDC
##
oidc:
## @param oidc.enabled Enable single sign-on (SSO) with OpenID Connect (OIDC)
##
enabled: false
## OIDC credentials
## @param oidc.clientId A string unique to the provider that identifies your app.
## @param oidc.clientSecret A secret string that the provider uses to confirm ownership of a client ID.
##
## NOTE: Either specify inline `clientId` and `clientSecret` or refer to them via `existingSecret`
##
clientId: ""
clientSecret: ""
## @param oidc.existingSecret Name of an existing secret containing OIDC credentials
## NOTE: Must contain key `clientId` and `clientSecret`
## NOTE: When it's set, the `clientId` and `clientSecret` parameters are ignored
##
existingSecret: ""
## @param oidc.issuerUrl The OpenID connect metadata document endpoint
##
issuerUrl: ""
## @param oidc.scopes A list of scopes required for OIDC client.
## If empty will default to `openid`, `profile` and `email`
## NOTE: Planka needs the email and name claims
##
scopes: []
## Admin permissions configuration
admin:
## @param oidc.admin.ignoreRoles If set to true, the admin roles will be ignored.
## It is useful if you want to use OIDC for authentication but not for authorization.
## If empty will default to `false`
##
ignoreRoles: false
## @param oidc.admin.rolesAttribute The name of a custom group claim that you have configured in your OIDC provider
## If empty will default to `groups`
##
rolesAttribute: groups
## @param oidc.admin.roles The names of the admin groups
##
roles: []
# - planka-admin

@ -0,0 +1 @@
REACT_APP_VERSION=1.12.0

46096
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -49,76 +49,67 @@
"**/*.test.js" "**/*.test.js"
] ]
} }
],
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
] ]
} }
}, },
"dependencies": { "dependencies": {
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"classnames": "^2.5.1", "classnames": "^2.3.2",
"date-fns": "^2.30.0", "date-fns": "^2.29.3",
"dequal": "^2.0.3", "dequal": "^2.0.3",
"easymde": "^2.18.0", "easymde": "^2.18.0",
"history": "^5.3.0", "history": "^5.3.0",
"i18next": "^23.15.1", "i18next": "^22.0.6",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^7.0.1",
"initials": "^3.1.2", "initials": "^3.1.2",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.1",
"jwt-decode": "^4.0.0", "jwt-decode": "^3.1.2",
"linkify-react": "^4.1.3",
"linkifyjs": "^4.1.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"nanoid": "^5.0.7", "node-sass": "^8.0.0",
"node-sass": "^9.0.0", "photoswipe": "^5.3.3",
"photoswipe": "^5.4.4",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "18.2.0", "react": "^18.2.0",
"react-app-rewired": "^2.2.1", "react-app-rewired": "^2.2.1",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
"react-datepicker": "^4.25.0", "react-datepicker": "^4.8.0",
"react-dom": "18.2.0", "react-dom": "^18.2.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-i18next": "^15.0.2", "react-i18next": "^12.0.0",
"react-input-mask": "^2.0.4", "react-input-mask": "^2.0.4",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.3",
"react-photoswipe-gallery": "^2.2.7", "react-oidc-context": "^2.2.2",
"react-redux": "^8.1.3", "react-photoswipe-gallery": "^2.2.2",
"react-router-dom": "^6.26.2", "react-redux": "^8.0.5",
"react-router-dom": "^6.4.3",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-simplemde-editor": "^5.2.0", "react-simplemde-editor": "^5.2.0",
"react-textarea-autosize": "^8.5.3", "react-textarea-autosize": "^8.4.0",
"redux": "^4.2.1", "redux": "^4.2.0",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-orm": "^0.16.2", "redux-orm": "^0.16.2",
"redux-saga": "^1.3.0", "redux-saga": "^1.2.1",
"remark-breaks": "^4.0.0", "remark-breaks": "^3.0.2",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"reselect": "^4.1.8", "reselect": "^4.1.7",
"sails.io.js": "^1.2.1", "sails.io.js": "^1.2.1",
"semantic-ui-react": "^2.1.5", "semantic-ui-react": "^2.1.3",
"socket.io-client": "^2.5.0", "socket.io-client": "^2.5.0",
"validator": "^13.12.0", "validator": "^13.7.0",
"whatwg-fetch": "^3.6.20", "whatwg-fetch": "^3.6.2",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^13.4.0",
"@testing-library/react": "^15.0.7", "@testing-library/user-event": "^13.5.0",
"@testing-library/user-event": "^14.5.2",
"babel-preset-airbnb": "^5.0.0", "babel-preset-airbnb": "^5.0.0",
"chai": "^4.5.0", "chai": "^4.3.7",
"eslint": "^8.57.0", "eslint": "^8.28.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.30.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.36.1", "eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^4.6.0",
"react-test-renderer": "18.2.0" "react-test-renderer": "^18.2.0"
} }
} }

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"

@ -23,14 +23,10 @@ createCard.failure = (localId, error) => ({
}, },
}); });
const handleCardCreate = (card, cardMemberships, cardLabels, tasks, attachments) => ({ const handleCardCreate = (card) => ({
type: ActionTypes.CARD_CREATE_HANDLE, type: ActionTypes.CARD_CREATE_HANDLE,
payload: { payload: {
card, card,
cardMemberships,
cardLabels,
tasks,
attachments,
}, },
}); });
@ -57,43 +53,10 @@ updateCard.failure = (id, error) => ({
}, },
}); });
const handleCardUpdate = (card, isFetched, cardMemberships, cardLabels, tasks, attachments) => ({ const handleCardUpdate = (card) => ({
type: ActionTypes.CARD_UPDATE_HANDLE, type: ActionTypes.CARD_UPDATE_HANDLE,
payload: { payload: {
card, card,
isFetched,
cardMemberships,
cardLabels,
tasks,
attachments,
},
});
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,
}, },
}); });
@ -126,21 +89,11 @@ const handleCardDelete = (card) => ({
}, },
}); });
const filterText = (boardId, text) => ({
type: ActionTypes.TEXT_FILTER_IN_CURRENT_BOARD,
payload: {
boardId,
text,
},
});
export default { export default {
createCard, createCard,
handleCardCreate, handleCardCreate,
updateCard, updateCard,
handleCardUpdate, handleCardUpdate,
duplicateCard,
deleteCard, deleteCard,
handleCardDelete, handleCardDelete,
filterText,
}; };

@ -39,19 +39,9 @@ const initializeCore = (
}, },
}); });
// TODO: with success? const logout = () => ({
initializeCore.fetchConfig = (config) => ({
type: ActionTypes.CORE_INITIALIZE__CONFIG_FETCH,
payload: {
config,
},
});
const logout = (invalidateAccessToken) => ({
type: ActionTypes.LOGOUT, type: ActionTypes.LOGOUT,
payload: { payload: {},
invalidateAccessToken,
},
}); });
logout.invalidateAccessToken = () => ({ logout.invalidateAccessToken = () => ({

@ -60,38 +60,6 @@ const handleListUpdate = (list) => ({
}, },
}); });
const sortList = (id, data) => ({
type: ActionTypes.LIST_SORT,
payload: {
id,
data,
},
});
sortList.success = (list, cards) => ({
type: ActionTypes.LIST_SORT__SUCCESS,
payload: {
list,
cards,
},
});
sortList.failure = (id, error) => ({
type: ActionTypes.LIST_SORT__FAILURE,
payload: {
id,
error,
},
});
const handleListSort = (list, cards) => ({
type: ActionTypes.LIST_SORT_HANDLE,
payload: {
list,
cards,
},
});
const deleteList = (id) => ({ const deleteList = (id) => ({
type: ActionTypes.LIST_DELETE, type: ActionTypes.LIST_DELETE,
payload: { payload: {
@ -126,8 +94,6 @@ export default {
handleListCreate, handleListCreate,
updateList, updateList,
handleListUpdate, handleListUpdate,
sortList,
handleListSort,
deleteList, deleteList,
handleListDelete, handleListDelete,
}; };

@ -1,12 +1,5 @@
import ActionTypes from '../constants/ActionTypes'; import ActionTypes from '../constants/ActionTypes';
const initializeLogin = (config) => ({
type: ActionTypes.LOGIN_INITIALIZE,
payload: {
config,
},
});
const authenticate = (data) => ({ const authenticate = (data) => ({
type: ActionTypes.AUTHENTICATE, type: ActionTypes.AUTHENTICATE,
payload: { payload: {
@ -28,33 +21,12 @@ authenticate.failure = (error) => ({
}, },
}); });
const authenticateUsingOidc = () => ({
type: ActionTypes.USING_OIDC_AUTHENTICATE,
payload: {},
});
authenticateUsingOidc.success = (accessToken) => ({
type: ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS,
payload: {
accessToken,
},
});
authenticateUsingOidc.failure = (error) => ({
type: ActionTypes.USING_OIDC_AUTHENTICATE__FAILURE,
payload: {
error,
},
});
const clearAuthenticateError = () => ({ const clearAuthenticateError = () => ({
type: ActionTypes.AUTHENTICATE_ERROR_CLEAR, type: ActionTypes.AUTHENTICATE_ERROR_CLEAR,
payload: {}, payload: {},
}); });
export default { export default {
initializeLogin,
authenticate, authenticate,
authenticateUsingOidc,
clearAuthenticateError, clearAuthenticateError,
}; };

@ -1,17 +1,17 @@
import http from './http'; import http from './http';
import socket from './socket';
/* Actions */ /* Actions */
const createAccessToken = (data, headers) => const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
http.post('/access-tokens?withHttpOnlyToken=true', data, headers); const exchangeOidcToken = (accessToken, headers) =>
http.post('/access-tokens/exchange', { token: accessToken }, headers);
const exchangeForAccessTokenUsingOidc = (data, headers) => const deleteCurrentAccessToken = (headers) =>
http.post('/access-tokens/exchange-using-oidc?withHttpOnlyToken=true', data, headers); socket.delete('/access-tokens/me', undefined, headers);
const deleteCurrentAccessToken = (headers) => http.delete('/access-tokens/me', undefined, headers);
export default { export default {
createAccessToken, createAccessToken,
exchangeForAccessTokenUsingOidc,
deleteCurrentAccessToken, deleteCurrentAccessToken,
exchangeOidcToken,
}; };

@ -57,12 +57,6 @@ const updateCard = (id, data, headers) =>
item: transformCard(body.item), item: transformCard(body.item),
})); }));
const duplicateCard = (id, data, headers) =>
socket.post(`/cards/${id}/duplicate`, data, headers).then((body) => ({
...body,
item: transformCard(body.item),
}));
const deleteCard = (id, headers) => const deleteCard = (id, headers) =>
socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({ socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({
...body, ...body,
@ -87,7 +81,6 @@ export default {
getCard, getCard,
updateCard, updateCard,
deleteCard, deleteCard,
duplicateCard,
makeHandleCardCreate, makeHandleCardCreate,
makeHandleCardUpdate, makeHandleCardUpdate,
makeHandleCardDelete, makeHandleCardDelete,

@ -5,11 +5,9 @@ import Config from '../constants/Config';
const http = {}; const http = {};
// TODO: add all methods // TODO: add all methods
['GET', 'POST', 'DELETE'].forEach((method) => { ['POST'].forEach((method) => {
http[method.toLowerCase()] = (url, data, headers) => { http[method.toLowerCase()] = (url, data, headers) => {
const formData = const formData = Object.keys(data).reduce((result, key) => {
data &&
Object.keys(data).reduce((result, key) => {
result.append(key, data[key]); result.append(key, data[key]);
return result; return result;
@ -19,7 +17,6 @@ const http = {};
method, method,
headers, headers,
body: formData, body: formData,
credentials: 'include',
}) })
.then((response) => .then((response) =>
response.json().then((body) => ({ response.json().then((body) => ({

@ -1,6 +1,5 @@
import http from './http'; import http from './http';
import socket from './socket'; import socket from './socket';
import root from './root';
import accessTokens from './access-tokens'; import accessTokens from './access-tokens';
import users from './users'; import users from './users';
import projects from './projects'; import projects from './projects';
@ -21,7 +20,6 @@ import notifications from './notifications';
export { http, socket }; export { http, socket };
export default { export default {
...root,
...accessTokens, ...accessTokens,
...users, ...users,
...projects, ...projects,

@ -1,5 +1,4 @@
import socket from './socket'; import socket from './socket';
import { transformCard } from './cards';
/* Actions */ /* Actions */
@ -8,33 +7,10 @@ const createList = (boardId, data, headers) =>
const updateList = (id, data, headers) => socket.patch(`/lists/${id}`, data, headers); const updateList = (id, data, headers) => socket.patch(`/lists/${id}`, data, headers);
const sortList = (id, data, headers) =>
socket.post(`/lists/${id}/sort`, data, headers).then((body) => ({
...body,
included: {
...body.included,
cards: body.included.cards.map(transformCard),
},
}));
const deleteList = (id, headers) => socket.delete(`/lists/${id}`, undefined, headers); const deleteList = (id, headers) => socket.delete(`/lists/${id}`, undefined, headers);
/* Event handlers */
const makeHandleListSort = (next) => (body) => {
next({
...body,
included: {
...body.included,
cards: body.included.cards.map(transformCard),
},
});
};
export default { export default {
createList, createList,
updateList, updateList,
sortList,
deleteList, deleteList,
makeHandleListSort,
}; };

@ -1,9 +0,0 @@
import http from './http';
/* Actions */
const getConfig = (headers) => http.get('/config', undefined, headers);
export default {
getConfig,
};

@ -13,7 +13,7 @@ io.sails.environment = process.env.NODE_ENV;
const { socket } = io; const { socket } = io;
socket.path = `${Config.SERVER_BASE_PATH}/socket.io`; socket.path = `${Config.BASE_PATH}/socket.io`;
socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => { ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => {

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 559 KiB

@ -1,8 +1,6 @@
:global(#app) { :global(#app) {
.wrapper { .wrapper {
height: 100%; height: 100%;
max-height: 100vh;
max-width: 100vw;
position: fixed; position: fixed;
width: 100%; width: 100%;
z-index: -1; z-index: -1;

@ -48,6 +48,16 @@
min-width: 100%; min-width: 100%;
} }
.panel {
align-items: center;
display: flex;
margin-bottom: 20px;
}
.panelItem {
margin-right: 20px;
}
.wrapper { .wrapper {
margin: 0 20px; margin: 0 20px;
} }

@ -13,7 +13,6 @@ const BoardActions = React.memo(
labels, labels,
filterUsers, filterUsers,
filterLabels, filterLabels,
filterText,
allUsers, allUsers,
canEdit, canEdit,
canEditMemberships, canEditMemberships,
@ -28,7 +27,6 @@ const BoardActions = React.memo(
onLabelUpdate, onLabelUpdate,
onLabelMove, onLabelMove,
onLabelDelete, onLabelDelete,
onTextFilterUpdate,
}) => { }) => {
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
@ -48,7 +46,6 @@ const BoardActions = React.memo(
<Filters <Filters
users={filterUsers} users={filterUsers}
labels={filterLabels} labels={filterLabels}
filterText={filterText}
allBoardMemberships={memberships} allBoardMemberships={memberships}
allLabels={labels} allLabels={labels}
canEdit={canEdit} canEdit={canEdit}
@ -60,7 +57,6 @@ const BoardActions = React.memo(
onLabelUpdate={onLabelUpdate} onLabelUpdate={onLabelUpdate}
onLabelMove={onLabelMove} onLabelMove={onLabelMove}
onLabelDelete={onLabelDelete} onLabelDelete={onLabelDelete}
onTextFilterUpdate={onTextFilterUpdate}
/> />
</div> </div>
</div> </div>
@ -75,7 +71,6 @@ BoardActions.propTypes = {
labels: PropTypes.array.isRequired, labels: PropTypes.array.isRequired,
filterUsers: PropTypes.array.isRequired, filterUsers: PropTypes.array.isRequired,
filterLabels: PropTypes.array.isRequired, filterLabels: PropTypes.array.isRequired,
filterText: PropTypes.string.isRequired,
allUsers: PropTypes.array.isRequired, allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
canEdit: PropTypes.bool.isRequired, canEdit: PropTypes.bool.isRequired,
@ -91,7 +86,6 @@ BoardActions.propTypes = {
onLabelUpdate: PropTypes.func.isRequired, onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired, onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired, onLabelDelete: PropTypes.func.isRequired,
onTextFilterUpdate: PropTypes.func.isRequired,
}; };
export default BoardActions; export default BoardActions;

@ -1,15 +1,12 @@
:global(#app) { :global(#app) {
.action { .action {
align-items: center;
display: flex;
flex: 0 0 auto; flex: 0 0 auto;
margin-right: 20px;
} }
.actions { .actions {
align-items: center; align-items: center;
display: flex; display: flex;
gap: 20px;
justify-content: flex-start;
margin: 20px 20px; margin: 20px 20px;
} }

@ -1,10 +1,7 @@
import React, { useCallback, useRef, useState } from 'react'; import React, { useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon } from 'semantic-ui-react';
import { usePopup } from '../../lib/popup'; import { usePopup } from '../../lib/popup';
import { Input } from '../../lib/custom-ui';
import User from '../User'; import User from '../User';
import Label from '../Label'; import Label from '../Label';
@ -17,7 +14,6 @@ const Filters = React.memo(
({ ({
users, users,
labels, labels,
filterText,
allBoardMemberships, allBoardMemberships,
allLabels, allLabels,
canEdit, canEdit,
@ -29,17 +25,8 @@ const Filters = React.memo(
onLabelUpdate, onLabelUpdate,
onLabelMove, onLabelMove,
onLabelDelete, onLabelDelete,
onTextFilterUpdate,
}) => { }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [isSearchFocused, setIsSearchFocused] = useState(false);
const searchFieldRef = useRef(null);
const cancelSearch = useCallback(() => {
onTextFilterUpdate('');
searchFieldRef.current.blur();
}, [onTextFilterUpdate]);
const handleRemoveUserClick = useCallback( const handleRemoveUserClick = useCallback(
(id) => { (id) => {
@ -55,39 +42,9 @@ const Filters = React.memo(
[onLabelRemove], [onLabelRemove],
); );
const handleSearchChange = useCallback(
(_, { value }) => {
onTextFilterUpdate(value);
},
[onTextFilterUpdate],
);
const handleSearchFocus = useCallback(() => {
setIsSearchFocused(true);
}, []);
const handleSearchKeyDown = useCallback(
(event) => {
if (event.key === 'Escape') {
cancelSearch();
}
},
[cancelSearch],
);
const handleSearchBlur = useCallback(() => {
setIsSearchFocused(false);
}, []);
const handleCancelSearchClick = useCallback(() => {
cancelSearch();
}, [cancelSearch]);
const BoardMembershipsPopup = usePopup(BoardMembershipsStep); const BoardMembershipsPopup = usePopup(BoardMembershipsStep);
const LabelsPopup = usePopup(LabelsStep); const LabelsPopup = usePopup(LabelsStep);
const isSearchActive = filterText || isSearchFocused;
return ( return (
<> <>
<span className={styles.filter}> <span className={styles.filter}>
@ -143,25 +100,6 @@ const Filters = React.memo(
</span> </span>
))} ))}
</span> </span>
<span className={styles.filter}>
<Input
ref={searchFieldRef}
value={filterText}
placeholder={t('common.searchCards')}
icon={
isSearchActive ? (
<Icon link name="cancel" onClick={handleCancelSearchClick} />
) : (
'search'
)
}
className={classNames(styles.search, !isSearchActive && styles.searchInactive)}
onFocus={handleSearchFocus}
onKeyDown={handleSearchKeyDown}
onChange={handleSearchChange}
onBlur={handleSearchBlur}
/>
</span>
</> </>
); );
}, },
@ -171,7 +109,6 @@ Filters.propTypes = {
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired, users: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired, labels: PropTypes.array.isRequired,
filterText: PropTypes.string.isRequired,
allBoardMemberships: PropTypes.array.isRequired, allBoardMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired, allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
@ -184,7 +121,6 @@ Filters.propTypes = {
onLabelUpdate: PropTypes.func.isRequired, onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired, onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired, onLabelDelete: PropTypes.func.isRequired,
onTextFilterUpdate: PropTypes.func.isRequired,
}; };
export default Filters; export default Filters;

@ -43,36 +43,4 @@
line-height: 20px; line-height: 20px;
padding: 2px 12px; padding: 2px 12px;
} }
.search {
height: 30px;
margin: 0 12px;
transition: width 0.2s ease;
width: 280px;
@media only screen and (max-width: 797px) {
width: 220px;
}
input {
font-size: 13px;
}
}
.searchInactive {
color: #fff;
height: 24px;
width: 220px;
input {
background: rgba(0, 0, 0, 0.24);
border: none;
color: #fff !important;
font-size: 12px;
&::placeholder {
color: #fff;
}
}
}
} }

@ -4,6 +4,7 @@
max-height: 60vh; max-height: 60vh;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin;
width: 100%; width: 100%;
&::-webkit-scrollbar { &::-webkit-scrollbar {

@ -85,6 +85,7 @@
height: 56px; height: 56px;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
scrollbar-width: thin;
&:hover { &:hover {
height: 38px; height: 38px;

@ -36,7 +36,6 @@ const ActionsStep = React.memo(
onUpdate, onUpdate,
onMove, onMove,
onTransfer, onTransfer,
onDuplicate,
onDelete, onDelete,
onUserAdd, onUserAdd,
onUserRemove, onUserRemove,
@ -77,11 +76,6 @@ const ActionsStep = React.memo(
openStep(StepTypes.MOVE); openStep(StepTypes.MOVE);
}, [openStep]); }, [openStep]);
const handleDuplicateClick = useCallback(() => {
onDuplicate();
onClose();
}, [onDuplicate, onClose]);
const handleDeleteClick = useCallback(() => { const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE); openStep(StepTypes.DELETE);
}, [openStep]); }, [openStep]);
@ -213,11 +207,6 @@ const ActionsStep = React.memo(
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDuplicateClick}>
{t('action.duplicateCard', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}> <Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteCard', { {t('action.deleteCard', {
context: 'title', context: 'title',
@ -243,7 +232,6 @@ ActionsStep.propTypes = {
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired, onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired, onTransfer: PropTypes.func.isRequired,
onDuplicate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired, onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired, onUserRemove: PropTypes.func.isRequired,

@ -24,7 +24,6 @@ const Card = React.memo(
index, index,
name, name,
dueDate, dueDate,
isDueDateCompleted,
stopwatch, stopwatch,
coverUrl, coverUrl,
boardId, boardId,
@ -42,7 +41,6 @@ const Card = React.memo(
onUpdate, onUpdate,
onMove, onMove,
onTransfer, onTransfer,
onDuplicate,
onDelete, onDelete,
onUserAdd, onUserAdd,
onUserRemove, onUserRemove,
@ -121,7 +119,7 @@ const Card = React.memo(
)} )}
{dueDate && ( {dueDate && (
<span className={classNames(styles.attachment, styles.attachmentLeft)}> <span className={classNames(styles.attachment, styles.attachmentLeft)}>
<DueDate value={dueDate} isCompleted={isDueDateCompleted} size="tiny" /> <DueDate value={dueDate} size="tiny" />
</span> </span>
)} )}
{stopwatch && ( {stopwatch && (
@ -187,7 +185,6 @@ const Card = React.memo(
onUpdate={onUpdate} onUpdate={onUpdate}
onMove={onMove} onMove={onMove}
onTransfer={onTransfer} onTransfer={onTransfer}
onDuplicate={onDuplicate}
onDelete={onDelete} onDelete={onDelete}
onUserAdd={onUserAdd} onUserAdd={onUserAdd}
onUserRemove={onUserRemove} onUserRemove={onUserRemove}
@ -222,7 +219,6 @@ Card.propTypes = {
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
dueDate: PropTypes.instanceOf(Date), dueDate: PropTypes.instanceOf(Date),
isDueDateCompleted: PropTypes.bool,
stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types
coverUrl: PropTypes.string, coverUrl: PropTypes.string,
boardId: PropTypes.string.isRequired, boardId: PropTypes.string.isRequired,
@ -242,7 +238,6 @@ Card.propTypes = {
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired, onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired, onTransfer: PropTypes.func.isRequired,
onDuplicate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired, onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired, onUserRemove: PropTypes.func.isRequired,
@ -257,7 +252,6 @@ Card.propTypes = {
Card.defaultProps = { Card.defaultProps = {
dueDate: undefined, dueDate: undefined,
isDueDateCompleted: undefined,
stopwatch: undefined, stopwatch: undefined,
coverUrl: undefined, coverUrl: undefined,
}; };

@ -21,14 +21,6 @@
background: #ebeef0; background: #ebeef0;
color: #516b7a; color: #516b7a;
} }
@media only screen and (max-width: 797px) {
&:focus,
&:active {
background: #ebeef0;
color: #516b7a;
}
}
} }
.attachment { .attachment {
@ -63,12 +55,6 @@
box-shadow: 0 1px 0 #ccc; box-shadow: 0 1px 0 #ccc;
position: relative; position: relative;
@media only screen and (max-width: 797px) {
.target {
opacity: 1;
}
}
&:hover { &:hover {
background: #f5f6f7; background: #f5f6f7;
border-bottom-color: rgba(9, 30, 66, 0.25); border-bottom-color: rgba(9, 30, 66, 0.25);

@ -5,7 +5,6 @@ import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react'; import { Button, Form, TextArea } from 'semantic-ui-react';
import { useClosableForm, useField } from '../../hooks'; import { useClosableForm, useField } from '../../hooks';
import { focusEnd } from '../../utils/element-helpers';
import styles from './NameEdit.module.scss'; import styles from './NameEdit.module.scss';
@ -80,7 +79,7 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
useEffect(() => { useEffect(() => {
if (isOpened) { if (isOpened) {
focusEnd(field.current.ref.current); field.current.ref.current.focus();
} }
}, [isOpened]); }, [isOpened]);

@ -4,8 +4,6 @@ import classNames from 'classnames';
import { Progress } from 'semantic-ui-react'; import { Progress } from 'semantic-ui-react';
import { useToggle } from '../../lib/hooks'; import { useToggle } from '../../lib/hooks';
import Linkify from '../Linkify';
import styles from './Tasks.module.scss'; import styles from './Tasks.module.scss';
const Tasks = React.memo(({ items }) => { const Tasks = React.memo(({ items }) => {
@ -50,7 +48,7 @@ const Tasks = React.memo(({ items }) => {
key={item.id} key={item.id}
className={classNames(styles.task, item.isCompleted && styles.taskCompleted)} className={classNames(styles.task, item.isCompleted && styles.taskCompleted)}
> >
<Linkify linkStopPropagation>{item.name}</Linkify> {item.name}
</li> </li>
))} ))}
</ul> </ul>

@ -55,10 +55,8 @@
display: block; display: block;
font-size: 12px; font-size: 12px;
line-height: 14px; line-height: 14px;
overflow: hidden;
padding-bottom: 6px; padding-bottom: 6px;
padding-left: 14px; padding-left: 14px;
text-overflow: ellipsis;
&:before { &:before {
content: ""; content: "";

@ -6,7 +6,6 @@ import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react'; import { Button, Form, TextArea } from 'semantic-ui-react';
import { useForm } from '../../../hooks'; import { useForm } from '../../../hooks';
import { focusEnd } from '../../../utils/element-helpers';
import styles from './CommentEdit.module.scss'; import styles from './CommentEdit.module.scss';
@ -71,7 +70,7 @@ const CommentEdit = React.forwardRef(({ children, defaultData, onUpdate }, ref)
useEffect(() => { useEffect(() => {
if (isOpened) { if (isOpened) {
focusEnd(textField.current.ref.current); textField.current.ref.current.focus();
} }
}, [isOpened]); }, [isOpened]);

@ -4,7 +4,6 @@ import classNames from 'classnames';
import { useTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';
import { Comment } from 'semantic-ui-react'; import { Comment } from 'semantic-ui-react';
import getDateFormat from '../../../utils/get-date-format';
import { ActivityTypes } from '../../../constants/Enums'; import { ActivityTypes } from '../../../constants/Enums';
import ItemComment from './ItemComment'; import ItemComment from './ItemComment';
import User from '../../User'; import User from '../../User';
@ -67,7 +66,7 @@ const Item = React.memo(({ type, data, createdAt, user }) => {
<div className={classNames(styles.content)}> <div className={classNames(styles.content)}>
<div>{contentNode}</div> <div>{contentNode}</div>
<span className={styles.date}> <span className={styles.date}>
{t(`format:${getDateFormat(createdAt)}`, { {t('format:longDateTime', {
postProcess: 'formatDate', postProcess: 'formatDate',
value: createdAt, value: createdAt,
})} })}

@ -6,7 +6,6 @@ import { Comment } from 'semantic-ui-react';
import { usePopup } from '../../../lib/popup'; import { usePopup } from '../../../lib/popup';
import { Markdown } from '../../../lib/custom-ui'; import { Markdown } from '../../../lib/custom-ui';
import getDateFormat from '../../../utils/get-date-format';
import CommentEdit from './CommentEdit'; import CommentEdit from './CommentEdit';
import User from '../../User'; import User from '../../User';
import DeleteStep from '../../DeleteStep'; import DeleteStep from '../../DeleteStep';
@ -34,7 +33,7 @@ const ItemComment = React.memo(
<div className={styles.title}> <div className={styles.title}>
<span className={styles.author}>{user.name}</span> <span className={styles.author}>{user.name}</span>
<span className={styles.date}> <span className={styles.date}>
{t(`format:${getDateFormat(createdAt)}`, { {t('format:longDateTime', {
postProcess: 'formatDate', postProcess: 'formatDate',
value: createdAt, value: createdAt,
})} })}

@ -94,7 +94,7 @@ const AttachmentAddZone = React.memo(({ children, onCreate }) => {
return ( return (
<> <>
{/* eslint-disable-next-line react/jsx-props-no-spreading */} {/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div {...getRootProps()}> <div {...getRootProps()} className={styles.wrapper}>
{isDragActive && <div className={styles.dropzone}>{t('common.dropFileToUpload')}</div>} {isDragActive && <div className={styles.dropzone}>{t('common.dropFileToUpload')}</div>}
{children} {children}
{/* eslint-disable-next-line react/jsx-props-no-spreading */} {/* eslint-disable-next-line react/jsx-props-no-spreading */}

@ -2,7 +2,7 @@
.dropzone { .dropzone {
background: white; background: white;
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: 700;
height: 100%; height: 100%;
line-height: 30px; line-height: 30px;
opacity: 0.7; opacity: 0.7;
@ -12,4 +12,8 @@
width: 100%; width: 100%;
z-index: 2001; z-index: 2001;
} }
.wrapper {
overflow: hidden;
}
} }

@ -16,7 +16,7 @@
.contentError { .contentError {
color: #fff; color: #fff;
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: 700;
height: 20px; height: 20px;
width: 470px; width: 470px;
} }

@ -35,7 +35,7 @@
color: #5e6c84; color: #5e6c84;
display: block; display: block;
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: 700;
height: 100%; height: 100%;
line-height: 80px; line-height: 80px;
overflow: hidden; overflow: hidden;
@ -50,7 +50,7 @@
.name { .name {
color: #17394d; color: #17394d;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: 700;
line-height: 20px; line-height: 20px;
word-wrap: break-word; word-wrap: break-word;
} }

@ -1,8 +1,8 @@
import React, { useCallback, useRef, useState } from 'react'; import React, { useCallback, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Checkbox, Grid, Icon, Modal } from 'semantic-ui-react'; import { Button, Grid, Icon, Modal } from 'semantic-ui-react';
import { usePopup } from '../../lib/popup'; import { usePopup } from '../../lib/popup';
import { Markdown } from '../../lib/custom-ui'; import { Markdown } from '../../lib/custom-ui';
@ -32,7 +32,6 @@ const CardModal = React.memo(
name, name,
description, description,
dueDate, dueDate,
isDueDateCompleted,
stopwatch, stopwatch,
isSubscribed, isSubscribed,
isActivitiesFetching, isActivitiesFetching,
@ -56,7 +55,6 @@ const CardModal = React.memo(
onUpdate, onUpdate,
onMove, onMove,
onTransfer, onTransfer,
onDuplicate,
onDelete, onDelete,
onUserAdd, onUserAdd,
onUserRemove, onUserRemove,
@ -82,7 +80,6 @@ const CardModal = React.memo(
onClose, onClose,
}) => { }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [isLinkCopied, setIsLinkCopied] = useState(false);
const isGalleryOpened = useRef(false); const isGalleryOpened = useRef(false);
@ -119,12 +116,6 @@ const CardModal = React.memo(
[onUpdate], [onUpdate],
); );
const handleDueDateCompletionChange = useCallback(() => {
onUpdate({
isDueDateCompleted: !isDueDateCompleted,
});
}, [isDueDateCompleted, onUpdate]);
const handleStopwatchUpdate = useCallback( const handleStopwatchUpdate = useCallback(
(newStopwatch) => { (newStopwatch) => {
onUpdate({ onUpdate({
@ -149,19 +140,6 @@ const CardModal = React.memo(
}); });
}, [isSubscribed, onUpdate]); }, [isSubscribed, onUpdate]);
const handleDuplicateClick = useCallback(() => {
onDuplicate();
onClose();
}, [onDuplicate, onClose]);
const handleCopyLinkClick = useCallback(() => {
navigator.clipboard.writeText(window.location.href);
setIsLinkCopied(true);
setTimeout(() => {
setIsLinkCopied(false);
}, 5000);
}, []);
const handleGalleryOpen = useCallback(() => { const handleGalleryOpen = useCallback(() => {
isGalleryOpened.current = true; isGalleryOpened.current = true;
}, []); }, []);
@ -239,7 +217,6 @@ const CardModal = React.memo(
onUserSelect={onUserAdd} onUserSelect={onUserAdd}
onUserDeselect={onUserRemove} onUserDeselect={onUserRemove}
> >
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<button <button
type="button" type="button"
className={classNames(styles.attachment, styles.dueDate)} className={classNames(styles.attachment, styles.dueDate)}
@ -289,7 +266,6 @@ const CardModal = React.memo(
onMove={onLabelMove} onMove={onLabelMove}
onDelete={onLabelDelete} onDelete={onLabelDelete}
> >
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<button <button
type="button" type="button"
className={classNames(styles.attachment, styles.dueDate)} className={classNames(styles.attachment, styles.dueDate)}
@ -307,24 +283,13 @@ const CardModal = React.memo(
context: 'title', context: 'title',
})} })}
</div> </div>
<span className={classNames(styles.attachment, styles.attachmentDueDate)}> <span className={styles.attachment}>
{canEdit ? ( {canEdit ? (
<>
<Checkbox
checked={isDueDateCompleted}
disabled={!canEdit}
onChange={handleDueDateCompletionChange}
/>
<DueDateEditPopup defaultValue={dueDate} onUpdate={handleDueDateUpdate}> <DueDateEditPopup defaultValue={dueDate} onUpdate={handleDueDateUpdate}>
<DueDate <DueDate value={dueDate} />
withStatusIcon
value={dueDate}
isCompleted={isDueDateCompleted}
/>
</DueDateEditPopup> </DueDateEditPopup>
</>
) : ( ) : (
<DueDate withStatusIcon value={dueDate} isCompleted={isDueDateCompleted} /> <DueDate value={dueDate} />
)} )}
</span> </span>
</div> </div>
@ -349,11 +314,10 @@ const CardModal = React.memo(
)} )}
</span> </span>
{canEdit && ( {canEdit && (
// eslint-disable-next-line jsx-a11y/control-has-associated-label
<button <button
onClick={handleToggleStopwatchClick}
type="button" type="button"
className={classNames(styles.attachment, styles.dueDate)} className={classNames(styles.attachment, styles.dueDate)}
onClick={handleToggleStopwatchClick}
> >
<Icon <Icon
name={stopwatch.startedAt ? 'pause' : 'play'} name={stopwatch.startedAt ? 'pause' : 'play'}
@ -529,23 +493,6 @@ const CardModal = React.memo(
{t('action.move')} {t('action.move')}
</Button> </Button>
</CardMovePopup> </CardMovePopup>
<Button fluid className={styles.actionButton} onClick={handleDuplicateClick}>
<Icon name="copy outline" className={styles.actionIcon} />
{t('action.duplicate')}
</Button>
{window.isSecureContext && (
<Button fluid className={styles.actionButton} onClick={handleCopyLinkClick}>
<Icon
name={isLinkCopied ? 'linkify' : 'unlink'}
className={styles.actionIcon}
/>
{isLinkCopied
? t('common.linkIsCopied')
: t('action.copyLink', {
context: 'title',
})}
</Button>
)}
<DeletePopup <DeletePopup
title="common.deleteCard" title="common.deleteCard"
content="common.areYouSureYouWantToDeleteThisCard" content="common.areYouSureYouWantToDeleteThisCard"
@ -580,7 +527,6 @@ CardModal.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
description: PropTypes.string, description: PropTypes.string,
dueDate: PropTypes.instanceOf(Date), dueDate: PropTypes.instanceOf(Date),
isDueDateCompleted: PropTypes.bool,
stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types
isSubscribed: PropTypes.bool.isRequired, isSubscribed: PropTypes.bool.isRequired,
isActivitiesFetching: PropTypes.bool.isRequired, isActivitiesFetching: PropTypes.bool.isRequired,
@ -606,7 +552,6 @@ CardModal.propTypes = {
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired, onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired, onTransfer: PropTypes.func.isRequired,
onDuplicate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired, onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired, onUserRemove: PropTypes.func.isRequired,
@ -635,7 +580,6 @@ CardModal.propTypes = {
CardModal.defaultProps = { CardModal.defaultProps = {
description: undefined, description: undefined,
dueDate: undefined, dueDate: undefined,
isDueDateCompleted: false,
stopwatch: undefined, stopwatch: undefined,
}; };

@ -20,26 +20,18 @@
.actionIcon { .actionIcon {
color: #17394d; color: #17394d;
display: inline;
margin-right: 8px; margin-right: 8px;
} }
.actions { .actions {
margin-bottom: 24px; margin-bottom: 24px;
@media only screen and (max-width: 797px) {
flex: 1;
}
@media only screen and (max-width: 425px) {
padding: 0;
width: 100%;
}
} }
.actionsTitle { .actionsTitle {
color: #8c8c8c; color: #8c8c8c;
font-size: 12px; font-size: 12px;
font-weight: normal; font-weight: 500;
letter-spacing: 0.04em; letter-spacing: 0.04em;
margin-top: 16px; margin-top: 16px;
text-transform: uppercase; text-transform: uppercase;
@ -58,12 +50,6 @@
max-width: 100%; max-width: 100%;
} }
.attachmentDueDate {
align-items: center;
display: flex;
gap: 4px;
}
.attachments { .attachments {
display: inline-block; display: inline-block;
margin: 0 8px 8px 0; margin: 0 8px 8px 0;
@ -77,11 +63,6 @@
.contentPadding { .contentPadding {
padding: 8px 8px 0 16px; padding: 8px 8px 0 16px;
@media only screen and (max-width: 797px) {
padding-right: 16px;
width: 100% !important;
}
} }
.cursorPointer { .cursorPointer {
@ -158,7 +139,7 @@
.headerTitle { .headerTitle {
color: #17394d; color: #17394d;
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: 700;
line-height: 24px; line-height: 24px;
} }
@ -174,11 +155,6 @@
.modalPadding { .modalPadding {
padding: 0px; padding: 0px;
@media only screen and (max-width: 797px) {
display: flex;
flex-flow: column nowrap;
}
} }
.moduleHeader { .moduleHeader {
@ -209,15 +185,6 @@
.sidebarPadding { .sidebarPadding {
padding: 8px 16px 0 8px; padding: 8px 16px 0 8px;
@media only screen and (max-width: 797px) {
align-items: flex-start;
display: flex;
flex-flow: row wrap;
gap: 10px;
padding-left: 16px;
width: 100% !important;
}
} }
.text { .text {

@ -57,7 +57,6 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
const mdEditorOptions = useMemo( const mdEditorOptions = useMemo(
() => ({ () => ({
autoDownloadFontAwesome: false,
autofocus: true, autofocus: true,
spellChecker: false, spellChecker: false,
status: false, status: false,

@ -6,7 +6,7 @@
box-shadow: none; box-shadow: none;
color: #17394d; color: #17394d;
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: 700;
line-height: 24px; line-height: 24px;
margin: -5px; margin: -5px;
overflow: hidden; overflow: hidden;

@ -8,7 +8,6 @@ import { usePopup } from '../../../lib/popup';
import NameEdit from './NameEdit'; import NameEdit from './NameEdit';
import ActionsStep from './ActionsStep'; import ActionsStep from './ActionsStep';
import Linkify from '../../Linkify';
import styles from './Item.module.scss'; import styles from './Item.module.scss';
@ -66,7 +65,7 @@ const Item = React.memo(
onClick={handleClick} onClick={handleClick}
> >
<span className={classNames(styles.task, isCompleted && styles.taskCompleted)}> <span className={classNames(styles.task, isCompleted && styles.taskCompleted)}>
<Linkify linkStopPropagation>{name}</Linkify> {name}
</span> </span>
</span> </span>
{isPersisted && canEdit && ( {isPersisted && canEdit && (

@ -5,7 +5,6 @@ import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react'; import { Button, Form, TextArea } from 'semantic-ui-react';
import { useField } from '../../../hooks'; import { useField } from '../../../hooks';
import { focusEnd } from '../../../utils/element-helpers';
import styles from './NameEdit.module.scss'; import styles from './NameEdit.module.scss';
@ -66,7 +65,7 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
useEffect(() => { useEffect(() => {
if (isOpened) { if (isOpened) {
focusEnd(field.current.ref.current); field.current.ref.current.focus();
} }
}, [isOpened]); }, [isOpened]);

@ -48,8 +48,6 @@ const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onMove, onDelete
return ( return (
<> <>
{items.length > 0 && ( {items.length > 0 && (
<>
<span className={styles.progressWrapper}>
<Progress <Progress
autoSuccess autoSuccess
value={completedItems.length} value={completedItems.length}
@ -58,11 +56,6 @@ const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onMove, onDelete
size="tiny" size="tiny"
className={styles.progress} className={styles.progress}
/> />
</span>
<span className={styles.count}>
{completedItems.length}/{items.length}
</span>
</>
)} )}
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}> <DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="tasks" type={DroppableTypes.TASK}> <Droppable droppableId="tasks" type={DroppableTypes.TASK}>

@ -3,23 +3,6 @@
margin: 0 0 16px; margin: 0 0 16px;
} }
.progressWrapper {
display: inline-block;
padding: 3px 0;
vertical-align: top;
width: calc(100% - 50px);
}
.count {
color: #8c8c8c;
display: inline-block;
font-size: 14px;
line-height: 14px;
text-align: right;
vertical-align: top;
width: 50px;
}
.taskButton { .taskButton {
background: transparent; background: transparent;
border: none; border: none;

@ -4,7 +4,6 @@
border-radius: 4px; border-radius: 4px;
bottom: 20px; bottom: 20px;
box-shadow: #b04632 0 1px 0; box-shadow: #b04632 0 1px 0;
max-width: calc(100% - 40px);
padding: 12px 18px; padding: 12px 18px;
position: fixed; position: fixed;
right: 20px; right: 20px;

@ -1,12 +1,8 @@
import upperFirst from 'lodash/upperFirst'; import upperFirst from 'lodash/upperFirst';
import React, { useEffect, useRef } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon } from 'semantic-ui-react';
import { useForceUpdate } from '../../lib/hooks';
import getDateFormat from '../../utils/get-date-format';
import styles from './DueDate.module.scss'; import styles from './DueDate.module.scss';
@ -16,111 +12,27 @@ const SIZES = {
MEDIUM: 'medium', MEDIUM: 'medium',
}; };
const STATUSES = { const FORMATS = {
DUE_SOON: 'dueSoon',
OVERDUE: 'overdue',
COMPLETED: 'completed',
};
const LONG_DATE_FORMAT_BY_SIZE = {
tiny: 'longDate', tiny: 'longDate',
small: 'longDate', small: 'longDate',
medium: 'longDateTime', medium: 'longDateTime',
}; };
const FULL_DATE_FORMAT_BY_SIZE = { const DueDate = React.memo(({ value, size, isDisabled, onClick }) => {
tiny: 'fullDate',
small: 'fullDate',
medium: 'fullDateTime',
};
const STATUS_ICON_PROPS_BY_STATUS = {
[STATUSES.DUE_SOON]: {
name: 'hourglass half',
color: 'orange',
},
[STATUSES.OVERDUE]: {
name: 'hourglass end',
color: 'red',
},
[STATUSES.COMPLETED]: {
name: 'checkmark',
color: 'green',
},
};
const getStatus = (dateTime, isCompleted) => {
if (isCompleted) {
return STATUSES.COMPLETED;
}
const secondsLeft = Math.floor((dateTime.getTime() - new Date().getTime()) / 1000);
if (secondsLeft <= 0) {
return STATUSES.OVERDUE;
}
if (secondsLeft <= 24 * 60 * 60) {
return STATUSES.DUE_SOON;
}
return null;
};
const DueDate = React.memo(({ value, size, isCompleted, isDisabled, withStatusIcon, onClick }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const forceUpdate = useForceUpdate();
const statusRef = useRef(null);
statusRef.current = getStatus(value, isCompleted);
const intervalRef = useRef(null);
const dateFormat = getDateFormat(
value,
LONG_DATE_FORMAT_BY_SIZE[size],
FULL_DATE_FORMAT_BY_SIZE[size],
);
useEffect(() => {
if ([null, STATUSES.DUE_SOON].includes(statusRef.current)) {
intervalRef.current = setInterval(() => {
const status = getStatus(value, isCompleted);
if (status !== statusRef.current) {
forceUpdate();
}
if (status === STATUSES.OVERDUE) {
clearInterval(intervalRef.current);
}
}, 1000);
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [value, isCompleted, forceUpdate]);
const contentNode = ( const contentNode = (
<span <span
className={classNames( className={classNames(
styles.wrapper, styles.wrapper,
styles[`wrapper${upperFirst(size)}`], styles[`wrapper${upperFirst(size)}`],
!withStatusIcon && statusRef.current && styles[`wrapper${upperFirst(statusRef.current)}`],
onClick && styles.wrapperHoverable, onClick && styles.wrapperHoverable,
)} )}
> >
{t(`format:${dateFormat}`, { {t(`format:${FORMATS[size]}`, {
value, value,
postProcess: 'formatDate', postProcess: 'formatDate',
})} })}
{withStatusIcon && statusRef.current && (
// eslint-disable-next-line react/jsx-props-no-spreading
<Icon {...STATUS_ICON_PROPS_BY_STATUS[statusRef.current]} className={styles.statusIcon} />
)}
</span> </span>
); );
@ -136,19 +48,14 @@ const DueDate = React.memo(({ value, size, isCompleted, isDisabled, withStatusIc
DueDate.propTypes = { DueDate.propTypes = {
value: PropTypes.instanceOf(Date).isRequired, value: PropTypes.instanceOf(Date).isRequired,
size: PropTypes.oneOf(Object.values(SIZES)), size: PropTypes.oneOf(Object.values(SIZES)),
isCompleted: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
withStatusIcon: PropTypes.bool,
onClick: PropTypes.func, onClick: PropTypes.func,
onCompletionToggle: PropTypes.func,
}; };
DueDate.defaultProps = { DueDate.defaultProps = {
size: SIZES.MEDIUM, size: SIZES.MEDIUM,
isDisabled: false, isDisabled: false,
withStatusIcon: false,
onClick: undefined, onClick: undefined,
onCompletionToggle: undefined,
}; };
export default DueDate; export default DueDate;

@ -8,17 +8,16 @@
padding: 0; padding: 0;
} }
.statusIcon {
line-height: 1;
margin: 0 0 0 8px;
}
.wrapper { .wrapper {
background: #dce0e4; background: #dce0e4;
border: none;
border-radius: 3px; border-radius: 3px;
color: #6a808b; color: #6a808b;
display: inline-block; display: inline-block;
outline: none;
text-align: left;
transition: background 0.3s ease; transition: background 0.3s ease;
vertical-align: top;
} }
.wrapperHoverable:hover { .wrapperHoverable:hover {
@ -44,21 +43,4 @@
line-height: 20px; line-height: 20px;
padding: 6px 12px; padding: 6px 12px;
} }
/* Statuses */
.wrapperDueSoon {
background: #f2711c;
color: #fff;
}
.wrapperOverdue {
background: #db2828;
color: #fff;
}
.wrapperCompleted {
background: #21ba45;
color: #fff;
}
} }

@ -1,6 +1,5 @@
:global(#app) { :global(#app) {
.wrapper { .wrapper {
max-width: 100vw;
position: fixed; position: fixed;
width: 100%; width: 100%;
z-index: 1; z-index: 1;

@ -3,11 +3,11 @@ import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button, Icon, Menu } from 'semantic-ui-react'; import { Button, Icon, Menu } from 'semantic-ui-react';
import { useAuth } from 'react-oidc-context';
import { usePopup } from '../../lib/popup'; import { usePopup } from '../../lib/popup';
import Paths from '../../constants/Paths'; import Paths from '../../constants/Paths';
import NotificationsStep from './NotificationsStep'; import NotificationsStep from './NotificationsStep';
import User from '../User';
import UserStep from '../UserStep'; import UserStep from '../UserStep';
import styles from './Header.module.scss'; import styles from './Header.module.scss';
@ -30,6 +30,7 @@ const Header = React.memo(
onUserSettingsClick, onUserSettingsClick,
onLogout, onLogout,
}) => { }) => {
const auth = useAuth();
const handleProjectSettingsClick = useCallback(() => { const handleProjectSettingsClick = useCallback(() => {
if (canEditProject) { if (canEditProject) {
onProjectSettingsClick(); onProjectSettingsClick();
@ -39,6 +40,11 @@ const Header = React.memo(
const NotificationsPopup = usePopup(NotificationsStep, POPUP_PROPS); const NotificationsPopup = usePopup(NotificationsStep, POPUP_PROPS);
const UserPopup = usePopup(UserStep, POPUP_PROPS); const UserPopup = usePopup(UserStep, POPUP_PROPS);
const onFullLogout = () => {
auth.signoutSilent();
onLogout();
};
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
{!project && ( {!project && (
@ -89,11 +95,10 @@ const Header = React.memo(
<UserPopup <UserPopup
isLogouting={isLogouting} isLogouting={isLogouting}
onSettingsClick={onUserSettingsClick} onSettingsClick={onUserSettingsClick}
onLogout={onLogout} onLogout={onFullLogout}
> >
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}> <Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
<span className={styles.userName}>{user.name}</span> {user.name}
<User name={user.name} avatarUrl={user.avatarUrl} size="small" />
</Menu.Item> </Menu.Item>
</UserPopup> </UserPopup>
</Menu.Menu> </Menu.Menu>

@ -86,15 +86,6 @@
font-weight: bold; font-weight: bold;
} }
.userName {
display: none;
margin-right: 10px;
@media only screen and (min-width: 797px) {
display: block;
}
}
.wrapper { .wrapper {
background: rgba(0, 0, 0, 0.24); background: rgba(0, 0, 0, 0.24);
display: flex; display: flex;

@ -49,6 +49,7 @@
max-height: 60vh; max-height: 60vh;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 5px; width: 5px;

@ -17,7 +17,7 @@ const SIZES = {
const Label = React.memo(({ name, color, size, isDisabled, onClick }) => { const Label = React.memo(({ name, color, size, isDisabled, onClick }) => {
const contentNode = ( const contentNode = (
<span <div
title={name} title={name}
className={classNames( className={classNames(
styles.wrapper, styles.wrapper,
@ -28,7 +28,7 @@ const Label = React.memo(({ name, color, size, isDisabled, onClick }) => {
)} )}
> >
{name || '\u00A0'} {name || '\u00A0'}
</span> </div>
); );
return onClick ? ( return onClick ? (

@ -11,17 +11,17 @@
.wrapper { .wrapper {
border-radius: 3px; border-radius: 3px;
box-sizing: border-box;
color: #fff; color: #fff;
display: inline-block; font-weight: 400;
font-weight: normal; outline: none;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2); text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2);
vertical-align: top;
white-space: nowrap; white-space: nowrap;
} }
.wrapperNameless { .wrapperNameless{
width: 40px; width: 40px;
} }

@ -25,6 +25,7 @@
max-height: 60vh; max-height: 60vh;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 5px; width: 5px;

@ -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;

@ -5,17 +5,15 @@ import { Menu } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui'; import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks'; import { useSteps } from '../../hooks';
import ListSortStep from '../ListSortStep';
import DeleteStep from '../DeleteStep'; import DeleteStep from '../DeleteStep';
import styles from './ActionsStep.module.scss'; import styles from './ActionsStep.module.scss';
const StepTypes = { const StepTypes = {
DELETE: 'DELETE', DELETE: 'DELETE',
SORT: 'SORT',
}; };
const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onSort, onDelete, onClose }) => { const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps(); const [step, openStep, handleBack] = useSteps();
@ -29,30 +27,11 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onSort, onDelete, onClo
onClose(); onClose();
}, [onCardAdd, onClose]); }, [onCardAdd, onClose]);
const handleSortClick = useCallback(() => {
openStep(StepTypes.SORT);
}, [openStep]);
const handleDeleteClick = useCallback(() => { const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE); openStep(StepTypes.DELETE);
}, [openStep]); }, [openStep]);
const handleSortTypeSelect = useCallback( if (step && step.type === StepTypes.DELETE) {
(type) => {
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 ( return (
<DeleteStep <DeleteStep
title="common.deleteList" title="common.deleteList"
@ -62,8 +41,6 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onSort, onDelete, onClo
onBack={handleBack} onBack={handleBack}
/> />
); );
default:
}
} }
return ( return (
@ -85,11 +62,6 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onSort, onDelete, onClo
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleSortClick}>
{t('action.sortList', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}> <Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteList', { {t('action.deleteList', {
context: 'title', context: 'title',
@ -105,7 +77,6 @@ ActionsStep.propTypes = {
onNameEdit: PropTypes.func.isRequired, onNameEdit: PropTypes.func.isRequired,
onCardAdd: PropTypes.func.isRequired, onCardAdd: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };

@ -16,18 +16,7 @@ import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-ic
import styles from './List.module.scss'; import styles from './List.module.scss';
const List = React.memo( const List = React.memo(
({ ({ id, index, name, isPersisted, cardIds, canEdit, onUpdate, onDelete, onCardCreate }) => {
id,
index,
name,
isPersisted,
cardIds,
canEdit,
onUpdate,
onDelete,
onSort,
onCardCreate,
}) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [isAddCardOpened, setIsAddCardOpened] = useState(false); const [isAddCardOpened, setIsAddCardOpened] = useState(false);
@ -125,7 +114,6 @@ const List = React.memo(
onNameEdit={handleNameEdit} onNameEdit={handleNameEdit}
onCardAdd={handleCardAdd} onCardAdd={handleCardAdd}
onDelete={onDelete} onDelete={onDelete}
onSort={onSort}
> >
<Button className={classNames(styles.headerButton, styles.target)}> <Button className={classNames(styles.headerButton, styles.target)}>
<Icon fitted name="pencil" size="small" /> <Icon fitted name="pencil" size="small" />
@ -171,7 +159,6 @@ List.propTypes = {
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool.isRequired, canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onCardCreate: PropTypes.func.isRequired, onCardCreate: PropTypes.func.isRequired,
}; };

@ -43,6 +43,7 @@
max-height: calc(100vh - 268px); max-height: calc(100vh - 268px);
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin;
width: 290px; width: 290px;
&:hover { &:hover {
@ -80,12 +81,6 @@
&:hover .target { &:hover .target {
opacity: 1; opacity: 1;
} }
@media only screen and (max-width: 797px) {
.target {
opacity: 1;
}
}
} }
.headerEditable { .headerEditable {
@ -109,15 +104,6 @@
background: rgba(9, 30, 66, 0.13); background: rgba(9, 30, 66, 0.13);
color: #516b7a; color: #516b7a;
} }
@media only screen and (max-width: 797px) {
&:focus,
&:active {
background: rgba(9, 30, 66, 0.13);
color: #516b7a;
}
}
} }
.headerName { .headerName {

@ -4,7 +4,6 @@ import TextareaAutosize from 'react-textarea-autosize';
import { TextArea } from 'semantic-ui-react'; import { TextArea } from 'semantic-ui-react';
import { useField } from '../../hooks'; import { useField } from '../../hooks';
import { focusEnd } from '../../utils/element-helpers';
import styles from './NameEdit.module.scss'; import styles from './NameEdit.module.scss';
@ -72,7 +71,7 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
useEffect(() => { useEffect(() => {
if (isOpened) { if (isOpened) {
focusEnd(field.current.ref.current); field.current.ref.current.select();
} }
}, [isOpened]); }, [isOpened]);

@ -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;

@ -1,9 +1,10 @@
import isEmail from 'validator/lib/isEmail'; import isEmail from 'validator/lib/isEmail';
import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useAuth } from 'react-oidc-context';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Form, Grid, Header, Message } from 'semantic-ui-react'; import { Form, Grid, Header, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks'; import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
import { Input } from '../../lib/custom-ui'; import { Input } from '../../lib/custom-ui';
@ -18,11 +19,6 @@ const createMessage = (error) => {
} }
switch (error.message) { switch (error.message) {
case 'Invalid credentials':
return {
type: 'error',
content: 'common.invalidCredentials',
};
case 'Invalid email or username': case 'Invalid email or username':
return { return {
type: 'error', type: 'error',
@ -33,21 +29,6 @@ const createMessage = (error) => {
type: 'error', type: 'error',
content: 'common.invalidPassword', content: 'common.invalidPassword',
}; };
case 'Use single sign-on':
return {
type: 'error',
content: 'common.useSingleSignOn',
};
case 'Email already in use':
return {
type: 'error',
content: 'common.emailAlreadyInUse',
};
case 'Username already in use':
return {
type: 'error',
content: 'common.usernameAlreadyInUse',
};
case 'Failed to fetch': case 'Failed to fetch':
return { return {
type: 'warning', type: 'warning',
@ -67,17 +48,8 @@ const createMessage = (error) => {
}; };
const Login = React.memo( const Login = React.memo(
({ ({ defaultData, isSubmitting, error, onAuthenticate, onMessageDismiss }) => {
defaultData, const auth = useAuth();
isSubmitting,
isSubmittingUsingOidc,
error,
withOidc,
isOidcEnforced,
onAuthenticate,
onAuthenticateUsingOidc,
onMessageDismiss,
}) => {
const [t] = useTranslation(); const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting); const wasSubmitting = usePrevious(isSubmitting);
@ -113,15 +85,12 @@ const Login = React.memo(
}, [onAuthenticate, data]); }, [onAuthenticate, data]);
useEffect(() => { useEffect(() => {
if (!isOidcEnforced) {
emailOrUsernameField.current.focus(); emailOrUsernameField.current.focus();
} }, []);
}, [isOidcEnforced]);
useEffect(() => { useEffect(() => {
if (wasSubmitting && !isSubmitting && error) { if (wasSubmitting && !isSubmitting && error) {
switch (error.message) { switch (error.message) {
case 'Invalid credentials':
case 'Invalid email or username': case 'Invalid email or username':
emailOrUsernameField.current.select(); emailOrUsernameField.current.select();
@ -168,7 +137,6 @@ const Login = React.memo(
onDismiss={onMessageDismiss} onDismiss={onMessageDismiss}
/> />
)} )}
{!isOidcEnforced && (
<Form size="large" onSubmit={handleSubmit}> <Form size="large" onSubmit={handleSubmit}>
<div className={styles.inputWrapper}> <div className={styles.inputWrapper}>
<div className={styles.inputLabel}>{t('common.emailOrUsername')}</div> <div className={styles.inputLabel}>{t('common.emailOrUsername')}</div>
@ -202,24 +170,12 @@ const Login = React.memo(
content={t('action.logIn')} content={t('action.logIn')}
floated="right" floated="right"
loading={isSubmitting} loading={isSubmitting}
disabled={isSubmitting || isSubmittingUsingOidc} disabled={isSubmitting}
/> />
</Form> </Form>
)} <Form.Button type="button" onClick={() => auth.signinRedirect()}>
{withOidc && ( Log in with SSO
<Button </Form.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>
</div> </div>
</Grid.Column> </Grid.Column>
@ -250,16 +206,10 @@ const Login = React.memo(
); );
Login.propTypes = { Login.propTypes = {
/* eslint-disable react/forbid-prop-types */ defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
defaultData: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */
isSubmitting: PropTypes.bool.isRequired, isSubmitting: PropTypes.bool.isRequired,
isSubmittingUsingOidc: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
withOidc: PropTypes.bool.isRequired,
isOidcEnforced: PropTypes.bool.isRequired,
onAuthenticate: PropTypes.func.isRequired, onAuthenticate: PropTypes.func.isRequired,
onAuthenticateUsingOidc: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired, onMessageDismiss: PropTypes.func.isRequired,
}; };

@ -1,19 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Loader } from 'semantic-ui-react';
import LoginContainer from '../containers/LoginContainer';
const LoginWrapper = React.memo(({ isInitializing }) => {
if (isInitializing) {
return <Loader active size="massive" />;
}
return <LoginContainer />;
});
LoginWrapper.propTypes = {
isInitializing: PropTypes.bool.isRequired,
};
export default LoginWrapper;

@ -3,7 +3,6 @@ import React, { useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react'; import { Button } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks'; import { useSteps } from '../../hooks';
import User from '../User'; import User from '../User';
@ -20,7 +19,6 @@ const ActionsStep = React.memo(
({ ({
membership, membership,
permissionsSelectStep, permissionsSelectStep,
title,
leaveButtonContent, leaveButtonContent,
leaveConfirmationTitle, leaveConfirmationTitle,
leaveConfirmationContent, leaveConfirmationContent,
@ -33,7 +31,6 @@ const ActionsStep = React.memo(
canLeave, canLeave,
onUpdate, onUpdate,
onDelete, onDelete,
onBack,
onClose, onClose,
}) => { }) => {
const [t] = useTranslation(); const [t] = useTranslation();
@ -56,11 +53,6 @@ const ActionsStep = React.memo(
[onUpdate], [onUpdate],
); );
const handleDeleteConfirm = useCallback(() => {
onDelete();
onClose();
}, [onDelete, onClose]);
if (step) { if (step) {
switch (step.type) { switch (step.type) {
case StepTypes.EDIT_PERMISSIONS: { case StepTypes.EDIT_PERMISSIONS: {
@ -89,7 +81,7 @@ const ActionsStep = React.memo(
? leaveConfirmationButtonContent ? leaveConfirmationButtonContent
: deleteConfirmationButtonContent : deleteConfirmationButtonContent
} }
onConfirm={handleDeleteConfirm} onConfirm={onDelete}
onBack={handleBack} onBack={handleBack}
/> />
); );
@ -97,7 +89,7 @@ const ActionsStep = React.memo(
} }
} }
const contentNode = ( return (
<> <>
<span className={styles.user}> <span className={styles.user}>
<User name={membership.user.name} avatarUrl={membership.user.avatarUrl} size="large" /> <User name={membership.user.name} avatarUrl={membership.user.avatarUrl} size="large" />
@ -133,26 +125,12 @@ const ActionsStep = React.memo(
)} )}
</> </>
); );
return onBack ? (
<>
<Popup.Header onBack={onBack}>
{t(title, {
context: 'title',
})}
</Popup.Header>
<Popup.Content>{contentNode}</Popup.Content>
</>
) : (
contentNode
);
}, },
); );
ActionsStep.propTypes = { ActionsStep.propTypes = {
membership: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types membership: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
permissionsSelectStep: PropTypes.elementType, permissionsSelectStep: PropTypes.elementType,
title: PropTypes.string,
leaveButtonContent: PropTypes.string, leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string, leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string, leaveConfirmationContent: PropTypes.string,
@ -165,13 +143,11 @@ ActionsStep.propTypes = {
canLeave: PropTypes.bool.isRequired, canLeave: PropTypes.bool.isRequired,
onUpdate: PropTypes.func, onUpdate: PropTypes.func,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
ActionsStep.defaultProps = { ActionsStep.defaultProps = {
permissionsSelectStep: undefined, permissionsSelectStep: undefined,
title: 'common.memberActions',
leaveButtonContent: 'action.leaveBoard', leaveButtonContent: 'action.leaveBoard',
leaveConfirmationTitle: 'common.leaveBoard', leaveConfirmationTitle: 'common.leaveBoard',
leaveConfirmationContent: 'common.areYouSureYouWantToLeaveBoard', leaveConfirmationContent: 'common.areYouSureYouWantToLeaveBoard',
@ -181,7 +157,6 @@ ActionsStep.defaultProps = {
deleteConfirmationContent: 'common.areYouSureYouWantToRemoveThisMemberFromBoard', deleteConfirmationContent: 'common.areYouSureYouWantToRemoveThisMemberFromBoard',
deleteConfirmationButtonContent: 'action.removeMember', deleteConfirmationButtonContent: 'action.removeMember',
onUpdate: undefined, onUpdate: undefined,
onBack: undefined,
}; };
export default ActionsStep; export default ActionsStep;

@ -4,6 +4,7 @@
max-height: 60vh; max-height: 60vh;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 5px; width: 5px;

@ -5,21 +5,16 @@ import { usePopup } from '../../lib/popup';
import AddStep from './AddStep'; import AddStep from './AddStep';
import ActionsStep from './ActionsStep'; import ActionsStep from './ActionsStep';
import MembershipsStep from './MembershipsStep';
import User from '../User'; import User from '../User';
import styles from './Memberships.module.scss'; import styles from './Memberships.module.scss';
const MAX_MEMBERS = 6;
const Memberships = React.memo( const Memberships = React.memo(
({ ({
items, items,
allUsers, allUsers,
permissionsSelectStep, permissionsSelectStep,
title,
addTitle, addTitle,
actionsTitle,
leaveButtonContent, leaveButtonContent,
leaveConfirmationTitle, leaveConfirmationTitle,
leaveConfirmationContent, leaveConfirmationContent,
@ -36,14 +31,11 @@ const Memberships = React.memo(
}) => { }) => {
const AddPopup = usePopup(AddStep); const AddPopup = usePopup(AddStep);
const ActionsPopup = usePopup(ActionsStep); const ActionsPopup = usePopup(ActionsStep);
const MembershipsPopup = usePopup(MembershipsStep);
const remainMembersCount = items.length - MAX_MEMBERS;
return ( return (
<> <>
<span className={styles.users}> <span className={styles.users}>
{items.slice(0, MAX_MEMBERS).map((item) => ( {items.map((item) => (
<span key={item.id} className={styles.user}> <span key={item.id} className={styles.user}>
<ActionsPopup <ActionsPopup
membership={item} membership={item}
@ -71,30 +63,6 @@ const Memberships = React.memo(
</span> </span>
))} ))}
</span> </span>
{remainMembersCount > 0 && (
<MembershipsPopup
items={items}
permissionsSelectStep={permissionsSelectStep}
title={title}
actionsTitle={actionsTitle}
leaveButtonContent={leaveButtonContent}
leaveConfirmationTitle={leaveConfirmationTitle}
leaveConfirmationContent={leaveConfirmationContent}
leaveConfirmationButtonContent={leaveConfirmationButtonContent}
deleteButtonContent={deleteButtonContent}
deleteConfirmationTitle={deleteConfirmationTitle}
deleteConfirmationContent={deleteConfirmationContent}
deleteConfirmationButtonContent={deleteConfirmationButtonContent}
canEdit={canEdit}
canLeave={items.length > 1 || canLeaveIfLast}
onUpdate={onUpdate}
onDelete={onDelete}
>
<Button icon className={styles.addUser}>
+{remainMembersCount < 99 ? remainMembersCount : 99}
</Button>
</MembershipsPopup>
)}
{canEdit && ( {canEdit && (
<AddPopup <AddPopup
users={allUsers} users={allUsers}
@ -117,9 +85,7 @@ Memberships.propTypes = {
allUsers: PropTypes.array.isRequired, allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
permissionsSelectStep: PropTypes.elementType, permissionsSelectStep: PropTypes.elementType,
title: PropTypes.string,
addTitle: PropTypes.string, addTitle: PropTypes.string,
actionsTitle: PropTypes.string,
leaveButtonContent: PropTypes.string, leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string, leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string, leaveConfirmationContent: PropTypes.string,
@ -137,9 +103,7 @@ Memberships.propTypes = {
Memberships.defaultProps = { Memberships.defaultProps = {
permissionsSelectStep: undefined, permissionsSelectStep: undefined,
title: undefined,
addTitle: undefined, addTitle: undefined,
actionsTitle: undefined,
leaveButtonContent: undefined, leaveButtonContent: undefined,
leaveConfirmationTitle: undefined, leaveConfirmationTitle: undefined,
leaveConfirmationContent: undefined, leaveConfirmationContent: undefined,

@ -11,10 +11,6 @@
vertical-align: top; vertical-align: top;
width: 36px; width: 36px;
@media only screen and (max-width: 797px) {
margin-left: 10px;
}
&:hover { &:hover {
background: rgba(0, 0, 0, 0.32); background: rgba(0, 0, 0, 0.32);
} }

@ -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;

@ -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;

@ -0,0 +1,3 @@
import OidcLogin from './OidcLogin';
export default OidcLogin;

@ -12,9 +12,7 @@ const ManagersPane = React.memo(({ items, allUsers, onCreate, onDelete }) => {
<Memberships <Memberships
items={items} items={items}
allUsers={allUsers} allUsers={allUsers}
title="common.managers"
addTitle="common.addManager" addTitle="common.addManager"
actionsTitle="common.managerActions"
leaveButtonContent="action.leaveProject" leaveButtonContent="action.leaveProject"
leaveConfirmationTitle="common.leaveProject" leaveConfirmationTitle="common.leaveProject"
leaveConfirmationContent="common.areYouSureYouWantToLeaveProject" leaveConfirmationContent="common.areYouSureYouWantToLeaveProject"

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { AuthProvider } from 'react-oidc-context';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { Route, Routes } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import { ReduxRouter } from '../lib/redux-router'; import { ReduxRouter } from '../lib/redux-router';
import Paths from '../constants/Paths'; import Paths from '../constants/Paths';
import LoginWrapperContainer from '../containers/LoginWrapperContainer'; import LoginContainer from '../containers/LoginContainer';
import CoreContainer from '../containers/CoreContainer'; import CoreContainer from '../containers/CoreContainer';
import NotFound from './NotFound'; import NotFound from './NotFound';
@ -13,16 +14,25 @@ import 'react-datepicker/dist/react-datepicker.css';
import 'photoswipe/dist/photoswipe.css'; import 'photoswipe/dist/photoswipe.css';
import 'easymde/dist/easymde.min.css'; import 'easymde/dist/easymde.min.css';
import '../lib/custom-ui/styles.css'; import '../lib/custom-ui/styles.css';
import '../assets/css/font-awesome.css';
import '../styles.module.scss'; import '../styles.module.scss';
import OidcLoginContainer from '../containers/OidcLoginContainer';
function Root({ store, history }) { function Root({ store, history, config }) {
return ( return (
<AuthProvider
authority={config.authority}
client_id={config.clientId}
redirect_uri={config.redirectUri}
scope={config.scopes}
onSigninCallback={() => {
window.history.replaceState({}, document.title, window.location.pathname);
}}
>
<Provider store={store}> <Provider store={store}>
<ReduxRouter history={history}> <ReduxRouter history={history}>
<Routes> <Routes>
<Route path={Paths.LOGIN} element={<LoginWrapperContainer />} /> <Route path={Paths.LOGIN} element={<LoginContainer />} />
<Route path={Paths.OIDC_CALLBACK} element={<LoginWrapperContainer />} /> <Route path={Paths.OIDC_LOGIN} element={<OidcLoginContainer />} />
<Route path={Paths.ROOT} element={<CoreContainer />} /> <Route path={Paths.ROOT} element={<CoreContainer />} />
<Route path={Paths.PROJECTS} element={<CoreContainer />} /> <Route path={Paths.PROJECTS} element={<CoreContainer />} />
<Route path={Paths.BOARDS} element={<CoreContainer />} /> <Route path={Paths.BOARDS} element={<CoreContainer />} />
@ -31,13 +41,14 @@ function Root({ store, history }) {
</Routes> </Routes>
</ReduxRouter> </ReduxRouter>
</Provider> </Provider>
</AuthProvider>
); );
} }
Root.propTypes = { Root.propTypes = {
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
store: PropTypes.object.isRequired, store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
config: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
}; };

@ -10,10 +10,13 @@
.wrapper { .wrapper {
background: #dce0e4; background: #dce0e4;
border: none;
border-radius: 3px; border-radius: 3px;
color: #6a808b; color: #6a808b;
display: inline-block; display: inline-block;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
outline: none;
text-align: left;
transition: background 0.3s ease; transition: background 0.3s ease;
vertical-align: top; vertical-align: top;
} }

@ -14,10 +14,12 @@
} }
.wrapper { .wrapper {
border: none;
border-radius: 50%; border-radius: 50%;
color: #fff; color: #fff;
display: inline-block; display: inline-block;
line-height: 1; line-height: 1;
outline: none;
text-align: center; text-align: center;
vertical-align: top; vertical-align: top;
} }
@ -30,6 +32,7 @@
.wrapperTiny { .wrapperTiny {
font-size: 10px; font-size: 10px;
font-weight: 400;
height: 24px; height: 24px;
line-height: 20px; line-height: 20px;
padding: 2px 0; padding: 2px 0;
@ -38,6 +41,7 @@
.wrapperSmall { .wrapperSmall {
font-size: 12px; font-size: 12px;
font-weight: 400;
height: 28px; height: 28px;
padding: 8px 0; padding: 8px 0;
width: 28px; width: 28px;
@ -45,6 +49,7 @@
.wrapperMedium { .wrapperMedium {
font-size: 14px; font-size: 14px;
font-weight: 500;
height: 32px; height: 32px;
padding: 10px 0; padding: 10px 0;
width: 32px; width: 32px;
@ -52,6 +57,7 @@
.wrapperLarge { .wrapperLarge {
font-size: 14px; font-size: 14px;
font-weight: 500;
height: 36px; height: 36px;
padding: 12px 0 10px; padding: 12px 0 10px;
width: 36px; width: 36px;
@ -59,6 +65,7 @@
.wrapperMassive { .wrapperMassive {
font-size: 36px; font-size: 36px;
font-weight: 500;
height: 100px; height: 100px;
padding: 32px 0 10px; padding: 32px 0 10px;
width: 100px; width: 100px;

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

Loading…
Cancel
Save