mirror of
https://github.com/plankanban/planka.git
synced 2026-05-04 18:00:55 +03:00
Compare commits
119 Commits
planka-1.1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8dcd7cef3 | ||
|
|
6e2e6539d1 | ||
|
|
e429b478ca | ||
|
|
0e5dedd627 | ||
|
|
eb90b12b0e | ||
|
|
cd9ba8ff4f | ||
|
|
4e23f3c8a9 | ||
|
|
a9f28b45f4 | ||
|
|
d44e0dde93 | ||
|
|
20eb57de4c | ||
|
|
4ccc71419f | ||
|
|
0e93111e1b | ||
|
|
11f7814e87 | ||
|
|
9a4241e3d1 | ||
|
|
4fd164c81a | ||
|
|
5331f590a2 | ||
|
|
082b257a50 | ||
|
|
fd0981b5b3 | ||
|
|
9b68d3fc30 | ||
|
|
922d70e289 | ||
|
|
40a3e436ad | ||
|
|
227332768c | ||
|
|
555117912c | ||
|
|
9ba5f86073 | ||
|
|
3bf6e6fa19 | ||
|
|
8cc00ef2ef | ||
|
|
3d3c8f374c | ||
|
|
7a3873cbeb | ||
|
|
512b69080d | ||
|
|
5fa6fe825b | ||
|
|
f61299fde8 | ||
|
|
a55214e617 | ||
|
|
dbad8e976b | ||
|
|
3915536e72 | ||
|
|
3b7cc9c8a7 | ||
|
|
081d0cee1f | ||
|
|
60aaa4ad92 | ||
|
|
dbff5b2b1b | ||
|
|
f9102db1b2 | ||
|
|
9abad91b04 | ||
|
|
2b5921c30d | ||
|
|
03acecc43e | ||
|
|
06bedc1f7d | ||
|
|
e31eca2273 | ||
|
|
d3ff3e2db8 | ||
|
|
952a078aa1 | ||
|
|
4ea0322dc4 | ||
|
|
105b37ef57 | ||
|
|
4fb498667e | ||
|
|
b7d3602dcd | ||
|
|
91eb43f472 | ||
|
|
f47c188331 | ||
|
|
38a45c66e3 | ||
|
|
c342d2edd7 | ||
|
|
95e479cd3a | ||
|
|
d9ae02899d | ||
|
|
5e6195b252 | ||
|
|
61a3ff55cc | ||
|
|
af0cd79535 | ||
|
|
bb907d62e4 | ||
|
|
7604a31a74 | ||
|
|
2685e5d5fc | ||
|
|
5ebe320396 | ||
|
|
66ff3b65c6 | ||
|
|
8009fbacc6 | ||
|
|
d5d3f1de44 | ||
|
|
4e9e842e3d | ||
|
|
605dcace54 | ||
|
|
61753f08eb | ||
|
|
52c96c6c8f | ||
|
|
2a1760393f | ||
|
|
7758312e05 | ||
|
|
414418130d | ||
|
|
d83ea4b146 | ||
|
|
addad4378a | ||
|
|
b9967feeea | ||
|
|
dbcdd62bdf | ||
|
|
ff4177f27a | ||
|
|
f68e7d156e | ||
|
|
bda32e0247 | ||
|
|
168776aef8 | ||
|
|
d84b615815 | ||
|
|
16b228e54e | ||
|
|
0db1a5cf0e | ||
|
|
1d0b3e9af8 | ||
|
|
dabdedf6c4 | ||
|
|
a82ae49fe4 | ||
|
|
e7326303cd | ||
|
|
b2e3aac314 | ||
|
|
8c4859fed5 | ||
|
|
e219e5be42 | ||
|
|
7d613dc171 | ||
|
|
d63cc28f23 | ||
|
|
8898a0f0a2 | ||
|
|
538280d197 | ||
|
|
aa3ebd5add | ||
|
|
f8cd7474d1 | ||
|
|
450bd875c1 | ||
|
|
8df5a111bf | ||
|
|
3c33161df6 | ||
|
|
052edc9fb1 | ||
|
|
6335b3bd3c | ||
|
|
db99227f32 | ||
|
|
a5dc0a64ac | ||
|
|
b37ca68d61 | ||
|
|
31dd816e30 | ||
|
|
d688a64e36 | ||
|
|
2c4369159b | ||
|
|
b2da1a5e18 | ||
|
|
2b699f77f4 | ||
|
|
ee917c545c | ||
|
|
3b8ad26169 | ||
|
|
ffdb7254b3 | ||
|
|
b52604045d | ||
|
|
267fce0505 | ||
|
|
6ec0bafecb | ||
|
|
b2c4c530c6 | ||
|
|
59da747e75 | ||
|
|
1264fd5715 |
@@ -11,24 +11,13 @@ server/test
|
||||
server/.tmp
|
||||
server/.venv
|
||||
|
||||
server/views/*
|
||||
!server/views/.gitkeep
|
||||
server/views/index.ejs
|
||||
|
||||
server/public/*
|
||||
!server/public/preloaded-favicons
|
||||
!server/public/favicons
|
||||
server/public/favicons/*
|
||||
!server/public/favicons/.gitkeep
|
||||
!server/public/user-avatars
|
||||
server/public/user-avatars/*
|
||||
!server/public/user-avatars/.gitkeep
|
||||
!server/public/background-images
|
||||
server/public/background-images/*
|
||||
!server/public/background-images/.gitkeep
|
||||
server/data/*
|
||||
!server/data/.gitkeep
|
||||
|
||||
server/private/*
|
||||
!server/private/attachments
|
||||
server/private/attachments/*
|
||||
!server/private/attachments/.gitkeep
|
||||
server/terms/*
|
||||
!server/terms/_template
|
||||
!server/terms/cloud
|
||||
|
||||
client/dist
|
||||
|
||||
@@ -18,9 +18,6 @@ jobs:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Update npm
|
||||
run: npm install npm --global
|
||||
|
||||
- name: Install server dependencies
|
||||
run: npm install --omit=prod --ignore-scripts
|
||||
working-directory: ./server
|
||||
@@ -34,7 +31,7 @@ jobs:
|
||||
working-directory: ./client
|
||||
|
||||
- name: Build client
|
||||
run: DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
run: INDEX_FORMAT=ejs DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
working-directory: ./client
|
||||
|
||||
- name: Include licenses into dist
|
||||
@@ -45,7 +42,7 @@ jobs:
|
||||
- name: Include built client into dist
|
||||
run: |
|
||||
mv ../../client/dist/* public
|
||||
cp public/index.html views
|
||||
mv public/index.ejs views
|
||||
working-directory: ./server/dist
|
||||
|
||||
- name: Create release package
|
||||
|
||||
@@ -12,6 +12,30 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install client dependencies
|
||||
run: npm install --omit=dev
|
||||
working-directory: ./client
|
||||
|
||||
- name: Build client
|
||||
run: |
|
||||
INDEX_FORMAT=ejs DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
mv dist build
|
||||
working-directory: ./client
|
||||
|
||||
- name: Update Dockerfile to use prebuilt client
|
||||
run: |
|
||||
sed -i '/^FROM node:22 AS client/,/^ && INDEX_FORMAT=ejs DISABLE_ESLINT_PLUGIN=true npm run build$/c\
|
||||
FROM node:22 AS client\n\
|
||||
WORKDIR /app\n\
|
||||
COPY client/build /app/dist' Dockerfile
|
||||
cat Dockerfile
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
@@ -40,6 +64,10 @@ jobs:
|
||||
name=ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=raw,value=${{ steps.set-version.outputs.result }}
|
||||
type=raw,value=latest
|
||||
labels: |
|
||||
org.opencontainers.image.licenses=Fair Use License
|
||||
org.opencontainers.image.url=https://planka.app
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
|
||||
@@ -17,6 +17,38 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Update version with build number
|
||||
run: |
|
||||
npm version "$(node -p "require('./package.json').version")-nightly.$(git rev-list --count HEAD)" --no-git-tag-version
|
||||
npx --yes genversion --source . --template server/version-template.ejs server/version.js
|
||||
npx --yes genversion --source . --template client/version-template.ejs client/src/version.js
|
||||
|
||||
- name: Install client dependencies
|
||||
run: npm install --omit=dev
|
||||
working-directory: ./client
|
||||
|
||||
- name: Build client
|
||||
run: |
|
||||
INDEX_FORMAT=ejs DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
mv dist build
|
||||
working-directory: ./client
|
||||
|
||||
- name: Update Dockerfile to use prebuilt client
|
||||
run: |
|
||||
sed -i '/^FROM node:22 AS client/,/^ && INDEX_FORMAT=ejs DISABLE_ESLINT_PLUGIN=true npm run build$/c\
|
||||
FROM node:22 AS client\n\
|
||||
WORKDIR /app\n\
|
||||
COPY client/build /app/dist' Dockerfile
|
||||
cat Dockerfile
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
@@ -39,6 +71,9 @@ jobs:
|
||||
name=ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=raw,value=nightly
|
||||
labels: |
|
||||
org.opencontainers.image.licenses=Fair Use License
|
||||
org.opencontainers.image.url=https://planka.app
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
|
||||
4
.github/workflows/build-and-test.yml
vendored
4
.github/workflows/build-and-test.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
run: |
|
||||
npm install
|
||||
cd client
|
||||
npm run build
|
||||
INDEX_FORMAT=ejs npm run build
|
||||
|
||||
- name: Set up and start server for testing
|
||||
env:
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Seed database with terms signature
|
||||
run: |
|
||||
TERMS_SIGNATURE=$(sha256sum terms/en-US/extended.md | awk '{print $1}')
|
||||
TERMS_SIGNATURE=$(sha256sum terms/_template/en-US.md | awk '{print $1}')
|
||||
PGPASSWORD=$POSTGRES_PASSWORD psql -h localhost -U $POSTGRES_USERNAME -d $POSTGRES_DATABASE -c "UPDATE user_account SET terms_signature = '$TERMS_SIGNATURE';"
|
||||
working-directory: ./server
|
||||
|
||||
|
||||
17
Dockerfile
17
Dockerfile
@@ -8,8 +8,7 @@ WORKDIR /app
|
||||
|
||||
COPY server .
|
||||
|
||||
RUN npm install npm --global \
|
||||
&& npm install \
|
||||
RUN npm install \
|
||||
&& npm run build \
|
||||
&& npm prune --production
|
||||
|
||||
@@ -22,14 +21,13 @@ COPY client .
|
||||
|
||||
RUN npm install npm --global \
|
||||
&& npm install --omit=dev \
|
||||
&& DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
&& INDEX_FORMAT=ejs DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
|
||||
# Stage 3: Final image
|
||||
FROM node:22-alpine
|
||||
|
||||
RUN apk -U upgrade \
|
||||
&& apk add bash python3 --no-cache \
|
||||
&& npm install npm --global
|
||||
&& apk add bash python3 squid --no-cache
|
||||
|
||||
USER node
|
||||
WORKDIR /app
|
||||
@@ -41,18 +39,15 @@ COPY --from=server --chown=node:node /app/node_modules node_modules
|
||||
COPY --from=server --chown=node:node /app/dist .
|
||||
|
||||
COPY --from=client --chown=node:node /app/dist public
|
||||
COPY --from=client --chown=node:node /app/dist/index.html views
|
||||
|
||||
RUN python3 -m venv .venv \
|
||||
&& .venv/bin/pip3 install --upgrade pip \
|
||||
&& .venv/bin/pip3 install -r requirements.txt --no-cache-dir \
|
||||
&& mv .env.sample .env \
|
||||
&& mv public/index.ejs views \
|
||||
&& npm config set update-notifier false
|
||||
|
||||
VOLUME /app/public/favicons
|
||||
VOLUME /app/public/user-avatars
|
||||
VOLUME /app/public/background-images
|
||||
VOLUME /app/private/attachments
|
||||
|
||||
VOLUME /app/data
|
||||
EXPOSE 1337
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=2s --start-period=15s \
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
RUN apk -U upgrade \
|
||||
&& apk add bash build-base python3 xdg-utils --no-cache \
|
||||
&& npm install npm --global
|
||||
&& apk add bash build-base python3 xdg-utils --no-cache
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
47
README.md
47
README.md
@@ -1,23 +1,27 @@
|
||||
# PLANKA
|
||||
<div align="center">
|
||||
|
||||
**Project mastering driven by fun**
|
||||

|
||||
|
||||
 [](https://github.com/plankanban/planka/pkgs/container/planka) [](https://github.com/plankanban/planka/graphs/contributors) [](https://discord.gg/WqqYNd7Jvt)
|
||||
# PLANKA
|
||||
|
||||

|
||||
_Project mastering driven by fun_
|
||||
|
||||
[**Client demo**](https://plankanban.github.io/planka) (without server features).
|
||||
 [](https://github.com/plankanban/planka/pkgs/container/planka) [](https://github.com/plankanban/planka/graphs/contributors) [](https://discord.gg/WqqYNd7Jvt)
|
||||
|
||||
> ⚠️ The demo GIF and client demo are based on **v1** and will be updated soon.
|
||||
[Install](https://docs.planka.cloud/docs/installation/docker/production-version/) · [Demo](https://planka.app) · [Docs](https://docs.planka.cloud/docs/welcome/) · [API](https://plankanban.github.io/planka/swagger-ui/) · [Cloud](https://planka.app/pricing) · [Pro version](https://planka.app/pro)
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Collaborative Kanban Boards**: Create projects, boards, lists, cards, and manage tasks with an intuitive drag-and-drop interface
|
||||
- **Real-Time Updates**: Instant syncing across all users, no refresh needed
|
||||
- **Rich Markdown Support**: Write beautifully formatted card descriptions with a powerful markdown editor
|
||||
- **Flexible Notifications**: Get alerts through 100+ providers, fully customizable to your workflow
|
||||
- **Seamless Authentication**: Single sign-on with OpenID Connect integration
|
||||
- **Multilingual & Easy to Translate**: Full internationalization support for a global audience
|
||||
- **Collaborative Kanban Boards:** Create projects, boards, lists, cards, and manage tasks with an intuitive drag-and-drop interface
|
||||
- **Real-Time Updates:** Instant syncing across all users, no refresh needed
|
||||
- **Rich Markdown Support:** Write beautifully formatted card descriptions with a powerful markdown editor
|
||||
- **Flexible Notifications:** Get alerts through 100+ providers, fully customizable to your workflow
|
||||
- **Seamless Authentication:** Single sign-on with OpenID Connect integration
|
||||
- **Multilingual & Easy to Translate:** Full internationalization support for a global audience
|
||||
|
||||
## How to Deploy
|
||||
|
||||
@@ -25,9 +29,16 @@ PLANKA is easy to install using multiple methods - learn more in the [installati
|
||||
|
||||
For configuration and environment settings, see the [configuration section](https://docs.planka.cloud/docs/category/configuration/).
|
||||
|
||||
## Contact
|
||||
Interested in a hosted or [Pro version](https://planka.app/pro) of PLANKA? Check out the pricing on our [website](https://planka.app/pricing).
|
||||
|
||||
Interested in a hosted version of PLANKA? Email us at [github@planka.group](mailto:github@planka.group).
|
||||
## Notes App
|
||||
|
||||
A testing version of the Notes app is now available on multiple platforms:
|
||||
|
||||
- **iOS:** Join the [TestFlight](https://testflight.apple.com/join/5eJqTaJW) to try the app
|
||||
- **Windows & Android:** Download the app [here](https://planka-notes.hillerdaniel.de)
|
||||
|
||||
## Contact
|
||||
|
||||
For any security issues, please do not create a public issue on GitHub - instead, report it privately by emailing [security@planka.group](mailto:security@planka.group).
|
||||
|
||||
@@ -39,10 +50,10 @@ For any security issues, please do not create a public issue on GitHub - instead
|
||||
|
||||
PLANKA is [fair-code](https://faircode.io) distributed under the [Fair Use License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20EN.md) and [PLANKA Pro/Enterprise License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20EN.md).
|
||||
|
||||
- **Source Available**: The source code is always visible
|
||||
- **Self-Hostable**: Deploy and host it anywhere
|
||||
- **Extensible**: Customize with your own functionality
|
||||
- **Enterprise Licenses**: Available for additional features and support
|
||||
- **Source Available:** The source code is always visible
|
||||
- **Self-Hostable:** Deploy and host it anywhere
|
||||
- **Extensible:** Customize with your own functionality
|
||||
- **Enterprise Licenses:** Available for additional features and support
|
||||
|
||||
For more details, check the [License Guide](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20EN.md).
|
||||
|
||||
|
||||
BIN
assets/demo.gif
BIN
assets/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.6 MiB |
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -15,13 +15,13 @@ 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: 1.1.2
|
||||
version: 2.1.0
|
||||
|
||||
# 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: "2.0.0-rc.4"
|
||||
appVersion: "2.1.0"
|
||||
|
||||
dependencies:
|
||||
- alias: postgresql
|
||||
|
||||
@@ -68,7 +68,7 @@ helm install planka . --set secretkey=$SECRETKEY \
|
||||
|
||||
or create a values.yaml file like:
|
||||
|
||||
```yaml
|
||||
````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```
|
||||
@@ -89,11 +89,11 @@ ingress:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
|
||||
# Needed for HTTPS
|
||||
# Needed for HTTPS
|
||||
tls:
|
||||
- secretName: planka-tls # existing TLS secret in k8s
|
||||
hosts:
|
||||
- planka.example.dev
|
||||
- secretName: planka-tls # existing TLS secret in k8s
|
||||
hosts:
|
||||
- planka.example.dev
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -135,14 +135,14 @@ extraMounts:
|
||||
subPath: ca.crt
|
||||
readOnly: true
|
||||
configMap:
|
||||
name: ca-certificates # Must exist
|
||||
name: ca-certificates # Must exist
|
||||
|
||||
# Mount TLS certificates from existing Secret
|
||||
- name: tls-certs
|
||||
mountPath: /etc/ssl/private
|
||||
readOnly: true
|
||||
secret:
|
||||
secretName: planka-tls-secret # Must exist
|
||||
secretName: planka-tls-secret # Must exist
|
||||
items:
|
||||
- key: tls.crt
|
||||
path: server.crt
|
||||
@@ -178,11 +178,13 @@ extraMounts:
|
||||
A common use case is configuring OIDC with a self-hosted Keycloak instance that uses custom CA certificates.
|
||||
|
||||
First, create the CA certificate ConfigMap:
|
||||
|
||||
```bash
|
||||
kubectl create configmap ca-certificates --from-file=ca.crt=/path/to/your/ca.crt
|
||||
```
|
||||
|
||||
Then configure the chart:
|
||||
|
||||
```yaml
|
||||
# Mount custom CA certificate from existing ConfigMap
|
||||
extraMounts:
|
||||
@@ -225,6 +227,90 @@ extraEnv:
|
||||
key: api-key
|
||||
```
|
||||
|
||||
### Custom Terms of Service
|
||||
|
||||
You can provide your own End User Terms of Service by passing the markdown files directly via `values.yaml` in the `terms` configuration block. This automates the creation of a corresponding ConfigMap and volume mount.
|
||||
|
||||
```yaml
|
||||
terms:
|
||||
enabled: true
|
||||
customFiles:
|
||||
en-US.md: |
|
||||
# End User Terms of Service
|
||||
...
|
||||
[confirmations]::
|
||||
---
|
||||
✔️ **I have read and accept these End User Terms of Service**
|
||||
de-DE.md: |
|
||||
# Nutzungsbedingungen
|
||||
...
|
||||
[confirmations]::
|
||||
---
|
||||
✔️ **Ich habe diese Nutzungsbedingungen gelesen und akzeptiere sie**
|
||||
```
|
||||
|
||||
### Image Digest Pinning
|
||||
|
||||
For enhanced security and reproducibility, you can pin the container image using its SHA256 digest instead of relying solely on tags. This ensures you always deploy the exact same image, preventing tag mutations or accidental updates.
|
||||
|
||||
#### Finding the Image Digest
|
||||
|
||||
You can find the digest of a specific image tag using:
|
||||
|
||||
```bash
|
||||
docker inspect ghcr.io/plankanban/planka:latest --format='{{index .RepoDigests 0}}'
|
||||
# Output: ghcr.io/plankanban/planka@sha256:abc123def456...
|
||||
|
||||
# Or with skopeo
|
||||
skopeo inspect docker://ghcr.io/plankanban/planka:latest
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
You can use digest pinning in several ways:
|
||||
|
||||
**Option 1: Digest with tag (recommended)**
|
||||
|
||||
Includes the tag for reference while using the digest for verification:
|
||||
|
||||
```bash
|
||||
helm install planka . --set secretkey=$SECRETKEY \
|
||||
--set image.tag=latest \
|
||||
--set image.digest=abc123def456... \
|
||||
--set admin_email="demo@demo.demo" \
|
||||
--set admin_password="demo" \
|
||||
--set admin_name="Demo Demo" \
|
||||
--set admin_username="demo"
|
||||
```
|
||||
|
||||
Or in values.yaml:
|
||||
|
||||
```yaml
|
||||
image:
|
||||
repository: ghcr.io/plankanban/planka
|
||||
tag: latest
|
||||
digest: "abc123def456ab89cd12ef34ab56cd78ef90ab12cd34ef56ab78cd90ef12ab34"
|
||||
```
|
||||
|
||||
**Option 2: Digest only**
|
||||
|
||||
If you prefer to pin only by digest without specifying a tag:
|
||||
|
||||
```yaml
|
||||
image:
|
||||
repository: ghcr.io/plankanban/planka
|
||||
tag: "" # Empty - digest alone identifies the image
|
||||
digest: "abc123def456ab89cd12ef34ab56cd78ef90ab12cd34ef56ab78cd90ef12ab34"
|
||||
```
|
||||
|
||||
#### Security Benefits
|
||||
|
||||
- **Immutability**: Ensures you always deploy the exact same image
|
||||
- **Supply Chain Security**: Protects against tag mutations or registry compromise
|
||||
- **Reproducibility**: Makes deployments fully reproducible across environments
|
||||
- **Audit Trail**: Provides clear image identity in deployment manifests
|
||||
|
||||
### Complete Example
|
||||
|
||||
See `values-example.yaml` for a comprehensive example that demonstrates all the advanced features including OIDC configuration with custom CA certificates.
|
||||
````
|
||||
|
||||
13
charts/planka/templates/configmap-terms.yaml
Normal file
13
charts/planka/templates/configmap-terms.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.terms.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "planka.fullname" . }}-terms
|
||||
labels:
|
||||
{{- include "planka.labels" . | nindent 4 }}
|
||||
data:
|
||||
{{- range $key, $value := .Values.terms.customFiles }}
|
||||
{{ $key }}: |
|
||||
{{- $value | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -39,7 +39,16 @@ spec:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
{{- $imageTag := .Values.image.tag | default .Chart.AppVersion }}
|
||||
{{- if .Values.image.digest }}
|
||||
{{- if $imageTag }}
|
||||
image: "{{ .Values.image.repository }}:{{ $imageTag }}@sha256:{{ .Values.image.digest }}"
|
||||
{{- else }}
|
||||
image: "{{ .Values.image.repository }}@sha256:{{ .Values.image.digest }}"
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
image: "{{ .Values.image.repository }}:{{ $imageTag }}"
|
||||
{{- end }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
@@ -54,22 +63,23 @@ spec:
|
||||
path: /
|
||||
port: http
|
||||
volumeMounts:
|
||||
- mountPath: /app/public/favicons
|
||||
subPath: favicons
|
||||
name: planka
|
||||
- mountPath: /app/public/user-avatars
|
||||
subPath: user-avatars
|
||||
name: planka
|
||||
- mountPath: /app/public/background-images
|
||||
subPath: background-images
|
||||
name: planka
|
||||
- mountPath: /app/private/attachments
|
||||
subPath: attachments
|
||||
- mountPath: /app/data
|
||||
subPath: data
|
||||
name: planka
|
||||
{{- if .Values.securityContext.readOnlyRootFilesystem }}
|
||||
- mountPath: /app/logs
|
||||
subPath: app-logs
|
||||
name: emptydir
|
||||
- mountPath: /app/.tmp
|
||||
subPath: app-tmp
|
||||
name: emptydir
|
||||
- mountPath: /tmp
|
||||
subPath: tmp
|
||||
name: emptydir
|
||||
{{- end }}
|
||||
{{- if .Values.terms.enabled }}
|
||||
- mountPath: /app/terms/custom
|
||||
name: planka-terms
|
||||
{{- end }}
|
||||
{{- /* Extra volume mounts */}}
|
||||
{{- range .Values.extraMounts }}
|
||||
@@ -216,6 +226,11 @@ spec:
|
||||
{{- if .Values.securityContext.readOnlyRootFilesystem }}
|
||||
- name: emptydir
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- if .Values.terms.enabled }}
|
||||
- name: planka-terms
|
||||
configMap:
|
||||
name: {{ include "planka.fullname" . }}-terms
|
||||
{{- end }}
|
||||
{{- /* Extra volumes */}}
|
||||
{{- range .Values.extraMounts }}
|
||||
|
||||
@@ -9,6 +9,10 @@ image:
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
# Optional: specify the image digest for pinning by SHA256
|
||||
# When set, the image reference will include the digest for enhanced security
|
||||
# Example: "abc123def456..." (without sha256: prefix)
|
||||
digest: ""
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
@@ -244,6 +248,22 @@ extraEnv: []
|
||||
## - name: SMTP_FROM
|
||||
## value: "your_email@example.com"
|
||||
|
||||
## End User Terms of Service configuration
|
||||
## Mount custom terms of service markdown files into the Planka deployment
|
||||
##
|
||||
terms:
|
||||
enabled: false
|
||||
# Provide individual language files as key-value pairs
|
||||
# e.g.,
|
||||
# customFiles:
|
||||
# en-US.md: |
|
||||
# # End User Terms of Service
|
||||
# ...
|
||||
# de-DE.md: |
|
||||
# # Nutzungsbedingungen
|
||||
# ...
|
||||
customFiles: {}
|
||||
|
||||
## Extra volume mounts configuration
|
||||
## Mount ConfigMaps, Secrets, and arbitrary volumes to the PLANKA container
|
||||
## This allows mounting any pre-existing ConfigMaps, Secrets, or other volume types
|
||||
|
||||
5277
client/package-lock.json
generated
5277
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -78,18 +78,23 @@
|
||||
"^.+\\.(js|jsx)$": "babel-jest"
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"react-mentions": {
|
||||
"@babel/runtime": "^7.28.6"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@ballerina/highlightjs-ballerina": "^1.0.1",
|
||||
"@diplodoc/cut-extension": "^0.7.4",
|
||||
"@diplodoc/transform": "^4.64.1",
|
||||
"@gravity-ui/components": "^4.17.0",
|
||||
"@gravity-ui/markdown-editor": "^15.27.2",
|
||||
"@gravity-ui/uikit": "^7.29.0",
|
||||
"@diplodoc/cut-extension": "^1.1.1",
|
||||
"@diplodoc/transform": "^4.70.2",
|
||||
"@gravity-ui/components": "^4.18.0",
|
||||
"@gravity-ui/markdown-editor": "^15.35.1",
|
||||
"@gravity-ui/uikit": "^7.34.0",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"classnames": "^2.5.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dequal": "^2.0.3",
|
||||
"highlight.js": "^11.11.1",
|
||||
"highlightjs-4d": "^1.0.6",
|
||||
@@ -102,7 +107,7 @@
|
||||
"highlightjs-jolie": "^0.1.8",
|
||||
"highlightjs-lean": "^1.2.0",
|
||||
"highlightjs-lookml": "^1.0.2",
|
||||
"highlightjs-macaulay2": "^0.2.5",
|
||||
"highlightjs-macaulay2": "^0.5.0",
|
||||
"highlightjs-mlir": "^0.0.1",
|
||||
"highlightjs-qsharp": "^1.0.2",
|
||||
"highlightjs-redbol": "^2.1.2",
|
||||
@@ -115,72 +120,71 @@
|
||||
"highlightjs-zenscript": "^2.0.0",
|
||||
"hightlightjs-papyrus": "^0.0.4",
|
||||
"history": "^5.3.0",
|
||||
"i18next": "^25.7.4",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"initials": "^3.1.2",
|
||||
"javascript-time-ago": "^2.5.12",
|
||||
"i18next": "^25.8.18",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"javascript-time-ago": "^2.6.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"linkify-react": "^4.3.2",
|
||||
"linkifyjs": "^4.3.2",
|
||||
"lodash": "^4.17.23",
|
||||
"lodash": "^4.18.1",
|
||||
"lowlight": "^3.3.0",
|
||||
"markdown-it": "^13.0.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"nanoid": "^5.1.7",
|
||||
"papaparse": "^5.5.3",
|
||||
"patch-package": "^8.0.1",
|
||||
"photoswipe": "^5.4.4",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-datepicker": "^4.25.0",
|
||||
"react-datepicker": "^9.1.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-dropzone": "^15.0.0",
|
||||
"react-frame-component": "^5.2.7",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^15.7.4",
|
||||
"react-i18next": "^16.5.8",
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-intersection-observer": "^10.0.3",
|
||||
"react-mentions": "^4.4.10",
|
||||
"react-photoswipe-gallery": "^2.2.7",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"react-photoswipe-gallery": "^4.0.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.13.1",
|
||||
"react-textarea-autosize": "^8.5.9",
|
||||
"react-time-ago": "^7.3.5",
|
||||
"redux": "^4.2.1",
|
||||
"react-time-ago": "^7.4.4",
|
||||
"redux": "^5.0.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-orm": "^0.16.2",
|
||||
"redux-saga": "^1.4.2",
|
||||
"reselect": "^4.1.8",
|
||||
"reselect": "^5.1.1",
|
||||
"sails.io.js": "^1.2.1",
|
||||
"sass-embedded": "^1.97.2",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"semantic-ui-react": "^2.1.5",
|
||||
"socket.io-client": "^2.5.0",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"validator": "^13.15.26",
|
||||
"vite": "^6.4.1",
|
||||
"vite": "^7.3.2",
|
||||
"vite-plugin-commonjs": "^0.10.4",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-plugin-node-polyfills": "^0.25.0",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.28.6",
|
||||
"@babel/preset-env": "^7.28.6",
|
||||
"@cucumber/cucumber": "^11.3.0",
|
||||
"@cucumber/pretty-formatter": "^1.0.1",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"@babel/preset-env": "^7.29.0",
|
||||
"@cucumber/cucumber": "^12.7.0",
|
||||
"@cucumber/pretty-formatter": "^3.2.0",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"babel-jest": "^30.3.0",
|
||||
"babel-preset-airbnb": "^5.0.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^9.1.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"jest": "^29.7.0",
|
||||
"playwright": "^1.57.0",
|
||||
"prettier": "3.3.3"
|
||||
"jest": "^30.3.0",
|
||||
"playwright": "^1.58.2",
|
||||
"prettier": "3.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/node_modules/@diplodoc/transform/lib/md.js b/node_modules/@diplodoc/transform/lib/md.js
|
||||
index a2d222e..8d1377e 100644
|
||||
index c9faa96..e4bef9b 100644
|
||||
--- a/node_modules/@diplodoc/transform/lib/md.js
|
||||
+++ b/node_modules/@diplodoc/transform/lib/md.js
|
||||
@@ -101,8 +101,12 @@ function initPlugins(md, options, pluginOptions) {
|
||||
@@ -107,8 +107,12 @@ function initPlugins(md, options, pluginOptions) {
|
||||
}
|
||||
md.use(ol_attr_conversion_1.olAttrConversion);
|
||||
plugins.forEach((plugin) => md.use(plugin, pluginOptions));
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/node_modules/@gravity-ui/markdown-editor/build/esm/bundle/wysiwyg-preset.js b/node_modules/@gravity-ui/markdown-editor/build/esm/bundle/wysiwyg-preset.js
|
||||
index 288d462..8851e98 100644
|
||||
index c0d13c3..4c6e4e9 100644
|
||||
--- a/node_modules/@gravity-ui/markdown-editor/build/esm/bundle/wysiwyg-preset.js
|
||||
+++ b/node_modules/@gravity-ui/markdown-editor/build/esm/bundle/wysiwyg-preset.js
|
||||
@@ -102,7 +102,6 @@ export const BundlePreset = (builder, opts) => {
|
||||
@@ -107,7 +107,6 @@ export const BundlePreset = (builder, opts) => {
|
||||
enableNewImageSizeCalculation: opts.enableNewImageSizeCalculation,
|
||||
...opts.imgSize,
|
||||
},
|
||||
@@ -10,7 +10,7 @@ index 288d462..8851e98 100644
|
||||
deflist: {
|
||||
deflistTermPlaceholder: () => i18nPlaceholder('deflist_term'),
|
||||
deflistDescPlaceholder: () => i18nPlaceholder('deflist_desc'),
|
||||
@@ -123,11 +122,6 @@ export const BundlePreset = (builder, opts) => {
|
||||
@@ -128,11 +127,6 @@ export const BundlePreset = (builder, opts) => {
|
||||
...opts.yfmTable,
|
||||
controls: opts.mobile ? false : opts.yfmTable?.controls,
|
||||
},
|
||||
@@ -36,6 +36,23 @@ index 8aefe20..99e59e3 100644
|
||||
}
|
||||
if (options.pmTransformers) {
|
||||
this.#pmTransformers = options.pmTransformers;
|
||||
diff --git a/node_modules/@gravity-ui/markdown-editor/build/esm/extensions/markdown/CodeBlock/CodeBlockHighlight/TooltipPlugin/index.js b/node_modules/@gravity-ui/markdown-editor/build/esm/extensions/markdown/CodeBlock/CodeBlockHighlight/TooltipPlugin/index.js
|
||||
index 5eec9bb..3abd31a 100644
|
||||
--- a/node_modules/@gravity-ui/markdown-editor/build/esm/extensions/markdown/CodeBlock/CodeBlockHighlight/TooltipPlugin/index.js
|
||||
+++ b/node_modules/@gravity-ui/markdown-editor/build/esm/extensions/markdown/CodeBlock/CodeBlockHighlight/TooltipPlugin/index.js
|
||||
@@ -75,12 +75,6 @@ export const codeLangSelectTooltipViewCreator = (view, langItems, mapping = {},
|
||||
dispatch: view.dispatch,
|
||||
}),
|
||||
},
|
||||
- {
|
||||
- id: 'code-block-copy',
|
||||
- type: ToolbarDataType.ReactNodeFn,
|
||||
- width: 28,
|
||||
- content: () => _jsx(ClipboardButton, { text: node.textContent }),
|
||||
- },
|
||||
].filter(isTruthy),
|
||||
[
|
||||
{
|
||||
diff --git a/node_modules/@gravity-ui/markdown-editor/build/esm/extensions/yfm/YfmNote/YfmNoteSpecs/index.js b/node_modules/@gravity-ui/markdown-editor/build/esm/extensions/yfm/YfmNote/YfmNoteSpecs/index.js
|
||||
index 212c583..b709383 100644
|
||||
--- a/node_modules/@gravity-ui/markdown-editor/build/esm/extensions/yfm/YfmNote/YfmNoteSpecs/index.js
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/node_modules/react-photoswipe-gallery/dist/gallery.js b/node_modules/react-photoswipe-gallery/dist/gallery.js
|
||||
index 53cc02c..f4baccb 100644
|
||||
--- a/node_modules/react-photoswipe-gallery/dist/gallery.js
|
||||
+++ b/node_modules/react-photoswipe-gallery/dist/gallery.js
|
||||
@@ -181,7 +181,7 @@ export const Gallery = ({
|
||||
alt
|
||||
} = pswpInstance.currSlide.data;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
- el.innerHTML = caption || alt || '';
|
||||
+ el.textContent = caption || alt || '';
|
||||
});
|
||||
}
|
||||
});
|
||||
87
client/patches/sails.io.js+1.2.1.patch
Normal file
87
client/patches/sails.io.js+1.2.1.patch
Normal file
@@ -0,0 +1,87 @@
|
||||
diff --git a/node_modules/sails.io.js/sails.io.js b/node_modules/sails.io.js/sails.io.js
|
||||
index 11694b5..bb5e594 100644
|
||||
--- a/node_modules/sails.io.js/sails.io.js
|
||||
+++ b/node_modules/sails.io.js/sails.io.js
|
||||
@@ -138,6 +138,15 @@
|
||||
CONNECTION_METADATA_PARAMS.platform + '=' + SDK_INFO.platform + '&' +
|
||||
CONNECTION_METADATA_PARAMS.language + '=' + SDK_INFO.language;
|
||||
|
||||
+ var MANAGER_EVENT_NAMES = new Set([
|
||||
+ 'error',
|
||||
+ 'reconnect',
|
||||
+ 'reconnect_attempt',
|
||||
+ 'reconnect_error',
|
||||
+ 'reconnect_failed',
|
||||
+ 'ping'
|
||||
+ ]);
|
||||
+
|
||||
|
||||
|
||||
|
||||
@@ -668,6 +677,7 @@
|
||||
// Okay to change global headers while socket is connected
|
||||
if (option == 'headers') {return;}
|
||||
Object.defineProperty(self, option, {
|
||||
+ enumerable: true,
|
||||
get: function() {
|
||||
if (option == 'url') {
|
||||
return _opts[option] || (self._raw && self._raw.io && self._raw.io.uri);
|
||||
@@ -986,7 +996,7 @@
|
||||
consolog('====================================');
|
||||
});
|
||||
|
||||
- self.on('reconnecting', function(numAttempts) {
|
||||
+ self.on('reconnect_attempt', function(numAttempts) {
|
||||
consolog(
|
||||
'\n'+
|
||||
' Socket is trying to reconnect to '+(self.url ? self.url : 'Sails')+'...\n'+
|
||||
@@ -1124,7 +1134,7 @@
|
||||
// off to the self._raw for consumption
|
||||
for (var evName in self.eventQueue) {
|
||||
for (var i in self.eventQueue[evName]) {
|
||||
- self._raw.on(evName, self.eventQueue[evName][i]);
|
||||
+ self._getEventTarget(evName).on(evName, self.eventQueue[evName][i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1150,10 +1160,11 @@
|
||||
* @return {SailsSocket}
|
||||
*/
|
||||
SailsSocket.prototype.on = function (evName, fn){
|
||||
+ var target = this._getEventTarget(evName);
|
||||
|
||||
// Bind the event to the raw underlying socket if possible.
|
||||
- if (this._raw) {
|
||||
- this._raw.on(evName, fn);
|
||||
+ if (target) {
|
||||
+ target.on(evName, fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1176,10 +1187,11 @@
|
||||
* @return {SailsSocket}
|
||||
*/
|
||||
SailsSocket.prototype.off = function (evName, fn){
|
||||
+ var target = this._getEventTarget(evName);
|
||||
|
||||
// Bind the event to the raw underlying socket if possible.
|
||||
- if (this._raw) {
|
||||
- this._raw.off(evName, fn);
|
||||
+ if (target) {
|
||||
+ target.off(evName, fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1491,6 +1503,12 @@
|
||||
throw new Error('`_request()` was a private API deprecated as of v0.11 of the sails.io.js client. Use `.request()` instead.');
|
||||
};
|
||||
|
||||
+ SailsSocket.prototype._getEventTarget = function (evName) {
|
||||
+ if (!this._raw) return null;
|
||||
+
|
||||
+ return MANAGER_EVENT_NAMES.has(evName) ? this._raw.io : this._raw;
|
||||
+ };
|
||||
+
|
||||
|
||||
|
||||
|
||||
17
client/src/actions/bootstrap.js
vendored
Normal file
17
client/src/actions/bootstrap.js
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const handleBootstrapUpdate = (bootstrap) => ({
|
||||
type: ActionTypes.BOOTSTRAP_UPDATE_HANDLE,
|
||||
payload: {
|
||||
bootstrap,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
handleBootstrapUpdate,
|
||||
};
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import router from './router';
|
||||
import socket from './socket';
|
||||
import bootstrap from './bootstrap';
|
||||
import login from './login';
|
||||
import core from './core';
|
||||
import modals from './modals';
|
||||
@@ -34,6 +35,7 @@ import notificationServices from './notification-services';
|
||||
export default {
|
||||
...router,
|
||||
...socket,
|
||||
...bootstrap,
|
||||
...login,
|
||||
...core,
|
||||
...modals,
|
||||
|
||||
@@ -54,6 +54,13 @@ authenticateWithOidc.failure = (error, terms) => ({
|
||||
},
|
||||
});
|
||||
|
||||
authenticateWithOidc.debug = (logs) => ({
|
||||
type: ActionTypes.WITH_OIDC_AUTHENTICATE__DEBUG,
|
||||
payload: {
|
||||
logs,
|
||||
},
|
||||
});
|
||||
|
||||
const clearAuthenticateError = () => ({
|
||||
type: ActionTypes.AUTHENTICATE_ERROR_CLEAR,
|
||||
payload: {},
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const handleUsersReset = (users) => ({
|
||||
type: ActionTypes.USERS_RESET_HANDLE,
|
||||
payload: {
|
||||
users,
|
||||
},
|
||||
});
|
||||
|
||||
const createUser = (data) => ({
|
||||
type: ActionTypes.USER_CREATE,
|
||||
payload: {
|
||||
@@ -399,6 +406,7 @@ const removeUserFromBoardFilter = (id, boardId, currentListId) => ({
|
||||
});
|
||||
|
||||
export default {
|
||||
handleUsersReset,
|
||||
createUser,
|
||||
handleUserCreate,
|
||||
clearUserCreateError,
|
||||
|
||||
@@ -13,6 +13,8 @@ const createAccessToken = (data, headers) =>
|
||||
const exchangeForAccessTokenWithOidc = (data, headers) =>
|
||||
http.post('/access-tokens/exchange-with-oidc?withHttpOnlyToken=true', data, headers);
|
||||
|
||||
const debugOidc = (data, headers) => http.post('/access-tokens/debug-oidc', data, headers);
|
||||
|
||||
// TODO: rename?
|
||||
const acceptTerms = (data, headers) => http.post('/access-tokens/accept-terms', data, headers);
|
||||
|
||||
@@ -24,6 +26,7 @@ const deleteCurrentAccessToken = (headers) => http.delete('/access-tokens/me', u
|
||||
export default {
|
||||
createAccessToken,
|
||||
exchangeForAccessTokenWithOidc,
|
||||
debugOidc,
|
||||
acceptTerms,
|
||||
revokePendingToken,
|
||||
deleteCurrentAccessToken,
|
||||
|
||||
@@ -18,7 +18,7 @@ const http = {};
|
||||
return result;
|
||||
}, new FormData());
|
||||
|
||||
return fetch(`${Config.SERVER_BASE_URL}/api${url}`, {
|
||||
return fetch(`${Config.BASE_PATH}/api${url}`, {
|
||||
method,
|
||||
headers,
|
||||
body: formData,
|
||||
|
||||
@@ -10,7 +10,7 @@ import Config from '../constants/Config';
|
||||
|
||||
const io = sailsIOClient(socketIOClient);
|
||||
|
||||
io.sails.url = Config.SERVER_BASE_URL;
|
||||
io.sails.path = `${Config.BASE_PATH}/socket.io`;
|
||||
io.sails.autoConnect = false;
|
||||
io.sails.reconnection = true;
|
||||
io.sails.useCORSRouteToGetCookie = false;
|
||||
@@ -30,8 +30,8 @@ socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
|
||||
headers,
|
||||
url: `/api${url}`,
|
||||
},
|
||||
(_, { body, error }) => {
|
||||
if (error) {
|
||||
(body, { error }) => {
|
||||
if (body instanceof Error || error) {
|
||||
reject(body);
|
||||
} else {
|
||||
resolve(body);
|
||||
|
||||
@@ -7,8 +7,8 @@ import http from './http';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const getTerms = (type, language, headers) =>
|
||||
http.get(`/terms/${type}${language ? `?language=${language}` : ''}`, undefined, headers);
|
||||
const getTerms = (language, headers) =>
|
||||
http.get(`/terms${language ? `?language=${language}` : ''}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
getTerms,
|
||||
|
||||
@@ -1,84 +1,200 @@
|
||||
## [2.4.0] - 2025-03-22
|
||||
# [2.1.1] - 2026-04-18
|
||||
|
||||
### Added
|
||||
|
||||
* Lorem ipsum dolor sit amet consectetur adipiscing elit.
|
||||
* Sed do eiusmod tempor incididunt ut labore et dolore magna.
|
||||
* Ut enim ad minim veniam quis nostrud exercitation ullamco.
|
||||
* Allow admins to edit user avatars
|
||||
* Add Pro features discovery banner
|
||||
|
||||
### Changed
|
||||
|
||||
* Duis aute irure dolor in reprehenderit in voluptate velit esse.
|
||||
* Excepteur sint occaecat cupidatat non proident sunt in culpa.
|
||||
* Remove global npm installation from Dockerfiles
|
||||
|
||||
### Fixed
|
||||
|
||||
* Nulla pariatur consectetur adipiscing elit sed do eiusmod.
|
||||
* Tempor incididunt ut labore et dolore magna aliqua enim.
|
||||
* Fix Unicode and emoji characters in user avatar initials
|
||||
* Fix S3 request checksum calculation
|
||||
* Limit proxy file descriptors to prevent excessive memory allocation
|
||||
* Improve security by fixing multiple dependency vulnerabilities
|
||||
|
||||
---
|
||||
|
||||
## [2.3.2] - 2025-02-18
|
||||
|
||||
### Fixed
|
||||
|
||||
* Minim veniam quis nostrud exercitation ullamco laboris nisi.
|
||||
* Aliquip ex ea commodo consequat duis aute irure dolor.
|
||||
|
||||
---
|
||||
|
||||
## [2.3.0] - 2025-01-29
|
||||
## [2.1.0] - 2026-03-19
|
||||
|
||||
### Added
|
||||
|
||||
* Reprehenderit in voluptate velit esse cillum dolore eu fugiat.
|
||||
* Excepteur sint occaecat cupidatat non proident culpa qui officia.
|
||||
* Support running under subpath
|
||||
* Add ability to display card ages
|
||||
* Allow exposing Swagger specification
|
||||
* Configurable HTTP timeout for OIDC
|
||||
|
||||
## [2.0.3] - 2026-03-01
|
||||
|
||||
### Fixed
|
||||
|
||||
* Improve security by ensuring the outgoing proxy is not accessible from outside
|
||||
|
||||
## [2.0.2] - 2026-02-23
|
||||
|
||||
### Fixed
|
||||
|
||||
* Prevent dropzone from overflowing content
|
||||
* Update Gravatar hash algorithm
|
||||
* Improve backup and restore scripts
|
||||
* Improve installation on Windows and containerized environments
|
||||
|
||||
## [2.0.1] - 2026-02-17
|
||||
|
||||
### Fixed
|
||||
|
||||
* Improve connection reliability after the app is idle
|
||||
* Allow loading custom End User Terms of Service
|
||||
|
||||
## [2.0.0] - 2026-02-11
|
||||
|
||||
### Added
|
||||
|
||||
* Mention users in comments
|
||||
* Add download button for file attachments
|
||||
* Enable strikethrough for cards in closed lists
|
||||
* Expand card descriptions
|
||||
* Enable copy-to-clipboard for custom fields
|
||||
* Include task assignees in member filters
|
||||
* Link tasks to cards
|
||||
* Open card actions menu on right-click
|
||||
* Hide completed tasks
|
||||
* Add dedicated button to make projects private
|
||||
* Track navigation paths when switching cards
|
||||
* Support OAuth callbacks for OIDC
|
||||
* Display legal requirements in the app
|
||||
* Track storage usage
|
||||
* Move lists between boards
|
||||
* Restore toggleable due dates
|
||||
* Add Gravatar support for avatars
|
||||
* Add board setting to expand task lists by default
|
||||
* Configure and test SMTP via UI
|
||||
* Add API key authentication
|
||||
* Add create-board button on the open-board screen
|
||||
* Support object-path mapping for OIDC attributes
|
||||
* Add basic keyboard shortcuts for cards
|
||||
* Enable copy/cut cards with keyboard shortcuts
|
||||
* Enhance card actions menu with separators and action bar
|
||||
* Display last updates in the About modal
|
||||
* Allow unlinking SSO from user accounts
|
||||
* Apply color to entire lists instead of only card bottoms
|
||||
|
||||
### Changed
|
||||
|
||||
* Deserunt mollit anim id est laborum sed ut perspiciatis.
|
||||
|
||||
### Deprecated
|
||||
|
||||
* Unde omnis iste natus error sit voluptatem accusantium doloremque.
|
||||
|
||||
---
|
||||
|
||||
## [2.2.0] - 2024-12-14
|
||||
|
||||
### Added
|
||||
|
||||
* Totam rem aperiam eaque ipsa quae ab illo inventore veritatis.
|
||||
* Quasi architecto beatae vitae dicta sunt explicabo nemo.
|
||||
|
||||
### Changed
|
||||
|
||||
* Enim ipsam voluptatem quia voluptas sit aspernatur aut odit.
|
||||
* Consequuntur magni dolores eos qui ratione voluptatem sequi.
|
||||
* Move webhooks configuration to UI
|
||||
* Parse dates as UTC without relying on TZ environment variable
|
||||
* Move About and Terms into a separate modal
|
||||
* Move infrequent card actions to a more-actions menu
|
||||
* Improve error page display
|
||||
* Enable favorites panel by default
|
||||
* Improve login page appearance
|
||||
* Enhance Markdown editor (colors, quote borders, disable fuzzy links)
|
||||
* Improve PDF viewer compatibility across browsers
|
||||
* Update background color for own comments
|
||||
* Improve browser caching for public files and attachments
|
||||
* Optimize and parallelize image processing tasks
|
||||
* Re-stream static files from S3 with protected access
|
||||
* Unify file storage directory
|
||||
* Configure proxy for outgoing traffic to prevent SSRF
|
||||
|
||||
### Fixed
|
||||
|
||||
* Neque porro quisquam est qui dolorem ipsum quia dolor.
|
||||
* Prevent editors from deleting other comments
|
||||
* Handle escape actions in mentions input correctly
|
||||
* Prevent text overflow in activities
|
||||
* Prevent deactivated users from receiving notifications
|
||||
* Preserve newlines in markdown with mentions
|
||||
* Fix app crash when boards are added before their projects
|
||||
* Enable spellcheck on all textareas
|
||||
* Fix multiple UI, toolbar, and popup styling issues
|
||||
* Limit attachment gallery content to prevent layout issues
|
||||
* Correct translations for client, server, and Markdown editor
|
||||
* Fix minor UI issues
|
||||
|
||||
---
|
||||
|
||||
## [2.1.0] - 2024-10-05
|
||||
## [2.0.0-rc.4] - 2025-09-04
|
||||
|
||||
### Added
|
||||
### Fixed
|
||||
|
||||
* Adipisci velit sed quia non numquam eius modi tempora incidunt.
|
||||
* Ut labore et dolore magnam aliquam quaerat voluptatem neque.
|
||||
* Prevent vulnerability where maliciously renamed file attachments could execute JavaScript in the gallery UI
|
||||
|
||||
---
|
||||
|
||||
## [2.0.0] - 2024-08-20
|
||||
## [2.0.0-rc.3] - 2025-05-28
|
||||
|
||||
### Added
|
||||
|
||||
* Porro quisquam est qui dolorem ipsum quia dolor sit amet.
|
||||
* Consectetur adipisci velit sed quia non numquam eius modi.
|
||||
* Tempora incidunt ut labore et dolore magnam aliquam quaerat.
|
||||
* Notify users when they are added to a card
|
||||
* Emphasize cards in colored and closed lists
|
||||
* Track board activity log changes
|
||||
* Display total number of comments on cards
|
||||
* Add CSV attachment viewer
|
||||
* Log actions when a user is removed from a card
|
||||
* Log actions when task completion status changes
|
||||
* Support Docker secrets
|
||||
|
||||
### Removed
|
||||
### Changed
|
||||
|
||||
* Voluptatem neque porro quisquam est qui dolorem ipsum quia.
|
||||
* Improve notifications popup appearance
|
||||
* Improve card content rendering and styling
|
||||
* Limit attachment content display for clarity
|
||||
* Increase maximum length of OIDC code challenge
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix disabled cards display
|
||||
* Correct translations for client, server, and Markdown editor
|
||||
* Fix minor UI issues
|
||||
|
||||
---
|
||||
|
||||
## [2.0.0-rc.2] - 2025-05-10
|
||||
|
||||
### Added
|
||||
|
||||
* Add global user roles and improve user management
|
||||
* Enable user deactivation
|
||||
* Support private and shared projects
|
||||
* Search projects by name and project groups
|
||||
* Add favorite projects with favorites panel
|
||||
* Add project descriptions and background image gallery
|
||||
* Add list types: Closed, Archive, Trash
|
||||
* Add board views: List and Grid
|
||||
* Add new Markdown editor
|
||||
* Link attachments (attach URLs)
|
||||
* Enable quick filter by current user
|
||||
* Add board settings modal
|
||||
* Subscribe to entire boards
|
||||
* Assign users to tasks
|
||||
* Support multiple task lists
|
||||
* Add more label colors
|
||||
* Always display card creator option
|
||||
* Show notification badge for board tabs
|
||||
* Display message about new version availability
|
||||
|
||||
### Changed
|
||||
|
||||
* Restrict access to users based on global roles
|
||||
* Limit email visibility
|
||||
* Make projects page responsive
|
||||
* Redesign card appearance
|
||||
* Show edit buttons only when needed
|
||||
* Use time-ago format for dates
|
||||
* Highlight recent cards
|
||||
* Improve attachment viewers and syntax highlighting
|
||||
* Restyle comments
|
||||
* Restyle login page
|
||||
* Enable user auto-subscription when commenting
|
||||
* Navigate to adjacent cards using arrow keys
|
||||
* Open same-site links in current tab
|
||||
* Improve card deletion workflow
|
||||
* Archive all cards in a closed list with one button
|
||||
* Confirm deletion actions
|
||||
* Close only active elements when clicking outside
|
||||
|
||||
### Fixed
|
||||
|
||||
* Prevent deleting the last project manager
|
||||
* Prevent deleting projects with existing boards
|
||||
|
||||
@@ -7,7 +7,7 @@ import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link } from 'react-router';
|
||||
import { Comment } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
|
||||
@@ -43,6 +43,14 @@ const Others = React.memo(() => {
|
||||
className={styles.radio}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Radio
|
||||
toggle
|
||||
name="displayCardAges"
|
||||
checked={board.displayCardAges}
|
||||
label={t('common.displayCardAges')}
|
||||
className={styles.radio}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Radio
|
||||
toggle
|
||||
name="expandTaskListsByDefault"
|
||||
|
||||
@@ -7,7 +7,7 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link } from 'react-router';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { Button, Icon } from 'semantic-ui-react';
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import React, { useCallback, useContext, useMemo, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
@@ -25,15 +23,12 @@ import EditName from './EditName';
|
||||
import CardActionsStep from '../CardActionsStep';
|
||||
|
||||
import styles from './Card.module.scss';
|
||||
import globalStyles from '../../../styles.module.scss';
|
||||
|
||||
const Card = React.memo(({ id, isInline }) => {
|
||||
const selectCardById = useMemo(() => selectors.makeSelectCardById(), []);
|
||||
const selectIsCardWithIdRecent = useMemo(() => selectors.makeSelectIsCardWithIdRecent(), []);
|
||||
const selectListById = useMemo(() => selectors.makeSelectListById(), []);
|
||||
|
||||
const card = useSelector((state) => selectCardById(state, id));
|
||||
const list = useSelector((state) => selectListById(state, card.listId));
|
||||
|
||||
const isHighlightedAsRecent = useSelector((state) => {
|
||||
const { turnOffRecentCardHighlighting } = selectors.selectCurrentUser(state);
|
||||
@@ -133,15 +128,6 @@ const Card = React.memo(({ id, isInline }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const colorLineNode = list.color && (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.colorLine,
|
||||
globalStyles[`background${upperFirst(camelCase(list.color))}`],
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.wrapper, isHighlightedAsRecent && styles.wrapperRecent, 'card')}
|
||||
@@ -162,7 +148,6 @@ const Card = React.memo(({ id, isInline }) => {
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<Content cardId={id} />
|
||||
{colorLineNode}
|
||||
</div>
|
||||
{canUseActions && (
|
||||
<CardActionsPopup ref={actionsPopupRef} cardId={id} onNameEdit={handleNameEdit}>
|
||||
@@ -175,7 +160,6 @@ const Card = React.memo(({ id, isInline }) => {
|
||||
) : (
|
||||
<span className={classNames(styles.content, card.isClosed && styles.contentDisabled)}>
|
||||
<Content cardId={id} />
|
||||
{colorLineNode}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -29,11 +29,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.colorLine {
|
||||
border-radius: 0 0 3px 3px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.content {
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { BoardMembershipRoles, BoardViews } from '../../../constants/Enums';
|
||||
import TaskList from './TaskList';
|
||||
import DueDateChip from '../DueDateChip';
|
||||
import StopwatchChip from '../StopwatchChip';
|
||||
import TimeAgo from '../../common/TimeAgo';
|
||||
import UserAvatar from '../../users/UserAvatar';
|
||||
import LabelChip from '../../labels/LabelChip';
|
||||
import CustomFieldValueChip from '../../custom-field-values/CustomFieldValueChip';
|
||||
@@ -75,12 +76,13 @@ const ProjectContent = React.memo(({ cardId }) => {
|
||||
return attachment && attachment.data.thumbnailUrls.outside360;
|
||||
});
|
||||
|
||||
const { listName, withCreator } = useSelector((state) => {
|
||||
const { listName, withCreator, withAge } = useSelector((state) => {
|
||||
const board = selectors.selectCurrentBoard(state);
|
||||
|
||||
return {
|
||||
listName: list.name && (board.view === BoardViews.KANBAN ? null : list.name),
|
||||
withCreator: board.alwaysDisplayCardCreator,
|
||||
withAge: board.displayCardAges,
|
||||
};
|
||||
}, shallowEqual);
|
||||
|
||||
@@ -115,6 +117,7 @@ const ProjectContent = React.memo(({ cardId }) => {
|
||||
card.dueDate ||
|
||||
card.stopwatch ||
|
||||
card.commentsTotal > 0 ||
|
||||
withAge ||
|
||||
attachmentsTotal > 0 ||
|
||||
notificationsTotal > 0 ||
|
||||
listName;
|
||||
@@ -236,6 +239,14 @@ const ProjectContent = React.memo(({ cardId }) => {
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{withAge && card.createdAt && (
|
||||
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
||||
<span className={styles.attachmentContent}>
|
||||
<Icon name="history" />
|
||||
<TimeAgo date={card.createdAt} />
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{!isCompact && usersNode}
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { Icon } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import markdownToText from '../../../utils/markdown-to-text';
|
||||
import { BoardViews } from '../../../constants/Enums';
|
||||
import TimeAgo from '../../common/TimeAgo';
|
||||
import LabelChip from '../../labels/LabelChip';
|
||||
import CustomFieldValueChip from '../../custom-field-values/CustomFieldValueChip';
|
||||
|
||||
@@ -52,19 +53,14 @@ const StoryContent = React.memo(({ cardId }) => {
|
||||
selectNotificationsTotalByCardId(state, cardId),
|
||||
);
|
||||
|
||||
const listName = useSelector((state) => {
|
||||
if (!list.name) {
|
||||
return null;
|
||||
}
|
||||
const { listName, withAge } = useSelector((state) => {
|
||||
const board = selectors.selectCurrentBoard(state);
|
||||
|
||||
const { view } = selectors.selectCurrentBoard(state);
|
||||
|
||||
if (view === BoardViews.KANBAN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return list.name;
|
||||
});
|
||||
return {
|
||||
listName: list.name && (board.view === BoardViews.KANBAN ? null : list.name),
|
||||
withAge: board.displayCardAges,
|
||||
};
|
||||
}, shallowEqual);
|
||||
|
||||
const coverUrl = useSelector((state) => {
|
||||
const attachment = selectAttachmentById(state, card.coverAttachmentId);
|
||||
@@ -109,7 +105,7 @@ const StoryContent = React.memo(({ cardId }) => {
|
||||
{card.name}
|
||||
</div>
|
||||
{card.description && <div className={styles.descriptionText}>{descriptionText}</div>}
|
||||
{(attachmentsTotal > 0 || notificationsTotal > 0 || listName) && (
|
||||
{(withAge || attachmentsTotal > 0 || notificationsTotal > 0 || listName) && (
|
||||
<span className={styles.attachments}>
|
||||
{notificationsTotal > 0 && (
|
||||
<span
|
||||
@@ -138,6 +134,14 @@ const StoryContent = React.memo(({ cardId }) => {
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{withAge && card.createdAt && (
|
||||
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
||||
<span className={styles.attachmentContent}>
|
||||
<Icon name="history" />
|
||||
<TimeAgo date={card.createdAt} />
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link } from 'react-router';
|
||||
import { Icon } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../../selectors';
|
||||
|
||||
@@ -132,7 +132,11 @@ const AddAttachmentZone = React.memo(({ children }) => {
|
||||
<>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<div {...getRootProps()}>
|
||||
{isDragActive && <div className={styles.dropzone}>{t('common.dropFileToUpload')}</div>}
|
||||
{isDragActive && (
|
||||
<div className={styles.dropzone}>
|
||||
<div className={styles.dropzoneText}>{t('common.dropFileToUpload')}</div>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<input {...getInputProps()} />
|
||||
|
||||
@@ -8,13 +8,18 @@
|
||||
background: white;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
height: 100%;
|
||||
inset: 0;
|
||||
line-height: 30px;
|
||||
opacity: 0.7;
|
||||
padding: 200px 50px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
z-index: 2001;
|
||||
}
|
||||
|
||||
.dropzoneText {
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: min(200px, 50%);
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ const AboutPane = React.memo(() => {
|
||||
<a href="https://github.com/plankanban/planka" target="_blank" rel="noreferrer">
|
||||
<Image centered src={aboutLogo} size="large" />
|
||||
</a>
|
||||
<div className={styles.version}>{version}</div>
|
||||
<div className={styles.version}>Community v{version}</div>
|
||||
<Divider horizontal>
|
||||
<Header as="h4">
|
||||
{t('common.whatsNew', {
|
||||
|
||||
@@ -4,19 +4,15 @@
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Loader, Tab } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import api from '../../../api';
|
||||
import Markdown from '../Markdown';
|
||||
|
||||
import styles from './TermsPane.module.scss';
|
||||
|
||||
const TermsPane = React.memo(() => {
|
||||
const type = useSelector((state) => selectors.selectCurrentUser(state).termsType);
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
const [content, setContent] = useState(null);
|
||||
|
||||
@@ -24,7 +20,7 @@ const TermsPane = React.memo(() => {
|
||||
async function fetchTerms() {
|
||||
let terms;
|
||||
try {
|
||||
({ item: terms } = await api.getTerms(type, i18n.resolvedLanguage));
|
||||
({ item: terms } = await api.getTerms(i18n.resolvedLanguage));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
@@ -33,7 +29,7 @@ const TermsPane = React.memo(() => {
|
||||
}
|
||||
|
||||
fetchTerms();
|
||||
}, [type, i18n.resolvedLanguage]);
|
||||
}, [i18n.resolvedLanguage]);
|
||||
|
||||
return (
|
||||
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
|
||||
import { dequal } from 'dequal';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import omit from 'lodash/omit';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -26,6 +27,7 @@ const SmtpPane = React.memo(() => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const [passwordFieldRef, handlePasswordFieldRef] = useNestedRef('inputRef');
|
||||
const isPasswordTouchedRef = useRef(false);
|
||||
|
||||
const defaultData = useMemo(
|
||||
() => ({
|
||||
@@ -68,7 +70,15 @@ const SmtpPane = React.memo(() => {
|
||||
[data, isPasswordSet],
|
||||
);
|
||||
|
||||
const isModified = useMemo(() => {
|
||||
const cleanDataToCheck = omit(cleanData, 'smtpPassword');
|
||||
const defaultDataToCheck = omit(defaultData, 'smtpPassword');
|
||||
|
||||
return !dequal(cleanDataToCheck, defaultDataToCheck) || isPasswordTouchedRef.current;
|
||||
}, [defaultData, cleanData]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
isPasswordTouchedRef.current = false;
|
||||
dispatch(entryActions.updateConfig(cleanData));
|
||||
}, [dispatch, cleanData]);
|
||||
|
||||
@@ -86,7 +96,13 @@ const SmtpPane = React.memo(() => {
|
||||
dispatch(entryActions.testSmtpConfig());
|
||||
}, [dispatch]);
|
||||
|
||||
const isModified = !dequal(cleanData, defaultData);
|
||||
const handlePasswordChange = useCallback(
|
||||
(event, { value, ...props }) => {
|
||||
isPasswordTouchedRef.current = value !== '';
|
||||
handleFieldChange(event, { value, ...props });
|
||||
},
|
||||
[handleFieldChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||
@@ -173,7 +189,7 @@ const SmtpPane = React.memo(() => {
|
||||
maxLength={256}
|
||||
className={styles.field}
|
||||
onClear={!data.smtpPassword && isPasswordSet ? handlePasswordClear : undefined}
|
||||
onChange={handleFieldChange}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
<div className={styles.text}>
|
||||
{t('common.defaultFrom')} (
|
||||
|
||||
@@ -17,6 +17,7 @@ import SelectRoleStep from './SelectRoleStep';
|
||||
import ApiKeyStep from './ApiKeyStep';
|
||||
import ConfirmationStep from '../../ConfirmationStep';
|
||||
import EditUserInformationStep from '../../../users/EditUserInformationStep';
|
||||
import EditUserAvatarStep from '../../../users/EditUserAvatarStep';
|
||||
import EditUserUsernameStep from '../../../users/EditUserUsernameStep';
|
||||
import EditUserEmailStep from '../../../users/EditUserEmailStep';
|
||||
import EditUserPasswordStep from '../../../users/EditUserPasswordStep';
|
||||
@@ -25,11 +26,13 @@ import styles from './ActionsStep.module.scss';
|
||||
|
||||
const StepTypes = {
|
||||
EDIT_INFORMATION: 'EDIT_INFORMATION',
|
||||
EDIT_AVATAR: 'EDIT_AVATAR',
|
||||
EDIT_USERNAME: 'EDIT_USERNAME',
|
||||
EDIT_EMAIL: 'EDIT_EMAIL',
|
||||
EDIT_PASSWORD: 'EDIT_PASSWORD',
|
||||
EDIT_ROLE: 'EDIT_ROLE',
|
||||
API_KEY: 'API_KEY',
|
||||
UNLINK_SSO: 'UNLINK_SSO',
|
||||
ACTIVATE: 'ACTIVATE',
|
||||
DEACTIVATE: 'DEACTIVATE',
|
||||
DELETE: 'DELETE',
|
||||
@@ -38,8 +41,8 @@ const StepTypes = {
|
||||
const ActionsStep = React.memo(({ userId, onClose }) => {
|
||||
const selectUserById = useMemo(() => selectors.makeSelectUserById(), []);
|
||||
|
||||
const activeUserLimit = useSelector(selectors.selectActiveUserLimit);
|
||||
const activeUserTotal = useSelector(selectors.selectActiveUserTotal);
|
||||
const activeUsersLimit = useSelector(selectors.selectActiveUsersLimit);
|
||||
const activeUsersTotal = useSelector(selectors.selectActiveUsersTotal);
|
||||
const user = useSelector((state) => selectUserById(state, userId));
|
||||
const isCurrentUser = useSelector((state) => user.id === selectors.selectCurrentUserId(state));
|
||||
|
||||
@@ -58,6 +61,16 @@ const ActionsStep = React.memo(({ userId, onClose }) => {
|
||||
[userId, dispatch],
|
||||
);
|
||||
|
||||
const handleUnlinkSsoConfirm = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.updateUser(userId, {
|
||||
isSsoUser: false,
|
||||
}),
|
||||
);
|
||||
|
||||
onClose();
|
||||
}, [userId, onClose, dispatch]);
|
||||
|
||||
const handleActivateConfirm = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.updateUser(userId, {
|
||||
@@ -86,6 +99,10 @@ const ActionsStep = React.memo(({ userId, onClose }) => {
|
||||
openStep(StepTypes.EDIT_INFORMATION);
|
||||
}, [openStep]);
|
||||
|
||||
const handleEditAvatarClick = useCallback(() => {
|
||||
openStep(StepTypes.EDIT_AVATAR);
|
||||
}, [openStep]);
|
||||
|
||||
const handleEditUsernameClick = useCallback(() => {
|
||||
openStep(StepTypes.EDIT_USERNAME);
|
||||
}, [openStep]);
|
||||
@@ -106,6 +123,10 @@ const ActionsStep = React.memo(({ userId, onClose }) => {
|
||||
openStep(StepTypes.API_KEY);
|
||||
}, [openStep]);
|
||||
|
||||
const handleUnlinkSsoClick = useCallback(() => {
|
||||
openStep(StepTypes.UNLINK_SSO);
|
||||
}, [openStep]);
|
||||
|
||||
const handleActivateClick = useCallback(() => {
|
||||
openStep(StepTypes.ACTIVATE);
|
||||
}, [openStep]);
|
||||
@@ -122,6 +143,8 @@ const ActionsStep = React.memo(({ userId, onClose }) => {
|
||||
switch (step.type) {
|
||||
case StepTypes.EDIT_INFORMATION:
|
||||
return <EditUserInformationStep id={userId} onBack={handleBack} onClose={onClose} />;
|
||||
case StepTypes.EDIT_AVATAR:
|
||||
return <EditUserAvatarStep id={userId} onBack={handleBack} onClose={onClose} />;
|
||||
case StepTypes.EDIT_USERNAME:
|
||||
return <EditUserUsernameStep id={userId} onBack={handleBack} onClose={onClose} />;
|
||||
case StepTypes.EDIT_EMAIL:
|
||||
@@ -142,6 +165,16 @@ const ActionsStep = React.memo(({ userId, onClose }) => {
|
||||
);
|
||||
case StepTypes.API_KEY:
|
||||
return <ApiKeyStep userId={userId} onBack={handleBack} onClose={onClose} />;
|
||||
case StepTypes.UNLINK_SSO:
|
||||
return (
|
||||
<ConfirmationStep
|
||||
title="common.unlinkSso"
|
||||
content="common.areYouSureYouWantToUnlinkSsoFromThisUser"
|
||||
buttonContent="action.unlinkSso"
|
||||
onConfirm={handleUnlinkSsoConfirm}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
case StepTypes.ACTIVATE:
|
||||
return (
|
||||
<ConfirmationStep
|
||||
@@ -194,6 +227,12 @@ const ActionsStep = React.memo(({ userId, onClose }) => {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditAvatarClick}>
|
||||
<Icon name="image outline" className={styles.menuItemIcon} />
|
||||
{t('action.editAvatar', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
{!user.lockedFieldNames.includes('username') && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
|
||||
<Icon name="at" className={styles.menuItemIcon} />
|
||||
@@ -232,13 +271,21 @@ const ActionsStep = React.memo(({ userId, onClose }) => {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
{user.isSsoUser && !user.lockedFieldNames.includes('isSsoUser') && !isCurrentUser && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleUnlinkSsoClick}>
|
||||
<Icon name="unlink" className={styles.menuItemIcon} />
|
||||
{t('action.unlinkSso', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{!isCurrentUser && (
|
||||
<>
|
||||
<Menu.Item
|
||||
disabled={
|
||||
user.isDeactivated &&
|
||||
activeUserLimit !== null &&
|
||||
activeUserTotal >= activeUserLimit
|
||||
activeUsersLimit !== null &&
|
||||
activeUsersTotal >= activeUsersLimit
|
||||
}
|
||||
className={styles.menuItem}
|
||||
onClick={user.isDeactivated ? handleActivateClick : handleDeactivateClick}
|
||||
|
||||
@@ -17,8 +17,8 @@ import AddStep from './AddStep';
|
||||
import styles from './UsersPane.module.scss';
|
||||
|
||||
const UsersPane = React.memo(() => {
|
||||
const activeUserTotal = useSelector(selectors.selectActiveUserTotal);
|
||||
const activeUserLimit = useSelector(selectors.selectActiveUserLimit);
|
||||
const activeUsersLimit = useSelector(selectors.selectActiveUsersLimit);
|
||||
const activeUsersTotal = useSelector(selectors.selectActiveUsersTotal);
|
||||
const users = useSelector(selectors.selectUsers);
|
||||
|
||||
const canAdd = useSelector((state) => {
|
||||
@@ -106,13 +106,13 @@ const UsersPane = React.memo(() => {
|
||||
<AddPopup>
|
||||
<Button
|
||||
positive
|
||||
disabled={activeUserLimit !== null && activeUserTotal >= activeUserLimit}
|
||||
disabled={activeUsersLimit !== null && activeUsersTotal >= activeUsersLimit}
|
||||
className={styles.addButton}
|
||||
>
|
||||
{t('action.addUser')}
|
||||
{activeUserLimit !== null && (
|
||||
{activeUsersLimit !== null && (
|
||||
<span className={styles.addButtonCounter}>
|
||||
{activeUserTotal}/{activeUserLimit}
|
||||
{activeUsersTotal}/{activeUsersLimit}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Copyright (c) 2026 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useSelector } from 'react-redux';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import Header from '../Header';
|
||||
import PromoBanner from '../PromoBanner/PromoBanner';
|
||||
import Favorites from '../Favorites';
|
||||
import HomeActions from '../HomeActions';
|
||||
import Project from '../../projects/Project';
|
||||
@@ -22,6 +23,7 @@ const Fixed = React.memo(() => {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Header />
|
||||
<PromoBanner />
|
||||
<Favorites />
|
||||
{projectId === undefined && <HomeActions />}
|
||||
{projectId && <Project />}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link } from 'react-router';
|
||||
import { Button, Icon, Menu } from 'semantic-ui-react';
|
||||
import { usePopup } from '../../../lib/popup';
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useSelector } from 'react-redux';
|
||||
import history from '../../../history';
|
||||
import selectors from '../../../selectors';
|
||||
import matchPaths from '../../../utils/match-paths';
|
||||
import Config from '../../../constants/Config';
|
||||
import Paths from '../../../constants/Paths';
|
||||
|
||||
const Link = React.memo(({ href, content, stopPropagation, ...props }) => {
|
||||
@@ -23,7 +24,8 @@ const Link = React.memo(({ href, content, stopPropagation, ...props }) => {
|
||||
}
|
||||
}, [href]);
|
||||
|
||||
const isSameSite = !!url && url.origin === window.location.origin;
|
||||
const isSameSite =
|
||||
!!url && url.origin === window.location.origin && url.pathname.startsWith(Config.BASE_PATH);
|
||||
|
||||
const cardsPathMatch = useMemo(() => {
|
||||
if (!isSameSite) {
|
||||
|
||||
@@ -8,7 +8,8 @@ import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import { Button, Divider, Form, Grid, Header, Message } from 'semantic-ui-react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { Button, Divider, Form, Grid, Header, Message, TextArea } from 'semantic-ui-react';
|
||||
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
|
||||
import { Input } from '../../../lib/custom-ui';
|
||||
|
||||
@@ -23,7 +24,7 @@ import logo from '../../../assets/images/logo.png';
|
||||
|
||||
import styles from './Content.module.scss';
|
||||
|
||||
const createMessage = (error) => {
|
||||
const createMessage = (error, isDebug) => {
|
||||
if (!error) {
|
||||
return error;
|
||||
}
|
||||
@@ -64,10 +65,10 @@ const createMessage = (error) => {
|
||||
type: 'error',
|
||||
content: 'common.usernameAlreadyInUse',
|
||||
};
|
||||
case 'Active user limit reached':
|
||||
case 'Active users limit reached':
|
||||
return {
|
||||
type: 'error',
|
||||
content: 'common.activeUserLimitReached',
|
||||
content: 'common.activeUsersLimitReached',
|
||||
};
|
||||
case 'Failed to fetch':
|
||||
return {
|
||||
@@ -82,7 +83,7 @@ const createMessage = (error) => {
|
||||
default:
|
||||
return {
|
||||
type: 'warning',
|
||||
content: 'common.unknownError',
|
||||
content: isDebug ? error.message : 'common.unknownError',
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -95,6 +96,7 @@ const Content = React.memo(() => {
|
||||
isSubmitting,
|
||||
isSubmittingWithOidc,
|
||||
error,
|
||||
debugLogs,
|
||||
step,
|
||||
} = useSelector(selectors.selectAuthenticateForm);
|
||||
|
||||
@@ -102,13 +104,33 @@ const Content = React.memo(() => {
|
||||
const [t] = useTranslation();
|
||||
const wasSubmitting = usePrevious(isSubmitting);
|
||||
|
||||
const [data, handleFieldChange, setData] = useForm(() => ({
|
||||
emailOrUsername: '',
|
||||
password: '',
|
||||
...defaultData,
|
||||
}));
|
||||
const [data, handleFieldChange, setData] = useForm(() => {
|
||||
const initialData = {
|
||||
emailOrUsername: '',
|
||||
password: '',
|
||||
...defaultData,
|
||||
};
|
||||
|
||||
const message = useMemo(() => createMessage(error), [error]);
|
||||
if (bootstrap.isDemoMode) {
|
||||
const params = new URLSearchParams(window.location.hash.slice(1));
|
||||
|
||||
Object.keys(initialData).forEach((fieldName) => {
|
||||
const value = params.get(fieldName);
|
||||
|
||||
if (value !== null) {
|
||||
initialData[fieldName] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return initialData;
|
||||
});
|
||||
|
||||
const withOidc = !!bootstrap.oidc;
|
||||
const isOidcEnforced = withOidc && bootstrap.oidc.isEnforced;
|
||||
const isOidcDebug = withOidc && bootstrap.oidc.debug;
|
||||
|
||||
const message = useMemo(() => createMessage(error, isOidcDebug), [error, isOidcDebug]);
|
||||
const [focusPasswordFieldState, focusPasswordField] = useToggle();
|
||||
|
||||
const [emailOrUsernameFieldRef, handleEmailOrUsernameFieldRef] = useNestedRef('inputRef');
|
||||
@@ -141,14 +163,11 @@ const Content = React.memo(() => {
|
||||
dispatch(entryActions.clearAuthenticateError());
|
||||
}, [dispatch]);
|
||||
|
||||
const withOidc = !!bootstrap.oidc;
|
||||
const isOidcEnforced = withOidc && bootstrap.oidc.isEnforced;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOidcEnforced) {
|
||||
emailOrUsernameFieldRef.current.focus();
|
||||
}
|
||||
}, [emailOrUsernameFieldRef, isOidcEnforced]);
|
||||
}, [isOidcEnforced, emailOrUsernameFieldRef]);
|
||||
|
||||
useDidUpdate(() => {
|
||||
if (wasSubmitting && !isSubmitting && error) {
|
||||
@@ -253,16 +272,27 @@ const Content = React.memo(() => {
|
||||
</>
|
||||
)}
|
||||
{withOidc && (
|
||||
<Button
|
||||
fluid
|
||||
primary={isOidcEnforced}
|
||||
icon={isOidcEnforced ? 'right arrow' : undefined}
|
||||
labelPosition={isOidcEnforced ? 'right' : undefined}
|
||||
content={t('action.logInWithSso')}
|
||||
loading={isSubmittingWithOidc}
|
||||
disabled={isSubmitting || isSubmittingWithOidc}
|
||||
onClick={handleAuthenticateWithOidcClick}
|
||||
/>
|
||||
<>
|
||||
<Button
|
||||
fluid
|
||||
primary={isOidcDebug ? undefined : isOidcEnforced}
|
||||
color={isOidcDebug ? 'orange' : undefined}
|
||||
icon={isOidcEnforced ? 'right arrow' : undefined}
|
||||
labelPosition={isOidcEnforced ? 'right' : undefined}
|
||||
content={isOidcDebug ? t('action.debugSso') : t('action.logInWithSso')}
|
||||
loading={isSubmittingWithOidc}
|
||||
disabled={isSubmitting || isSubmittingWithOidc}
|
||||
onClick={handleAuthenticateWithOidcClick}
|
||||
/>
|
||||
{debugLogs && (
|
||||
<TextArea
|
||||
readOnly
|
||||
as={TextareaAutosize}
|
||||
value={debugLogs.join('\n')}
|
||||
className={styles.debugLog}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.poweredBy}>
|
||||
|
||||
@@ -19,6 +19,16 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.debugLog {
|
||||
border: 1px solid rgba(9, 30, 66, 0.13);
|
||||
border-radius: 3px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
margin-top: 16px;
|
||||
padding: 8px 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.divider {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@@ -3,29 +3,65 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Checkbox, Dropdown, Modal } from 'semantic-ui-react';
|
||||
import { Button, Checkbox, Dropdown, Modal, Segment } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { localeByLanguage } from '../../../locales';
|
||||
import TERMS_LANGUAGES from '../../../constants/TermsLanguages';
|
||||
import Markdown from '../Markdown';
|
||||
|
||||
import styles from './TermsModal.module.scss';
|
||||
|
||||
const LOCALES = TERMS_LANGUAGES.map((language) => localeByLanguage[language]);
|
||||
const splitTermsAndConfirmations = (content) => {
|
||||
const separator = '\n[confirmations]::\n---\n';
|
||||
const index = content.lastIndexOf(separator);
|
||||
|
||||
if (index === -1) {
|
||||
return [content.trim(), []];
|
||||
}
|
||||
|
||||
const terms = content.slice(0, index).trim();
|
||||
|
||||
const confirmations = content
|
||||
.slice(index + separator.length)
|
||||
.split('\n')
|
||||
.map((confirmation) => confirmation.replace(/^✔️\s*/, '').replace(/\*\*(.*?)\*\*/, '$1'))
|
||||
.filter(Boolean);
|
||||
|
||||
return [terms, confirmations];
|
||||
};
|
||||
|
||||
const TermsModal = React.memo(() => {
|
||||
const { termsLanguages } = useSelector(selectors.selectBootstrap);
|
||||
|
||||
const {
|
||||
termsForm: { payload: terms, isSubmitting, isCancelling, isLanguageUpdating },
|
||||
} = useSelector(selectors.selectAuthenticateForm);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const [isTermsAccepted, setIsTermsAccepted] = useState(false);
|
||||
const [acceptedConfirmationsSet, setAcceptedConfirmationsSet] = useState(new Set());
|
||||
|
||||
const locales = useMemo(
|
||||
() =>
|
||||
termsLanguages.map(
|
||||
(language) =>
|
||||
localeByLanguage[language] || {
|
||||
language,
|
||||
country: language.split('-')[1]?.toLowerCase(),
|
||||
name: language,
|
||||
},
|
||||
),
|
||||
[termsLanguages],
|
||||
);
|
||||
|
||||
const [content, confirmations] = useMemo(
|
||||
() => splitTermsAndConfirmations(terms.content),
|
||||
[terms.content],
|
||||
);
|
||||
|
||||
const handleContinueClick = useCallback(() => {
|
||||
dispatch(entryActions.acceptTerms(terms.signature));
|
||||
@@ -42,17 +78,29 @@ const TermsModal = React.memo(() => {
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleToggleAcceptClick = useCallback((_, { checked }) => {
|
||||
setIsTermsAccepted(checked);
|
||||
const handleToggleConfirmationAccept = useCallback((index) => {
|
||||
setAcceptedConfirmationsSet((prevAcceptedConfirmationsSet) => {
|
||||
const nextAcceptedConfirmationsSet = new Set(prevAcceptedConfirmationsSet);
|
||||
|
||||
if (nextAcceptedConfirmationsSet.has(index)) {
|
||||
nextAcceptedConfirmationsSet.delete(index);
|
||||
} else {
|
||||
nextAcceptedConfirmationsSet.add(index);
|
||||
}
|
||||
|
||||
return nextAcceptedConfirmationsSet;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isAllConfirmationsAccepted = acceptedConfirmationsSet.size === confirmations.length;
|
||||
|
||||
return (
|
||||
<Modal open centered={false}>
|
||||
<Modal.Content>
|
||||
<Dropdown
|
||||
fluid
|
||||
selection
|
||||
options={LOCALES.map((locale) => ({
|
||||
options={locales.map((locale) => ({
|
||||
value: locale.language,
|
||||
flag: locale.country,
|
||||
text: locale.name,
|
||||
@@ -63,7 +111,20 @@ const TermsModal = React.memo(() => {
|
||||
className={styles.language}
|
||||
onChange={handleLanguageChange}
|
||||
/>
|
||||
<Markdown>{terms.content}</Markdown>
|
||||
<Markdown>{content}</Markdown>
|
||||
{confirmations.length > 0 && (
|
||||
<Segment size="massive" className={styles.confirmations}>
|
||||
{confirmations.map((confirmation, index) => (
|
||||
<Checkbox
|
||||
key={confirmation}
|
||||
checked={acceptedConfirmationsSet.has(index)}
|
||||
label={confirmation}
|
||||
className={styles.confirmationCheckbox}
|
||||
onChange={() => handleToggleConfirmationAccept(index)}
|
||||
/>
|
||||
))}
|
||||
</Segment>
|
||||
)}
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button
|
||||
@@ -74,15 +135,11 @@ const TermsModal = React.memo(() => {
|
||||
className={styles.cancelButton}
|
||||
onClick={handleCancelClick}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('common.iHaveReadAndAgreeToTheseTerms')}
|
||||
onChange={handleToggleAcceptClick}
|
||||
/>
|
||||
<Button
|
||||
positive
|
||||
content={t('action.continue')}
|
||||
loading={isSubmitting}
|
||||
disabled={!isTermsAccepted || isSubmitting || isCancelling}
|
||||
disabled={!isAllConfirmationsAccepted || isSubmitting || isCancelling}
|
||||
onClick={handleContinueClick}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
|
||||
@@ -8,6 +8,19 @@
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.confirmations {
|
||||
border-color: #5aac44;
|
||||
border-width: 2px;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 12px;
|
||||
}
|
||||
|
||||
.confirmationCheckbox {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.language {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
111
client/src/components/common/PromoBanner/PromoBanner.jsx
Normal file
111
client/src/components/common/PromoBanner/PromoBanner.jsx
Normal file
@@ -0,0 +1,111 @@
|
||||
/*!
|
||||
* Copyright (c) 2026 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
|
||||
import styles from './PromoBanner.module.scss';
|
||||
|
||||
const PRO_URL = 'https://planka.app/pro?ref=app-banner';
|
||||
const DISMISS_DURATION_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
const CYCLE_INTERVAL_MS = 8000;
|
||||
const CSS_VAR = '--promo-banner-height';
|
||||
|
||||
const FEATURES = ['proFeatureCalendar', 'proFeatureRecurringCards', 'proFeatureGuestRoles'];
|
||||
|
||||
// Alternates: main, sub1, main, sub2, main, sub3
|
||||
const TEXTS = [
|
||||
'discoverPlankaPro',
|
||||
FEATURES[0],
|
||||
'discoverPlankaPro',
|
||||
FEATURES[1],
|
||||
'discoverPlankaPro',
|
||||
FEATURES[2],
|
||||
];
|
||||
|
||||
function getDismissKey(userId) {
|
||||
return `planka_proBannerDismissed_${userId}`;
|
||||
}
|
||||
|
||||
function isBannerDismissed(userId) {
|
||||
const stored = localStorage.getItem(getDismissKey(userId));
|
||||
if (!stored) return false;
|
||||
return Date.now() - Date.parse(stored) < DISMISS_DURATION_MS;
|
||||
}
|
||||
|
||||
const PromoBanner = React.memo(() => {
|
||||
const userId = useSelector(selectors.selectCurrentUserId);
|
||||
|
||||
const [dismissed, setDismissed] = useState(() => isBannerDismissed(userId));
|
||||
const [textIndex, setTextIndex] = useState(0);
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
const wrapperRef = useRef(null);
|
||||
const [t] = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (dismissed) {
|
||||
document.documentElement.style.removeProperty(CSS_VAR);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const height = wrapperRef.current ? wrapperRef.current.offsetHeight : 0;
|
||||
document.documentElement.style.setProperty(CSS_VAR, `${height}px`);
|
||||
|
||||
return () => document.documentElement.style.removeProperty(CSS_VAR);
|
||||
}, [dismissed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dismissed) return undefined;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setVisible(false);
|
||||
setTimeout(() => {
|
||||
setTextIndex((i) => (i + 1) % TEXTS.length);
|
||||
setVisible(true);
|
||||
}, 400);
|
||||
}, CYCLE_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [dismissed]);
|
||||
|
||||
const handleDismiss = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
localStorage.setItem(getDismissKey(userId), new Date().toISOString());
|
||||
setDismissed(true);
|
||||
},
|
||||
[userId],
|
||||
);
|
||||
|
||||
if (dismissed) return null;
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} className={styles.wrapper}>
|
||||
<a
|
||||
href={PRO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`${styles.textLink} ${visible ? styles.textVisible : styles.textHidden}`}
|
||||
>
|
||||
{t(`common.${TEXTS[textIndex]}`)}
|
||||
<span className={styles.externalIcon}>↗</span>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
title={t('common.dismissProBannerFor30Days')}
|
||||
className={styles.closeButton}
|
||||
onClick={handleDismiss}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default PromoBanner;
|
||||
@@ -0,0 +1,60 @@
|
||||
/*!
|
||||
* Copyright (c) 2026 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.wrapper {
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
padding: 5px 16px;
|
||||
}
|
||||
|
||||
.textLink {
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
flex: 1 1 auto;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #bdff22;
|
||||
}
|
||||
}
|
||||
|
||||
.externalIcon {
|
||||
font-size: 11px;
|
||||
margin-left: 4px;
|
||||
opacity: 0.7;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
.textVisible {
|
||||
opacity: 1;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.textHidden {
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.closeButton:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { Route, Routes } from 'react-router';
|
||||
import { ThemeProvider, ToasterProvider } from '@gravity-ui/uikit';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { toaster } from '@gravity-ui/uikit/toaster-singleton';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Copyright (c) 2026 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
@@ -37,15 +37,15 @@
|
||||
|
||||
.wrapper {
|
||||
height: 100%;
|
||||
margin-top: 116px;
|
||||
margin-top: calc(116px + var(--promo-banner-height, 0px));
|
||||
}
|
||||
|
||||
.wrapperBoard {
|
||||
margin-top: 174px;
|
||||
margin-top: calc(174px + var(--promo-banner-height, 0px));
|
||||
}
|
||||
|
||||
.wrapperBoardWithFavorites {
|
||||
margin-top: 264px;
|
||||
margin-top: calc(264px + var(--promo-banner-height, 0px));
|
||||
}
|
||||
|
||||
.wrapperFlex {
|
||||
@@ -57,11 +57,11 @@
|
||||
}
|
||||
|
||||
.wrapperProject {
|
||||
margin-top: 98px;
|
||||
margin-top: calc(98px + var(--promo-banner-height, 0px));
|
||||
}
|
||||
|
||||
.wrapperProjectWithFavorites {
|
||||
margin-top: 188px;
|
||||
margin-top: calc(188px + var(--promo-banner-height, 0px));
|
||||
}
|
||||
|
||||
.wrapperTransitioning {
|
||||
@@ -73,6 +73,6 @@
|
||||
}
|
||||
|
||||
.wrapperWithFavorites {
|
||||
margin-top: 206px;
|
||||
margin-top: calc(206px + var(--promo-banner-height, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
margin-bottom: 6px;
|
||||
padding: 1px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -201,6 +201,7 @@ const List = React.memo(({ id, index }) => {
|
||||
className={classNames(
|
||||
styles.outerWrapper,
|
||||
isFavoritesActive && styles.outerWrapperWithFavorites,
|
||||
list.color && globalStyles[`background${upperFirst(camelCase(list.color))}Soft`],
|
||||
)}
|
||||
onTransitionEnd={handleWrapperTransitionEnd}
|
||||
>
|
||||
@@ -261,7 +262,11 @@ const List = React.memo(({ id, index }) => {
|
||||
<button
|
||||
type="button"
|
||||
disabled={!list.isPersisted}
|
||||
className={styles.addCardButton}
|
||||
className={classNames(
|
||||
styles.addCardButton,
|
||||
list.color &&
|
||||
globalStyles[`background${upperFirst(camelCase(list.color))}Soft`],
|
||||
)}
|
||||
onClick={handleAddCardClick}
|
||||
>
|
||||
<PlusMathIcon className={styles.addCardButtonIcon} />
|
||||
|
||||
@@ -8,7 +8,7 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link } from 'react-router';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
|
||||
@@ -9,7 +9,7 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link } from 'react-router';
|
||||
import { Button, Icon } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
|
||||
@@ -68,7 +68,11 @@ const AddImageZone = React.memo(({ children, onCreate }) => {
|
||||
return (
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
<div {...getRootProps()}>
|
||||
{isDragActive && <div className={styles.dropzone}>{t('common.dropFileToUpload')}</div>}
|
||||
{isDragActive && (
|
||||
<div className={styles.dropzone}>
|
||||
<div className={styles.dropzoneText}>{t('common.dropFileToUpload')}</div>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<input {...getInputProps()} />
|
||||
|
||||
@@ -8,13 +8,18 @@
|
||||
background: white;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
height: 100%;
|
||||
inset: 0;
|
||||
line-height: 30px;
|
||||
opacity: 0.7;
|
||||
padding-top: 200px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
z-index: 2001;
|
||||
}
|
||||
|
||||
.dropzoneText {
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: min(200px, 50%);
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link } from 'react-router';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { Button, Checkbox, Icon } from 'semantic-ui-react';
|
||||
import { useDidUpdate } from '../../../../lib/hooks';
|
||||
|
||||
@@ -3,20 +3,22 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
import { FilePicker, Popup } from '../../../../lib/custom-ui';
|
||||
import { FilePicker, Popup } from '../../../lib/custom-ui';
|
||||
|
||||
import selectors from '../../../../selectors';
|
||||
import entryActions from '../../../../entry-actions';
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
|
||||
import styles from './EditAvatarStep.module.scss';
|
||||
import styles from './EditUserAvatarStep.module.scss';
|
||||
|
||||
const EditAvatarStep = React.memo(({ onClose }) => {
|
||||
const defaultValue = useSelector((state) => selectors.selectCurrentUser(state).avatar);
|
||||
const EditUserAvatarStep = React.memo(({ id, onBack, onClose }) => {
|
||||
const selectUserById = useMemo(() => selectors.makeSelectUserById(), []);
|
||||
|
||||
const avatar = useSelector((state) => selectUserById(state, id).avatar);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
@@ -26,33 +28,33 @@ const EditAvatarStep = React.memo(({ onClose }) => {
|
||||
const handleFileSelect = useCallback(
|
||||
(file) => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentUserAvatar({
|
||||
entryActions.updateUserAvatar(id, {
|
||||
file,
|
||||
}),
|
||||
);
|
||||
|
||||
onClose();
|
||||
},
|
||||
[onClose, dispatch],
|
||||
[dispatch, id, onClose],
|
||||
);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentUser({
|
||||
entryActions.updateUser(id, {
|
||||
avatar: null,
|
||||
}),
|
||||
);
|
||||
|
||||
onClose();
|
||||
}, [onClose, dispatch]);
|
||||
}, [dispatch, id, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
fieldRef.current.focus();
|
||||
fieldRef.current?.focus?.();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t('common.editAvatar', {
|
||||
context: 'title',
|
||||
})}
|
||||
@@ -67,7 +69,7 @@ const EditAvatarStep = React.memo(({ onClose }) => {
|
||||
/>
|
||||
</FilePicker>
|
||||
</div>
|
||||
{defaultValue && (
|
||||
{avatar && (
|
||||
<Button negative content={t('action.deleteAvatar')} onClick={handleDeleteClick} />
|
||||
)}
|
||||
</Popup.Content>
|
||||
@@ -75,8 +77,14 @@ const EditAvatarStep = React.memo(({ onClose }) => {
|
||||
);
|
||||
});
|
||||
|
||||
EditAvatarStep.propTypes = {
|
||||
EditUserAvatarStep.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
onBack: PropTypes.func,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default EditAvatarStep;
|
||||
EditUserAvatarStep.defaultProps = {
|
||||
onBack: undefined,
|
||||
};
|
||||
|
||||
export default EditUserAvatarStep;
|
||||
@@ -3,6 +3,6 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import AccountPane from './AccountPane';
|
||||
import EditUserAvatarStep from './EditUserAvatarStep';
|
||||
|
||||
export default AccountPane;
|
||||
export default EditUserAvatarStep;
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Copyright (c) 2026 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
@@ -104,6 +104,17 @@ const UserActionsStep = React.memo(({ onClose }) => {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
href="https://planka.app/pro?ref=app-menu"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.proMenuItem}
|
||||
>
|
||||
<Icon name="gem" className={styles.proMenuItemIcon} />
|
||||
{withAdministration
|
||||
? t('common.upgradeTeamToPro', { context: 'title' })
|
||||
: t('common.discoverPlankaPro', { context: 'title' })}
|
||||
</Menu.Item>
|
||||
<hr className={styles.divider} />
|
||||
<Menu.Item
|
||||
{...logoutMenuItemProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Copyright (c) 2026 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
@@ -25,4 +25,14 @@
|
||||
float: left;
|
||||
margin: 0 0.5em 0 0;
|
||||
}
|
||||
|
||||
.proMenuItem {
|
||||
margin: 0;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.proMenuItemIcon {
|
||||
float: left;
|
||||
margin: 0 0.5em 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import initials from 'initials';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
@@ -35,6 +34,19 @@ const COLORS = [
|
||||
'midnight-blue',
|
||||
];
|
||||
|
||||
const getInitials = (name) => {
|
||||
const words = name
|
||||
.trim()
|
||||
.split(/[\s-]+/)
|
||||
.filter(Boolean);
|
||||
if (words.length === 0) return '';
|
||||
if (words.length === 1) return [...words[0]].slice(0, 2).join('');
|
||||
return words
|
||||
.slice(0, 2)
|
||||
.map((word) => [...word][0])
|
||||
.join('');
|
||||
};
|
||||
|
||||
const getColor = (name) => {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < name.length; i += 1) {
|
||||
@@ -78,7 +90,7 @@ const UserAvatar = React.memo(
|
||||
background: avatarUrl && `url("${avatarUrl}") center / cover`,
|
||||
}}
|
||||
>
|
||||
{!avatarUrl && <span className={styles.initials}>{initials(user.name).slice(0, 2)}</span>}
|
||||
{!avatarUrl && <span className={styles.initials}>{getInitials(user.name)}</span>}
|
||||
{withCreatorIndicator && <span className={styles.creatorIndicator}>+</span>}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -8,16 +8,16 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Divider, Dropdown, Header, Tab } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../../selectors';
|
||||
import entryActions from '../../../../entry-actions';
|
||||
import { usePopupInClosableContext } from '../../../../hooks';
|
||||
import locales from '../../../../locales';
|
||||
import EditAvatarStep from './EditAvatarStep';
|
||||
import EditUserInformation from '../../EditUserInformation';
|
||||
import EditUserUsernameStep from '../../EditUserUsernameStep';
|
||||
import EditUserEmailStep from '../../EditUserEmailStep';
|
||||
import EditUserPasswordStep from '../../EditUserPasswordStep';
|
||||
import UserAvatar from '../../UserAvatar';
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { usePopupInClosableContext } from '../../../hooks';
|
||||
import locales from '../../../locales';
|
||||
import EditUserInformation from '../EditUserInformation';
|
||||
import EditUserAvatarStep from '../EditUserAvatarStep';
|
||||
import EditUserUsernameStep from '../EditUserUsernameStep';
|
||||
import EditUserEmailStep from '../EditUserEmailStep';
|
||||
import EditUserPasswordStep from '../EditUserPasswordStep';
|
||||
import UserAvatar from '../UserAvatar';
|
||||
|
||||
import styles from './AccountPane.module.scss';
|
||||
|
||||
@@ -34,7 +34,7 @@ const AccountPane = React.memo(() => {
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const EditAvatarPopup = usePopupInClosableContext(EditAvatarStep);
|
||||
const EditUserAvatarPopup = usePopupInClosableContext(EditUserAvatarStep);
|
||||
const EditUserUsernamePopup = usePopupInClosableContext(EditUserUsernameStep);
|
||||
const EditUserEmailPopup = usePopupInClosableContext(EditUserEmailStep);
|
||||
const EditUserPasswordPopup = usePopupInClosableContext(EditUserPasswordStep);
|
||||
@@ -45,9 +45,9 @@ const AccountPane = React.memo(() => {
|
||||
|
||||
return (
|
||||
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||
<EditAvatarPopup>
|
||||
<EditUserAvatarPopup id={user.id}>
|
||||
<UserAvatar id={user.id} size="massive" isDisabled={user.isAvatarUpdating} />
|
||||
</EditAvatarPopup>
|
||||
</EditUserAvatarPopup>
|
||||
<br />
|
||||
<br />
|
||||
<EditUserInformation id={user.id} />
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import history from '../../history';
|
||||
import Config from '../../constants/Config';
|
||||
|
||||
const SAME_SITE_CLASS = 'same-site';
|
||||
|
||||
@@ -30,7 +31,9 @@ function process(token, nextToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSameSite = url.origin === window.location.origin;
|
||||
const isSameSite =
|
||||
url.origin === window.location.origin && url.pathname.startsWith(Config.BASE_PATH);
|
||||
|
||||
const trimOrigin = isSameSite && nextToken.type === 'text' && nextToken.content === href;
|
||||
|
||||
if (isSameSite) {
|
||||
|
||||
@@ -16,6 +16,10 @@ export default {
|
||||
SOCKET_RECONNECT_HANDLE: 'SOCKET_RECONNECT_HANDLE',
|
||||
SOCKET_RECONNECT_HANDLE__CORE_FETCH: 'SOCKET_RECONNECT_HANDLE__CORE_FETCH',
|
||||
|
||||
/* Bootstrap */
|
||||
|
||||
BOOTSTRAP_UPDATE_HANDLE: 'BOOTSTRAP_UPDATE_HANDLE',
|
||||
|
||||
/* Login */
|
||||
|
||||
LOGIN_INITIALIZE: 'LOGIN_INITIALIZE',
|
||||
@@ -25,6 +29,7 @@ export default {
|
||||
WITH_OIDC_AUTHENTICATE: 'WITH_OIDC_AUTHENTICATE',
|
||||
WITH_OIDC_AUTHENTICATE__SUCCESS: 'WITH_OIDC_AUTHENTICATE__SUCCESS',
|
||||
WITH_OIDC_AUTHENTICATE__FAILURE: 'WITH_OIDC_AUTHENTICATE__FAILURE',
|
||||
WITH_OIDC_AUTHENTICATE__DEBUG: 'WITH_OIDC_AUTHENTICATE__DEBUG',
|
||||
AUTHENTICATE_ERROR_CLEAR: 'AUTHENTICATE_ERROR_CLEAR',
|
||||
TERMS_ACCEPT: 'TERMS_ACCEPT',
|
||||
TERMS_ACCEPT__SUCCESS: 'TERMS_ACCEPT__SUCCESS',
|
||||
@@ -78,6 +83,7 @@ export default {
|
||||
|
||||
/* Users */
|
||||
|
||||
USERS_RESET_HANDLE: 'USERS_RESET_HANDLE',
|
||||
USER_CREATE: 'USER_CREATE',
|
||||
USER_CREATE__SUCCESS: 'USER_CREATE__SUCCESS',
|
||||
USER_CREATE__FAILURE: 'USER_CREATE__FAILURE',
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const SERVER_BASE_URL =
|
||||
import.meta.env.VITE_SERVER_BASE_URL || (import.meta.env.DEV ? 'http://localhost:1337' : '');
|
||||
const BASE_PATH = window.BASE_PATH || '';
|
||||
|
||||
const ACCESS_TOKEN_KEY = 'accessToken';
|
||||
const ACCESS_TOKEN_VERSION_KEY = 'accessTokenVersion';
|
||||
@@ -20,7 +19,7 @@ const MAX_SIZE_TO_DISPLAY_CONTENT = 256 * 1024;
|
||||
const IS_MAC = navigator.platform.startsWith('Mac');
|
||||
|
||||
export default {
|
||||
SERVER_BASE_URL,
|
||||
BASE_PATH,
|
||||
ACCESS_TOKEN_KEY,
|
||||
ACCESS_TOKEN_VERSION_KEY,
|
||||
ACCESS_TOKEN_VERSION,
|
||||
|
||||
@@ -13,6 +13,10 @@ export default {
|
||||
SOCKET_DISCONNECT_HANDLE: `${PREFIX}/SOCKET_DISCONNECT_HANDLE`,
|
||||
SOCKET_RECONNECT_HANDLE: `${PREFIX}/SOCKET_RECONNECT_HANDLE`,
|
||||
|
||||
/* Bootstrap */
|
||||
|
||||
BOOTSTRAP_UPDATE_HANDLE: `${PREFIX}/BOOTSTRAP_UPDATE_HANDLE`,
|
||||
|
||||
/* Login */
|
||||
|
||||
AUTHENTICATE: `${PREFIX}/AUTHENTICATE`,
|
||||
@@ -51,6 +55,7 @@ export default {
|
||||
|
||||
/* Users */
|
||||
|
||||
USERS_RESET_HANDLE: `${PREFIX}/USERS_RESET_HANDLE`,
|
||||
USER_CREATE: `${PREFIX}/USER_CREATE`,
|
||||
USER_CREATE_HANDLE: `${PREFIX}/USER_CREATE_HANDLE`,
|
||||
USER_CREATE_ERROR_CLEAR: `${PREFIX}/USER_CREATE_ERROR_CLEAR`,
|
||||
@@ -70,6 +75,7 @@ export default {
|
||||
CURRENT_USER_USERNAME_UPDATE: `${PREFIX}/CURRENT_USER_USERNAME_UPDATE`,
|
||||
USER_USERNAME_UPDATE_ERROR_CLEAR: `${PREFIX}/USER_USERNAME_UPDATE_ERROR_CLEAR`,
|
||||
CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR: `${PREFIX}/CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR`,
|
||||
USER_AVATAR_UPDATE: `${PREFIX}/USER_AVATAR_UPDATE`,
|
||||
CURRENT_USER_AVATAR_UPDATE: `${PREFIX}/CURRENT_USER_AVATAR_UPDATE`,
|
||||
USER_API_KEY_CREATE: `${PREFIX}/USER_API_KEY_CREATE`,
|
||||
USER_API_KEY_DELETE: `${PREFIX}/USER_API_KEY_DELETE`,
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const ROOT = '/';
|
||||
const LOGIN = '/login';
|
||||
const OIDC_CALLBACK = '/oidc-callback';
|
||||
const PROJECTS = '/projects/:id';
|
||||
const BOARDS = '/boards/:id';
|
||||
const CARDS = '/cards/:id';
|
||||
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`;
|
||||
|
||||
export default {
|
||||
ROOT,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
export default ['de-DE', 'en-US'];
|
||||
17
client/src/entry-actions/bootstrap.js
vendored
Normal file
17
client/src/entry-actions/bootstrap.js
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import EntryActionTypes from '../constants/EntryActionTypes';
|
||||
|
||||
const handleBootstrapUpdate = (bootstrap) => ({
|
||||
type: EntryActionTypes.BOOTSTRAP_UPDATE_HANDLE,
|
||||
payload: {
|
||||
bootstrap,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
handleBootstrapUpdate,
|
||||
};
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import socket from './socket';
|
||||
import bootstrap from './bootstrap';
|
||||
import login from './login';
|
||||
import core from './core';
|
||||
import modals from './modals';
|
||||
@@ -32,6 +33,7 @@ import notificationServices from './notification-services';
|
||||
|
||||
export default {
|
||||
...socket,
|
||||
...bootstrap,
|
||||
...login,
|
||||
...core,
|
||||
...modals,
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
|
||||
import EntryActionTypes from '../constants/EntryActionTypes';
|
||||
|
||||
const handleUsersReset = () => ({
|
||||
type: EntryActionTypes.USERS_RESET_HANDLE,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
const createUser = (data) => ({
|
||||
type: EntryActionTypes.USER_CREATE,
|
||||
payload: {
|
||||
@@ -134,6 +139,14 @@ const clearCurrentUserUsernameUpdateError = () => ({
|
||||
payload: {},
|
||||
});
|
||||
|
||||
const updateUserAvatar = (id, data) => ({
|
||||
type: EntryActionTypes.USER_AVATAR_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
const updateCurrentUserAvatar = (data) => ({
|
||||
type: EntryActionTypes.CURRENT_USER_AVATAR_UPDATE,
|
||||
payload: {
|
||||
@@ -246,6 +259,7 @@ const removeUserFromFilterInCurrentBoard = (id) => ({
|
||||
});
|
||||
|
||||
export default {
|
||||
handleUsersReset,
|
||||
createUser,
|
||||
handleUserCreate,
|
||||
clearUserCreateError,
|
||||
@@ -265,6 +279,7 @@ export default {
|
||||
updateCurrentUserUsername,
|
||||
clearUserUsernameUpdateError,
|
||||
clearCurrentUserUsernameUpdateError,
|
||||
updateUserAvatar,
|
||||
updateCurrentUserAvatar,
|
||||
createUserApiKey,
|
||||
deleteUserApiKey,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import React, { useLayoutEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { Router } from 'react-router';
|
||||
|
||||
import { handleLocationChange } from './actions';
|
||||
|
||||
|
||||
@@ -83,6 +83,8 @@ export default {
|
||||
'هل أنت متأكد أنك تريد إزالة هذا المدير من المشروع؟',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'هل أنت متأكد أنك تريد إزالة هذا العضو من اللوحة؟',
|
||||
areYouSureYouWantToUnlinkSsoFromThisUser:
|
||||
'هل أنت متأكد أنك تريد إلغاء ربط تسجيل الدخول الموحد من هذا المستخدم؟ سيسمح هذا للمستخدم بتسجيل الدخول باستخدام كلمة المرور.',
|
||||
assignAsOwner_title: 'تعيين كمالك',
|
||||
atLeastOneListMustBePresent: 'يجب وجود قائمة واحدة على الأقل',
|
||||
attachment: 'مرفق',
|
||||
@@ -136,6 +138,13 @@ export default {
|
||||
customFieldGroups_title: 'مجموعات الحقول المخصصة',
|
||||
customField_title: 'الحقل المخصص',
|
||||
customFields_title: 'الحقول المخصصة',
|
||||
discoverPlankaPro: '✨ المزيد من الميزات لألواحك: اكتشف PLANKA Pro',
|
||||
discoverPlankaPro_title: 'اكتشف PLANKA Pro',
|
||||
upgradeTeamToPro_title: 'ترقية الفريق إلى Pro',
|
||||
proFeatureCalendar: '✨ عرض التقويم للوحاتك',
|
||||
proFeatureRecurringCards: '✨ البطاقات المتكررة والأتمتة',
|
||||
proFeatureGuestRoles: '✨ أدوار الضيف والتعاون الخارجي',
|
||||
dismissProBannerFor30Days: 'إغلاق لمدة 30 يومًا',
|
||||
customerPanel_title: 'لوحة العملاء',
|
||||
dangerZone_title: 'منطقة الخطر',
|
||||
date: 'تاريخ',
|
||||
@@ -164,6 +173,7 @@ export default {
|
||||
deletedUser_title: 'مستخدم محذوف',
|
||||
description: 'الوصف',
|
||||
display: 'عرض',
|
||||
displayCardAges: 'عرض أعمار البطاقات',
|
||||
dropFileToUpload: 'أفلت الملف لرفعه',
|
||||
dueDate_title: 'تاريخ الاستحقاق',
|
||||
dynamicAndUnevenlySpacedLayout: 'تخطيط ديناميكي وغير متساوي المسافات.',
|
||||
@@ -320,6 +330,7 @@ export default {
|
||||
turnOffRecentCardHighlighting: 'إيقاف تمييز البطاقات الحديثة',
|
||||
typeNameToConfirm: 'اكتب الاسم للتأكيد.',
|
||||
typeTitleToConfirm: 'اكتب العنوان للتأكيد.',
|
||||
unlinkSso_title: 'إلغاء ربط تسجيل الدخول الموحد',
|
||||
unsavedChanges: 'تغييرات غير محفوظة',
|
||||
uploadFailedFileIsTooBig: 'فشل الرفع: الملف كبير جداً.',
|
||||
uploadFailedNotEnoughStorageSpace: 'فشل الرفع: مساحة التخزين غير كافية.',
|
||||
@@ -432,6 +443,7 @@ export default {
|
||||
download: 'تحميل',
|
||||
duplicateCard_title: 'تكرار البطاقة',
|
||||
edit: 'تعديل',
|
||||
editAvatar_title: 'تعديل الصورة الرمزية',
|
||||
editColor_title: 'تعديل اللون',
|
||||
editDescription_title: 'تعديل الوصف',
|
||||
editDueDate_title: 'تعديل تاريخ الاستحقاق',
|
||||
@@ -485,6 +497,8 @@ export default {
|
||||
start: 'ابدأ',
|
||||
stop: 'توقف',
|
||||
subscribe: 'اشترك',
|
||||
unlinkSso: 'إلغاء ربط تسجيل الدخول الموحد',
|
||||
unlinkSso_title: 'إلغاء ربط تسجيل الدخول الموحد',
|
||||
unsubscribe: 'إلغاء الاشتراك',
|
||||
uploadNewAvatar: 'رفع صورة رمزية جديدة',
|
||||
uploadNewImage: 'رفع صورة جديدة',
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
export default {
|
||||
translation: {
|
||||
common: {
|
||||
activeUserLimitReached: 'تم الوصول إلى حد المستخدمين النشطين',
|
||||
activeUsersLimitReached: 'تم الوصول إلى حد المستخدمين النشطين',
|
||||
adminLoginRequiredToInitializeInstance: 'مطلوب تسجيل دخول المدير لتهيئة المثيل',
|
||||
emailAlreadyInUse: 'البريد الإلكتروني مستخدم بالفعل',
|
||||
emailOrUsername: 'البريد الإلكتروني أو اسم المستخدم',
|
||||
iHaveReadAndAgreeToTheseTerms: 'لقد قرأت ووافقت على هذه الشروط',
|
||||
invalidCredentials: 'بيانات الاعتماد غير صالحة',
|
||||
invalidEmailOrUsername: 'البريد الإلكتروني أو اسم المستخدم غير صالح',
|
||||
invalidPassword: 'كلمة المرور غير صالحة',
|
||||
@@ -25,6 +24,7 @@ export default {
|
||||
action: {
|
||||
cancelAndClose: 'إلغاء وإغلاق',
|
||||
continue: 'متابعة',
|
||||
debugSso: 'تصحيح أخطاء تسجيل الدخول الموحد',
|
||||
goBack: 'العودة',
|
||||
goHome: 'الذهاب للرئيسية',
|
||||
logIn: 'تسجيل الدخول',
|
||||
|
||||
@@ -92,6 +92,8 @@ export default {
|
||||
'Сигурни ли сте, че искате да премахнете този мениджър от проекта?',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'Сигурни ли сте, че искате да премахнете този член от таблото?',
|
||||
areYouSureYouWantToUnlinkSsoFromThisUser:
|
||||
'Сигурни ли сте, че искате да премахнете SSO връзката от този потребител? Това ще позволи на потребителя да влиза с парола.',
|
||||
assignAsOwner_title: 'Назначаване като собственик',
|
||||
atLeastOneListMustBePresent: 'Трябва да присъства поне един списък',
|
||||
attachment: 'Прикачен файл',
|
||||
@@ -147,6 +149,13 @@ export default {
|
||||
customFieldGroups_title: 'Групи персонализирани полета',
|
||||
customField_title: 'Персонализирано поле',
|
||||
customFields_title: 'Персонализирани полета',
|
||||
discoverPlankaPro: '✨ Повече функции за вашите дъски: Открийте PLANKA Pro',
|
||||
discoverPlankaPro_title: 'Открийте PLANKA Pro',
|
||||
upgradeTeamToPro_title: 'Надградете екипа до Pro',
|
||||
proFeatureCalendar: '✨ Изглед Календар за вашите дъски',
|
||||
proFeatureRecurringCards: '✨ Повтарящи се карти и автоматизация',
|
||||
proFeatureGuestRoles: '✨ Роли за гости и външно сътрудничество',
|
||||
dismissProBannerFor30Days: 'Скрий за 30 дни',
|
||||
customerPanel_title: 'Панел на клиента',
|
||||
dangerZone_title: 'Опасна зона',
|
||||
date: 'Дата',
|
||||
@@ -176,6 +185,7 @@ export default {
|
||||
deletedUser_title: 'Изтрит потребител',
|
||||
description: 'Описание',
|
||||
display: 'Показване',
|
||||
displayCardAges: 'Показвай възрастта на картите',
|
||||
dropFileToUpload: 'Пуснете файл за качване',
|
||||
dueDate_title: 'Краен срок',
|
||||
dynamicAndUnevenlySpacedLayout: 'Динамично и неравномерно разположение.',
|
||||
@@ -333,6 +343,7 @@ export default {
|
||||
turnOffRecentCardHighlighting: 'Изключване на подчертаването на скорошни карти',
|
||||
typeNameToConfirm: 'Въведете име за потвърждение.',
|
||||
typeTitleToConfirm: 'Въведете заглавие за потвърждение.',
|
||||
unlinkSso_title: 'Премахване на SSO връзка',
|
||||
unsavedChanges: 'Незапазени промени',
|
||||
uploadFailedFileIsTooBig: 'Качването неуспешно: файлът е твърде голям.',
|
||||
uploadFailedNotEnoughStorageSpace:
|
||||
@@ -447,6 +458,7 @@ export default {
|
||||
download: 'Изтегляне',
|
||||
duplicateCard_title: 'Дублирана карта',
|
||||
edit: 'Редактиране',
|
||||
editAvatar_title: 'Редактиране на аватар',
|
||||
editColor_title: 'Редактиране на цвят',
|
||||
editDescription_title: 'Редактиране на описание',
|
||||
editDueDate_title: 'Редактиране на краен срок',
|
||||
@@ -500,6 +512,8 @@ export default {
|
||||
start: 'Старт',
|
||||
stop: 'Стоп',
|
||||
subscribe: 'Абонирайте се',
|
||||
unlinkSso: 'Премахни SSO връзка',
|
||||
unlinkSso_title: 'Премахни SSO връзка',
|
||||
unsubscribe: 'Отписване',
|
||||
uploadNewAvatar: 'Качване на нов аватар',
|
||||
uploadNewImage: 'Качване на ново изображение',
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
export default {
|
||||
translation: {
|
||||
common: {
|
||||
activeUserLimitReached: 'Достигнат е лимитът на активни потребители',
|
||||
activeUsersLimitReached: 'Достигнат е лимитът на активни потребители',
|
||||
adminLoginRequiredToInitializeInstance:
|
||||
'Необходимо е влизане на администратор за инициализиране на инстанцията',
|
||||
emailAlreadyInUse: 'Имейлът вече се използва',
|
||||
emailOrUsername: 'Имейл или потребителско име',
|
||||
iHaveReadAndAgreeToTheseTerms: 'Прочетох и се съгласявам с тези условия',
|
||||
invalidCredentials: 'Невалидни данни за вход',
|
||||
invalidEmailOrUsername: 'Невалиден имейл или потребителско име',
|
||||
invalidPassword: 'Невалидна парола',
|
||||
@@ -26,6 +25,7 @@ export default {
|
||||
action: {
|
||||
cancelAndClose: 'Отказ и затваряне',
|
||||
continue: 'Продължи',
|
||||
debugSso: 'Дебъгване на SSO',
|
||||
goBack: 'Назад',
|
||||
goHome: 'Към началото',
|
||||
logIn: 'Вход',
|
||||
|
||||
@@ -90,6 +90,8 @@ export default {
|
||||
'Estàs segur que vols eliminar aquest gestor del projecte?',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'Estàs segur que vols eliminar aquest membre del tauler?',
|
||||
areYouSureYouWantToUnlinkSsoFromThisUser:
|
||||
"Estàs segur que vols desvincular SSO d'aquest usuari? Això permetrà que l'usuari iniciï sessió amb una contrasenya.",
|
||||
assignAsOwner_title: 'Assignar com a propietari',
|
||||
atLeastOneListMustBePresent: "Hi ha d'haver com a mínim una llista",
|
||||
attachment: 'Fitxer adjunt',
|
||||
@@ -146,6 +148,13 @@ export default {
|
||||
customFieldGroups_title: 'Grups de camps personalitzats',
|
||||
customField_title: 'Camp personalitzat',
|
||||
customFields_title: 'Camps personalitzats',
|
||||
discoverPlankaPro: '✨ Més funcions per als vostres taulers: Descobriu PLANKA Pro',
|
||||
discoverPlankaPro_title: 'Descobriu PLANKA Pro',
|
||||
upgradeTeamToPro_title: "Actualitzeu l'equip a Pro",
|
||||
proFeatureCalendar: '✨ Vista de calendari per als vostres taulers',
|
||||
proFeatureRecurringCards: '✨ Targetes recurrents i automatització',
|
||||
proFeatureGuestRoles: '✨ Rols de convidat i col·laboració externa',
|
||||
dismissProBannerFor30Days: 'Tanca durant 30 dies',
|
||||
customerPanel_title: 'Panell del client',
|
||||
dangerZone_title: 'Zona de perill',
|
||||
date: 'Data',
|
||||
@@ -175,6 +184,7 @@ export default {
|
||||
deletedUser_title: 'Usuari eliminat',
|
||||
description: 'Descripció',
|
||||
display: 'Mostrar',
|
||||
displayCardAges: "Mostrar l'antiguitat de les targetes",
|
||||
dropFileToUpload: 'Arrossega fitxer per pujar-lo',
|
||||
dueDate_title: 'Data de venciment',
|
||||
dynamicAndUnevenlySpacedLayout: 'Disseny dinàmic i amb espaiat irregular.',
|
||||
@@ -334,6 +344,7 @@ export default {
|
||||
turnOffRecentCardHighlighting: 'Desactivar ressaltat de targetes recents',
|
||||
typeNameToConfirm: 'Escriu el nom per confirmar.',
|
||||
typeTitleToConfirm: 'Escriu el títol per confirmar.',
|
||||
unlinkSso_title: 'Desvinculació de SSO',
|
||||
unsavedChanges: 'Canvis sense desar',
|
||||
uploadFailedFileIsTooBig: 'Error en pujar: El fitxer és massa gran.',
|
||||
uploadFailedNotEnoughStorageSpace: "Error en pujar: No hi ha prou espai d'emmagatzematge.",
|
||||
@@ -449,6 +460,7 @@ export default {
|
||||
download: 'Descarregar',
|
||||
duplicateCard_title: 'Duplicar targeta',
|
||||
edit: 'Editar',
|
||||
editAvatar_title: 'Editar avatar',
|
||||
editColor_title: 'Editar color',
|
||||
editDescription_title: 'Editar descripció',
|
||||
editDueDate_title: 'Editar data de venciment',
|
||||
@@ -502,6 +514,8 @@ export default {
|
||||
start: 'Iniciar',
|
||||
stop: 'Aturar',
|
||||
subscribe: "Subscriure's",
|
||||
unlinkSso: 'Desvincular SSO',
|
||||
unlinkSso_title: 'Desvincular SSO',
|
||||
unsubscribe: 'Cancel·lar subscripció',
|
||||
uploadNewAvatar: 'Pujar nou avatar',
|
||||
uploadNewImage: 'Pujar nova imatge',
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
export default {
|
||||
translation: {
|
||||
common: {
|
||||
activeUserLimitReached: "S'ha assolit el límit d'usuaris actius",
|
||||
activeUsersLimitReached: "S'ha assolit el límit d'usuaris actius",
|
||||
adminLoginRequiredToInitializeInstance:
|
||||
"Es requereix inici de sessió d'administrador per inicialitzar la instància",
|
||||
emailAlreadyInUse: 'Correu electrònic ja en ús',
|
||||
emailOrUsername: "Correu electrònic o nom d'usuari",
|
||||
iHaveReadAndAgreeToTheseTerms: 'He llegit i accepto aquests termes',
|
||||
invalidCredentials: 'Credencials no vàlides',
|
||||
invalidEmailOrUsername: "Correu electrònic o nom d'usuari no vàlid",
|
||||
invalidPassword: 'Contrasenya no vàlida',
|
||||
@@ -26,6 +25,7 @@ export default {
|
||||
action: {
|
||||
cancelAndClose: 'Cancel·lar i tancar',
|
||||
continue: 'Continuar',
|
||||
debugSso: 'Depurar SSO',
|
||||
goBack: 'Tornar',
|
||||
goHome: "Anar a l'inici",
|
||||
logIn: 'Iniciar sessió',
|
||||
|
||||
@@ -85,6 +85,8 @@ export default {
|
||||
'Opravdu chcete tohoto správce z projektu odebrat?',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'Opravdu chcete tohoto člena odebrat z nástěnky?',
|
||||
areYouSureYouWantToUnlinkSsoFromThisUser:
|
||||
'Opravdu chcete odpojit SSO od tohoto uživatele? Tím umožníte uživateli přihlásit se pomocí hesla.',
|
||||
assignAsOwner_title: 'Přiřadit jako vlastníka',
|
||||
atLeastOneListMustBePresent: 'Musí být k dispozici alespoň jeden seznam',
|
||||
attachment: 'Příloha',
|
||||
@@ -138,6 +140,13 @@ export default {
|
||||
customFieldGroups_title: 'Skupina vlastních polí',
|
||||
customField_title: 'Vlastní pole',
|
||||
customFields_title: 'Vlastní pole',
|
||||
discoverPlankaPro: '✨ Více funkcí pro vaše tabule: Objevte PLANKA Pro',
|
||||
discoverPlankaPro_title: 'Objevte PLANKA Pro',
|
||||
upgradeTeamToPro_title: 'Upgradovat tým na Pro',
|
||||
proFeatureCalendar: '✨ Zobrazení kalendáře pro vaše tabule',
|
||||
proFeatureRecurringCards: '✨ Opakující se karty a automatizace',
|
||||
proFeatureGuestRoles: '✨ Role hosta a externí spolupráce',
|
||||
dismissProBannerFor30Days: 'Zavřít na 30 dní',
|
||||
customerPanel_title: 'Panel zákazníka',
|
||||
dangerZone_title: 'Nebezpečná zóna',
|
||||
date: 'Datum',
|
||||
@@ -167,6 +176,7 @@ export default {
|
||||
deletedUser_title: 'Smazaný uživatel',
|
||||
description: 'Popis',
|
||||
display: 'Zobrazit',
|
||||
displayCardAges: 'Zobrazit stáří karet',
|
||||
dropFileToUpload: 'Přetažením nahrát soubor',
|
||||
dueDate_title: 'Termín',
|
||||
dynamicAndUnevenlySpacedLayout: 'Dynamické a nerovnoměrné rozložení.',
|
||||
@@ -324,6 +334,7 @@ export default {
|
||||
turnOffRecentCardHighlighting: 'Vypnout zvýraznění posledních karet',
|
||||
typeNameToConfirm: 'Zadejte název pro potvrzení.',
|
||||
typeTitleToConfirm: 'Zadejte titulek pro potvrzení.',
|
||||
unlinkSso_title: 'Odpojení SSO',
|
||||
unsavedChanges: 'Neuložené změny',
|
||||
uploadFailedFileIsTooBig: 'Nahrávání se nezdařilo: Soubor je příliš velký.',
|
||||
uploadFailedNotEnoughStorageSpace: 'Nahrávání se nezdařilo: Nedostatek úložného prostoru.',
|
||||
@@ -437,6 +448,7 @@ export default {
|
||||
download: 'Stáhnout',
|
||||
duplicateCard_title: 'Duplikovat kartu',
|
||||
edit: 'Upravit',
|
||||
editAvatar_title: 'Upravit avatar',
|
||||
editColor_title: 'Upravit barvu',
|
||||
editDescription_title: 'Upravit popis',
|
||||
editDueDate_title: 'Upravit termín',
|
||||
@@ -490,6 +502,8 @@ export default {
|
||||
start: 'Start',
|
||||
stop: 'Stop',
|
||||
subscribe: 'Odebírat',
|
||||
unlinkSso: 'Odpojit SSO',
|
||||
unlinkSso_title: 'Odpojit SSO',
|
||||
unsubscribe: 'Neodebírat',
|
||||
uploadNewAvatar: 'Nahrát nový avatar',
|
||||
uploadNewImage: 'Nahrát nový obrázek',
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
export default {
|
||||
translation: {
|
||||
common: {
|
||||
activeUserLimitReached: 'Dosažený limit aktivních uživatelů',
|
||||
activeUsersLimitReached: 'Dosažený limit aktivních uživatelů',
|
||||
adminLoginRequiredToInitializeInstance:
|
||||
'K inicializaci instance je nutné přihlášení správce.',
|
||||
emailAlreadyInUse: 'E-mail se již používá',
|
||||
emailOrUsername: 'E-mail nebo uživatelské jméno',
|
||||
iHaveReadAndAgreeToTheseTerms: 'Přečetl jsem si tyto podmínky a souhlasím s nimi.',
|
||||
invalidCredentials: 'Neplatné přihlašovací údaje',
|
||||
invalidEmailOrUsername: 'Nesprávný e-mail nebo uživatelské jméno',
|
||||
invalidPassword: 'Nesprávné heslo',
|
||||
@@ -26,6 +25,7 @@ export default {
|
||||
action: {
|
||||
cancelAndClose: 'Zrušit a zavřít',
|
||||
continue: 'Pokračovat',
|
||||
debugSso: 'Ladit SSO',
|
||||
goBack: 'Zpět',
|
||||
goHome: 'Domů',
|
||||
logIn: 'Přihlásit se',
|
||||
|
||||
@@ -88,6 +88,8 @@ export default {
|
||||
'Er du sikker på at du vil fjerne denne projektleder fra projektet?',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'Er du sikker på at du vil fjerne dette medlem fra tavlen?',
|
||||
areYouSureYouWantToUnlinkSsoFromThisUser:
|
||||
'Er du sikker på, at du vil fjerne SSO-forbindelsen fra denne bruger? Dette vil tillade brugeren at logge ind med en adgangskode.',
|
||||
assignAsOwner_title: 'Sæt som ejer',
|
||||
atLeastOneListMustBePresent: 'Mindst én liste skal være til stede',
|
||||
attachment: 'Vedhæft fil',
|
||||
@@ -142,6 +144,13 @@ export default {
|
||||
customFieldGroups_title: 'Brugerdefinerede feltgrupper',
|
||||
customField_title: 'Brugerdefineret felt',
|
||||
customFields_title: 'Brugerdefinerede felter',
|
||||
discoverPlankaPro: '✨ Flere funktioner til dine boards: Opdag PLANKA Pro',
|
||||
discoverPlankaPro_title: 'Opdag PLANKA Pro',
|
||||
upgradeTeamToPro_title: 'Opgrader team til Pro',
|
||||
proFeatureCalendar: '✨ Kalendervisning til dine boards',
|
||||
proFeatureRecurringCards: '✨ Tilbagevendende kort og automatisering',
|
||||
proFeatureGuestRoles: '✨ Gæsteroller og eksternt samarbejde',
|
||||
dismissProBannerFor30Days: 'Luk i 30 dage',
|
||||
customerPanel_title: 'Kundepanel',
|
||||
dangerZone_title: 'Farezone',
|
||||
date: 'Dato',
|
||||
@@ -171,6 +180,7 @@ export default {
|
||||
deletedUser_title: 'Slettet bruger',
|
||||
description: 'Beskrivelse',
|
||||
display: 'Vis',
|
||||
displayCardAges: 'Vis kortalder',
|
||||
dropFileToUpload: 'Slip fil for at uploade',
|
||||
dueDate_title: 'Frist',
|
||||
dynamicAndUnevenlySpacedLayout: 'Dynamisk og ujævnt fordelt layout.',
|
||||
@@ -330,6 +340,7 @@ export default {
|
||||
turnOffRecentCardHighlighting: 'Slå fremhævelse af nylige kort fra',
|
||||
typeNameToConfirm: 'Skriv navnet for at bekræfte.',
|
||||
typeTitleToConfirm: 'Skriv overskriften for at bekræfte.',
|
||||
unlinkSso_title: 'Fjernelse af SSO-forbindelse',
|
||||
unsavedChanges: 'Ikke-gemte ændringer',
|
||||
uploadFailedFileIsTooBig: 'Upload mislykkedes: Filen er for stor.',
|
||||
uploadFailedNotEnoughStorageSpace: 'Upload mislykkedes: Ikke nok lagerplads.',
|
||||
@@ -443,6 +454,7 @@ export default {
|
||||
download: 'Download',
|
||||
duplicateCard_title: 'Duplikér kort',
|
||||
edit: 'Rediger',
|
||||
editAvatar_title: 'Rediger profilbillede',
|
||||
editColor_title: 'Rediger farve',
|
||||
editDescription_title: 'Rediger beskrivelse',
|
||||
editDueDate_title: 'Rediger frist',
|
||||
@@ -496,6 +508,8 @@ export default {
|
||||
start: 'Start',
|
||||
stop: 'Stop',
|
||||
subscribe: 'Abonnér',
|
||||
unlinkSso: 'Fjern SSO-forbindelse',
|
||||
unlinkSso_title: 'Fjern SSO-forbindelse',
|
||||
unsubscribe: 'Opsig abonnement',
|
||||
uploadNewAvatar: 'Tilføj nyt profilbillede',
|
||||
uploadNewImage: 'Tilføj nyt billede',
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
export default {
|
||||
translation: {
|
||||
common: {
|
||||
activeUserLimitReached: 'Grænsen for aktive brugere er nået',
|
||||
activeUsersLimitReached: 'Grænsen for aktive brugere er nået',
|
||||
adminLoginRequiredToInitializeInstance:
|
||||
'Administrator login påkrævet for at initialisere instans',
|
||||
emailAlreadyInUse: 'E-mail allerede i brug',
|
||||
emailOrUsername: 'E-mail eller brugernavn',
|
||||
iHaveReadAndAgreeToTheseTerms: 'Jeg har læst og accepterer disse vilkår',
|
||||
invalidCredentials: 'Forkerte loginoplysninger',
|
||||
invalidEmailOrUsername: 'Ugyldig e-mail eller brugernavn',
|
||||
invalidPassword: 'Ugyldig adgangskode',
|
||||
@@ -26,6 +25,7 @@ export default {
|
||||
action: {
|
||||
cancelAndClose: 'Annuller og luk',
|
||||
continue: 'Fortsæt',
|
||||
debugSso: 'Fejlfind SSO',
|
||||
goBack: 'Gå tilbage',
|
||||
goHome: 'Gå hjem',
|
||||
logIn: 'Log på',
|
||||
|
||||
@@ -101,6 +101,8 @@ export default {
|
||||
'Sind Sie sicher, dass Sie diesen Projektleiter aus dem Projekt entfernen möchten?',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'Sind Sie sicher, dass Sie dieses Mitglied aus dem Arbeitsbereich entfernen möchten?',
|
||||
areYouSureYouWantToUnlinkSsoFromThisUser:
|
||||
'Sind Sie sicher, dass Sie die SSO-Verknüpfung von diesem Benutzer aufheben möchten? Dies ermöglicht dem Benutzer die Anmeldung mit einem Passwort.',
|
||||
assignAsOwner_title: 'Als Eigentümer zuweisen',
|
||||
atLeastOneListMustBePresent: 'Mindestens eine Liste muss vorhanden sein',
|
||||
attachment: 'Anhang',
|
||||
@@ -159,6 +161,13 @@ export default {
|
||||
customFields_title: 'Feldgruppen',
|
||||
customerPanel_title: 'Kundenpanel',
|
||||
dangerZone_title: 'Gefahrenbereich',
|
||||
discoverPlankaPro: '✨ Mehr Features für eure Boards: PLANKA Pro entdecken',
|
||||
discoverPlankaPro_title: 'PLANKA Pro entdecken',
|
||||
dismissProBannerFor30Days: 'Für 30 Tage schließen',
|
||||
upgradeTeamToPro_title: 'Team auf Pro upgraden',
|
||||
proFeatureCalendar: '✨ Kalenderansicht für eure Boards',
|
||||
proFeatureRecurringCards: '✨ Wiederkehrende Karten',
|
||||
proFeatureGuestRoles: '✨ Gastrollen & externe Zusammenarbeit',
|
||||
date: 'Datum',
|
||||
deactivateUser_title: 'Benutzer deaktivieren',
|
||||
defaultCardType_title: 'Standard-Kartentyp',
|
||||
@@ -186,6 +195,7 @@ export default {
|
||||
deletedUser_title: 'Gelöschter Benutzer',
|
||||
description: 'Beschreibung',
|
||||
display: 'Anzeige',
|
||||
displayCardAges: 'Kartenalter anzeigen',
|
||||
dropFileToUpload: 'Datei für Upload ablegen',
|
||||
dueDate_title: 'Fälligkeitsdatum',
|
||||
dynamicAndUnevenlySpacedLayout: 'Dynamisches und ungleichmäßig verteiltes Layout.',
|
||||
@@ -344,6 +354,7 @@ export default {
|
||||
turnOffRecentCardHighlighting: 'Hervorhebung neuer Karten ausschalten',
|
||||
typeNameToConfirm: 'Namen zur Bestätigung eingeben.',
|
||||
typeTitleToConfirm: 'Titel zur Bestätigung eingeben.',
|
||||
unlinkSso_title: 'SSO-Verknüpfung aufheben',
|
||||
unsavedChanges: 'Ungespeicherte Änderungen',
|
||||
uploadFailedFileIsTooBig: 'Upload fehlgeschlagen: Datei ist zu groß.',
|
||||
uploadFailedNotEnoughStorageSpace: 'Upload fehlgeschlagen: Nicht genügend Speicherplatz.',
|
||||
@@ -459,6 +470,7 @@ export default {
|
||||
download: 'Herunterladen',
|
||||
duplicateCard_title: 'Karte duplizieren',
|
||||
edit: 'Bearbeiten',
|
||||
editAvatar_title: 'Profilbild bearbeiten',
|
||||
editColor_title: 'Farbe bearbeiten',
|
||||
editDescription_title: 'Beschreibung ändern',
|
||||
editDueDate_title: 'Fälligkeitsdatum bearbeiten',
|
||||
@@ -512,6 +524,8 @@ export default {
|
||||
start: 'Start',
|
||||
stop: 'Stopp',
|
||||
subscribe: 'Abonnieren',
|
||||
unlinkSso: 'SSO-Verknüpfung aufheben',
|
||||
unlinkSso_title: 'SSO-Verknüpfung aufheben',
|
||||
unsubscribe: 'De-abonnieren',
|
||||
uploadNewAvatar: 'Neuen Avatar hochladen',
|
||||
uploadNewImage: 'Neues Bild hochladen',
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
export default {
|
||||
translation: {
|
||||
common: {
|
||||
activeUserLimitReached: 'Maximale Anzahl aktiver Benutzer erreicht',
|
||||
activeUsersLimitReached: 'Maximale Anzahl aktiver Benutzer erreicht',
|
||||
adminLoginRequiredToInitializeInstance:
|
||||
'Admin-Anmeldung erforderlich zur Initialisierung der Instanz',
|
||||
emailAlreadyInUse: 'E-mail Adresse wird bereits benutzt',
|
||||
emailOrUsername: 'E-Mail-Adresse oder Benutzername',
|
||||
iHaveReadAndAgreeToTheseTerms: 'Ich habe diese Bedingungen gelesen und stimme ihnen zu',
|
||||
invalidCredentials: 'Ungültige Anmeldeinformationen',
|
||||
invalidEmailOrUsername: 'Ungültige E-Mail-Adresse oder Benutzername',
|
||||
invalidPassword: 'Ungültiges Passwort',
|
||||
@@ -26,6 +25,7 @@ export default {
|
||||
action: {
|
||||
cancelAndClose: 'Abbrechen und schließen',
|
||||
continue: 'Fortfahren',
|
||||
debugSso: 'SSO debuggen',
|
||||
goBack: 'Zurück gehen',
|
||||
goHome: 'Zur Startseite',
|
||||
logIn: 'Einloggen',
|
||||
|
||||
@@ -100,6 +100,8 @@ export default {
|
||||
'Είστε σίγουροι ότι θέλετε να αφαιρέσετε αυτόν τον διαχειριστή από το έργο;',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'Είστε σίγουροι ότι θέλετε να αφαιρέσετε αυτό το μέλος από τον πίνακα;',
|
||||
areYouSureYouWantToUnlinkSsoFromThisUser:
|
||||
'Είστε σίγουροι ότι θέλετε να αποσυνδέσετε το SSO από αυτόν τον χρήστη; Αυτό θα επιτρέψει στον χρήστη να συνδεθεί με κωδικό πρόσβασης.',
|
||||
assignAsOwner_title: 'Ορισμός ως ιδιοκτήτης',
|
||||
atLeastOneListMustBePresent: 'Πρέπει να υπάρχει τουλάχιστον μία λίστα',
|
||||
attachment: 'Συνημμένο',
|
||||
@@ -155,6 +157,14 @@ export default {
|
||||
customFieldGroups_title: 'Ομάδες προσαρμοσμένων πεδίων',
|
||||
customField_title: 'Προσαρμοσμένο πεδίο',
|
||||
customFields_title: 'Προσαρμοσμένα πεδία',
|
||||
discoverPlankaPro:
|
||||
'✨ Περισσότερες λειτουργίες για τους πίνακές σας: Ανακαλύψτε το PLANKA Pro',
|
||||
discoverPlankaPro_title: 'Ανακαλύψτε το PLANKA Pro',
|
||||
upgradeTeamToPro_title: 'Αναβαθμίστε την ομάδα σε Pro',
|
||||
proFeatureCalendar: '✨ Προβολή ημερολογίου για τους πίνακές σας',
|
||||
proFeatureRecurringCards: '✨ Επαναλαμβανόμενες κάρτες και αυτοματισμός',
|
||||
proFeatureGuestRoles: '✨ Ρόλοι επισκεπτών και εξωτερική συνεργασία',
|
||||
dismissProBannerFor30Days: 'Κλείσιμο για 30 ημέρες',
|
||||
customerPanel_title: 'Πίνακας πελάτη',
|
||||
dangerZone_title: 'Επικίνδυνη ζώνη',
|
||||
date: 'Ημερομηνία',
|
||||
@@ -184,6 +194,7 @@ export default {
|
||||
deletedUser_title: 'Διαγραμμένος χρήστης',
|
||||
description: 'Περιγραφή',
|
||||
display: 'Εμφάνιση',
|
||||
displayCardAges: 'Εμφάνιση ηλικίας καρτών',
|
||||
dropFileToUpload: 'Σύρετε το αρχείο για μεταφόρτωση',
|
||||
dueDate_title: 'Ημερομηνία λήξης',
|
||||
dynamicAndUnevenlySpacedLayout: 'Δυναμική και άνισα κατανεμημένη διάταξη.',
|
||||
@@ -343,6 +354,7 @@ export default {
|
||||
turnOffRecentCardHighlighting: 'Απενεργοποίηση επισήμανσης πρόσφατων καρτών',
|
||||
typeNameToConfirm: 'Πληκτρολογήστε το όνομα για επιβεβαίωση.',
|
||||
typeTitleToConfirm: 'Πληκτρολογήστε τον τίτλο για επιβεβαίωση.',
|
||||
unlinkSso_title: 'Αποσύνδεση SSO',
|
||||
unsavedChanges: 'Μη αποθηκευμένες αλλαγές',
|
||||
uploadFailedFileIsTooBig: 'Η μεταφόρτωση απέτυχε: το αρχείο είναι πολύ μεγάλο.',
|
||||
uploadFailedNotEnoughStorageSpace:
|
||||
@@ -463,6 +475,7 @@ export default {
|
||||
download: 'Λήψη',
|
||||
duplicateCard_title: 'Διπλασιασμός κάρτας',
|
||||
edit: 'Επεξεργασία',
|
||||
editAvatar_title: 'Επεξεργασία avatar',
|
||||
editColor_title: 'Επεξεργασία χρώματος',
|
||||
editDescription_title: 'Επεξεργασία περιγραφής',
|
||||
editDueDate_title: 'Επεξεργασία ημερομηνίας λήξης',
|
||||
@@ -516,6 +529,8 @@ export default {
|
||||
start: 'Έναρξη',
|
||||
stop: 'Διακοπή',
|
||||
subscribe: 'Εγγραφή',
|
||||
unlinkSso: 'Αποσύνδεση SSO',
|
||||
unlinkSso_title: 'Αποσύνδεση SSO',
|
||||
unsubscribe: 'Απεγγραφή',
|
||||
uploadNewAvatar: 'Μεταφόρτωση νέου avatar',
|
||||
uploadNewImage: 'Μεταφόρτωση νέας εικόνας',
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
export default {
|
||||
translation: {
|
||||
common: {
|
||||
activeUserLimitReached: 'Έχει επιτευχθεί το όριο ενεργών χρηστών',
|
||||
activeUsersLimitReached: 'Έχει επιτευχθεί το όριο ενεργών χρηστών',
|
||||
adminLoginRequiredToInitializeInstance:
|
||||
'Απαιτείται σύνδεση διαχειριστή για την αρχικοποίηση της εφαρμογής',
|
||||
emailAlreadyInUse: 'Το e-mail χρησιμοποιείται ήδη',
|
||||
emailOrUsername: 'E-mail ή όνομα χρήστη',
|
||||
iHaveReadAndAgreeToTheseTerms: 'Έχω διαβάσει και συμφωνώ με αυτούς τους όρους',
|
||||
invalidCredentials: 'Μη έγκυρα στοιχεία σύνδεσης',
|
||||
invalidEmailOrUsername: 'Μη έγκυρο e-mail ή όνομα χρήστη',
|
||||
invalidPassword: 'Μη έγκυρος κωδικός',
|
||||
@@ -26,6 +25,7 @@ export default {
|
||||
action: {
|
||||
cancelAndClose: 'Ακύρωση και κλείσιμο',
|
||||
continue: 'Συνέχεια',
|
||||
debugSso: 'Αποσφαλμάτωση SSO',
|
||||
goBack: 'Επιστροφή',
|
||||
goHome: 'Αρχική σελίδα',
|
||||
logIn: 'Σύνδεση',
|
||||
|
||||
@@ -88,6 +88,8 @@ export default {
|
||||
'Are you sure you want to remove this manager from the project?',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'Are you sure you want to remove this member from the board?',
|
||||
areYouSureYouWantToUnlinkSsoFromThisUser:
|
||||
'Are you sure you want to unlink SSO from this user? This will allow the user to log in with a password.',
|
||||
assignAsOwner_title: 'Assign As Owner',
|
||||
atLeastOneListMustBePresent: 'At least one list must be present',
|
||||
attachment: 'Attachment',
|
||||
@@ -141,6 +143,13 @@ export default {
|
||||
customFieldGroups_title: 'Custom Field Groups',
|
||||
customField_title: 'Custom Field',
|
||||
customFields_title: 'Custom Fields',
|
||||
discoverPlankaPro: '✨ More features for your boards: Discover PLANKA Pro',
|
||||
discoverPlankaPro_title: 'Discover PLANKA Pro',
|
||||
upgradeTeamToPro_title: 'Upgrade Team to Pro',
|
||||
proFeatureCalendar: '✨ Calendar View for your boards',
|
||||
proFeatureRecurringCards: '✨ Recurring Cards & Automation',
|
||||
proFeatureGuestRoles: '✨ Guest Roles & External Collaboration',
|
||||
dismissProBannerFor30Days: 'Dismiss for 30 days',
|
||||
customerPanel_title: 'Customer Panel',
|
||||
dangerZone_title: 'Danger Zone',
|
||||
date: 'Date',
|
||||
@@ -170,6 +179,7 @@ export default {
|
||||
deletedUser_title: 'Deleted User',
|
||||
description: 'Description',
|
||||
display: 'Display',
|
||||
displayCardAges: 'Display card ages',
|
||||
dropFileToUpload: 'Drop file to upload',
|
||||
dueDate_title: 'Due Date',
|
||||
dynamicAndUnevenlySpacedLayout: 'Dynamic and unevenly spaced layout.',
|
||||
@@ -327,6 +337,7 @@ export default {
|
||||
turnOffRecentCardHighlighting: 'Turn off recent card highlighting',
|
||||
typeNameToConfirm: 'Type the name to confirm.',
|
||||
typeTitleToConfirm: 'Type the title to confirm.',
|
||||
unlinkSso_title: 'Unlink SSO',
|
||||
unsavedChanges: 'Unsaved changes',
|
||||
uploadFailedFileIsTooBig: 'Upload failed: File is too big.',
|
||||
uploadFailedNotEnoughStorageSpace: 'Upload failed: Not enough storage space.',
|
||||
@@ -439,6 +450,7 @@ export default {
|
||||
download: 'Download',
|
||||
duplicateCard_title: 'Duplicate Card',
|
||||
edit: 'Edit',
|
||||
editAvatar_title: 'Edit Avatar',
|
||||
editColor_title: 'Edit Color',
|
||||
editDescription_title: 'Edit Description',
|
||||
editDueDate_title: 'Edit Due Date',
|
||||
@@ -492,6 +504,8 @@ export default {
|
||||
start: 'Start',
|
||||
stop: 'Stop',
|
||||
subscribe: 'Subscribe',
|
||||
unlinkSso: 'Unlink SSO',
|
||||
unlinkSso_title: 'Unlink SSO',
|
||||
unsubscribe: 'Unsubscribe',
|
||||
uploadNewAvatar: 'Upload new avatar',
|
||||
uploadNewImage: 'Upload new image',
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
export default {
|
||||
translation: {
|
||||
common: {
|
||||
activeUserLimitReached: 'Active user limit reached',
|
||||
activeUsersLimitReached: 'Active users limit reached',
|
||||
adminLoginRequiredToInitializeInstance: 'Admin login required to initialize instance',
|
||||
emailAlreadyInUse: 'E-mail already in use',
|
||||
emailOrUsername: 'E-mail or username',
|
||||
iHaveReadAndAgreeToTheseTerms: 'I have read and agree to these Terms',
|
||||
invalidCredentials: 'Invalid credentials',
|
||||
invalidEmailOrUsername: 'Invalid e-mail or username',
|
||||
invalidPassword: 'Invalid password',
|
||||
@@ -25,6 +24,7 @@ export default {
|
||||
action: {
|
||||
cancelAndClose: 'Cancel and close',
|
||||
continue: 'Continue',
|
||||
debugSso: 'Debug SSO',
|
||||
goBack: 'Go back',
|
||||
goHome: 'Go home',
|
||||
logIn: 'Log in',
|
||||
|
||||
@@ -83,6 +83,8 @@ export default {
|
||||
'Are you sure you want to remove this manager from the project?',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'Are you sure you want to remove this member from the board?',
|
||||
areYouSureYouWantToUnlinkSsoFromThisUser:
|
||||
'Are you sure you want to unlink SSO from this user? This will allow the user to log in with a password.',
|
||||
assignAsOwner_title: 'Assign As Owner',
|
||||
atLeastOneListMustBePresent: 'At least one list must be present',
|
||||
attachment: 'Attachment',
|
||||
@@ -138,6 +140,13 @@ export default {
|
||||
customFields_title: 'Custom Fields',
|
||||
customerPanel_title: 'Customer Panel',
|
||||
dangerZone_title: 'Danger Zone',
|
||||
discoverPlankaPro: '✨ More features for your boards: Discover PLANKA Pro',
|
||||
discoverPlankaPro_title: 'Discover PLANKA Pro',
|
||||
dismissProBannerFor30Days: 'Dismiss for 30 days',
|
||||
upgradeTeamToPro_title: 'Upgrade Team to Pro',
|
||||
proFeatureCalendar: '✨ Calendar View for your boards',
|
||||
proFeatureRecurringCards: '✨ Recurring Cards',
|
||||
proFeatureGuestRoles: '✨ Guest Roles & External Collaboration',
|
||||
date: 'Date',
|
||||
deactivateUser_title: 'Deactivate User',
|
||||
defaultCardType_title: 'Default Card Type',
|
||||
@@ -165,6 +174,7 @@ export default {
|
||||
deletedUser_title: 'Deleted User',
|
||||
description: 'Description',
|
||||
display: 'Display',
|
||||
displayCardAges: 'Display card ages',
|
||||
dropFileToUpload: 'Drop file to upload',
|
||||
dueDate_title: 'Due Date',
|
||||
dynamicAndUnevenlySpacedLayout: 'Dynamic and unevenly spaced layout.',
|
||||
@@ -322,6 +332,7 @@ export default {
|
||||
turnOffRecentCardHighlighting: 'Turn off recent card highlighting',
|
||||
typeNameToConfirm: 'Type the name to confirm.',
|
||||
typeTitleToConfirm: 'Type the title to confirm.',
|
||||
unlinkSso_title: 'Inlink SSO',
|
||||
unsavedChanges: 'Unsaved changes',
|
||||
uploadFailedFileIsTooBig: 'Upload failed: File is too big.',
|
||||
uploadFailedNotEnoughStorageSpace: 'Upload failed: Not enough storage space.',
|
||||
@@ -434,6 +445,7 @@ export default {
|
||||
download: 'Download',
|
||||
duplicateCard_title: 'Duplicate Card',
|
||||
edit: 'Edit',
|
||||
editAvatar_title: 'Edit Avatar',
|
||||
editColor_title: 'Edit Color',
|
||||
editDescription_title: 'Edit Description',
|
||||
editDueDate_title: 'Edit Due Date',
|
||||
@@ -487,6 +499,8 @@ export default {
|
||||
start: 'Start',
|
||||
stop: 'Stop',
|
||||
subscribe: 'Subscribe',
|
||||
unlinkSso: 'Unlink SSO',
|
||||
unlinkSso_title: 'Unlink SSO',
|
||||
unsubscribe: 'Unsubscribe',
|
||||
uploadNewAvatar: 'Upload new avatar',
|
||||
uploadNewImage: 'Upload new image',
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
export default {
|
||||
translation: {
|
||||
common: {
|
||||
activeUserLimitReached: 'Active user limit reached',
|
||||
activeUsersLimitReached: 'Active users limit reached',
|
||||
adminLoginRequiredToInitializeInstance: 'Admin login required to initialize instance',
|
||||
emailAlreadyInUse: 'E-mail already in use',
|
||||
emailOrUsername: 'E-mail or username',
|
||||
iHaveReadAndAgreeToTheseTerms: 'I have read and agree to these Terms',
|
||||
invalidCredentials: 'Invalid credentials',
|
||||
invalidEmailOrUsername: 'Invalid e-mail or username',
|
||||
invalidPassword: 'Invalid password',
|
||||
@@ -25,6 +24,7 @@ export default {
|
||||
action: {
|
||||
cancelAndClose: 'Cancel and close',
|
||||
continue: 'Continue',
|
||||
debugSso: 'Debug SSO',
|
||||
goBack: 'Go back',
|
||||
goHome: 'Go home',
|
||||
logIn: 'Log in',
|
||||
|
||||
@@ -92,6 +92,8 @@ export default {
|
||||
'¿Estás seguro de que quieres eliminar este gestor del proyecto?',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'¿Estás seguro de que quieres eliminar este miembro del tablero?',
|
||||
areYouSureYouWantToUnlinkSsoFromThisUser:
|
||||
'¿Estás seguro de que quieres desvincular SSO de este usuario? Esto permitirá que el usuario inicie sesión con una contraseña.',
|
||||
assignAsOwner_title: 'Asignar como propietario',
|
||||
atLeastOneListMustBePresent: 'Debe existir al menos una lista',
|
||||
attachment: 'Archivo adjunto',
|
||||
@@ -147,6 +149,13 @@ export default {
|
||||
customFieldGroups_title: 'Grupos de campos personalizados',
|
||||
customField_title: 'Campo personalizado',
|
||||
customFields_title: 'Campos personalizados',
|
||||
discoverPlankaPro: '✨ Más funciones para tus tableros: Descubre PLANKA Pro',
|
||||
discoverPlankaPro_title: 'Descubre PLANKA Pro',
|
||||
upgradeTeamToPro_title: 'Actualizar equipo a Pro',
|
||||
proFeatureCalendar: '✨ Vista de calendario para tus tableros',
|
||||
proFeatureRecurringCards: '✨ Tarjetas recurrentes y automatización',
|
||||
proFeatureGuestRoles: '✨ Roles de invitado y colaboración externa',
|
||||
dismissProBannerFor30Days: 'Cerrar durante 30 días',
|
||||
customerPanel_title: 'Panel del cliente',
|
||||
dangerZone_title: 'Zona de peligro',
|
||||
date: 'Fecha',
|
||||
@@ -176,6 +185,7 @@ export default {
|
||||
deletedUser_title: 'Usuario eliminado',
|
||||
description: 'Descripción',
|
||||
display: 'Mostrar',
|
||||
displayCardAges: 'Mostrar antigüedad de las tarjetas',
|
||||
dropFileToUpload: 'Arrastra archivo para subir',
|
||||
dueDate_title: 'Fecha de vencimiento',
|
||||
dynamicAndUnevenlySpacedLayout: 'Diseño dinámico y con espaciado irregular.',
|
||||
@@ -335,6 +345,7 @@ export default {
|
||||
turnOffRecentCardHighlighting: 'Desactivar resaltado de tarjetas recientes',
|
||||
typeNameToConfirm: 'Escribe el nombre para confirmar.',
|
||||
typeTitleToConfirm: 'Escribe el título para confirmar.',
|
||||
unlinkSso_title: 'Desvinculación de SSO',
|
||||
unsavedChanges: 'Cambios sin guardar',
|
||||
uploadFailedFileIsTooBig: 'Error al subir: El archivo es demasiado grande.',
|
||||
uploadFailedNotEnoughStorageSpace:
|
||||
@@ -449,6 +460,7 @@ export default {
|
||||
download: 'Descargar',
|
||||
duplicateCard_title: 'Duplicar tarjeta',
|
||||
edit: 'Editar',
|
||||
editAvatar_title: 'Editar avatar',
|
||||
editColor_title: 'Editar color',
|
||||
editDescription_title: 'Editar descripción',
|
||||
editDueDate_title: 'Editar fecha de vencimiento',
|
||||
@@ -502,6 +514,8 @@ export default {
|
||||
start: 'Iniciar',
|
||||
stop: 'Detener',
|
||||
subscribe: 'Suscribirse',
|
||||
unlinkSso: 'Desvincular SSO',
|
||||
unlinkSso_title: 'Desvincular SSO',
|
||||
unsubscribe: 'Cancelar suscripción',
|
||||
uploadNewAvatar: 'Subir nuevo avatar',
|
||||
uploadNewImage: 'Subir nueva imagen',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user