Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92f75789fa | ||
|
|
8a8c1fee0c | ||
|
|
16499052f7 | ||
|
|
cc32daa009 | ||
|
|
ea1b3b7f92 | ||
|
|
4e2863faa7 | ||
|
|
b46fb43e6f | ||
|
|
bcacfa8fe2 | ||
|
|
4df2becd6f | ||
|
|
2990ea593a | ||
|
|
b976673826 | ||
|
|
16e034efcb | ||
|
|
6712625c3f | ||
|
|
b5bbf6a6a4 | ||
|
|
3d3e7761f5 | ||
|
|
fa56b9c01f | ||
|
|
882213784f | ||
|
|
251bd793f3 | ||
|
|
088b0e760b | ||
|
|
cc1fa1cc3b | ||
|
|
bcd3ea86e8 | ||
|
|
0176650f67 | ||
|
|
f3963aded3 | ||
|
|
bbe720481a | ||
|
|
04f9030474 | ||
|
|
6d135be859 | ||
|
|
63cbe7063e | ||
|
|
7985bea2b8 | ||
|
|
733abccaa9 | ||
|
|
119cce82b4 | ||
|
|
988e70ec40 | ||
|
|
5e3fca3771 | ||
|
|
6d9ce56f4a | ||
|
|
3c8c04fd37 | ||
|
|
790a1d5cc9 | ||
|
|
4b352fd878 | ||
|
|
a196f2ef11 | ||
|
|
6c65d135f7 | ||
|
|
a1a1e9a86a | ||
|
|
0d39a7567f | ||
|
|
76a18d06d6 | ||
|
|
ce52fc5af7 | ||
|
|
0a2863a79c | ||
|
|
39e320fe4b | ||
|
|
2f3dfe775e | ||
|
|
22964cb375 | ||
|
|
fd89cf7f2b | ||
|
|
5acf84c20b | ||
|
|
6cd9da844f | ||
|
|
d7847fe89b | ||
|
|
d033520927 | ||
|
|
d911030831 | ||
|
|
9d95ed6c41 | ||
|
|
1a5a853fa3 | ||
|
|
bb358e01dd | ||
|
|
9f0fce098e | ||
|
|
60ab09771d | ||
|
|
cb3ef783ba | ||
|
|
fe376b10c1 | ||
|
|
786441cda0 | ||
|
|
0d3cf0a32d | ||
|
|
b4e01d7256 | ||
|
|
73a641b935 | ||
|
|
6802a0dc69 | ||
|
|
81d6c43cd9 | ||
|
|
ca6a1a14f5 | ||
|
|
c096444acb | ||
|
|
51d05f3cd3 | ||
|
|
221da3c455 | ||
|
|
92098e136c | ||
|
|
fd5c33dd5d | ||
|
|
0c1c7e2192 | ||
|
|
b8d262f745 | ||
|
|
a1fd694248 | ||
|
|
634d6ceab1 | ||
|
|
31d4d5f38d | ||
|
|
3f99438402 | ||
|
|
7e7727e3e1 | ||
|
|
273d994750 | ||
|
|
86d21d6abd | ||
|
|
a9c200cc3b | ||
|
|
3fedc14062 | ||
|
|
c42e1fb5dd | ||
|
|
9961340118 | ||
|
|
f54bd22757 | ||
|
|
1043dacd67 | ||
|
|
616304a1fa | ||
|
|
1e01148138 | ||
|
|
ede56e1ae3 | ||
|
|
b2e0c45539 | ||
|
|
389648b546 | ||
|
|
f4789c28db | ||
|
|
668092de7d | ||
|
|
920f9c097d | ||
|
|
da63598de3 | ||
|
|
517cb34cfe | ||
|
|
079b8eba53 | ||
|
|
af02cdd8bf | ||
|
|
e1bc189b88 | ||
|
|
5f2723623a | ||
|
|
fee6841d0a | ||
|
|
ca6fb3b962 | ||
|
|
28c3f28e01 | ||
|
|
80d12aaeeb | ||
|
|
89af209a2e | ||
|
|
f1fed3e533 | ||
|
|
eddeb96b16 | ||
|
|
9e8ff3b579 | ||
|
|
fc1278011c | ||
|
|
24163463fc | ||
|
|
fdef87e3b9 | ||
|
|
964498dbaa | ||
|
|
5b519cb62f | ||
|
|
11fc3b4485 | ||
|
|
37c0b59f82 | ||
|
|
239611ad51 | ||
|
|
0575e665fe | ||
|
|
019766dced | ||
|
|
6dc9e4ed99 | ||
|
|
d4b64b90fc | ||
|
|
1a49826b85 | ||
|
|
815643eb2c | ||
|
|
bcc4c0d923 | ||
|
|
cf7810b067 | ||
|
|
f1c72df8b6 | ||
|
|
5056d38848 | ||
|
|
3ef5b3ead8 | ||
|
|
ded58dc8b0 | ||
|
|
d8ee1ecdd1 | ||
|
|
fc2f4cd5d3 | ||
|
|
7ef457157e | ||
|
|
23ee815200 | ||
|
|
40c04c35ff | ||
|
|
743f2956c8 | ||
|
|
5bf5db27d8 | ||
|
|
667c5daf25 | ||
|
|
6662b0a717 | ||
|
|
348bf4d6c4 | ||
|
|
3ac2d289d0 | ||
|
|
3bc73c43ea | ||
|
|
8e0c60f5be | ||
|
|
aae69cb5e4 | ||
|
|
782c0356c8 | ||
|
|
757cdb14a4 | ||
|
|
8a4bdbd1fe | ||
|
|
5bf42be786 | ||
|
|
d896c3183b | ||
|
|
13ee0c3466 | ||
|
|
211c841379 | ||
|
|
90a30961ff | ||
|
|
7cb9774e05 | ||
|
|
875895b331 | ||
|
|
91bc889fed | ||
|
|
a34d8e9753 | ||
|
|
107cb85ba2 | ||
|
|
9333ef562e | ||
|
|
7d28abc7e6 | ||
|
|
5317950b5c | ||
|
|
7501619b7e | ||
|
|
080e29aa03 | ||
|
|
48c532b2c2 | ||
|
|
a62815f825 | ||
|
|
6938232dc4 | ||
|
|
4c4cfa9c66 | ||
|
|
ba89d41b19 | ||
|
|
5c435414f7 | ||
|
|
2d7812a679 | ||
|
|
d386c82973 | ||
|
|
6c4db967c2 | ||
|
|
932d8225ed | ||
|
|
0d481703da | ||
|
|
23e2b4a622 | ||
|
|
f4cc51e1c0 | ||
|
|
793ddd5e20 | ||
|
|
3d84888eb1 |
@@ -0,0 +1,57 @@
|
||||
name: Build and publish release package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
|
||||
jobs:
|
||||
build-and-publish-release-package:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Workflow install pnpm
|
||||
run: npm install pnpm -g
|
||||
|
||||
- name: Client install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Server install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Server include into dist
|
||||
run: mv server/ dist/
|
||||
|
||||
- name: Client build production
|
||||
run: |
|
||||
npm run build
|
||||
working-directory: ./client
|
||||
|
||||
- name: Client include into dist
|
||||
run: |
|
||||
mv build/index.html ../dist/views/index.ejs
|
||||
mv build/* ../dist/public/
|
||||
working-directory: ./client
|
||||
|
||||
- name: Dist include README.md SECURITY.md LICENSE start.sh
|
||||
run: mv README.md SECURITY.md LICENSE start.sh dist/
|
||||
|
||||
- name: Dist Remove node modules
|
||||
run: rm -R dist/node_modules
|
||||
|
||||
- name: Dist create .zip file
|
||||
run: |
|
||||
mv dist/ planka/
|
||||
zip -r planka-prebuild-${{ github.event.release.tag_name }}.zip planka
|
||||
|
||||
- name: Dist upload assets
|
||||
run: |
|
||||
gh release upload ${{ github.event.release.tag_name }} planka-prebuild-${{ github.event.release.tag_name }}.zip
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -13,26 +13,26 @@ env:
|
||||
|
||||
jobs:
|
||||
build-and-push-docker-base-image:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# https://docs.docker.com/build/ci/github-actions/multi-platform/
|
||||
name: Build and push Docker DEV image
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- 'charts/**'
|
||||
- 'docker-*.sh'
|
||||
- '*.md'
|
||||
branches: [master]
|
||||
|
||||
env:
|
||||
REGISTRY_IMAGE: ghcr.io/plankanban/planka
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: [self-hosted, x64]
|
||||
platform: linux/amd64
|
||||
- os: [self-hosted, arm64]
|
||||
platform: linux/arm64
|
||||
- os: [self-hosted, arm64]
|
||||
platform: linux/arm/v7
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: digests
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: [self-hosted]
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: digests
|
||||
path: /tmp/digests
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
rerun-failed-jobs:
|
||||
runs-on: [self-hosted]
|
||||
needs: [ build, merge]
|
||||
if: failure()
|
||||
steps:
|
||||
- name: Rerun failed jobs in the current workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh run rerun ${{ github.run_id }} --failed
|
||||
@@ -6,19 +6,19 @@ on:
|
||||
|
||||
jobs:
|
||||
build-and-push-docker-image:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
script: return context.payload.release.tag_name.replace('v', '')
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Release Charts
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "charts/**"
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
release:
|
||||
# depending on default permission settings for your org (contents being read-only or read-write for workloads), you will have to add permissions
|
||||
# see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "$GITHUB_ACTOR"
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
|
||||
- name: Add repositories
|
||||
run: |
|
||||
for dir in $(ls -d charts/*/); do
|
||||
helm dependency list $dir 2> /dev/null | tail +2 | head -n -1 | awk '{ print "helm repo add " $1 " " $3 }' | while read cmd; do $cmd; done
|
||||
done
|
||||
|
||||
- name: Run chart-releaser for stable
|
||||
uses: helm/chart-releaser-action@v1.6.0
|
||||
with:
|
||||
charts_dir: charts
|
||||
mark_as_latest: false
|
||||
env:
|
||||
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
@@ -10,3 +10,6 @@ docker-compose.override.yml
|
||||
# `npm i --package-lock-only`
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
# Chart dependencies
|
||||
**/charts/*.tgz
|
||||
|
||||
+23
-8
@@ -1,33 +1,44 @@
|
||||
FROM ghcr.io/plankanban/planka:base-latest as server-dependencies
|
||||
FROM node:18-alpine as server-dependencies
|
||||
|
||||
RUN apk -U upgrade \
|
||||
&& apk add build-base python3 \
|
||||
--no-cache
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY server/package.json server/package-lock.json .
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
|
||||
RUN npm install npm@latest --global \
|
||||
&& npm clean-install --omit=dev
|
||||
&& npm install pnpm --global \
|
||||
&& pnpm import \
|
||||
&& pnpm install --prod
|
||||
|
||||
FROM node:lts AS client
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY client/package.json client/package-lock.json .
|
||||
COPY client/package.json client/package-lock.json ./
|
||||
|
||||
RUN npm install npm@latest --global \
|
||||
&& npm clean-install --omit=dev
|
||||
&& npm install pnpm --global \
|
||||
&& pnpm import \
|
||||
&& pnpm install --prod
|
||||
|
||||
COPY client .
|
||||
RUN DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
|
||||
FROM ghcr.io/plankanban/planka:base-latest
|
||||
FROM node:18-alpine
|
||||
|
||||
RUN apk del vips-dependencies --purge
|
||||
RUN apk -U upgrade \
|
||||
&& apk add bash \
|
||||
--no-cache
|
||||
|
||||
USER node
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=node:node start.sh .
|
||||
COPY --chown=node:node server .
|
||||
COPY --chown=node:node healthcheck.js .
|
||||
|
||||
RUN mv .env.sample .env
|
||||
|
||||
@@ -42,4 +53,8 @@ VOLUME /app/private/attachments
|
||||
|
||||
EXPOSE 1337
|
||||
|
||||
CMD ["./start.sh"]
|
||||
HEALTHCHECK --interval=10s --timeout=2s --start-period=15s \
|
||||
CMD node ./healthcheck.js
|
||||
|
||||
|
||||
CMD [ "bash", "start.sh" ]
|
||||
|
||||
+12
-17
@@ -1,27 +1,22 @@
|
||||
FROM node:lts-alpine
|
||||
FROM node:18-alpine
|
||||
|
||||
ARG ALPINE_VERSION=3.16
|
||||
ARG VIPS_VERSION=8.13.3
|
||||
ARG VIPS_VERSION=8.14.5
|
||||
|
||||
RUN apk -U upgrade \
|
||||
&& apk add \
|
||||
bash giflib glib lcms2 libexif \
|
||||
libgsf libjpeg-turbo libpng librsvg libwebp \
|
||||
orc pango tiff \
|
||||
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
|
||||
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
|
||||
bash pkgconf \
|
||||
libjpeg-turbo libexif librsvg cgif tiff libspng libimagequant \
|
||||
--no-cache \
|
||||
&& apk add \
|
||||
build-base giflib-dev glib-dev lcms2-dev libexif-dev \
|
||||
libgsf-dev libjpeg-turbo-dev libpng-dev librsvg-dev libwebp-dev \
|
||||
orc-dev pango-dev tiff-dev \
|
||||
build-base gobject-introspection-dev meson \
|
||||
libjpeg-turbo-dev libexif-dev librsvg-dev cgif-dev tiff-dev libspng-dev libimagequant-dev \
|
||||
--virtual vips-dependencies \
|
||||
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
|
||||
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
|
||||
--no-cache \
|
||||
&& wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.gz | tar xzC /tmp \
|
||||
&& wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz | tar xJC /tmp \
|
||||
&& cd /tmp/vips-${VIPS_VERSION} \
|
||||
&& ./configure \
|
||||
&& make \
|
||||
&& make install-strip \
|
||||
&& meson setup build-dir \
|
||||
&& cd build-dir \
|
||||
&& ninja \
|
||||
&& ninja test \
|
||||
&& ninja install \
|
||||
&& rm -rf /tmp/vips-${VIPS_VERSION}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Planka
|
||||
#### Elegant open source project tracking
|
||||
#### Elegant open source project tracking.
|
||||
|
||||
  
|
||||
|
||||
@@ -10,19 +10,18 @@
|
||||
## Features
|
||||
|
||||
- Create projects, boards, lists, cards, labels and tasks
|
||||
- Add card members, track time, set a due date, add attachments, write comments
|
||||
- Markdown support in a card description and comment
|
||||
- Add card members, track time, set due dates, add attachments, write comments
|
||||
- Markdown support in card description and comments
|
||||
- Filter by members and labels
|
||||
- Customize project background
|
||||
- Customize project backgrounds
|
||||
- Real-time updates
|
||||
- User notifications
|
||||
- Internationalization
|
||||
- Internal notifications
|
||||
- Multiple interface languages
|
||||
- Single sign-on via OpenID Connect
|
||||
|
||||
## How to deploy Planka
|
||||
|
||||
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)
|
||||
There are many ways to install Planka, [check them out](https://docs.planka.cloud/docs/intro).
|
||||
|
||||
For configuration, please see the [configuration section](https://docs.planka.cloud/docs/category/configuration).
|
||||
|
||||
@@ -46,3 +45,7 @@ See the [development section](https://docs.planka.cloud/docs/Development).
|
||||
## License
|
||||
|
||||
Planka is [AGPL-3.0 licensed](https://github.com/plankanban/planka/blob/master/LICENSE).
|
||||
|
||||
## Contributors
|
||||
|
||||
[](https://github.com/plankanban/planka/graphs/contributors)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -0,0 +1,6 @@
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
version: 12.5.1
|
||||
digest: sha256:01dfb2d07ab6800b4a5a6c81f20f3377a758124b2b96b891d0cd6b4f64cf783b
|
||||
generated: "2023-05-15T00:54:48.1308917+01:00"
|
||||
@@ -0,0 +1,31 @@
|
||||
apiVersion: v2
|
||||
name: planka
|
||||
description: A Helm chart to deploy Planka and it's dependencies.
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.26
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.4"
|
||||
|
||||
dependencies:
|
||||
- alias: postgresql
|
||||
condition: postgresql.enabled
|
||||
name: postgresql
|
||||
repository: &bitnami-repo https://charts.bitnami.com/bitnami
|
||||
version: 12.5.1
|
||||
@@ -0,0 +1,107 @@
|
||||
# Planka Helm Chart
|
||||
|
||||
[Planka](https://github.com/plankanban/planka) is an OSS alternative to Trello that you can host yourself, and this is a Helm Chart to make it easier to deploy to K8s.
|
||||
|
||||
Shoutout to [this issue](https://github.com/plankanban/planka/issues/192) who have been asking for this Helm Chart.
|
||||
|
||||
## Issues
|
||||
|
||||
By using the Bitnami chart for PostgreSQL, there is an issue where once deployed, if trying to use a different password then it will be ignored as the Persistant Volume (PV) will already exist with the previous password. See warning from Bitnami below:
|
||||
|
||||
> **Warning!** Setting a password will be ignored on new installation in the case when previous Posgresql release was deleted through the helm command. In that case, old PVC will have an old password, and setting it through helm won't take effect. Deleting persistent volumes (PVs) will solve the issue. Refer to [issue 2061](https://github.com/bitnami/charts/issues/2061) for more details
|
||||
|
||||
If you want to fully uninstall this chart including the data, follow [these steps](https://github.com/bitnami/charts/blob/main/bitnami/postgresql/README.md#uninstalling-the-chart) from the Bitnami Chart's docs.
|
||||
|
||||
## Usage
|
||||
|
||||
The basic usage of the chart can be found below:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Chris-Greaves/planka-helm-chart.git
|
||||
cd planka-helm-chart
|
||||
helm dependency build
|
||||
export SECRETKEY=$(openssl rand -hex 64)
|
||||
helm install planka . --set secretkey=$SECRETKEY \
|
||||
--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.
|
||||
|
||||
To access Planka you can port forward using the following command:
|
||||
|
||||
```bash
|
||||
kubectl port-forward $POD_NAME 3000:1337
|
||||
```
|
||||
|
||||
### Accessing Externally
|
||||
|
||||
To access Planka externally you can use the following configuration
|
||||
|
||||
```bash
|
||||
# HTTP only
|
||||
helm install planka . --set secretkey=$SECRETKEY \
|
||||
--set admin_email="demo@demo.demo" \
|
||||
--set admin_password="demo" \
|
||||
--set admin_name="Demo Demo" \
|
||||
--set admin_username="demo" \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.hosts[0].host=planka.example.dev \
|
||||
|
||||
# HTTPS
|
||||
helm install planka . --set secretkey=$SECRETKEY \
|
||||
--set admin_email="demo@demo.demo" \
|
||||
--set admin_password="demo" \
|
||||
--set admin_name="Demo Demo" \
|
||||
--set admin_username="demo" \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.hosts[0].host=planka.example.dev \
|
||||
--set ingress.tls[0].secretName=planka-tls \
|
||||
--set ingress.tls[0].hosts[0]=planka.example.dev \
|
||||
```
|
||||
|
||||
or create a values.yaml file like:
|
||||
|
||||
```yaml
|
||||
secretkey: "<InsertSecretKey>"
|
||||
# The admin section needs to be present for new instances of Planka, after the first start you can remove the lines starting with admin_. If you want the admin user to be unchangeable admin_email: has to stay
|
||||
# After changing the config you have to run ```helm upgrade planka . -f values.yaml```
|
||||
|
||||
# Admin user
|
||||
admin_email: "demo@demo.demo" # Do not remove if you want to prevent this user from being edited/deleted
|
||||
admin_password: "demo"
|
||||
admin_name: "Demo Demo"
|
||||
admin_username: "demo"
|
||||
# Admin user
|
||||
|
||||
# Ingress
|
||||
ingress:
|
||||
enabled: true
|
||||
hosts:
|
||||
- host: planka.example.dev
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
|
||||
# Needed for HTTPS
|
||||
tls:
|
||||
- secretName: planka-tls # existing TLS secret in k8s
|
||||
hosts:
|
||||
- planka.example.dev
|
||||
```
|
||||
|
||||
```bash
|
||||
helm install planka . -f values.yaml
|
||||
```
|
||||
|
||||
### Things to consider if production hosting
|
||||
|
||||
If you want to host Planka for more than just playing around with, you might want to do the following things:
|
||||
|
||||
- Create a `values.yaml` with your config, as this will make applying upgrades much easier in the future.
|
||||
- Create your `secretkey` once and store it either in a secure vault, or in your `values.yaml` file so it will be the same for upgrading in the future.
|
||||
- Specify a password for `postgresql.auth.password` as there have been issues with the postgresql chart generating new passwords locking you out of the data you've already stored. (see [this issue](https://github.com/bitnami/charts/issues/2061))
|
||||
|
||||
Any questions or concerns, [raise an issue](https://github.com/Chris-Greaves/planka-helm-chart/issues/new).
|
||||
@@ -0,0 +1,22 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "planka.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "planka.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "planka.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "planka.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://localhost:3000 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 3000:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
@@ -0,0 +1,62 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "planka.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "planka.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "planka.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "planka.labels" -}}
|
||||
helm.sh/chart: {{ include "planka.chart" . }}
|
||||
{{ include "planka.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "planka.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "planka.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "planka.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "planka.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,150 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "planka.fullname" . }}
|
||||
labels:
|
||||
{{- include "planka.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "planka.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "planka.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "planka.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.containerPort | default 1337 }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
volumeMounts:
|
||||
- mountPath: /app/public/user-avatars
|
||||
subPath: user-avatars
|
||||
name: planka
|
||||
- mountPath: /app/public/project-background-images
|
||||
subPath: project-background-images
|
||||
name: planka
|
||||
- mountPath: /app/private/attachments
|
||||
subPath: attachments
|
||||
name: planka
|
||||
{{- if .Values.securityContext.readOnlyRootFilesystem }}
|
||||
- mountPath: /app/logs
|
||||
subPath: app-logs
|
||||
name: emptydir
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if not .Values.postgresql.enabled }}
|
||||
- name: DATABASE_URL
|
||||
value: {{ required "If the included postgresql deployment is disabled you need to define a Database URL in 'dburl'" .Values.dburl }}
|
||||
{{- else }}
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: planka-postgresql-svcbind-custom-user
|
||||
key: uri
|
||||
{{- end }}
|
||||
- name: BASE_URL
|
||||
{{- if .Values.baseUrl }}
|
||||
value: {{ .Values.baseUrl }}
|
||||
{{- else if .Values.ingress.enabled }}
|
||||
value: {{ printf "https://%s" (first .Values.ingress.hosts).host }}
|
||||
{{- else }}
|
||||
value: http://localhost:3000
|
||||
{{- end }}
|
||||
- name: SECRET_KEY
|
||||
value: {{ required "A secret key needs to be generated using 'openssl rand -hex 64' and assigned to secretkey." .Values.secretkey }}
|
||||
- name: TRUST_PROXY
|
||||
value: "0"
|
||||
- name: DEFAULT_ADMIN_EMAIL
|
||||
value: {{ .Values.admin_email }}
|
||||
- name: DEFAULT_ADMIN_PASSWORD
|
||||
value: {{ .Values.admin_password }}
|
||||
- name: DEFAULT_ADMIN_NAME
|
||||
value: {{ .Values.admin_name }}
|
||||
- name: DEFAULT_ADMIN_USERNAME
|
||||
value: {{ .Values.admin_username }}
|
||||
{{ range $k, $v := .Values.env }}
|
||||
- name: {{ $k | quote }}
|
||||
value: {{ $v | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.oidc.enabled }}
|
||||
{{- $secretName := default (printf "%s-oidc" (include "planka.fullname" .)) .Values.oidc.existingSecret }}
|
||||
- name: OIDC_CLIENT_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: clientId
|
||||
name: {{ $secretName }}
|
||||
- name: OIDC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: clientSecret
|
||||
name: {{ $secretName }}
|
||||
- name: OIDC_ISSUER
|
||||
value: {{ required "issuerUrl is required when configuring OIDC" .Values.oidc.issuerUrl | quote }}
|
||||
- name: OIDC_SCOPES
|
||||
value: {{ join " " .Values.oidc.scopes | default "openid profile email" | quote }}
|
||||
{{- if .Values.oidc.admin.roles }}
|
||||
- name: OIDC_ADMIN_ROLES
|
||||
value: {{ join "," .Values.oidc.admin.roles | quote }}
|
||||
{{- end }}
|
||||
- name: OIDC_ROLES_ATTRIBUTE
|
||||
value: {{ .Values.oidc.admin.rolesAttribute | default "groups" | quote }}
|
||||
{{- if .Values.oidc.admin.ignoreRoles }}
|
||||
- name: OIDC_IGNORE_ROLES
|
||||
value: {{ .Values.oidc.admin.ignoreRoles | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: planka
|
||||
{{- if .Values.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.persistence.existingClaim | default (include "planka.fullname" .) }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- if .Values.securityContext.readOnlyRootFilesystem }}
|
||||
- name: emptydir
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,32 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "planka.fullname" . }}
|
||||
labels:
|
||||
{{- include "planka.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "planka.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,61 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "planka.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "planka.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||
pathType: {{ .pathType }}
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,25 @@
|
||||
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
|
||||
---
|
||||
kind: PersistentVolumeClaim
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: {{ include "planka.fullname" . }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "planka.name" . }}
|
||||
helm.sh/chart: {{ include "planka.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
spec:
|
||||
accessModes:
|
||||
- {{ .Values.persistence.accessMode }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.size | quote }}
|
||||
{{- if .Values.persistence.storageClass }}
|
||||
{{- if (eq "-" .Values.persistence.storageClass) }}
|
||||
storageClassName: ""
|
||||
{{- else }}
|
||||
storageClassName: "{{ .Values.persistence.storageClass }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "planka.fullname" . }}
|
||||
labels:
|
||||
{{- include "planka.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "planka.selectorLabels" . | nindent 4 }}
|
||||
@@ -0,0 +1,12 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "planka.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "planka.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "planka.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "planka.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "planka.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
||||
@@ -0,0 +1,182 @@
|
||||
# Default values for planka.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: ghcr.io/plankanban/planka
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# Generate a secret using openssl rand -base64 45
|
||||
secretkey: ""
|
||||
|
||||
# Base url for Planka. Will override `ingress.hosts[0].host`
|
||||
# Defaults to `http://localhost:3000` if ingress is disabled.
|
||||
baseUrl: ""
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: true
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 1337
|
||||
## @param service.containerPort Planka HTTP container port
|
||||
## If empty will default to 1337
|
||||
##
|
||||
containerPort: 1337
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
# Used to set planka BASE_URL if no `baseurl` is provided.
|
||||
- host: planka.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: planka-tls
|
||||
# hosts:
|
||||
# - planka.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
postgresql:
|
||||
enabled: true
|
||||
auth:
|
||||
database: planka
|
||||
username: planka
|
||||
password: ""
|
||||
postgresPassword: ""
|
||||
replicationPassword: ""
|
||||
# existingSecret: planka-postgresql
|
||||
serviceBindings:
|
||||
enabled: true
|
||||
|
||||
## Set this if you disable the built-in postgresql deployment
|
||||
dburl:
|
||||
|
||||
## PVC-based data storage configuration
|
||||
persistence:
|
||||
enabled: false
|
||||
# existingClaim: netbox-data
|
||||
# storageClass: "-"
|
||||
accessMode: ReadWriteOnce
|
||||
size: 10Gi
|
||||
|
||||
## OpenID Identity Management configuration
|
||||
##
|
||||
## Example:
|
||||
## ---------------
|
||||
## oidc:
|
||||
## enabled: true
|
||||
## clientId: sxxaAIAxVXlCxTmc1YLHBbQr8NL8MqLI2DUbt42d
|
||||
## clientSecret: om4RTMRVHRszU7bqxB7RZNkHIzA8e4sGYWxeCwIMYQXPwEBWe4SY5a0wwCe9ltB3zrq5f0dnFnp34cEHD7QSMHsKvV9AiV5Z7eqDraMnv0I8IFivmuV5wovAECAYreSI
|
||||
## issuerUrl: https://auth.local/application/o/planka/
|
||||
## admin:
|
||||
## roles:
|
||||
## - planka-admin
|
||||
##
|
||||
## ---------------
|
||||
## NOTE: A minimal configuration requires setting `clientId`, `clientSecret` and `issuerUrl`. (plus `admin.roles` for administrators)
|
||||
## ref: https://docs.planka.cloud/docs/Configuration/OIDC
|
||||
##
|
||||
oidc:
|
||||
## @param oidc.enabled Enable single sign-on (SSO) with OpenID Connect (OIDC)
|
||||
##
|
||||
enabled: false
|
||||
|
||||
## OIDC credentials
|
||||
## @param oidc.clientId A string unique to the provider that identifies your app.
|
||||
## @param oidc.clientSecret A secret string that the provider uses to confirm ownership of a client ID.
|
||||
##
|
||||
## NOTE: Either specify inline `clientId` and `clientSecret` or refer to them via `existingSecret`
|
||||
##
|
||||
clientId: ""
|
||||
clientSecret: ""
|
||||
|
||||
## @param oidc.existingSecret Name of an existing secret containing OIDC credentials
|
||||
## NOTE: Must contain key `clientId` and `clientSecret`
|
||||
## NOTE: When it's set, the `clientId` and `clientSecret` parameters are ignored
|
||||
##
|
||||
existingSecret: ""
|
||||
|
||||
## @param oidc.issuerUrl The OpenID connect metadata document endpoint
|
||||
##
|
||||
issuerUrl: ""
|
||||
|
||||
## @param oidc.scopes A list of scopes required for OIDC client.
|
||||
## If empty will default to `openid`, `profile` and `email`
|
||||
## NOTE: Planka needs the email and name claims
|
||||
##
|
||||
scopes: []
|
||||
|
||||
## Admin permissions configuration
|
||||
admin:
|
||||
## @param oidc.admin.ignoreRoles If set to true, the admin roles will be ignored.
|
||||
## It is useful if you want to use OIDC for authentication but not for authorization.
|
||||
## If empty will default to `false`
|
||||
##
|
||||
ignoreRoles: false
|
||||
|
||||
## @param oidc.admin.rolesAttribute The name of a custom group claim that you have configured in your OIDC provider
|
||||
## If empty will default to `groups`
|
||||
##
|
||||
rolesAttribute: groups
|
||||
|
||||
## @param oidc.admin.roles The names of the admin groups
|
||||
##
|
||||
roles: []
|
||||
# - planka-admin
|
||||
+1
-1
@@ -1 +1 @@
|
||||
REACT_APP_VERSION=1.10.3
|
||||
REACT_APP_VERSION=1.16.4
|
||||
|
||||
Generated
+3395
-21947
File diff suppressed because it is too large
Load Diff
+38
-29
@@ -49,65 +49,74 @@
|
||||
"**/*.test.js"
|
||||
]
|
||||
}
|
||||
],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"classnames": "^2.3.2",
|
||||
"date-fns": "^2.29.3",
|
||||
"date-fns": "^2.30.0",
|
||||
"dequal": "^2.0.3",
|
||||
"easymde": "^2.18.0",
|
||||
"history": "^5.3.0",
|
||||
"i18next": "^22.0.6",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"i18next": "^23.7.6",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"initials": "^3.1.2",
|
||||
"js-cookie": "^3.0.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"linkify-react": "^4.1.3",
|
||||
"linkifyjs": "^4.1.3",
|
||||
"lodash": "^4.17.21",
|
||||
"node-sass": "^8.0.0",
|
||||
"photoswipe": "^5.3.3",
|
||||
"nanoid": "^5.0.3",
|
||||
"node-sass": "^9.0.0",
|
||||
"photoswipe": "^5.4.2",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-datepicker": "^4.8.0",
|
||||
"react-datepicker": "^4.21.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-i18next": "^12.0.0",
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-photoswipe-gallery": "^2.2.2",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.4.3",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-photoswipe-gallery": "^2.2.7",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-router-dom": "^6.19.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-simplemde-editor": "^5.2.0",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
"redux": "^4.2.0",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"redux": "^4.2.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-orm": "^0.16.2",
|
||||
"redux-saga": "^1.2.1",
|
||||
"remark-breaks": "^3.0.2",
|
||||
"redux-saga": "^1.2.3",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"reselect": "^4.1.7",
|
||||
"reselect": "^4.1.8",
|
||||
"sails.io.js": "^1.2.1",
|
||||
"semantic-ui-react": "^2.1.3",
|
||||
"semantic-ui-react": "^2.1.4",
|
||||
"socket.io-client": "^2.5.0",
|
||||
"validator": "^13.7.0",
|
||||
"whatwg-fetch": "^3.6.2",
|
||||
"validator": "^13.11.0",
|
||||
"whatwg-fetch": "^3.6.19",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@testing-library/jest-dom": "^6.1.4",
|
||||
"@testing-library/react": "^14.1.0",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"babel-preset-airbnb": "^5.0.0",
|
||||
"chai": "^4.3.7",
|
||||
"eslint": "^8.28.0",
|
||||
"chai": "^4.3.10",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"react-test-renderer": "^18.2.0"
|
||||
}
|
||||
|
||||
@@ -23,10 +23,14 @@ createCard.failure = (localId, error) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const handleCardCreate = (card) => ({
|
||||
const handleCardCreate = (card, cardMemberships, cardLabels, tasks, attachments) => ({
|
||||
type: ActionTypes.CARD_CREATE_HANDLE,
|
||||
payload: {
|
||||
card,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
attachments,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -60,6 +64,34 @@ const handleCardUpdate = (card) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const duplicateCard = (id, card, taskIds) => ({
|
||||
type: ActionTypes.CARD_DUPLICATE,
|
||||
payload: {
|
||||
id,
|
||||
card,
|
||||
taskIds,
|
||||
},
|
||||
});
|
||||
|
||||
duplicateCard.success = (localId, card, cardMemberships, cardLabels, tasks) => ({
|
||||
type: ActionTypes.CARD_DUPLICATE__SUCCESS,
|
||||
payload: {
|
||||
localId,
|
||||
card,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
},
|
||||
});
|
||||
|
||||
duplicateCard.failure = (id, error) => ({
|
||||
type: ActionTypes.CARD_DUPLICATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteCard = (id) => ({
|
||||
type: ActionTypes.CARD_DELETE,
|
||||
payload: {
|
||||
@@ -94,6 +126,7 @@ export default {
|
||||
handleCardCreate,
|
||||
updateCard,
|
||||
handleCardUpdate,
|
||||
duplicateCard,
|
||||
deleteCard,
|
||||
handleCardDelete,
|
||||
};
|
||||
|
||||
@@ -39,9 +39,19 @@ const initializeCore = (
|
||||
},
|
||||
});
|
||||
|
||||
const logout = () => ({
|
||||
// TODO: with success?
|
||||
initializeCore.fetchConfig = (config) => ({
|
||||
type: ActionTypes.CORE_INITIALIZE__CONFIG_FETCH,
|
||||
payload: {
|
||||
config,
|
||||
},
|
||||
});
|
||||
|
||||
const logout = (invalidateAccessToken) => ({
|
||||
type: ActionTypes.LOGOUT,
|
||||
payload: {},
|
||||
payload: {
|
||||
invalidateAccessToken,
|
||||
},
|
||||
});
|
||||
|
||||
logout.invalidateAccessToken = () => ({
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const initializeLogin = (config) => ({
|
||||
type: ActionTypes.LOGIN_INITIALIZE,
|
||||
payload: {
|
||||
config,
|
||||
},
|
||||
});
|
||||
|
||||
const authenticate = (data) => ({
|
||||
type: ActionTypes.AUTHENTICATE,
|
||||
payload: {
|
||||
@@ -21,12 +28,33 @@ authenticate.failure = (error) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const authenticateUsingOidc = () => ({
|
||||
type: ActionTypes.USING_OIDC_AUTHENTICATE,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
authenticateUsingOidc.success = (accessToken) => ({
|
||||
type: ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS,
|
||||
payload: {
|
||||
accessToken,
|
||||
},
|
||||
});
|
||||
|
||||
authenticateUsingOidc.failure = (error) => ({
|
||||
type: ActionTypes.USING_OIDC_AUTHENTICATE__FAILURE,
|
||||
payload: {
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const clearAuthenticateError = () => ({
|
||||
type: ActionTypes.AUTHENTICATE_ERROR_CLEAR,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
export default {
|
||||
initializeLogin,
|
||||
authenticate,
|
||||
authenticateUsingOidc,
|
||||
clearAuthenticateError,
|
||||
};
|
||||
|
||||
@@ -5,10 +5,14 @@ import socket from './socket';
|
||||
|
||||
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
|
||||
|
||||
const exchangeForAccessTokenUsingOidc = (data, headers) =>
|
||||
http.post('/access-tokens/exchange-using-oidc', data, headers);
|
||||
|
||||
const deleteCurrentAccessToken = (headers) =>
|
||||
socket.delete('/access-tokens/me', undefined, headers);
|
||||
|
||||
export default {
|
||||
createAccessToken,
|
||||
exchangeForAccessTokenUsingOidc,
|
||||
deleteCurrentAccessToken,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import socket from './socket';
|
||||
import { transformUser } from './users';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
@@ -13,6 +14,10 @@ const getActivities = (cardId, data, headers) =>
|
||||
socket.get(`/cards/${cardId}/actions`, data, headers).then((body) => ({
|
||||
...body,
|
||||
items: body.items.map(transformActivity),
|
||||
included: {
|
||||
...body.included,
|
||||
users: body.included.users.map(transformUser),
|
||||
},
|
||||
}));
|
||||
|
||||
/* Event handlers */
|
||||
|
||||
@@ -1,18 +1,50 @@
|
||||
import socket from './socket';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
export const transformBoardMembership = (boardMembership) => ({
|
||||
...boardMembership,
|
||||
createdAt: new Date(boardMembership.createdAt),
|
||||
});
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createBoardMembership = (boardId, data, headers) =>
|
||||
socket.post(`/boards/${boardId}/memberships`, data, headers);
|
||||
socket.post(`/boards/${boardId}/memberships`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformBoardMembership(body.item),
|
||||
}));
|
||||
|
||||
const updateBoardMembership = (id, data, headers) =>
|
||||
socket.patch(`/board-memberships/${id}`, data, headers);
|
||||
socket.patch(`/board-memberships/${id}`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformBoardMembership(body.item),
|
||||
}));
|
||||
|
||||
const deleteBoardMembership = (id, headers) =>
|
||||
socket.delete(`/board-memberships/${id}`, undefined, headers);
|
||||
socket.delete(`/board-memberships/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformBoardMembership(body.item),
|
||||
}));
|
||||
|
||||
/* Event handlers */
|
||||
|
||||
const makeHandleBoardMembershipCreate = (next) => (body) => {
|
||||
next({
|
||||
...body,
|
||||
item: transformBoardMembership(body.item),
|
||||
});
|
||||
};
|
||||
|
||||
const makeHandleBoardMembershipUpdate = makeHandleBoardMembershipCreate;
|
||||
|
||||
const makeHandleBoardMembershipDelete = makeHandleBoardMembershipCreate;
|
||||
|
||||
export default {
|
||||
createBoardMembership,
|
||||
updateBoardMembership,
|
||||
deleteBoardMembership,
|
||||
makeHandleBoardMembershipCreate,
|
||||
makeHandleBoardMembershipUpdate,
|
||||
makeHandleBoardMembershipDelete,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import socket from './socket';
|
||||
import http from './http';
|
||||
import { transformUser } from './users';
|
||||
import { transformBoardMembership } from './board-memberships';
|
||||
import { transformCard } from './cards';
|
||||
import { transformAttachment } from './attachments';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createBoard = (projectId, data, headers) =>
|
||||
socket.post(`/projects/${projectId}/boards`, data, headers);
|
||||
socket.post(`/projects/${projectId}/boards`, data, headers).then((body) => ({
|
||||
...body,
|
||||
included: {
|
||||
...body.included,
|
||||
boardMemberships: body.included.boardMemberships.map(transformBoardMembership),
|
||||
},
|
||||
}));
|
||||
|
||||
const createBoardWithImport = (projectId, data, requestId, headers) =>
|
||||
http.post(`/projects/${projectId}/boards?requestId=${requestId}`, data, headers);
|
||||
@@ -18,6 +26,8 @@ const getBoard = (id, subscribe, headers) =>
|
||||
...body,
|
||||
included: {
|
||||
...body.included,
|
||||
users: body.included.users.map(transformUser),
|
||||
boardMemberships: body.included.boardMemberships.map(transformBoardMembership),
|
||||
cards: body.included.cards.map(transformCard),
|
||||
attachments: body.included.attachments.map(transformAttachment),
|
||||
},
|
||||
|
||||
@@ -57,6 +57,12 @@ const updateCard = (id, data, headers) =>
|
||||
item: transformCard(body.item),
|
||||
}));
|
||||
|
||||
const duplicateCard = (id, data, headers) =>
|
||||
socket.post(`/cards/${id}/duplicate`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformCard(body.item),
|
||||
}));
|
||||
|
||||
const deleteCard = (id, headers) =>
|
||||
socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
@@ -81,6 +87,7 @@ export default {
|
||||
getCard,
|
||||
updateCard,
|
||||
deleteCard,
|
||||
duplicateCard,
|
||||
makeHandleCardCreate,
|
||||
makeHandleCardUpdate,
|
||||
makeHandleCardDelete,
|
||||
|
||||
@@ -5,9 +5,11 @@ import Config from '../constants/Config';
|
||||
const http = {};
|
||||
|
||||
// TODO: add all methods
|
||||
['POST'].forEach((method) => {
|
||||
['GET', 'POST'].forEach((method) => {
|
||||
http[method.toLowerCase()] = (url, data, headers) => {
|
||||
const formData = Object.keys(data).reduce((result, key) => {
|
||||
const formData =
|
||||
data &&
|
||||
Object.keys(data).reduce((result, key) => {
|
||||
result.append(key, data[key]);
|
||||
|
||||
return result;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import http from './http';
|
||||
import socket from './socket';
|
||||
import root from './root';
|
||||
import accessTokens from './access-tokens';
|
||||
import users from './users';
|
||||
import projects from './projects';
|
||||
@@ -20,6 +21,7 @@ import notifications from './notifications';
|
||||
export { http, socket };
|
||||
|
||||
export default {
|
||||
...root,
|
||||
...accessTokens,
|
||||
...users,
|
||||
...projects,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import omit from 'lodash/omit';
|
||||
|
||||
import socket from './socket';
|
||||
import { transformUser } from './users';
|
||||
import { transformCard } from './cards';
|
||||
import { transformActivity } from './activities';
|
||||
|
||||
@@ -19,6 +20,7 @@ const getNotifications = (headers) =>
|
||||
items: body.items.map(transformNotification),
|
||||
included: {
|
||||
...omit(body.included, 'actions'),
|
||||
users: body.included.users.map(transformUser),
|
||||
cards: body.included.cards.map(transformCard),
|
||||
activities: body.included.actions.map(transformActivity),
|
||||
},
|
||||
@@ -30,6 +32,7 @@ const getNotification = (id, headers) =>
|
||||
item: transformNotification(body.item),
|
||||
included: {
|
||||
...omit(body.included, 'actions'),
|
||||
users: body.included.users.map(transformUser),
|
||||
cards: body.included.cards.map(transformCard),
|
||||
activities: body.included.actions.map(transformActivity),
|
||||
},
|
||||
|
||||
@@ -1,14 +1,40 @@
|
||||
import socket from './socket';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
export const transformProjectManager = (projectManager) => ({
|
||||
...projectManager,
|
||||
createdAt: new Date(projectManager.createdAt),
|
||||
});
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createProjectManager = (projectId, data, headers) =>
|
||||
socket.post(`/projects/${projectId}/managers`, data, headers);
|
||||
socket.post(`/projects/${projectId}/managers`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformProjectManager(body.item),
|
||||
}));
|
||||
|
||||
const deleteProjectManager = (id, headers) =>
|
||||
socket.delete(`/project-managers/${id}`, undefined, headers);
|
||||
socket.delete(`/project-managers/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformProjectManager(body.item),
|
||||
}));
|
||||
|
||||
/* Event handlers */
|
||||
|
||||
const makeHandleProjectManagerCreate = (next) => (body) => {
|
||||
next({
|
||||
...body,
|
||||
item: transformProjectManager(body.item),
|
||||
});
|
||||
};
|
||||
|
||||
const makeHandleProjectManagerDelete = makeHandleProjectManagerCreate;
|
||||
|
||||
export default {
|
||||
createProjectManager,
|
||||
deleteProjectManager,
|
||||
makeHandleProjectManagerCreate,
|
||||
makeHandleProjectManagerDelete,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,41 @@
|
||||
import http from './http';
|
||||
import socket from './socket';
|
||||
import { transformUser } from './users';
|
||||
import { transformProjectManager } from './project-managers';
|
||||
import { transformBoardMembership } from './board-memberships';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const getProjects = (headers) => socket.get('/projects', undefined, headers);
|
||||
const getProjects = (headers) =>
|
||||
socket.get('/projects', undefined, headers).then((body) => ({
|
||||
...body,
|
||||
included: {
|
||||
...body.included,
|
||||
users: body.included.users.map(transformUser),
|
||||
projectManagers: body.included.projectManagers.map(transformProjectManager),
|
||||
boardMemberships: body.included.boardMemberships.map(transformBoardMembership),
|
||||
},
|
||||
}));
|
||||
|
||||
const createProject = (data, headers) => socket.post('/projects', data, headers);
|
||||
const createProject = (data, headers) =>
|
||||
socket.post('/projects', data, headers).then((body) => ({
|
||||
...body,
|
||||
included: {
|
||||
...body.included,
|
||||
projectManagers: body.included.projectManagers.map(transformProjectManager),
|
||||
},
|
||||
}));
|
||||
|
||||
const getProject = (id, headers) => socket.get(`/projects/${id}`, undefined, headers);
|
||||
const getProject = (id, headers) =>
|
||||
socket.get(`/projects/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
included: {
|
||||
...body.included,
|
||||
users: body.included.users.map(transformUser),
|
||||
projectManagers: body.included.projectManagers.map(transformProjectManager),
|
||||
boardMemberships: body.included.boardMemberships.map(transformBoardMembership),
|
||||
},
|
||||
}));
|
||||
|
||||
const updateProject = (id, data, headers) => socket.patch(`/projects/${id}`, data, headers);
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import http from './http';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const getConfig = (headers) => http.get('/config', undefined, headers);
|
||||
|
||||
export default {
|
||||
getConfig,
|
||||
};
|
||||
+70
-10
@@ -1,30 +1,87 @@
|
||||
import http from './http';
|
||||
import socket from './socket';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
export const transformUser = (user) => ({
|
||||
...user,
|
||||
createdAt: new Date(user.createdAt),
|
||||
});
|
||||
|
||||
/* Actions */
|
||||
|
||||
const getUsers = (headers) => socket.get('/users', undefined, headers);
|
||||
const getUsers = (headers) =>
|
||||
socket.get('/users', undefined, headers).then((body) => ({
|
||||
...body,
|
||||
items: body.items.map(transformUser),
|
||||
}));
|
||||
|
||||
const createUser = (data, headers) => socket.post('/users', data, headers);
|
||||
const createUser = (data, headers) =>
|
||||
socket.post('/users', data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
|
||||
const getUser = (id, headers) => socket.get(`/users/${id}`, undefined, headers);
|
||||
const getUser = (id, headers) =>
|
||||
socket.get(`/users/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
|
||||
const getCurrentUser = (subscribe, headers) =>
|
||||
socket.get(`/users/me${subscribe ? '?subscribe=true' : ''}`, undefined, headers);
|
||||
socket.get(`/users/me${subscribe ? '?subscribe=true' : ''}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
|
||||
const updateUser = (id, data, headers) => socket.patch(`/users/${id}`, data, headers);
|
||||
const updateUser = (id, data, headers) =>
|
||||
socket.patch(`/users/${id}`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
|
||||
const updateUserEmail = (id, data, headers) => socket.patch(`/users/${id}/email`, data, headers);
|
||||
const updateUserEmail = (id, data, headers) =>
|
||||
socket.patch(`/users/${id}/email`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
|
||||
const updateUserPassword = (id, data, headers) =>
|
||||
socket.patch(`/users/${id}/password`, data, headers);
|
||||
socket.patch(`/users/${id}/password`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
|
||||
const updateUserUsername = (id, data, headers) =>
|
||||
socket.patch(`/users/${id}/username`, data, headers);
|
||||
socket.patch(`/users/${id}/username`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
|
||||
const updateUserAvatar = (id, data, headers) => http.post(`/users/${id}/avatar`, data, headers);
|
||||
const updateUserAvatar = (id, data, headers) =>
|
||||
http.post(`/users/${id}/avatar`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
|
||||
const deleteUser = (id, headers) => socket.delete(`/users/${id}`, undefined, headers);
|
||||
const deleteUser = (id, headers) =>
|
||||
socket.delete(`/users/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
|
||||
/* Event handlers */
|
||||
|
||||
const makeHandleUserCreate = (next) => (body) => {
|
||||
next({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
});
|
||||
};
|
||||
|
||||
const makeHandleUserUpdate = makeHandleUserCreate;
|
||||
|
||||
const makeHandleUserDelete = makeHandleUserCreate;
|
||||
|
||||
export default {
|
||||
getUsers,
|
||||
@@ -37,4 +94,7 @@ export default {
|
||||
updateUserUsername,
|
||||
updateUserAvatar,
|
||||
deleteUser,
|
||||
makeHandleUserCreate,
|
||||
makeHandleUserUpdate,
|
||||
makeHandleUserDelete,
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
max-height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
width: 100%;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
height: 56px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&:hover {
|
||||
height: 38px;
|
||||
|
||||
@@ -36,6 +36,7 @@ const ActionsStep = React.memo(
|
||||
onUpdate,
|
||||
onMove,
|
||||
onTransfer,
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
onUserAdd,
|
||||
onUserRemove,
|
||||
@@ -76,6 +77,11 @@ const ActionsStep = React.memo(
|
||||
openStep(StepTypes.MOVE);
|
||||
}, [openStep]);
|
||||
|
||||
const handleDuplicateClick = useCallback(() => {
|
||||
onDuplicate();
|
||||
onClose();
|
||||
}, [onDuplicate, onClose]);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
openStep(StepTypes.DELETE);
|
||||
}, [openStep]);
|
||||
@@ -207,6 +213,11 @@ const ActionsStep = React.memo(
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleDuplicateClick}>
|
||||
{t('action.duplicateCard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
|
||||
{t('action.deleteCard', {
|
||||
context: 'title',
|
||||
@@ -232,6 +243,7 @@ ActionsStep.propTypes = {
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onMove: PropTypes.func.isRequired,
|
||||
onTransfer: PropTypes.func.isRequired,
|
||||
onDuplicate: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onUserAdd: PropTypes.func.isRequired,
|
||||
onUserRemove: PropTypes.func.isRequired,
|
||||
|
||||
@@ -41,6 +41,7 @@ const Card = React.memo(
|
||||
onUpdate,
|
||||
onMove,
|
||||
onTransfer,
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
onUserAdd,
|
||||
onUserRemove,
|
||||
@@ -185,6 +186,7 @@ const Card = React.memo(
|
||||
onUpdate={onUpdate}
|
||||
onMove={onMove}
|
||||
onTransfer={onTransfer}
|
||||
onDuplicate={onDuplicate}
|
||||
onDelete={onDelete}
|
||||
onUserAdd={onUserAdd}
|
||||
onUserRemove={onUserRemove}
|
||||
@@ -238,6 +240,7 @@ Card.propTypes = {
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onMove: PropTypes.func.isRequired,
|
||||
onTransfer: PropTypes.func.isRequired,
|
||||
onDuplicate: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onUserAdd: PropTypes.func.isRequired,
|
||||
onUserRemove: PropTypes.func.isRequired,
|
||||
|
||||
@@ -4,6 +4,8 @@ import classNames from 'classnames';
|
||||
import { Progress } from 'semantic-ui-react';
|
||||
import { useToggle } from '../../lib/hooks';
|
||||
|
||||
import Linkify from '../Linkify';
|
||||
|
||||
import styles from './Tasks.module.scss';
|
||||
|
||||
const Tasks = React.memo(({ items }) => {
|
||||
@@ -48,7 +50,7 @@ const Tasks = React.memo(({ items }) => {
|
||||
key={item.id}
|
||||
className={classNames(styles.task, item.isCompleted && styles.taskCompleted)}
|
||||
>
|
||||
{item.name}
|
||||
<Linkify linkStopPropagation>{item.name}</Linkify>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -55,8 +55,10 @@
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 6px;
|
||||
padding-left: 14px;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:before {
|
||||
content: "–";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { Button, Form, TextArea } from 'semantic-ui-react';
|
||||
|
||||
import { useClosableForm, useForm } from '../../../hooks';
|
||||
import { useForm } from '../../../hooks';
|
||||
|
||||
import styles from './CommentEdit.module.scss';
|
||||
|
||||
@@ -35,12 +35,7 @@ const CommentEdit = React.forwardRef(({ children, defaultData, onUpdate }, ref)
|
||||
text: data.text.trim(),
|
||||
};
|
||||
|
||||
if (!cleanData.text) {
|
||||
textField.current.ref.current.select();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dequal(cleanData, defaultData)) {
|
||||
if (cleanData.text && !dequal(cleanData, defaultData)) {
|
||||
onUpdate(cleanData);
|
||||
}
|
||||
|
||||
@@ -65,10 +60,9 @@ const CommentEdit = React.forwardRef(({ children, defaultData, onUpdate }, ref)
|
||||
[submit],
|
||||
);
|
||||
|
||||
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
|
||||
close,
|
||||
isOpened,
|
||||
);
|
||||
const handleFieldBlur = useCallback(() => {
|
||||
submit();
|
||||
}, [submit]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
submit();
|
||||
@@ -99,13 +93,7 @@ const CommentEdit = React.forwardRef(({ children, defaultData, onUpdate }, ref)
|
||||
onBlur={handleFieldBlur}
|
||||
/>
|
||||
<div className={styles.controls}>
|
||||
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
|
||||
<Button
|
||||
positive
|
||||
content={t('action.save')}
|
||||
onMouseOver={handleControlMouseOver}
|
||||
onMouseOut={handleControlMouseOut}
|
||||
/>
|
||||
<Button positive content={t('action.save')} />
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import classNames from 'classnames';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import { Comment } from 'semantic-ui-react';
|
||||
|
||||
import getDateFormat from '../../../utils/get-date-format';
|
||||
import { ActivityTypes } from '../../../constants/Enums';
|
||||
import ItemComment from './ItemComment';
|
||||
import User from '../../User';
|
||||
@@ -66,7 +67,7 @@ const Item = React.memo(({ type, data, createdAt, user }) => {
|
||||
<div className={classNames(styles.content)}>
|
||||
<div>{contentNode}</div>
|
||||
<span className={styles.date}>
|
||||
{t('format:longDateTime', {
|
||||
{t(`format:${getDateFormat(createdAt)}`, {
|
||||
postProcess: 'formatDate',
|
||||
value: createdAt,
|
||||
})}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Comment } from 'semantic-ui-react';
|
||||
import { usePopup } from '../../../lib/popup';
|
||||
import { Markdown } from '../../../lib/custom-ui';
|
||||
|
||||
import getDateFormat from '../../../utils/get-date-format';
|
||||
import CommentEdit from './CommentEdit';
|
||||
import User from '../../User';
|
||||
import DeleteStep from '../../DeleteStep';
|
||||
@@ -33,7 +34,7 @@ const ItemComment = React.memo(
|
||||
<div className={styles.title}>
|
||||
<span className={styles.author}>{user.name}</span>
|
||||
<span className={styles.date}>
|
||||
{t('format:longDateTime', {
|
||||
{t(`format:${getDateFormat(createdAt)}`, {
|
||||
postProcess: 'formatDate',
|
||||
value: createdAt,
|
||||
})}
|
||||
|
||||
@@ -94,7 +94,7 @@ const AttachmentAddZone = React.memo(({ children, onCreate }) => {
|
||||
return (
|
||||
<>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<div {...getRootProps()} className={styles.wrapper}>
|
||||
<div {...getRootProps()}>
|
||||
{isDragActive && <div className={styles.dropzone}>{t('common.dropFileToUpload')}</div>}
|
||||
{children}
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
|
||||
@@ -12,8 +12,4 @@
|
||||
width: 100%;
|
||||
z-index: 2001;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ const CardModal = React.memo(
|
||||
onUpdate,
|
||||
onMove,
|
||||
onTransfer,
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
onUserAdd,
|
||||
onUserRemove,
|
||||
@@ -140,6 +141,11 @@ const CardModal = React.memo(
|
||||
});
|
||||
}, [isSubscribed, onUpdate]);
|
||||
|
||||
const handleDuplicateClick = useCallback(() => {
|
||||
onDuplicate();
|
||||
onClose();
|
||||
}, [onDuplicate, onClose]);
|
||||
|
||||
const handleGalleryOpen = useCallback(() => {
|
||||
isGalleryOpened.current = true;
|
||||
}, []);
|
||||
@@ -217,6 +223,7 @@ const CardModal = React.memo(
|
||||
onUserSelect={onUserAdd}
|
||||
onUserDeselect={onUserRemove}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(styles.attachment, styles.dueDate)}
|
||||
@@ -266,6 +273,7 @@ const CardModal = React.memo(
|
||||
onMove={onLabelMove}
|
||||
onDelete={onLabelDelete}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(styles.attachment, styles.dueDate)}
|
||||
@@ -314,10 +322,11 @@ const CardModal = React.memo(
|
||||
)}
|
||||
</span>
|
||||
{canEdit && (
|
||||
// eslint-disable-next-line jsx-a11y/control-has-associated-label
|
||||
<button
|
||||
onClick={handleToggleStopwatchClick}
|
||||
type="button"
|
||||
className={classNames(styles.attachment, styles.dueDate)}
|
||||
onClick={handleToggleStopwatchClick}
|
||||
>
|
||||
<Icon
|
||||
name={stopwatch.startedAt ? 'pause' : 'play'}
|
||||
@@ -493,6 +502,10 @@ const CardModal = React.memo(
|
||||
{t('action.move')}
|
||||
</Button>
|
||||
</CardMovePopup>
|
||||
<Button fluid className={styles.actionButton} onClick={handleDuplicateClick}>
|
||||
<Icon name="copy outline" className={styles.actionIcon} />
|
||||
{t('action.duplicate')}
|
||||
</Button>
|
||||
<DeletePopup
|
||||
title="common.deleteCard"
|
||||
content="common.areYouSureYouWantToDeleteThisCard"
|
||||
@@ -552,6 +565,7 @@ CardModal.propTypes = {
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onMove: PropTypes.func.isRequired,
|
||||
onTransfer: PropTypes.func.isRequired,
|
||||
onDuplicate: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onUserAdd: PropTypes.func.isRequired,
|
||||
onUserRemove: PropTypes.func.isRequired,
|
||||
|
||||
@@ -100,7 +100,6 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
|
||||
onChange={setValue}
|
||||
/>
|
||||
<div className={styles.controls}>
|
||||
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
|
||||
<Button positive content={t('action.save')} />
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { usePopup } from '../../../lib/popup';
|
||||
|
||||
import NameEdit from './NameEdit';
|
||||
import ActionsStep from './ActionsStep';
|
||||
import Linkify from '../../Linkify';
|
||||
|
||||
import styles from './Item.module.scss';
|
||||
|
||||
@@ -65,7 +66,7 @@ const Item = React.memo(
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span className={classNames(styles.task, isCompleted && styles.taskCompleted)}>
|
||||
{name}
|
||||
<Linkify linkStopPropagation>{name}</Linkify>
|
||||
</span>
|
||||
</span>
|
||||
{isPersisted && canEdit && (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { Button, Form, TextArea } from 'semantic-ui-react';
|
||||
|
||||
import { useClosableForm, useField } from '../../../hooks';
|
||||
import { useField } from '../../../hooks';
|
||||
|
||||
import styles from './NameEdit.module.scss';
|
||||
|
||||
@@ -28,12 +28,7 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
|
||||
const submit = useCallback(() => {
|
||||
const cleanValue = value.trim();
|
||||
|
||||
if (!cleanValue) {
|
||||
field.current.ref.current.select();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cleanValue !== defaultValue) {
|
||||
if (cleanValue && cleanValue !== defaultValue) {
|
||||
onUpdate(cleanValue);
|
||||
}
|
||||
|
||||
@@ -60,10 +55,9 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
|
||||
[submit],
|
||||
);
|
||||
|
||||
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
|
||||
close,
|
||||
isOpened,
|
||||
);
|
||||
const handleFieldBlur = useCallback(() => {
|
||||
submit();
|
||||
}, [submit]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
submit();
|
||||
@@ -93,13 +87,7 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
|
||||
onBlur={handleFieldBlur}
|
||||
/>
|
||||
<div className={styles.controls}>
|
||||
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
|
||||
<Button
|
||||
positive
|
||||
content={t('action.save')}
|
||||
onMouseOver={handleControlMouseOver}
|
||||
onMouseOut={handleControlMouseOut}
|
||||
/>
|
||||
<Button positive content={t('action.save')} />
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -48,6 +48,8 @@ const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onMove, onDelete
|
||||
return (
|
||||
<>
|
||||
{items.length > 0 && (
|
||||
<>
|
||||
<span className={styles.progressWrapper}>
|
||||
<Progress
|
||||
autoSuccess
|
||||
value={completedItems.length}
|
||||
@@ -56,6 +58,11 @@ const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onMove, onDelete
|
||||
size="tiny"
|
||||
className={styles.progress}
|
||||
/>
|
||||
</span>
|
||||
<span className={styles.count}>
|
||||
{completedItems.length}/{items.length}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<Droppable droppableId="tasks" type={DroppableTypes.TASK}>
|
||||
|
||||
@@ -3,6 +3,23 @@
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.progressWrapper {
|
||||
display: inline-block;
|
||||
padding: 3px 0;
|
||||
vertical-align: top;
|
||||
width: calc(100% - 50px);
|
||||
}
|
||||
|
||||
.count {
|
||||
color: #8c8c8c;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.taskButton {
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
@@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import getDateFormat from '../../utils/get-date-format';
|
||||
|
||||
import styles from './DueDate.module.scss';
|
||||
|
||||
const SIZES = {
|
||||
@@ -12,15 +14,27 @@ const SIZES = {
|
||||
MEDIUM: 'medium',
|
||||
};
|
||||
|
||||
const FORMATS = {
|
||||
const LONG_DATE_FORMAT_BY_SIZE = {
|
||||
tiny: 'longDate',
|
||||
small: 'longDate',
|
||||
medium: 'longDateTime',
|
||||
};
|
||||
|
||||
const FULL_DATE_FORMAT_BY_SIZE = {
|
||||
tiny: 'fullDate',
|
||||
small: 'fullDate',
|
||||
medium: 'fullDateTime',
|
||||
};
|
||||
|
||||
const DueDate = React.memo(({ value, size, isDisabled, onClick }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const dateFormat = getDateFormat(
|
||||
value,
|
||||
LONG_DATE_FORMAT_BY_SIZE[size],
|
||||
FULL_DATE_FORMAT_BY_SIZE[size],
|
||||
);
|
||||
|
||||
const contentNode = (
|
||||
<span
|
||||
className={classNames(
|
||||
@@ -29,7 +43,7 @@ const DueDate = React.memo(({ value, size, isDisabled, onClick }) => {
|
||||
onClick && styles.wrapperHoverable,
|
||||
)}
|
||||
>
|
||||
{t(`format:${FORMATS[size]}`, {
|
||||
{t(`format:${dateFormat}`, {
|
||||
value,
|
||||
postProcess: 'formatDate',
|
||||
})}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Icon, Menu } from 'semantic-ui-react';
|
||||
import { Button, Icon, Menu } from 'semantic-ui-react';
|
||||
import { usePopup } from '../../lib/popup';
|
||||
|
||||
import Paths from '../../constants/Paths';
|
||||
@@ -55,15 +55,16 @@ const Header = React.memo(
|
||||
>
|
||||
<Icon fitted name="arrow left" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
className={classNames(
|
||||
styles.item,
|
||||
canEditProject && styles.itemHoverable,
|
||||
styles.title,
|
||||
)}
|
||||
<Menu.Item className={classNames(styles.item, styles.title)}>
|
||||
{project.name}
|
||||
{canEditProject && (
|
||||
<Button
|
||||
className={classNames(styles.editButton, styles.target)}
|
||||
onClick={handleProjectSettingsClick}
|
||||
>
|
||||
{project.name}
|
||||
<Icon fitted name="pencil" size="small" />
|
||||
</Button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
:global(#app) {
|
||||
.editButton {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
line-height: 34px;
|
||||
margin-left: 8px;
|
||||
opacity: 0;
|
||||
padding: 0;
|
||||
width: 34px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
cursor: auto;
|
||||
user-select: auto;
|
||||
@@ -11,6 +27,10 @@
|
||||
&:hover {
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
|
||||
.target {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
max-height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
max-height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import LinkifyReact from 'linkify-react';
|
||||
|
||||
import history from '../history';
|
||||
|
||||
const Linkify = React.memo(({ children, linkStopPropagation, ...props }) => {
|
||||
const handleLinkClick = useCallback(
|
||||
(event) => {
|
||||
if (linkStopPropagation) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (!event.target.getAttribute('target')) {
|
||||
event.preventDefault();
|
||||
history.push(event.target.href);
|
||||
}
|
||||
},
|
||||
[linkStopPropagation],
|
||||
);
|
||||
|
||||
const linkRenderer = useCallback(
|
||||
({ attributes: { href, ...linkProps }, content }) => {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(href, window.location);
|
||||
} catch (error) {} // eslint-disable-line no-empty
|
||||
|
||||
const isSameSite = !!url && url.origin === window.location.origin;
|
||||
|
||||
return (
|
||||
<a
|
||||
{...linkProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
href={href}
|
||||
target={isSameSite ? undefined : '_blank'}
|
||||
rel={isSameSite ? undefined : 'noreferrer'}
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
{isSameSite ? url.pathname : content}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
[handleLinkClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<LinkifyReact
|
||||
{...props} // eslint-disable-line react/jsx-props-no-spreading
|
||||
options={{
|
||||
defaultProtocol: 'https',
|
||||
render: linkRenderer,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LinkifyReact>
|
||||
);
|
||||
});
|
||||
|
||||
Linkify.propTypes = {
|
||||
children: PropTypes.string.isRequired,
|
||||
linkStopPropagation: PropTypes.bool,
|
||||
};
|
||||
|
||||
Linkify.defaultProps = {
|
||||
linkStopPropagation: false,
|
||||
};
|
||||
|
||||
export default Linkify;
|
||||
@@ -43,7 +43,6 @@
|
||||
max-height: calc(100vh - 268px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
width: 290px;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Grid, Header, Message } from 'semantic-ui-react';
|
||||
import { Button, Form, Grid, Header, Message } from 'semantic-ui-react';
|
||||
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
|
||||
import { Input } from '../../lib/custom-ui';
|
||||
|
||||
@@ -28,6 +28,21 @@ const createMessage = (error) => {
|
||||
type: 'error',
|
||||
content: 'common.invalidPassword',
|
||||
};
|
||||
case 'Use single sign-on':
|
||||
return {
|
||||
type: 'error',
|
||||
content: 'common.useSingleSignOn',
|
||||
};
|
||||
case 'Email already in use':
|
||||
return {
|
||||
type: 'error',
|
||||
content: 'common.emailAlreadyInUse',
|
||||
};
|
||||
case 'Username already in use':
|
||||
return {
|
||||
type: 'error',
|
||||
content: 'common.usernameAlreadyInUse',
|
||||
};
|
||||
case 'Failed to fetch':
|
||||
return {
|
||||
type: 'warning',
|
||||
@@ -47,7 +62,17 @@ const createMessage = (error) => {
|
||||
};
|
||||
|
||||
const Login = React.memo(
|
||||
({ defaultData, isSubmitting, error, onAuthenticate, onMessageDismiss }) => {
|
||||
({
|
||||
defaultData,
|
||||
isSubmitting,
|
||||
isSubmittingUsingOidc,
|
||||
error,
|
||||
withOidc,
|
||||
isOidcEnforced,
|
||||
onAuthenticate,
|
||||
onAuthenticateUsingOidc,
|
||||
onMessageDismiss,
|
||||
}) => {
|
||||
const [t] = useTranslation();
|
||||
const wasSubmitting = usePrevious(isSubmitting);
|
||||
|
||||
@@ -83,8 +108,10 @@ const Login = React.memo(
|
||||
}, [onAuthenticate, data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOidcEnforced) {
|
||||
emailOrUsernameField.current.focus();
|
||||
}, []);
|
||||
}
|
||||
}, [isOidcEnforced]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasSubmitting && !isSubmitting && error) {
|
||||
@@ -135,6 +162,7 @@ const Login = React.memo(
|
||||
onDismiss={onMessageDismiss}
|
||||
/>
|
||||
)}
|
||||
{!isOidcEnforced && (
|
||||
<Form size="large" onSubmit={handleSubmit}>
|
||||
<div className={styles.inputWrapper}>
|
||||
<div className={styles.inputLabel}>{t('common.emailOrUsername')}</div>
|
||||
@@ -168,9 +196,24 @@ const Login = React.memo(
|
||||
content={t('action.logIn')}
|
||||
floated="right"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
disabled={isSubmitting || isSubmittingUsingOidc}
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
{withOidc && (
|
||||
<Button
|
||||
type="button"
|
||||
fluid={isOidcEnforced}
|
||||
primary={isOidcEnforced}
|
||||
size={isOidcEnforced ? 'large' : undefined}
|
||||
icon={isOidcEnforced ? 'right arrow' : undefined}
|
||||
labelPosition={isOidcEnforced ? 'right' : undefined}
|
||||
content={t('action.logInWithSSO')}
|
||||
loading={isSubmittingUsingOidc}
|
||||
disabled={isSubmitting || isSubmittingUsingOidc}
|
||||
onClick={onAuthenticateUsingOidc}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Grid.Column>
|
||||
@@ -201,10 +244,16 @@ const Login = React.memo(
|
||||
);
|
||||
|
||||
Login.propTypes = {
|
||||
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
defaultData: PropTypes.object.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
isSubmitting: PropTypes.bool.isRequired,
|
||||
isSubmittingUsingOidc: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
withOidc: PropTypes.bool.isRequired,
|
||||
isOidcEnforced: PropTypes.bool.isRequired,
|
||||
onAuthenticate: PropTypes.func.isRequired,
|
||||
onAuthenticateUsingOidc: PropTypes.func.isRequired,
|
||||
onMessageDismiss: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
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;
|
||||
@@ -4,7 +4,6 @@
|
||||
max-height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Route, Routes } from 'react-router-dom';
|
||||
import { ReduxRouter } from '../lib/redux-router';
|
||||
|
||||
import Paths from '../constants/Paths';
|
||||
import LoginContainer from '../containers/LoginContainer';
|
||||
import LoginWrapperContainer from '../containers/LoginWrapperContainer';
|
||||
import CoreContainer from '../containers/CoreContainer';
|
||||
import NotFound from './NotFound';
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'react-datepicker/dist/react-datepicker.css';
|
||||
import 'photoswipe/dist/photoswipe.css';
|
||||
import 'easymde/dist/easymde.min.css';
|
||||
import '../lib/custom-ui/styles.css';
|
||||
|
||||
import '../styles.module.scss';
|
||||
|
||||
function Root({ store, history }) {
|
||||
@@ -21,7 +20,8 @@ function Root({ store, history }) {
|
||||
<Provider store={store}>
|
||||
<ReduxRouter history={history}>
|
||||
<Routes>
|
||||
<Route path={Paths.LOGIN} element={<LoginContainer />} />
|
||||
<Route path={Paths.LOGIN} element={<LoginWrapperContainer />} />
|
||||
<Route path={Paths.OIDC_CALLBACK} element={<LoginWrapperContainer />} />
|
||||
<Route path={Paths.ROOT} element={<CoreContainer />} />
|
||||
<Route path={Paths.PROJECTS} element={<CoreContainer />} />
|
||||
<Route path={Paths.BOARDS} element={<CoreContainer />} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { dequal } from 'dequal';
|
||||
import omit from 'lodash/omit';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -9,7 +10,7 @@ import { useForm } from '../../hooks';
|
||||
|
||||
import styles from './UserInformationEdit.module.scss';
|
||||
|
||||
const UserInformationEdit = React.memo(({ defaultData, onUpdate }) => {
|
||||
const UserInformationEdit = React.memo(({ defaultData, isNameEditable, onUpdate }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const [data, handleFieldChange] = useForm(() => ({
|
||||
@@ -32,13 +33,17 @@ const UserInformationEdit = React.memo(({ defaultData, onUpdate }) => {
|
||||
const nameField = useRef(null);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (isNameEditable) {
|
||||
if (!cleanData.name) {
|
||||
nameField.current.select();
|
||||
return;
|
||||
}
|
||||
|
||||
onUpdate(cleanData);
|
||||
}, [onUpdate, cleanData]);
|
||||
} else {
|
||||
onUpdate(omit(cleanData, 'name'));
|
||||
}
|
||||
}, [isNameEditable, onUpdate, cleanData]);
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
@@ -48,6 +53,7 @@ const UserInformationEdit = React.memo(({ defaultData, onUpdate }) => {
|
||||
ref={nameField}
|
||||
name="name"
|
||||
value={data.name}
|
||||
disabled={!isNameEditable}
|
||||
className={styles.field}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
@@ -74,6 +80,7 @@ const UserInformationEdit = React.memo(({ defaultData, onUpdate }) => {
|
||||
|
||||
UserInformationEdit.propTypes = {
|
||||
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
isNameEditable: PropTypes.bool.isRequired,
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import { Popup } from '../lib/custom-ui';
|
||||
|
||||
import UserInformationEdit from './UserInformationEdit';
|
||||
|
||||
const UserInformationEditStep = React.memo(({ defaultData, onUpdate, onBack, onClose }) => {
|
||||
const UserInformationEditStep = React.memo(
|
||||
({ defaultData, isNameEditable, onUpdate, onBack, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
@@ -24,14 +25,20 @@ const UserInformationEditStep = React.memo(({ defaultData, onUpdate, onBack, onC
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<UserInformationEdit defaultData={defaultData} onUpdate={handleUpdate} />
|
||||
<UserInformationEdit
|
||||
defaultData={defaultData}
|
||||
isNameEditable={isNameEditable}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
UserInformationEditStep.propTypes = {
|
||||
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
isNameEditable: PropTypes.bool.isRequired,
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
|
||||
@@ -23,6 +23,8 @@ const AccountPane = React.memo(
|
||||
phone,
|
||||
organization,
|
||||
language,
|
||||
isLocked,
|
||||
isUsernameLocked,
|
||||
isAvatarUpdating,
|
||||
usernameUpdateForm,
|
||||
emailUpdateForm,
|
||||
@@ -74,6 +76,7 @@ const AccountPane = React.memo(
|
||||
phone,
|
||||
organization,
|
||||
}}
|
||||
isNameEditable={!isLocked}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
<Divider horizontal section>
|
||||
@@ -102,6 +105,8 @@ const AccountPane = React.memo(
|
||||
value={language || 'auto'}
|
||||
onChange={handleLanguageChange}
|
||||
/>
|
||||
{(!isLocked || !isUsernameLocked) && (
|
||||
<>
|
||||
<Divider horizontal section>
|
||||
<Header as="h4">
|
||||
{t('common.authentication', {
|
||||
@@ -109,13 +114,14 @@ const AccountPane = React.memo(
|
||||
})}
|
||||
</Header>
|
||||
</Divider>
|
||||
{!isUsernameLocked && (
|
||||
<div className={styles.action}>
|
||||
<UserUsernameEditPopup
|
||||
usePasswordConfirmation
|
||||
defaultData={usernameUpdateForm.data}
|
||||
username={username}
|
||||
isSubmitting={usernameUpdateForm.isSubmitting}
|
||||
error={usernameUpdateForm.error}
|
||||
usePasswordConfirmation={!isLocked} // FIXME: hack
|
||||
onUpdate={onUsernameUpdate}
|
||||
onMessageDismiss={onUsernameUpdateMessageDismiss}
|
||||
>
|
||||
@@ -126,6 +132,9 @@ const AccountPane = React.memo(
|
||||
</Button>
|
||||
</UserUsernameEditPopup>
|
||||
</div>
|
||||
)}
|
||||
{!isLocked && (
|
||||
<>
|
||||
<div className={styles.action}>
|
||||
<UserEmailEditPopup
|
||||
usePasswordConfirmation
|
||||
@@ -159,6 +168,10 @@ const AccountPane = React.memo(
|
||||
</Button>
|
||||
</UserPasswordEditPopup>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Tab.Pane>
|
||||
);
|
||||
},
|
||||
@@ -172,6 +185,8 @@ AccountPane.propTypes = {
|
||||
phone: PropTypes.string,
|
||||
organization: PropTypes.string,
|
||||
language: PropTypes.string,
|
||||
isLocked: PropTypes.bool.isRequired,
|
||||
isUsernameLocked: PropTypes.bool.isRequired,
|
||||
isAvatarUpdating: PropTypes.bool.isRequired,
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
usernameUpdateForm: PropTypes.object.isRequired,
|
||||
|
||||
@@ -17,6 +17,8 @@ const UserSettingsModal = React.memo(
|
||||
organization,
|
||||
language,
|
||||
subscribeToOwnCards,
|
||||
isLocked,
|
||||
isUsernameLocked,
|
||||
isAvatarUpdating,
|
||||
usernameUpdateForm,
|
||||
emailUpdateForm,
|
||||
@@ -48,6 +50,8 @@ const UserSettingsModal = React.memo(
|
||||
phone={phone}
|
||||
organization={organization}
|
||||
language={language}
|
||||
isLocked={isLocked}
|
||||
isUsernameLocked={isUsernameLocked}
|
||||
isAvatarUpdating={isAvatarUpdating}
|
||||
usernameUpdateForm={usernameUpdateForm}
|
||||
emailUpdateForm={emailUpdateForm}
|
||||
@@ -105,6 +109,8 @@ UserSettingsModal.propTypes = {
|
||||
organization: PropTypes.string,
|
||||
language: PropTypes.string,
|
||||
subscribeToOwnCards: PropTypes.bool.isRequired,
|
||||
isLocked: PropTypes.bool.isRequired,
|
||||
isUsernameLocked: PropTypes.bool.isRequired,
|
||||
isAvatarUpdating: PropTypes.bool.isRequired,
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
usernameUpdateForm: PropTypes.object.isRequired,
|
||||
|
||||
@@ -64,6 +64,7 @@ const ActionsStep = React.memo(
|
||||
return (
|
||||
<UserInformationEditStep
|
||||
defaultData={pick(user, ['name', 'phone', 'organization'])}
|
||||
isNameEditable={!user.isLocked}
|
||||
onUpdate={onUpdate}
|
||||
onBack={handleBack}
|
||||
onClose={onClose}
|
||||
@@ -135,11 +136,15 @@ const ActionsStep = React.memo(
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
{!user.isUsernameLocked && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
|
||||
{t('action.editUsername', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{!user.isLocked && (
|
||||
<>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditEmailClick}>
|
||||
{t('action.editEmail', {
|
||||
context: 'title',
|
||||
@@ -150,11 +155,15 @@ const ActionsStep = React.memo(
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
{!user.isDeletionLocked && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
|
||||
{t('action.deleteUser', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
</Popup.Content>
|
||||
</>
|
||||
|
||||
@@ -17,6 +17,10 @@ const Item = React.memo(
|
||||
organization,
|
||||
phone,
|
||||
isAdmin,
|
||||
isLocked,
|
||||
isRoleLocked,
|
||||
isUsernameLocked,
|
||||
isDeletionLocked,
|
||||
emailUpdateForm,
|
||||
passwordUpdateForm,
|
||||
usernameUpdateForm,
|
||||
@@ -46,7 +50,7 @@ const Item = React.memo(
|
||||
<Table.Cell>{username || '-'}</Table.Cell>
|
||||
<Table.Cell>{email}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Radio toggle checked={isAdmin} onChange={handleIsAdminChange} />
|
||||
<Radio toggle checked={isAdmin} disabled={isRoleLocked} onChange={handleIsAdminChange} />
|
||||
</Table.Cell>
|
||||
<Table.Cell textAlign="right">
|
||||
<ActionsPopup
|
||||
@@ -57,6 +61,9 @@ const Item = React.memo(
|
||||
organization,
|
||||
phone,
|
||||
isAdmin,
|
||||
isLocked,
|
||||
isUsernameLocked,
|
||||
isDeletionLocked,
|
||||
emailUpdateForm,
|
||||
passwordUpdateForm,
|
||||
usernameUpdateForm,
|
||||
@@ -88,6 +95,10 @@ Item.propTypes = {
|
||||
organization: PropTypes.string,
|
||||
phone: PropTypes.string,
|
||||
isAdmin: PropTypes.bool.isRequired,
|
||||
isLocked: PropTypes.bool.isRequired,
|
||||
isRoleLocked: PropTypes.bool.isRequired,
|
||||
isUsernameLocked: PropTypes.bool.isRequired,
|
||||
isDeletionLocked: PropTypes.bool.isRequired,
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
emailUpdateForm: PropTypes.object.isRequired,
|
||||
passwordUpdateForm: PropTypes.object.isRequired,
|
||||
|
||||
@@ -10,6 +10,7 @@ import Item from './Item';
|
||||
const UsersModal = React.memo(
|
||||
({
|
||||
items,
|
||||
canAdd,
|
||||
onUpdate,
|
||||
onUsernameUpdate,
|
||||
onUsernameUpdateMessageDismiss,
|
||||
@@ -110,6 +111,10 @@ const UsersModal = React.memo(
|
||||
organization={item.organization}
|
||||
phone={item.phone}
|
||||
isAdmin={item.isAdmin}
|
||||
isLocked={item.isLocked}
|
||||
isRoleLocked={item.isRoleLocked}
|
||||
isUsernameLocked={item.isUsernameLocked}
|
||||
isDeletionLocked={item.isDeletionLocked}
|
||||
emailUpdateForm={item.emailUpdateForm}
|
||||
passwordUpdateForm={item.passwordUpdateForm}
|
||||
usernameUpdateForm={item.usernameUpdateForm}
|
||||
@@ -126,11 +131,13 @@ const UsersModal = React.memo(
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Modal.Content>
|
||||
{canAdd && (
|
||||
<Modal.Actions>
|
||||
<UserAddPopupContainer>
|
||||
<Button positive content={t('action.addUser')} />
|
||||
</UserAddPopupContainer>
|
||||
</Modal.Actions>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
@@ -138,6 +145,7 @@ const UsersModal = React.memo(
|
||||
|
||||
UsersModal.propTypes = {
|
||||
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
canAdd: PropTypes.bool.isRequired,
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onUsernameUpdate: PropTypes.func.isRequired,
|
||||
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
|
||||
|
||||
@@ -12,14 +12,19 @@ export default {
|
||||
|
||||
/* Login */
|
||||
|
||||
LOGIN_INITIALIZE: 'LOGIN_INITIALIZE',
|
||||
AUTHENTICATE: 'AUTHENTICATE',
|
||||
AUTHENTICATE__SUCCESS: 'AUTHENTICATE__SUCCESS',
|
||||
AUTHENTICATE__FAILURE: 'AUTHENTICATE__FAILURE',
|
||||
USING_OIDC_AUTHENTICATE: 'USING_OIDC_AUTHENTICATE',
|
||||
USING_OIDC_AUTHENTICATE__SUCCESS: 'USING_OIDC_AUTHENTICATE__SUCCESS',
|
||||
USING_OIDC_AUTHENTICATE__FAILURE: 'USING_OIDC_AUTHENTICATE__FAILURE',
|
||||
AUTHENTICATE_ERROR_CLEAR: 'AUTHENTICATE_ERROR_CLEAR',
|
||||
|
||||
/* Core */
|
||||
|
||||
CORE_INITIALIZE: 'CORE_INITIALIZE',
|
||||
CORE_INITIALIZE__CONFIG_FETCH: 'CORE_INITIALIZE__CONFIG_FETCH',
|
||||
LOGOUT: 'LOGOUT',
|
||||
LOGOUT__ACCESS_TOKEN_INVALIDATE: 'LOGOUT__ACCESS_TOKEN_INVALIDATE',
|
||||
|
||||
@@ -189,6 +194,9 @@ export default {
|
||||
CARD_TRANSFER: 'CARD_TRANSFER',
|
||||
CARD_TRANSFER__SUCCESS: 'CARD_TRANSFER__SUCCESS',
|
||||
CARD_TRANSFER__FAILURE: 'CARD_TRANSFER__FAILURE',
|
||||
CARD_DUPLICATE: 'CARD_DUPLICATE',
|
||||
CARD_DUPLICATE__SUCCESS: 'CARD_DUPLICATE__SUCCESS',
|
||||
CARD_DUPLICATE__FAILURE: 'CARD_DUPLICATE__FAILURE',
|
||||
CARD_DELETE: 'CARD_DELETE',
|
||||
CARD_DELETE__SUCCESS: 'CARD_DELETE__SUCCESS',
|
||||
CARD_DELETE__FAILURE: 'CARD_DELETE__FAILURE',
|
||||
|
||||
@@ -11,11 +11,11 @@ export default {
|
||||
/* Login */
|
||||
|
||||
AUTHENTICATE: `${PREFIX}/AUTHENTICATE`,
|
||||
USING_OIDC_AUTHENTICATE: `${PREFIX}/USING_OIDC_AUTHENTICATE`,
|
||||
AUTHENTICATE_ERROR_CLEAR: `${PREFIX}/AUTHENTICATE_ERROR_CLEAR`,
|
||||
|
||||
/* Core */
|
||||
|
||||
CORE_INITIALIZE: `${PREFIX}/CORE_INITIALIZE`,
|
||||
LOGOUT: `${PREFIX}/LOGOUT`,
|
||||
|
||||
/* Modals */
|
||||
@@ -132,6 +132,8 @@ export default {
|
||||
CURRENT_CARD_MOVE: `${PREFIX}/CURRENT_CARD_MOVE`,
|
||||
CARD_TRANSFER: `${PREFIX}/CARD_TRANSFER`,
|
||||
CURRENT_CARD_TRANSFER: `${PREFIX}/CURRENT_CARD_TRANSFER`,
|
||||
CARD_DUPLICATE: `${PREFIX}/CARD_DUPLICATE`,
|
||||
CURRENT_CARD_DUPLICATE: `${PREFIX}/CURRENT_CARD_DUPLICATE`,
|
||||
CARD_DELETE: `${PREFIX}/CARD_DELETE`,
|
||||
CURRENT_CARD_DELETE: `${PREFIX}/CURRENT_CARD_DELETE`,
|
||||
CARD_DELETE_HANDLE: `${PREFIX}/CARD_DELETE_HANDLE`,
|
||||
|
||||
@@ -2,6 +2,7 @@ import Config from './Config';
|
||||
|
||||
const ROOT = `${Config.BASE_PATH}/`;
|
||||
const LOGIN = `${Config.BASE_PATH}/login`;
|
||||
const OIDC_CALLBACK = `${Config.BASE_PATH}/oidc-callback`;
|
||||
const PROJECTS = `${Config.BASE_PATH}/projects/:id`;
|
||||
const BOARDS = `${Config.BASE_PATH}/boards/:id`;
|
||||
const CARDS = `${Config.BASE_PATH}/cards/:id`;
|
||||
@@ -9,6 +10,7 @@ const CARDS = `${Config.BASE_PATH}/cards/:id`;
|
||||
export default {
|
||||
ROOT,
|
||||
LOGIN,
|
||||
OIDC_CALLBACK,
|
||||
PROJECTS,
|
||||
BOARDS,
|
||||
CARDS,
|
||||
|
||||
@@ -62,6 +62,7 @@ const mapDispatchToProps = (dispatch, { id }) =>
|
||||
onUpdate: (data) => entryActions.updateCard(id, data),
|
||||
onMove: (listId, index) => entryActions.moveCard(id, listId, index),
|
||||
onTransfer: (boardId, listId) => entryActions.transferCard(id, boardId, listId),
|
||||
onDuplicate: () => entryActions.duplicateCard(id),
|
||||
onDelete: () => entryActions.deleteCard(id),
|
||||
onUserAdd: (userId) => entryActions.addUserToCard(userId, id),
|
||||
onUserRemove: (userId) => entryActions.removeUserFromCard(userId, id),
|
||||
|
||||
@@ -78,6 +78,7 @@ const mapDispatchToProps = (dispatch) =>
|
||||
onUpdate: entryActions.updateCurrentCard,
|
||||
onMove: entryActions.moveCurrentCard,
|
||||
onTransfer: entryActions.transferCurrentCard,
|
||||
onDuplicate: entryActions.duplicateCurrentCard,
|
||||
onDelete: entryActions.deleteCurrentCard,
|
||||
onUserAdd: entryActions.addUserToCurrentCard,
|
||||
onUserRemove: entryActions.removeUserFromCurrentCard,
|
||||
|
||||
@@ -4,18 +4,18 @@ import selectors from '../selectors';
|
||||
import Core from '../components/Core';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const isCoreInitializing = selectors.selectIsCoreInitializing(state);
|
||||
const isInitializing = selectors.selectIsInitializing(state);
|
||||
const isSocketDisconnected = selectors.selectIsSocketDisconnected(state);
|
||||
const currentModal = selectors.selectCurrentModal(state);
|
||||
const currentProject = selectors.selectCurrentProject(state);
|
||||
const currentBoard = selectors.selectCurrentBoard(state);
|
||||
|
||||
return {
|
||||
isInitializing,
|
||||
isSocketDisconnected,
|
||||
currentModal,
|
||||
currentProject,
|
||||
currentBoard,
|
||||
isInitializing: isCoreInitializing,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import selectors from '../selectors';
|
||||
import entryActions from '../entry-actions';
|
||||
import Login from '../components/Login';
|
||||
|
||||
const mapStateToProps = ({
|
||||
const mapStateToProps = (state) => {
|
||||
const oidcConfig = selectors.selectOidcConfig(state);
|
||||
|
||||
const {
|
||||
ui: {
|
||||
authenticateForm: { data: defaultData, isSubmitting, error },
|
||||
authenticateForm: { data: defaultData, isSubmitting, isSubmittingUsingOidc, error },
|
||||
},
|
||||
}) => ({
|
||||
} = state;
|
||||
|
||||
return {
|
||||
defaultData,
|
||||
isSubmitting,
|
||||
isSubmittingUsingOidc,
|
||||
error,
|
||||
});
|
||||
withOidc: !!oidcConfig,
|
||||
isOidcEnforced: !!oidcConfig && oidcConfig.isEnforced,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
onAuthenticate: entryActions.authenticate,
|
||||
onAuthenticateUsingOidc: entryActions.authenticateUsingOidc,
|
||||
onMessageDismiss: entryActions.clearAuthenticateError,
|
||||
},
|
||||
dispatch,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import selectors from '../selectors';
|
||||
import LoginWrapper from '../components/LoginWrapper';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const isInitializing = selectors.selectIsInitializing(state);
|
||||
|
||||
return {
|
||||
isInitializing,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(LoginWrapper);
|
||||
@@ -15,6 +15,8 @@ const mapStateToProps = (state) => {
|
||||
organization,
|
||||
language,
|
||||
subscribeToOwnCards,
|
||||
isLocked,
|
||||
isUsernameLocked,
|
||||
isAvatarUpdating,
|
||||
emailUpdateForm,
|
||||
passwordUpdateForm,
|
||||
@@ -30,6 +32,8 @@ const mapStateToProps = (state) => {
|
||||
organization,
|
||||
language,
|
||||
subscribeToOwnCards,
|
||||
isLocked,
|
||||
isUsernameLocked,
|
||||
isAvatarUpdating,
|
||||
emailUpdateForm,
|
||||
passwordUpdateForm,
|
||||
|
||||
@@ -6,10 +6,12 @@ import entryActions from '../entry-actions';
|
||||
import UsersModal from '../components/UsersModal';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const oidcConfig = selectors.selectOidcConfig(state);
|
||||
const users = selectors.selectUsersExceptCurrent(state);
|
||||
|
||||
return {
|
||||
items: users,
|
||||
canAdd: !oidcConfig || !oidcConfig.isEnforced,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ const handleCardUpdate = (card) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const moveCard = (id, listId, index = 0) => ({
|
||||
const moveCard = (id, listId, index) => ({
|
||||
type: EntryActionTypes.CARD_MOVE,
|
||||
payload: {
|
||||
id,
|
||||
@@ -47,7 +47,7 @@ const moveCard = (id, listId, index = 0) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const moveCurrentCard = (listId, index = 0) => ({
|
||||
const moveCurrentCard = (listId, index) => ({
|
||||
type: EntryActionTypes.CURRENT_CARD_MOVE,
|
||||
payload: {
|
||||
listId,
|
||||
@@ -55,7 +55,7 @@ const moveCurrentCard = (listId, index = 0) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const transferCard = (id, boardId, listId, index = 0) => ({
|
||||
const transferCard = (id, boardId, listId, index) => ({
|
||||
type: EntryActionTypes.CARD_TRANSFER,
|
||||
payload: {
|
||||
id,
|
||||
@@ -65,7 +65,7 @@ const transferCard = (id, boardId, listId, index = 0) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const transferCurrentCard = (boardId, listId, index = 0) => ({
|
||||
const transferCurrentCard = (boardId, listId, index) => ({
|
||||
type: EntryActionTypes.CURRENT_CARD_TRANSFER,
|
||||
payload: {
|
||||
boardId,
|
||||
@@ -74,6 +74,18 @@ const transferCurrentCard = (boardId, listId, index = 0) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const duplicateCard = (id) => ({
|
||||
type: EntryActionTypes.CARD_DUPLICATE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
const duplicateCurrentCard = () => ({
|
||||
type: EntryActionTypes.CURRENT_CARD_DUPLICATE,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
const deleteCard = (id) => ({
|
||||
type: EntryActionTypes.CARD_DELETE,
|
||||
payload: {
|
||||
@@ -103,6 +115,8 @@ export default {
|
||||
moveCurrentCard,
|
||||
transferCard,
|
||||
transferCurrentCard,
|
||||
duplicateCard,
|
||||
duplicateCurrentCard,
|
||||
deleteCard,
|
||||
deleteCurrentCard,
|
||||
handleCardDelete,
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import EntryActionTypes from '../constants/EntryActionTypes';
|
||||
|
||||
const initializeCore = () => ({
|
||||
type: EntryActionTypes.CORE_INITIALIZE,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
const logout = () => ({
|
||||
const logout = (invalidateAccessToken) => ({
|
||||
type: EntryActionTypes.LOGOUT,
|
||||
payload: {},
|
||||
payload: {
|
||||
invalidateAccessToken,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
initializeCore,
|
||||
logout,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,11 @@ const authenticate = (data) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const authenticateUsingOidc = () => ({
|
||||
type: EntryActionTypes.USING_OIDC_AUTHENTICATE,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
const clearAuthenticateError = () => ({
|
||||
type: EntryActionTypes.AUTHENTICATE_ERROR_CLEAR,
|
||||
payload: {},
|
||||
@@ -14,5 +19,6 @@ const clearAuthenticateError = () => ({
|
||||
|
||||
export default {
|
||||
authenticate,
|
||||
authenticateUsingOidc,
|
||||
clearAuthenticateError,
|
||||
};
|
||||
|
||||
@@ -9,10 +9,13 @@ export default {
|
||||
dateTime: '$t(format:date) $t(format:time)',
|
||||
longDate: 'd MMM',
|
||||
longDateTime: "d MMMM 'v' p",
|
||||
fullDate: 'd MMM, y',
|
||||
fullDateTime: "d MMMM, y 'v' p",
|
||||
},
|
||||
|
||||
translation: {
|
||||
common: {
|
||||
aboutPlanka: 'O aplikaci Planka',
|
||||
account: 'Účet',
|
||||
actions: 'Akce',
|
||||
addAttachment_title: 'Přidat přílohu',
|
||||
@@ -23,7 +26,7 @@ export default {
|
||||
administrator: 'Administrátor',
|
||||
all: 'Vše',
|
||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||
'Všechny změny budou automaticky uloženy<br />po obnově spojení',
|
||||
'Všechny změny budou automaticky uloženy<br />po obnově spojení.',
|
||||
areYouSureYouWantToDeleteThisAttachment: 'Opravdu chcete smazat tuto přílohu?',
|
||||
areYouSureYouWantToDeleteThisBoard: 'Opravdu chcete smazat tuto tabuli?',
|
||||
areYouSureYouWantToDeleteThisCard: 'Opravdu chcete smazat tuto kartu?',
|
||||
@@ -36,22 +39,26 @@ export default {
|
||||
areYouSureYouWantToLeaveBoard: 'Opravdu chcete opustit tuto tabuli?',
|
||||
areYouSureYouWantToLeaveProject: 'Opravdu chcete opustit projekt?',
|
||||
areYouSureYouWantToRemoveThisManagerFromProject:
|
||||
'Are you sure you want to remove this manager from the project?',
|
||||
'Jste si jisti, že chcete tohoto správce z projektu odebrat?',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'Opravdu chcete odstranit tohoto člena z tabule?',
|
||||
'Jste si jisti, že chcete tohoto člena odebrat z tabule?',
|
||||
attachment: 'Příloha',
|
||||
attachments: 'Přílohy',
|
||||
authentication: 'Ověření',
|
||||
background: 'Pozadí',
|
||||
board: 'Tabule',
|
||||
boardNotFound_title: 'Tabule nenalezena',
|
||||
canComment: 'Může komentovat',
|
||||
canEditContentOfBoard: 'Může upravovat obsah tabule.',
|
||||
canOnlyViewBoard: 'Může zobrazit pouze tabuli.',
|
||||
cardActions_title: 'Akce na kartě',
|
||||
cardNotFound_title: 'Karta nenalezena',
|
||||
cardOrActionAreDeleted: 'Karta nebo akce je smazána',
|
||||
cardOrActionAreDeleted: 'Karta nebo akce je smazána.',
|
||||
color: 'Barva',
|
||||
copy_inline: 'copy',
|
||||
createBoard_title: 'Vytvořit tabuli',
|
||||
createLabel_title: 'Vytvořit štítek',
|
||||
createNewOneOrSelectExistingOne: 'Vytvořit nový nebo vyberte<br />již existující',
|
||||
createNewOneOrSelectExistingOne: 'Vytvořit nový nebo vyberte<br />již existující.',
|
||||
createProject_title: 'Vytvořit projekt',
|
||||
createTextFile_title: 'Vytvořit textový soubor',
|
||||
currentPassword: 'Aktuální heslo',
|
||||
@@ -68,14 +75,18 @@ export default {
|
||||
deleteTask_title: 'Smazat úkol',
|
||||
deleteUser_title: 'Smazat uživatele',
|
||||
description: 'Popis',
|
||||
detectAutomatically: 'Automatická detekce',
|
||||
dropFileToUpload: 'Přetáhnutím nahraj soubor',
|
||||
editor: 'Editor',
|
||||
editAttachment_title: 'Upravit přílohu',
|
||||
editAvatar_title: 'Upravit avatar',
|
||||
editBoard_title: 'Upravit tabuli',
|
||||
editDueDate_title: 'Upravit Termín do',
|
||||
editEmail_title: 'Upravit e-mail',
|
||||
editInformation_title: 'Upravit informace',
|
||||
editLabel_title: 'Upravit štítek',
|
||||
editPassword_title: 'Upravit heslo',
|
||||
editPermissions_title: 'Upravit oprávnění',
|
||||
editStopwatch_title: 'Upravit časovač',
|
||||
editUsername_title: 'Upravit uživatelské jméno',
|
||||
email: 'E-mail',
|
||||
@@ -89,10 +100,13 @@ export default {
|
||||
filterByLabels_title: 'Filtruj podle štítku',
|
||||
filterByMembers_title: 'Filtruj podle člena',
|
||||
fromComputer_title: 'Z počítače',
|
||||
fromTrello: 'Z Trella',
|
||||
general: 'Obecné',
|
||||
hours: 'Hodiny',
|
||||
importBoard_title: 'Importovat tabuli',
|
||||
invalidCurrentPassword: 'Neplatné aktuální heslo',
|
||||
labels: 'Štítky',
|
||||
language: 'Jazyk',
|
||||
leaveBoard_title: 'Opustit tabuli',
|
||||
leaveProject_title: 'Opustit projekt',
|
||||
list: 'Seznam',
|
||||
@@ -117,20 +131,25 @@ export default {
|
||||
phone: 'Telefon',
|
||||
preferences: 'Volby',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: dejte Ctrl-V (Cmd-V na Mac) pro vložení přílohy ze schránky',
|
||||
'Tip: stisknutím Ctrl-V (Cmd-V na Mac) přidáte přílohu ze schránky.',
|
||||
project: 'Projekt',
|
||||
projectNotFound_title: 'Projekt nenalezen',
|
||||
removeManager_title: 'Odstranit vedoucího',
|
||||
removeMember_title: 'Odstranit člena',
|
||||
searchLabels: 'Hledat štítky...',
|
||||
searchMembers: 'Hledat členy...',
|
||||
searchUsers: 'Hledat uživatele...',
|
||||
seconds: 'Vteřin',
|
||||
selectBoard: 'Vybrat tabuli',
|
||||
selectList: 'Vybrat seznam',
|
||||
selectPermissions_title: 'Vybrat oprávnění',
|
||||
selectProject: 'Vybrat projekt',
|
||||
settings: 'Nastavení',
|
||||
stopwatch: 'Časovač',
|
||||
subscribeToMyOwnCardsByDefault: 'Ve výchozím nastavení odebírat vlastní karty',
|
||||
taskActions_title: 'Akce na úkolu',
|
||||
tasks: 'Úkoly',
|
||||
thereIsNoPreviewAvailableForThisAttachment: 'Pro tuto přílohu není k dispozici žádný náhled.',
|
||||
time: 'Čas',
|
||||
title: 'Titulek',
|
||||
userActions_title: 'Akce na uživateli',
|
||||
@@ -142,6 +161,8 @@ export default {
|
||||
username: 'Uživatelské jméno',
|
||||
usernameAlreadyInUse: 'Uživatelské jméno se již používá',
|
||||
users: 'Uživatelé',
|
||||
version: 'Verze',
|
||||
viewer: 'Prohlížeč',
|
||||
writeComment: 'Napsat komentář...',
|
||||
},
|
||||
|
||||
@@ -153,6 +174,7 @@ export default {
|
||||
addCard_title: 'Přidat kartu',
|
||||
addComment: 'Přidat komentář',
|
||||
addList: 'Přidat seznam',
|
||||
addMember: 'Přidat člena',
|
||||
addMoreDetailedDescription: 'Přidat další detailní popis',
|
||||
addTask: 'Přidat úkol',
|
||||
addToCard: 'Přidat na kartu',
|
||||
@@ -178,14 +200,20 @@ export default {
|
||||
deleteTask: 'Smazat úkol',
|
||||
deleteTask_title: 'Smazat úkol',
|
||||
deleteUser: 'Smazat uživatele',
|
||||
duplicate: 'Duplikovat',
|
||||
duplicateCard_title: 'Duplikovat kartu',
|
||||
edit: 'Upravit',
|
||||
editDueDate_title: 'Upravit termín do',
|
||||
editDescription_title: 'Upravit popis',
|
||||
editEmail_title: 'Upravit e-mail',
|
||||
editInformation_title: 'Upravit informace',
|
||||
editPassword_title: 'Upravit heslo',
|
||||
editPermissions: 'Upravit oprávnění',
|
||||
editStopwatch_title: 'Upravit časovač',
|
||||
editTitle_title: 'Upravit titulek',
|
||||
editUsername_title: 'Upravit uživatelské jméno',
|
||||
hideDetails: 'Skrýt detaily',
|
||||
import: 'Import',
|
||||
leaveBoard: 'Opustit tabuli',
|
||||
leaveProject: 'Opustit projekt',
|
||||
logOut_title: 'Odhlásit se',
|
||||
@@ -201,12 +229,13 @@ export default {
|
||||
removeMember: 'Odstranit člena',
|
||||
save: 'Uložit',
|
||||
showAllAttachments: 'Zozbrazit všechny přílohy ({{hidden}} skryté)',
|
||||
showDetails: 'Zobrazit detaily',
|
||||
showFewerAttachments: 'Zobrazit méně příloh',
|
||||
start: 'Start',
|
||||
stop: 'Stop',
|
||||
subscribe: 'Odebírat',
|
||||
unsubscribe: 'Neodebírat',
|
||||
uploadNewAvatar: 'Nahrát nového avatara',
|
||||
uploadNewAvatar: 'Nahrát nový avatar',
|
||||
uploadNewImage: 'Nahrát nový obrázek',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -9,6 +9,8 @@ export default {
|
||||
dateTime: '$t(format:date) $t(format:time)',
|
||||
longDate: 'MMM d',
|
||||
longDateTime: "MMMM d 'at' p",
|
||||
fullDate: 'MMM d, y',
|
||||
fullDateTime: "MMMM d, y 'a' p",
|
||||
},
|
||||
|
||||
translation: {
|
||||
@@ -23,7 +25,7 @@ export default {
|
||||
administrator: 'Administrator',
|
||||
all: 'Alle',
|
||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||
'Alle ændringer vil automatisk blive gemt<br />ved genforbindelse',
|
||||
'Alle ændringer vil automatisk blive gemt<br />ved genforbindelse.',
|
||||
areYouSureYouWantToDeleteThisAttachment: 'Vil du slette den vedhæftede fil?',
|
||||
areYouSureYouWantToDeleteThisBoard: 'Vil du slette dette bræt?',
|
||||
areYouSureYouWantToDeleteThisCard: 'Vil du slette dette kort?',
|
||||
@@ -46,11 +48,11 @@ export default {
|
||||
boardNotFound_title: 'Bræt ikke fundet',
|
||||
cardActions_title: 'Korthandlinger',
|
||||
cardNotFound_title: 'Kort ikke fundet',
|
||||
cardOrActionAreDeleted: 'Kort eller handling er slettet',
|
||||
cardOrActionAreDeleted: 'Kort eller handling er slettet.',
|
||||
color: 'Farve',
|
||||
createBoard_title: 'Opret bræt',
|
||||
createLabel_title: 'Opret mærkat',
|
||||
createNewOneOrSelectExistingOne: 'Lav en ny eller marker<br />en eksisterende',
|
||||
createNewOneOrSelectExistingOne: 'Lav en ny eller marker<br />en eksisterende.',
|
||||
createProject_title: 'Opret projekt',
|
||||
createTextFile_title: 'Opret tekstfil',
|
||||
currentPassword: 'Nuværende løsen',
|
||||
@@ -109,14 +111,14 @@ export default {
|
||||
noLists: 'Ingen lister',
|
||||
noProjects: 'Ingen projekter',
|
||||
notifications: 'Notifikationer',
|
||||
noUnreadNotifications: 'Ingen ulæste notifikationer',
|
||||
noUnreadNotifications: 'Ingen ulæste notifikationer.',
|
||||
openBoard_title: 'Åben bræt',
|
||||
optional_inline: 'valgfri',
|
||||
organization: 'Organisation',
|
||||
phone: 'Telefon',
|
||||
preferences: 'Præferencer',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: Tryk Ctrl-V (Cmd-V på Mac) for at vedhæfte direkte fra udklipsholder',
|
||||
'Tip: Tryk Ctrl-V (Cmd-V på Mac) for at vedhæfte direkte fra udklipsholder.',
|
||||
project: 'Projekt',
|
||||
projectNotFound_title: 'Projekt ej fundet',
|
||||
removeManager_title: 'Fjern projektleder',
|
||||
|
||||
@@ -9,6 +9,8 @@ export default {
|
||||
dateTime: '$t(format:date) $t(format:time)',
|
||||
longDate: 'd. MMM',
|
||||
longDateTime: "d. MMMM 'um' p",
|
||||
fullDate: 'd. MMM. y',
|
||||
fullDateTime: "d. MMMM. y 'um' p",
|
||||
},
|
||||
|
||||
translation: {
|
||||
@@ -51,7 +53,7 @@ export default {
|
||||
boardNotFound_title: 'Board nicht gefunden',
|
||||
cardActions_title: 'Kartenaktionen',
|
||||
cardNotFound_title: 'Karte nicht gefunden',
|
||||
cardOrActionAreDeleted: 'Karte oder Aktion wurden gelöscht',
|
||||
cardOrActionAreDeleted: 'Karte oder Aktion wurden gelöscht.',
|
||||
color: 'Farbe',
|
||||
createBoard_title: 'Board erstellen',
|
||||
createLabel_title: 'Label erstellen',
|
||||
@@ -117,14 +119,14 @@ export default {
|
||||
noLists: 'Keine Listen',
|
||||
noProjects: 'Keine Projekte',
|
||||
notifications: 'Benachrichtigungen',
|
||||
noUnreadNotifications: 'Keine ungelesenen Benachrichtigungen',
|
||||
noUnreadNotifications: 'Keine ungelesenen Benachrichtigungen.',
|
||||
openBoard_title: 'Board öffnen',
|
||||
optional_inline: 'Optional',
|
||||
organization: 'Organisation',
|
||||
phone: 'Telefon',
|
||||
preferences: 'Voreinstellungen',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tipp: Drücken Sie STRG-V (Cmd-V auf Mac), um einen Anhang aus der Zwischenablage hinzuzufügen',
|
||||
'Tipp: Drücken Sie STRG-V (Cmd-V auf Mac), um einen Anhang aus der Zwischenablage hinzuzufügen.',
|
||||
project: 'Projekt',
|
||||
projectNotFound_title: 'Projekt nicht gefunden',
|
||||
removeManager_title: 'Manager entfernen',
|
||||
|
||||
@@ -5,6 +5,8 @@ export default {
|
||||
dateTime: '$t(format:date) $t(format:time)',
|
||||
longDate: 'MMM d',
|
||||
longDateTime: "MMMM d 'at' p",
|
||||
fullDate: 'MMM d, y',
|
||||
fullDateTime: "MMMM d, y 'at' p",
|
||||
},
|
||||
|
||||
translation: {
|
||||
@@ -20,7 +22,7 @@ export default {
|
||||
administrator: 'Administrator',
|
||||
all: 'All',
|
||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||
'All changes will be automatically saved<br />after connection restored',
|
||||
'All changes will be automatically saved<br />after connection restored.',
|
||||
areYouSureYouWantToDeleteThisAttachment: 'Are you sure you want to delete this attachment?',
|
||||
areYouSureYouWantToDeleteThisBoard: 'Are you sure you want to delete this board?',
|
||||
areYouSureYouWantToDeleteThisCard: 'Are you sure you want to delete this card?',
|
||||
@@ -43,15 +45,16 @@ export default {
|
||||
board: 'Board',
|
||||
boardNotFound_title: 'Board Not Found',
|
||||
canComment: 'Can comment',
|
||||
canEditContentOfBoard: 'Can edit the content of the board',
|
||||
canOnlyViewBoard: 'Can only view the board',
|
||||
canEditContentOfBoard: 'Can edit the content of the board.',
|
||||
canOnlyViewBoard: 'Can only view the board.',
|
||||
cardActions_title: 'Card Actions',
|
||||
cardNotFound_title: 'Card Not Found',
|
||||
cardOrActionAreDeleted: 'Card or action are deleted',
|
||||
cardOrActionAreDeleted: 'Card or action are deleted.',
|
||||
color: 'Color',
|
||||
copy_inline: 'copy',
|
||||
createBoard_title: 'Create Board',
|
||||
createLabel_title: 'Create Label',
|
||||
createNewOneOrSelectExistingOne: 'Create a new one or select<br />an existing one',
|
||||
createNewOneOrSelectExistingOne: 'Create a new one or select<br />an existing one.',
|
||||
createProject_title: 'Create Project',
|
||||
createTextFile_title: 'Create Text File',
|
||||
currentPassword: 'Current password',
|
||||
@@ -84,7 +87,7 @@ export default {
|
||||
editUsername_title: 'Edit Username',
|
||||
email: 'E-mail',
|
||||
emailAlreadyInUse: 'E-mail already in use',
|
||||
enterCardTitle: 'Enter card title... [Ctrl+Enter] to auto-open',
|
||||
enterCardTitle: 'Enter card title... [Ctrl+Enter] to auto-open.',
|
||||
enterDescription: 'Enter description...',
|
||||
enterFilename: 'Enter filename',
|
||||
enterListTitle: 'Enter list title...',
|
||||
@@ -117,14 +120,14 @@ export default {
|
||||
noLists: 'No lists',
|
||||
noProjects: 'No projects',
|
||||
notifications: 'Notifications',
|
||||
noUnreadNotifications: 'No unread notifications',
|
||||
noUnreadNotifications: 'No unread notifications.',
|
||||
openBoard_title: 'Open Board',
|
||||
optional_inline: 'optional',
|
||||
organization: 'Organization',
|
||||
phone: 'Phone',
|
||||
preferences: 'Preferences',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: press Ctrl-V (Cmd-V on Mac) to add an attachment from the clipboard',
|
||||
'Tip: press Ctrl-V (Cmd-V on Mac) to add an attachment from the clipboard.',
|
||||
project: 'Project',
|
||||
projectNotFound_title: 'Project Not Found',
|
||||
removeManager_title: 'Remove Manager',
|
||||
@@ -143,7 +146,7 @@ export default {
|
||||
taskActions_title: 'Task Actions',
|
||||
tasks: 'Tasks',
|
||||
thereIsNoPreviewAvailableForThisAttachment:
|
||||
'There is no preview available for this attachment',
|
||||
'There is no preview available for this attachment.',
|
||||
time: 'Time',
|
||||
title: 'Title',
|
||||
userActions_title: 'User Actions',
|
||||
@@ -194,6 +197,8 @@ export default {
|
||||
deleteTask: 'Delete task',
|
||||
deleteTask_title: 'Delete Task',
|
||||
deleteUser: 'Delete user',
|
||||
duplicate: 'Duplicate',
|
||||
duplicateCard_title: 'Duplicate Card',
|
||||
edit: 'Edit',
|
||||
editDueDate_title: 'Edit Due Date',
|
||||
editDescription_title: 'Edit Description',
|
||||
|
||||
@@ -11,10 +11,12 @@ export default {
|
||||
projectManagement: 'Project management',
|
||||
serverConnectionFailed: 'Server connection failed',
|
||||
unknownError: 'Unknown error, try again later',
|
||||
useSingleSignOn: 'Use single sign-on',
|
||||
},
|
||||
|
||||
action: {
|
||||
logIn: 'Log in',
|
||||
logInWithSSO: 'Log in with SSO',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@ export default {
|
||||
dateTime: '$t(format:date) $t(format:time)',
|
||||
longDate: 'MMM d',
|
||||
longDateTime: "MMMM d 'a' p",
|
||||
fullDate: 'MMM d, y',
|
||||
fullDateTime: "MMMM d, y 'a' p",
|
||||
},
|
||||
|
||||
translation: {
|
||||
@@ -22,7 +24,7 @@ export default {
|
||||
administrator: 'Administrador',
|
||||
all: 'Todos',
|
||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||
'Todos los cambios serán automáticamente guardados<br />después de que se restaure la conexión',
|
||||
'Todos los cambios serán automáticamente guardados<br />después de que se restaure la conexión.',
|
||||
areYouSureYouWantToDeleteThisAttachment: '¿Estás seguro de que quieres borrar este adjunto?',
|
||||
areYouSureYouWantToDeleteThisBoard: '¿Estás seguro de que quieres borrar este tablero?',
|
||||
areYouSureYouWantToDeleteThisCard: '¿Estás seguro de que quieres borrar esta tarjeta?',
|
||||
@@ -41,11 +43,11 @@ export default {
|
||||
boardNotFound_title: 'Tablero no encontrado',
|
||||
cardActions_title: 'Acciones de las tarjetas',
|
||||
cardNotFound_title: 'tarjeta no encontrada',
|
||||
cardOrActionAreDeleted: 'La tarjeta o la acción está eliminada',
|
||||
cardOrActionAreDeleted: 'La tarjeta o la acción está eliminada.',
|
||||
color: 'Color',
|
||||
createBoard_title: 'Crear Tablero',
|
||||
createLabel_title: 'Crear Etiqueta',
|
||||
createNewOneOrSelectExistingOne: 'Crear uno nuevo o seleccionar<br /> uno existente',
|
||||
createNewOneOrSelectExistingOne: 'Crear uno nuevo o seleccionar<br />uno existente.',
|
||||
createProject_title: 'Crear Proyecto',
|
||||
createTextFile_title: 'Crear archivo de Texto',
|
||||
currentPassword: 'Contraseña Actual',
|
||||
@@ -106,7 +108,7 @@ export default {
|
||||
phone: 'Teléfono',
|
||||
preferences: 'Preferencias',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: presiona Ctrl-V (Cmd-V en Mac) para añadir adjuntos desde el portapapeles',
|
||||
'Tip: presiona Ctrl-V (Cmd-V en Mac) para añadir adjuntos desde el portapapeles.',
|
||||
project: 'Proyecto',
|
||||
projectNotFound_title: 'Proyecto no encontrado',
|
||||
removeMember_title: 'Remover Miembro',
|
||||
|
||||
@@ -9,6 +9,8 @@ export default {
|
||||
dateTime: '$t(format:date) $t(format:time)',
|
||||
longDate: 'd MMM',
|
||||
longDateTime: "d MMMM 'à' p",
|
||||
fullDate: 'd MMM y',
|
||||
fullDateTime: "d MMMM y 'à' p",
|
||||
},
|
||||
|
||||
translation: {
|
||||
@@ -22,7 +24,7 @@ export default {
|
||||
administrator: 'Administrateur',
|
||||
all: 'Tout',
|
||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||
'Toutes les modifications seront automatiquement enregistrées <br /> une fois la connexion rétablie',
|
||||
'Toutes les modifications seront automatiquement enregistrées<br />une fois la connexion rétablie.',
|
||||
areYouSureYouWantToDeleteThisAttachment: 'Voulez-vous vraiment supprimer cette pièce jointe?',
|
||||
areYouSureYouWantToDeleteThisBoard: 'Êtes-vous sûr de vouloir supprimer ce forum?',
|
||||
areYouSureYouWantToDeleteThisCard: 'Voulez-vous vraiment supprimer cette carte?',
|
||||
@@ -41,11 +43,11 @@ export default {
|
||||
boardNotFound_title: 'Carte non trouvée',
|
||||
cardActions_title: 'Actions sur la carte',
|
||||
cardNotFound_title: 'Carte non trouvée',
|
||||
cardOrActionAreDeleted: "La carte ou l'action sont supprimées",
|
||||
cardOrActionAreDeleted: "La carte ou l'action sont supprimées.",
|
||||
color: 'Couleur',
|
||||
createBoard_title: 'Créer un tableau',
|
||||
createLabel_title: 'Créer une étiquette',
|
||||
createNewOneOrSelectExistingOne: 'Créez-en un nouveau ou sélectionnez <br /> un existant',
|
||||
createNewOneOrSelectExistingOne: 'Créez-en un nouveau ou sélectionnez<br />un existant.',
|
||||
createProject_title: 'Créer un projet',
|
||||
createTextFile_title: 'Créer un fichier texte',
|
||||
currentPassword: 'Mot de passe actuel',
|
||||
@@ -73,12 +75,12 @@ export default {
|
||||
editUsername_title: "Modifier le nom d'utilisateur",
|
||||
email: 'E-mail',
|
||||
emailAlreadyInUse: 'Email déjà utilisé',
|
||||
enterCardTitle: 'Entrer le titre de la carte ...',
|
||||
enterDescription: 'Entrez la description ...',
|
||||
enterCardTitle: 'Entrer le titre de la carte...',
|
||||
enterDescription: 'Entrez la description...',
|
||||
enterFilename: 'Entrez le nom du fichier',
|
||||
enterListTitle: 'Entrer le titre de la liste ...',
|
||||
enterListTitle: 'Entrer le titre de la liste...',
|
||||
enterProjectTitle: 'Saisir le titre du projet',
|
||||
enterTaskDescription: 'Saisir la description de la tâche ...',
|
||||
enterTaskDescription: 'Saisir la description de la tâche...',
|
||||
filterByLabels_title: 'Filtrer par libellés',
|
||||
filterByMembers_title: 'Filtrer par membres',
|
||||
fromComputer_title: "Depuis l'ordinateur",
|
||||
@@ -99,7 +101,7 @@ export default {
|
||||
noLists: 'Pas de listes',
|
||||
noProjects: 'Pas de projets',
|
||||
notifications: 'Notifications',
|
||||
noUnreadNotifications: 'Aucune notification non lue',
|
||||
noUnreadNotifications: 'Aucune notification non lue.',
|
||||
openBoard_title: 'Open Board',
|
||||
optional_inline: 'optionnel',
|
||||
organization: 'Organisation',
|
||||
@@ -167,6 +169,7 @@ export default {
|
||||
deleteTask: 'Supprimer la tâche',
|
||||
deleteTask_title: 'Supprimer la tâche',
|
||||
deleteUser: "Supprimer l'utilisateur",
|
||||
duplicate: 'Dupliquer',
|
||||
edit: 'Modifier',
|
||||
editDueDate_title: "Modifier la date d'échéance",
|
||||
editDescription_title: 'Éditer la description',
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
import dateFns from 'date-fns/locale/id';
|
||||
|
||||
export default {
|
||||
dateFns,
|
||||
|
||||
format: {
|
||||
date: 'dd MMM yyyy',
|
||||
time: 'p',
|
||||
dateTime: '$t(format:date) $t(format:time)',
|
||||
longDate: 'd MMM',
|
||||
longDateTime: "d MMMM 'pada' p",
|
||||
fullDate: 'd MMM, y',
|
||||
fullDateTime: "d MMM, y 'pada' p",
|
||||
},
|
||||
|
||||
translation: {
|
||||
common: {
|
||||
aboutPlanka: 'Tentang Planka',
|
||||
account: 'Akun',
|
||||
actions: 'Tindakan',
|
||||
addAttachment_title: 'Tambah Lampiran',
|
||||
addComment: 'Tambahkan komentar',
|
||||
addManager_title: 'Tambahkan Manager',
|
||||
addMember_title: 'Tambahkan Anggota',
|
||||
addUser_title: 'Tambahkan Pengguna',
|
||||
administrator: 'Administrator',
|
||||
all: 'Semua',
|
||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||
'Semua perubahan akan disimpan<br />setelah koneksi pulih.',
|
||||
areYouSureYouWantToDeleteThisAttachment: 'Apakah anda ingin menghapus lampiran ini?',
|
||||
areYouSureYouWantToDeleteThisBoard: 'Apakah anda ingin menghapus papan ini?',
|
||||
areYouSureYouWantToDeleteThisCard: 'Apakah anda ingin menghapus kartu ini?',
|
||||
areYouSureYouWantToDeleteThisComment: 'Apakah anda ingin menghapus komentar ini?',
|
||||
areYouSureYouWantToDeleteThisLabel: 'Apakah anda ingin menghapus label ini?',
|
||||
areYouSureYouWantToDeleteThisList: 'Apakah anda ingin menghapus daftar ini?',
|
||||
areYouSureYouWantToDeleteThisProject: 'Apakah anda ingin menghapus proyek ini?',
|
||||
areYouSureYouWantToDeleteThisTask: 'Apakah anda ingin menghapus tugas ini?',
|
||||
areYouSureYouWantToDeleteThisUser: 'Apakah anda ingin menghapus pengguna ini?',
|
||||
areYouSureYouWantToLeaveBoard: 'Apakah anda ingin keluar dari papan ini?',
|
||||
areYouSureYouWantToLeaveProject: 'Apakah anda ingin keluar dari proyek ini?',
|
||||
areYouSureYouWantToRemoveThisManagerFromProject:
|
||||
'Apakah anda ingin menghapus manajer ini dari papan ini?',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'Apakah anda ingin menghapus anggota ini dari papan ini?',
|
||||
attachment: 'Lampiran',
|
||||
attachments: 'Lampiran-lampiran',
|
||||
authentication: 'Autentikasi',
|
||||
background: 'Latar belakang',
|
||||
board: 'Papan',
|
||||
boardNotFound_title: 'Papan Tidak Ditemukan',
|
||||
canComment: 'Bisa berkomentar',
|
||||
canEditContentOfBoard: 'Bisa mengubah isi papan.',
|
||||
canOnlyViewBoard: 'Hanya dapat menglihat isi papan.',
|
||||
cardActions_title: 'Aksi Kartu',
|
||||
cardNotFound_title: 'Kartu Tidak Ditemukan',
|
||||
cardOrActionAreDeleted: 'Kartu atau aksi telah dihapus.',
|
||||
color: 'Warna',
|
||||
createBoard_title: 'Buat Papan',
|
||||
createLabel_title: 'Buat Label',
|
||||
createNewOneOrSelectExistingOne: 'Create a new one or select<br />an existing one.',
|
||||
createProject_title: 'Buat Proyek',
|
||||
createTextFile_title: 'Buat Berkas Teks',
|
||||
currentPassword: 'Kata sandi sekarang',
|
||||
dangerZone_title: 'Zona Berbahaya',
|
||||
date: 'Tanggal',
|
||||
dueDate_title: 'Tenggat Waktu',
|
||||
deleteAttachment_title: 'Hapus Lampiran',
|
||||
deleteBoard_title: 'Hapus Papan',
|
||||
deleteCard_title: 'Hapus Kartu',
|
||||
deleteComment_title: 'Hapus Komentar',
|
||||
deleteLabel_title: 'Hapus Label',
|
||||
deleteList_title: 'Hapus Daftar',
|
||||
deleteProject_title: 'Hapus Proyek',
|
||||
deleteTask_title: 'Hapus Tugas',
|
||||
deleteUser_title: 'Hapus Pengguna',
|
||||
description: 'Deskripsi',
|
||||
detectAutomatically: 'Deteksi otomatis',
|
||||
dropFileToUpload: 'Tarik berkas untuk menggungah',
|
||||
editor: 'Pengubah',
|
||||
editAttachment_title: 'Ubah Lampiran',
|
||||
editAvatar_title: 'Ubah Avatar',
|
||||
editBoard_title: 'Ubah Papan',
|
||||
editDueDate_title: 'Ubah Tenggat Waktu',
|
||||
editEmail_title: 'Ubah E-mail',
|
||||
editInformation_title: 'Ubah Informasi',
|
||||
editLabel_title: 'Ubah Label',
|
||||
editPassword_title: 'Ubah Kata Sandi',
|
||||
editPermissions_title: 'Ubah Izin',
|
||||
editStopwatch_title: 'Ubah Stopwatch',
|
||||
editUsername_title: 'Ubah Username',
|
||||
email: 'E-mail',
|
||||
emailAlreadyInUse: 'E-mail telah digunakan',
|
||||
enterCardTitle: 'Masukkan judul kartu... [Ctrl+Enter] untuk membuka otomatis.',
|
||||
enterDescription: 'Masukkan deskripsi...',
|
||||
enterFilename: 'Masukkan nama berkas...',
|
||||
enterListTitle: 'Masukkan judul daftar...',
|
||||
enterProjectTitle: 'Masukkan judul proyek',
|
||||
enterTaskDescription: 'Masukkan deskripsi tugas...',
|
||||
filterByLabels_title: 'Saring berdasarkan Label',
|
||||
filterByMembers_title: 'Saring berdasarkan Anggota',
|
||||
fromComputer_title: 'Dari Komputer',
|
||||
fromTrello: 'Dari Trello',
|
||||
general: 'Umum',
|
||||
hours: 'Jam',
|
||||
importBoard_title: 'Impor Papan',
|
||||
invalidCurrentPassword: 'Kata sandi saat ini tidak valid',
|
||||
labels: 'Label',
|
||||
language: 'Bahasa',
|
||||
leaveBoard_title: 'Keluar dari Papan',
|
||||
leaveProject_title: 'Keluar dari Proyek',
|
||||
list: 'Daftar',
|
||||
listActions_title: 'Aksi Daftar',
|
||||
managers: 'Manager',
|
||||
members: 'Anggota',
|
||||
minutes: 'Menit',
|
||||
moveCard_title: 'Pindahkan Kartu',
|
||||
name: 'Nama',
|
||||
newEmail: 'E-mail baru',
|
||||
newPassword: 'Kata sandi baru',
|
||||
newUsername: 'Username baru',
|
||||
noConnectionToServer: 'Tidak ada koneksi ke server',
|
||||
noBoards: 'Tidak ada papan',
|
||||
noLists: 'Tidak ada daftar',
|
||||
noProjects: 'Tidak ada projek',
|
||||
notifications: 'Notifikasi',
|
||||
noUnreadNotifications: 'Tiada notifikasi yang belum dibaca.',
|
||||
openBoard_title: 'Buka Papan',
|
||||
optional_inline: 'opsional',
|
||||
organization: 'Organisasi',
|
||||
phone: 'Ponsel',
|
||||
preferences: 'Preferensi',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: tekan Ctrl-V (Cmd-V di Mac) untuk menambahkan lampiran dari papan klip.',
|
||||
project: 'Proyek',
|
||||
projectNotFound_title: 'Proyek Tidak Ditemukan',
|
||||
removeManager_title: 'Hapus Manager',
|
||||
removeMember_title: 'Hapus Anggota',
|
||||
searchLabels: 'Cari label...',
|
||||
searchMembers: 'Cari anggota...',
|
||||
searchUsers: 'Cari pengguna...',
|
||||
seconds: 'Detik',
|
||||
selectBoard: 'Pilih papan',
|
||||
selectList: 'Pilih daftar',
|
||||
selectPermissions_title: 'Pilih Izin',
|
||||
selectProject: 'Pilih proyek',
|
||||
settings: 'Setelan',
|
||||
stopwatch: 'Stopwatch',
|
||||
subscribeToMyOwnCardsByDefault: 'Berlangganan kartu saya sendiri secara default',
|
||||
taskActions_title: 'Aksi Tugas',
|
||||
tasks: 'Tugas',
|
||||
thereIsNoPreviewAvailableForThisAttachment:
|
||||
'Tidak ada pratinjau yang tersedia untuk lampiran ini.',
|
||||
time: 'Waktu',
|
||||
title: 'Judul',
|
||||
userActions_title: 'Aksi Pengguna',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> menambahkan kartu ini ke {{list}}</1>',
|
||||
userLeftNewCommentToCard: '{{user}} mengomentari «{{comment}}» di <2>{{card}}</2>',
|
||||
userMovedCardFromListToList:
|
||||
'{{user}} memindahkan <2>{{card}}</2> dari {{fromList}} ke {{toList}}',
|
||||
userMovedThisCardFromListToList:
|
||||
'<0>{{user}}</0><1> memindahkan kartu ini dari {{fromList}} ke {{toList}}</1>',
|
||||
username: 'Username',
|
||||
usernameAlreadyInUse: 'Username telah digunakan',
|
||||
users: 'Pengguna',
|
||||
version: 'Versi',
|
||||
viewer: 'Penglihat',
|
||||
writeComment: 'Tuliskan komentar...',
|
||||
},
|
||||
|
||||
action: {
|
||||
addAnotherCard: 'Tambahkan kartu lain',
|
||||
addAnotherList: 'Tambahkan daftar lain',
|
||||
addAnotherTask: 'Tambahkan aksi lain',
|
||||
addCard: 'Tambah kartu',
|
||||
addCard_title: 'Tambah Kartu',
|
||||
addComment: 'Tambah komentar',
|
||||
addList: 'Tambah daftar',
|
||||
addMember: 'Tambah anggota',
|
||||
addMoreDetailedDescription: 'Tambahkan deskripsi yang lebih detail',
|
||||
addTask: 'Tambah tugas',
|
||||
addToCard: 'Tambahkan ke kartu',
|
||||
addUser: 'Tambah pengguna',
|
||||
createBoard: 'Tambah papan',
|
||||
createFile: 'Tambah berkas',
|
||||
createLabel: 'Tambah label',
|
||||
createNewLabel: 'Tambah label baru',
|
||||
createProject: 'Tambah proyek',
|
||||
delete: 'Hapus',
|
||||
deleteAttachment: 'Hapus lampiran',
|
||||
deleteAvatar: 'Hapus avatar',
|
||||
deleteBoard: 'Hapus papan',
|
||||
deleteCard: 'Hapus kartu',
|
||||
deleteCard_title: 'Hapus Kartu',
|
||||
deleteComment: 'Hapus komentar',
|
||||
deleteImage: 'Hapus gambar',
|
||||
deleteLabel: 'Hapus labek',
|
||||
deleteList: 'Hapus daftar',
|
||||
deleteList_title: 'Hapus Daftar',
|
||||
deleteProject: 'Hapus proyek',
|
||||
deleteProject_title: 'Hapus Proyek',
|
||||
deleteTask: 'Hapus tugas',
|
||||
deleteTask_title: 'Hapus Tugas',
|
||||
deleteUser: 'Hapus pengguna',
|
||||
edit: 'Ubah',
|
||||
editDueDate_title: 'Ubah Tenggat Waktu',
|
||||
editDescription_title: 'Ubdah Deskripsi',
|
||||
editEmail_title: 'Ubah E-mail',
|
||||
editInformation_title: 'Ubah Informasi',
|
||||
editPassword_title: 'Ubah Kata Sandi',
|
||||
editPermissions: 'Ubah izin',
|
||||
editStopwatch_title: 'Ubah Stopwatch',
|
||||
editTitle_title: 'Ubah Judul',
|
||||
editUsername_title: 'Ubah Username',
|
||||
hideDetails: 'Sembunyikan detail',
|
||||
import: 'Impor',
|
||||
leaveBoard: 'Keluar dari papan',
|
||||
leaveProject: 'Keluar dari proyek',
|
||||
logOut_title: 'Keluar',
|
||||
makeCover_title: 'Buat Cover',
|
||||
move: 'Pindah',
|
||||
moveCard_title: 'Pindahkan Kartu',
|
||||
remove: 'Hapus',
|
||||
removeBackground: 'Hapus latar belakang',
|
||||
removeCover_title: 'Hapus Cover',
|
||||
removeFromBoard: 'Hapus dari papan',
|
||||
removeFromProject: 'Hapus dari proyek',
|
||||
removeManager: 'Hapus manager',
|
||||
removeMember: 'Hapus papan',
|
||||
save: 'Simpan',
|
||||
showAllAttachments: 'Tampilkan semua lampiran ({{hidden}} tersembunyi)',
|
||||
showDetails: 'Tampilkan detail',
|
||||
showFewerAttachments: 'Tampilkan lampiran lebih sedikit',
|
||||
start: 'Mulai',
|
||||
stop: 'Berhenti',
|
||||
subscribe: 'Berlanggan',
|
||||
unsubscribe: 'Berhenti berlangganan',
|
||||
uploadNewAvatar: 'Unggah avatar baru',
|
||||
uploadNewImage: 'Unggah gambar baru',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import login from './login';
|
||||
|
||||
export default {
|
||||
language: 'id',
|
||||
country: 'id',
|
||||
name: 'Bahasa Indonesia',
|
||||
embeddedLocale: login,
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
export default {
|
||||
translation: {
|
||||
common: {
|
||||
emailOrUsername: 'E-mail atau username',
|
||||
invalidEmailOrUsername: 'E-mail atau username salah',
|
||||
invalidPassword: 'Kata sandi salah',
|
||||
logInToPlanka: 'Masuk ke Planka',
|
||||
noInternetConnection: 'Tidak ada koneksi internet',
|
||||
pageNotFound_title: 'Halaman Tidak Ditemukan',
|
||||
password: 'Kata sandi',
|
||||
projectManagement: 'Manajemen projek',
|
||||
serverConnectionFailed: 'Koneksi server gagal',
|
||||
unknownError: 'Kesalahan tidak diketahui, coba lagi nanti.',
|
||||
useSingleSignOn: 'Gunakan single sign-on',
|
||||
},
|
||||
|
||||
action: {
|
||||
logIn: 'Masuk',
|
||||
logInWithSSO: 'Masuk dengan SSO',
|
||||
},
|
||||
},
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user