From 3bd8eba4164069863c7e29bfb5ce888ee0d6957e Mon Sep 17 00:00:00 2001
From: leroyloren <57643470+leroyloren@users.noreply.github.com>
Date: Wed, 14 May 2025 22:28:39 +0200
Subject: [PATCH 001/108] fix: Update Czech translation (#1129)
---
client/src/locales/cs-CZ/core.js | 69 ++++++++++++++++----------------
1 file changed, 35 insertions(+), 34 deletions(-)
diff --git a/client/src/locales/cs-CZ/core.js b/client/src/locales/cs-CZ/core.js
index ec9911af..3fdfd8c7 100644
--- a/client/src/locales/cs-CZ/core.js
+++ b/client/src/locales/cs-CZ/core.js
@@ -25,7 +25,7 @@ export default {
addAttachment_title: 'Přidat přílohu',
addCustomField_title: 'Přidat vlastní pole',
addCustomFieldGroup_title: 'Přidat vlastní skupinu polí',
- addManager_title: 'Přidat vedoucího',
+ addManager_title: 'Přidat správce',
addMember_title: 'Přidat člena',
addTaskList_title: 'Přidat seznam úkolů',
addUser_title: 'Přidat uživatele',
@@ -43,12 +43,12 @@ export default {
areYouSureYouWantToArchiveCards: 'Opravdu chcete archivovat karty?',
areYouSureYouWantToArchiveThisCard: 'Opravdu chcete archivovat tuto kartu?',
areYouSureYouWantToAssignThisProjectManagerAsOwner:
- 'Opravdu chcete přiřadit tohoto projektového manažera jako vlastníka?',
+ 'Opravdu chcete tohoto správce přiřadit jako vlastníka?',
areYouSureYouWantToDeactivateThisUser: 'Opravdu chcete deaktivovat tohoto uživatele?',
areYouSureYouWantToDeleteThisAttachment: 'Opravdu chcete smazat tuto přílohu?',
areYouSureYouWantToDeleteThisBackgroundImage:
'Opravdu chcete tento obrázek na pozadí odstranit?',
- areYouSureYouWantToDeleteThisBoard: 'Opravdu chcete smazat tuto tabuli?',
+ areYouSureYouWantToDeleteThisBoard: 'Opravdu chcete smazat tuto nástěnku?',
areYouSureYouWantToDeleteThisCard: 'Opravdu chcete smazat tuto kartu?',
areYouSureYouWantToDeleteThisCardForever: 'Opravdu chcete tuto kartu navždy odstranit?',
areYouSureYouWantToDeleteThisComment: 'Opravdu chcete smazat tento komentář?',
@@ -64,12 +64,13 @@ export default {
areYouSureYouWantToDeleteThisTaskList: 'Opravdu chcete tento seznam úkolů odstranit?',
areYouSureYouWantToDeleteThisUser: 'Opravdu chcete smazat tohoto uživatele?',
areYouSureYouWantToEmptyTrash: 'Opravdu chcete vysypat koš?',
- areYouSureYouWantToLeaveBoard: 'Opravdu chcete opustit tuto tabuli?',
+ areYouSureYouWantToLeaveBoard: 'Opravdu chcete opustit tuto nástěnku?',
areYouSureYouWantToLeaveProject: 'Opravdu chcete opustit projekt?',
areYouSureYouWantToMakeThisProjectShared: 'Opravdu chcete tento projekt sdílet?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Opravdu chcete tohoto správce z projektu odebrat?',
- areYouSureYouWantToRemoveThisMemberFromBoard: 'Opravdu chcete tohoto člena odebrat z tabule?',
+ areYouSureYouWantToRemoveThisMemberFromBoard:
+ 'Opravdu chcete tohoto člena odebrat z nástěnky?',
assignAsOwner_title: 'Přiřadit jako vlastníka',
atLeastOneListMustBePresent: 'Musí být k dispozici alespoň jeden seznam',
attachment: 'Příloha',
@@ -78,27 +79,27 @@ export default {
background: 'Pozadí',
baseCustomFields_title: 'Základní vlastní pole',
baseGroup: 'Základní skupina',
- board: 'Tabule',
- boardActions_title: 'Akce tabule',
- boardNotFound_title: 'Tabule nenalezena',
- boardSubscribed: 'Tabule odebíraná',
- boardUser: 'Uživatel tabule',
+ board: 'Nástěnka',
+ boardActions_title: 'Akce nástěnky',
+ boardNotFound_title: 'Nástěnka nenalezena',
+ boardSubscribed: 'Nástěnka odebíraná',
+ boardUser: 'Uživatel nástěnky',
byCreationTime: 'Podle času vytvoření',
byDefault: 'Ve výchozím nastavení',
- byDueDate: 'Do data platnosti',
- canBeInvitedToWorkInBoards: 'Může být přizván k práci ve tabulích.',
+ byDueDate: 'Podle data platnosti',
+ canBeInvitedToWorkInBoards: 'Může být pozván k práci v nástěnkách.',
canComment: 'Může komentovat',
canCreateOwnProjectsAndBeInvitedToWorkInOthers:
- 'Může vytvářet vlastní projekty a být přizván k práci na jiných.',
+ 'Může vytvářet vlastní projekty a být pozván k práci na jiných.',
canEditBoardLayoutAndAssignMembersToCards:
- 'Může upravovat rozvržení tabule a přiřazovat členy ke kartám.',
+ 'Může upravovat rozvržení nástěnky a přiřazovat členy ke kartám.',
canManageSystemWideSettingsAndActAsProjectOwner:
'Může spravovat nastavení celého systému a být jako vlastník projektu.',
- canOnlyViewBoard: 'Může zobrazit pouze tabuli.',
- cardActions_title: 'Akce na kartě',
+ canOnlyViewBoard: 'Může zobrazit pouze nástěnku.',
+ cardActions_title: 'Akce karty',
cardNotFound_title: 'Karta nenalezena',
cardsOnThisListAreAvailableToAllBoardMembers:
- 'Karty na tomto seznamu jsou k dispozici všem členům tabule.',
+ 'Karty na tomto seznamu jsou k dispozici všem členům nástěnky.',
cardsOnThisListAreCompleteAndReadyToBeArchived:
'Karty na tomto seznamu jsou kompletní a připravené k archivaci.',
cardsOnThisListAreReadyToBeWorkedOn: 'Karty na tomto seznamu jsou připraveny k práci.',
@@ -108,7 +109,7 @@ export default {
comments: 'Komentáře',
contentExceedsLimit: 'Obsah překračuje {{limit}}',
copy_inline: 'kopie',
- createBoard_title: 'Vytvořit tabuli',
+ createBoard_title: 'Vytvořit nástěnku',
createCustomFieldGroup_title: 'Vytvořit vlastní skupinu polí',
createLabel_title: 'Vytvořit štítek',
createNewOneOrSelectExistingOne: 'Vytvořit nový nebo vybrat již existující.',
@@ -127,10 +128,10 @@ export default {
defaultView_title: 'Výchozí zobrazení',
deletedUser_title: 'Smazaný uživatel',
deleteAllBoardsToBeAbleToDeleteThisProject:
- 'Smazat všechny tabule aby bylo možné tento projekt smazat',
+ 'Pro smazání tohoto projektu je třeba nejprve smazat všechny nástěnky',
deleteAttachment_title: 'Smazat přílohu',
deleteBackgroundImage_title: 'Smazat obrázek pozadí',
- deleteBoard_title: 'Smazat tabuli',
+ deleteBoard_title: 'Smazat nástěnku',
deleteCard_title: 'Smazat kartu',
deleteCardForever_title: 'Smazat kartu navždy',
deleteComment_title: 'Smazat komentář',
@@ -182,19 +183,19 @@ export default {
grid: 'Mřížka',
hideFromProjectListAndFavorites: 'Skrýt ze seznamu projektů a oblíbených položek',
hours: 'Hodiny',
- importBoard_title: 'Importovat tabuli',
+ importBoard_title: 'Importovat nástěnku',
invalidCurrentPassword: 'Neplatné aktuální heslo',
kanban: 'Kanban',
labels: 'Štítky',
language: 'Jazyk',
- leaveBoard_title: 'Opustit tabuli',
+ leaveBoard_title: 'Opustit nástěnku',
leaveProject_title: 'Opustit projekt',
limitCardTypesToDefaultOne: 'Omezit typy karet na jeden výchozí',
list: 'Seznam',
lists: 'Seznamy',
listActions_title: 'Seznam akcí',
makeProjectShared_title: 'Vytvořit sdílený projekt',
- managers: 'Vedoucí',
+ managers: 'Správci',
members: 'Členové',
memberActions_title: 'Akce člena',
minutes: 'Minuty',
@@ -207,13 +208,13 @@ export default {
newUsername: 'Nové uživatelské jméno',
newVersionAvailable: 'Nová verze je k dispozici',
noConnectionToServer: 'Není spojení k serveru',
- noBoards: 'Žádné tabule',
+ noBoards: 'Žádné nástěnky',
noLists: 'Žádné seznamy',
noProjects: 'Žádné projekty',
notifications: 'Oznámení',
noUnreadNotifications: 'Žádné nepřečtené oznámení.',
oldestFirst: 'Nejstarší',
- openBoard_title: 'Otevřít tabuli',
+ openBoard_title: 'Otevřít nástěnku',
optional_inline: 'volitelné',
organization: 'Společnost',
others: 'Jiné',
@@ -228,7 +229,7 @@ export default {
projectNotFound_title: 'Projekt nenalezen',
projectOwner: 'Vlastník projektu',
referenceDataAndKnowledgeStorage: 'Uchovávání referenčních údajů a znalostí.',
- removeManager_title: 'Odstranit vedoucího',
+ removeManager_title: 'Odstranit správce',
removeMember_title: 'Odstranit člena',
role: 'Role',
searchCards: 'Hledat karty...',
@@ -241,7 +242,7 @@ export default {
searchUsers: 'Hledat uživatele...',
seconds: 'Vteřin',
selectAssignee_title: 'Vybrat přiřazení',
- selectBoard: 'Vybrat tabuli',
+ selectBoard: 'Vybrat nástěnku',
selectList: 'Vybrat seznam',
selectListToRestoreThisCard: 'Vybrat seznam pro obnovení této karty',
selectOrder_title: 'Vybrat řazení',
@@ -313,7 +314,7 @@ export default {
archiveCards_title: 'Archiv karet',
assignAsOwner: 'Přiřadit jako vlastníka',
cancel: 'Zrušit',
- createBoard: 'Vytvořit tabuli',
+ createBoard: 'Vytvořit nástěnku',
createCustomFieldGroup: 'Vytvořit vlastní skupinu polí',
createFile: 'Vytvořit soubor',
createLabel: 'Vytvořit štítek',
@@ -325,8 +326,8 @@ export default {
deleteAttachment: 'Smazat přílohu',
deleteAvatar: 'Smazat avatar',
deleteBackgroundImage: 'Smazat obrázek pozadí',
- deleteBoard: 'Smazat tabuli',
- deleteBoard_title: 'Smazat tabuli',
+ deleteBoard: 'Smazat nástěnku',
+ deleteBoard_title: 'Smazat nástěnku',
deleteCard: 'Smazat kartu',
deleteCard_title: 'Smazat kartu',
deleteCardForever: 'Smazat kartu navždy',
@@ -368,7 +369,7 @@ export default {
import: 'Import',
join: 'Připojit',
leave: 'Opustit',
- leaveBoard: 'Opustit tabuli',
+ leaveBoard: 'Opustit nástěnku',
leaveProject: 'Opustit projekt',
logOut_title: 'Odhlásit se',
makeCover_title: 'Vytvořit obal',
@@ -380,12 +381,12 @@ export default {
removeAssignee: 'Odstranit přiřazení',
removeColor: 'Smazat barvu',
removeCover_title: 'Odstranit obal',
- removeFromBoard: 'Odstranit z tabule',
+ removeFromBoard: 'Odstranit z nástěnky',
removeFromProject: 'Odstranit z projektu',
- removeManager: 'Odstranit vedoucího',
+ removeManager: 'Odstranit správce',
removeMember: 'Odstranit člena',
restoreToList: 'Obnovit do {{list}}',
- returnToBoard: 'Návrat na tabuli',
+ returnToBoard: 'Návrat na nástěnku',
save: 'Uložit',
showActive: 'Zobrazit aktivní',
showAllAttachments: 'Zozbrazit všechny přílohy ({{hidden}} skryté)',
From ac8361aee3b127693d693b2afc1f847a1234bc1d Mon Sep 17 00:00:00 2001
From: iosabi
Date: Thu, 15 May 2025 19:32:37 +0200
Subject: [PATCH 002/108] feat: Support for Docker secrets (#1132)
Closes #1001
---
docker-compose-dev.yml | 15 ++++++++-----
docker-compose.yml | 33 +++++++++++++++++++++++-----
server/.env.sample | 15 ++++++++-----
server/start.sh | 49 +++++++++++++++++++++++++++++++++++++++++-
4 files changed, 96 insertions(+), 16 deletions(-)
diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml
index e971d974..3beb5685 100644
--- a/docker-compose-dev.yml
+++ b/docker-compose-dev.yml
@@ -21,22 +21,27 @@ services:
# - TOKEN_EXPIRES_IN=365 # In days
# related: https://github.com/knex/knex/issues/2354
- # As knex does not pass query parameters from the connection string we
- # have to use environment variables in order to pass the desired values, e.g.
+ # As knex does not pass query parameters from the connection string,
+ # we have to use environment variables in order to pass the desired values, e.g.
# - PGSSLMODE=
# Configure knex to accept SSL certificates
# - KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
- # - DEFAULT_LANGUAGE=en-US # Used for per-board notifications
+ # Used for per-board notifications
+ # - DEFAULT_LANGUAGE=en-US
- # - DEFAULT_ADMIN_EMAIL=demo@demo.demo # Do not remove if you want to prevent this user from being edited/deleted
+ # Do not comment out DEFAULT_ADMIN_EMAIL if you want to prevent this user from being edited/deleted
+ # - DEFAULT_ADMIN_EMAIL=demo@demo.demo
# - DEFAULT_ADMIN_PASSWORD=demo
# - DEFAULT_ADMIN_NAME=Demo Demo
# - DEFAULT_ADMIN_USERNAME=demo
# - ACTIVE_USERS_LIMIT=
- # - SHOW_DETAILED_AUTH_ERRORS=false # Set to true to show more detailed authentication error messages. It should not be enabled without a rate limiter for security reasons.
+
+ # Set to true to show more detailed authentication error messages.
+ # It should not be enabled without a rate limiter for security reasons.
+ # - SHOW_DETAILED_AUTH_ERRORS=false
# - S3_ENDPOINT=
# - S3_REGION=
diff --git a/docker-compose.yml b/docker-compose.yml
index 038ee4f1..bd8f8e9c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -12,7 +12,17 @@ services:
environment:
- BASE_URL=http://localhost:3000
- DATABASE_URL=postgresql://postgres@postgres/planka
+
+ # Optionally store the database password in secrets:
+ # - DATABASE_URL=postgresql://postgres:$${DATABASE_PASSWORD}@postgres/planka
+ # - DATABASE_PASSWORD__FILE=/run/secrets/database_password
+ # And add the following to the service:
+ # secrets:
+ # - database_password
+
- SECRET_KEY=notsecretkey
+ # Optionally store in secrets - then SECRET_KEY should not be set
+ # - SECRET_KEY__FILE=/run/secrets/secret_key
# - LOG_LEVEL=warn
@@ -20,33 +30,44 @@ services:
# - TOKEN_EXPIRES_IN=365 # In days
# related: https://github.com/knex/knex/issues/2354
- # As knex does not pass query parameters from the connection string we
- # have to use environment variables in order to pass the desired values, e.g.
+ # As knex does not pass query parameters from the connection string,
+ # we have to use environment variables in order to pass the desired values, e.g.
# - PGSSLMODE=
# Configure knex to accept SSL certificates
# - KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
- # - DEFAULT_LANGUAGE=en-US # Used for per-board notifications
+ # Used for per-board notifications
+ # - DEFAULT_LANGUAGE=en-US
- # - DEFAULT_ADMIN_EMAIL=demo@demo.demo # Do not remove if you want to prevent this user from being edited/deleted
+ # Do not comment out DEFAULT_ADMIN_EMAIL if you want to prevent this user from being edited/deleted
+ # - DEFAULT_ADMIN_EMAIL=demo@demo.demo
# - DEFAULT_ADMIN_PASSWORD=demo
+ # Optionally store in secrets - then DEFAULT_ADMIN_PASSWORD should not be set
+ # - DEFAULT_ADMIN_PASSWORD__FILE=/run/secrets/default_admin_password
# - DEFAULT_ADMIN_NAME=Demo Demo
# - DEFAULT_ADMIN_USERNAME=demo
# - ACTIVE_USERS_LIMIT=
- # - SHOW_DETAILED_AUTH_ERRORS=false # Set to true to show more detailed authentication error messages. It should not be enabled without a rate limiter for security reasons.
+
+ # Set to true to show more detailed authentication error messages.
+ # It should not be enabled without a rate limiter for security reasons.
+ # - SHOW_DETAILED_AUTH_ERRORS=false
# - S3_ENDPOINT=
# - S3_REGION=
# - S3_ACCESS_KEY_ID=
# - S3_SECRET_ACCESS_KEY=
+ # Optionally store in secrets - then S3_SECRET_ACCESS_KEY should not be set
+ # - S3_SECRET_ACCESS_KEY__FILE=/run/secrets/s3_secret_access_key
# - S3_BUCKET=
# - S3_FORCE_PATH_STYLE=true
# - OIDC_ISSUER=
# - OIDC_CLIENT_ID=
# - OIDC_CLIENT_SECRET=
+ # Optionally store in secrets - then OIDC_CLIENT_SECRET should not be set
+ # - OIDC_CLIENT_SECRET__FILE=/run/secrets/oidc_client_secret
# - OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG=
# - OIDC_USERINFO_SIGNED_RESPONSE_ALG=
# - OIDC_SCOPES=openid email profile
@@ -69,6 +90,8 @@ services:
# - SMTP_SECURE=true
# - SMTP_USER=
# - SMTP_PASSWORD=
+ # Optionally store in secrets - then SMTP_PASSWORD should not be set
+ # - SMTP_PASSWORD__FILE=/run/secrets/smtp_password
# - SMTP_FROM="Demo Demo"
# - SMTP_TLS_REJECT_UNAUTHORIZED=false
diff --git a/server/.env.sample b/server/.env.sample
index 6420a09d..8817bb76 100644
--- a/server/.env.sample
+++ b/server/.env.sample
@@ -13,22 +13,27 @@ SECRET_KEY=notsecretkey
# TOKEN_EXPIRES_IN=365 # In days
# related: https://github.com/knex/knex/issues/2354
-# As knex does not pass query parameters from the connection string we
-# have to use environment variables in order to pass the desired values, e.g.
+# As knex does not pass query parameters from the connection string,
+# we have to use environment variables in order to pass the desired values, e.g.
# PGSSLMODE=
# Configure knex to accept SSL certificates
# KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
-# DEFAULT_LANGUAGE=en-US # Used for per-board notifications
+# Used for per-board notifications
+# DEFAULT_LANGUAGE=en-US
-# DEFAULT_ADMIN_EMAIL=demo@demo.demo # Do not remove if you want to prevent this user from being edited/deleted
+# Do not comment out DEFAULT_ADMIN_EMAIL if you want to prevent this user from being edited/deleted
+# DEFAULT_ADMIN_EMAIL=demo@demo.demo
# DEFAULT_ADMIN_PASSWORD=demo
# DEFAULT_ADMIN_NAME=Demo Demo
# DEFAULT_ADMIN_USERNAME=demo
# ACTIVE_USERS_LIMIT=
-# SHOW_DETAILED_AUTH_ERRORS=false # Set to true to show more detailed authentication error messages. It should not be enabled without a rate limiter for security reasons.
+
+# Set to true to show more detailed authentication error messages.
+# It should not be enabled without a rate limiter for security reasons.
+# SHOW_DETAILED_AUTH_ERRORS=false
# S3_ENDPOINT=
# S3_REGION=
diff --git a/server/start.sh b/server/start.sh
index 440950cc..0f41de93 100755
--- a/server/start.sh
+++ b/server/start.sh
@@ -1,2 +1,49 @@
#!/bin/bash
-export NODE_ENV=production && set -e && node db/init.js && node app.js --prod
+
+set -eu
+
+# Load secrets from files if *__FILE variables are provided.
+# Only the first line of each file is read (newline excluded).
+
+# DATABASE_PASSWORD (used to dynamically inject into DATABASE_URL)
+if [[ -n "${DATABASE_URL}" ]]; then
+ if [[ -z "${DATABASE_PASSWORD:-}" && -e "${DATABASE_PASSWORD__FILE:-}" ]]; then
+ read DATABASE_PASSWORD < "${DATABASE_PASSWORD__FILE}"
+ export DATABASE_URL="${DATABASE_URL/\$\{DATABASE_PASSWORD\}/${DATABASE_PASSWORD}}"
+ fi
+fi
+
+# SECRET_KEY
+if [[ -z "${SECRET_KEY:-}" && -e "${SECRET_KEY__FILE:-}" ]]; then
+ read SECRET_KEY < "${SECRET_KEY__FILE}"
+ export SECRET_KEY
+fi
+
+# DEFAULT_ADMIN_PASSWORD
+if [[ -z "${DEFAULT_ADMIN_PASSWORD:-}" && -e "${DEFAULT_ADMIN_PASSWORD__FILE:-}" ]]; then
+ read DEFAULT_ADMIN_PASSWORD < "${DEFAULT_ADMIN_PASSWORD__FILE}"
+ export DEFAULT_ADMIN_PASSWORD
+fi
+
+# S3_SECRET_ACCESS_KEY
+if [[ -z "${S3_SECRET_ACCESS_KEY:-}" && -e "${S3_SECRET_ACCESS_KEY__FILE:-}" ]]; then
+ read S3_SECRET_ACCESS_KEY < "${S3_SECRET_ACCESS_KEY__FILE}"
+ export S3_SECRET_ACCESS_KEY
+fi
+
+# OIDC_CLIENT_SECRET
+if [[ -z "${OIDC_CLIENT_SECRET:-}" && -e "${OIDC_CLIENT_SECRET__FILE:-}" ]]; then
+ read OIDC_CLIENT_SECRET < "${OIDC_CLIENT_SECRET__FILE}"
+ export OIDC_CLIENT_SECRET
+fi
+
+# SMTP_PASSWORD
+if [[ -z "${SMTP_PASSWORD:-}" && -e "${SMTP_PASSWORD__FILE:-}" ]]; then
+ read SMTP_PASSWORD < "${SMTP_PASSWORD__FILE}"
+ export SMTP_PASSWORD
+fi
+
+export NODE_ENV=production
+
+node db/init.js
+exec node app.js --prod
From f43785c3d05c923bd513190a1b2895b02eae1358 Mon Sep 17 00:00:00 2001
From: Hannes
Date: Fri, 16 May 2025 11:18:12 +0200
Subject: [PATCH 003/108] fix: Update German translation (#1134)
---
client/src/locales/de-DE/core.js | 153 ++++++++++++++--
client/src/locales/de-DE/login.js | 6 +
client/src/locales/de-DE/markdown-editor.json | 165 ++++++++++++++++++
3 files changed, 313 insertions(+), 11 deletions(-)
create mode 100644 client/src/locales/de-DE/markdown-editor.json
diff --git a/client/src/locales/de-DE/core.js b/client/src/locales/de-DE/core.js
index ac11fedb..022e3ffc 100644
--- a/client/src/locales/de-DE/core.js
+++ b/client/src/locales/de-DE/core.js
@@ -1,18 +1,20 @@
import dateFns from 'date-fns/locale/de';
import timeAgo from 'javascript-time-ago/locale/de';
+import markdownEditor from './markdown-editor.json';
export default {
dateFns,
timeAgo,
+ markdownEditor,
format: {
date: 'd.MM.yyyy',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
- longDate: 'd. MMM',
- longDateTime: "d. MMMM yy 'um' p",
fullDate: 'd. MMM. y',
fullDateTime: "d. MMMM. y 'um' p",
+ longDate: 'd. MMM',
+ longDateTime: "d. MMMM yy 'um' p",
},
translation: {
@@ -20,23 +22,44 @@ export default {
aboutPlanka: 'Über PLANKA',
account: 'Konto',
actions: 'Aktionen',
+ activateUser_title: 'Benutzer aktivieren',
active: 'Aktiv',
addAttachment_title: 'Anhang hinzufügen',
addCustomField_title: 'Datenfeld hinzufügen',
addCustomFieldGroup_title: 'Feldgruppe hinzufügen',
addManager_title: 'Projektleiter hinzufügen',
addMember_title: 'Mitglied hinzufügen',
+ addTaskList_title: 'Aufgabenliste hinzufügen',
addUser_title: 'Benutzer hinzufügen',
admin: 'Administrator',
administration: 'Verwaltung',
all: 'Alle',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Alle Änderungen werden automatisch gespeichert, sobald die Verbindung wiederhergestellt wurde.',
+ alphabetically: 'Alphabetisch',
+ alwaysDisplayCardCreator: 'Kartenersteller immer anzeigen',
+ archive: 'Archiv',
+ archiveCard_title: 'Karte archivieren',
+ archiveCards_title: 'Karten archivieren',
+ areYouSureYouWantToActivateThisUser:
+ 'Sind Sie sicher, dass Sie diesen Benutzer aktivieren möchten?',
+ areYouSureYouWantToArchiveCards:
+ 'Sind Sie sicher, dass Sie diese Karten archivieren möchten?',
+ areYouSureYouWantToArchiveThisCard:
+ 'Sind Sie sicher, dass Sie diese Karte archivieren möchten?',
+ areYouSureYouWantToAssignThisProjectManagerAsOwner:
+ 'Sind Sie sicher, dass Sie diesen Projektleiter als Eigentümer festlegen möchten?',
+ areYouSureYouWantToDeactivateThisUser:
+ 'Sind Sie sicher, dass Sie diesen Benutzer deaktivieren möchten?',
areYouSureYouWantToDeleteThisAttachment:
'Sind Sie sicher, dass Sie diesen Anhang löschen möchten?',
+ areYouSureYouWantToDeleteThisBackgroundImage:
+ 'Sind Sie sicher, dass Sie dieses Hintergrundbild löschen möchten?',
areYouSureYouWantToDeleteThisBoard:
'Sind Sie sicher, dass Sie diesen Arbeitsbereich löschen möchten?',
areYouSureYouWantToDeleteThisCard: 'Sind Sie sicher, dass Sie diese Karte löschen möchten?',
+ areYouSureYouWantToDeleteThisCardForever:
+ 'Sind Sie sicher, dass Sie diese Karte endgültig löschen möchten?',
areYouSureYouWantToDeleteThisComment:
'Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?',
areYouSureYouWantToDeleteThisCustomField:
@@ -45,18 +68,27 @@ export default {
'Sind Sie sicher, dass Sie diese Feldgruppe löschen möchten?',
areYouSureYouWantToDeleteThisLabel: 'Sind Sie sicher, dass Sie dieses Label löschen möchten?',
areYouSureYouWantToDeleteThisList: 'Sind Sie sicher, dass Sie diese Liste löschen möchten?',
+ areYouSureYouWantToDeleteThisNotificationService:
+ 'Sind Sie sicher, dass Sie diesen Benachrichtigungsdienst löschen möchten?',
areYouSureYouWantToDeleteThisProject:
'Sind Sie sicher, dass Sie dieses Projekt löschen möchten?',
areYouSureYouWantToDeleteThisTask: 'Sind Sie sicher, dass Sie diese Aufgabe löschen möchten?',
+ areYouSureYouWantToDeleteThisTaskList:
+ 'Sind Sie sicher, dass Sie diese Aufgabenliste löschen möchten?',
areYouSureYouWantToDeleteThisUser:
'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?',
+ areYouSureYouWantToEmptyTrash: 'Sind Sie sicher, dass Sie den Papierkorb leeren möchten?',
areYouSureYouWantToLeaveBoard:
'Sind Sie sicher, dass Sie den Arbeitsbereich verlassen möchten?',
areYouSureYouWantToLeaveProject: 'Sind Sie sicher, dass Sie das Projekt verlassen möchten?',
+ areYouSureYouWantToMakeThisProjectShared:
+ 'Sind Sie sicher, dass Sie dieses Projekt freigeben möchten?',
areYouSureYouWantToRemoveThisManagerFromProject:
'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?',
+ assignAsOwner_title: 'Als Eigentümer zuweisen',
+ atLeastOneListMustBePresent: 'Mindestens eine Liste muss vorhanden sein',
attachment: 'Anhang',
attachments: 'Anhänge',
authentication: 'Authentifizierung',
@@ -64,8 +96,13 @@ export default {
baseCustomFields_title: 'Feldgruppe',
baseGroup: 'Feldgruppe',
board: 'Arbeitsbereich',
+ boardActions_title: 'Arbeitsbereich-Aktionen',
boardNotFound_title: 'Arbeitsbereich nicht gefunden',
+ boardSubscribed: 'Arbeitsbereich abonniert',
boardUser: 'Benutzer',
+ byCreationTime: 'Nach Erstellungszeit',
+ byDefault: 'Standardmäßig',
+ byDueDate: 'Nach Fälligkeitsdatum',
canBeInvitedToWorkInBoards: 'Kann zu Projekten und Boards eingeladen werden.',
canComment: 'Kann kommentieren',
canCreateOwnProjectsAndBeInvitedToWorkInOthers:
@@ -79,37 +116,57 @@ export default {
cardNotFound_title: 'Karte nicht gefunden',
cardsOnThisListAreAvailableToAllBoardMembers:
'Cards on this list are available to all Arbeitsbereich members.',
+ cardsOnThisListAreCompleteAndReadyToBeArchived:
+ 'Karten in dieser Liste sind abgeschlossen und können archiviert werden.',
+ cardsOnThisListAreReadyToBeWorkedOn: 'Karten in dieser Liste sind bereit zur Bearbeitung.',
+ clickHereOrRefreshPageToUpdate:
+ '<0>Hier klicken0> oder Seite aktualisieren, um zu aktualisieren.',
closed: 'Geschlossen',
color: 'Farbe',
+ comments: 'Kommentare',
+ contentExceedsLimit: 'Inhalt überschreitet {{limit}}',
copy_inline: 'kopieren',
createBoard_title: 'Arbeitsbereich erstellen',
createCustomFieldGroup_title: 'Feldgruppe erstellen',
createLabel_title: 'Label erstellen',
- createNewOneOrSelectExistingOne: 'Create a new one or select an existing one.',
+ createNewOneOrSelectExistingOne: 'Create a new one or select an existing one.',
createProject_title: 'Projekt erstellen',
createTextFile_title: 'Textdatei erstellen',
+ creator: 'Ersteller',
currentPassword: 'Derzeitiges Passwort',
customField_title: 'Feldgruppe',
- customFields_title: 'Feldgruppen',
customFieldGroup_title: 'Feldgruppe',
+ customFieldGroups_title: 'Benutzerdefinierte Feldgruppen',
+ customFields_title: 'Feldgruppen',
dangerZone_title: 'Gefahrenbereich',
date: 'Datum',
+ deactivateUser_title: 'Benutzer deaktivieren',
+ defaultCardType_title: 'Standard-Kartentyp',
+ defaultView_title: 'Standardansicht',
+ deleteAllBoardsToBeAbleToDeleteThisProject:
+ 'Löschen Sie alle Arbeitsbereiche, um dieses Projekt löschen zu können',
deleteAttachment_title: 'Anhang löschen',
+ deleteBackgroundImage_title: 'Hintergrundbild löschen',
deleteBoard_title: 'Arbeitsbereich löschen',
deleteCard_title: 'Karte löschen',
+ deleteCardForever_title: 'Karte endgültig löschen',
deleteComment_title: 'Kommentar löschen',
deleteCustomField_title: 'Datenfeld löschen',
deleteCustomFieldGroup_title: 'Feldgruppe löschen',
+ deletedUser_title: 'Gelöschter Benutzer',
deleteLabel_title: 'Label löschen',
deleteList_title: 'Liste löschen',
+ deleteNotificationService_title: 'Benachrichtigungsdienst löschen',
deleteProject_title: 'Projekt löschen',
deleteTask_title: 'Aufgabe löschen',
+ deleteTaskList_title: 'Aufgabenliste löschen',
deleteUser_title: 'Benutzer löschen',
description: 'Beschreibung',
detectAutomatically: 'Automatisch erkennen',
+ display: 'Anzeige',
dropFileToUpload: 'Datei für Upload ablegen',
dueDate_title: 'Fälligkeitsdatum',
- editor: 'Verwalter',
+ dynamicAndUnevenlySpacedLayout: 'Dynamisches und ungleichmäßig verteiltes Layout.',
editAttachment_title: 'Anhang bearbeiten',
editAvatar_title: 'Profilbild bearbeiten',
editColor_title: 'Farbe bearbeiten',
@@ -125,7 +182,10 @@ export default {
editStopwatch_title: 'Stoppuhr bearbeiten',
editType_title: 'Typ ändern',
editUsername_title: 'Benutzername ändern',
+ editor: 'Bearbeiter',
+ editors: 'Bearbeiter',
email: 'E-mail',
+ emptyTrash_title: 'Papierkorb leeren',
enterCardTitle: 'Kartentitel eingeben...',
enterDescription: 'Beschreibung eingeben...',
enterFilename: 'Dateiname eingeben',
@@ -133,43 +193,58 @@ export default {
enterTaskDescription: 'Aufgabenbeschreibung eingeben...',
filterByLabels_title: 'Nach Label filtern',
filterByMembers_title: 'Nach Mitgliedern filtern',
+ forPersonalProjects: 'Für persönliche Projekte.',
+ forTeamBasedProjects: 'Für teambasierte Projekte.',
fromComputer_title: 'Vom Computer',
fromTrello: 'Von Trello',
general: 'Allgemein',
+ gradients: 'Verläufe',
+ grid: 'Raster',
+ hideFromProjectListAndFavorites: 'Aus Projektliste und Favoriten ausblenden',
hours: 'Stunden',
importBoard_title: 'Board importieren',
invalidCurrentPassword: 'Das aktuelle Passwort ist falsch',
+ kanban: 'Kanban',
labels: 'Labels',
language: 'Sprache',
leaveBoard_title: 'Arbeitsbereich verlassen',
leaveProject_title: 'Projekt verlassen',
+ limitCardTypesToDefaultOne: 'Kartentypen auf Standardtyp beschränken',
list: 'Liste',
- lists: 'Listen',
listActions_title: 'Aufgaben auflisten',
+ lists: 'Listen',
+ makeProjectShared_title: 'Projekt freigeben',
+ managerActions_title: 'Projektleiteraktionen',
managers: 'Projektleiter',
- members: 'Mitglieder',
memberActions_title: 'Mitglieder-Aktionen',
+ members: 'Mitglieder',
minutes: 'Minuten',
moveCard_title: 'Karte verschieben',
+ myOwn_title: 'Meine eigenen',
name: 'Name',
- newestFirst: 'Neueste zuerst',
newEmail: 'Neue E-Mail-Adresse',
newPassword: 'Neues Passwort',
newUsername: 'Neuer Benutzername',
- noConnectionToServer: 'Keine Verbindung zum Server',
+ newVersionAvailable: 'Neue Version verfügbar',
+ newestFirst: 'Neueste zuerst',
noBoards: 'Keine Arbeitsbereiche',
+ noConnectionToServer: 'Keine Verbindung zum Server',
noLists: 'Keine Listen',
noProjects: 'Keine Projekte',
- notifications: 'Benachrichtigungen',
noUnreadNotifications: 'Keine ungelesenen Benachrichtigungen.',
+ notifications: 'Benachrichtigungen',
oldestFirst: 'Älteste zuerst',
openBoard_title: 'Arbeitsbereich öffnen',
optional_inline: 'Optional',
organization: 'Organisation',
+ others: 'Andere',
phone: 'Telefon',
+ plankaUsesAppriseToSendNotificationsToOver100PopularServices:
+ 'PLANKA verwendet <1><0>Apprise0>1>, um Benachrichtigungen an über 100 beliebte Dienste zu senden.',
preferences: 'Voreinstellungen',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tipp: Drücken Sie STRG-V (Cmd-V auf Mac), um einen Anhang aus der Zwischenablage hinzuzufügen.',
+ private: 'Privat',
project: 'Projekt',
projectNotFound_title: 'Projekt nicht gefunden',
projectOwner: 'Projektleitung',
@@ -178,28 +253,48 @@ export default {
removeMember_title: 'Mitglied entfernen',
role: 'Rolle',
searchCards: 'Karte suchen...',
+ searchCustomFieldGroups: 'Benutzerdefinierte Feldgruppen suchen...',
searchCustomFields: 'In Feldgruppen suchen...',
searchLabels: 'Label suchen...',
searchLists: 'Liste suchen...',
searchMembers: 'Mitglied suchen...',
+ searchProjects: 'Projekte suchen...',
searchUsers: 'Benutzer suchen...',
seconds: 'Sekunden',
+ selectAssignee_title: 'Zuständigen auswählen',
selectBoard: 'Arbeitsbereich auswählen',
selectList: 'Liste auswählen',
+ selectListToRestoreThisCard: 'Liste auswählen, um diese Karte wiederherzustellen',
+ selectOrder_title: 'Reihenfolge auswählen',
selectPermissions_title: 'Berechtigungen auswählen',
selectProject: 'Projekt auswählen',
selectRole_title: 'Rolle auswählen',
selectType_title: 'Typ auswählen',
+ sequentialDisplayOfCards: 'Sequenzielle Anzeige von Karten.',
settings: 'Einstellungen',
+ shared: 'Geteilt',
+ sharedWithMe_title: 'Mit mir geteilt',
+ showOnFrontOfCard: 'Auf der Vorderseite der Karte anzeigen',
sortList_title: 'Liste sortieren',
stopwatch: 'Stoppuhr',
story: 'Wissen',
+ subscribeToCardWhenCommenting: 'Karte beim Kommentieren abonnieren',
subscribeToMyOwnCardsByDefault: 'Standardmäßig meine eigenen Karten abonnieren',
taskActions_title: 'Aufgabenaktionen',
taskAssignmentAndProjectCompletion: 'Verwaltung von Aufgaben und Projekten.',
+ taskList_title: 'Aufgabenliste',
+ taskListActions_title: 'Aufgabenlisten-Aktionen',
+ team: 'Team',
thereIsNoPreviewAvailableForThisAttachment: 'Für diesen Anhang ist keine Vorschau verfügbar.',
time: 'Zeit',
title: 'Titel',
+ trash: 'Papierkorb',
+ trashHasBeenSuccessfullyEmptied: 'Papierkorb wurde erfolgreich geleert.',
+ turnOffRecentCardHighlighting: 'Hervorhebung neuer Karten ausschalten',
+ typeNameToConfirm: 'Namen zur Bestätigung eingeben.',
+ typeTitleToConfirm: 'Titel zur Bestätigung eingeben.',
+ unsavedChanges: 'Ungespeicherte Änderungen',
+ uploadedImages: 'Hochgeladene Bilder',
userActions_title: 'Benutzeraktionen',
userAddedThisCardToList: '<0>{{user}}0><1> hat diese Karte hinzugefügt zu {{list}}1>',
userLeftNewCommentToCard:
@@ -211,10 +306,15 @@ export default {
username: 'Benutzername',
users: 'Benutzer',
viewer: 'Betrachter',
+ viewers: 'Betrachter',
+ visualTaskManagementWithLists: 'Visuelle Aufgabenverwaltung mit Listen.',
+ withoutBaseGroup: 'Ohne Basisgruppe',
writeComment: 'Kommentar verfassen...',
},
action: {
+ activateUser: 'Benutzer aktivieren',
+ activateUser_title: 'Benutzer aktivieren',
addAnotherCard: 'Karte hinzufügen',
addAnotherList: 'Liste hinzufügen',
addAnotherTask: 'Aufgabe hinzufügen',
@@ -227,36 +327,56 @@ export default {
addMember: 'Mitglied hinzufügen',
addMoreDetailedDescription: 'Eine detaillierte Beschreibung hinzufügen',
addTask: 'Aufgabe hinzufügen',
+ addTaskList: 'Aufgabenliste hinzufügen',
addToCard: 'Zu Karte hinzufügen',
addUser: 'Benutzer hinzufügen',
+ archive: 'Archivieren',
+ archiveCard: 'Karte archivieren',
+ archiveCard_title: 'Karte archivieren',
+ archiveCards: 'Karten archivieren',
+ archiveCards_title: 'Karten archivieren',
+ assignAsOwner: 'Als Eigentümer zuweisen',
+ cancel: 'Abbrechen',
createBoard: 'Arbeitsbereich erstellen',
createCustomFieldGroup: 'Feldgruppe erstellen',
createFile: 'Datei erstellen',
createLabel: 'Label erstellen',
createNewLabel: 'Neues Label erstellen',
createProject: 'Projekt erstellen',
+ deactivateUser: 'Benutzer deaktivieren',
+ deactivateUser_title: 'Benutzer deaktivieren',
delete: 'Löschen',
deleteAttachment: 'Anhang löschen',
deleteAvatar: 'Avatar löschen',
+ deleteBackgroundImage: 'Hintergrundbild löschen',
deleteBoard: 'Arbeitsbereich löschen',
+ deleteBoard_title: 'Arbeitsbereich löschen',
deleteCard: 'Karte löschen',
deleteCard_title: 'Karte löschen',
+ deleteCardForever: 'Karte endgültig löschen',
deleteComment: 'Kommentar löschen',
deleteCustomField: 'Datenfeld löschen',
deleteCustomFieldGroup: 'Feldgruppe löschen',
+ deleteForever_title: 'Endgültig löschen',
deleteGroup: 'Gruppe löschen',
deleteLabel: 'Label löschen',
deleteList: 'Liste löschen',
deleteList_title: 'Liste löschen',
+ deleteNotificationService: 'Benachrichtigungsdienst löschen',
deleteProject: 'Projekt löschen',
deleteProject_title: 'Projekt löschen',
deleteTask: 'Aufgabe löschen',
deleteTask_title: 'Aufgabe löschen',
+ deleteTaskList: 'Aufgabenliste löschen',
deleteUser: 'Benutzer löschen',
+ deleteUser_title: 'Benutzer löschen',
+ dismissAll: 'Alle verwerfen',
+ duplicate: 'Duplizieren',
+ duplicateCard_title: 'Karte duplizieren',
edit: 'Bearbeiten',
editColor_title: 'Farbe bearbeiten',
- editDueDate_title: 'Fälligkeitsdatum bearbeiten',
editDescription_title: 'Beschreibung ändern',
+ editDueDate_title: 'Fälligkeitsdatum bearbeiten',
editEmail_title: 'E-Mail-Adresse bearbeiten',
editGroup: 'Gruppe bearbeiten',
editInformation_title: 'Informationen bearbeiten',
@@ -267,23 +387,34 @@ export default {
editTitle_title: 'Titel bearbeiten',
editType_title: 'Typ ändern ',
editUsername_title: 'Benutzername ändern',
+ emptyTrash: 'Papierkorb leeren',
+ emptyTrash_title: 'Papierkorb leeren',
import: 'Import',
+ join: 'Beitreten',
+ leave: 'Verlassen',
leaveBoard: 'Arbeitsbereich verlassen',
leaveProject: 'Projekt verlassen',
logOut_title: 'Ausloggen',
makeCover_title: 'Als Vorschau festlegen',
+ makeProjectShared: 'Projekt freigeben',
+ makeProjectShared_title: 'Projekt freigeben',
move: 'Verschieben',
moveCard_title: 'Karte bewegen',
remove: 'Löschen',
+ removeAssignee: 'Zuständigen entfernen',
removeColor: 'Farbe löschen',
removeCover_title: 'Vorschau löschen',
removeFromBoard: 'Vom Arbeitsbereich entfernen',
removeFromProject: 'Vom Projekt entfernen',
removeManager: 'Projektleiter entfernen',
removeMember: 'Mitglied entfernen',
+ restoreToList: 'Wiederherstellen in {{list}}',
+ returnToBoard: 'Zurück zum Arbeitsbereich',
save: 'Speichern',
+ showActive: 'Aktive anzeigen',
showAllAttachments: 'Alle Anhänge anzeigen ({{hidden}} versteckt)',
showCardsWithThisUser: 'Karten mit diesem Benutzer zeigen',
+ showDeactivated: 'Deaktivierte anzeigen',
showFewerAttachments: 'Weniger Anhänge anzeigen',
sortList_title: 'Liste sortieren',
start: 'Start',
diff --git a/client/src/locales/de-DE/login.js b/client/src/locales/de-DE/login.js
index bf35cc7a..e357d0b5 100644
--- a/client/src/locales/de-DE/login.js
+++ b/client/src/locales/de-DE/login.js
@@ -1,21 +1,27 @@
export default {
translation: {
common: {
+ activeUsersLimitReached: 'Maximale Anzahl aktiver Benutzer erreicht',
emailAlreadyInUse: 'E-mail Adresse wird bereits benutzt',
emailOrUsername: 'E-Mail-Adresse oder Benutzername',
+ invalidCredentials: 'Ungültige Anmeldeinformationen',
invalidEmailOrUsername: 'Ungültige E-Mail-Adresse oder Benutzername',
invalidPassword: 'Ungültiges Passwort',
logIn_title: 'Anmelden',
noInternetConnection: 'Keine Internetverbindung',
+ or: 'Oder',
pageNotFound_title: 'Seite nicht gefunden',
password: 'Passwort',
+ poweredByPlanka: 'Powered by PLANKA',
serverConnectionFailed: 'Serververbindung fehlgeschlagen',
unknownError: 'Unbekannter Fehler, bitte später erneut versuchen',
usernameAlreadyInUse: 'Benutzername wird bereits verwendet',
+ useSingleSignOn: 'Einmalige Anmeldung (SSO) verwenden',
},
action: {
logIn: 'Einloggen',
+ logInWithSso: 'Einloggen mit SSO',
},
},
};
diff --git a/client/src/locales/de-DE/markdown-editor.json b/client/src/locales/de-DE/markdown-editor.json
new file mode 100644
index 00000000..e979d30d
--- /dev/null
+++ b/client/src/locales/de-DE/markdown-editor.json
@@ -0,0 +1,165 @@
+{
+ "action-previews": {
+ "text": "Dies ist ein Text ohne Titel.\nSowohl der Titel als auch der Text\nkönnen fett, kursiv, farbig,\ndurchgestrichen und unterstrichen hervorgehoben werden.",
+ "text-with-head": "Dies ist ein Text mit einem Titel.\nSowohl der Titel als auch der Text\nkönnen fett, kursiv, farbig,\ndurchgestrichen und unterstrichen hervorgehoben werden.",
+ "heading": "Titel"
+ },
+ "bundle": {
+ "error-title": "Fehler im Markdown-Editor",
+ "settings_wysiwyg": "Visueller Editor (WYSIWYG)",
+ "settings_markup": "Markdown-Auszeichnung",
+ "markup_placeholder": "Markdown-Auszeichnung eingeben..."
+ },
+ "codeblock": {
+ "remove": "Entfernen",
+ "empty_option": "Keine Übereinstimmungen gefunden"
+ },
+ "common": {
+ "delete": "Löschen",
+ "edit": "Bearbeiten",
+ "toolbar_action_disabled": "Inkompatibles Markup-Element"
+ },
+ "forms": {
+ "common_action_cancel": "Abbrechen",
+ "common_action_submit": "Absenden",
+ "common_action_upload": "Auswählen",
+ "common_tab_attach": "Vom Gerät hinzufügen",
+ "common_tab_link": "Per Link hinzufügen",
+ "common_link": "Link",
+ "common_sizes": "Größe, px",
+ "image_name": "Titel",
+ "image_link_href": "Bildlink",
+ "image_link_href_help": "Adresse, zu der der Bildlink führt.",
+ "image_alt": "Alternativtext",
+ "image_alt_help": "Der Alternativtext wird angezeigt, wenn das Bild nicht geladen werden kann.",
+ "image_upload_help": "JPEG-, GIF- oder PNG-Bild mit maximal 1 MB.",
+ "image_upload_failed": "Bild konnte nicht hinzugefügt werden",
+ "image_size_width": "Breite",
+ "image_size_height": "Höhe",
+ "link_url_help": "Adresse, zu der der Link führt.",
+ "link_text": "Linktext",
+ "link_text_help": "Text, der als Link angezeigt wird.",
+ "link_open_help": "Link in neuem Tab öffnen"
+ },
+ "md-hints": {
+ "header_title": "Überschrift",
+ "header_hint": "# Dein Text",
+ "italic_title": "Kursiv",
+ "italic_hint": "_Dein Text_",
+ "bold_title": "Fett",
+ "bold_hint": "**Dein Text**",
+ "strikethrough_title": "Durchgestrichen",
+ "strikethrough_hint": "~~Dein Text~~",
+ "blockquote_title": "Blockzitat",
+ "blockquote_hint": "> Dein Text",
+ "code_title": "Code",
+ "code_hint": "```Dein Text```",
+ "link_title": "Link",
+ "link_hint": "[Dein Text](URL)",
+ "image_title": "Bild",
+ "image_hint": "",
+ "list_title": "Listenelement",
+ "list_hint": "- Dein Text",
+ "numbered-list_title": "Nummerierte Liste",
+ "numbered-list_hint": "1. Dein Text",
+ "documentation": "Dokumentation",
+ "documentation_link": " https://diplodoc.com/docs/en/syntax/"
+ },
+ "menubar": {
+ "bold": "Fett",
+ "code": "Code",
+ "code_inline": "Inline-Code",
+ "codeblock": "Codeblock",
+ "colorify": "Textfarbe",
+ "colorify__color_blue": "Blau",
+ "colorify__color_default": "Standard",
+ "colorify__color_gray": "Grau",
+ "colorify__color_green": "Grün",
+ "colorify__color_orange": "Orange",
+ "colorify__color_red": "Rot",
+ "colorify__color_violet": "Violett",
+ "colorify__color_yellow": "Gelb",
+ "colorify__group_text": "Text",
+ "cut": "Ausschneiden",
+ "emoji": "Emoji",
+ "emoji__hint": "Emojis können im WYSIWYG-Editor hinzugefügt oder manuell mit Markup eingefügt werden",
+ "heading": "Überschrift",
+ "heading1": "Überschrift 1",
+ "heading2": "Überschrift 2",
+ "heading3": "Überschrift 3",
+ "heading4": "Überschrift 4",
+ "heading5": "Überschrift 5",
+ "heading6": "Überschrift 6",
+ "hrule": "Trennlinie",
+ "image": "Bild",
+ "italic": "Kursiv",
+ "link": "Link",
+ "list": "Liste",
+ "list__action_lift": "Element anheben",
+ "list__action_sink": "Element einrücken",
+ "list_action_disabled": "Widerspricht der Logik der Liste",
+ "mark": "Markiert",
+ "mono": "Monospace",
+ "more_action": "Weitere Aktionen",
+ "note": "Hinweis",
+ "olist": "Geordnete Liste",
+ "quote": "Zitat",
+ "redo": "Wiederherstellen",
+ "strike": "Durchgestrichen",
+ "table": "Tabelle",
+ "text": "Text",
+ "ulist": "Aufzählungsliste",
+ "underline": "Unterstrichen",
+ "undo": "Rückgängig"
+ },
+ "placeholder": {
+ "doc_empty": "Tippe / für Slashbefehle...",
+ "checkbox": "Aufgabenbeschreibung eingeben...",
+ "deflist_term": "Begriff",
+ "deflist_desc": "Definitionsbeschreibung",
+ "heading": "Überschrift",
+ "cut_title": "Titel",
+ "cut_content": "Inhalt, der beim Klicken angezeigt wird",
+ "note_title": "Titel",
+ "note_content": "Hinweisinhalt",
+ "table_cell": "Zelleninhalt",
+ "select_filter": "Sprachen durchsuchen..."
+ },
+ "search": {
+ "label_case-sensitive": "Groß-/Kleinschreibung beachten",
+ "label_whole-word": "Ganzes Wort",
+ "title": "Im Code suchen"
+ },
+ "suggest": {
+ "empty-msg": "Nicht gefunden"
+ },
+ "widgets": {
+ "image": "Bild hinzufügen",
+ "link": "Link hinzufügen"
+ },
+ "yfm-note": {
+ "info": "Hinweis",
+ "tip": "Tipp",
+ "warning": "Warnung",
+ "alert": "Alarm",
+ "remove": "Entfernen"
+ },
+ "yfm-table": {
+ "column.add.before": "Spalte davor einfügen",
+ "column.add.after": "Spalte danach einfügen",
+ "column.remove": "Spalte entfernen",
+ "row.add.before": "Zeile davor einfügen",
+ "row.add.after": "Zeile danach einfügen",
+ "row.remove": "Zeile entfernen",
+ "table.remove": "Tabelle entfernen",
+ "table.menu.cell.align.left": "Zelleninhalt linksbündig ausrichten",
+ "table.menu.cell.align.right": "Zelleninhalt rechtsbündig ausrichten",
+ "table.menu.cell.align.center": "Zelleninhalt zentrieren",
+ "table.menu.row.add": "Zeile danach einfügen",
+ "table.menu.row.remove": "Zeile entfernen",
+ "table.menu.column.add": "Spalte danach einfügen",
+ "table.menu.column.remove": "Spalte entfernen",
+ "table.menu.convert.yfm": "In YFM-Tabelle umwandeln",
+ "table.menu.table.remove": "Tabelle entfernen"
+ }
+}
From f6568ce41b0e759254bf67419d7bfab2880a7be0 Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Sat, 17 May 2025 01:50:40 +0200
Subject: [PATCH 004/108] feat: Add notification when user is added to card
---
.../components/activities/Activities/Item.jsx | 30 +++++++
.../notifications/NotificationsStep/Item.jsx | 18 ++++
client/src/constants/Enums.js | 2 +
client/src/locales/en-GB/core.js | 3 +
client/src/locales/en-US/core.js | 3 +
.../controllers/card-memberships/create.js | 10 ++-
server/api/helpers/actions/create-one.js | 87 ++++++++++++-------
.../helpers/card-memberships/create-one.js | 20 ++++-
.../api/helpers/notifications/create-one.js | 27 ++++++
server/api/models/Action.js | 1 +
server/api/models/Notification.js | 1 +
server/config/locales/en-GB.json | 2 +
server/config/locales/en-US.json | 2 +
server/config/locales/ru-RU.json | 2 +
14 files changed, 169 insertions(+), 39 deletions(-)
diff --git a/client/src/components/activities/Activities/Item.jsx b/client/src/components/activities/Activities/Item.jsx
index 7768f415..181e5970 100755
--- a/client/src/components/activities/Activities/Item.jsx
+++ b/client/src/components/activities/Activities/Item.jsx
@@ -84,6 +84,36 @@ const Item = React.memo(({ id }) => {
break;
}
+ case ActivityTypes.ADD_MEMBER_TO_CARD:
+ contentNode =
+ user.id === activity.data.user.id ? (
+
+ {userName}
+ {' joined this card'}
+
+ ) : (
+
+ {userName}
+
+ {' added '}
+ {activity.data.user.name}
+ {' to this card'}
+
+
+ );
+
+ break;
default:
contentNode = null;
}
diff --git a/client/src/components/notifications/NotificationsStep/Item.jsx b/client/src/components/notifications/NotificationsStep/Item.jsx
index 7679fddc..69c65b5b 100644
--- a/client/src/components/notifications/NotificationsStep/Item.jsx
+++ b/client/src/components/notifications/NotificationsStep/Item.jsx
@@ -103,6 +103,24 @@ const Item = React.memo(({ id, onClose }) => {
break;
}
+ case NotificationTypes.ADD_MEMBER_TO_CARD:
+ contentNode = (
+
+ {creatorUserName}
+ {` added you to `}
+
+ {cardName}
+
+
+ );
+
+ break;
default:
contentNode = null;
}
diff --git a/client/src/constants/Enums.js b/client/src/constants/Enums.js
index 219d10b0..0cfcf20a 100755
--- a/client/src/constants/Enums.js
+++ b/client/src/constants/Enums.js
@@ -90,11 +90,13 @@ export const AttachmentTypes = {
export const ActivityTypes = {
CREATE_CARD: 'createCard',
MOVE_CARD: 'moveCard',
+ ADD_MEMBER_TO_CARD: 'addMemberToCard',
};
export const NotificationTypes = {
MOVE_CARD: 'moveCard',
COMMENT_CARD: 'commentCard',
+ ADD_MEMBER_TO_CARD: 'addMemberToCard',
};
export const NotificationServiceFormats = {
diff --git a/client/src/locales/en-GB/core.js b/client/src/locales/en-GB/core.js
index f5c256d8..d19b63a2 100644
--- a/client/src/locales/en-GB/core.js
+++ b/client/src/locales/en-GB/core.js
@@ -287,6 +287,9 @@ export default {
uploadedImages: 'Uploaded images',
userActions_title: 'User Actions',
userAddedThisCardToList: '<0>{{user}}0><1> added this card to {{list}}1>',
+ userAddedUserToThisCard: '<0>{{actorUser}}0><1> added {{addedUser}} to this card1>',
+ userAddedYouToCard: '{{user}} added you to <2>{{card}}2>',
+ userJoinedThisCard: `<0>{{user}}0><1> joined this card1>`,
userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}2>',
userMovedCardFromListToList: '{{user}} moved <2>{{card}}2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:
diff --git a/client/src/locales/en-US/core.js b/client/src/locales/en-US/core.js
index 0b0345e8..468eb945 100644
--- a/client/src/locales/en-US/core.js
+++ b/client/src/locales/en-US/core.js
@@ -282,6 +282,9 @@ export default {
uploadedImages: 'Uploaded images',
userActions_title: 'User Actions',
userAddedThisCardToList: '<0>{{user}}0><1> added this card to {{list}}1>',
+ userAddedUserToThisCard: '<0>{{actorUser}}0><1> added {{addedUser}} to this card1>',
+ userAddedYouToCard: '{{user}} added you to <2>{{card}}2>',
+ userJoinedThisCard: `<0>{{user}}0><1> joined this card1>`,
userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}2>',
userMovedCardFromListToList: '{{user}} moved <2>{{card}}2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:
diff --git a/server/api/controllers/card-memberships/create.js b/server/api/controllers/card-memberships/create.js
index 5d3aa4c7..c5750764 100755
--- a/server/api/controllers/card-memberships/create.js
+++ b/server/api/controllers/card-memberships/create.js
@@ -67,7 +67,13 @@ module.exports = {
throw Errors.NOT_ENOUGH_RIGHTS;
}
- const isBoardMember = await sails.helpers.users.isBoardMember(inputs.userId, board.id);
+ const user = await User.qm.getOneById(inputs.userId);
+
+ if (!user) {
+ throw Errors.USER_NOT_FOUND;
+ }
+
+ const isBoardMember = await sails.helpers.users.isBoardMember(user.id, board.id);
if (!isBoardMember) {
throw Errors.USER_NOT_FOUND;
@@ -80,7 +86,7 @@ module.exports = {
list,
values: {
card,
- userId: inputs.userId,
+ user,
},
actorUser: currentUser,
request: this.req,
diff --git a/server/api/helpers/actions/create-one.js b/server/api/helpers/actions/create-one.js
index 8aa73fd3..ca23d358 100644
--- a/server/api/helpers/actions/create-one.js
+++ b/server/api/helpers/actions/create-one.js
@@ -143,55 +143,76 @@ module.exports = {
});
if (action.type !== Action.Types.CREATE_CARD) {
- const cardSubscriptionUserIds = await sails.helpers.cards.getSubscriptionUserIds(
- action.cardId,
- action.userId,
- );
-
- const boardSubscriptionUserIds = await sails.helpers.boards.getSubscriptionUserIds(
- inputs.board.id,
- action.userId,
- );
-
- const notifiableUserIds = _.union(cardSubscriptionUserIds, boardSubscriptionUserIds);
-
- await Promise.all(
- notifiableUserIds.map((userId) =>
- sails.helpers.notifications.createOne.with({
+ if (action.type === Action.Types.ADD_MEMBER_TO_CARD) {
+ if (values.user !== action.data.user.id) {
+ await sails.helpers.notifications.createOne.with({
values: {
- userId,
action,
type: action.type,
data: {
...action.data,
card: _.pick(values.card, ['name']),
},
+ userId: action.data.user.id,
creatorUser: values.user,
card: values.card,
},
project: inputs.project,
board: inputs.board,
list: inputs.list,
- }),
- ),
- );
- }
+ });
+ }
+ } else {
+ const cardSubscriptionUserIds = await sails.helpers.cards.getSubscriptionUserIds(
+ action.cardId,
+ action.userId,
+ );
- const notificationServices = await NotificationService.qm.getByBoardId(inputs.board.id);
+ const boardSubscriptionUserIds = await sails.helpers.boards.getSubscriptionUserIds(
+ inputs.board.id,
+ action.userId,
+ );
- if (notificationServices.length > 0) {
- const services = notificationServices.map((notificationService) =>
- _.pick(notificationService, ['url', 'format']),
- );
+ const notifiableUserIds = _.union(cardSubscriptionUserIds, boardSubscriptionUserIds);
- buildAndSendNotifications(
- services,
- inputs.board,
- values.card,
- action,
- values.user,
- sails.helpers.utils.makeTranslator(),
- );
+ await Promise.all(
+ notifiableUserIds.map((userId) =>
+ sails.helpers.notifications.createOne.with({
+ values: {
+ userId,
+ action,
+ type: action.type,
+ data: {
+ ...action.data,
+ card: _.pick(values.card, ['name']),
+ },
+ creatorUser: values.user,
+ card: values.card,
+ },
+ project: inputs.project,
+ board: inputs.board,
+ list: inputs.list,
+ }),
+ ),
+ );
+
+ const notificationServices = await NotificationService.qm.getByBoardId(inputs.board.id);
+
+ if (notificationServices.length > 0) {
+ const services = notificationServices.map((notificationService) =>
+ _.pick(notificationService, ['url', 'format']),
+ );
+
+ buildAndSendNotifications(
+ services,
+ inputs.board,
+ values.card,
+ action,
+ values.user,
+ sails.helpers.utils.makeTranslator(),
+ );
+ }
+ }
}
return action;
diff --git a/server/api/helpers/card-memberships/create-one.js b/server/api/helpers/card-memberships/create-one.js
index bb41285e..b66fd2fb 100644
--- a/server/api/helpers/card-memberships/create-one.js
+++ b/server/api/helpers/card-memberships/create-one.js
@@ -37,15 +37,12 @@ module.exports = {
async fn(inputs) {
const { values } = inputs;
- if (values.user) {
- values.userId = values.user.id;
- }
-
let cardMembership;
try {
cardMembership = await CardMembership.qm.createOne({
...values,
cardId: values.card.id,
+ userId: values.user.id,
});
} catch (error) {
if (error.code === 'E_UNIQUE') {
@@ -69,6 +66,7 @@ module.exports = {
buildData: () => ({
item: cardMembership,
included: {
+ users: [values.user],
projects: [inputs.project],
boards: [inputs.board],
lists: [inputs.list],
@@ -107,6 +105,20 @@ module.exports = {
// TODO: send webhooks
}
+ await sails.helpers.actions.createOne.with({
+ values: {
+ type: Action.Types.ADD_MEMBER_TO_CARD,
+ data: {
+ user: _.pick(values.user, ['id', 'name']),
+ },
+ user: inputs.actorUser,
+ card: values.card,
+ },
+ project: inputs.project,
+ board: inputs.board,
+ list: inputs.list,
+ });
+
return cardMembership;
},
};
diff --git a/server/api/helpers/notifications/create-one.js b/server/api/helpers/notifications/create-one.js
index 63ff59f1..990e6a41 100644
--- a/server/api/helpers/notifications/create-one.js
+++ b/server/api/helpers/notifications/create-one.js
@@ -12,6 +12,8 @@ const buildTitle = (notification, t) => {
return t('Card Moved');
case Notification.Types.COMMENT_CARD:
return t('New Comment');
+ case Notification.Types.ADD_MEMBER_TO_CARD:
+ return t('You Were Added to Card');
default:
return null;
}
@@ -77,6 +79,22 @@ const buildBodyByFormat = (board, card, notification, actorUser, t) => {
)}:\n\n${escapeHtml(commentText)} `,
};
}
+ case Notification.Types.ADD_MEMBER_TO_CARD:
+ return {
+ text: t('%s added you to %s on %s', actorUser.name, card.name, board.name),
+ markdown: t(
+ '%s added you to %s on %s',
+ escapeMarkdown(actorUser.name),
+ markdownCardLink,
+ escapeMarkdown(board.name),
+ ),
+ html: t(
+ '%s added you to %s on %s',
+ escapeHtml(actorUser.name),
+ htmlCardLink,
+ escapeHtml(board.name),
+ ),
+ };
default:
return null;
}
@@ -120,6 +138,15 @@ const buildAndSendEmail = async (board, card, notification, actorUser, notifiabl
boardLink,
)}
${escapeHtml(notification.data.text)}
`;
+ break;
+ case Notification.Types.ADD_MEMBER_TO_CARD:
+ html = `${t(
+ '%s added you to %s on %s',
+ escapeHtml(actorUser.name),
+ cardLink,
+ boardLink,
+ )}
`;
+
break;
default:
return;
diff --git a/server/api/models/Action.js b/server/api/models/Action.js
index 59921b45..6fd8711f 100755
--- a/server/api/models/Action.js
+++ b/server/api/models/Action.js
@@ -13,6 +13,7 @@
const Types = {
CREATE_CARD: 'createCard',
MOVE_CARD: 'moveCard',
+ ADD_MEMBER_TO_CARD: 'addMemberToCard',
};
module.exports = {
diff --git a/server/api/models/Notification.js b/server/api/models/Notification.js
index c970d7dd..2fdfde03 100755
--- a/server/api/models/Notification.js
+++ b/server/api/models/Notification.js
@@ -13,6 +13,7 @@
const Types = {
MOVE_CARD: 'moveCard',
COMMENT_CARD: 'commentCard',
+ ADD_MEMBER_TO_CARD: 'addMemberToCard',
};
module.exports = {
diff --git a/server/config/locales/en-GB.json b/server/config/locales/en-GB.json
index 30d4f65b..9e9c1322 100644
--- a/server/config/locales/en-GB.json
+++ b/server/config/locales/en-GB.json
@@ -6,6 +6,8 @@
"This is a test text message!": "This is a test text message!",
"This is a *test* **markdown** `message`!": "This is a *test* **markdown** `message`!",
"This is a test html message
": "This is a test html message
",
+ "You Were Added to Card": "Your Were Added to Card",
+ "%s added you to %s on %s": "%s added you to %s on %s",
"%s created %s in %s on %s": "%s created %s in %s on %s",
"%s left a new comment to %s on %s": "%s left a new comment to %s on %s",
"%s moved %s from %s to %s on %s": "%s moved %s from %s to %s on %s"
diff --git a/server/config/locales/en-US.json b/server/config/locales/en-US.json
index c5190d91..f9b9b376 100644
--- a/server/config/locales/en-US.json
+++ b/server/config/locales/en-US.json
@@ -6,6 +6,8 @@
"This is a test text message!": "This is a test text message!",
"This is a *test* **markdown** `message`!": "This is a *test* **markdown** `message`!",
"This is a test html message
": "This is a test html message
",
+ "You Were Added to Card": "Your Were Added to Card",
+ "%s added you to %s on %s": "%s added you to %s on %s",
"%s created %s in %s on %s": "%s created %s in %s on %s",
"%s left a new comment to %s on %s": "%s left a new comment to %s on %s",
"%s moved %s from %s to %s on %s": "%s moved %s from %s to %s on %s"
diff --git a/server/config/locales/ru-RU.json b/server/config/locales/ru-RU.json
index bc6bebd2..6f300160 100644
--- a/server/config/locales/ru-RU.json
+++ b/server/config/locales/ru-RU.json
@@ -6,6 +6,8 @@
"This is a test text message!": "Это тестовое сообщение!",
"This is a *test* **markdown** `message`!": "Это *тестовое* **markdown** `сообщение`!",
"This is a test html message
": "Это тестовое html сообщение
",
+ "You Were Added to Card": "Вы были добавлены к карточке",
+ "%s added you to %s on %s": "%s добавил(а) вас к %s на %s",
"%s created %s in %s on %s": "%s создал(а) %s в %s на %s",
"%s left a new comment to %s on %s": "%s оставил(а) новый комментарий к %s на %s",
"%s moved %s from %s to %s on %s": "%s переместил(а) %s из %s в %s на %s"
From eeb52d19bbab20405f58f2bfb8a106096369b73e Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Sat, 17 May 2025 15:40:43 +0200
Subject: [PATCH 005/108] feat: Enhance card emphasis in colored and closed
lists
Closes #428, closes #506
---
client/src/components/cards/Card/Card.jsx | 40 +++++++++----------
.../components/cards/Card/Card.module.scss | 5 +++
2 files changed, 25 insertions(+), 20 deletions(-)
diff --git a/client/src/components/cards/Card/Card.jsx b/client/src/components/cards/Card/Card.jsx
index c05e9112..dc78ef09 100755
--- a/client/src/components/cards/Card/Card.jsx
+++ b/client/src/components/cards/Card/Card.jsx
@@ -3,16 +3,17 @@
* 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, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import { shallowEqual, useDispatch, useSelector } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import { Button, Icon } from 'semantic-ui-react';
import { push } from '../../../lib/redux-router';
import { usePopup } from '../../../lib/popup';
import selectors from '../../../selectors';
-import { isListArchiveOrTrash } from '../../../utils/record-helpers';
import Paths from '../../../constants/Paths';
import { BoardMembershipRoles, CardTypes, ListTypes } from '../../../constants/Enums';
import ProjectContent from './ProjectContent';
@@ -22,6 +23,7 @@ import EditName from './EditName';
import ActionsStep from './ActionsStep';
import styles from './Card.module.scss';
+import globalStyles from '../../../styles.module.scss';
const Card = React.memo(({ id, isInline }) => {
const selectCardById = useMemo(() => selectors.makeSelectCardById(), []);
@@ -29,6 +31,7 @@ const Card = React.memo(({ id, isInline }) => {
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);
@@ -40,24 +43,10 @@ const Card = React.memo(({ id, isInline }) => {
return selectIsCardWithIdRecent(state, id);
});
- const { isDisabled, canUseActions } = useSelector((state) => {
- const list = selectListById(state, card.listId);
-
+ const canUseActions = useSelector((state) => {
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
- const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
-
- if (isListArchiveOrTrash(list)) {
- return {
- isDisabled: false,
- canUseActions: isEditor,
- };
- }
-
- return {
- isDisabled: list.type === ListTypes.CLOSED && !isEditor,
- canUseActions: isEditor,
- };
- }, shallowEqual);
+ return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
+ });
const dispatch = useDispatch();
const [isEditNameOpened, setIsEditNameOpened] = useState(false);
@@ -101,11 +90,20 @@ const Card = React.memo(({ id, isInline }) => {
}
}
+ const colorLineNode = list.color && (
+
+ );
+
return (
{
jsx-a11y/no-static-element-interactions */}
+ {colorLineNode}
{canUseActions && (
@@ -128,6 +127,7 @@ const Card = React.memo(({ id, isInline }) => {
) : (
+ {colorLineNode}
)}
diff --git a/client/src/components/cards/Card/Card.module.scss b/client/src/components/cards/Card/Card.module.scss
index 99dcee12..4e0c4681 100644
--- a/client/src/components/cards/Card/Card.module.scss
+++ b/client/src/components/cards/Card/Card.module.scss
@@ -28,6 +28,11 @@
}
}
+ .colorLine {
+ border-radius: 0 0 3px 3px;
+ height: 4px;
+ }
+
.content {
cursor: pointer;
From 3aedbff5d0679f59b0042faef1a46f3706efb566 Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Sat, 17 May 2025 22:24:37 +0200
Subject: [PATCH 006/108] feat: Log action when user is removed from card
---
.../components/activities/Activities/Item.jsx | 30 ++++++++++++++++
client/src/constants/Enums.js | 1 +
client/src/locales/en-GB/core.js | 3 ++
client/src/locales/en-US/core.js | 3 ++
.../controllers/card-memberships/create.js | 2 +-
.../controllers/card-memberships/delete.js | 3 ++
server/api/helpers/actions/create-one.js | 34 ++++++++++---------
.../helpers/card-memberships/delete-one.js | 19 +++++++++++
server/api/models/Action.js | 8 +++++
9 files changed, 86 insertions(+), 17 deletions(-)
diff --git a/client/src/components/activities/Activities/Item.jsx b/client/src/components/activities/Activities/Item.jsx
index 181e5970..e1901076 100755
--- a/client/src/components/activities/Activities/Item.jsx
+++ b/client/src/components/activities/Activities/Item.jsx
@@ -113,6 +113,36 @@ const Item = React.memo(({ id }) => {
);
+ break;
+ case ActivityTypes.REMOVE_MEMBER_FROM_CARD:
+ contentNode =
+ user.id === activity.data.user.id ? (
+
+ {userName}
+ {' left this card'}
+
+ ) : (
+
+ {userName}
+
+ {' removed '}
+ {activity.data.user.name}
+ {' from this card'}
+
+
+ );
+
break;
default:
contentNode = null;
diff --git a/client/src/constants/Enums.js b/client/src/constants/Enums.js
index 0cfcf20a..02f591cf 100755
--- a/client/src/constants/Enums.js
+++ b/client/src/constants/Enums.js
@@ -91,6 +91,7 @@ export const ActivityTypes = {
CREATE_CARD: 'createCard',
MOVE_CARD: 'moveCard',
ADD_MEMBER_TO_CARD: 'addMemberToCard',
+ REMOVE_MEMBER_FROM_CARD: 'removeMemberFromCard',
};
export const NotificationTypes = {
diff --git a/client/src/locales/en-GB/core.js b/client/src/locales/en-GB/core.js
index d19b63a2..688eebf2 100644
--- a/client/src/locales/en-GB/core.js
+++ b/client/src/locales/en-GB/core.js
@@ -291,9 +291,12 @@ export default {
userAddedYouToCard: '{{user}} added you to <2>{{card}}2>',
userJoinedThisCard: `<0>{{user}}0><1> joined this card1>`,
userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}2>',
+ userLeftThisCard: '<0>{{user}}0><1> left this card1>',
userMovedCardFromListToList: '{{user}} moved <2>{{card}}2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> moved this card from {{fromList}} to {{toList}}1>',
+ userRemovedUserFromThisCard:
+ '<0>{{actorUser}}0><1> removed {{removedUser}} from this card1>',
username: 'Username',
users: 'Users',
viewer: 'Viewer',
diff --git a/client/src/locales/en-US/core.js b/client/src/locales/en-US/core.js
index 468eb945..50487bd3 100644
--- a/client/src/locales/en-US/core.js
+++ b/client/src/locales/en-US/core.js
@@ -286,9 +286,12 @@ export default {
userAddedYouToCard: '{{user}} added you to <2>{{card}}2>',
userJoinedThisCard: `<0>{{user}}0><1> joined this card1>`,
userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}2>',
+ userLeftThisCard: '<0>{{user}}0><1> left this card1>',
userMovedCardFromListToList: '{{user}} moved <2>{{card}}2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> moved this card from {{fromList}} to {{toList}}1>',
+ userRemovedUserFromThisCard:
+ '<0>{{actorUser}}0><1> removed {{removedUser}} from this card1>',
username: 'Username',
users: 'Users',
viewer: 'Viewer',
diff --git a/server/api/controllers/card-memberships/create.js b/server/api/controllers/card-memberships/create.js
index c5750764..e0199150 100755
--- a/server/api/controllers/card-memberships/create.js
+++ b/server/api/controllers/card-memberships/create.js
@@ -76,7 +76,7 @@ module.exports = {
const isBoardMember = await sails.helpers.users.isBoardMember(user.id, board.id);
if (!isBoardMember) {
- throw Errors.USER_NOT_FOUND;
+ throw Errors.USER_NOT_FOUND; // Forbidden
}
const cardMembership = await sails.helpers.cardMemberships.createOne
diff --git a/server/api/controllers/card-memberships/delete.js b/server/api/controllers/card-memberships/delete.js
index 73857573..ec70c435 100755
--- a/server/api/controllers/card-memberships/delete.js
+++ b/server/api/controllers/card-memberships/delete.js
@@ -70,7 +70,10 @@ module.exports = {
throw Errors.USER_NOT_CARD_MEMBER;
}
+ const user = await User.qm.getOneById(cardMembership.userId);
+
cardMembership = await sails.helpers.cardMemberships.deleteOne.with({
+ user,
project,
board,
list,
diff --git a/server/api/helpers/actions/create-one.js b/server/api/helpers/actions/create-one.js
index ca23d358..ed35b5e7 100644
--- a/server/api/helpers/actions/create-one.js
+++ b/server/api/helpers/actions/create-one.js
@@ -142,8 +142,8 @@ module.exports = {
user: values.user,
});
- if (action.type !== Action.Types.CREATE_CARD) {
- if (action.type === Action.Types.ADD_MEMBER_TO_CARD) {
+ if (Action.INTERNAL_NOTIFIABLE_TYPES.includes(action.type)) {
+ if (Action.PERSONAL_NOTIFIABLE_TYPES.includes(action.type)) {
if (values.user !== action.data.user.id) {
await sails.helpers.notifications.createOne.with({
values: {
@@ -195,23 +195,25 @@ module.exports = {
}),
),
);
+ }
+ }
- const notificationServices = await NotificationService.qm.getByBoardId(inputs.board.id);
+ if (Action.EXTERNAL_NOTIFIABLE_TYPES.includes(action.type)) {
+ const notificationServices = await NotificationService.qm.getByBoardId(inputs.board.id);
- if (notificationServices.length > 0) {
- const services = notificationServices.map((notificationService) =>
- _.pick(notificationService, ['url', 'format']),
- );
+ if (notificationServices.length > 0) {
+ const services = notificationServices.map((notificationService) =>
+ _.pick(notificationService, ['url', 'format']),
+ );
- buildAndSendNotifications(
- services,
- inputs.board,
- values.card,
- action,
- values.user,
- sails.helpers.utils.makeTranslator(),
- );
- }
+ buildAndSendNotifications(
+ services,
+ inputs.board,
+ values.card,
+ action,
+ values.user,
+ sails.helpers.utils.makeTranslator(),
+ );
}
}
diff --git a/server/api/helpers/card-memberships/delete-one.js b/server/api/helpers/card-memberships/delete-one.js
index f9ed0470..659bb90a 100644
--- a/server/api/helpers/card-memberships/delete-one.js
+++ b/server/api/helpers/card-memberships/delete-one.js
@@ -9,6 +9,10 @@ module.exports = {
type: 'ref',
required: true,
},
+ user: {
+ type: 'ref',
+ required: true,
+ },
project: {
type: 'ref',
required: true,
@@ -52,6 +56,7 @@ module.exports = {
buildData: () => ({
item: cardMembership,
included: {
+ users: [inputs.user],
projects: [inputs.project],
boards: [inputs.board],
lists: [inputs.list],
@@ -75,6 +80,20 @@ module.exports = {
},
});
}
+
+ await sails.helpers.actions.createOne.with({
+ values: {
+ type: Action.Types.REMOVE_MEMBER_FROM_CARD,
+ data: {
+ user: _.pick(inputs.user, ['id', 'name']),
+ },
+ user: inputs.actorUser,
+ card: inputs.card,
+ },
+ project: inputs.project,
+ board: inputs.board,
+ list: inputs.list,
+ });
}
return cardMembership;
diff --git a/server/api/models/Action.js b/server/api/models/Action.js
index 6fd8711f..888d34f1 100755
--- a/server/api/models/Action.js
+++ b/server/api/models/Action.js
@@ -14,10 +14,18 @@ const Types = {
CREATE_CARD: 'createCard',
MOVE_CARD: 'moveCard',
ADD_MEMBER_TO_CARD: 'addMemberToCard',
+ REMOVE_MEMBER_FROM_CARD: 'removeMemberFromCard',
};
+const INTERNAL_NOTIFIABLE_TYPES = [Types.MOVE_CARD, Types.ADD_MEMBER_TO_CARD];
+const EXTERNAL_NOTIFIABLE_TYPES = [Types.CREATE_CARD, Types.MOVE_CARD];
+const PERSONAL_NOTIFIABLE_TYPES = [Types.ADD_MEMBER_TO_CARD];
+
module.exports = {
Types,
+ INTERNAL_NOTIFIABLE_TYPES,
+ EXTERNAL_NOTIFIABLE_TYPES,
+ PERSONAL_NOTIFIABLE_TYPES,
attributes: {
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
From 652c5e9475f792d62ce794ad47e80070d75aeef5 Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Mon, 19 May 2025 17:27:18 +0200
Subject: [PATCH 007/108] fix: Use batch inserts to avoid query size limits
---
server/db/upgrade.js | 551 +++++++++++++++++++++++--------------------
1 file changed, 300 insertions(+), 251 deletions(-)
diff --git a/server/db/upgrade.js b/server/db/upgrade.js
index 16ea33cd..d3ca37ed 100644
--- a/server/db/upgrade.js
+++ b/server/db/upgrade.js
@@ -97,50 +97,53 @@ const upgradeDatabase = async () => {
const whereInUserIds = ['0', ...userIds];
if (users.length > 0) {
- await trx('user_account').insert(
- users.map((user) => ({
- ..._.pick(user, [
- 'id',
- 'email',
- 'password',
- 'name',
- 'username',
- 'phone',
- 'organization',
- 'language',
- 'subscribe_to_own_cards',
- 'created_at',
- 'updated_at',
- 'password_changed_at',
- ]),
- role: user.is_admin ? User.Roles.ADMIN : User.Roles.BOARD_USER,
- avatar: user.avatar && {
- ...user.avatar,
- sizeInBytes: 0,
- },
- subscribe_to_card_when_commenting: true,
- turn_off_recent_card_highlighting: false,
- enable_favorites_by_default: false,
- default_editor_mode: User.EditorModes.WYSIWYG,
- default_home_view: User.HomeViews.GROUPED_PROJECTS,
- default_projects_order: User.ProjectOrders.BY_DEFAULT,
- is_sso_user: user.is_sso,
- is_deactivated: false,
- })),
- );
+ await knex
+ .batchInsert(
+ 'user_account',
+ users.map((user) => ({
+ ..._.pick(user, [
+ 'id',
+ 'email',
+ 'password',
+ 'name',
+ 'username',
+ 'phone',
+ 'organization',
+ 'language',
+ 'subscribe_to_own_cards',
+ 'created_at',
+ 'updated_at',
+ 'password_changed_at',
+ ]),
+ role: user.is_admin ? User.Roles.ADMIN : User.Roles.BOARD_USER,
+ avatar: user.avatar && {
+ ...user.avatar,
+ sizeInBytes: 0,
+ },
+ subscribe_to_card_when_commenting: true,
+ turn_off_recent_card_highlighting: false,
+ enable_favorites_by_default: false,
+ default_editor_mode: User.EditorModes.WYSIWYG,
+ default_home_view: User.HomeViews.GROUPED_PROJECTS,
+ default_projects_order: User.ProjectOrders.BY_DEFAULT,
+ is_sso_user: user.is_sso,
+ is_deactivated: false,
+ })),
+ )
+ .transacting(trx);
const identityProviderUsers = await trx('identity_provider_user')
.withSchema('v1')
.whereIn('user_id', whereInUserIds);
if (identityProviderUsers.length > 0) {
- await trx('identity_provider_user').insert(identityProviderUsers);
+ await knex.batchInsert('identity_provider_user', identityProviderUsers).transacting(trx);
}
const sessions = await trx('session').withSchema('v1').whereIn('user_id', whereInUserIds);
if (sessions.length > 0) {
- await trx('session').insert(sessions);
+ await knex.batchInsert('session', sessions).transacting(trx);
}
}
@@ -154,47 +157,53 @@ const upgradeDatabase = async () => {
if (projectsWithBackgroundImage.length > 0) {
const createdAt = new Date().toISOString();
- const backgroundImages = await trx('background_image').insert(
- projectsWithBackgroundImage.map((project) => ({
- ...project.background_image,
- project_id: project.id,
- size_in_bytes: 0,
- created_at: createdAt,
- })),
- ['id', 'project_id'],
- );
+ const backgroundImages = await knex
+ .batchInsert(
+ 'background_image',
+ projectsWithBackgroundImage.map((project) => ({
+ ...project.background_image,
+ project_id: project.id,
+ size_in_bytes: 0,
+ created_at: createdAt,
+ })),
+ )
+ .returning(['id', 'project_id'])
+ .transacting(trx);
backgroundImages.forEach((backgroundImage) => {
backgroundImageIdByProjectId[backgroundImage.project_id] = backgroundImage.id;
});
}
- await trx('project').insert(
- projects.map((project) => {
- const data = {
- ..._.pick(project, ['id', 'name', 'created_at', 'updated_at']),
- is_hidden: false,
- };
+ await knex
+ .batchInsert(
+ 'project',
+ projects.map((project) => {
+ const data = {
+ ..._.pick(project, ['id', 'name', 'created_at', 'updated_at']),
+ is_hidden: false,
+ };
- if (project.background) {
- data.background_type = project.background.type;
+ if (project.background) {
+ data.background_type = project.background.type;
- switch (project.background.type) {
- case Project.BackgroundTypes.GRADIENT:
- data.background_gradient = project.background.name;
+ switch (project.background.type) {
+ case Project.BackgroundTypes.GRADIENT:
+ data.background_gradient = project.background.name;
- break;
- case Project.BackgroundTypes.IMAGE:
- data.background_image_id = backgroundImageIdByProjectId[project.id];
+ break;
+ case Project.BackgroundTypes.IMAGE:
+ data.background_image_id = backgroundImageIdByProjectId[project.id];
- break;
- default:
+ break;
+ default:
+ }
}
- }
- return data;
- }),
- );
+ return data;
+ }),
+ )
+ .transacting(trx);
}
const projectManagers = await trx('project_manager')
@@ -202,7 +211,7 @@ const upgradeDatabase = async () => {
.whereIn('project_id', whereInProjectIds);
if (projectManagers.length > 0) {
- await trx('project_manager').insert(projectManagers);
+ await knex.batchInsert('project_manager', projectManagers).transacting(trx);
}
const boards = await trx('board').withSchema('v1').whereIn('project_id', whereInProjectIds);
@@ -218,27 +227,33 @@ const upgradeDatabase = async () => {
const whereInBoardIds = ['0', ...Object.keys(projectIdByBoardId)];
if (boards.length > 0) {
- await trx('board').insert(
- boards.map((board) => ({
- ..._.pick(board, ['id', 'project_id', 'position', 'name', 'created_at', 'updated_at']),
- default_view: Board.Views.KANBAN,
- default_card_type: Card.Types.PROJECT,
- limit_card_types_to_default_one: false,
- always_display_card_creator: false,
- })),
- );
+ await knex
+ .batchInsert(
+ 'board',
+ boards.map((board) => ({
+ ..._.pick(board, ['id', 'project_id', 'position', 'name', 'created_at', 'updated_at']),
+ default_view: Board.Views.KANBAN,
+ default_card_type: Card.Types.PROJECT,
+ limit_card_types_to_default_one: false,
+ always_display_card_creator: false,
+ })),
+ )
+ .transacting(trx);
const createdAt = new Date().toISOString();
- await trx('list').insert(
- boards.flatMap((board) =>
- [List.Types.ARCHIVE, List.Types.TRASH].map((type) => ({
- type,
- board_id: board.id,
- created_at: createdAt,
- })),
- ),
- );
+ await knex
+ .batchInsert(
+ 'list',
+ boards.flatMap((board) =>
+ [List.Types.ARCHIVE, List.Types.TRASH].map((type) => ({
+ type,
+ board_id: board.id,
+ created_at: createdAt,
+ })),
+ ),
+ )
+ .transacting(trx);
}
const boardMemberships = await trx('board_membership')
@@ -246,46 +261,52 @@ const upgradeDatabase = async () => {
.whereIn('board_id', whereInBoardIds);
if (boardMemberships.length > 0) {
- await trx('board_membership').insert(
- boardMemberships.map((boardMembership) => ({
- ..._.pick(boardMembership, [
- 'id',
- 'board_id',
- 'user_id',
- 'role',
- 'can_comment',
- 'created_at',
- 'updated_at',
- ]),
- project_id: projectIdByBoardId[boardMembership.board_id],
- })),
- );
+ await knex
+ .batchInsert(
+ 'board_membership',
+ boardMemberships.map((boardMembership) => ({
+ ..._.pick(boardMembership, [
+ 'id',
+ 'board_id',
+ 'user_id',
+ 'role',
+ 'can_comment',
+ 'created_at',
+ 'updated_at',
+ ]),
+ project_id: projectIdByBoardId[boardMembership.board_id],
+ })),
+ )
+ .transacting(trx);
}
const labels = await trx('label').withSchema('v1').whereIn('board_id', whereInBoardIds);
if (labels.length > 0) {
- await trx('label').insert(labels);
+ await knex.batchInsert('label', labels).transacting(trx);
}
const lists = await trx('list').withSchema('v1').whereIn('board_id', whereInBoardIds);
const whereInListIds = ['0', ...lists.map(({ id }) => id)];
if (lists.length > 0) {
- await trx('list').insert(
- lists.map((list) => ({
- ..._.pick(list, [
- 'id',
- 'board_id',
- 'position',
- 'name',
- 'color',
- 'created_at',
- 'updated_at',
- ]),
- type: List.Types.ACTIVE,
- })),
- );
+ await knex
+ .batchInsert(
+ 'list',
+ lists.map((list) => ({
+ ..._.pick(list, [
+ 'id',
+ 'board_id',
+ 'position',
+ 'name',
+ 'color',
+ 'created_at',
+ 'updated_at',
+ ]),
+ type: List.Types.ACTIVE,
+ })),
+ )
+ .transacting(trx);
}
const cards = await trx('card')
@@ -297,26 +318,29 @@ const upgradeDatabase = async () => {
const whereInCardIds = ['0', ...Object.keys(cardById)];
if (cards.length > 0) {
- await trx('card').insert(
- cards.map((card) => ({
- ..._.pick(card, [
- 'id',
- 'board_id',
- 'list_id',
- 'cover_attachment_id',
- 'position',
- 'name',
- 'description',
- 'due_date',
- 'stopwatch',
- 'created_at',
- 'updated_at',
- ]),
- creator_user_id: userIdsSet.has(card.creator_user_id) ? card.creator_user_id : null,
- type: Card.Types.PROJECT,
- list_changed_at: card.created_at,
- })),
- );
+ await knex
+ .batchInsert(
+ 'card',
+ cards.map((card) => ({
+ ..._.pick(card, [
+ 'id',
+ 'board_id',
+ 'list_id',
+ 'cover_attachment_id',
+ 'position',
+ 'name',
+ 'description',
+ 'due_date',
+ 'stopwatch',
+ 'created_at',
+ 'updated_at',
+ ]),
+ creator_user_id: userIdsSet.has(card.creator_user_id) ? card.creator_user_id : null,
+ type: Card.Types.PROJECT,
+ list_changed_at: card.created_at,
+ })),
+ )
+ .transacting(trx);
}
const cardSubscriptions = await trx('card_subscription')
@@ -324,7 +348,7 @@ const upgradeDatabase = async () => {
.whereIn('card_id', whereInCardIds);
if (cardSubscriptions.length > 0) {
- await trx('card_subscription').insert(cardSubscriptions);
+ await knex.batchInsert('card_subscription', cardSubscriptions).transacting(trx);
}
const cardMemberships = await trx('card_membership')
@@ -332,13 +356,13 @@ const upgradeDatabase = async () => {
.whereIn('card_id', whereInCardIds);
if (cardMemberships.length > 0) {
- await trx('card_membership').insert(cardMemberships);
+ await knex.batchInsert('card_membership', cardMemberships).transacting(trx);
}
const cardLabels = await trx('card_label').withSchema('v1').whereIn('card_id', whereInCardIds);
if (cardLabels.length > 0) {
- await trx('card_label').insert(cardLabels);
+ await knex.batchInsert('card_label', cardLabels).transacting(trx);
}
const tasks = await trx('task').withSchema('v1').whereIn('card_id', whereInCardIds);
@@ -349,47 +373,63 @@ const upgradeDatabase = async () => {
if (taskCardIds.length > 0) {
const createdAt = new Date().toISOString();
- const taskLists = await trx('task_list').insert(
- taskCardIds.map((cardId) => ({
- card_id: cardId,
- position: POSITION_GAP,
- name: 'Task List',
- show_on_front_of_card: true,
- created_at: createdAt,
- })),
- ['id', 'card_id'],
- );
-
- await trx('task').insert(
- taskLists.flatMap((taskList) =>
- tasksByCardId[taskList.card_id].map((task) => ({
- ..._.pick(task, ['id', 'position', 'name', 'is_completed', 'created_at', 'updated_at']),
- task_list_id: taskList.id,
+ const taskLists = await knex
+ .batchInsert(
+ 'task_list',
+ taskCardIds.map((cardId) => ({
+ card_id: cardId,
+ position: POSITION_GAP,
+ name: 'Task List',
+ show_on_front_of_card: true,
+ created_at: createdAt,
})),
- ),
- );
+ )
+ .returning(['id', 'card_id'])
+ .transacting(trx);
+
+ await knex
+ .batchInsert(
+ 'task',
+ taskLists.flatMap((taskList) =>
+ tasksByCardId[taskList.card_id].map((task) => ({
+ ..._.pick(task, [
+ 'id',
+ 'position',
+ 'name',
+ 'is_completed',
+ 'created_at',
+ 'updated_at',
+ ]),
+ task_list_id: taskList.id,
+ })),
+ ),
+ )
+ .transacting(trx);
}
const attachments = await trx('attachment').withSchema('v1').whereIn('card_id', whereInCardIds);
if (attachments.length > 0) {
- await trx('attachment').insert(
- attachments.map((attachment) => ({
- ..._.pick(attachment, ['id', 'card_id', 'name', 'created_at', 'updated_at']),
- creator_user_id: userIdsSet.has(attachment.creator_user_id)
- ? attachment.creator_user_id
- : null,
- type: Attachment.Types.FILE,
- data: {
- fileReferenceId: attachment.dirname,
- filename: attachment.filename,
- mimeType: mime.getType(attachment.filename),
- sizeInBytes: 0,
- encoding: null,
- image: attachment.image,
- },
- })),
- );
+ await knex
+ .batchInsert(
+ 'attachment',
+ attachments.map((attachment) => ({
+ ..._.pick(attachment, ['id', 'card_id', 'name', 'created_at', 'updated_at']),
+ creator_user_id: userIdsSet.has(attachment.creator_user_id)
+ ? attachment.creator_user_id
+ : null,
+ type: Attachment.Types.FILE,
+ data: {
+ fileReferenceId: attachment.dirname,
+ filename: attachment.filename,
+ mimeType: mime.getType(attachment.filename),
+ sizeInBytes: 0,
+ encoding: null,
+ image: attachment.image,
+ },
+ })),
+ )
+ .transacting(trx);
}
const actions = await trx('action').withSchema('v1').whereIn('card_id', whereInCardIds);
@@ -409,52 +449,58 @@ const upgradeDatabase = async () => {
});
if (commentActions.length > 0) {
- await trx('comment').insert(
- commentActions.map((action) => ({
- ..._.pick(action, ['id', 'card_id', 'created_at', 'updated_at']),
- user_id: userIdsSet.has(action.user_id) ? action.user_id : null,
- text: action.data.text,
- })),
- );
+ await knex
+ .batchInsert(
+ 'comment',
+ commentActions.map((action) => ({
+ ..._.pick(action, ['id', 'card_id', 'created_at', 'updated_at']),
+ user_id: userIdsSet.has(action.user_id) ? action.user_id : null,
+ text: action.data.text,
+ })),
+ )
+ .transacting(trx);
}
if (otherActions.length > 0) {
- await trx('action').insert(
- otherActions.map((action) => {
- const data = {
- ..._.pick(action, ['id', 'card_id', 'type', 'created_at', 'updated_at']),
- user_id: userIdsSet.has(action.user_id) ? action.user_id : null,
- };
+ await knex
+ .batchInsert(
+ 'action',
+ otherActions.map((action) => {
+ const data = {
+ ..._.pick(action, ['id', 'card_id', 'type', 'created_at', 'updated_at']),
+ user_id: userIdsSet.has(action.user_id) ? action.user_id : null,
+ };
- switch (action.type) {
- case Action.Types.CREATE_CARD:
- data.data = {
- list: {
- ...action.data.list,
- type: List.Types.ACTIVE,
- },
- };
+ switch (action.type) {
+ case Action.Types.CREATE_CARD:
+ data.data = {
+ list: {
+ ...action.data.list,
+ type: List.Types.ACTIVE,
+ },
+ };
- break;
- case Action.Types.MOVE_CARD:
- data.data = {
- fromList: {
- ...action.data.fromList,
- type: List.Types.ACTIVE,
- },
- toList: {
- ...action.data.toList,
- type: List.Types.ACTIVE,
- },
- };
+ break;
+ case Action.Types.MOVE_CARD:
+ data.data = {
+ fromList: {
+ ...action.data.fromList,
+ type: List.Types.ACTIVE,
+ },
+ toList: {
+ ...action.data.toList,
+ type: List.Types.ACTIVE,
+ },
+ };
- break;
- default:
- }
+ break;
+ default:
+ }
- return data;
- }),
- );
+ return data;
+ }),
+ )
+ .transacting(trx);
}
const notifications = await trx('notification')
@@ -464,55 +510,58 @@ const upgradeDatabase = async () => {
.whereIn('card_id', whereInCardIds);
if (notifications.length > 0) {
- await trx('notification').insert(
- notifications.map((notification) => {
- const card = cardById[notification.card_id];
- const action = actionById[notification.action_id];
+ await knex
+ .batchInsert(
+ 'notification',
+ notifications.map((notification) => {
+ const card = cardById[notification.card_id];
+ const action = actionById[notification.action_id];
- const data = {
- ..._.pick(notification, [
- 'id',
- 'user_id',
- 'card_id',
- 'is_read',
- 'created_at',
- 'updated_at',
- ]),
- creator_user_id: userIdsSet.has(notification.creator_user_id)
- ? notification.creator_user_id
- : null,
- board_id: card.board_id,
- type: action.type,
- };
+ const data = {
+ ..._.pick(notification, [
+ 'id',
+ 'user_id',
+ 'card_id',
+ 'is_read',
+ 'created_at',
+ 'updated_at',
+ ]),
+ creator_user_id: userIdsSet.has(notification.creator_user_id)
+ ? notification.creator_user_id
+ : null,
+ board_id: card.board_id,
+ type: action.type,
+ };
- if (action.type === PrevActionTypes.COMMENT_CARD) {
- Object.assign(data, {
- comment_id: action.id,
- data: {
- card: _.pick(card, ['name']),
- text: action.data.text,
- },
- });
- } else {
- Object.assign(data, {
- action_id: action.id,
- data: {
- fromList: {
- ...action.data.fromList,
- type: List.Types.ACTIVE,
+ if (action.type === PrevActionTypes.COMMENT_CARD) {
+ Object.assign(data, {
+ comment_id: action.id,
+ data: {
+ card: _.pick(card, ['name']),
+ text: action.data.text,
},
- toList: {
- ...action.data.toList,
- type: List.Types.ACTIVE,
+ });
+ } else {
+ Object.assign(data, {
+ action_id: action.id,
+ data: {
+ fromList: {
+ ...action.data.fromList,
+ type: List.Types.ACTIVE,
+ },
+ toList: {
+ ...action.data.toList,
+ type: List.Types.ACTIVE,
+ },
+ card: _.pick(card, ['name']),
},
- card: _.pick(card, ['name']),
- },
- });
- }
+ });
+ }
- return data;
- }),
- );
+ return data;
+ }),
+ )
+ .transacting(trx);
}
await trx.schema.dropSchema('v1', true);
From 3b8bec5743eff01ba4476a366e6833894c6126f2 Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Mon, 19 May 2025 18:03:08 +0200
Subject: [PATCH 008/108] docs: Update docker pulls counter
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 032d3b2d..0b07a568 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
**Project mastering driven by fun**
- [](https://github.com/plankanban/planka/pkgs/container/planka) [](https://github.com/plankanban/planka/graphs/contributors) [](https://discord.gg/WqqYNd7Jvt)
+ [](https://github.com/plankanban/planka/pkgs/container/planka) [](https://github.com/plankanban/planka/graphs/contributors) [](https://discord.gg/WqqYNd7Jvt)

From 5d53f44fe8b897f0cd85351df3353b1ce2610a6c Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Tue, 20 May 2025 00:06:51 +0200
Subject: [PATCH 009/108] fix: Add clearfix and missing id property
---
client/src/components/cards/Card/ProjectContent.module.scss | 6 ++++++
server/api/helpers/actions/create-one.js | 2 +-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/client/src/components/cards/Card/ProjectContent.module.scss b/client/src/components/cards/Card/ProjectContent.module.scss
index 48853615..72503ee2 100644
--- a/client/src/components/cards/Card/ProjectContent.module.scss
+++ b/client/src/components/cards/Card/ProjectContent.module.scss
@@ -101,6 +101,12 @@
.wrapper {
padding: 6px 8px 0;
+
+ &:after {
+ clear: both;
+ content: "";
+ display: table;
+ }
}
:global(.card):hover {
diff --git a/server/api/helpers/actions/create-one.js b/server/api/helpers/actions/create-one.js
index ed35b5e7..ba1e6494 100644
--- a/server/api/helpers/actions/create-one.js
+++ b/server/api/helpers/actions/create-one.js
@@ -144,7 +144,7 @@ module.exports = {
if (Action.INTERNAL_NOTIFIABLE_TYPES.includes(action.type)) {
if (Action.PERSONAL_NOTIFIABLE_TYPES.includes(action.type)) {
- if (values.user !== action.data.user.id) {
+ if (values.user.id !== action.data.user.id) {
await sails.helpers.notifications.createOne.with({
values: {
action,
From d5fd2fd33bfb8f73ec127a9856577b7d5f940705 Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Tue, 20 May 2025 21:17:51 +0200
Subject: [PATCH 010/108] feat: Log actions when task completion status changes
---
.../components/activities/Activities/Item.jsx | 38 +++++++++++++++++++
client/src/constants/Enums.js | 2 +
client/src/locales/en-GB/core.js | 3 ++
client/src/locales/en-US/core.js | 3 ++
server/api/helpers/tasks/update-one.js | 16 ++++++++
server/api/models/Action.js | 2 +
6 files changed, 64 insertions(+)
diff --git a/client/src/components/activities/Activities/Item.jsx b/client/src/components/activities/Activities/Item.jsx
index e1901076..e4d34acb 100755
--- a/client/src/components/activities/Activities/Item.jsx
+++ b/client/src/components/activities/Activities/Item.jsx
@@ -143,6 +143,44 @@ const Item = React.memo(({ id }) => {
);
+ break;
+ case ActivityTypes.COMPLETE_TASK:
+ contentNode = (
+
+ {userName}
+
+ {' completed '}
+ {activity.data.task.name}
+ {' on this card'}
+
+
+ );
+
+ break;
+ case ActivityTypes.UNCOMPLETE_TASK:
+ contentNode = (
+
+ {userName}
+
+ {' marked '}
+ {activity.data.task.name}
+ {' incomplete on this card'}
+
+
+ );
+
break;
default:
contentNode = null;
diff --git a/client/src/constants/Enums.js b/client/src/constants/Enums.js
index 02f591cf..9da896a2 100755
--- a/client/src/constants/Enums.js
+++ b/client/src/constants/Enums.js
@@ -92,6 +92,8 @@ export const ActivityTypes = {
MOVE_CARD: 'moveCard',
ADD_MEMBER_TO_CARD: 'addMemberToCard',
REMOVE_MEMBER_FROM_CARD: 'removeMemberFromCard',
+ COMPLETE_TASK: 'completeTask',
+ UNCOMPLETE_TASK: 'uncompleteTask',
};
export const NotificationTypes = {
diff --git a/client/src/locales/en-GB/core.js b/client/src/locales/en-GB/core.js
index 688eebf2..2cb913e3 100644
--- a/client/src/locales/en-GB/core.js
+++ b/client/src/locales/en-GB/core.js
@@ -289,9 +289,12 @@ export default {
userAddedThisCardToList: '<0>{{user}}0><1> added this card to {{list}}1>',
userAddedUserToThisCard: '<0>{{actorUser}}0><1> added {{addedUser}} to this card1>',
userAddedYouToCard: '{{user}} added you to <2>{{card}}2>',
+ userCompletedTaskOnThisCard: '<0>{{user}}0><1> completed {{task}} on this card1>',
userJoinedThisCard: `<0>{{user}}0><1> joined this card1>`,
userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}2>',
userLeftThisCard: '<0>{{user}}0><1> left this card1>',
+ userMarkedTaskIncompleteOnThisCard:
+ '<0>{{user}}0><1> marked {{task}} incomplete on this card1>',
userMovedCardFromListToList: '{{user}} moved <2>{{card}}2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> moved this card from {{fromList}} to {{toList}}1>',
diff --git a/client/src/locales/en-US/core.js b/client/src/locales/en-US/core.js
index 50487bd3..28cae078 100644
--- a/client/src/locales/en-US/core.js
+++ b/client/src/locales/en-US/core.js
@@ -284,9 +284,12 @@ export default {
userAddedThisCardToList: '<0>{{user}}0><1> added this card to {{list}}1>',
userAddedUserToThisCard: '<0>{{actorUser}}0><1> added {{addedUser}} to this card1>',
userAddedYouToCard: '{{user}} added you to <2>{{card}}2>',
+ userCompletedTaskOnThisCard: '<0>{{user}}0><1> completed {{task}} on this card1>',
userJoinedThisCard: `<0>{{user}}0><1> joined this card1>`,
userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}2>',
userLeftThisCard: '<0>{{user}}0><1> left this card1>',
+ userMarkedTaskIncompleteOnThisCard:
+ '<0>{{user}}0><1> marked {{task}} incomplete on this card1>',
userMovedCardFromListToList: '{{user}} moved <2>{{card}}2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> moved this card from {{fromList}} to {{toList}}1>',
diff --git a/server/api/helpers/tasks/update-one.js b/server/api/helpers/tasks/update-one.js
index 84cd13e4..6261398f 100644
--- a/server/api/helpers/tasks/update-one.js
+++ b/server/api/helpers/tasks/update-one.js
@@ -132,6 +132,22 @@ module.exports = {
}),
user: inputs.actorUser,
});
+
+ if (inputs.record.isCompleted !== task.isCompleted) {
+ await sails.helpers.actions.createOne.with({
+ values: {
+ type: task.isCompleted ? Action.Types.COMPLETE_TASK : Action.Types.UNCOMPLETE_TASK,
+ data: {
+ task: _.pick(task, ['id', 'name']),
+ },
+ user: inputs.actorUser,
+ card: inputs.card,
+ },
+ project: inputs.project,
+ board: inputs.board,
+ list: inputs.list,
+ });
+ }
}
return task;
diff --git a/server/api/models/Action.js b/server/api/models/Action.js
index 888d34f1..955bf812 100755
--- a/server/api/models/Action.js
+++ b/server/api/models/Action.js
@@ -15,6 +15,8 @@ const Types = {
MOVE_CARD: 'moveCard',
ADD_MEMBER_TO_CARD: 'addMemberToCard',
REMOVE_MEMBER_FROM_CARD: 'removeMemberFromCard',
+ COMPLETE_TASK: 'completeTask',
+ UNCOMPLETE_TASK: 'uncompleteTask',
};
const INTERNAL_NOTIFIABLE_TYPES = [Types.MOVE_CARD, Types.ADD_MEMBER_TO_CARD];
From c05dcf45f2c9cd5fcbff39daa76f1b94b002952f Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Wed, 21 May 2025 13:03:25 +0200
Subject: [PATCH 011/108] feat: Enhance notifications popup appearance
---
client/src/api/notifications.js | 3 +++
.../activities/Activities/Item.module.scss | 2 --
.../notifications/NotificationsStep/Item.jsx | 14 ++++++++++----
.../NotificationsStep/Item.module.scss | 13 ++++++++++++-
client/src/locales/ar-YE/core.js | 5 +++--
client/src/locales/bg-BG/core.js | 5 +++--
client/src/locales/cs-CZ/core.js | 11 +++++++----
client/src/locales/da-DK/core.js | 5 +++--
client/src/locales/de-DE/core.js | 4 ++--
client/src/locales/en-GB/core.js | 8 +++++---
client/src/locales/en-US/core.js | 8 +++++---
client/src/locales/es-ES/core.js | 5 +++--
client/src/locales/fa-IR/core.js | 5 +++--
client/src/locales/fr-FR/core.js | 8 ++++----
client/src/locales/hu-HU/core.js | 4 ++--
client/src/locales/id-ID/core.js | 4 ++--
client/src/locales/it-IT/core.js | 4 ++--
client/src/locales/ja-JP/core.js | 6 +++---
client/src/locales/ko-KR/core.js | 5 +++--
client/src/locales/nl-NL/core.js | 4 ++--
client/src/locales/pl-PL/core.js | 5 +++--
client/src/locales/pt-BR/core.js | 5 +++--
client/src/locales/ro-RO/core.js | 4 ++--
client/src/locales/ru-RU/core.js | 5 +++--
client/src/locales/sk-SK/core.js | 6 ++++--
client/src/locales/sr-Cyrl-CS/core.js | 5 +++--
client/src/locales/sr-Latn-CS/core.js | 5 +++--
client/src/locales/sv-SE/core.js | 5 +++--
client/src/locales/tr-TR/core.js | 4 ++--
client/src/locales/uk-UA/core.js | 4 ++--
client/src/locales/uz-UZ/core.js | 5 +++--
client/src/locales/zh-CN/core.js | 4 ++--
client/src/locales/zh-TW/core.js | 4 ++--
33 files changed, 112 insertions(+), 72 deletions(-)
diff --git a/client/src/api/notifications.js b/client/src/api/notifications.js
index b0456229..6d36332a 100755
--- a/client/src/api/notifications.js
+++ b/client/src/api/notifications.js
@@ -16,6 +16,9 @@ export const transformNotification = (notification) => ({
activityId: notification.actionId,
}
: notification),
+ ...(notification.createdAt && {
+ createdAt: new Date(notification.createdAt),
+ }),
});
/* Actions */
diff --git a/client/src/components/activities/Activities/Item.module.scss b/client/src/components/activities/Activities/Item.module.scss
index 37c92a31..7245597b 100644
--- a/client/src/components/activities/Activities/Item.module.scss
+++ b/client/src/components/activities/Activities/Item.module.scss
@@ -6,7 +6,6 @@
:global(#app) {
.author {
color: #17394d;
- display: inline-block;
font-weight: bold;
line-height: 20px;
}
@@ -21,7 +20,6 @@
.date {
color: #6b808c;
- display: inline-block;
font-size: 12px;
line-height: 20px;
}
diff --git a/client/src/components/notifications/NotificationsStep/Item.jsx b/client/src/components/notifications/NotificationsStep/Item.jsx
index 69c65b5b..ac424ab2 100644
--- a/client/src/components/notifications/NotificationsStep/Item.jsx
+++ b/client/src/components/notifications/NotificationsStep/Item.jsx
@@ -16,6 +16,7 @@ import entryActions from '../../../entry-actions';
import Paths from '../../../constants/Paths';
import { StaticUserIds } from '../../../constants/StaticUsers';
import { NotificationTypes } from '../../../constants/Enums';
+import TimeAgo from '../../common/TimeAgo';
import UserAvatar from '../../users/UserAvatar';
import styles from './Item.module.scss';
@@ -67,7 +68,7 @@ const Item = React.memo(({ id, onClose }) => {
toList: toListName,
}}
>
- {creatorUserName}
+ {creatorUserName}
{' moved '}
{cardName}
@@ -93,7 +94,7 @@ const Item = React.memo(({ id, onClose }) => {
card: cardName,
}}
>
- {creatorUserName}
+ {creatorUserName}
{` left a new comment «${commentText}» to `}
{cardName}
@@ -112,7 +113,7 @@ const Item = React.memo(({ id, onClose }) => {
card: cardName,
}}
>
- {creatorUserName}
+ {creatorUserName}
{` added you to `}
{cardName}
@@ -128,7 +129,12 @@ const Item = React.memo(({ id, onClose }) => {
return (
-
{contentNode}
+
+ {contentNode}
+
+
+
+
{{user}}0><1> تمت إضافة هذه البطاقة إلى {{list}}1>',
- userLeftNewCommentToCard: '{{user}} ترك تعليق جديد «{{comment}}» إلى <2>{{card}}2>',
- userMovedCardFromListToList: '{{user}} انتقل <2>{{card}}2> من {{fromList}} إلى {{toList}}',
+ userLeftNewCommentToCard: '<0>{{user}}0> ترك تعليق جديد «{{comment}}» إلى <2>{{card}}2>',
+ userMovedCardFromListToList:
+ '<0>{{user}}0> انتقل <2>{{card}}2> من {{fromList}} إلى {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> نُقلت هذه البطاقة من {{fromList}} إلى {{toList}}1>',
username: 'اسم المستخدم',
diff --git a/client/src/locales/bg-BG/core.js b/client/src/locales/bg-BG/core.js
index fb41911c..82bc7b41 100644
--- a/client/src/locales/bg-BG/core.js
+++ b/client/src/locales/bg-BG/core.js
@@ -154,9 +154,10 @@ export default {
title: 'Заглавие',
userActions_title: 'Потребителски действия',
userAddedThisCardToList: '<0>{{user}}0><1> добави тази карта в {{list}}1>',
- userLeftNewCommentToCard: '{{user}} остави нов коментар «{{comment}}» в <2>{{card}}2>',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0> остави нов коментар «{{comment}}» в <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} премести <2>{{card}}2> от {{fromList}} към {{toList}}',
+ '<0>{{user}}0> премести <2>{{card}}2> от {{fromList}} към {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> премести тази карта от {{fromList}} към {{toList}}1>',
username: 'Потребителско име',
diff --git a/client/src/locales/cs-CZ/core.js b/client/src/locales/cs-CZ/core.js
index 3fdfd8c7..5001995c 100644
--- a/client/src/locales/cs-CZ/core.js
+++ b/client/src/locales/cs-CZ/core.js
@@ -276,10 +276,13 @@ export default {
unsavedChanges: 'Neuložené změny',
uploadedImages: 'Nahrané obrázky',
userActions_title: 'Akce uživatele',
- userAddedThisCardToList: '{{user}} přidal kartu do {{list}}',
- userLeftNewCommentToCard: '{{user}} zanechal nový komentář «{{comment}}» k {{card}}',
- userMovedCardFromListToList: '{{user}} přesunul {{card}} z {{fromList}} do {{toList}}',
- userMovedThisCardFromListToList: '{{user}} přesunul tuto kartu z {{fromList}} do {{toList}}',
+ userAddedThisCardToList: '<0>{{user}}0><1> přidal kartu do {{list}}1>',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0> zanechal nový komentář «{{comment}}» k <2>{{card}}2>',
+ userMovedCardFromListToList:
+ '<0>{{user}}0> přesunul <2>{{card}}2> z {{fromList}} do {{toList}}',
+ userMovedThisCardFromListToList:
+ '<0>{{user}}0><1> přesunul tuto kartu z {{fromList}} do {{toList}}1>',
username: 'Uživatelské jméno',
users: 'Uživatelé',
viewer: 'Prohlížeč',
diff --git a/client/src/locales/da-DK/core.js b/client/src/locales/da-DK/core.js
index c8d9d922..508b726a 100644
--- a/client/src/locales/da-DK/core.js
+++ b/client/src/locales/da-DK/core.js
@@ -155,9 +155,10 @@ export default {
title: 'Overskrift',
userActions_title: 'Brugerhandlinger',
userAddedThisCardToList: '<0>{{user}}0><1> tilføjede kortet til {{list}}1>',
- userLeftNewCommentToCard: '{{user}} skrevet en ny kommentar «{{comment}}» på <2>{{card}}2>',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0> skrevet en ny kommentar «{{comment}}» på <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} flyttede <2>{{card}}2> fra {{fromList}} til {{toList}}',
+ '<0>{{user}}0> flyttede <2>{{card}}2> fra {{fromList}} til {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> flyttede kortet fra {{fromList}} til {{toList}}1>',
username: 'Brugernavn',
diff --git a/client/src/locales/de-DE/core.js b/client/src/locales/de-DE/core.js
index 022e3ffc..57ff436e 100644
--- a/client/src/locales/de-DE/core.js
+++ b/client/src/locales/de-DE/core.js
@@ -298,9 +298,9 @@ export default {
userActions_title: 'Benutzeraktionen',
userAddedThisCardToList: '<0>{{user}}0><1> hat diese Karte hinzugefügt zu {{list}}1>',
userLeftNewCommentToCard:
- '{{user}} hat einen neuen Kommentar verfasst: «{{comment}}» in <2>{{card}}2>',
+ '<0>{{user}}0> hat einen neuen Kommentar verfasst: «{{comment}}» in <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} bewegte <2>{{card}}2> von {{fromList}} nach {{toList}}',
+ '<0>{{user}}0> bewegte <2>{{card}}2> von {{fromList}} nach {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> bewegte diese Karte von {{fromList}} nach {{toList}}1>',
username: 'Benutzername',
diff --git a/client/src/locales/en-GB/core.js b/client/src/locales/en-GB/core.js
index 2cb913e3..0be9ccb0 100644
--- a/client/src/locales/en-GB/core.js
+++ b/client/src/locales/en-GB/core.js
@@ -288,14 +288,16 @@ export default {
userActions_title: 'User Actions',
userAddedThisCardToList: '<0>{{user}}0><1> added this card to {{list}}1>',
userAddedUserToThisCard: '<0>{{actorUser}}0><1> added {{addedUser}} to this card1>',
- userAddedYouToCard: '{{user}} added you to <2>{{card}}2>',
+ userAddedYouToCard: '<0>{{user}}0> added you to <2>{{card}}2>',
userCompletedTaskOnThisCard: '<0>{{user}}0><1> completed {{task}} on this card1>',
userJoinedThisCard: `<0>{{user}}0><1> joined this card1>`,
- userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}2>',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0> left a new comment «{{comment}}» to <2>{{card}}2>',
userLeftThisCard: '<0>{{user}}0><1> left this card1>',
userMarkedTaskIncompleteOnThisCard:
'<0>{{user}}0><1> marked {{task}} incomplete on this card1>',
- userMovedCardFromListToList: '{{user}} moved <2>{{card}}2> from {{fromList}} to {{toList}}',
+ userMovedCardFromListToList:
+ '<0>{{user}}0> moved <2>{{card}}2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> moved this card from {{fromList}} to {{toList}}1>',
userRemovedUserFromThisCard:
diff --git a/client/src/locales/en-US/core.js b/client/src/locales/en-US/core.js
index 28cae078..cc858c3c 100644
--- a/client/src/locales/en-US/core.js
+++ b/client/src/locales/en-US/core.js
@@ -283,14 +283,16 @@ export default {
userActions_title: 'User Actions',
userAddedThisCardToList: '<0>{{user}}0><1> added this card to {{list}}1>',
userAddedUserToThisCard: '<0>{{actorUser}}0><1> added {{addedUser}} to this card1>',
- userAddedYouToCard: '{{user}} added you to <2>{{card}}2>',
+ userAddedYouToCard: '<0>{{user}}0> added you to <2>{{card}}2>',
userCompletedTaskOnThisCard: '<0>{{user}}0><1> completed {{task}} on this card1>',
userJoinedThisCard: `<0>{{user}}0><1> joined this card1>`,
- userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}2>',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0> left a new comment «{{comment}}» to <2>{{card}}2>',
userLeftThisCard: '<0>{{user}}0><1> left this card1>',
userMarkedTaskIncompleteOnThisCard:
'<0>{{user}}0><1> marked {{task}} incomplete on this card1>',
- userMovedCardFromListToList: '{{user}} moved <2>{{card}}2> from {{fromList}} to {{toList}}',
+ userMovedCardFromListToList:
+ '<0>{{user}}0> moved <2>{{card}}2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> moved this card from {{fromList}} to {{toList}}1>',
userRemovedUserFromThisCard:
diff --git a/client/src/locales/es-ES/core.js b/client/src/locales/es-ES/core.js
index 4fbc3bfe..41bb79e2 100644
--- a/client/src/locales/es-ES/core.js
+++ b/client/src/locales/es-ES/core.js
@@ -119,8 +119,9 @@ export default {
userActions_title: 'Acciones de Usuario',
userAddedThisCardToList: '<0>{{user}}0><1> añadido a esta tarjeta en {{list}}1>',
userLeftNewCommentToCard:
- '{{user}} dejó un nuevo comentario «{{comment}}» en <2>{{card}}2>',
- userMovedCardFromListToList: '{{user}} movió <2>{{card}}2> de {{fromList}} a {{toList}}',
+ '<0>{{user}}0> dejó un nuevo comentario «{{comment}}» en <2>{{card}}2>',
+ userMovedCardFromListToList:
+ '<0>{{user}}0> movió <2>{{card}}2> de {{fromList}} a {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> movió esta tarjeta de {{fromList}} a {{toList}}1>',
username: 'Nombre de usuario',
diff --git a/client/src/locales/fa-IR/core.js b/client/src/locales/fa-IR/core.js
index c0a42211..795ca8e0 100644
--- a/client/src/locales/fa-IR/core.js
+++ b/client/src/locales/fa-IR/core.js
@@ -154,9 +154,10 @@ export default {
title: 'عنوان',
userActions_title: 'اقدامات کاربر',
userAddedThisCardToList: '<0>{{user}}0><1> این کارت را به {{list}} اضافه کرد1>',
- userLeftNewCommentToCard: '{{user}} نظر جدید «{{comment}}» را به <2>{{card}}2> اضافه کرد',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0> نظر جدید «{{comment}}» را به <2>{{card}}2> اضافه کرد',
userMovedCardFromListToList:
- '{{user}} <2>{{card}}2> را از {{fromList}} به {{toList}} منتقل کرد',
+ '<0>{{user}}0> <2>{{card}}2> را از {{fromList}} به {{toList}} منتقل کرد',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> این کارت را از {{fromList}} به {{toList}} منتقل کرد1>',
username: 'نام کاربری',
diff --git a/client/src/locales/fr-FR/core.js b/client/src/locales/fr-FR/core.js
index ea8e8e6f..74356971 100644
--- a/client/src/locales/fr-FR/core.js
+++ b/client/src/locales/fr-FR/core.js
@@ -153,13 +153,13 @@ export default {
time: 'Temps',
title: 'Titre',
userActions_title: "Actions de l'utilisateur",
- userAddedThisCardToList: '<0> {{user}} 0> <1> a ajouté cette carte à {{list}} 1>',
+ userAddedThisCardToList: '<0>{{user}}0><1> a ajouté cette carte à {{list}}1>',
userLeftNewCommentToCard:
- '{{user}} a laissé un nouveau commentaire {{comment}} à <2> {{card}} 2>',
+ '<0>{{user}}0> a laissé un nouveau commentaire {{comment}} à <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} a déplacé <2> {{card}} 2> de {{fromList}} vers {{toList}}',
+ '<0>{{user}}0> a déplacé <2>{{card}}2> de {{fromList}} vers {{toList}}',
userMovedThisCardFromListToList:
- '<0> {{user}} 0> <1> a déplacé cette carte de {{fromList}} vers {{toList}} 1>',
+ '<0>{{user}}0><1> a déplacé cette carte de {{fromList}} vers {{toList}}1>',
username: "Nom d'utilisateur",
users: 'Utilisateurs',
viewer: 'Spectateur',
diff --git a/client/src/locales/hu-HU/core.js b/client/src/locales/hu-HU/core.js
index 33254a57..6f9fe79b 100644
--- a/client/src/locales/hu-HU/core.js
+++ b/client/src/locales/hu-HU/core.js
@@ -154,9 +154,9 @@ export default {
userAddedThisCardToList:
'<0>{{user}}0><1> hozzáadta ezt a kártyát a következőhöz: {{list}}1>',
userLeftNewCommentToCard:
- '{{user}} új kommentet hagyott itt: «{{comment}}» a következő kártyán: <2>{{card}}2>',
+ '<0>{{user}}0> új kommentet hagyott itt: «{{comment}}» a következő kártyán: <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} áthelyezte ezt a kártyát innen: {{fromList}} ide: {{toList}}',
+ '<0>{{user}}0> áthelyezte ezt a kártyát innen: {{fromList}} ide: {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> áthelyezte ezt a kártyát innen: {{fromList}} ide: {{toList}}1>',
username: 'Felhasználónév',
diff --git a/client/src/locales/id-ID/core.js b/client/src/locales/id-ID/core.js
index 81550eef..0c064f40 100644
--- a/client/src/locales/id-ID/core.js
+++ b/client/src/locales/id-ID/core.js
@@ -148,9 +148,9 @@ export default {
title: 'Judul',
userActions_title: 'Aksi Pengguna',
userAddedThisCardToList: '<0>{{user}}0><1> menambahkan kartu ini ke {{list}}1>',
- userLeftNewCommentToCard: '{{user}} mengomentari «{{comment}}» di <2>{{card}}2>',
+ userLeftNewCommentToCard: '<0>{{user}}0> mengomentari «{{comment}}» di <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} memindahkan <2>{{card}}2> dari {{fromList}} ke {{toList}}',
+ '<0>{{user}}0> memindahkan <2>{{card}}2> dari {{fromList}} ke {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> memindahkan kartu ini dari {{fromList}} ke {{toList}}1>',
username: 'Username',
diff --git a/client/src/locales/it-IT/core.js b/client/src/locales/it-IT/core.js
index 4df38ea7..21e3bb44 100644
--- a/client/src/locales/it-IT/core.js
+++ b/client/src/locales/it-IT/core.js
@@ -286,9 +286,9 @@ export default {
userActions_title: 'Azioni utente',
userAddedThisCardToList: '<0>{{user}}0><1> ha aggiunto questa scheda a {{list}}1>',
userLeftNewCommentToCard:
- '{{user}} ha lasciato un commento «{{comment}}» a <2>{{scheda}}2>',
+ '<0>{{user}}0> ha lasciato un commento «{{comment}}» a <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} ha spostato <2>{{scheda}}2> da {{fromList}} a {{toList}}',
+ '<0>{{user}}0> ha spostato <2>{{card}}2> da {{fromList}} a {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> ha spostato questa scheda da {{fromList}} a {{toList}}1>',
username: 'Username',
diff --git a/client/src/locales/ja-JP/core.js b/client/src/locales/ja-JP/core.js
index c87b60b2..d6946dcb 100644
--- a/client/src/locales/ja-JP/core.js
+++ b/client/src/locales/ja-JP/core.js
@@ -148,11 +148,11 @@ export default {
userActions_title: 'ユーザーのアクション',
userAddedThisCardToList: '<0>{{user}}0> 様が <1>{{list}} をこのカードに追加しました1>',
userLeftNewCommentToCard:
- '{{user}} 様が <2> {{card}} 2> に新しいコメント «{{comment}}» を残しました',
+ '<0>{{user}}0> 様が <2>{{card}}2> に新しいコメント «{{comment}}» を残しました',
userMovedCardFromListToList:
- '{{user}} 様が <2> {{card}} 2> を {{fromList}} から {{toList}} に移動しました',
+ '<0>{{user}}0> 様が <2>{{card}}2> を {{fromList}} から {{toList}} に移動しました',
userMovedThisCardFromListToList:
- '<0>{{user}}0> <1>様がこのカードを {{fromList}} から {{toList}} に移動しました1>',
+ '<0>{{user}}0><1> 様がこのカードを {{fromList}} から {{toList}} に移動しました1>',
username: 'ユーザー名',
users: 'ユーザー',
viewer: 'ビューア',
diff --git a/client/src/locales/ko-KR/core.js b/client/src/locales/ko-KR/core.js
index 42991a28..590a4ffe 100644
--- a/client/src/locales/ko-KR/core.js
+++ b/client/src/locales/ko-KR/core.js
@@ -152,9 +152,10 @@ export default {
title: '제목',
userActions_title: '사용자 작업',
userAddedThisCardToList: '<0>{{user}}0><1>님이 이 카드를 {{list}}에 추가했습니다1>',
- userLeftNewCommentToCard: '{{user}}님이 <2>{{card}}2>에 새 댓글 «{{comment}}»을 남겼습니다',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0>님이 <2>{{card}}2>에 새 댓글 «{{comment}}»을 남겼습니다',
userMovedCardFromListToList:
- '{{user}}님이 <2>{{card}}2>를 {{fromList}}에서 {{toList}}로 이동했습니다',
+ '<0>{{user}}0>님이 <2>{{card}}2>를 {{fromList}}에서 {{toList}}로 이동했습니다',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> 님이 {{fromList}}에서 {{toList}}로 이 카드를 옮겼습니다1>',
username: '사용자 이름',
diff --git a/client/src/locales/nl-NL/core.js b/client/src/locales/nl-NL/core.js
index 6455afa2..6f0f2697 100644
--- a/client/src/locales/nl-NL/core.js
+++ b/client/src/locales/nl-NL/core.js
@@ -149,9 +149,9 @@ export default {
userActions_title: 'Gebruikersacties',
userAddedThisCardToList: '<0>{{user}}0><1> heeft deze kaart toegevoegd aan {{list}}1>',
userLeftNewCommentToCard:
- '{{user}} heeft een nieuwe opmerking achtergelaten «{{comment}}» bij <2>{{card}}2>',
+ '<0>{{user}}0> heeft een nieuwe opmerking achtergelaten «{{comment}}» bij <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} heeft <2>{{card}}2> verplaatst van {{fromList}} naar {{toList}}',
+ '<0>{{user}}0> heeft <2>{{card}}2> verplaatst van {{fromList}} naar {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> heeft deze kaart verplaatst van {{fromList}} naar {{toList}}1>',
username: 'Gebruikersnaam',
diff --git a/client/src/locales/pl-PL/core.js b/client/src/locales/pl-PL/core.js
index 2b68141b..70ac7cdf 100644
--- a/client/src/locales/pl-PL/core.js
+++ b/client/src/locales/pl-PL/core.js
@@ -149,9 +149,10 @@ export default {
title: 'Tytuł',
userActions_title: 'Akcje użytkownika',
userAddedThisCardToList: '<0>{{user}}0><1> dodał tę kartę w {{list}}1>',
- userLeftNewCommentToCard: '{{user}} zamieścił nowy komentarz «{{comment}}» w <2>{{card}}2>',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0> zamieścił nowy komentarz «{{comment}}» w <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} przeniósł <2>{{card}}2> z {{fromList}} do {{toList}}',
+ '<0>{{user}}0> przeniósł <2>{{card}}2> z {{fromList}} do {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> przeniósł tę kartę z {{fromList}} do {{toList}}1>',
username: 'Nazwa Użytkownika',
diff --git a/client/src/locales/pt-BR/core.js b/client/src/locales/pt-BR/core.js
index 549f3def..6e088dad 100644
--- a/client/src/locales/pt-BR/core.js
+++ b/client/src/locales/pt-BR/core.js
@@ -149,8 +149,9 @@ export default {
userActions_title: 'Ações do Usuário',
userAddedThisCardToList: '<0>{{user}}0><1> adicionou este cartão a {{list}}1>',
userLeftNewCommentToCard:
- '{{user}} deixou um novo comentário «{{comment}}» em <2>{{card}}2>',
- userMovedCardFromListToList: '{{user}} moveu <2>{{card}}2> de {{fromList}} para {{toList}}',
+ '<0>{{user}}0> deixou um novo comentário «{{comment}}» em <2>{{card}}2>',
+ userMovedCardFromListToList:
+ '<0>{{user}}0> moveu <2>{{card}}2> de {{fromList}} para {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> moveu este cartão de {{fromList}} para {{toList}}1>',
username: 'Nome de usuário',
diff --git a/client/src/locales/ro-RO/core.js b/client/src/locales/ro-RO/core.js
index 6e59df46..412cbea5 100644
--- a/client/src/locales/ro-RO/core.js
+++ b/client/src/locales/ro-RO/core.js
@@ -149,9 +149,9 @@ export default {
userActions_title: 'Acțiunile utilizatorului',
userAddedThisCardToList: '<0>{{user}}0><1> a adăugat acest card în {{list}}1>',
userLeftNewCommentToCard:
- '{{user}} a lăsat un nou comentariu «{{comment}}» în <2>{{card}}2>',
+ '<0>{{user}}0> a lăsat un nou comentariu «{{comment}}» în <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} a mutat <2>{{card}}2> din {{fromList}} în {{toList}}',
+ '<0>{{user}}0> a mutat <2>{{card}}2> din {{fromList}} în {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> a mutat aceast card din {{fromList}} în {{toList}}1>',
username: 'Nume utilizator',
diff --git a/client/src/locales/ru-RU/core.js b/client/src/locales/ru-RU/core.js
index 35aa094c..2ee40787 100644
--- a/client/src/locales/ru-RU/core.js
+++ b/client/src/locales/ru-RU/core.js
@@ -285,9 +285,10 @@ export default {
uploadedImages: 'Загруженные изображения',
userActions_title: 'Действия с пользователем',
userAddedThisCardToList: '<0>{{user}}0><1> добавил(а) эту карточку в {{list}}1>',
- userLeftNewCommentToCard: '{{user}} оставил(а) комментарий «{{comment}}» к <2>{{card}}2>',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0> оставил(а) комментарий «{{comment}}» к <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} переместил(а) <2>{{card}}2> из {{fromList}} в {{toList}}',
+ '<0>{{user}}0> переместил(а) <2>{{card}}2> из {{fromList}} в {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> переместил(а) эту карточку из {{fromList}} в {{toList}}1>',
username: 'Имя пользователя',
diff --git a/client/src/locales/sk-SK/core.js b/client/src/locales/sk-SK/core.js
index db34b94c..b344cabd 100644
--- a/client/src/locales/sk-SK/core.js
+++ b/client/src/locales/sk-SK/core.js
@@ -133,8 +133,10 @@ export default {
title: 'Názov',
userActions_title: 'Akcie na používateľovi',
userAddedThisCardToList: '<0>{{user}}0><1> pridal kartu do {{list}}1>',
- userLeftNewCommentToCard: '{{user}} zanechal nový komentár «{{comment}}» k <2>{{card}}2>',
- userMovedCardFromListToList: '{{user}} presunul <2>{{card}}2> z {{fromList}} do {{toList}}',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0> zanechal nový komentár «{{comment}}» k <2>{{card}}2>',
+ userMovedCardFromListToList:
+ '<0>{{user}}0> presunul <2>{{card}}2> z {{fromList}} do {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> presunul túto kartu z {{fromList}} do {{toList}}1>',
username: 'Používateľské meno',
diff --git a/client/src/locales/sr-Cyrl-CS/core.js b/client/src/locales/sr-Cyrl-CS/core.js
index e687dba9..87a47917 100644
--- a/client/src/locales/sr-Cyrl-CS/core.js
+++ b/client/src/locales/sr-Cyrl-CS/core.js
@@ -153,9 +153,10 @@ export default {
title: 'Наслов',
userActions_title: 'Корисничке радње',
userAddedThisCardToList: '<0>{{user}}0><1> је додао ову картицу на {{list}}1>',
- userLeftNewCommentToCard: '{{user}} је оставио нови коментар «{{comment}}» у <2>{{card}}2>',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0> је оставио нови коментар «{{comment}}» у <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} је преместио <2>{{card}}2> са {{fromList}} у {{toList}}',
+ '<0>{{user}}0> је преместио <2>{{card}}2> са {{fromList}} у {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> је преместио ову картицу са {{fromList}} на {{toList}}1>',
username: 'Корисничко име',
diff --git a/client/src/locales/sr-Latn-CS/core.js b/client/src/locales/sr-Latn-CS/core.js
index 45c9b949..3bdf748b 100644
--- a/client/src/locales/sr-Latn-CS/core.js
+++ b/client/src/locales/sr-Latn-CS/core.js
@@ -153,9 +153,10 @@ export default {
title: 'Naslov',
userActions_title: 'Korisničke radnje',
userAddedThisCardToList: '<0>{{user}}0><1> je dodao ovu karticu na {{list}}1>',
- userLeftNewCommentToCard: '{{user}} je ostavio novi komentar «{{comment}}» u <2>{{card}}2>',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0> je ostavio novi komentar «{{comment}}» u <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} je premestio <2>{{card}}2> sa {{fromList}} u {{toList}}',
+ '<0>{{user}}0> je premestio <2>{{card}}2> sa {{fromList}} u {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> je premestio ovu karticu sa {{fromList}} na {{toList}}1>',
username: 'Korisničko ime',
diff --git a/client/src/locales/sv-SE/core.js b/client/src/locales/sv-SE/core.js
index 845199b5..0a8c729e 100644
--- a/client/src/locales/sv-SE/core.js
+++ b/client/src/locales/sv-SE/core.js
@@ -134,9 +134,10 @@ export default {
title: 'Titel',
userActions_title: 'Användaråtgärder',
userAddedThisCardToList: '<0>{{user}}0><1> lade till detta kort i {{list}}1>',
- userLeftNewCommentToCard: '{{user}} lämnade en ny kommentar «{{comment}}» på <2>{{card}}2>',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0> lämnade en ny kommentar «{{comment}}» på <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} flyttade <2>{{card}}2> från {{fromList}} till {{toList}}',
+ '<0>{{user}}0> flyttade <2>{{card}}2> från {{fromList}} till {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> flyttade detta kort från {{fromList}} till {{toList}}1>',
username: 'Användarnamn',
diff --git a/client/src/locales/tr-TR/core.js b/client/src/locales/tr-TR/core.js
index 83cbd18a..b667f2b5 100644
--- a/client/src/locales/tr-TR/core.js
+++ b/client/src/locales/tr-TR/core.js
@@ -136,9 +136,9 @@ export default {
userActions_title: 'Kullanıcı İşlemleri',
userAddedThisCardToList: '<0>{{user}}0><1> bu kartı {{list}1> listesine ekledi',
userLeftNewCommentToCard:
- '{{user}} yeni bir yorum yazdı: <2>{{card}2> kartına «{{comment}}»',
+ '<0>{{user}}0> yeni bir yorum yazdı: <2>{{card}2> kartına «{{comment}}»',
userMovedCardFromListToList:
- '{{user}}, <2>{{card}>2> kartını {{fromList}} listesinden {{toList}} listesine taşıdı',
+ '<0>{{user}}0>, <2>{{card}>2> kartını {{fromList}} listesinden {{toList}} listesine taşıdı',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> bu kartı {{fromList}} konumundan {{toList}}1> konumuna taşıdı',
username: 'kullanıcı adı',
diff --git a/client/src/locales/uk-UA/core.js b/client/src/locales/uk-UA/core.js
index ae720306..b3b384ea 100644
--- a/client/src/locales/uk-UA/core.js
+++ b/client/src/locales/uk-UA/core.js
@@ -283,9 +283,9 @@ export default {
userActions_title: 'Дії користувача',
userAddedThisCardToList: '<0>{{user}}0><1> додав(ла) цю картку до {{list}}1>',
userLeftNewCommentToCard:
- '{{user}} залишив(ла) новий коментар «{{comment}}» до <2>{{card}}2>',
+ '<0>{{user}}0> залишив(ла) новий коментар «{{comment}}» до <2>{{card}}2>',
userMovedCardFromListToList:
- '{{user}} перемістив(ла) <2>{{card}}2> з {{fromList}} в {{toList}}',
+ '<0>{{user}}0> перемістив(ла) <2>{{card}}2> з {{fromList}} в {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> перемістив(ла) цю картку з {{fromList}} в {{toList}}1>',
username: "Ім'я користувача",
diff --git a/client/src/locales/uz-UZ/core.js b/client/src/locales/uz-UZ/core.js
index 1543d1b4..af7245b4 100644
--- a/client/src/locales/uz-UZ/core.js
+++ b/client/src/locales/uz-UZ/core.js
@@ -131,9 +131,10 @@ export default {
title: 'Sarlavha',
userActions_title: 'Foydalanuvchi Amallari',
userAddedThisCardToList: "<1>Ushbu kartani {{list}} ga1><0>{{user}}0> qo'shdi",
- userLeftNewCommentToCard: '{{user}} <2>{{card}}2> ga yangi izoh qoldirdi «{{comment}}»',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0> <2>{{card}}2> ga yangi izoh qoldirdi «{{comment}}»',
userMovedCardFromListToList:
- "{{user}} <2>{{card}}2> ni {{fromList}} dan {{toList}} ga ko'chirdi",
+ "<0>{{user}}0> <2>{{card}}2> ni {{fromList}} dan {{toList}} ga ko'chirdi",
userMovedThisCardFromListToList:
"<0>{{user}}0><1> ushbu kartani {{fromList}} dan {{toList}}1> ga ko'chirdi",
username: 'Foydalanuvchi nomi',
diff --git a/client/src/locales/zh-CN/core.js b/client/src/locales/zh-CN/core.js
index 5f14367b..d8f3485d 100644
--- a/client/src/locales/zh-CN/core.js
+++ b/client/src/locales/zh-CN/core.js
@@ -144,9 +144,9 @@ export default {
title: '标题',
userActions_title: '用户操作',
userAddedThisCardToList: '<0>{{user}}0><1> 向列表 {{list}} 添加了该卡片1>',
- userLeftNewCommentToCard: '{{user}} 给 {{card}} 添加了一个新评论 «{{comment}}»',
+ userLeftNewCommentToCard: '<0>{{user}}0> 给 {{card}} 添加了一个新评论 «{{comment}}»',
userMovedCardFromListToList:
- '{{user}} 将卡片 <2>{{card}}2> 从 {{fromList}} 移动到 {{toList}}',
+ '<0>{{user}}0> 将卡片 <2>{{card}}2> 从 {{fromList}} 移动到 {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> 将该卡片从 {{fromList}} 移动到 {{toList}}1>',
username: '用户名',
diff --git a/client/src/locales/zh-TW/core.js b/client/src/locales/zh-TW/core.js
index 772cdc16..7b2e9b69 100644
--- a/client/src/locales/zh-TW/core.js
+++ b/client/src/locales/zh-TW/core.js
@@ -144,9 +144,9 @@ export default {
title: '標題',
userActions_title: '使用者操作',
userAddedThisCardToList: '<0>{{user}}0><1> 向列表 {{list}} 添加了該卡片1>',
- userLeftNewCommentToCard: '{{user}} 給 {{card}} 添加了一條新評論 「{{comment}}」',
+ userLeftNewCommentToCard: '<0>{{user}}0> 給 {{card}} 添加了一條新評論 「{{comment}}」',
userMovedCardFromListToList:
- '{{user}} 將卡片 <2>{{card}}2> 從 {{fromList}} 移動到 {{toList}}',
+ '<0>{{user}}0> 將卡片 <2>{{card}}2> 從 {{fromList}} 移動到 {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0><1> 將該卡片從 {{fromList}} 移動到 {{toList}}1>',
username: '使用者名稱',
From 2db75ed420c2ddb52d106b4b136cad2ec4133c5a Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Wed, 21 May 2025 13:25:54 +0200
Subject: [PATCH 012/108] chore: Downgrade and pin i18next version
---
client/package-lock.json | 20 +++++++++++++-------
client/package.json | 2 +-
2 files changed, 14 insertions(+), 8 deletions(-)
diff --git a/client/package-lock.json b/client/package-lock.json
index 924bb434..2c27be92 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -42,7 +42,7 @@
"highlightjs-zenscript": "^2.0.0",
"hightlightjs-papyrus": "^0.0.4",
"history": "^5.3.0",
- "i18next": "^23.16.8",
+ "i18next": "23.15.2",
"i18next-browser-languagedetector": "^8.1.0",
"initials": "^3.1.2",
"javascript-time-ago": "^2.5.11",
@@ -4756,11 +4756,17 @@
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
"dev": true
},
+ "node_modules/@types/prop-types": {
+ "version": "15.7.14",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
+ "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="
+ },
"node_modules/@types/react": {
- "version": "19.1.3",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz",
- "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==",
+ "version": "18.3.22",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz",
+ "integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==",
"dependencies": {
+ "@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
@@ -8577,9 +8583,9 @@
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="
},
"node_modules/i18next": {
- "version": "23.16.8",
- "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
- "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
+ "version": "23.15.2",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.15.2.tgz",
+ "integrity": "sha512-zcPSWzCvw6uKnuYHIqs4W7hTuB9e3AFcSdZgvCWoPXIZsBjBd4djN2/2uOHIB+1DFFkQnMBXvhNg7J3WyCuywQ==",
"funding": [
{
"type": "individual",
diff --git a/client/package.json b/client/package.json
index 18991335..ac1528d2 100755
--- a/client/package.json
+++ b/client/package.json
@@ -113,7 +113,7 @@
"highlightjs-zenscript": "^2.0.0",
"hightlightjs-papyrus": "^0.0.4",
"history": "^5.3.0",
- "i18next": "^23.16.8",
+ "i18next": "23.15.2",
"i18next-browser-languagedetector": "^8.1.0",
"initials": "^3.1.2",
"javascript-time-ago": "^2.5.11",
From 9fd9370120db99610da841c5d85374bd78223ea7 Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Thu, 22 May 2025 11:16:08 +0200
Subject: [PATCH 013/108] fix: Remove curly bracket typo from HTML links
Closes #1142
---
server/api/helpers/actions/create-one.js | 2 +-
server/api/helpers/notifications/create-one.js | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/server/api/helpers/actions/create-one.js b/server/api/helpers/actions/create-one.js
index ba1e6494..d90b4020 100644
--- a/server/api/helpers/actions/create-one.js
+++ b/server/api/helpers/actions/create-one.js
@@ -19,7 +19,7 @@ const buildTitle = (action, t) => {
const buildBodyByFormat = (board, card, action, actorUser, t) => {
const markdownCardLink = `[${escapeMarkdown(card.name)}](${sails.config.custom.baseUrl}/cards/${card.id})`;
- const htmlCardLink = `${escapeHtml(card.name)} `;
+ const htmlCardLink = `${escapeHtml(card.name)} `;
switch (action.type) {
case Action.Types.CREATE_CARD: {
diff --git a/server/api/helpers/notifications/create-one.js b/server/api/helpers/notifications/create-one.js
index 990e6a41..70aedbb9 100644
--- a/server/api/helpers/notifications/create-one.js
+++ b/server/api/helpers/notifications/create-one.js
@@ -21,7 +21,7 @@ const buildTitle = (notification, t) => {
const buildBodyByFormat = (board, card, notification, actorUser, t) => {
const markdownCardLink = `[${escapeMarkdown(card.name)}](${sails.config.custom.baseUrl}/cards/${card.id})`;
- const htmlCardLink = `${escapeHtml(card.name)} `;
+ const htmlCardLink = `${escapeHtml(card.name)} `;
switch (notification.type) {
case Notification.Types.MOVE_CARD: {
@@ -110,8 +110,8 @@ const buildAndSendNotifications = async (services, board, card, notification, ac
// TODO: use templates (views) to build html
const buildAndSendEmail = async (board, card, notification, actorUser, notifiableUser, t) => {
- const cardLink = `${escapeHtml(card.name)} `;
- const boardLink = `${escapeHtml(board.name)} `;
+ const cardLink = `${escapeHtml(card.name)} `;
+ const boardLink = `${escapeHtml(board.name)} `;
let html;
switch (notification.type) {
From 777ff467f3019b52290fb4d54b1588e7706ab506 Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Thu, 22 May 2025 16:40:46 +0200
Subject: [PATCH 014/108] fix: Prevent upgrade script from running other
migrations
---
server/db/upgrade.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/server/db/upgrade.js b/server/db/upgrade.js
index d3ca37ed..0047aeee 100644
--- a/server/db/upgrade.js
+++ b/server/db/upgrade.js
@@ -88,7 +88,9 @@ const upgradeDatabase = async () => {
ALTER TABLE notification SET SCHEMA v1;
`);
- await trx.migrate.up();
+ await trx.migrate.up({
+ name: '20250228000022_version_2.js',
+ });
const users = await trx('user_account').withSchema('v1').whereNull('deleted_at');
From 86cfd155f2c2e13c4d5f2ca424db5f0427917c59 Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Thu, 22 May 2025 23:14:46 +0200
Subject: [PATCH 015/108] feat: Add board activity log
---
client/src/actions/activities.js | 39 +++-
client/src/api/activities.js | 11 +-
.../BoardActivitiesModal.jsx | 73 ++++++
.../BoardActivitiesModal.module.scss | 18 ++
.../activities/BoardActivitiesModal/Item.jsx | 220 ++++++++++++++++++
.../Item.module.scss | 7 +-
.../activities/BoardActivitiesModal/index.js | 8 +
.../CardActivities.jsx} | 6 +-
.../CardActivities.module.scss} | 0
.../{Activities => CardActivities}/Item.jsx | 52 ++---
.../CardActivities/Item.module.scss | 31 +++
.../{Activities => CardActivities}/index.js | 4 +-
client/src/components/boards/Board/Board.jsx | 18 +-
.../BoardActions/RightSide/ActionsStep.jsx | 29 ++-
.../cards/CardModal/Communication.jsx | 4 +-
.../NotificationsStep/Item.module.scss | 2 +-
client/src/constants/ActionTypes.js | 9 +-
client/src/constants/EntryActionTypes.js | 1 +
client/src/constants/ModalTypes.js | 2 +
client/src/entry-actions/activities.js | 6 +
client/src/entry-actions/modals.js | 8 +
client/src/hooks/use-closable-modal.jsx | 1 +
client/src/locales/ar-YE/core.js | 4 +-
client/src/locales/bg-BG/core.js | 4 +-
client/src/locales/cs-CZ/core.js | 4 +-
client/src/locales/da-DK/core.js | 4 +-
client/src/locales/de-DE/core.js | 4 +-
client/src/locales/en-GB/core.js | 26 ++-
client/src/locales/en-US/core.js | 26 ++-
client/src/locales/es-ES/core.js | 4 +-
client/src/locales/fa-IR/core.js | 4 +-
client/src/locales/fr-FR/core.js | 4 +-
client/src/locales/hu-HU/core.js | 5 +-
client/src/locales/id-ID/core.js | 4 +-
client/src/locales/it-IT/core.js | 4 +-
client/src/locales/ja-JP/core.js | 4 +-
client/src/locales/ko-KR/core.js | 4 +-
client/src/locales/nl-NL/core.js | 4 +-
client/src/locales/pl-PL/core.js | 4 +-
client/src/locales/pt-BR/core.js | 4 +-
client/src/locales/ro-RO/core.js | 4 +-
client/src/locales/ru-RU/core.js | 4 +-
client/src/locales/sk-SK/core.js | 4 +-
client/src/locales/sr-Cyrl-CS/core.js | 4 +-
client/src/locales/sr-Latn-CS/core.js | 4 +-
client/src/locales/sv-SE/core.js | 4 +-
client/src/locales/tr-TR/core.js | 4 +-
client/src/locales/uk-UA/core.js | 4 +-
client/src/locales/uz-UZ/core.js | 4 +-
client/src/locales/zh-CN/core.js | 7 +-
client/src/locales/zh-TW/core.js | 8 +-
client/src/models/Activity.js | 8 +-
client/src/models/Board.js | 54 +++++
client/src/models/Card.js | 5 +-
client/src/models/User.js | 3 +-
client/src/sagas/core/services/activities.js | 47 +++-
client/src/sagas/core/watchers/activities.js | 3 +
client/src/selectors/boards.js | 19 ++
.../api/controllers/actions/index-in-board.js | 68 ++++++
.../actions/{index.js => index-in-card.js} | 0
server/api/helpers/actions/create-one.js | 11 +-
server/api/helpers/boards/delete-related.js | 9 +
.../helpers/card-memberships/create-one.js | 1 +
.../helpers/card-memberships/delete-one.js | 1 +
server/api/helpers/cards/create-one.js | 1 +
server/api/helpers/cards/duplicate-one.js | 1 +
server/api/helpers/cards/update-one.js | 1 +
server/api/helpers/tasks/update-one.js | 1 +
.../api/hooks/query-methods/models/Action.js | 15 ++
server/api/models/Action.js | 4 +
server/config/routes.js | 3 +-
.../20250522151122_add_board_activity_log.js | 30 +++
72 files changed, 833 insertions(+), 169 deletions(-)
create mode 100755 client/src/components/activities/BoardActivitiesModal/BoardActivitiesModal.jsx
create mode 100644 client/src/components/activities/BoardActivitiesModal/BoardActivitiesModal.module.scss
create mode 100755 client/src/components/activities/BoardActivitiesModal/Item.jsx
rename client/src/components/activities/{Activities => BoardActivitiesModal}/Item.module.scss (90%)
create mode 100755 client/src/components/activities/BoardActivitiesModal/index.js
rename client/src/components/activities/{Activities/Activities.jsx => CardActivities/CardActivities.jsx} (92%)
rename client/src/components/activities/{Activities/Activities.module.scss => CardActivities/CardActivities.module.scss} (100%)
rename client/src/components/activities/{Activities => CardActivities}/Item.jsx (82%)
create mode 100644 client/src/components/activities/CardActivities/Item.module.scss
rename client/src/components/activities/{Activities => CardActivities}/index.js (66%)
create mode 100755 server/api/controllers/actions/index-in-board.js
rename server/api/controllers/actions/{index.js => index-in-card.js} (100%)
create mode 100644 server/db/migrations/20250522151122_add_board_activity_log.js
diff --git a/client/src/actions/activities.js b/client/src/actions/activities.js
index d69f0134..1369da82 100644
--- a/client/src/actions/activities.js
+++ b/client/src/actions/activities.js
@@ -5,15 +5,39 @@
import ActionTypes from '../constants/ActionTypes';
-const fetchActivities = (cardId) => ({
- type: ActionTypes.ACTIVITIES_FETCH,
+const fetchActivitiesInBoard = (boardId) => ({
+ type: ActionTypes.ACTIVITIES_IN_BOARD_FETCH,
+ payload: {
+ boardId,
+ },
+});
+
+fetchActivitiesInBoard.success = (boardId, activities, users) => ({
+ type: ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS,
+ payload: {
+ boardId,
+ activities,
+ users,
+ },
+});
+
+fetchActivitiesInBoard.failure = (boardId, error) => ({
+ type: ActionTypes.ACTIVITIES_IN_BOARD_FETCH__FAILURE,
+ payload: {
+ boardId,
+ error,
+ },
+});
+
+const fetchActivitiesInCard = (cardId) => ({
+ type: ActionTypes.ACTIVITIES_IN_CARD_FETCH,
payload: {
cardId,
},
});
-fetchActivities.success = (cardId, activities, users) => ({
- type: ActionTypes.ACTIVITIES_FETCH__SUCCESS,
+fetchActivitiesInCard.success = (cardId, activities, users) => ({
+ type: ActionTypes.ACTIVITIES_IN_CARD_FETCH__SUCCESS,
payload: {
cardId,
activities,
@@ -21,8 +45,8 @@ fetchActivities.success = (cardId, activities, users) => ({
},
});
-fetchActivities.failure = (cardId, error) => ({
- type: ActionTypes.ACTIVITIES_FETCH__FAILURE,
+fetchActivitiesInCard.failure = (cardId, error) => ({
+ type: ActionTypes.ACTIVITIES_IN_CARD_FETCH__FAILURE,
payload: {
cardId,
error,
@@ -37,6 +61,7 @@ const handleActivityCreate = (activity) => ({
});
export default {
- fetchActivities,
+ fetchActivitiesInBoard,
+ fetchActivitiesInCard,
handleActivityCreate,
};
diff --git a/client/src/api/activities.js b/client/src/api/activities.js
index cb0cbbb8..7db3ef1d 100755
--- a/client/src/api/activities.js
+++ b/client/src/api/activities.js
@@ -16,7 +16,13 @@ export const transformActivity = (activity) => ({
/* Actions */
-const getActivities = (cardId, data, headers) =>
+const getActivitiesInBoard = (boardId, data, headers) =>
+ socket.get(`/boards/${boardId}/actions`, data, headers).then((body) => ({
+ ...body,
+ items: body.items.map(transformActivity),
+ }));
+
+const getActivitiesInCard = (cardId, data, headers) =>
socket.get(`/cards/${cardId}/actions`, data, headers).then((body) => ({
...body,
items: body.items.map(transformActivity),
@@ -32,6 +38,7 @@ const makeHandleActivityCreate = (next) => (body) => {
};
export default {
- getActivities,
+ getActivitiesInBoard,
+ getActivitiesInCard,
makeHandleActivityCreate,
};
diff --git a/client/src/components/activities/BoardActivitiesModal/BoardActivitiesModal.jsx b/client/src/components/activities/BoardActivitiesModal/BoardActivitiesModal.jsx
new file mode 100755
index 00000000..81c0240f
--- /dev/null
+++ b/client/src/components/activities/BoardActivitiesModal/BoardActivitiesModal.jsx
@@ -0,0 +1,73 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+import React, { useCallback } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useTranslation } from 'react-i18next';
+import { useInView } from 'react-intersection-observer';
+import { Comment, Loader } from 'semantic-ui-react';
+
+import selectors from '../../../selectors';
+import entryActions from '../../../entry-actions';
+import { useClosableModal } from '../../../hooks';
+import Item from './Item';
+
+import styles from './BoardActivitiesModal.module.scss';
+
+const BoardActivitiesModal = React.memo(() => {
+ const activityIds = useSelector(selectors.selectActivityIdsForCurrentBoard);
+
+ const { isActivitiesFetching, isAllActivitiesFetched } = useSelector(
+ selectors.selectCurrentBoard,
+ );
+
+ const dispatch = useDispatch();
+ const [t] = useTranslation();
+
+ const handleClose = useCallback(() => {
+ dispatch(entryActions.closeModal());
+ }, [dispatch]);
+
+ const [inViewRef] = useInView({
+ threshold: 1,
+ onChange: (inView) => {
+ if (inView) {
+ dispatch(entryActions.fetchActivitiesInCurrentBoard());
+ }
+ },
+ });
+
+ const [ClosableModal] = useClosableModal();
+
+ return (
+
+
+ {t('common.boardActions', {
+ context: 'title',
+ })}
+
+
+
+
+ {activityIds.map((activityId) => (
+
+ ))}
+
+
+ {isActivitiesFetching !== undefined && isAllActivitiesFetched !== undefined && (
+
+ {isActivitiesFetching ? (
+
+ ) : (
+ !isAllActivitiesFetched &&
+ )}
+
+ )}
+
+
+ );
+});
+
+export default BoardActivitiesModal;
diff --git a/client/src/components/activities/BoardActivitiesModal/BoardActivitiesModal.module.scss b/client/src/components/activities/BoardActivitiesModal/BoardActivitiesModal.module.scss
new file mode 100644
index 00000000..373d35ae
--- /dev/null
+++ b/client/src/components/activities/BoardActivitiesModal/BoardActivitiesModal.module.scss
@@ -0,0 +1,18 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+:global(#app) {
+ .items {
+ max-width: none;
+ }
+
+ .itemsWrapper {
+ margin-top: 12px;
+ }
+
+ .loaderWrapper {
+ margin-top: 10px;
+ }
+}
diff --git a/client/src/components/activities/BoardActivitiesModal/Item.jsx b/client/src/components/activities/BoardActivitiesModal/Item.jsx
new file mode 100755
index 00000000..35cd0720
--- /dev/null
+++ b/client/src/components/activities/BoardActivitiesModal/Item.jsx
@@ -0,0 +1,220 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+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 { Comment } from 'semantic-ui-react';
+
+import selectors from '../../../selectors';
+import Paths from '../../../constants/Paths';
+import { StaticUserIds } from '../../../constants/StaticUsers';
+import { ActivityTypes } from '../../../constants/Enums';
+import TimeAgo from '../../common/TimeAgo';
+import UserAvatar from '../../users/UserAvatar';
+
+import styles from './Item.module.scss';
+
+const Item = React.memo(({ id }) => {
+ const selectActivityById = useMemo(() => selectors.makeSelectActivityById(), []);
+ const selectUserById = useMemo(() => selectors.makeSelectUserById(), []);
+ const selectCardById = useMemo(() => selectors.makeSelectCardById(), []);
+
+ const activity = useSelector((state) => selectActivityById(state, id));
+ const user = useSelector((state) => selectUserById(state, activity.userId));
+ const card = useSelector((state) => selectCardById(state, activity.cardId));
+
+ const [t] = useTranslation();
+
+ const userName =
+ user.id === StaticUserIds.DELETED
+ ? t(`common.${user.name}`, {
+ context: 'title',
+ })
+ : user.name;
+
+ const cardName = card ? card.name : activity.data.card.name;
+
+ let contentNode;
+ switch (activity.type) {
+ case ActivityTypes.CREATE_CARD: {
+ const { list } = activity.data;
+ const listName = list.name || t(`common.${list.type}`);
+
+ contentNode = (
+
+ {userName}
+ {' added '}
+ {cardName}
+ {' to '}
+ {listName}
+
+ );
+
+ break;
+ }
+ case ActivityTypes.MOVE_CARD: {
+ const { fromList, toList } = activity.data;
+
+ const fromListName = fromList.name || t(`common.${fromList.type}`);
+ const toListName = toList.name || t(`common.${toList.type}`);
+
+ contentNode = (
+
+ {userName}
+ {' moved '}
+ {cardName}
+ {' from '}
+ {fromListName}
+ {' to '}
+ {toListName}
+
+ );
+
+ break;
+ }
+ case ActivityTypes.ADD_MEMBER_TO_CARD:
+ contentNode =
+ user.id === activity.data.user.id ? (
+
+ {userName}
+ {' joined '}
+ {cardName}
+
+ ) : (
+
+ {userName}
+ {' added '}
+ {activity.data.user.name}
+ {' to '}
+ {cardName}
+
+ );
+
+ break;
+ case ActivityTypes.REMOVE_MEMBER_FROM_CARD:
+ contentNode =
+ user.id === activity.data.user.id ? (
+
+ {userName}
+ {' left '}
+ {cardName}
+
+ ) : (
+
+ {userName}
+ {' removed '}
+ {activity.data.user.name}
+ {' from '}
+ {cardName}
+
+ );
+
+ break;
+ case ActivityTypes.COMPLETE_TASK:
+ contentNode = (
+
+ {userName}
+ {' completed '}
+ {activity.data.task.name}
+ {' on '}
+ {cardName}
+
+ );
+
+ break;
+ case ActivityTypes.UNCOMPLETE_TASK:
+ contentNode = (
+
+ {userName}
+ {' marked '}
+ {activity.data.task.name}
+ {' incomplete on '}
+ {cardName}
+
+ );
+
+ break;
+ default:
+ contentNode = null;
+ }
+
+ return (
+
+
+
+
+
+
+ );
+});
+
+Item.propTypes = {
+ id: PropTypes.string.isRequired,
+};
+
+export default Item;
diff --git a/client/src/components/activities/Activities/Item.module.scss b/client/src/components/activities/BoardActivitiesModal/Item.module.scss
similarity index 90%
rename from client/src/components/activities/Activities/Item.module.scss
rename to client/src/components/activities/BoardActivitiesModal/Item.module.scss
index 7245597b..2b420687 100644
--- a/client/src/components/activities/Activities/Item.module.scss
+++ b/client/src/components/activities/BoardActivitiesModal/Item.module.scss
@@ -7,12 +7,12 @@
.author {
color: #17394d;
font-weight: bold;
- line-height: 20px;
}
.content {
border-bottom: 1px solid #092d4221;
display: inline-block;
+ line-height: 20px;
padding-bottom: 14px;
vertical-align: top;
width: calc(100% - 40px);
@@ -21,11 +21,6 @@
.date {
color: #6b808c;
font-size: 12px;
- line-height: 20px;
- }
-
- .text {
- line-height: 20px;
}
.user {
diff --git a/client/src/components/activities/BoardActivitiesModal/index.js b/client/src/components/activities/BoardActivitiesModal/index.js
new file mode 100755
index 00000000..ba2ef125
--- /dev/null
+++ b/client/src/components/activities/BoardActivitiesModal/index.js
@@ -0,0 +1,8 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+import BoardActivitiesModal from './BoardActivitiesModal';
+
+export default BoardActivitiesModal;
diff --git a/client/src/components/activities/Activities/Activities.jsx b/client/src/components/activities/CardActivities/CardActivities.jsx
similarity index 92%
rename from client/src/components/activities/Activities/Activities.jsx
rename to client/src/components/activities/CardActivities/CardActivities.jsx
index 686d3c4d..39868ede 100755
--- a/client/src/components/activities/Activities/Activities.jsx
+++ b/client/src/components/activities/CardActivities/CardActivities.jsx
@@ -12,9 +12,9 @@ import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import Item from './Item';
-import styles from './Activities.module.scss';
+import styles from './CardActivities.module.scss';
-const Activities = React.memo(() => {
+const CardActivities = React.memo(() => {
const activityIds = useSelector(selectors.selectActivityIdsForCurrentCard);
const { isActivitiesFetching, isAllActivitiesFetched } = useSelector(selectors.selectCurrentCard);
@@ -51,4 +51,4 @@ const Activities = React.memo(() => {
);
});
-export default Activities;
+export default CardActivities;
diff --git a/client/src/components/activities/Activities/Activities.module.scss b/client/src/components/activities/CardActivities/CardActivities.module.scss
similarity index 100%
rename from client/src/components/activities/Activities/Activities.module.scss
rename to client/src/components/activities/CardActivities/CardActivities.module.scss
diff --git a/client/src/components/activities/Activities/Item.jsx b/client/src/components/activities/CardActivities/Item.jsx
similarity index 82%
rename from client/src/components/activities/Activities/Item.jsx
rename to client/src/components/activities/CardActivities/Item.jsx
index e4d34acb..4a644f2d 100755
--- a/client/src/components/activities/Activities/Item.jsx
+++ b/client/src/components/activities/CardActivities/Item.jsx
@@ -48,10 +48,8 @@ const Item = React.memo(({ id }) => {
}}
>
{userName}
-
- {' added this card to '}
- {listName}
-
+ {' added this card to '}
+ {listName}
);
@@ -73,12 +71,10 @@ const Item = React.memo(({ id }) => {
}}
>
{userName}
-
- {' moved this card from '}
- {fromListName}
- {' to '}
- {toListName}
-
+ {' moved this card from '}
+ {fromListName}
+ {' to '}
+ {toListName}
);
@@ -94,7 +90,7 @@ const Item = React.memo(({ id }) => {
}}
>
{userName}
- {' joined this card'}
+ {' joined this card'}
) : (
{
}}
>
{userName}
-
- {' added '}
- {activity.data.user.name}
- {' to this card'}
-
+ {' added '}
+ {activity.data.user.name}
+ {' to this card'}
);
@@ -124,7 +118,7 @@ const Item = React.memo(({ id }) => {
}}
>
{userName}
- {' left this card'}
+ {' left this card'}
) : (
{
}}
>
{userName}
-
- {' removed '}
- {activity.data.user.name}
- {' from this card'}
-
+ {' removed '}
+ {activity.data.user.name}
+ {' from this card'}
);
@@ -154,11 +146,9 @@ const Item = React.memo(({ id }) => {
}}
>
{userName}
-
- {' completed '}
- {activity.data.task.name}
- {' on this card'}
-
+ {' completed '}
+ {activity.data.task.name}
+ {' on this card'}
);
@@ -173,11 +163,9 @@ const Item = React.memo(({ id }) => {
}}
>
{userName}
-
- {' marked '}
- {activity.data.task.name}
- {' incomplete on this card'}
-
+ {' marked '}
+ {activity.data.task.name}
+ {' incomplete on this card'}
);
diff --git a/client/src/components/activities/CardActivities/Item.module.scss b/client/src/components/activities/CardActivities/Item.module.scss
new file mode 100644
index 00000000..2b420687
--- /dev/null
+++ b/client/src/components/activities/CardActivities/Item.module.scss
@@ -0,0 +1,31 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+:global(#app) {
+ .author {
+ color: #17394d;
+ font-weight: bold;
+ }
+
+ .content {
+ border-bottom: 1px solid #092d4221;
+ display: inline-block;
+ line-height: 20px;
+ padding-bottom: 14px;
+ vertical-align: top;
+ width: calc(100% - 40px);
+ }
+
+ .date {
+ color: #6b808c;
+ font-size: 12px;
+ }
+
+ .user {
+ display: inline-block;
+ padding: 4px 8px 0 0;
+ vertical-align: top;
+ }
+}
diff --git a/client/src/components/activities/Activities/index.js b/client/src/components/activities/CardActivities/index.js
similarity index 66%
rename from client/src/components/activities/Activities/index.js
rename to client/src/components/activities/CardActivities/index.js
index 26c9f8db..775e3cd0 100755
--- a/client/src/components/activities/Activities/index.js
+++ b/client/src/components/activities/CardActivities/index.js
@@ -3,6 +3,6 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
-import Activities from './Activities';
+import CardActivities from './CardActivities';
-export default Activities;
+export default CardActivities;
diff --git a/client/src/components/boards/Board/Board.jsx b/client/src/components/boards/Board/Board.jsx
index 1929a9fb..401fe508 100644
--- a/client/src/components/boards/Board/Board.jsx
+++ b/client/src/components/boards/Board/Board.jsx
@@ -7,14 +7,17 @@ import React from 'react';
import { useSelector } from 'react-redux';
import selectors from '../../../selectors';
+import ModalTypes from '../../../constants/ModalTypes';
import { BoardContexts, BoardViews } from '../../../constants/Enums';
import KanbanContent from './KanbanContent';
import FiniteContent from './FiniteContent';
import EndlessContent from './EndlessContent';
import CardModal from '../../cards/CardModal';
+import BoardActivitiesModal from '../../activities/BoardActivitiesModal';
const Board = React.memo(() => {
const board = useSelector(selectors.selectCurrentBoard);
+ const modal = useSelector(selectors.selectCurrentModal);
const isCardModalOpened = useSelector((state) => !!selectors.selectPath(state).cardId);
let Content;
@@ -35,10 +38,23 @@ const Board = React.memo(() => {
}
}
+ let modalNode = null;
+ if (isCardModalOpened) {
+ modalNode = ;
+ } else if (modal) {
+ switch (modal.type) {
+ case ModalTypes.BOARD_ACTIVITIES:
+ modalNode = ;
+
+ break;
+ default:
+ }
+ }
+
return (
<>
- {isCardModalOpened && }
+ {modalNode}
>
);
});
diff --git a/client/src/components/boards/BoardActions/RightSide/ActionsStep.jsx b/client/src/components/boards/BoardActions/RightSide/ActionsStep.jsx
index 7a50caba..f4bb8fe3 100755
--- a/client/src/components/boards/BoardActions/RightSide/ActionsStep.jsx
+++ b/client/src/components/boards/BoardActions/RightSide/ActionsStep.jsx
@@ -21,14 +21,14 @@ import CustomFieldGroupsStep from '../../../custom-field-groups/CustomFieldGroup
import styles from './ActionsStep.module.scss';
const StepTypes = {
- EMPTY_TRASH: 'EMPTY_TRASH',
CUSTOM_FIELD_GROUPS: 'CUSTOM_FIELD_GROUPS',
+ EMPTY_TRASH: 'EMPTY_TRASH',
};
const ActionsStep = React.memo(({ onClose }) => {
const board = useSelector(selectors.selectCurrentBoard);
- const { withSubscribe, withTrashEmptier, withCustomFieldGroups } = useSelector((state) => {
+ const { withSubscribe, withCustomFieldGroups, withTrashEmptier } = useSelector((state) => {
const isManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
@@ -42,8 +42,8 @@ const ActionsStep = React.memo(({ onClose }) => {
return {
withSubscribe: isMember, // TODO: rename?
- withTrashEmptier: board.context === BoardContexts.TRASH && (isManager || isEditor),
withCustomFieldGroups: isEditor,
+ withTrashEmptier: board.context === BoardContexts.TRASH && (isManager || isEditor),
};
}, shallowEqual);
@@ -69,21 +69,28 @@ const ActionsStep = React.memo(({ onClose }) => {
[onClose, dispatch],
);
+ const handleActivitiesClick = useCallback(() => {
+ dispatch(entryActions.openBoardActivitiesModal());
+ onClose();
+ }, [onClose, dispatch]);
+
const handleEmptyTrashConfirm = useCallback(() => {
dispatch(entryActions.clearTrashListInCurrentBoard());
onClose();
}, [onClose, dispatch]);
- const handleEmptyTrashClick = useCallback(() => {
- openStep(StepTypes.EMPTY_TRASH);
- }, [openStep]);
-
const handleCustomFieldsClick = useCallback(() => {
openStep(StepTypes.CUSTOM_FIELD_GROUPS);
}, [openStep]);
+ const handleEmptyTrashClick = useCallback(() => {
+ openStep(StepTypes.EMPTY_TRASH);
+ }, [openStep]);
+
if (step) {
switch (step.type) {
+ case StepTypes.CUSTOM_FIELD_GROUPS:
+ return ;
case StepTypes.EMPTY_TRASH:
return (
{
onBack={handleBack}
/>
);
- case StepTypes.CUSTOM_FIELD_GROUPS:
- return ;
default:
}
}
@@ -128,6 +133,12 @@ const ActionsStep = React.memo(({ onClose }) => {
})}
)}
+
+
+ {t('common.actions', {
+ context: 'title',
+ })}
+
{withTrashEmptier && (
<>
{(withSubscribe || withCustomFieldGroups) && }
diff --git a/client/src/components/cards/CardModal/Communication.jsx b/client/src/components/cards/CardModal/Communication.jsx
index 9d72f547..3b9bbc72 100644
--- a/client/src/components/cards/CardModal/Communication.jsx
+++ b/client/src/components/cards/CardModal/Communication.jsx
@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
import { Menu, Tab } from 'semantic-ui-react';
import Comments from '../../comments/Comments';
-import Activities from '../../activities/Activities';
+import CardActivities from '../../activities/CardActivities';
import styles from './Communication.module.scss';
@@ -34,7 +34,7 @@ const Communication = React.memo(() => {
})}
),
- render: () => ,
+ render: () => ,
},
];
diff --git a/client/src/components/notifications/NotificationsStep/Item.module.scss b/client/src/components/notifications/NotificationsStep/Item.module.scss
index b9358e9f..22cc4b1e 100644
--- a/client/src/components/notifications/NotificationsStep/Item.module.scss
+++ b/client/src/components/notifications/NotificationsStep/Item.module.scss
@@ -29,6 +29,7 @@
.content {
display: inline-block;
font-size: 13px;
+ line-height: 20px;
min-height: 36px;
overflow: hidden;
padding: 0 4px 0 8px;
@@ -40,7 +41,6 @@
.date {
color: #6b808c;
font-size: 12px;
- line-height: 20px;
}
.wrapper {
diff --git a/client/src/constants/ActionTypes.js b/client/src/constants/ActionTypes.js
index f8754a73..567afcee 100644
--- a/client/src/constants/ActionTypes.js
+++ b/client/src/constants/ActionTypes.js
@@ -360,9 +360,12 @@ export default {
/* Activities */
- ACTIVITIES_FETCH: 'ACTIVITIES_FETCH',
- ACTIVITIES_FETCH__SUCCESS: 'ACTIVITIES_FETCH__SUCCESS',
- ACTIVITIES_FETCH__FAILURE: 'ACTIVITIES_FETCH__FAILURE',
+ ACTIVITIES_IN_BOARD_FETCH: 'ACTIVITIES_IN_BOARD_FETCH',
+ ACTIVITIES_IN_BOARD_FETCH__SUCCESS: 'ACTIVITIES_IN_BOARD_FETCH__SUCCESS',
+ ACTIVITIES_IN_BOARD_FETCH__FAILURE: 'ACTIVITIES_IN_BOARD_FETCH__FAILURE',
+ ACTIVITIES_IN_CARD_FETCH: 'ACTIVITIES_IN_CARD_FETCH',
+ ACTIVITIES_IN_CARD_FETCH__SUCCESS: 'ACTIVITIES_IN_CARD_FETCH__SUCCESS',
+ ACTIVITIES_IN_CARD_FETCH__FAILURE: 'ACTIVITIES_IN_CARD_FETCH__FAILURE',
ACTIVITY_CREATE_HANDLE: 'ACTIVITY_CREATE_HANDLE',
/* Notifications */
diff --git a/client/src/constants/EntryActionTypes.js b/client/src/constants/EntryActionTypes.js
index 2222c37d..9195eac1 100755
--- a/client/src/constants/EntryActionTypes.js
+++ b/client/src/constants/EntryActionTypes.js
@@ -255,6 +255,7 @@ export default {
/* Activities */
+ ACTIVITIES_IN_CURRENT_BOARD_FETCH: `${PREFIX}/ACTIVITIES_IN_CURRENT_BOARD_FETCH`,
ACTIVITIES_IN_CURRENT_CARD_FETCH: `${PREFIX}/ACTIVITIES_IN_CURRENT_CARD_FETCH`,
ACTIVITY_CREATE_HANDLE: `${PREFIX}/ACTIVITY_CREATE_HANDLE`,
diff --git a/client/src/constants/ModalTypes.js b/client/src/constants/ModalTypes.js
index fe59e827..66750985 100755
--- a/client/src/constants/ModalTypes.js
+++ b/client/src/constants/ModalTypes.js
@@ -8,6 +8,7 @@ const USER_SETTINGS = 'USER_SETTINGS';
const ADD_PROJECT = 'ADD_PROJECT';
const PROJECT_SETTINGS = 'PROJECT_SETTINGS';
const BOARD_SETTINGS = 'BOARD_SETTINGS';
+const BOARD_ACTIVITIES = 'BOARD_ACTIVITIES';
export default {
ADMINISTRATION,
@@ -15,4 +16,5 @@ export default {
ADD_PROJECT,
PROJECT_SETTINGS,
BOARD_SETTINGS,
+ BOARD_ACTIVITIES,
};
diff --git a/client/src/entry-actions/activities.js b/client/src/entry-actions/activities.js
index 65fcfbda..6c786d30 100755
--- a/client/src/entry-actions/activities.js
+++ b/client/src/entry-actions/activities.js
@@ -5,6 +5,11 @@
import EntryActionTypes from '../constants/EntryActionTypes';
+const fetchActivitiesInCurrentBoard = () => ({
+ type: EntryActionTypes.ACTIVITIES_IN_CURRENT_BOARD_FETCH,
+ payload: {},
+});
+
const fetchActivitiesInCurrentCard = () => ({
type: EntryActionTypes.ACTIVITIES_IN_CURRENT_CARD_FETCH,
payload: {},
@@ -18,6 +23,7 @@ const handleActivityCreate = (activity) => ({
});
export default {
+ fetchActivitiesInCurrentBoard,
fetchActivitiesInCurrentCard,
handleActivityCreate,
};
diff --git a/client/src/entry-actions/modals.js b/client/src/entry-actions/modals.js
index a36372b6..1c1224c3 100755
--- a/client/src/entry-actions/modals.js
+++ b/client/src/entry-actions/modals.js
@@ -47,6 +47,13 @@ const openBoardSettingsModal = (boardId) => ({
},
});
+const openBoardActivitiesModal = () => ({
+ type: EntryActionTypes.MODAL_OPEN,
+ payload: {
+ type: ModalTypes.BOARD_ACTIVITIES,
+ },
+});
+
const closeModal = () => ({
type: EntryActionTypes.MODAL_CLOSE,
payload: {},
@@ -58,5 +65,6 @@ export default {
openAddProjectModal,
openProjectSettingsModal,
openBoardSettingsModal,
+ openBoardActivitiesModal,
closeModal,
};
diff --git a/client/src/hooks/use-closable-modal.jsx b/client/src/hooks/use-closable-modal.jsx
index 2e30802b..f165fecb 100644
--- a/client/src/hooks/use-closable-modal.jsx
+++ b/client/src/hooks/use-closable-modal.jsx
@@ -59,6 +59,7 @@ export default (initialClosableValue) => {
onClose: undefined,
};
+ ClosableModal.Header = Modal.Header;
ClosableModal.Content = Modal.Content;
ClosableModal.Actions = Modal.Actions;
diff --git a/client/src/locales/ar-YE/core.js b/client/src/locales/ar-YE/core.js
index a2b52b17..3713654a 100644
--- a/client/src/locales/ar-YE/core.js
+++ b/client/src/locales/ar-YE/core.js
@@ -152,12 +152,12 @@ export default {
time: 'الوقت',
title: 'العنوان',
userActions_title: 'إجراءات المستخدم',
- userAddedThisCardToList: '<0>{{user}}0><1> تمت إضافة هذه البطاقة إلى {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> تمت إضافة هذه البطاقة إلى {{list}}',
userLeftNewCommentToCard: '<0>{{user}}0> ترك تعليق جديد «{{comment}}» إلى <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> انتقل <2>{{card}}2> من {{fromList}} إلى {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> نُقلت هذه البطاقة من {{fromList}} إلى {{toList}}1>',
+ '<0>{{user}}0> نُقلت هذه البطاقة من {{fromList}} إلى {{toList}}',
username: 'اسم المستخدم',
users: 'المستخدمين',
viewer: 'مشاهد',
diff --git a/client/src/locales/bg-BG/core.js b/client/src/locales/bg-BG/core.js
index 82bc7b41..23a5e241 100644
--- a/client/src/locales/bg-BG/core.js
+++ b/client/src/locales/bg-BG/core.js
@@ -153,13 +153,13 @@ export default {
time: 'Време',
title: 'Заглавие',
userActions_title: 'Потребителски действия',
- userAddedThisCardToList: '<0>{{user}}0><1> добави тази карта в {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> добави тази карта в {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> остави нов коментар «{{comment}}» в <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> премести <2>{{card}}2> от {{fromList}} към {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> премести тази карта от {{fromList}} към {{toList}}1>',
+ '<0>{{user}}0> премести тази карта от {{fromList}} към {{toList}}',
username: 'Потребителско име',
users: 'Потребители',
viewer: 'Зрител',
diff --git a/client/src/locales/cs-CZ/core.js b/client/src/locales/cs-CZ/core.js
index 5001995c..c0d74dc6 100644
--- a/client/src/locales/cs-CZ/core.js
+++ b/client/src/locales/cs-CZ/core.js
@@ -276,13 +276,13 @@ export default {
unsavedChanges: 'Neuložené změny',
uploadedImages: 'Nahrané obrázky',
userActions_title: 'Akce uživatele',
- userAddedThisCardToList: '<0>{{user}}0><1> přidal kartu do {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> přidal kartu do {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> zanechal nový komentář «{{comment}}» k <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> přesunul <2>{{card}}2> z {{fromList}} do {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> přesunul tuto kartu z {{fromList}} do {{toList}}1>',
+ '<0>{{user}}0> přesunul tuto kartu z {{fromList}} do {{toList}}',
username: 'Uživatelské jméno',
users: 'Uživatelé',
viewer: 'Prohlížeč',
diff --git a/client/src/locales/da-DK/core.js b/client/src/locales/da-DK/core.js
index 508b726a..6de72c0a 100644
--- a/client/src/locales/da-DK/core.js
+++ b/client/src/locales/da-DK/core.js
@@ -154,13 +154,13 @@ export default {
time: 'Tid',
title: 'Overskrift',
userActions_title: 'Brugerhandlinger',
- userAddedThisCardToList: '<0>{{user}}0><1> tilføjede kortet til {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> tilføjede kortet til {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> skrevet en ny kommentar «{{comment}}» på <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> flyttede <2>{{card}}2> fra {{fromList}} til {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> flyttede kortet fra {{fromList}} til {{toList}}1>',
+ '<0>{{user}}0> flyttede kortet fra {{fromList}} til {{toList}}',
username: 'Brugernavn',
users: 'Brugere',
viewer: 'Læser',
diff --git a/client/src/locales/de-DE/core.js b/client/src/locales/de-DE/core.js
index 57ff436e..a4798104 100644
--- a/client/src/locales/de-DE/core.js
+++ b/client/src/locales/de-DE/core.js
@@ -296,13 +296,13 @@ export default {
unsavedChanges: 'Ungespeicherte Änderungen',
uploadedImages: 'Hochgeladene Bilder',
userActions_title: 'Benutzeraktionen',
- userAddedThisCardToList: '<0>{{user}}0><1> hat diese Karte hinzugefügt zu {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> hat diese Karte hinzugefügt zu {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> hat einen neuen Kommentar verfasst: «{{comment}}» in <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> bewegte <2>{{card}}2> von {{fromList}} nach {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> bewegte diese Karte von {{fromList}} nach {{toList}}1>',
+ '<0>{{user}}0> bewegte diese Karte von {{fromList}} nach {{toList}}',
username: 'Benutzername',
users: 'Benutzer',
viewer: 'Betrachter',
diff --git a/client/src/locales/en-GB/core.js b/client/src/locales/en-GB/core.js
index 0be9ccb0..ae9d0d4e 100644
--- a/client/src/locales/en-GB/core.js
+++ b/client/src/locales/en-GB/core.js
@@ -286,22 +286,28 @@ export default {
unsavedChanges: 'Unsaved changes',
uploadedImages: 'Uploaded images',
userActions_title: 'User Actions',
- userAddedThisCardToList: '<0>{{user}}0><1> added this card to {{list}}1>',
- userAddedUserToThisCard: '<0>{{actorUser}}0><1> added {{addedUser}} to this card1>',
+ userAddedCardToList: '<0>{{user}}0> added <2>{{card}}2> to {{list}}',
+ userAddedThisCardToList: '<0>{{user}}0> added this card to {{list}}',
+ userAddedUserToCard: '<0>{{actorUser}}0> added {{addedUser}} to <4>{{card}}4>',
+ userAddedUserToThisCard: '<0>{{actorUser}}0> added {{addedUser}} to this card',
userAddedYouToCard: '<0>{{user}}0> added you to <2>{{card}}2>',
- userCompletedTaskOnThisCard: '<0>{{user}}0><1> completed {{task}} on this card1>',
- userJoinedThisCard: `<0>{{user}}0><1> joined this card1>`,
+ userCompletedTaskOnCard: '<0>{{user}}0> completed {{task}} on <4>{{card}}4>',
+ userCompletedTaskOnThisCard: '<0>{{user}}0> completed {{task}} on this card',
+ userJoinedCard: `<0>{{user}}0> joined <2>{{card}}2>`,
+ userJoinedThisCard: `<0>{{user}}0> joined this card`,
userLeftNewCommentToCard:
'<0>{{user}}0> left a new comment «{{comment}}» to <2>{{card}}2>',
- userLeftThisCard: '<0>{{user}}0><1> left this card1>',
- userMarkedTaskIncompleteOnThisCard:
- '<0>{{user}}0><1> marked {{task}} incomplete on this card1>',
+ userLeftCard: '<0>{{user}}0> left <2>{{card}}2>',
+ userLeftThisCard: '<0>{{user}}0> left this card',
+ userMarkedTaskIncompleteOnCard:
+ '<0>{{user}}0> marked {{task}} incomplete on <4>{{card}}4>',
+ userMarkedTaskIncompleteOnThisCard: '<0>{{user}}0> marked {{task}} incomplete on this card',
userMovedCardFromListToList:
'<0>{{user}}0> moved <2>{{card}}2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> moved this card from {{fromList}} to {{toList}}1>',
- userRemovedUserFromThisCard:
- '<0>{{actorUser}}0><1> removed {{removedUser}} from this card1>',
+ '<0>{{user}}0> moved this card from {{fromList}} to {{toList}}',
+ userRemovedUserFromCard: '<0>{{actorUser}}0> removed {{removedUser}} from <4>{{card}}4>',
+ userRemovedUserFromThisCard: '<0>{{actorUser}}0> removed {{removedUser}} from this card',
username: 'Username',
users: 'Users',
viewer: 'Viewer',
diff --git a/client/src/locales/en-US/core.js b/client/src/locales/en-US/core.js
index cc858c3c..a42ac99d 100644
--- a/client/src/locales/en-US/core.js
+++ b/client/src/locales/en-US/core.js
@@ -281,22 +281,28 @@ export default {
unsavedChanges: 'Unsaved changes',
uploadedImages: 'Uploaded images',
userActions_title: 'User Actions',
- userAddedThisCardToList: '<0>{{user}}0><1> added this card to {{list}}1>',
- userAddedUserToThisCard: '<0>{{actorUser}}0><1> added {{addedUser}} to this card1>',
+ userAddedCardToList: '<0>{{user}}0> added <2>{{card}}2> to {{list}}',
+ userAddedThisCardToList: '<0>{{user}}0> added this card to {{list}}',
+ userAddedUserToCard: '<0>{{actorUser}}0> added {{addedUser}} to <4>{{card}}4>',
+ userAddedUserToThisCard: '<0>{{actorUser}}0> added {{addedUser}} to this card',
userAddedYouToCard: '<0>{{user}}0> added you to <2>{{card}}2>',
- userCompletedTaskOnThisCard: '<0>{{user}}0><1> completed {{task}} on this card1>',
- userJoinedThisCard: `<0>{{user}}0><1> joined this card1>`,
+ userCompletedTaskOnCard: '<0>{{user}}0> completed {{task}} on <4>{{card}}4>',
+ userCompletedTaskOnThisCard: '<0>{{user}}0> completed {{task}} on this card',
+ userJoinedCard: `<0>{{user}}0> joined <2>{{card}}2>`,
+ userJoinedThisCard: `<0>{{user}}0> joined this card`,
userLeftNewCommentToCard:
'<0>{{user}}0> left a new comment «{{comment}}» to <2>{{card}}2>',
- userLeftThisCard: '<0>{{user}}0><1> left this card1>',
- userMarkedTaskIncompleteOnThisCard:
- '<0>{{user}}0><1> marked {{task}} incomplete on this card1>',
+ userLeftCard: '<0>{{user}}0> left <2>{{card}}2>',
+ userLeftThisCard: '<0>{{user}}0> left this card',
+ userMarkedTaskIncompleteOnCard:
+ '<0>{{user}}0> marked {{task}} incomplete on <4>{{card}}4>',
+ userMarkedTaskIncompleteOnThisCard: '<0>{{user}}0> marked {{task}} incomplete on this card',
userMovedCardFromListToList:
'<0>{{user}}0> moved <2>{{card}}2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> moved this card from {{fromList}} to {{toList}}1>',
- userRemovedUserFromThisCard:
- '<0>{{actorUser}}0><1> removed {{removedUser}} from this card1>',
+ '<0>{{user}}0> moved this card from {{fromList}} to {{toList}}',
+ userRemovedUserFromCard: '<0>{{actorUser}}0> removed {{removedUser}} from <4>{{card}}4>',
+ userRemovedUserFromThisCard: '<0>{{actorUser}}0> removed {{removedUser}} from this card',
username: 'Username',
users: 'Users',
viewer: 'Viewer',
diff --git a/client/src/locales/es-ES/core.js b/client/src/locales/es-ES/core.js
index 41bb79e2..20bc484c 100644
--- a/client/src/locales/es-ES/core.js
+++ b/client/src/locales/es-ES/core.js
@@ -117,13 +117,13 @@ export default {
time: 'Tiempo',
title: 'Título',
userActions_title: 'Acciones de Usuario',
- userAddedThisCardToList: '<0>{{user}}0><1> añadido a esta tarjeta en {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> añadido a esta tarjeta en {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> dejó un nuevo comentario «{{comment}}» en <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> movió <2>{{card}}2> de {{fromList}} a {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> movió esta tarjeta de {{fromList}} a {{toList}}1>',
+ '<0>{{user}}0> movió esta tarjeta de {{fromList}} a {{toList}}',
username: 'Nombre de usuario',
users: 'Usuarios',
writeComment: 'Escribir un comentario...',
diff --git a/client/src/locales/fa-IR/core.js b/client/src/locales/fa-IR/core.js
index 795ca8e0..100a71ba 100644
--- a/client/src/locales/fa-IR/core.js
+++ b/client/src/locales/fa-IR/core.js
@@ -153,13 +153,13 @@ export default {
time: 'زمان',
title: 'عنوان',
userActions_title: 'اقدامات کاربر',
- userAddedThisCardToList: '<0>{{user}}0><1> این کارت را به {{list}} اضافه کرد1>',
+ userAddedThisCardToList: '<0>{{user}}0> این کارت را به {{list}} اضافه کرد',
userLeftNewCommentToCard:
'<0>{{user}}0> نظر جدید «{{comment}}» را به <2>{{card}}2> اضافه کرد',
userMovedCardFromListToList:
'<0>{{user}}0> <2>{{card}}2> را از {{fromList}} به {{toList}} منتقل کرد',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> این کارت را از {{fromList}} به {{toList}} منتقل کرد1>',
+ '<0>{{user}}0> این کارت را از {{fromList}} به {{toList}} منتقل کرد',
username: 'نام کاربری',
users: 'کاربران',
viewer: 'بیننده',
diff --git a/client/src/locales/fr-FR/core.js b/client/src/locales/fr-FR/core.js
index 74356971..f690c019 100644
--- a/client/src/locales/fr-FR/core.js
+++ b/client/src/locales/fr-FR/core.js
@@ -153,13 +153,13 @@ export default {
time: 'Temps',
title: 'Titre',
userActions_title: "Actions de l'utilisateur",
- userAddedThisCardToList: '<0>{{user}}0><1> a ajouté cette carte à {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> a ajouté cette carte à {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> a laissé un nouveau commentaire {{comment}} à <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> a déplacé <2>{{card}}2> de {{fromList}} vers {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> a déplacé cette carte de {{fromList}} vers {{toList}}1>',
+ '<0>{{user}}0> a déplacé cette carte de {{fromList}} vers {{toList}}',
username: "Nom d'utilisateur",
users: 'Utilisateurs',
viewer: 'Spectateur',
diff --git a/client/src/locales/hu-HU/core.js b/client/src/locales/hu-HU/core.js
index 6f9fe79b..e7a00406 100644
--- a/client/src/locales/hu-HU/core.js
+++ b/client/src/locales/hu-HU/core.js
@@ -151,14 +151,13 @@ export default {
time: 'Idő',
title: 'Cím',
userActions_title: 'Felhasználói műveletek',
- userAddedThisCardToList:
- '<0>{{user}}0><1> hozzáadta ezt a kártyát a következőhöz: {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> hozzáadta ezt a kártyát a következőhöz: {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> új kommentet hagyott itt: «{{comment}}» a következő kártyán: <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> áthelyezte ezt a kártyát innen: {{fromList}} ide: {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> áthelyezte ezt a kártyát innen: {{fromList}} ide: {{toList}}1>',
+ '<0>{{user}}0> áthelyezte ezt a kártyát innen: {{fromList}} ide: {{toList}}',
username: 'Felhasználónév',
users: 'Felhasználók',
viewer: 'Néző',
diff --git a/client/src/locales/id-ID/core.js b/client/src/locales/id-ID/core.js
index 0c064f40..e5a154c0 100644
--- a/client/src/locales/id-ID/core.js
+++ b/client/src/locales/id-ID/core.js
@@ -147,12 +147,12 @@ export default {
time: 'Waktu',
title: 'Judul',
userActions_title: 'Aksi Pengguna',
- userAddedThisCardToList: '<0>{{user}}0><1> menambahkan kartu ini ke {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> menambahkan kartu ini ke {{list}}',
userLeftNewCommentToCard: '<0>{{user}}0> mengomentari «{{comment}}» di <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> memindahkan <2>{{card}}2> dari {{fromList}} ke {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> memindahkan kartu ini dari {{fromList}} ke {{toList}}1>',
+ '<0>{{user}}0> memindahkan kartu ini dari {{fromList}} ke {{toList}}',
username: 'Username',
users: 'Pengguna',
viewer: 'Penglihat',
diff --git a/client/src/locales/it-IT/core.js b/client/src/locales/it-IT/core.js
index 21e3bb44..032cf709 100644
--- a/client/src/locales/it-IT/core.js
+++ b/client/src/locales/it-IT/core.js
@@ -284,13 +284,13 @@ export default {
unsavedChanges: 'Modifiche non salvate',
uploadedImages: 'Immagini caricate',
userActions_title: 'Azioni utente',
- userAddedThisCardToList: '<0>{{user}}0><1> ha aggiunto questa scheda a {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> ha aggiunto questa scheda a {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> ha lasciato un commento «{{comment}}» a <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> ha spostato <2>{{card}}2> da {{fromList}} a {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> ha spostato questa scheda da {{fromList}} a {{toList}}1>',
+ '<0>{{user}}0> ha spostato questa scheda da {{fromList}} a {{toList}}',
username: 'Username',
users: 'Utenti',
viewer: 'Visualizzatore',
diff --git a/client/src/locales/ja-JP/core.js b/client/src/locales/ja-JP/core.js
index d6946dcb..8841b6dc 100644
--- a/client/src/locales/ja-JP/core.js
+++ b/client/src/locales/ja-JP/core.js
@@ -146,13 +146,13 @@ export default {
time: '時間',
title: 'タイトル',
userActions_title: 'ユーザーのアクション',
- userAddedThisCardToList: '<0>{{user}}0> 様が <1>{{list}} をこのカードに追加しました1>',
+ userAddedThisCardToList: '<0>{{user}}0> 様が {{list}} をこのカードに追加しました',
userLeftNewCommentToCard:
'<0>{{user}}0> 様が <2>{{card}}2> に新しいコメント «{{comment}}» を残しました',
userMovedCardFromListToList:
'<0>{{user}}0> 様が <2>{{card}}2> を {{fromList}} から {{toList}} に移動しました',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> 様がこのカードを {{fromList}} から {{toList}} に移動しました1>',
+ '<0>{{user}}0> 様がこのカードを {{fromList}} から {{toList}} に移動しました',
username: 'ユーザー名',
users: 'ユーザー',
viewer: 'ビューア',
diff --git a/client/src/locales/ko-KR/core.js b/client/src/locales/ko-KR/core.js
index 590a4ffe..e2022a4e 100644
--- a/client/src/locales/ko-KR/core.js
+++ b/client/src/locales/ko-KR/core.js
@@ -151,13 +151,13 @@ export default {
time: '시간',
title: '제목',
userActions_title: '사용자 작업',
- userAddedThisCardToList: '<0>{{user}}0><1>님이 이 카드를 {{list}}에 추가했습니다1>',
+ userAddedThisCardToList: '<0>{{user}}0>님이 이 카드를 {{list}}에 추가했습니다',
userLeftNewCommentToCard:
'<0>{{user}}0>님이 <2>{{card}}2>에 새 댓글 «{{comment}}»을 남겼습니다',
userMovedCardFromListToList:
'<0>{{user}}0>님이 <2>{{card}}2>를 {{fromList}}에서 {{toList}}로 이동했습니다',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> 님이 {{fromList}}에서 {{toList}}로 이 카드를 옮겼습니다1>',
+ '<0>{{user}}0> 님이 {{fromList}}에서 {{toList}}로 이 카드를 옮겼습니다',
username: '사용자 이름',
users: '사용자들',
viewer: '뷰어',
diff --git a/client/src/locales/nl-NL/core.js b/client/src/locales/nl-NL/core.js
index 6f0f2697..f50d43b7 100644
--- a/client/src/locales/nl-NL/core.js
+++ b/client/src/locales/nl-NL/core.js
@@ -147,13 +147,13 @@ export default {
time: 'Tijd',
title: 'Titel',
userActions_title: 'Gebruikersacties',
- userAddedThisCardToList: '<0>{{user}}0><1> heeft deze kaart toegevoegd aan {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> heeft deze kaart toegevoegd aan {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> heeft een nieuwe opmerking achtergelaten «{{comment}}» bij <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> heeft <2>{{card}}2> verplaatst van {{fromList}} naar {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> heeft deze kaart verplaatst van {{fromList}} naar {{toList}}1>',
+ '<0>{{user}}0> heeft deze kaart verplaatst van {{fromList}} naar {{toList}}',
username: 'Gebruikersnaam',
users: 'Gebruikers',
viewer: 'Kijker',
diff --git a/client/src/locales/pl-PL/core.js b/client/src/locales/pl-PL/core.js
index 70ac7cdf..b0af5cff 100644
--- a/client/src/locales/pl-PL/core.js
+++ b/client/src/locales/pl-PL/core.js
@@ -148,13 +148,13 @@ export default {
time: 'Czas',
title: 'Tytuł',
userActions_title: 'Akcje użytkownika',
- userAddedThisCardToList: '<0>{{user}}0><1> dodał tę kartę w {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> dodał tę kartę w {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> zamieścił nowy komentarz «{{comment}}» w <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> przeniósł <2>{{card}}2> z {{fromList}} do {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> przeniósł tę kartę z {{fromList}} do {{toList}}1>',
+ '<0>{{user}}0> przeniósł tę kartę z {{fromList}} do {{toList}}',
username: 'Nazwa Użytkownika',
users: 'Użytkownicy',
viewer: 'Odwiedzający',
diff --git a/client/src/locales/pt-BR/core.js b/client/src/locales/pt-BR/core.js
index 6e088dad..84a2f16d 100644
--- a/client/src/locales/pt-BR/core.js
+++ b/client/src/locales/pt-BR/core.js
@@ -147,13 +147,13 @@ export default {
time: 'Tempo',
title: 'Título',
userActions_title: 'Ações do Usuário',
- userAddedThisCardToList: '<0>{{user}}0><1> adicionou este cartão a {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> adicionou este cartão a {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> deixou um novo comentário «{{comment}}» em <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> moveu <2>{{card}}2> de {{fromList}} para {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> moveu este cartão de {{fromList}} para {{toList}}1>',
+ '<0>{{user}}0> moveu este cartão de {{fromList}} para {{toList}}',
username: 'Nome de usuário',
users: 'Usuários',
viewer: 'Visualizador',
diff --git a/client/src/locales/ro-RO/core.js b/client/src/locales/ro-RO/core.js
index 412cbea5..02d01ea5 100644
--- a/client/src/locales/ro-RO/core.js
+++ b/client/src/locales/ro-RO/core.js
@@ -147,13 +147,13 @@ export default {
time: 'Timp',
title: 'Titlu',
userActions_title: 'Acțiunile utilizatorului',
- userAddedThisCardToList: '<0>{{user}}0><1> a adăugat acest card în {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> a adăugat acest card în {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> a lăsat un nou comentariu «{{comment}}» în <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> a mutat <2>{{card}}2> din {{fromList}} în {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> a mutat aceast card din {{fromList}} în {{toList}}1>',
+ '<0>{{user}}0> a mutat aceast card din {{fromList}} în {{toList}}',
username: 'Nume utilizator',
users: 'Utilizatori',
viewer: 'Vizualizator',
diff --git a/client/src/locales/ru-RU/core.js b/client/src/locales/ru-RU/core.js
index 2ee40787..8a99fa58 100644
--- a/client/src/locales/ru-RU/core.js
+++ b/client/src/locales/ru-RU/core.js
@@ -284,13 +284,13 @@ export default {
unsavedChanges: 'Несохранённые изменения',
uploadedImages: 'Загруженные изображения',
userActions_title: 'Действия с пользователем',
- userAddedThisCardToList: '<0>{{user}}0><1> добавил(а) эту карточку в {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> добавил(а) эту карточку в {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> оставил(а) комментарий «{{comment}}» к <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> переместил(а) <2>{{card}}2> из {{fromList}} в {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> переместил(а) эту карточку из {{fromList}} в {{toList}}1>',
+ '<0>{{user}}0> переместил(а) эту карточку из {{fromList}} в {{toList}}',
username: 'Имя пользователя',
users: 'Пользователи',
viewer: 'Читатель',
diff --git a/client/src/locales/sk-SK/core.js b/client/src/locales/sk-SK/core.js
index b344cabd..1999e0fd 100644
--- a/client/src/locales/sk-SK/core.js
+++ b/client/src/locales/sk-SK/core.js
@@ -132,13 +132,13 @@ export default {
time: 'Čas',
title: 'Názov',
userActions_title: 'Akcie na používateľovi',
- userAddedThisCardToList: '<0>{{user}}0><1> pridal kartu do {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> pridal kartu do {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> zanechal nový komentár «{{comment}}» k <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> presunul <2>{{card}}2> z {{fromList}} do {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> presunul túto kartu z {{fromList}} do {{toList}}1>',
+ '<0>{{user}}0> presunul túto kartu z {{fromList}} do {{toList}}',
username: 'Používateľské meno',
users: 'Používatelia',
writeComment: 'Napísať komentár...',
diff --git a/client/src/locales/sr-Cyrl-CS/core.js b/client/src/locales/sr-Cyrl-CS/core.js
index 87a47917..46b143be 100644
--- a/client/src/locales/sr-Cyrl-CS/core.js
+++ b/client/src/locales/sr-Cyrl-CS/core.js
@@ -152,13 +152,13 @@ export default {
time: 'Време',
title: 'Наслов',
userActions_title: 'Корисничке радње',
- userAddedThisCardToList: '<0>{{user}}0><1> је додао ову картицу на {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> је додао ову картицу на {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> је оставио нови коментар «{{comment}}» у <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> је преместио <2>{{card}}2> са {{fromList}} у {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> је преместио ову картицу са {{fromList}} на {{toList}}1>',
+ '<0>{{user}}0> је преместио ову картицу са {{fromList}} на {{toList}}',
username: 'Корисничко име',
users: 'Корисници',
viewer: 'Прегледач',
diff --git a/client/src/locales/sr-Latn-CS/core.js b/client/src/locales/sr-Latn-CS/core.js
index 3bdf748b..4ca706f7 100644
--- a/client/src/locales/sr-Latn-CS/core.js
+++ b/client/src/locales/sr-Latn-CS/core.js
@@ -152,13 +152,13 @@ export default {
time: 'Vreme',
title: 'Naslov',
userActions_title: 'Korisničke radnje',
- userAddedThisCardToList: '<0>{{user}}0><1> je dodao ovu karticu na {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> je dodao ovu karticu na {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> je ostavio novi komentar «{{comment}}» u <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> je premestio <2>{{card}}2> sa {{fromList}} u {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> je premestio ovu karticu sa {{fromList}} na {{toList}}1>',
+ '<0>{{user}}0> je premestio ovu karticu sa {{fromList}} na {{toList}}',
username: 'Korisničko ime',
users: 'Korisnici',
viewer: 'Pregledač',
diff --git a/client/src/locales/sv-SE/core.js b/client/src/locales/sv-SE/core.js
index 0a8c729e..0310ce06 100644
--- a/client/src/locales/sv-SE/core.js
+++ b/client/src/locales/sv-SE/core.js
@@ -133,13 +133,13 @@ export default {
time: 'Tid',
title: 'Titel',
userActions_title: 'Användaråtgärder',
- userAddedThisCardToList: '<0>{{user}}0><1> lade till detta kort i {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> lade till detta kort i {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> lämnade en ny kommentar «{{comment}}» på <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> flyttade <2>{{card}}2> från {{fromList}} till {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> flyttade detta kort från {{fromList}} till {{toList}}1>',
+ '<0>{{user}}0> flyttade detta kort från {{fromList}} till {{toList}}',
username: 'Användarnamn',
users: 'Användare',
writeComment: 'Skriv en kommentar...',
diff --git a/client/src/locales/tr-TR/core.js b/client/src/locales/tr-TR/core.js
index b667f2b5..2f2de1fc 100644
--- a/client/src/locales/tr-TR/core.js
+++ b/client/src/locales/tr-TR/core.js
@@ -134,13 +134,13 @@ export default {
time: 'zaman',
title: 'başlık',
userActions_title: 'Kullanıcı İşlemleri',
- userAddedThisCardToList: '<0>{{user}}0><1> bu kartı {{list}1> listesine ekledi',
+ userAddedThisCardToList: '<0>{{user}}0> bu kartı {{list}} listesine ekledi',
userLeftNewCommentToCard:
'<0>{{user}}0> yeni bir yorum yazdı: <2>{{card}2> kartına «{{comment}}»',
userMovedCardFromListToList:
'<0>{{user}}0>, <2>{{card}>2> kartını {{fromList}} listesinden {{toList}} listesine taşıdı',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> bu kartı {{fromList}} konumundan {{toList}}1> konumuna taşıdı',
+ '<0>{{user}}0> bu kartı {{fromList}} konumundan {{toList}} konumuna taşıdı',
username: 'kullanıcı adı',
users: 'kullanıcı',
writeComment: 'Yorum yazın...',
diff --git a/client/src/locales/uk-UA/core.js b/client/src/locales/uk-UA/core.js
index b3b384ea..bdf293be 100644
--- a/client/src/locales/uk-UA/core.js
+++ b/client/src/locales/uk-UA/core.js
@@ -281,13 +281,13 @@ export default {
unsavedChanges: 'Незбережені зміни',
uploadedImages: 'Завантажені зображення',
userActions_title: 'Дії користувача',
- userAddedThisCardToList: '<0>{{user}}0><1> додав(ла) цю картку до {{list}}1>',
+ userAddedThisCardToList: '<0>{{user}}0> додав(ла) цю картку до {{list}}',
userLeftNewCommentToCard:
'<0>{{user}}0> залишив(ла) новий коментар «{{comment}}» до <2>{{card}}2>',
userMovedCardFromListToList:
'<0>{{user}}0> перемістив(ла) <2>{{card}}2> з {{fromList}} в {{toList}}',
userMovedThisCardFromListToList:
- '<0>{{user}}0><1> перемістив(ла) цю картку з {{fromList}} в {{toList}}1>',
+ '<0>{{user}}0> перемістив(ла) цю картку з {{fromList}} в {{toList}}',
username: "Ім'я користувача",
users: 'Користувачі',
viewer: 'Переглядач',
diff --git a/client/src/locales/uz-UZ/core.js b/client/src/locales/uz-UZ/core.js
index af7245b4..9048a968 100644
--- a/client/src/locales/uz-UZ/core.js
+++ b/client/src/locales/uz-UZ/core.js
@@ -130,13 +130,13 @@ export default {
time: 'Vaqt',
title: 'Sarlavha',
userActions_title: 'Foydalanuvchi Amallari',
- userAddedThisCardToList: "<1>Ushbu kartani {{list}} ga1><0>{{user}}0> qo'shdi",
+ userAddedThisCardToList: "Ushbu kartani {{list}} ga<0>{{user}}0> qo'shdi",
userLeftNewCommentToCard:
'<0>{{user}}0> <2>{{card}}2> ga yangi izoh qoldirdi «{{comment}}»',
userMovedCardFromListToList:
"<0>{{user}}0> <2>{{card}}2> ni {{fromList}} dan {{toList}} ga ko'chirdi",
userMovedThisCardFromListToList:
- "<0>{{user}}0><1> ushbu kartani {{fromList}} dan {{toList}}1> ga ko'chirdi",
+ "<0>{{user}}0> ushbu kartani {{fromList}} dan {{toList}} ga ko'chirdi",
username: 'Foydalanuvchi nomi',
users: 'Foydalanuvchilar',
writeComment: 'Izoh yozish...',
diff --git a/client/src/locales/zh-CN/core.js b/client/src/locales/zh-CN/core.js
index d8f3485d..665b3182 100644
--- a/client/src/locales/zh-CN/core.js
+++ b/client/src/locales/zh-CN/core.js
@@ -143,12 +143,11 @@ export default {
time: '时间',
title: '标题',
userActions_title: '用户操作',
- userAddedThisCardToList: '<0>{{user}}0><1> 向列表 {{list}} 添加了该卡片1>',
- userLeftNewCommentToCard: '<0>{{user}}0> 给 {{card}} 添加了一个新评论 «{{comment}}»',
+ userAddedThisCardToList: '<0>{{user}}0> 向列表 {{list}} 添加了该卡片',
+ userLeftNewCommentToCard: '<0>{{user}}0> 给 <2>{{card}}2> 添加了一个新评论 «{{comment}}»',
userMovedCardFromListToList:
'<0>{{user}}0> 将卡片 <2>{{card}}2> 从 {{fromList}} 移动到 {{toList}}',
- userMovedThisCardFromListToList:
- '<0>{{user}}0><1> 将该卡片从 {{fromList}} 移动到 {{toList}}1>',
+ userMovedThisCardFromListToList: '<0>{{user}}0> 将该卡片从 {{fromList}} 移动到 {{toList}}',
username: '用户名',
users: '用户',
viewer: '视图',
diff --git a/client/src/locales/zh-TW/core.js b/client/src/locales/zh-TW/core.js
index 7b2e9b69..91fe857c 100644
--- a/client/src/locales/zh-TW/core.js
+++ b/client/src/locales/zh-TW/core.js
@@ -143,12 +143,12 @@ export default {
time: '時間',
title: '標題',
userActions_title: '使用者操作',
- userAddedThisCardToList: '<0>{{user}}0><1> 向列表 {{list}} 添加了該卡片1>',
- userLeftNewCommentToCard: '<0>{{user}}0> 給 {{card}} 添加了一條新評論 「{{comment}}」',
+ userAddedThisCardToList: '<0>{{user}}0> 向列表 {{list}} 添加了該卡片',
+ userLeftNewCommentToCard:
+ '<0>{{user}}0> 給 <2>{{card}}2> 添加了一條新評論 「{{comment}}」',
userMovedCardFromListToList:
'<0>{{user}}0> 將卡片 <2>{{card}}2> 從 {{fromList}} 移動到 {{toList}}',
- userMovedThisCardFromListToList:
- '<0>{{user}}0><1> 將該卡片從 {{fromList}} 移動到 {{toList}}1>',
+ userMovedThisCardFromListToList: '<0>{{user}}0> 將該卡片從 {{fromList}} 移動到 {{toList}}',
username: '使用者名稱',
users: '使用者',
viewer: '檢視',
diff --git a/client/src/models/Activity.js b/client/src/models/Activity.js
index 71a630b2..a4d4a01c 100755
--- a/client/src/models/Activity.js
+++ b/client/src/models/Activity.js
@@ -18,6 +18,11 @@ export default class extends BaseModel {
createdAt: attr({
getDefault: () => new Date(),
}),
+ boardId: fk({
+ to: 'Board',
+ as: 'board',
+ relatedName: 'activities',
+ }),
cardId: fk({
to: 'Card',
as: 'card',
@@ -37,7 +42,8 @@ export default class extends BaseModel {
break;
case ActionTypes.LIST_CARDS_MOVE__SUCCESS:
- case ActionTypes.ACTIVITIES_FETCH__SUCCESS:
+ case ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS:
+ case ActionTypes.ACTIVITIES_IN_CARD_FETCH__SUCCESS:
payload.activities.forEach((activity) => {
Activity.upsert(activity);
});
diff --git a/client/src/models/Board.js b/client/src/models/Board.js
index b29e098c..429178b7 100755
--- a/client/src/models/Board.js
+++ b/client/src/models/Board.js
@@ -9,6 +9,7 @@ import BaseModel from './BaseModel';
import buildSearchParts from '../utils/build-search-parts';
import { isListFinite } from '../utils/record-helpers';
import ActionTypes from '../constants/ActionTypes';
+import Config from '../constants/Config';
import { BoardContexts, BoardViews } from '../constants/Enums';
const prepareFetchedBoard = (board) => ({
@@ -39,6 +40,15 @@ export default class extends BaseModel {
isFetching: attr({
getDefault: () => null,
}),
+ lastActivityId: attr({
+ getDefault: () => null,
+ }),
+ isActivitiesFetching: attr({
+ getDefault: () => false,
+ }),
+ isAllActivitiesFetched: attr({
+ getDefault: () => null,
+ }),
projectId: fk({
to: 'Project',
as: 'project',
@@ -241,6 +251,22 @@ export default class extends BaseModel {
case ActionTypes.LABEL_FROM_BOARD_FILTER_REMOVE:
Board.withId(payload.boardId).filterLabels.remove(payload.id);
+ break;
+ case ActionTypes.ACTIVITIES_IN_BOARD_FETCH:
+ Board.withId(payload.boardId).update({
+ isActivitiesFetching: true,
+ });
+
+ break;
+ case ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS:
+ Board.withId(payload.boardId).update({
+ isActivitiesFetching: false,
+ isAllActivitiesFetched: payload.activities.length < Config.ACTIVITIES_LIMIT,
+ ...(payload.activities.length > 0 && {
+ lastActivityId: payload.activities[payload.activities.length - 1].id,
+ }),
+ });
+
break;
default:
}
@@ -266,6 +292,10 @@ export default class extends BaseModel {
return this.customFieldGroups.orderBy(['position', 'id.length', 'id']);
}
+ getActivitiesQuerySet() {
+ return this.activities.orderBy(['id.length', 'id'], ['desc', 'desc']);
+ }
+
getUnreadNotificationsQuerySet() {
return this.notifications.filter({
isRead: false,
@@ -347,6 +377,30 @@ export default class extends BaseModel {
return cardModels;
}
+ getActivitiesModelArray() {
+ if (this.isAllActivitiesFetched === null) {
+ return [];
+ }
+
+ const activityModels = this.getActivitiesQuerySet().toModelArray();
+
+ if (this.lastActivityId && this.isAllActivitiesFetched === false) {
+ return activityModels.filter((activityModel) => {
+ if (activityModel.id.length > this.lastActivityId.length) {
+ return true;
+ }
+
+ if (activityModel.id.length < this.lastActivityId.length) {
+ return false;
+ }
+
+ return activityModel.id >= this.lastActivityId;
+ });
+ }
+
+ return activityModels;
+ }
+
hasMembershipWithUserId(userId) {
return this.memberships
.filter({
diff --git a/client/src/models/Card.js b/client/src/models/Card.js
index 5981aac8..8fe5b4bb 100755
--- a/client/src/models/Card.js
+++ b/client/src/models/Card.js
@@ -402,13 +402,13 @@ export default class extends BaseModel {
});
break;
- case ActionTypes.ACTIVITIES_FETCH:
+ case ActionTypes.ACTIVITIES_IN_CARD_FETCH:
Card.withId(payload.cardId).update({
isActivitiesFetching: true,
});
break;
- case ActionTypes.ACTIVITIES_FETCH__SUCCESS:
+ case ActionTypes.ACTIVITIES_IN_CARD_FETCH__SUCCESS:
Card.withId(payload.cardId).update({
isActivitiesFetching: false,
isAllActivitiesFetched: payload.activities.length < Config.ACTIVITIES_LIMIT,
@@ -607,7 +607,6 @@ export default class extends BaseModel {
this.customFieldValues.delete();
this.comments.delete();
- this.activities.delete();
}
deleteWithClearable() {
diff --git a/client/src/models/User.js b/client/src/models/User.js
index 3eb93569..5fd06e66 100755
--- a/client/src/models/User.js
+++ b/client/src/models/User.js
@@ -295,7 +295,8 @@ export default class extends BaseModel {
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.COMMENTS_FETCH__SUCCESS:
case ActionTypes.COMMENT_CREATE_HANDLE:
- case ActionTypes.ACTIVITIES_FETCH__SUCCESS:
+ case ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS:
+ case ActionTypes.ACTIVITIES_IN_CARD_FETCH__SUCCESS:
case ActionTypes.NOTIFICATION_CREATE_HANDLE:
payload.users.forEach((user) => {
User.upsert(user);
diff --git a/client/src/sagas/core/services/activities.js b/client/src/sagas/core/services/activities.js
index a72ebead..c319c1a4 100644
--- a/client/src/sagas/core/services/activities.js
+++ b/client/src/sagas/core/services/activities.js
@@ -10,10 +10,10 @@ import selectors from '../../../selectors';
import actions from '../../../actions';
import api from '../../../api';
-export function* fetchActivities(cardId) {
- const { lastActivityId } = yield select(selectors.selectCardById, cardId);
+export function* fetchActivitiesInBoard(boardId) {
+ const { lastActivityId } = yield select(selectors.selectBoardById, boardId);
- yield put(actions.fetchActivities(cardId));
+ yield put(actions.fetchActivitiesInBoard(boardId));
let activities;
let users;
@@ -22,21 +22,50 @@ export function* fetchActivities(cardId) {
({
items: activities,
included: { users },
- } = yield call(request, api.getActivities, cardId, {
+ } = yield call(request, api.getActivitiesInBoard, boardId, {
beforeId: lastActivityId || undefined,
}));
} catch (error) {
- yield put(actions.fetchActivities.failure(cardId, error));
+ yield put(actions.fetchActivitiesInBoard.failure(boardId, error));
return;
}
- yield put(actions.fetchActivities.success(cardId, activities, users));
+ yield put(actions.fetchActivitiesInBoard.success(boardId, activities, users));
+}
+
+export function* fetchActivitiesInCurrentBoard() {
+ const { boardId } = yield select(selectors.selectPath);
+
+ yield call(fetchActivitiesInBoard, boardId);
+}
+
+export function* fetchActivitiesInCard(cardId) {
+ const { lastActivityId } = yield select(selectors.selectCardById, cardId);
+
+ yield put(actions.fetchActivitiesInCard(cardId));
+
+ let activities;
+ let users;
+
+ try {
+ ({
+ items: activities,
+ included: { users },
+ } = yield call(request, api.getActivitiesInCard, cardId, {
+ beforeId: lastActivityId || undefined,
+ }));
+ } catch (error) {
+ yield put(actions.fetchActivitiesInCard.failure(cardId, error));
+ return;
+ }
+
+ yield put(actions.fetchActivitiesInCard.success(cardId, activities, users));
}
export function* fetchActivitiesInCurrentCard() {
const { cardId } = yield select(selectors.selectPath);
- yield call(fetchActivities, cardId);
+ yield call(fetchActivitiesInCard, cardId);
}
export function* handleActivityCreate(activity) {
@@ -44,7 +73,9 @@ export function* handleActivityCreate(activity) {
}
export default {
- fetchActivities,
+ fetchActivitiesInBoard,
+ fetchActivitiesInCurrentBoard,
+ fetchActivitiesInCard,
fetchActivitiesInCurrentCard,
handleActivityCreate,
};
diff --git a/client/src/sagas/core/watchers/activities.js b/client/src/sagas/core/watchers/activities.js
index a7a05ae1..5366d88d 100644
--- a/client/src/sagas/core/watchers/activities.js
+++ b/client/src/sagas/core/watchers/activities.js
@@ -10,6 +10,9 @@ import EntryActionTypes from '../../../constants/EntryActionTypes';
export default function* activitiesWatchers() {
yield all([
+ takeEvery(EntryActionTypes.ACTIVITIES_IN_CURRENT_BOARD_FETCH, () =>
+ services.fetchActivitiesInCurrentBoard(),
+ ),
takeEvery(EntryActionTypes.ACTIVITIES_IN_CURRENT_CARD_FETCH, () =>
services.fetchActivitiesInCurrentCard(),
),
diff --git a/client/src/selectors/boards.js b/client/src/selectors/boards.js
index 022d808d..c636d348 100644
--- a/client/src/selectors/boards.js
+++ b/client/src/selectors/boards.js
@@ -383,6 +383,24 @@ export const selectCustomFieldGroupsForCurrentBoard = createSelector(
},
);
+export const selectActivityIdsForCurrentBoard = createSelector(
+ orm,
+ (state) => selectPath(state).boardId,
+ ({ Board }, id) => {
+ if (!id) {
+ return id;
+ }
+
+ const boardModel = Board.withId(id);
+
+ if (!boardModel) {
+ return boardModel;
+ }
+
+ return boardModel.getActivitiesModelArray().map((activity) => activity.id);
+ },
+);
+
export const selectFilterUserIdsForCurrentBoard = createSelector(
orm,
(state) => selectPath(state).boardId,
@@ -447,6 +465,7 @@ export default {
selectFilteredCardIdsForCurrentBoard,
selectCustomFieldGroupIdsForCurrentBoard,
selectCustomFieldGroupsForCurrentBoard,
+ selectActivityIdsForCurrentBoard,
selectFilterUserIdsForCurrentBoard,
selectFilterLabelIdsForCurrentBoard,
selectIsBoardWithIdExists,
diff --git a/server/api/controllers/actions/index-in-board.js b/server/api/controllers/actions/index-in-board.js
new file mode 100755
index 00000000..91312bd8
--- /dev/null
+++ b/server/api/controllers/actions/index-in-board.js
@@ -0,0 +1,68 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+const { idInput } = require('../../../utils/inputs');
+
+const Errors = {
+ BOARD_NOT_FOUND: {
+ boardNotFound: 'Board not found',
+ },
+};
+
+module.exports = {
+ inputs: {
+ boardId: {
+ ...idInput,
+ required: true,
+ },
+ beforeId: idInput,
+ },
+
+ exits: {
+ boardNotFound: {
+ responseType: 'notFound',
+ },
+ },
+
+ async fn(inputs) {
+ const { currentUser } = this.req;
+
+ const { board, project } = await sails.helpers.boards
+ .getPathToProjectById(inputs.boardId)
+ .intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND);
+
+ const boardMembership = await BoardMembership.qm.getOneByBoardIdAndUserId(
+ board.id,
+ currentUser.id,
+ );
+
+ if (!boardMembership) {
+ if (currentUser.role !== User.Roles.ADMIN || project.ownerProjectManagerId) {
+ const isProjectManager = await sails.helpers.users.isProjectManager(
+ currentUser.id,
+ project.id,
+ );
+
+ if (!isProjectManager) {
+ throw Errors.BOARD_NOT_FOUND; // Forbidden
+ }
+ }
+ }
+
+ const actions = await Action.qm.getByBoardId(board.id, {
+ beforeId: inputs.beforeId,
+ });
+
+ const userIds = sails.helpers.utils.mapRecords(actions, 'userId', true, true);
+ const users = await User.qm.getByIds(userIds);
+
+ return {
+ items: actions,
+ included: {
+ users: sails.helpers.users.presentMany(users, currentUser),
+ },
+ };
+ },
+};
diff --git a/server/api/controllers/actions/index.js b/server/api/controllers/actions/index-in-card.js
similarity index 100%
rename from server/api/controllers/actions/index.js
rename to server/api/controllers/actions/index-in-card.js
diff --git a/server/api/helpers/actions/create-one.js b/server/api/helpers/actions/create-one.js
index d90b4020..d3d448cb 100644
--- a/server/api/helpers/actions/create-one.js
+++ b/server/api/helpers/actions/create-one.js
@@ -115,6 +115,7 @@ module.exports = {
const action = await Action.qm.createOne({
...values,
+ boardId: values.card.boardId,
cardId: values.card.id,
userId: values.user.id,
});
@@ -149,10 +150,7 @@ module.exports = {
values: {
action,
type: action.type,
- data: {
- ...action.data,
- card: _.pick(values.card, ['name']),
- },
+ data: action.data,
userId: action.data.user.id,
creatorUser: values.user,
card: values.card,
@@ -182,10 +180,7 @@ module.exports = {
userId,
action,
type: action.type,
- data: {
- ...action.data,
- card: _.pick(values.card, ['name']),
- },
+ data: action.data,
creatorUser: values.user,
card: values.card,
},
diff --git a/server/api/helpers/boards/delete-related.js b/server/api/helpers/boards/delete-related.js
index 24f0e875..5828172e 100644
--- a/server/api/helpers/boards/delete-related.js
+++ b/server/api/helpers/boards/delete-related.js
@@ -35,6 +35,15 @@ module.exports = {
await sails.helpers.lists.deleteRelated(lists);
+ await Action.qm.update(
+ {
+ boardId: boardIdOrIds,
+ },
+ {
+ boardId: null,
+ },
+ );
+
await NotificationService.qm.delete({
boardId: boardIdOrIds,
});
diff --git a/server/api/helpers/card-memberships/create-one.js b/server/api/helpers/card-memberships/create-one.js
index b66fd2fb..e71c2e0d 100644
--- a/server/api/helpers/card-memberships/create-one.js
+++ b/server/api/helpers/card-memberships/create-one.js
@@ -110,6 +110,7 @@ module.exports = {
type: Action.Types.ADD_MEMBER_TO_CARD,
data: {
user: _.pick(values.user, ['id', 'name']),
+ card: _.pick(values.card, ['name']),
},
user: inputs.actorUser,
card: values.card,
diff --git a/server/api/helpers/card-memberships/delete-one.js b/server/api/helpers/card-memberships/delete-one.js
index 659bb90a..4d07111e 100644
--- a/server/api/helpers/card-memberships/delete-one.js
+++ b/server/api/helpers/card-memberships/delete-one.js
@@ -86,6 +86,7 @@ module.exports = {
type: Action.Types.REMOVE_MEMBER_FROM_CARD,
data: {
user: _.pick(inputs.user, ['id', 'name']),
+ card: _.pick(inputs.card, ['name']),
},
user: inputs.actorUser,
card: inputs.card,
diff --git a/server/api/helpers/cards/create-one.js b/server/api/helpers/cards/create-one.js
index 0d721e1c..ae05a43e 100644
--- a/server/api/helpers/cards/create-one.js
+++ b/server/api/helpers/cards/create-one.js
@@ -124,6 +124,7 @@ module.exports = {
card,
type: Action.Types.CREATE_CARD,
data: {
+ card: _.pick(card, ['name']),
list: _.pick(values.list, ['id', 'type', 'name']),
},
user: values.creatorUser,
diff --git a/server/api/helpers/cards/duplicate-one.js b/server/api/helpers/cards/duplicate-one.js
index 317e68e6..3c0f3970 100644
--- a/server/api/helpers/cards/duplicate-one.js
+++ b/server/api/helpers/cards/duplicate-one.js
@@ -276,6 +276,7 @@ module.exports = {
card,
type: Action.Types.CREATE_CARD, // TODO: introduce separate type?
data: {
+ card: _.pick(card, ['name']),
list: _.pick(inputs.list, ['id', 'type', 'name']),
},
user: values.creatorUser,
diff --git a/server/api/helpers/cards/update-one.js b/server/api/helpers/cards/update-one.js
index 84cf8814..26071782 100644
--- a/server/api/helpers/cards/update-one.js
+++ b/server/api/helpers/cards/update-one.js
@@ -463,6 +463,7 @@ module.exports = {
card,
type: Action.Types.MOVE_CARD,
data: {
+ card: _.pick(card, ['name']),
fromList: _.pick(inputs.list, ['id', 'type', 'name']),
toList: _.pick(values.list, ['id', 'type', 'name']),
},
diff --git a/server/api/helpers/tasks/update-one.js b/server/api/helpers/tasks/update-one.js
index 6261398f..c503e915 100644
--- a/server/api/helpers/tasks/update-one.js
+++ b/server/api/helpers/tasks/update-one.js
@@ -138,6 +138,7 @@ module.exports = {
values: {
type: task.isCompleted ? Action.Types.COMPLETE_TASK : Action.Types.UNCOMPLETE_TASK,
data: {
+ card: _.pick(inputs.card, ['name']),
task: _.pick(task, ['id', 'name']),
},
user: inputs.actorUser,
diff --git a/server/api/hooks/query-methods/models/Action.js b/server/api/hooks/query-methods/models/Action.js
index b445a6d0..25a58b95 100644
--- a/server/api/hooks/query-methods/models/Action.js
+++ b/server/api/hooks/query-methods/models/Action.js
@@ -11,6 +11,20 @@ const create = (arrayOfValues) => Action.createEach(arrayOfValues).fetch();
const createOne = (values) => Action.create({ ...values }).fetch();
+const getByBoardId = (boardId, { beforeId } = {}) => {
+ const criteria = {
+ boardId,
+ };
+
+ if (beforeId) {
+ criteria.id = {
+ '<': beforeId,
+ };
+ }
+
+ return Action.find(criteria).sort('id DESC').limit(LIMIT);
+};
+
const getByCardId = (cardId, { beforeId } = {}) => {
const criteria = {
cardId,
@@ -33,6 +47,7 @@ const delete_ = (criteria) => Action.destroy(criteria).fetch();
module.exports = {
create,
createOne,
+ getByBoardId,
getByCardId,
update,
delete: delete_,
diff --git a/server/api/models/Action.js b/server/api/models/Action.js
index 955bf812..bc742385 100755
--- a/server/api/models/Action.js
+++ b/server/api/models/Action.js
@@ -52,6 +52,10 @@ module.exports = {
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
+ boardId: {
+ model: 'Board',
+ columnName: 'board_id',
+ },
cardId: {
model: 'Card',
required: true,
diff --git a/server/config/routes.js b/server/config/routes.js
index 3307beb3..ba2292bb 100644
--- a/server/config/routes.js
+++ b/server/config/routes.js
@@ -163,7 +163,8 @@ module.exports.routes = {
'PATCH /api/comments/:id': 'comments/update',
'DELETE /api/comments/:id': 'comments/delete',
- 'GET /api/cards/:cardId/actions': 'actions/index',
+ 'GET /api/boards/:boardId/actions': 'actions/index-in-board',
+ 'GET /api/cards/:cardId/actions': 'actions/index-in-card',
'GET /api/notifications': 'notifications/index',
'GET /api/notifications/:id': 'notifications/show',
diff --git a/server/db/migrations/20250522151122_add_board_activity_log.js b/server/db/migrations/20250522151122_add_board_activity_log.js
new file mode 100644
index 00000000..feaf095c
--- /dev/null
+++ b/server/db/migrations/20250522151122_add_board_activity_log.js
@@ -0,0 +1,30 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+exports.up = async (knex) => {
+ await knex.schema.alterTable('action', (table) => {
+ /* Columns */
+
+ table.bigInteger('board_id');
+
+ /* Indexes */
+
+ table.index('board_id');
+ });
+
+ return knex.raw(`
+ UPDATE action
+ SET
+ board_id = card.board_id,
+ data = data || jsonb_build_object('card', jsonb_build_object('name', card.name))
+ FROM card
+ WHERE action.card_id = card.id;
+ `);
+};
+
+exports.down = (knex) =>
+ knex.schema.table('action', (table) => {
+ table.dropColumn('board_id');
+ });
From f9d3e736515e58212d89a3ebbf8b42b8e17b1697 Mon Sep 17 00:00:00 2001
From: Hannes
Date: Fri, 23 May 2025 16:19:22 +0200
Subject: [PATCH 016/108] build: Improve Docker dev setup, add nodemon config
(#1145)
* Updated the base image in `Dockerfile.dev` from `node:18-alpine` to `node:lts-alpine` to use more or less the same as for production docker build (where the client is build lts and server is 18... so yeah not really...)
* Removed the `USER node` directive, to prevent permission issues as we hook the whole folder into the container.
* Modified the `docker-compose-dev.yml` file to enhance the database healthcheck. The new healthcheck ensures not only postgres is ready but also the database is ready by running a sample query (`SELECT 1`) and adjusts the interval, retries, and start period for better reliability.
* Added a `nodemon.json` configuration file to watch only relevant files (node_modules would be ignored but not files like the python venv) and avoid rapid successive restarts with the delay.
---
Dockerfile.dev | 3 +--
docker-compose-dev.yml | 11 ++++++++---
server/nodemon.json | 15 +++++++++++++++
3 files changed, 24 insertions(+), 5 deletions(-)
create mode 100644 server/nodemon.json
diff --git a/Dockerfile.dev b/Dockerfile.dev
index d59e3cf3..6985fc57 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -1,8 +1,7 @@
-FROM node:18-alpine
+FROM node:lts-alpine
RUN apk -U upgrade \
&& apk add bash build-base python3 xdg-utils --no-cache \
&& npm install npm --global
-USER node
WORKDIR /app
diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml
index 3beb5685..2e4d08ce 100644
--- a/docker-compose-dev.yml
+++ b/docker-compose-dev.yml
@@ -109,10 +109,15 @@ services:
- POSTGRES_DB=planka
- POSTGRES_HOST_AUTH_METHOD=trust
healthcheck:
- test: ["CMD-SHELL", "pg_isready -U postgres -d planka"]
- interval: 10s
+ test:
+ [
+ "CMD-SHELL",
+ "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-planka} && psql -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-planka} -c 'SELECT 1'",
+ ]
+ interval: 3s
timeout: 5s
- retries: 5
+ retries: 15
+ start_period: 10s
volumes:
db-data:
diff --git a/server/nodemon.json b/server/nodemon.json
new file mode 100644
index 00000000..d552807c
--- /dev/null
+++ b/server/nodemon.json
@@ -0,0 +1,15 @@
+{
+ "watch": [
+ "api/**/*.js",
+ "config/**/*.js",
+ "db/**/*.js",
+ "utils/**/*.js",
+ "app.js",
+ "constants.js"
+ ],
+ "ignore": ["node_modules", ".tmp", ".venv"],
+ "ext": "js,json",
+ "verbose": true,
+ "delay": 1000,
+ "legacyWatch": false
+}
From 4049b4c3965c3ad80108c6e0a3bed8c22c6f3049 Mon Sep 17 00:00:00 2001
From: Hannes
Date: Fri, 23 May 2025 17:31:28 +0200
Subject: [PATCH 017/108] feat: Display comments total on front of cards
(#1146)
Closes #1136
---
.../components/cards/Card/ProjectContent.jsx | 9 +++
client/src/models/Card.js | 3 +
client/src/models/Comment.js | 49 +++++++++---
.../api/hooks/query-methods/models/Comment.js | 80 ++++++++++++++++++-
server/api/models/Card.js | 5 ++
.../20250523131647_add_comments_counter.js | 32 ++++++++
6 files changed, 165 insertions(+), 13 deletions(-)
create mode 100644 server/db/migrations/20250523131647_add_comments_counter.js
diff --git a/client/src/components/cards/Card/ProjectContent.jsx b/client/src/components/cards/Card/ProjectContent.jsx
index 0f74693a..3582b73a 100644
--- a/client/src/components/cards/Card/ProjectContent.jsx
+++ b/client/src/components/cards/Card/ProjectContent.jsx
@@ -114,6 +114,7 @@ const ProjectContent = React.memo(({ cardId }) => {
card.description ||
card.dueDate ||
card.stopwatch ||
+ card.commentsTotal > 0 ||
attachmentsTotal > 0 ||
notificationsTotal > 0 ||
listName;
@@ -226,6 +227,14 @@ const ProjectContent = React.memo(({ cardId }) => {
)}
+ {card.commentsTotal > 0 && (
+
+
+
+ {card.commentsTotal}
+
+
+ )}
)}
{!isCompact && usersNode}
diff --git a/client/src/models/Card.js b/client/src/models/Card.js
index 8fe5b4bb..6da30db5 100755
--- a/client/src/models/Card.js
+++ b/client/src/models/Card.js
@@ -20,6 +20,9 @@ export default class extends BaseModel {
description: attr(),
dueDate: attr(),
stopwatch: attr(),
+ commentsTotal: attr({
+ getDefault: () => 0,
+ }),
createdAt: attr({
getDefault: () => new Date(),
}),
diff --git a/client/src/models/Comment.js b/client/src/models/Comment.js
index cda99f60..eea83d5c 100755
--- a/client/src/models/Comment.js
+++ b/client/src/models/Comment.js
@@ -42,31 +42,47 @@ export default class extends BaseModel {
break;
case ActionTypes.COMMENT_CREATE:
- case ActionTypes.COMMENT_CREATE_HANDLE:
- case ActionTypes.COMMENT_UPDATE__SUCCESS:
- case ActionTypes.COMMENT_UPDATE_HANDLE:
- Comment.upsert(payload.comment);
+ case ActionTypes.COMMENT_CREATE_HANDLE: {
+ const commentModel = Comment.upsert(payload.comment);
+
+ if (commentModel.card) {
+ commentModel.card.commentsTotal += 1;
+ }
break;
+ }
case ActionTypes.COMMENT_CREATE__SUCCESS:
Comment.withId(payload.localId).delete();
Comment.upsert(payload.comment);
break;
- case ActionTypes.COMMENT_CREATE__FAILURE:
- Comment.withId(payload.localId).delete();
+ case ActionTypes.COMMENT_CREATE__FAILURE: {
+ const commentModel = Comment.withId(payload.localId);
+ commentModel.delete();
+
+ if (commentModel.card) {
+ commentModel.card.commentsTotal -= 1;
+ }
break;
+ }
case ActionTypes.COMMENT_UPDATE:
Comment.withId(payload.id).update(payload.data);
break;
- case ActionTypes.COMMENT_DELETE:
- Comment.withId(payload.id).delete();
+ case ActionTypes.COMMENT_UPDATE__SUCCESS:
+ case ActionTypes.COMMENT_UPDATE_HANDLE:
+ Comment.upsert(payload.comment);
break;
- case ActionTypes.COMMENT_DELETE__SUCCESS:
- case ActionTypes.COMMENT_DELETE_HANDLE: {
+ case ActionTypes.COMMENT_DELETE: {
+ const commentModel = Comment.withId(payload.id);
+ commentModel.delete();
+ commentModel.card.commentsTotal -= 1;
+
+ break;
+ }
+ case ActionTypes.COMMENT_DELETE__SUCCESS: {
const commentModel = Comment.withId(payload.comment.id);
if (commentModel) {
@@ -75,6 +91,19 @@ export default class extends BaseModel {
break;
}
+ case ActionTypes.COMMENT_DELETE_HANDLE: {
+ const commentModel = Comment.withId(payload.comment.id);
+
+ if (commentModel) {
+ commentModel.delete();
+
+ if (commentModel.card) {
+ commentModel.card.commentsTotal -= 1;
+ }
+ }
+
+ break;
+ }
default:
}
}
diff --git a/server/api/hooks/query-methods/models/Comment.js b/server/api/hooks/query-methods/models/Comment.js
index 91f0712f..7d4a528f 100644
--- a/server/api/hooks/query-methods/models/Comment.js
+++ b/server/api/hooks/query-methods/models/Comment.js
@@ -10,7 +10,25 @@ const defaultFind = (criteria, { limit } = {}) =>
/* Query methods */
-const createOne = (values) => Comment.create({ ...values }).fetch();
+const createOne = (values) =>
+ sails.getDatastore().transaction(async (db) => {
+ const comment = await Comment.create({ ...values })
+ .fetch()
+ .usingConnection(db);
+
+ const queryResult = await sails
+ .sendNativeQuery(
+ 'UPDATE card SET comments_total = comments_total + 1, updated_at = $1 WHERE id = $2',
+ [new Date().toISOString(), comment.cardId],
+ )
+ .usingConnection(db);
+
+ if (queryResult.rowCount === 0) {
+ throw 'cardNotFound';
+ }
+
+ return comment;
+ });
const getByIds = (ids) => defaultFind(ids);
@@ -35,9 +53,65 @@ const update = (criteria, values) => Comment.update(criteria).set(values).fetch(
const updateOne = (criteria, values) => Comment.updateOne(criteria).set({ ...values });
// eslint-disable-next-line no-underscore-dangle
-const delete_ = (criteria) => Comment.destroy(criteria).fetch();
+const delete_ = (criteria) =>
+ sails.getDatastore().transaction(async (db) => {
+ const comments = await Comment.destroy(criteria).fetch().usingConnection(db);
-const deleteOne = (criteria) => Comment.destroyOne(criteria);
+ if (comments.length > 0) {
+ const commentsByCardId = _.groupBy(comments, 'cardId');
+
+ const cardIdsByTotal = Object.entries(commentsByCardId).reduce(
+ (result, [cardId, commentsItem]) => ({
+ ...result,
+ [commentsItem.length]: [...(result[commentsItem.length] || []), cardId],
+ }),
+ {},
+ );
+
+ const queryValues = [];
+ let query = 'UPDATE card SET comments_total = comments_total - CASE ';
+
+ Object.entries(cardIdsByTotal).forEach(([total, cardIds]) => {
+ const inValues = cardIds.map((cardId) => {
+ queryValues.push(cardId);
+ return `$${queryValues.length}`;
+ });
+
+ queryValues.push(total);
+ query += `WHEN id IN (${inValues.join(', ')}) THEN $${queryValues.length}::int `;
+ });
+
+ const inValues = Object.keys(commentsByCardId).map((cardId) => {
+ queryValues.push(cardId);
+ return `$${queryValues.length}`;
+ });
+
+ queryValues.push(new Date().toISOString());
+ query += `END, updated_at = $${queryValues.length} WHERE id IN (${inValues.join(', ')})`;
+
+ await sails.sendNativeQuery(query, queryValues).usingConnection(db);
+ }
+
+ return comments;
+ });
+
+const deleteOne = (criteria) =>
+ sails.getDatastore().transaction(async (db) => {
+ const comment = await Comment.destroyOne(criteria).usingConnection(db);
+
+ const queryResult = await sails
+ .sendNativeQuery(
+ 'UPDATE card SET comments_total = comments_total - 1, updated_at = $1 WHERE id = $2',
+ [new Date().toISOString(), comment.cardId],
+ )
+ .usingConnection(db);
+
+ if (queryResult.rowCount === 0) {
+ throw 'cardNotFound';
+ }
+
+ return comment;
+ });
module.exports = {
createOne,
diff --git a/server/api/models/Card.js b/server/api/models/Card.js
index 02cf67a5..dc3073c2 100755
--- a/server/api/models/Card.js
+++ b/server/api/models/Card.js
@@ -48,6 +48,11 @@ module.exports = {
stopwatch: {
type: 'json',
},
+ commentsTotal: {
+ type: 'number',
+ defaultsTo: 0,
+ columnName: 'comments_total',
+ },
listChangedAt: {
type: 'ref',
columnName: 'list_changed_at',
diff --git a/server/db/migrations/20250523131647_add_comments_counter.js b/server/db/migrations/20250523131647_add_comments_counter.js
new file mode 100644
index 00000000..2108832a
--- /dev/null
+++ b/server/db/migrations/20250523131647_add_comments_counter.js
@@ -0,0 +1,32 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+exports.up = async (knex) => {
+ await knex.schema.alterTable('card', (table) => {
+ /* Columns */
+
+ table.integer('comments_total').notNullable().defaultTo(0);
+ });
+
+ await knex.raw(`
+ UPDATE card
+ SET comments_total = comments_total_by_card_id.comments_total
+ FROM (
+ SELECT card_id, COUNT(*) as comments_total
+ FROM comment
+ GROUP BY card_id
+ ) AS comments_total_by_card_id
+ WHERE card.id = comments_total_by_card_id.card_id
+ `);
+
+ return knex.schema.alterTable('card', (table) => {
+ table.integer('comments_total').notNullable().alter();
+ });
+};
+
+exports.down = (knex) =>
+ knex.schema.table('card', (table) => {
+ table.dropColumn('comments_total');
+ });
From 71e2c5d8dfacb2e5f1164cb27f38fc3d46933569 Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Sat, 24 May 2025 13:01:07 +0200
Subject: [PATCH 018/108] fix: Apply disabled style to card content instead of
wrapper
---
client/src/components/cards/Card/Card.jsx | 22 ++++++++++++-------
.../components/cards/Card/Card.module.scss | 10 ++++-----
2 files changed, 19 insertions(+), 13 deletions(-)
diff --git a/client/src/components/cards/Card/Card.jsx b/client/src/components/cards/Card/Card.jsx
index dc78ef09..00dced39 100755
--- a/client/src/components/cards/Card/Card.jsx
+++ b/client/src/components/cards/Card/Card.jsx
@@ -101,18 +101,19 @@ const Card = React.memo(({ id, isInline }) => {
return (
{card.isPersisted ? (
<>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
-
+
{colorLineNode}
@@ -125,7 +126,12 @@ const Card = React.memo(({ id, isInline }) => {
)}
>
) : (
-
+
{colorLineNode}
diff --git a/client/src/components/cards/Card/Card.module.scss b/client/src/components/cards/Card/Card.module.scss
index 4e0c4681..4d71b897 100644
--- a/client/src/components/cards/Card/Card.module.scss
+++ b/client/src/components/cards/Card/Card.module.scss
@@ -43,6 +43,11 @@
}
}
+ .contentDisabled {
+ filter: saturate(0.5);
+ opacity: 0.64;
+ }
+
.wrapper {
background: #fff;
border-radius: 3px;
@@ -59,11 +64,6 @@
}
}
- .wrapperDisabled {
- filter: saturate(0.5);
- opacity: 0.64;
- }
-
.wrapperRecent:not(:hover) {
overflow: hidden;
From a7c1d4ca11d86fef57731e42156fa6d66602d57d Mon Sep 17 00:00:00 2001
From: Roman Zavarnitsyn
Date: Mon, 26 May 2025 12:49:52 +0200
Subject: [PATCH 019/108] build: Improve dev setup (#1149)
---
docker-compose-dev.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml
index 2e4d08ce..6cbe0513 100644
--- a/docker-compose-dev.yml
+++ b/docker-compose-dev.yml
@@ -12,6 +12,7 @@ services:
ports:
- 1337:1337
environment:
+ - BASE_URL=http://localhost:1337
- DATABASE_URL=postgresql://postgres@postgres/planka
- SECRET_KEY=notsecretkey
From 978b2a7402d29862a64ddb43ef6167e6b65a0617 Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Mon, 26 May 2025 14:35:23 +0200
Subject: [PATCH 020/108] fix: Set limit for displaying attachment content
Closes #1141
---
.../attachments/Attachments/Item.jsx | 23 +++++++++++++------
client/src/constants/Config.js | 3 +++
client/src/locales/en-GB/core.js | 1 +
client/src/locales/en-US/core.js | 1 +
4 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/client/src/components/attachments/Attachments/Item.jsx b/client/src/components/attachments/Attachments/Item.jsx
index 550b9b1c..7fedb769 100644
--- a/client/src/components/attachments/Attachments/Item.jsx
+++ b/client/src/components/attachments/Attachments/Item.jsx
@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
import { Item as GalleryItem } from 'react-photoswipe-gallery';
import selectors from '../../../selectors';
+import Config from '../../../constants/Config';
import Encodings from '../../../constants/Encodings';
import { AttachmentTypes } from '../../../constants/Enums';
import ItemContent from './ItemContent';
@@ -70,13 +71,21 @@ const Item = React.memo(({ id, isVisible }) => {
break;
default:
if (attachment.data.encoding === Encodings.UTF8) {
- content = (
-
- );
+ if (attachment.data.sizeInBytes <= Config.MAX_SIZE_IN_BYTES_TO_DISPLAY_CONTENT) {
+ content = (
+
+ );
+ } else {
+ content = (
+
+ {t('common.contentOfThisAttachmentIsTooBigToDisplay')}
+
+ );
+ }
} else {
content = (
diff --git a/client/src/constants/Config.js b/client/src/constants/Config.js
index 372f81cb..14e357c3 100755
--- a/client/src/constants/Config.js
+++ b/client/src/constants/Config.js
@@ -15,6 +15,8 @@ const CARDS_LIMIT = 50;
const COMMENTS_LIMIT = 50;
const ACTIVITIES_LIMIT = 50;
+const MAX_SIZE_IN_BYTES_TO_DISPLAY_CONTENT = 256 * 1024;
+
const IS_MAC = navigator.platform.startsWith('Mac');
export default {
@@ -26,5 +28,6 @@ export default {
CARDS_LIMIT,
COMMENTS_LIMIT,
ACTIVITIES_LIMIT,
+ MAX_SIZE_IN_BYTES_TO_DISPLAY_CONTENT,
IS_MAC,
};
diff --git a/client/src/locales/en-GB/core.js b/client/src/locales/en-GB/core.js
index ae9d0d4e..37cbe476 100644
--- a/client/src/locales/en-GB/core.js
+++ b/client/src/locales/en-GB/core.js
@@ -115,6 +115,7 @@ export default {
color: 'Color',
comments: 'Comments',
contentExceedsLimit: 'Content exceeds {{limit}}',
+ contentOfThisAttachmentIsTooBigToDisplay: 'Content of this attachment is too big to display.',
copy_inline: 'copy',
createBoard_title: 'Create Board',
createCustomFieldGroup_title: 'Create Custom Field Group',
diff --git a/client/src/locales/en-US/core.js b/client/src/locales/en-US/core.js
index a42ac99d..ce550e1c 100644
--- a/client/src/locales/en-US/core.js
+++ b/client/src/locales/en-US/core.js
@@ -110,6 +110,7 @@ export default {
color: 'Color',
comments: 'Comments',
contentExceedsLimit: 'Content exceeds {{limit}}',
+ contentOfThisAttachmentIsTooBigToDisplay: 'Content of this attachment is too big to display.',
copy_inline: 'copy',
createBoard_title: 'Create Board',
createCustomFieldGroup_title: 'Create Custom Field Group',
From 20b5351dfe7aec874642aad7e7a3d2dcb7db9469 Mon Sep 17 00:00:00 2001
From: Piotr Patalong
Date: Mon, 26 May 2025 15:02:51 +0200
Subject: [PATCH 021/108] fix: Update Polish translation (#1150)
---
client/src/locales/pl-PL/core.js | 296 +++++++++++++++---
client/src/locales/pl-PL/login.js | 13 +-
client/src/locales/pl-PL/markdown-editor.json | 165 ++++++++++
3 files changed, 422 insertions(+), 52 deletions(-)
create mode 100644 client/src/locales/pl-PL/markdown-editor.json
diff --git a/client/src/locales/pl-PL/core.js b/client/src/locales/pl-PL/core.js
index b0af5cff..0333ab07 100644
--- a/client/src/locales/pl-PL/core.js
+++ b/client/src/locales/pl-PL/core.js
@@ -1,9 +1,12 @@
import dateFns from 'date-fns/locale/pl';
import timeAgo from 'javascript-time-ago/locale/pl';
+import markdownEditor from './markdown-editor.json';
+
export default {
dateFns,
timeAgo,
+ markdownEditor,
format: {
date: 'd/M/yyyy',
@@ -20,98 +23,197 @@ export default {
aboutPlanka: 'O PLANKA',
account: 'Konto',
actions: 'Akcje',
+ activateUser_title: 'Aktywuj Użytkownika',
+ active: 'Aktywny',
addAttachment_title: 'Dodaj Załącznik',
- addManager_title: 'Dodaj Zarządce',
+ addCustomField_title: 'Dodaj Własne Pole',
+ addCustomFieldGroup_title: 'Dodaj Grupę Własnych Pól',
+ addManager_title: 'Dodaj Zarządcę',
addMember_title: 'Dodaj Członka',
+ addTaskList_title: 'Dodaj Listę Zadań',
addUser_title: 'Dodaj Użytkownika',
+ admin: 'Administrator',
+ administration: 'Administracja',
all: 'Wszystko',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Wszystkie zmiany zostaną automatycznie zapisane po przywróceniu połączenia.',
- areYouSureYouWantToDeleteThisAttachment: 'Jesteś pewien że chcesz usunąć ten załącznik?',
- areYouSureYouWantToDeleteThisBoard: 'Jesteś pewien że chcesz usunąć tę tablicę?',
- areYouSureYouWantToDeleteThisCard: 'Jesteś pewien że chcesz usunąć tę kartę?',
- areYouSureYouWantToDeleteThisComment: 'Jesteś pewien że chcesz usunąć ten komentarz?',
- areYouSureYouWantToDeleteThisLabel: 'Jesteś pewien że chcesz usunąć to oznaczenie?',
- areYouSureYouWantToDeleteThisList: 'Jesteś pewien że chcesz usunąć tę listę?',
- areYouSureYouWantToDeleteThisProject: 'Jesteś pewien że chcesz usunąć ten projekt?',
- areYouSureYouWantToDeleteThisTask: 'Jesteś pewien że chcesz usunąć to zadanie?',
- areYouSureYouWantToDeleteThisUser: 'Jesteś pewien że chcesz usunąć tego użytkownika?',
+ alphabetically: 'Alfabetycznie',
+ alwaysDisplayCardCreator: 'Zawsze pokazuj twórcę karty',
+ archive: 'Archiwum',
+ archiveCard_title: 'Archiwizuj Kartę',
+ archiveCards_title: 'Archiwizuj Karty',
+ areYouSureYouWantToActivateThisUser: 'Jesteś pewien że chcesz aktywować tego użytkownika?',
+ areYouSureYouWantToArchiveCards: 'Jesteś pewien że chcesz archiwizować karty?',
+ areYouSureYouWantToArchiveThisCard: 'Jesteś pewien że chcesz archiwizować tę kartę?',
+ areYouSureYouWantToAssignThisProjectManagerAsOwner:
+ 'Jesteś pewien że chcesz przypisać tego zarządcę jako właściciela projektu?',
+ areYouSureYouWantToDeactivateThisUser:
+ 'Jesteś pewien że chcesz dezaktywować tego użytkownika?',
+ areYouSureYouWantToUsuńThisAttachment: 'Jesteś pewien że chcesz usunąć ten załącznik?',
+ areYouSureYouWantToUsuńThisBackgroundImage: 'Jesteś pewien że chcesz usunąć te tło?',
+ areYouSureYouWantToUsuńThisBoard: 'Jesteś pewien że chcesz usunąć tę tablicę?',
+ areYouSureYouWantToUsuńThisCard: 'Jesteś pewien że chcesz usunąć tę kartę?',
+ areYouSureYouWantToUsuńThisCardForever:
+ 'Jesteś pewien że chcesz usunąć tę kartę permanentnie?',
+ areYouSureYouWantToUsuńThisComment: 'Jesteś pewien że chcesz usunąć ten komentarz?',
+ areYouSureYouWantToUsuńThisCustomField: 'Jesteś pewien że chcesz usunąć to pole własne?',
+ areYouSureYouWantToUsuńThisCustomFieldGroup:
+ 'Jesteś pewien że chcesz usunąć tę grupę pól własnych?',
+ areYouSureYouWantToUsuńThisLabel: 'Jesteś pewien że chcesz usunąć to oznaczenie?',
+ areYouSureYouWantToUsuńThisList:
+ 'Jesteś pewien że chcesz usunąć tę listę? Wszyskie karty zostaną przeniesione do kosza.',
+ areYouSureYouWantToUsuńThisNotificationService:
+ 'Jesteś pewien że chcesz usunąć ten serwis powiadomień?',
+ areYouSureYouWantToUsuńThisProject: 'Jesteś pewien że chcesz usunąć ten projekt?',
+ areYouSureYouWantToUsuńThisTask: 'Jesteś pewien że chcesz usunąć te zadanie?',
+ areYouSureYouWantToUsuńThisTaskList: 'Jesteś pewien że chcesz usunąć tę listę zadań?',
+ areYouSureYouWantToUsuńThisUser: 'Jesteś pewien że chcesz usunąć tego użytkownika?',
+ areYouSureYouWantToEmptyTrash: 'Jesteś pewien że chcesz opróżnić kosz?',
+ areYouSureYouWantToLeaveBoard: 'Jesteś pewien że chcesz opuścić tablicę?',
+ areYouSureYouWantToLeaveProject: 'Jesteś pewien że chcesz opuścić projekt?',
+ areYouSureYouWantToMakeThisProjectShared: 'Jesteś pewien że chcesz udostępnić ten projekt?',
areYouSureYouWantToRemoveThisManagerFromProject:
- 'Jesteś pewien że chcesz usunąć tego zarządce z projektu?',
+ 'Jesteś pewien że chcesz usunąć tego zarządcę z projektu?',
+ areYouSureYouWantToRemoveThisMemberFromBoard:
+ 'Jesteś pewien że chcesz usunąć tego członka z tablicy?',
+ assignAsOwner_title: 'Przypisz Jako Właściciela',
+ atLeastOneListMustBePresent: 'Przynajmniej jedna lista musi istnieć',
attachment: 'Załącznik',
attachments: 'Załączniki',
authentication: 'Uwierzytelnianie',
background: 'Tło',
+ baseCustomFields_title: 'Bazowe Pola Własne',
+ baseGroup: 'Grupa bazowa',
board: 'Tablica',
+ boardActions_title: 'Akcje Tablicy',
boardNotFound_title: 'Tablica Nie Znaleziona',
+ boardSubscribed: 'Zasubskrybowano Tablicę',
+ boardUser: 'Użytkownik Tablicy',
+ byCreationTime: 'Po dacie utworzenia',
+ byDefault: 'Domyślnie',
+ byDueDate: 'Po dacie wykonania',
+ canBeInvitedToWorkInBoards: 'Może być zaproszony do pracy przy tablicach.',
canComment: 'Może komentować',
- canOnlyViewBoard: 'Może tylko przeglądać tablicę.',
+ canCreateOwnProjectsAndBeInvitedToWorkInOthers:
+ 'Może tworzyć własne projekty i być zaproszony do pracy w innych.',
+ canEditBoardLayoutAndAssignMembersToCards:
+ 'Może edytować układ tablicy i przypisywać użytkowników do kart.',
+ canManageSystemWideSettingsAndActAsProjectOwner:
+ 'Może zarządzać ustawieniami systemowymi i działać jako właściciel projektu.',
+ canOnlyViewBoard: 'Może tylko wyświetlać tablicę.',
cardActions_title: 'Akcje Karty',
cardNotFound_title: 'Karta Nie Znaleziona',
+ cardsOnThisListAreAvailableToAllBoardMembers:
+ 'Karty na tej liście są dostępne dla wszystkich członków tablicy.',
+ cardsOnThisListAreCompleteAndReadyToBeArchived:
+ 'Karty na tej liście są ukończone i gotowe do zarchiwizowania.',
+ cardsOnThisListAreReadyToBeWorkedOn: 'Karty na tej liście są gotowe do pracy nad nimi.',
+ clickHereOrRefreshPageToUpdate: '<0>Naciśnij tutaj0> lub odśwież stronę, by zaktualizować.',
+ closed: 'Zamknięte',
color: 'Kolor',
+ comments: 'Komentarze',
+ contentExceedsLimit: 'Zawartość przekracza {{limit}}',
copy_inline: 'kopiuj',
createBoard_title: 'Utwórz Tablicę',
+ createCustomFieldGroup_title: 'Utwórz Grupę Pól Własnych',
createLabel_title: 'Utwórz Oznaczenie',
- createNewOneOrSelectExistingOne: 'Utwórz nowy lub wybierz istniejący.',
+ createNewOneOrSelectExistingOne: 'Stwórz nowe lub wybierz istniejące.',
createProject_title: 'Utwórz Projekt',
createTextFile_title: 'Utwórz Plik Tekstowy',
- currentPassword: 'Aktualne hasło',
+ creator: 'Twórca',
+ currentPassword: 'Obecne Hasło',
+ customField_title: 'Własne Pole',
+ customFields_title: 'Własne Pola',
+ customFieldGroup_title: 'Grupa Pól Własnych',
+ customFieldGroups_title: 'Grupy Pól Własnych',
dangerZone_title: 'Strefa Niebezpieczeństwa',
date: 'Data',
- dueDate_title: 'Termin',
+ deactivateUser_title: 'Dezaktywuj Użytkownika',
+ defaultCardType_title: 'Domyślny Typ Karty',
+ defaultView_title: 'Domyślny Widok',
+ deletedUser_title: 'Usunięty Użytkownik',
+ deleteAllBoardsToBeAbleToUsuńThisProject:
+ 'Usuń wszystkie tablice, by móc usunąć ten projekt.',
deleteAttachment_title: 'Usuń Załącznik',
+ deleteBackgroundImage_title: 'Usuń Tło',
deleteBoard_title: 'Usuń Tablicę',
deleteCard_title: 'Usuń Kartę',
+ deleteCardForever_title: 'Usuń Kartę Permamentnie',
deleteComment_title: 'Usuń Komentarz',
+ deleteCustomField_title: 'Usuń Pole Własne',
+ deleteCustomFieldGroup_title: 'Usuń Grupę Pól Własnych',
deleteLabel_title: 'Usuń Oznaczenie',
deleteList_title: 'Usuń Listę',
+ deleteNotificationService_title: 'Usuń Serwis Powiadomień',
deleteProject_title: 'Usuń Projekt',
deleteTask_title: 'Usuń Zadanie',
+ deleteTaskList_title: 'Usuń Listę Zadań',
deleteUser_title: 'Usuń Użytkownika',
description: 'Opis',
detectAutomatically: 'Wykryj automatycznie',
+ display: 'Wyświetlanie',
dropFileToUpload: 'Upuść plik aby wgrać',
+ dueDate_title: 'Termin',
+ dynamicAndUnevenlySpacedLayout: 'Dynamiczny i nierówny układ.',
editor: 'Edytor',
+ editors: 'Edytorzy',
editAttachment_title: 'Edytuj Załącznik',
- editAvatar_title: 'Edytuj Avatar',
+ editAvatar_title: 'Edytuj Awatar',
+ editColor_title: 'Edytuj Kolor',
+ editCustomField_title: 'Edytuj Pole Własne',
+ editCustomFieldGroup_title: 'Edytuj Grupę Pól Własnych',
editDueDate_title: 'Edytuj Termin',
editEmail_title: 'Edytuj E-mail',
- editInformation_title: 'Edytuj Informację',
+ editInformation_title: 'Edytuj Informacje',
editLabel_title: 'Edytuj Oznaczenie',
editPassword_title: 'Edytuj Hasło',
- editPermissions_title: 'Edytuj Uprownienia',
- editStopwatch_title: 'Edytuj Odliczanie',
+ editPermissions_title: 'Edytuj Uprawnienia',
+ editRole_title: 'Edytuj Rolę',
+ editStopwatch_title: 'Edytuj Stoper',
+ editType_title: 'Edytuj Typ',
editUsername_title: 'Edytuj Nazwę Użytkownika',
email: 'E-mail',
+ emptyTrash_title: 'Opróżnij Kosz',
enterCardTitle: 'Podaj tytuł karty...',
enterDescription: 'Podaj opis...',
enterFilename: 'Podaj nazwę pliku',
enterListTitle: 'Podaj tytuł listy...',
enterTaskDescription: 'Podaj opis zadania...',
- filterByLabels_title: 'Filtrowanie Po Oznaczeniach',
- filterByMembers_title: 'Filtrowanie Po Członkach',
- fromComputer_title: 'Z komputera',
+ filterByLabels_title: 'Filtruj Po Oznaczeniach',
+ filterByMembers_title: 'Filtruj Po Członkach',
+ forTeamBasedProjects: 'Dla projektów zespołowych.',
+ forPersonalProjects: 'Dla projektów osobistych.',
+ fromComputer_title: 'Z Komputera',
fromTrello: 'Z Trello',
general: 'Ogólne',
+ gradients: 'Gradienty',
+ grid: 'Siatka',
+ hideFromProjectListAndFavorites: 'Ukryj z listy projektów i ulubionych',
hours: 'Godzin',
importBoard_title: 'Importuj Tablicę',
- invalidCurrentPassword: 'Błędne aktualne hasło',
+ invalidCurrentPassword: 'Błędne obecne hasło',
+ kanban: 'Kanban',
labels: 'Oznaczenia',
language: 'Język',
- leaveBoard_title: 'Opóść Tablicę',
+ leaveBoard_title: 'Opuść Tablicę',
leaveProject_title: 'Opuść Projekt',
+ limitCardTypesToDefaultOne: 'Ogranicz typy kart do domyślnego',
list: 'Lista',
+ lists: 'Listy',
listActions_title: 'Akcje Listy',
+ makeProjectShared_title: 'Udostępnij Projekt',
managers: 'Zarządcy',
members: 'Członkowie',
- memberActions_title: 'Akcje Członka',
+ memberActions_title: 'Akcje Członków',
minutes: 'Minut',
- moveCard_title: 'Przenieś Kartę',
+ moveCard_title: 'Przenoszenie Karty',
+ myOwn_title: 'Moje',
name: 'Nazwa',
newestFirst: 'Najpierw najnowsze',
newEmail: 'Nowy e-mail',
newPassword: 'Nowe hasło',
newUsername: 'Nowa nazwa użytkownika',
+ newVersionAvailable: 'Nowa wersja dostępna',
noConnectionToServer: 'Brak połączenia z serwerem',
noBoards: 'Brak tablic',
noLists: 'Brak list',
@@ -122,112 +224,208 @@ export default {
openBoard_title: 'Otwórz Tablicę',
optional_inline: 'opcjonalny',
organization: 'Organizacja',
+ others: 'Inne',
phone: 'Telefon',
+ plankaUsesAppriseToSendNotificationsToOver100PopularServices:
+ 'PLANKA używa <1><0>Apprise0>1> do wysyłania powiadomień do ponad 100 popularnych serwisów.',
preferences: 'Preferencje',
pressPasteShortcutToAddAttachmentFromClipboard:
'Podpowiedź: naciśnij Ctrl-V (Cmd-V na Macu) aby dodać załącznik ze schowka.',
+ private: 'Prywatne',
project: 'Projekt',
- projectNotFound_title: 'Projekt Nie Znaleziony',
- removeManager_title: 'Usuń Zarządce',
+ projectNotFound_title: 'Projektu Nie Znaleziono',
+ projectOwner: 'Właściciel projektu',
+ referenceDataAndKnowledgeStorage: 'Odnoś się do danych i przechowuj wiedzę',
+ removeManager_title: 'Usuń Zarządcę',
removeMember_title: 'Usuń Członka',
- searchCards: 'Przeszukaj karty...',
- searchLabels: 'Przeszukaj oznaczenia...',
- searchMembers: 'Przeszukaj członków...',
+ role: 'Rola',
+ searchCards: 'Szukaj kart...',
+ searchCustomFields: 'Szukaj pól własnych...',
+ searchCustomFieldGroups: 'Szukaj grup pól własnych...',
+ searchLabels: 'Szukaj oznaczeń...',
+ searchLists: 'Szukaj list...',
+ searchMembers: 'Szukaj członków...',
+ searchProjects: 'Szukaj projektów...',
searchUsers: 'Szukaj użytkowników...',
seconds: 'Sekund',
+ selectAssignee_title: 'Wybierz Osobę Przypisaną',
selectBoard: 'Wybierz tablicę',
selectList: 'Wybierz listę',
+ selectListToRestoreThisCard: 'Wybierz listę, na którą ma być przywrócona ta karta',
+ selectOrder_title: 'Wybierz Kolejność',
selectPermissions_title: 'Wybierz Uprawnienia',
selectProject: 'Wybierz projekt',
+ selectRole_title: 'Wybierz Rolę',
+ selectType_title: 'Wybierz Typ',
+ sequentialDisplayOfCards: 'Sekwencyjne wyświetlania kart.',
settings: 'Ustawienia',
- sortList_title: 'Sortuj Listę',
- stopwatch: 'Odliczanie',
- subscribeToMyOwnCardsByDefault: 'Zapisz mnie do moich kart automatycznie',
- taskActions_title: 'Akcje Zadania',
+ shared: 'Udostępniane',
+ sharedWithMe_title: 'Udostępniane Dla Mnie',
+ showOnFrontOfCard: 'Pokazuj na przodzie karty',
+ sortList_title: 'Sortowanie Listy',
+ stopwatch: 'Stoper',
+ story: 'Scenorys',
+ subscribeToCardWhenCommenting: 'Subskrybuj kartę przy komentowaniu',
+ subscribeToMyOwnCardsByDefault: 'Subskrybuj moje karty automatycznie',
+ taskActions_title: 'Akcje Zadań',
+ taskAssignmentAndProjectCompletion: 'Przypisanie zadań i stopień ukończenia projektów.',
+ taskList_title: 'Lista Zadań',
+ taskListActions_title: 'Akcje Listy Zadań',
+ team: 'Zespół',
thereIsNoPreviewAvailableForThisAttachment: 'Brak podglądu dostępnego dla tego załącznika.',
time: 'Czas',
title: 'Tytuł',
- userActions_title: 'Akcje użytkownika',
- userAddedThisCardToList: '<0>{{user}}0> dodał tę kartę w {{list}}',
+ trash: 'Kosz',
+ trashHasBeenSuccessfullyEmptied: 'Kosz opróżniony pomyślnie.',
+ turnOffRecentCardHighlighting: 'Wyłącz podświetlanie nowych kart',
+ typeNameToConfirm: 'Wpisz nazwę aby potwierdzić.',
+ typeTitleToConfirm: 'Wpisz tytuł aby potwierdzić.',
+ unsavedChanges: 'Niezapisane zmiany',
+ uploadedImages: 'Wgrane obrazy',
+ userActions_title: 'Akcje Użytkownika',
+ userAddedCardToList: '<0>{{user}}0> dodał <2>{{card}}2> do {{list}}',
+ userAddedThisCardToList: '<0>{{user}}0> dodał tę kartę do {{list}}',
+ userAddedUserToCard: '<0>{{actorUser}}0> dodał {{addedUser}} do <4>{{card}}4>',
+ userAddedUserToThisCard: '<0>{{actorUser}}0> dodał {{addedUser}} do tej karty',
+ userAddedYouToCard: '<0>{{user}}0> dodał cię do <2>{{card}}2>',
+ userCompletedTaskOnCard:
+ '<0>{{user}}0> oznaczył {{task}} jako ukończone na <4>{{card}}4>',
+ userCompletedTaskOnThisCard: '<0>{{user}}0> oznaczył {{task}} jako ukończone na tej karcie',
+ userJoinedCard: `<0>{{user}}0> dołączył do <2>{{card}}2>`,
+ userJoinedThisCard: `<0>{{user}}0> dołączył do tej karty`,
userLeftNewCommentToCard:
- '<0>{{user}}0> zamieścił nowy komentarz «{{comment}}» w <2>{{card}}2>',
+ '<0>{{user}}0> zamieścił komentarz «{{comment}}» na <2>{{card}}2>',
+ userLeftCard: '<0>{{user}}0> opuścił <2>{{card}}2>',
+ userLeftThisCard: '<0>{{user}}0> opuścił tę kartę',
+ userMarkedTaskIncompleteOnCard:
+ '<0>{{user}}0> oznaczył {{task}} jako nieukończone na <4>{{card}}4>',
+ userMarkedTaskIncompleteOnThisCard:
+ '<0>{{user}}0> oznaczył {{task}} jako nieukończone na tej karcie',
userMovedCardFromListToList:
'<0>{{user}}0> przeniósł <2>{{card}}2> z {{fromList}} do {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0> przeniósł tę kartę z {{fromList}} do {{toList}}',
+ userRemovedUserFromCard: '<0>{{actorUser}}0> usunął {{removedUser}} z <4>{{card}}4>',
+ userRemovedUserFromThisCard: '<0>{{actorUser}}0> usunął {{removedUser}} z tej karty',
username: 'Nazwa Użytkownika',
users: 'Użytkownicy',
- viewer: 'Odwiedzający',
+ viewer: 'Wyświetlający',
+ viewers: 'Wyświetlający',
+ visualTaskManagementWithLists: 'Wizualne zarządzanie zadaniami z listami.',
+ withoutBaseGroup: 'Bez grupy bazowej',
writeComment: 'Napisz komentarz...',
},
action: {
+ activateUser: 'Aktywuj użytkownika',
+ activateUser_title: 'Aktywuj Użytkownika',
addAnotherCard: 'Dodaj kolejną kartę',
addAnotherList: 'Dodaj kolejną listę',
addAnotherTask: 'Dodaj kolejne zadanie',
addCard: 'Dodaj kartę',
addCard_title: 'Dodaj Kartę',
addComment: 'Dodaj komentarz',
+ addCustomField: 'Dodaj własne pole',
+ addCustomFieldGroup: 'Dodaj grupę własnych pól',
addList: 'Dodaj listę',
addMember: 'Dodaj członka',
addMoreDetailedDescription: 'Dodaj szczegółowy opis',
addTask: 'Dodaj zadanie',
+ addTaskList: 'Dodaj listę zadań',
addToCard: 'Dodaj do karty',
addUser: 'Dodaj użytkownika',
+ archive: 'Archiwizuj',
+ archiveCard: 'Archiwizuj kartę',
+ archiveCard_title: 'Archiwizuj Kartę',
+ archiveCards: 'Archiwizuj karty',
+ archiveCards_title: 'Archiwizuj Karty',
+ assignAsOwner: 'Przypisz jako właściciela',
+ cancel: 'Anuluj',
createBoard: 'Utwórz tablicę',
+ createCustomFieldGroup: 'Utwórz grupę pól własnych',
createFile: 'Utwórz plik',
createLabel: 'Utwórz oznaczenie',
createNewLabel: 'Utwórz nowe oznaczenie',
createProject: 'Utwórz projekt',
+ deactivateUser: 'Dezaktywuj użytkownika',
+ deactivateUser_title: 'Dezaktywuj Użytkownika',
delete: 'Usuń',
- deleteAttachment: 'Usuń załącznik',
- deleteAvatar: 'Usuń avatar',
+ deleteAttachment: 'Usuń attachment',
+ deleteAvatar: 'Usuń awatar',
+ deleteBackgroundImage: 'Usuń tło',
deleteBoard: 'Usuń tablicę',
+ deleteBoard_title: 'Usuń Tablicę',
deleteCard: 'Usuń kartę',
deleteCard_title: 'Usuń Kartę',
+ deleteCardForever: 'Usuń kartę permanetnie',
deleteComment: 'Usuń komentarz',
+ deleteCustomField: 'Usuń pole własne',
+ deleteCustomFieldGroup: 'Usuń grupę pól własnych',
+ deleteForever_title: 'Usuń Permanentnie',
+ deleteGroup: 'Usuń groupę',
deleteLabel: 'Usuń oznaczenie',
deleteList: 'Usuń listę',
deleteList_title: 'Usuń Listę',
+ deleteNotificationService: 'Usuń serwis powiadomień',
deleteProject: 'Usuń projekt',
deleteProject_title: 'Usuń Projekt',
deleteTask: 'Usuń zadanie',
deleteTask_title: 'Usuń Zadanie',
+ deleteTaskList: 'Usuń listę zadań',
deleteUser: 'Usuń użytkownika',
- duplicate: 'Zduplikuj',
- duplicateCard_title: 'Zduplikuj Kartę',
+ deleteUser_title: 'Usuń Użytkownika',
+ dismissAll: 'Odrzuć wszystkie',
+ duplicate: 'Duplikuj',
+ duplicateCard_title: 'Duplikuj Kartę',
edit: 'Edytuj',
+ editColor_title: 'Edytuj Kolor',
editDueDate_title: 'Edytuj Termin',
editDescription_title: 'Edytuj Opis',
editEmail_title: 'Edytuj E-mail',
- editInformation_title: 'Edytuj Dane',
+ editGroup: 'Edytuj groupę',
+ editInformation_title: 'Edytuj Informacje',
editPassword_title: 'Edytuj Hasło',
editPermissions: 'Edytuj uprawnienia',
- editStopwatch_title: 'Edytuj Odliczanie',
+ editRole_title: 'Edytuj Rolę',
+ editStopwatch_title: 'Edytuj Stoper',
editTitle_title: 'Edytuj Tytuł',
+ editType_title: 'Edytuj Typ',
editUsername_title: 'Edytuj Nazwę Użytkownika',
+ emptyTrash: 'Opróżnij kosz',
+ emptyTrash_title: 'Opróżnij Kosz',
import: 'Importuj',
+ join: 'Dołącz',
+ leave: 'Opuść',
leaveBoard: 'Opuść tablicę',
leaveProject: 'Opuść projekt',
logOut_title: 'Wyloguj',
makeCover_title: 'Stwórz Okładkę',
+ makeProjectShared: 'Udostępnij projekt',
+ makeProjectShared_title: 'Udostępnij Projekt',
move: 'Przenieś',
moveCard_title: 'Przenieś Kartę',
remove: 'Usuń',
+ removeAssignee: 'Usuń osobę przypisaną',
+ removeColor: 'Usuń kolor',
removeCover_title: 'Usuń Okładkę',
removeFromBoard: 'Usuń z tablicy',
removeFromProject: 'Usuń z projektu',
removeManager: 'Usuń zarządcę',
removeMember: 'Usuń członka',
+ restoreToList: 'Przywróć na {{list}}',
+ returnToBoard: 'Przywróć do tablicy',
save: 'Zapisz',
- showAllAttachments: 'Pokaż wszystkie załączniki ({{hidden}} ukryte)',
+ showActive: 'Pokaż aktywne',
+ showAllAttachments: 'Pokaż wszystkie załączniki ({{hidden}} są ukryte)',
+ showCardsWithThisUser: 'Pokaż karty z tym użytkownikiem',
+ showDeactivated: 'Pokaż dezaktywowane',
showFewerAttachments: 'Pokaż mniej załączników',
sortList_title: 'Sortuj Listę',
start: 'Start',
stop: 'Stop',
- subscribe: 'Zapisz się',
- unsubscribe: 'Wypisz się',
- uploadNewAvatar: 'Wgraj nowy avatar',
+ subscribe: 'Subskrybuj',
+ unsubscribe: 'Odsubskrybuj',
+ uploadNewAvatar: 'Wgraj nowy awatar',
uploadNewImage: 'Wgraj nowy obraz',
},
},
diff --git a/client/src/locales/pl-PL/login.js b/client/src/locales/pl-PL/login.js
index 4b8eed10..5cdf23f7 100644
--- a/client/src/locales/pl-PL/login.js
+++ b/client/src/locales/pl-PL/login.js
@@ -1,20 +1,27 @@
export default {
translation: {
common: {
+ activeUsersLimitReached: 'Osiągnięto limit aktywnych użytkowników',
emailAlreadyInUse: 'E-mail jest już używany',
emailOrUsername: 'E-mail lub nazwa użytkownika',
+ invalidCredentials: 'Błędne dane logowania',
invalidEmailOrUsername: 'Błędny e-mail lub nazwa użytkownika',
- invalidPassword: 'Błędne Hasło',
+ invalidPassword: 'Błędne hasło',
+ logIn_title: 'Logowanie',
noInternetConnection: 'Brak połączenia z internetem',
- pageNotFound_title: 'Nie znaleziono strony',
+ or: 'Lub',
+ pageNotFound_title: 'Strona nie znaleziona',
password: 'Hasło',
+ poweredByPlanka: 'Powered by PLANKA',
serverConnectionFailed: 'Błąd połączenia z serwerem',
unknownError: 'Nieznany błąd, spróbuj ponownie później',
- usernameAlreadyInUse: 'Nazwa użytkownika jest już używana',
+ usernameAlreadyInUse: 'Nazwa użytkownika nie jest dostępna',
+ useSingleSignOn: 'Użyj logowania SSO',
},
action: {
logIn: 'Zaloguj',
+ logInWithSso: 'Zaloguj z SSO',
},
},
};
diff --git a/client/src/locales/pl-PL/markdown-editor.json b/client/src/locales/pl-PL/markdown-editor.json
new file mode 100644
index 00000000..e71a1de1
--- /dev/null
+++ b/client/src/locales/pl-PL/markdown-editor.json
@@ -0,0 +1,165 @@
+{
+ "action-previews": {
+ "text": "To jest tekst bez tytułu.\nZarówno tytuł jak i tekst\nmogą być wyróżnione za\npomocą znaczników.",
+ "text-with-head": "To jest tekst z tytułem.\nZarówno tytuł jak i tekst\nmogą być wyróżnione za\npomocą znaczników.",
+ "heading": "Tytuł"
+ },
+ "bundle": {
+ "error-title": "Błąd w edytorze markdown",
+ "settings_wysiwyg": "Edytor graficzny (WYSIWYG)",
+ "settings_markup": "Znaczniki markdown",
+ "markup_placeholder": "Wpisz znacznik markdown..."
+ },
+ "codeblock": {
+ "remove": "Usuń",
+ "empty_option": "Nie znaleziono dopasowań"
+ },
+ "common": {
+ "delete": "Usuń",
+ "edit": "Edytuj",
+ "toolbar_action_disabled": "Niekompatybilny znacznik markup"
+ },
+ "forms": {
+ "common_action_cancel": "Anuluj",
+ "common_action_submit": "Submit",
+ "common_action_upload": "Wybierz",
+ "common_tab_attach": "Dodaj z urządzenia",
+ "common_tab_link": "Dodaj z linku",
+ "common_link": "Link",
+ "common_sizes": "Rozmiar, px",
+ "image_name": "Tytuł",
+ "image_link_href": "Link do obrazu",
+ "image_link_href_help": "Adres, do którego prowadzi link obrazu.",
+ "image_alt": "Tekst alternarywny",
+ "image_alt_help": "Tekst alternatywny jest wyświetlany, kiedy obraz nie może być załadowany.",
+ "image_upload_help": "Obraz JPEG, GIF lub PNG nie może być większy niż 1 MB.",
+ "image_upload_failed": "Nie udało się dodać obrazu.",
+ "image_size_width": "Szerokość",
+ "image_size_height": "Wysokość",
+ "link_url_help": "Adres, do którego prowadzi link.",
+ "link_text": "Tekst linku",
+ "link_text_help": "Tekst wyświetlany jako link.",
+ "link_open_help": "Otwórz link w nowej karcie."
+ },
+ "md-hints": {
+ "header_title": "Nagłówek",
+ "header_hint": "# Twój tekst",
+ "italic_title": "Kursywa",
+ "italic_hint": "_Twój tekst_",
+ "bold_title": "Pogrubienie",
+ "bold_hint": "**Twój tekst**",
+ "strikethrough_title": "Przekreślenie",
+ "strikethrough_hint": "~~Twój tekst~~",
+ "blockquote_title": "Cytat blokowy",
+ "blockquote_hint": "> Twój tekst",
+ "code_title": "Kod",
+ "code_hint": "```Twój tekst```",
+ "link_title": "Link",
+ "link_hint": "[Twój tekst](url)",
+ "image_title": "Obraz",
+ "image_hint": "",
+ "list_title": "Element listy",
+ "list_hint": "- Twój tekst",
+ "numbered-list_title": "Lista numerowana",
+ "numbered-list_hint": "1. Twój tekst",
+ "documentation": "Dokumentacja",
+ "documentation_link": " https://diplodoc.com/docs/en/syntax/"
+ },
+ "menubar": {
+ "bold": "Pogrubienie",
+ "code": "Kod",
+ "code_inline": "Kod w linii",
+ "codeblock": "Blok kodu",
+ "colorify": "Kolor tekstu",
+ "colorify__color_blue": "Niebieski",
+ "colorify__color_default": "Domyślny",
+ "colorify__color_gray": "Szary",
+ "colorify__color_green": "Zielony",
+ "colorify__color_orange": "Pomarańczowy",
+ "colorify__color_red": "Czerwony",
+ "colorify__color_violet": "Fioletowy",
+ "colorify__color_yellow": "Żółty",
+ "colorify__group_text": "Tekst",
+ "cut": "Wytnij",
+ "emoji": "Emoji",
+ "emoji__hint": "Emotikony mogą być dodane w edytorze WYSIWYG lub ręcznie za pomocą znaczników",
+ "heading": "Nagłówek",
+ "heading1": "Nagłówek 1",
+ "heading2": "Nagłówek 2",
+ "heading3": "Nagłówek 3",
+ "heading4": "Nagłówek 4",
+ "heading5": "Nagłówek 5",
+ "heading6": "Nagłówek 6",
+ "hrule": "Separator",
+ "image": "Obraz",
+ "italic": "Kursywa",
+ "link": "Link",
+ "list": "Lista",
+ "list__action_lift": "Podnieś element",
+ "list__action_sink": "Opuść element",
+ "list_action_disabled": "Przeczy logice listy",
+ "mark": "Zaznaczenie",
+ "mono": "Stała szerokość",
+ "more_action": "Więcej opcji",
+ "note": "Notka",
+ "olist": "Lista numerowana",
+ "quote": "Cytat",
+ "redo": "Ponów",
+ "strike": "Przekreślenie",
+ "table": "Tabela",
+ "text": "Tekst",
+ "ulist": "Lista wypunktowana",
+ "underline": "Podkreślenie",
+ "undo": "Cofnij"
+ },
+ "placeholder": {
+ "doc_empty": "Wpisz / by użyć komend...",
+ "checkbox": "Wpisz opis zadania...",
+ "deflist_term": "Termin",
+ "deflist_desc": "Opis definicji",
+ "heading": "Nagłówek",
+ "cut_title": "Tytuł",
+ "cut_content": "Zawartość wyświetlona po naciśnięciu",
+ "note_title": "Tytuł",
+ "note_content": "Zawartość notki",
+ "table_cell": "Zawartość komórki",
+ "select_filter": "Szukaj języków..."
+ },
+ "search": {
+ "label_case-sensitive": "Zwracając uwagę na wielkość liter",
+ "label_whole-word": "Całe słowa",
+ "title": "Szukaj w kodzie"
+ },
+ "suggest": {
+ "empty-msg": "Nie znaleziono"
+ },
+ "widgets": {
+ "image": "Dodaj obraz",
+ "link": "Dodaj link"
+ },
+ "yfm-note": {
+ "info": "Notka",
+ "tip": "Porada",
+ "warning": "Ostrzeżenie",
+ "alert": "Alert",
+ "remove": "Usuń"
+ },
+ "yfm-table": {
+ "column.add.before": "Dodaj kolumnę przed",
+ "column.add.after": "Dodaj kolumnę po",
+ "column.remove": "Usuń kolumnę",
+ "row.add.before": "Dodaj wiersz przed",
+ "row.add.after": "Dodaj wiersz po",
+ "row.remove": "Usuń wiersz",
+ "table.remove": "Usuń tabelę",
+ "table.menu.cell.align.left": "Wyrównaj zawartość komórki do lewej",
+ "table.menu.cell.align.right": "Wyrównaj zawartość komórki do prawej",
+ "table.menu.cell.align.center": "Wyrównaj zawartość komórki do środka",
+ "table.menu.row.add": "Dodaj wiersz po",
+ "table.menu.row.remove": "Usuń wiersz",
+ "table.menu.column.add": "Dodaj kolumnę po",
+ "table.menu.column.remove": "Usuń kolumnę",
+ "table.menu.convert.yfm": "Konwertuj na tabelę YFM",
+ "table.menu.table.remove": "Usuń tabelę"
+ }
+}
From 42817c51997528b1803d65423a91895072ee88e8 Mon Sep 17 00:00:00 2001
From: Roman Zavarnitsyn
Date: Tue, 27 May 2025 14:19:44 +0200
Subject: [PATCH 022/108] feat: Add CSV attachment viewer (#1154)
---
client/package-lock.json | 21 ++-
client/package.json | 2 +
.../attachments/Attachments/CsvViewer.jsx | 135 ++++++++++++++++++
.../Attachments/CsvViewer.module.scss | 14 ++
.../attachments/Attachments/Item.jsx | 10 ++
5 files changed, 179 insertions(+), 3 deletions(-)
create mode 100644 client/src/components/attachments/Attachments/CsvViewer.jsx
create mode 100644 client/src/components/attachments/Attachments/CsvViewer.module.scss
diff --git a/client/package-lock.json b/client/package-lock.json
index 2c27be92..6f912566 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -13,6 +13,7 @@
"@gravity-ui/markdown-editor": "^15.11.0",
"@gravity-ui/uikit": "^7.11.0",
"@juggle/resize-observer": "^3.4.0",
+ "@types/papaparse": "^5.3.16",
"@vitejs/plugin-react": "^4.4.1",
"browserslist-to-esbuild": "^2.1.1",
"classnames": "^2.5.1",
@@ -54,6 +55,7 @@
"lowlight": "^3.3.0",
"markdown-it": "^13.0.2",
"nanoid": "^5.1.5",
+ "papaparse": "^5.5.3",
"patch-package": "^8.0.0",
"photoswipe": "^5.4.4",
"prop-types": "^15.8.1",
@@ -4745,7 +4747,6 @@
"version": "22.15.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz",
"integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==",
- "devOptional": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -4756,6 +4757,15 @@
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
"dev": true
},
+ "node_modules/@types/papaparse": {
+ "version": "5.3.16",
+ "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.16.tgz",
+ "integrity": "sha512-T3VuKMC2H0lgsjI9buTB3uuKj3EMD2eap1MOuEQuBQ44EnDx/IkGhU6EwiTf9zG3za4SKlmwKAImdDKdNnCsXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
@@ -11227,6 +11237,12 @@
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
+ "node_modules/papaparse": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
+ "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==",
+ "license": "MIT"
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -14677,8 +14693,7 @@
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "devOptional": true
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.1",
diff --git a/client/package.json b/client/package.json
index ac1528d2..cf8921fd 100755
--- a/client/package.json
+++ b/client/package.json
@@ -84,6 +84,7 @@
"@gravity-ui/markdown-editor": "^15.11.0",
"@gravity-ui/uikit": "^7.11.0",
"@juggle/resize-observer": "^3.4.0",
+ "@types/papaparse": "^5.3.16",
"@vitejs/plugin-react": "^4.4.1",
"browserslist-to-esbuild": "^2.1.1",
"classnames": "^2.5.1",
@@ -125,6 +126,7 @@
"lowlight": "^3.3.0",
"markdown-it": "^13.0.2",
"nanoid": "^5.1.5",
+ "papaparse": "^5.5.3",
"patch-package": "^8.0.0",
"photoswipe": "^5.4.4",
"prop-types": "^15.8.1",
diff --git a/client/src/components/attachments/Attachments/CsvViewer.jsx b/client/src/components/attachments/Attachments/CsvViewer.jsx
new file mode 100644
index 00000000..dab29ae2
--- /dev/null
+++ b/client/src/components/attachments/Attachments/CsvViewer.jsx
@@ -0,0 +1,135 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+import React, { useEffect, useState, useCallback, useMemo } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { Pagination, Table } from 'semantic-ui-react';
+import Papa from 'papaparse';
+import Frame from 'react-frame-component';
+
+import styles from './CsvViewer.module.scss';
+
+const ROWS_PER_PAGE = 50;
+
+/* eslint-disable react/no-array-index-key */
+const CsvViewer = React.memo(({ src, className }) => {
+ const [csvData, setCsvData] = useState(null);
+ const [currentPage, setCurrentPage] = useState(1);
+
+ const frameStyles = useMemo(
+ () => [
+ ...Array.from(document.styleSheets).flatMap((styleSheet) =>
+ Array.from(styleSheet.cssRules).map((cssRule) => cssRule.cssText),
+ ),
+ 'body{background:rgb(248,248,248);min-width:fit-content;overflow-x:visible}',
+ '.frame-content{padding:40px}',
+ '.frame-content>pre{margin:0}',
+ '.hljs{padding:0}',
+ '::-webkit-scrollbar{height:10px}',
+ '.ui.pagination.menu{display:flex;justify-content:center;margin-top:20px;padding:10px 0}',
+ ],
+ [],
+ );
+
+ const handlePageChange = useCallback((e, { activePage }) => {
+ setCurrentPage(activePage);
+ }, []);
+
+ useEffect(() => {
+ async function fetchFile() {
+ try {
+ const response = await fetch(src, {
+ credentials: 'include',
+ });
+
+ const text = await response.text();
+
+ Papa.parse(text, {
+ skipEmptyLines: true,
+ complete: (results) => {
+ const rows = results.data;
+ setCsvData({
+ rows,
+ totalRows: rows.length,
+ });
+ },
+ });
+ } catch (err) {
+ setCsvData(null);
+ }
+ }
+
+ fetchFile();
+ }, [src]);
+
+ if (!csvData) {
+ return null;
+ }
+
+ const startIdx = (currentPage - 1) * ROWS_PER_PAGE;
+ const endIdx = startIdx + ROWS_PER_PAGE;
+ const currentRows = csvData.rows.slice(startIdx, endIdx);
+ const totalPages = Math.ceil(csvData.totalRows / ROWS_PER_PAGE);
+
+ const content = (
+
+
+
+
+
+ {csvData.rows[0].map((header, index) => (
+ {header}
+ ))}
+
+
+
+ {currentRows.slice(1).map((row, rowIndex) => (
+
+ {row.map((cell, cellIndex) => (
+ {cell}
+ ))}
+
+ ))}
+
+
+
+ {totalPages > 1 && (
+
+ )}
+
+ );
+
+ return (
+ {frameStyles.join('')}}
+ className={classNames(styles.wrapper, className)}
+ >
+ {content}
+
+ );
+});
+/* eslint-enable react/no-array-index-key */
+
+CsvViewer.propTypes = {
+ src: PropTypes.string.isRequired,
+ className: PropTypes.string,
+};
+
+CsvViewer.defaultProps = {
+ className: undefined,
+};
+
+export default CsvViewer;
diff --git a/client/src/components/attachments/Attachments/CsvViewer.module.scss b/client/src/components/attachments/Attachments/CsvViewer.module.scss
new file mode 100644
index 00000000..3827f58b
--- /dev/null
+++ b/client/src/components/attachments/Attachments/CsvViewer.module.scss
@@ -0,0 +1,14 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+:global(#app) {
+ .wrapper {
+ background: #fff;
+ border: 0;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+}
diff --git a/client/src/components/attachments/Attachments/Item.jsx b/client/src/components/attachments/Attachments/Item.jsx
index 7fedb769..1efb4035 100644
--- a/client/src/components/attachments/Attachments/Item.jsx
+++ b/client/src/components/attachments/Attachments/Item.jsx
@@ -16,6 +16,7 @@ import Encodings from '../../../constants/Encodings';
import { AttachmentTypes } from '../../../constants/Enums';
import ItemContent from './ItemContent';
import ContentViewer from './ContentViewer';
+import CsvViewer from './CsvViewer';
import styles from './Item.module.scss';
@@ -68,6 +69,15 @@ const Item = React.memo(({ id, isVisible }) => {
);
+ break;
+ case 'text/csv':
+ content = (
+
+ );
+
break;
default:
if (attachment.data.encoding === Encodings.UTF8) {
From 39df82c1ceeda9c0728259c8e8e641616e7c79b7 Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Tue, 27 May 2025 23:35:00 +0200
Subject: [PATCH 023/108] license: Provide additional details
---
LICENSE.md | 28 ++++--
LICENSES/PLANKA Commercial License DE.md | 35 ++++++++
LICENSES/PLANKA Commercial License EN.md | 35 ++++++++
LICENSES/PLANKA Community License DE.md | 32 ++++---
LICENSES/PLANKA Community License EN.md | 28 ++++--
LICENSES/PLANKA Enterprise License DE.md | 33 -------
LICENSES/PLANKA Enterprise License EN.md | 33 -------
LICENSES/PLANKA License Guide DE.md | 106 +++++++++++++++--------
LICENSES/PLANKA License Guide EN.md | 88 ++++++++++++-------
README.md | 2 +-
10 files changed, 260 insertions(+), 160 deletions(-)
create mode 100644 LICENSES/PLANKA Commercial License DE.md
create mode 100644 LICENSES/PLANKA Commercial License EN.md
delete mode 100644 LICENSES/PLANKA Enterprise License DE.md
delete mode 100644 LICENSES/PLANKA Enterprise License EN.md
diff --git a/LICENSE.md b/LICENSE.md
index 6e4b3298..93941c8b 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,15 +1,17 @@
**PLANKA Community License**
-Version 1.0 - Last updated: May 2, 2025
+Version 1.1 - Last updated: May 20, 2025
Related files in English:
+
- PLANKA Community License EN.md (this file)
-- [PLANKA Enterprise License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20EN.md)
+- [PLANKA Commercial License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20EN.md)
- [PLANKA License Guide EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20EN.md)
Related files in German:
+
- [PLANKA Community License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20DE.md)
-- [PLANKA Enterprise License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20DE.md)
+- [PLANKA Commercial License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20DE.md)
- [PLANKA License Guide DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20DE.md)
---
@@ -19,18 +21,18 @@ Related files in German:
Files accessible to and marked for community use are licensed as follows:
- Content of branches other than the main branch (usually "master" or "main") is not licensed.
-- Source code files or other files that contain ".pe." (for "PLANKA Enterprise") in their file names or folder names or are otherwise marked as "PLANKA Enterprise" in their file headers or folders are NOT licensed under the "Fair Use License". These files are "PLANKA Enterprise" files and are licensed under the "PLANKA Enterprise License".
-- To use any "PLANKA Enterprise" files or sources, you must own a valid "PLANKA Enterprise License". You can read more about our commercial license in the [PLANKA Enterprise License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20EN.md).
+- Source code files or other files that contain ".pe." (for "PLANKA Pro/Enterprise") in their file names or folder names or are otherwise marked as "PLANKA Pro/Enterprise" in their file headers or folders are NOT licensed under the "Fair Use License". These files are "PLANKA Pro/Enterprise" files and are licensed under the "PLANKA Pro/Enterprise License".
+- To use any "PLANKA Pro/Enterprise" files or sources, you must own a valid "PLANKA Pro/Enterprise License". You can read more about our commercial license in the [PLANKA Commercial License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20EN.md).
- All third-party components incorporated into our software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above-mentioned files or restrictions is available under the "Fair Use License" as defined below.
## Fair Use License
-Version 1.0
+Version 1.1
### Acceptance
-By using the software, you agree to all of the terms and conditions below.
+By using the software, you agree to all of the terms and conditions above and below.
### Copyright License
@@ -40,9 +42,19 @@ The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensa
"You may use the PLANKA name or logo only to describe that your service incorporates the software. Any other trademark use (e.g., in product names, domains, or marketing material) requires our prior written consent."
+### Permitted Use
+
+You may use or modify PLANKA (a) for personal, hobby, or educational purposes, (b) internally within your own organization, (c) for private hosting for a typical number of friends, family, or personal projects, (d) to provide free access to non-profit organizations (as recognized by applicable tax authorities), or if you are a recognized non-profit organization yourself involving external users into your mission, and (e) public educational institutions for academic/research purposes only.
+
+### Restricted Use
+
+Sharing accounts/credentials with third parties for business purposes or operating PLANKA as a hosted service for any commercial gain whatsoever is prohibited. Commercial gain includes any form of payment, advertising revenue, data monetization, or indirect commercial benefit or business advantage.
+
+For all other PLANKA-based hosting services or shared use of PLANKA accounts across organizations, you need to buy a commercial PLANKA license.
+
### Limitations
-You may use or modify the software (a) for personal, hobby, or educational purposes or (b) inside your own legal entity (including wholly owned subsidiaries) for workflows that are not offered as a paid product or as a cross-company service to a third party. Cross-company services or use between your legal entity and other unrelated third-party legal entities are explicitly prohibited. You may not alter, remove, or obscure any licensing, copyright, or other notices from the software provided by the licensor. Any use of the licensor's trademarks is subject to applicable law.
+You may not alter, remove, or obscure any licensing, copyright, or other notices from the software provided by the licensor. Any use of the licensor's trademarks is subject to applicable law.
### Patents
diff --git a/LICENSES/PLANKA Commercial License DE.md b/LICENSES/PLANKA Commercial License DE.md
new file mode 100644
index 00000000..986b1f02
--- /dev/null
+++ b/LICENSES/PLANKA Commercial License DE.md
@@ -0,0 +1,35 @@
+**PLANKA Commercial License**
+
+Version 1.1 - Zuletzt aktualisiert: 20. Mai 2025
+
+Zugehörige Dateien in Englisch:
+
+- [PLANKA Community License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20EN.md)
+- [PLANKA Commercial License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20EN.md)
+- [PLANKA License Guide EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20EN.md)
+
+Zugehörige Dateien in Deutsch:
+
+- [PLANKA Community License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20DE.md)
+- PLANKA Commercial License DE.md (diese Datei)
+- [PLANKA License Guide DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20DE.md)
+
+---
+
+# PLANKA Pro/Enterprise-Lizenz
+
+Copyright (c) 2025 bis heute von PLANKA Software GmbH.
+
+Unsere Software und zugehörige Dokumentationsdateien (die "Software") dürfen nur dann produktiv genutzt werden, wenn Sie (und jede juristische Person, die Sie vertreten) eine gültige "PLANKA Pro/Enterprise-Lizenz" besitzen, die Ihrer Nutzung entspricht. Vorbehaltlich des vorstehenden Satzes steht es Ihnen frei, unsere Software zu modifizieren und Patches dafür zu veröffentlichen. Sie stimmen zu, dass die PLANKA Software GmbH und/oder ihre Lizenzgeber (falls zutreffend) alle Rechte, Titel und Ansprüche an und auf alle solche Modifikationen und/oder Patches behalten, und alle solche Modifikationen und/oder Patches dürfen nur mit einer gültigen "PLANKA Pro/Enterprise-Lizenz" für die entsprechende Nutzung verwendet, kopiert, modifiziert, angezeigt, verteilt oder anderweitig genutzt werden. Ungeachtet des Vorstehenden dürfen Sie die Software für Entwicklungs- und Testzwecke ohne Abonnement kopieren und modifizieren. Sie stimmen zu, dass PLANKA Software GmbH und/oder ihre Lizenzgeber (falls zutreffend) alle Rechte, Titel und Ansprüche an und auf alle solche Modifikationen behalten. Es werden Ihnen keine anderen Rechte gewährt als die, die hier ausdrücklich genannt sind. Vorbehaltlich des Vorstehenden ist es verboten, die Software zu kopieren, zusammenzuführen, zu veröffentlichen, zu verteilen, zu unterlizenzieren und/oder zu verkaufen.
+
+#### Komponenten von Drittanbietern
+
+Für alle Komponenten von Drittanbietern, die in unsere Software integriert sind, werden diese Komponenten unter der ursprünglichen Lizenz lizenziert, die vom Eigentümer der jeweiligen Komponente bereitgestellt wird.
+
+## PLANKA Pro/Enterprise Repositories
+
+Nach dem Kauf unserer "PLANKA Pro/Enterprise-Lizenz" erhalten Sie Zugang zu unseren "PLANKA Pro/Enterprise"-Repositories. Hier finden Sie unsere neuesten stabilen Builds, die umfangreiche, eingehende Tests bestanden haben und als kampferprobt gelten. Unter keinen Umständen dürfen Sie Dateien, Quellcode oder Teile aus unseren "PLANKA Pro/Enterprise"-Repositories ohne vorherige Genehmigung der PLANKA Software GmbH an Personen weitergeben, die nicht zugangsberechtigt sind.
+
+## Eingeschränkte Garantie
+
+UNSERE SOFTWARE WIRD "WIE SIE IST" BEREITGESTELLT, OHNE JEGLICHE GARANTIE, AUSDRÜCKLICH ODER IMPLIZIERT, EINSCHLIEßLICH, ABER NICHT BESCHRÄNKT AUF DIE GARANTIEN DER MARKTGÄNGIGKEIT, EIGNUNG FÜR EINEN BESTIMMTEN ZWECK UND NICHTVERLETZUNG VON RECHTEN DRITTER. IN KEINEM FALL HAFTEN DIE AUTOREN ODER URHEBERRECHTSINHABER FÜR ANSPRÜCHE, SCHÄDEN ODER ANDERE HAFTUNG, OB AUS VERTRAG, UNERLAUBTER HANDLUNG ODER ANDERWEITIG, DIE SICH AUS, AUS ODER IN VERBINDUNG MIT DER SOFTWARE ODER DER NUTZUNG ODER ANDEREN GESCHÄFTEN MIT DER SOFTWARE ERGEBEN.
diff --git a/LICENSES/PLANKA Commercial License EN.md b/LICENSES/PLANKA Commercial License EN.md
new file mode 100644
index 00000000..6703ed69
--- /dev/null
+++ b/LICENSES/PLANKA Commercial License EN.md
@@ -0,0 +1,35 @@
+**PLANKA Commercial License**
+
+Version 1.1 - Last updated: May 20, 2025
+
+Related files in English:
+
+- [PLANKA Community License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20EN.md)
+- PLANKA Commercial License EN.md (this file)
+- [PLANKA License Guide EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20EN.md)
+
+Related files in German:
+
+- [PLANKA Community License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20DE.md)
+- [PLANKA Commercial License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20DE.md)
+- [PLANKA License Guide DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20DE.md)
+
+---
+
+# PLANKA Pro/Enterprise License
+
+Copyright (c) 2025 to present by PLANKA Software GmbH.
+
+Our software and associated documentation files (the "Software") may only be used in production if you (and any entity that you represent) hold a valid "PLANKA Pro/Enterprise License" corresponding to your usage. Subject to the foregoing sentence, you are free to modify our Software and publish patches for it. You agree that PLANKA Software GmbH and/or its licensors (as applicable) retain all right, title, and interest in and to all such modifications and/or patches, and all such modifications and/or patches may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid "PLANKA Pro/Enterprise License" for the corresponding usage. Notwithstanding the foregoing, you may copy and modify the Software for development and testing purposes without requiring a subscription. You agree that PLANKA Software GmbH and/or its licensors (as applicable) retain all right, title, and interest in and to all such modifications. You are not granted any other rights beyond what is expressly stated herein. Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, and/or sell the Software.
+
+#### Third-Party Components
+
+For all third-party components incorporated into our Software, those components are licensed under the original license provided by the owner of the applicable component.
+
+## PLANKA Pro/Enterprise Repositories
+
+After purchasing our "PLANKA Pro/Enterprise License", you get access to our "PLANKA Pro/Enterprise" repositories. Here you find our latest stable builds, which have passed extensive in-depth tests and are considered battle-proof. Under no circumstances are you allowed to pass files, source code, or any part of it from our "PLANKA Pro/Enterprise" repositories to anyone not eligible for access without prior permission from PLANKA Software GmbH.
+
+## Limited Warranty
+
+OUR SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/LICENSES/PLANKA Community License DE.md b/LICENSES/PLANKA Community License DE.md
index e6ad1e31..714b8159 100644
--- a/LICENSES/PLANKA Community License DE.md
+++ b/LICENSES/PLANKA Community License DE.md
@@ -1,36 +1,38 @@
**PLANKA Community License**
-Version 1.0 - Zuletzt aktualisiert: 2. Mai 2025
+Version 1.1 - Zuletzt aktualisiert: 20. Mai 2025
Zugehörige Dateien in Englisch:
+
- [PLANKA Community License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20EN.md)
-- [PLANKA Enterprise License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20EN.md)
+- [PLANKA Commercial License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20EN.md)
- [PLANKA License Guide EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20EN.md)
Zugehörige Dateien in Deutsch:
+
- PLANKA Community License DE.md (diese Datei)
-- [PLANKA Enterprise License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20DE.md)
+- [PLANKA Commercial License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20DE.md)
- [PLANKA License Guide DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20DE.md)
---
-# PLANKA-Gemeinnutzungslizenz
+# PLANKA Community-Lizenz
Für die Gemeinschaft zugängliche und gekennzeichnete Dateien sind wie folgt lizenziert:
-- Inhalte von Sourcecode-Branches außer dem Hauptbranch (üblicherweise "master" oder "main") sind nicht zur Gemeinnutzung bestimmt und lizenziert.
-- Quellcode-Dateien oder andere Dateien, die ".pe." (für "PLANKA Enterprise") in ihren Datei- oder Ordnernamen enthalten oder anderweitig durch "PLANKA Enterprise" in ihren Dateiköpfen oder Ordnern als Unternehmenslizenz gekennzeichnet sind, sind NICHT unter der "Fair Use Lizenz" lizenziert. Diese Dateien sind "PLANKA Enterprise"-Dateien und sind unter der "PLANKA-Unternehmenslizenz" lizenziert.
-- Um "PLANKA Enterprise"-Dateien oder Quellen zu nutzen, müssen Sie eine gültige "PLANKA-Unternehmenslizenz" besitzen. Sie können mehr über unsere kommerzielle Lizenz in der [PLANKA Enterprise License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20DE.md) lesen.
+- Inhalte von Sourcecode-Branches außer dem Hauptbranch (üblicherweise "master" oder "main") sind nicht lizenziert.
+- Quellcode-Dateien oder andere Dateien, die ".pe." (für "PLANKA Pro/Enterprise") in ihren Datei- oder Ordnernamen enthalten oder anderweitig durch "PLANKA Pro/Enterprise" in ihren Dateiköpfen oder Ordnern gekennzeichnet sind, sind NICHT unter der "Fair Use Lizenz" lizenziert. Diese Dateien sind "PLANKA Pro/Enterprise"-Dateien und sind unter der "PLANKA Pro/Enterprise-Lizenz" lizenziert.
+- Um "PLANKA Pro/Enterprise"-Dateien oder Quellen zu nutzen, müssen Sie eine gültige "PLANKA Pro/Enterprise-Lizenz" besitzen. Sie können mehr über unsere kommerzielle Lizenz in der [PLANKA Commercial License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20DE.md) lesen.
- Alle Komponenten von Drittanbietern, die in unsere Software integriert sind, sind unter der ursprünglichen Lizenz lizenziert, die vom Eigentümer der jeweiligen Komponente bereitgestellt wird.
- Inhalte außerhalb der oben genannten Dateien oder Einschränkungen sind unter der "Fair Use Lizenz" verfügbar, wie unten definiert.
## Fair Use Lizenz
-Version 1.0
+Version 1.1
### Annahme
-Durch die Nutzung der Software stimmen Sie allen unten aufgeführten Bedingungen zu.
+Durch die Nutzung der Software stimmen Sie allen oben und unten aufgeführten Bedingungen zu.
### Urheberrechtslizenz
@@ -40,9 +42,19 @@ Der Lizenzgeber gewährt Ihnen eine nicht-exklusive, gebührenfreie, weltweite,
"Sie dürfen den Namen oder das Logo von PLANKA nur verwenden, um zu beschreiben, dass Ihr Dienst die Software enthält. Jede andere Markennutzung (z.B. in Produktnamen, Domains oder Marketingmaterial) bedarf unserer vorherigen schriftlichen Zustimmung."
+### Erlaubte Nutzung
+
+Sie dürfen PLANKA nutzen oder modifizieren (a) für persönliche, Hobby- oder Bildungszwecke, (b) intern innerhalb Ihrer eigenen Organisation, (c) für privates Hosting für eine typische Anzahl von Freunden, Familie oder persönlichen Projekten, (d) um gemeinnützigen Organisationen (wie von den jeweiligen Steuerbehörden anerkannt) kostenlosen Zugang zu gewähren, oder wenn Sie selbst eine anerkannte gemeinnützige Organisation sind, die externe Nutzer in Ihre Mission einbezieht, und (e) öffentliche Bildungseinrichtungen ausschließlich für akademische/Forschungszwecke.
+
+### Eingeschränkte Nutzung
+
+Das Teilen von Konten/Zugangsdaten mit Dritten für geschäftliche Zwecke oder der Betrieb von PLANKA als gehosteter Dienst für jeglichen kommerziellen Gewinn ist untersagt. Kommerzieller Gewinn umfasst jede Form von Zahlung, Werbeeinnahmen, Datenmonetarisierung oder indirekten kommerziellen Nutzen oder Geschäftsvorteil.
+
+Für alle anderen PLANKA-basierten Hosting-Dienste oder die gemeinsame Nutzung von PLANKA-Konten zwischen Organisationen müssen Sie eine kommerzielle PLANKA-Lizenz erwerben.
+
### Einschränkungen
-Sie dürfen die Software nutzen oder modifizieren (a) für persönliche, Hobby- oder Bildungszwecke oder (b) innerhalb Ihrer eigenen juristischen Person (einschließlich hundertprozentiger Tochtergesellschaften) für Arbeitsabläufe, die nicht als kostenpflichtiges Produkt oder als unternehmensübergreifender Dienst an Dritte angeboten werden. Unternehmensübergreifende Dienste oder Nutzung zwischen deren juristischen Personen und anderen nicht verbundenen dritten juristischen Personen sind ausdrücklich untersagt. Sie dürfen keine vom Lizenzgeber bereitgestellten Lizenz-, Urheber- oder anderen Hinweise in der Software verändern, entfernen oder verschleiern. Jede Nutzung der Marken des Lizenzgebers unterliegt dem geltenden Recht.
+Sie dürfen keine vom Lizenzgeber bereitgestellten Lizenz-, Urheber- oder anderen Hinweise in der Software verändern, entfernen oder verschleiern. Jede Nutzung der Marken des Lizenzgebers unterliegt dem geltenden Recht.
### Patente
diff --git a/LICENSES/PLANKA Community License EN.md b/LICENSES/PLANKA Community License EN.md
index 6e4b3298..93941c8b 100644
--- a/LICENSES/PLANKA Community License EN.md
+++ b/LICENSES/PLANKA Community License EN.md
@@ -1,15 +1,17 @@
**PLANKA Community License**
-Version 1.0 - Last updated: May 2, 2025
+Version 1.1 - Last updated: May 20, 2025
Related files in English:
+
- PLANKA Community License EN.md (this file)
-- [PLANKA Enterprise License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20EN.md)
+- [PLANKA Commercial License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20EN.md)
- [PLANKA License Guide EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20EN.md)
Related files in German:
+
- [PLANKA Community License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20DE.md)
-- [PLANKA Enterprise License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20DE.md)
+- [PLANKA Commercial License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20DE.md)
- [PLANKA License Guide DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20DE.md)
---
@@ -19,18 +21,18 @@ Related files in German:
Files accessible to and marked for community use are licensed as follows:
- Content of branches other than the main branch (usually "master" or "main") is not licensed.
-- Source code files or other files that contain ".pe." (for "PLANKA Enterprise") in their file names or folder names or are otherwise marked as "PLANKA Enterprise" in their file headers or folders are NOT licensed under the "Fair Use License". These files are "PLANKA Enterprise" files and are licensed under the "PLANKA Enterprise License".
-- To use any "PLANKA Enterprise" files or sources, you must own a valid "PLANKA Enterprise License". You can read more about our commercial license in the [PLANKA Enterprise License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20EN.md).
+- Source code files or other files that contain ".pe." (for "PLANKA Pro/Enterprise") in their file names or folder names or are otherwise marked as "PLANKA Pro/Enterprise" in their file headers or folders are NOT licensed under the "Fair Use License". These files are "PLANKA Pro/Enterprise" files and are licensed under the "PLANKA Pro/Enterprise License".
+- To use any "PLANKA Pro/Enterprise" files or sources, you must own a valid "PLANKA Pro/Enterprise License". You can read more about our commercial license in the [PLANKA Commercial License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20EN.md).
- All third-party components incorporated into our software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above-mentioned files or restrictions is available under the "Fair Use License" as defined below.
## Fair Use License
-Version 1.0
+Version 1.1
### Acceptance
-By using the software, you agree to all of the terms and conditions below.
+By using the software, you agree to all of the terms and conditions above and below.
### Copyright License
@@ -40,9 +42,19 @@ The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensa
"You may use the PLANKA name or logo only to describe that your service incorporates the software. Any other trademark use (e.g., in product names, domains, or marketing material) requires our prior written consent."
+### Permitted Use
+
+You may use or modify PLANKA (a) for personal, hobby, or educational purposes, (b) internally within your own organization, (c) for private hosting for a typical number of friends, family, or personal projects, (d) to provide free access to non-profit organizations (as recognized by applicable tax authorities), or if you are a recognized non-profit organization yourself involving external users into your mission, and (e) public educational institutions for academic/research purposes only.
+
+### Restricted Use
+
+Sharing accounts/credentials with third parties for business purposes or operating PLANKA as a hosted service for any commercial gain whatsoever is prohibited. Commercial gain includes any form of payment, advertising revenue, data monetization, or indirect commercial benefit or business advantage.
+
+For all other PLANKA-based hosting services or shared use of PLANKA accounts across organizations, you need to buy a commercial PLANKA license.
+
### Limitations
-You may use or modify the software (a) for personal, hobby, or educational purposes or (b) inside your own legal entity (including wholly owned subsidiaries) for workflows that are not offered as a paid product or as a cross-company service to a third party. Cross-company services or use between your legal entity and other unrelated third-party legal entities are explicitly prohibited. You may not alter, remove, or obscure any licensing, copyright, or other notices from the software provided by the licensor. Any use of the licensor's trademarks is subject to applicable law.
+You may not alter, remove, or obscure any licensing, copyright, or other notices from the software provided by the licensor. Any use of the licensor's trademarks is subject to applicable law.
### Patents
diff --git a/LICENSES/PLANKA Enterprise License DE.md b/LICENSES/PLANKA Enterprise License DE.md
deleted file mode 100644
index 356ac98f..00000000
--- a/LICENSES/PLANKA Enterprise License DE.md
+++ /dev/null
@@ -1,33 +0,0 @@
-**PLANKA Enterprise License**
-
-Version 1.0 - Zuletzt aktualisiert: 2. Mai 2025
-
-Zugehörige Dateien in Englisch:
-- [PLANKA Community License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20EN.md)
-- [PLANKA Enterprise License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20EN.md)
-- [PLANKA License Guide EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20EN.md)
-
-Zugehörige Dateien in Deutsch:
-- [PLANKA Community License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20DE.md)
-- PLANKA Enterprise License DE.md (diese Datei)
-- [PLANKA License Guide DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20DE.md)
-
----
-
-# PLANKA-Unternehmenslizenz
-
-Copyright (c) 2025 von PLANKA Software GmbH.
-
-Unsere Software und zugehörige Dokumentationsdateien (die "Software") dürfen nur dann produktiv genutzt werden, wenn Sie (und jede juristische Person, die Sie vertreten) eine gültige "PLANKA Enterprise License", also eine "PLANKA-Unternehmenslizenz" besitzen, die Ihrer Nutzung entspricht. Vorbehaltlich des vorstehenden Satzes steht es Ihnen frei, unsere Software zu modifizieren und Patches dafür zu veröffentlichen. Sie stimmen zu, dass die PLANKA Software GmbH und/oder ihre Lizenzgeber (falls zutreffend) alle Rechte, Titel und Ansprüche an und auf alle solche Modifikationen und/oder Patches behalten, und alle solche Modifikationen und/oder Patches dürfen nur mit einer gültigen "PLANKA-Unternehmenslizenz" für die entsprechende Nutzung verwendet, kopiert, modifiziert, angezeigt, verteilt oder anderweitig genutzt werden. Ungeachtet des Vorstehenden dürfen Sie die Software für Entwicklungs- und Testzwecke ohne Abonnement kopieren und modifizieren. Sie stimmen zu, dass PLANKA Software GmbH und/oder ihre Lizenzgeber (falls zutreffend) alle Rechte, Titel und Ansprüche an und auf alle solche Modifikationen behalten. Es werden Ihnen keine anderen Rechte gewährt als die, die hier ausdrücklich genannt sind. Vorbehaltlich des Vorstehenden ist es verboten, die Software zu kopieren, zusammenzuführen, zu veröffentlichen, zu verteilen, zu unterlizenzieren und/oder zu verkaufen.
-
-#### Komponenten von Drittanbietern
-
-Für alle Komponenten von Drittanbietern, die in unsere Software integriert sind, werden diese Komponenten unter der ursprünglichen Lizenz lizenziert, die vom Eigentümer der jeweiligen Komponente bereitgestellt wird.
-
-## PLANKA Enterprise Repositories
-
-Nach dem Kauf unserer "PLANKA-Unternehmenslizenz" erhalten Sie Zugang zu unseren "PLANKA Enterprise"-Repositories. Hier finden Sie unsere neuesten stabilen Builds, die umfangreiche, eingehende Tests bestanden haben und als kampferprobt gelten. Unter keinen Umständen dürfen Sie Dateien, Quellcode oder Teile aus unseren "PLANKA Enterprise"-Repositories ohne vorherige Genehmigung der PLANKA Software GmbH an Personen weitergeben, die nicht zugangsberechtigt sind-.
-
-## Eingeschränkte Garantie
-
-UNSERE SOFTWARE WIRD "WIE SIE IST" BEREITGESTELLT, OHNE JEGLICHE GARANTIE, AUSDRÜCKLICH ODER IMPLIZIERT, EINSCHLIEßLICH, ABER NICHT BESCHRÄNKT AUF DIE GARANTIEN DER MARKTGÄNGIGKEIT, EIGNUNG FÜR EINEN BESTIMMTEN ZWECK UND NICHTVERLETZUNG VON RECHTEN DRITTER. IN KEINEM FALL HAFTEN DIE AUTOREN ODER URHEBERRECHTSINHABER FÜR ANSPRÜCHE, SCHÄDEN ODER ANDERE HAFTUNG, OB AUS VERTRAG, UNERLAUBTER HANDLUNG ODER ANDERWEITIG, DIE SICH AUS, AUS ODER IN VERBINDUNG MIT DER SOFTWARE ODER DER NUTZUNG ODER ANDEREN GESCHÄFTEN MIT DER SOFTWARE ERGEBEN.
diff --git a/LICENSES/PLANKA Enterprise License EN.md b/LICENSES/PLANKA Enterprise License EN.md
deleted file mode 100644
index 0d4e08cb..00000000
--- a/LICENSES/PLANKA Enterprise License EN.md
+++ /dev/null
@@ -1,33 +0,0 @@
-**PLANKA Enterprise License**
-
-Version 1.0 - Last updated: May 2, 2025
-
-Related files in English:
-- [PLANKA Community License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20EN.md)
-- PLANKA Enterprise License EN.md (this file)
-- [PLANKA License Guide EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20EN.md)
-
-Related files in German:
-- [PLANKA Community License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20DE.md)
-- [PLANKA Enterprise License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20DE.md)
-- [PLANKA License Guide DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20DE.md)
-
----
-
-# PLANKA Enterprise License
-
-Copyright (c) 2025 to present by PLANKA Software GmbH.
-
-Our software and associated documentation files (the "Software") may only be used in production if you (and any entity that you represent) hold a valid "PLANKA Enterprise License" corresponding to your usage. Subject to the foregoing sentence, you are free to modify our Software and publish patches for it. You agree that PLANKA Software GmbH and/or its licensors (as applicable) retain all right, title, and interest in and to all such modifications and/or patches, and all such modifications and/or patches may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid "PLANKA Enterprise License" for the corresponding usage. Notwithstanding the foregoing, you may copy and modify the Software for development and testing purposes without requiring a subscription. You agree that PLANKA Software GmbH and/or its licensors (as applicable) retain all right, title, and interest in and to all such modifications. You are not granted any other rights beyond what is expressly stated herein. Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, and/or sell the Software.
-
-#### Third-Party Components
-
-For all third-party components incorporated into our Software, those components are licensed under the original license provided by the owner of the applicable component.
-
-## PLANKA Enterprise Repositories
-
-After purchasing our "PLANKA Enterprise License", you get access to our "PLANKA Enterprise" repositories. Here you find our latest stable builds, which have passed extensive in-depth tests and are considered battle-proof. Under no circumstances are you allowed to pass files, source code, or any part of it from our "PLANKA Enterprise" repositories to anyone not eligible for access without prior permission from PLANKA Software GmbH.
-
-## Limited Warranty
-
-OUR SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/LICENSES/PLANKA License Guide DE.md b/LICENSES/PLANKA License Guide DE.md
index 1ba220e5..dd40a3f0 100644
--- a/LICENSES/PLANKA License Guide DE.md
+++ b/LICENSES/PLANKA License Guide DE.md
@@ -1,64 +1,66 @@
**PLANKA License Guide**
-Version 1.0 - Zuletzt aktualisiert: 2. Mai 2025
+Version 1.1 - Zuletzt aktualisiert: 20. Mai 2025
Zugehörige Dateien in Englisch:
+
- [PLANKA Community License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20EN.md)
-- [PLANKA Enterprise License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20EN.md)
+- [PLANKA Commercial License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20EN.md)
- [PLANKA License Guide EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20EN.md)
Zugehörige Dateien in Deutsch:
+
- [PLANKA Community License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20DE.md)
-- [PLANKA Enterprise License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20DE.md)
+- [PLANKA Commercial License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20DE.md)
- PLANKA License Guide DE.md (diese Datei)
---
-## PLANKAs "Fair Use Lizenz" und "PLANLA-Unternehmenslizenz" (Enterprise License)
+## PLANKAs "Fair Use Lizenz" und die "PLANKA Pro/Enterprise-Lizenz"
-Unsere [Fair Use Lizenz](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20DE.md) und unsere [PLANKA-Unternehmenslizenz](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20DE.md) basieren auf dem [fair-code](http://faircode.io)-Modell.
+Unsere [Fair Use Lizenz](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20DE.md) und unsere [PLANKA Pro/Enterprise-Lizenz](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20DE.md) basieren auf dem [fair-code](http://faircode.io)-Modell.
-#### Proprietäre Lizenzen für Unternehmen und die "PLANKA-Bildungslizenz" (Educational License)
+### Spezielle Lizenzierung für Bildungseinrichtungen und gemeinnützige Organisationen
-Wir bieten unseren Unternehmenskunden nach Absprache auch proprietäre Lizenzen sowie die "PLANKA-Bildungslizenz" für Schulen und Universitäten an. Bitte kontaktieren Sie uns für detaillierte Informationen unter [license@planka.group](mailto:license@planka.group).
+Bildungseinrichtungen und gemeinnützige Organisationen, die kommerzielle Funktionen oder eine Nutzung über unsere "Fair Use Lizenz" hinaus benötigen, sind eingeladen, uns für maßgeschneiderte Lizenzlösungen und Bildungspreise zu kontaktieren. Wir freuen uns darauf, Ihrer Organisation dabei zu helfen, ihre Mission zu erfüllen!
# Lizenz-FAQs
### Unter welcher Lizenz wird PLANKA angeboten?
-PLANKA verwendet die [Fair Use Lizenz](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20DE.md) und die [PLANKA-Unternehmenslizenz](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20DE.md). Diese Lizenzen basieren auf dem [fair-code](http://faircode.io)-Modell.
+PLANKA verwendet die [Fair Use Lizenz](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20DE.md) und die [PLANKA Pro/Enterprise-Lizenz](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20DE.md). Diese Lizenzen basieren auf dem [fair-code](http://faircode.io)-Modell.
### Welcher Quellcode ist durch PLANKAs "Fair Use Lizenz" abgedeckt?
Die [Fair Use Lizenz](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20DE.md) gilt für unseren Quellcode, der in unserem [GitHub-Hauptrepository](https://github.com/plankanban/planka) gehostet wird, mit folgenden Ausnahmen:
-* Inhalte von Branches außer dem Hauptbranch (üblicherweise "master" oder "main").
+- Inhalte von Branches außer dem Hauptbranch (üblicherweise "master" oder "main").
-* Quellcode-Dateien oder andere Dateien, die ".pe." (für "PLANKA Enterprise") in ihren Datei- oder Ordnernamen enthalten.
+- Quellcode-Dateien oder andere Dateien, die ".pe." (für "PLANKA Pro/Enterprise") in ihren Datei- oder Ordnernamen enthalten.
-* Quellcode-Dateien, die in den Dateien oder Ordnern als "PLANKA Enterprise" gekennzeichnet sind.
+- Quellcode-Dateien, die in den Dateien oder Ordnern als "PLANKA Pro/Enterprise" gekennzeichnet sind.
-* Quellcode in Ordnern, die separate Lizenzdateien enthalten, die sie eindeutig als "PLANKA Enterprise" kennzeichnen.
+- Quellcode in Ordnern, die separate Lizenzdateien enthalten, die sie eindeutig als "PLANKA Pro/Enterprise" kennzeichnen.
-Diese Ausnahmen sind unter der [PLANKA-Unternehmenslizenz](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20DE.md) lizenziert.
+Diese Ausnahmen sind unter der [PLANKA Pro/Enterprise-Lizenz](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20DE.md) lizenziert.
### Was genau ist die "Fair Use Lizenz"?
Die "Fair Use Lizenz" fällt in die Kategorie der sogenannten [fair-code](http://faircode.io)-Lizenzen. PLANKAs Lizenz basiert auf und erweitert die "Sustainable Use License", die von [n8n](https://n8n.io) eingeführt wurde und für deren Beratung wir sehr dankbar sind. Mit ähnlichen Zielen vor Augen haben wir beschlossen, deren Beispiel zu folgen und dieses Lizenz-Modell für unsere eigenen Bedürfnisse anzupassen. [Weiter unten](#warum-haben-sie-diese-lizenz-gewählt) können Sie nachlesen, warum wir diese Lizenzart gewählt haben.
-Die Lizenz gewährt unseren Benutzern das kostenlose Recht, die Community Version der Software zu nutzen, zu modifizieren und abgeleitete Werke zu erstellen und weiterzugeben, mit drei Einschränkungen:
+Die Lizenz gewährt Ihnen das kostenlose Recht, die Software zu nutzen, zu modifizieren, abgeleitete Werke zu erstellen und weiterzugeben unter folgenden Bedingungen:
-- Sie dürfen die Software nutzen oder modifizieren (a) für persönliche, Freizeit- oder Bildungszwecke oder (b) innerhalb Ihres eigenen Unternehmens bzw. Ihrer juristischen Person (einschließlich hundertprozentiger Tochtergesellschaften) für Arbeitsabläufe, die nicht als kostenpflichtiges Produkt oder als unternehmensübergreifender Dienst für Dritte angeboten werden.
+- Sie dürfen PLANKA nutzen oder modifizieren (a) für persönliche, Hobby- oder Bildungszwecke, (b) intern innerhalb Ihrer eigenen Organisation, (c) für privates Hosting für eine typische Anzahl von Freunden, Familie oder persönlichen Projekten, (d) um gemeinnützigen Organisationen (wie von den jeweiligen Steuerbehörden anerkannt) kostenlosen Zugang zu gewähren, oder wenn Sie selbst eine anerkannte gemeinnützige Organisation sind, die externe Nutzer in Ihre Mission einbezieht, und (e) öffentliche Bildungseinrichtungen ausschließlich für akademische/Forschungszwecke.
-- Sie dürfen keine Lizenz-, Urheber- oder sonstigen Hinweise des Lizenzgebers in der Software ändern, entfernen oder verschleiern. Jede Verwendung des Namens, bzw. von Marken des Lizenzgebers unterliegen dem geltenden Recht.
+- Das Teilen von Konten/Zugangsdaten mit Dritten für geschäftliche Zwecke oder der Betrieb von PLANKA als gehosteter Dienst für jeglichen kommerziellen Gewinn ist untersagt. Kommerzieller Gewinn umfasst jede Form von Zahlung, Werbeeinnahmen, Datenmonetarisierung oder indirekten kommerziellen Nutzen oder Geschäftsvorteil.
-- "Sie dürfen den Namen oder das Logo von PLANKA verwenden, um sachlich zu beschreiben, dass Ihr Dienst die PLANKA-Software enthält oder auf andere Weise nutzt. Jede andere Verwendung der Marke/des Namens (z.B. in Produktnamen oder -marken, Domainnamen oder Marketingmaterial) erfordert unsere vorherige schriftliche Zustimmung."
+- Sie dürfen keine vom Lizenzgeber bereitgestellten Lizenz-, Urheber- oder anderen Hinweise in der Software verändern, entfernen oder verschleiern. Jede Nutzung der Marken des Lizenzgebers unterliegt dem geltenden Recht.
### Was ist im Rahmen der Lizenz im Kontext von PLANKAs Produkten erlaubt und was NICHT?
-Unsere Lizenz beschränkt die Nutzung auf "interne Geschäftszwecke". In der Praxis bedeutet dies, dass jegliche Nutzung erlaubt ist, es sei denn, Sie verkaufen ein Produkt, einen Dienst oder ein Modul, bei dem der Wert vollständig oder im Wesentlichen von der PLANKA-Funktionalität abgeleitet wird.
+Unsere Lizenz erlaubt die Nutzung für "interne Geschäftszwecke" sowie für persönliche, Hobby-, Bildungs- und begrenzte private Hosting-Szenarien. Sie verbietet jedoch die Nutzung von PLANKA für jede Form des kommerziellen Gewinns, wie z.B. den Verkauf eines Produkts oder einer Dienstleistung, bei dem der Wert in irgendeiner Form von PLANKAs Funktionalität abgeleitet wird, den Betrieb als kostenpflichtiger gehosteter Dienst, die Monetarisierung von Zugang oder Benutzerdaten oder die Erzielung anderer indirekter kommerzieller Vorteile.
-###### Hier sind einige Beispiele, die nicht erlaubt wären:
+##### Hier sind einige Beispiele, die nicht erlaubt wären:
- PLANKA unter einem White-Label anzubieten und es Ihren Kunden oder Partnern gegen Geld anzubieten.
@@ -66,25 +68,25 @@ Unsere Lizenz beschränkt die Nutzung auf "interne Geschäftszwecke". In der Pra
- PLANKAs API für Dienste zu nutzen, für die Geld verlangt wird.
-- Die Verwendung von PLANKA, um Ihre eigene Organisation mit anderen Organisationen oder Personen außerhalb Ihrer eigenen Organisation zu verbinden.
+- Die Verwendung von PLANKA, um Ihre eigene juristische Person mit juristischen Personen oder Personen außerhalb Ihrer Organisation im Kontext einer kommerziellen Beziehung zu verbinden.
- PLANKA zur Durchführung oder Unterstützung illegaler oder rechtswidriger Aktivitäten zu verwenden.
-###### Die folgenden Beispiele sind unter unserer Lizenz erlaubt:
+##### Die folgenden Beispiele sind unter unserer Lizenz erlaubt:
- Verwendung von PLANKA zur Kontrolle Ihrer internen Prozesse und Verwaltung Ihrer internen Projekte.
- Integration von PLANKA in andere intern genutzte Produkte, um deren Fähigkeiten zu erweitern.
-- Bereitstellung von Beratungs- oder Bildungsdienstleistungen im Zusammenhang mit PLANKA, zum Beispiel zum Aufbau oder zur Integration von Arbeitsabläufen für oder in Verbindung mit PLANKA oder zur Entwicklung benutzerdefinierter Module zur Erweiterung seiner Funktionalitäten.
+- Bereitstellung von Beratungs- oder öffentlichen Bildungsdienstleistungen im Zusammenhang mit PLANKA, zum Beispiel zum Aufbau oder zur Integration von Arbeitsabläufen für oder in Verbindung mit PLANKA oder zur Entwicklung benutzerdefinierter Module zur Erweiterung seiner Funktionalitäten.
- Unterstützung von PLANKA, zum Beispiel durch Einrichtung oder Wartung auf einem internen Firmenserver.
### Ist es erlaubt, PLANKA als Backend-Integration zu nutzen?
-Wenn Sie PLANKA und sein Backend sowie zugehörige Dienste außerhalb der Grenzen interner Betriebsabläufe und Integrationen innerhalb Ihrer eigenen Organisation nutzen, müssen Sie eine "PLANKA-Unternehmenslizenz" erwerben. Dies gilt auch, wenn Sie PLANKA als Kern-Backend-Infrastruktur für Drittanbieteranwendungen, -dienste oder ein System nutzen möchten, das an externe Parteien verteilt wird oder von diesen genutzt wird.
+Wenn Sie PLANKA und seine Backend-Dienste über interne Betriebsabläufe innerhalb Ihrer eigenen Organisation hinaus nutzen, indem Sie Dritten Zugang für kommerziellen Gewinn ermöglichen, wie z.B. das Teilen von Konten mit Kunden oder den Verkauf eines Produkts oder einer Dienstleistung, bei dem der Wert in irgendeiner Form von PLANKAs Funktionalität abgeleitet wird, den Betrieb als kostenpflichtiger gehosteter Dienst, die Monetarisierung von Zugang oder Benutzerdaten oder die Erzielung anderer indirekter kommerzieller Vorteile, sind Sie verpflichtet, eine "PLANKA Pro/Enterprise-Lizenz" zu erwerben.
-Jede Vereinbarung, welche die Unterlizenzierung, Umlabelung oder anderweitige Bereitstellung von PLANKA für externe Parteien oder die Integration von PLANKA in ein anderes Produkt beinhaltet, um als primärer Dienst für dieses Produkt zu fungieren, erfordert ebenfalls eine gültige "PLANKA-Unternehmenslizenz" oder proprietäre Lizenz, die für unsere Unternehmenskunden ebenfalls verfügbar ist.
+Dies umfasst Szenarien, in denen PLANKA als Kerninfrastruktur für Drittanbieterlösungen dient, von Benutzern außerhalb Ihrer juristischen Person als Teil eines kostenpflichtigen oder kommerziellen Angebots genutzt wird oder bei denen Sie PLANKA unterlizenzieren, neu verpacken oder anderweitig externen Parteien zur Verfügung stellen. Jede Vereinbarung, die die Integration von PLANKA in ein anderes Produkt beinhaltet, um als primärer Betriebsmotor für dieses Produkt zu dienen, erfordert ebenfalls eine gültige "PLANKA Pro/Enterprise-Lizenz" oder proprietäre Lizenz, die für unsere Unternehmenskunden verfügbar ist.
### Können Sie mir einige schnelle Beispiele geben, um kostenlose Nutzung vs. Unternehmensnutzung zu verdeutlichen?
@@ -108,39 +110,67 @@ Eine Schule oder gemeinnützige Organisation bündelt PLANKA in einem kostenlose
**ERLAUBT** unter der "Fair Use Lizenz". Da PLANKA kostenlos und ohne kommerzielle Einnahmen im Sinn weitergegeben wird, sind Sie mehr als willkommen, gemeinnützigen Einrichtungen und Schulen einen besseren Zugang zu PLANKA zu ermöglichen.
-**JEDOCH** wenn Sie auch planen, Schülern und Studenten Zugang zu einer zentralen bzw. schulinternen PLANKA-Instanz zu gewähren, würde dies eine "PLANKA-Bildungslizenz" erfordern, die wir sehr gerne auf Anfrage anbieten.
+**JEDOCH** wenn Sie auch planen, Schülern und Studenten Zugang zu einer ansonsten schulinternen PLANKA-Instanz zu gewähren, würde dies unsere "PLANKA-Bildungslizenz" erfordern, die wir sehr gerne auf Anfrage anbieten.
##### Beispiel 4: PLANKA-Logins für Kunden und Partner bereitstellen
-Um ein besseres Projekt-Feedback zu ermöglichen, bieten Sie Ihrem Kunden oder Jemandem aus einem Partnerunternehmen Zugang zu Projektboards innerhalb Ihres Unternehmens an. Sie können nun z.B. Karten kommentieren und auch von PLANKAs Echtzeit-Update-Funktionen profitieren.
+Um ein besseres Projekt-Feedback zu ermöglichen, bieten Sie Ihrem Kunden und jemandem aus einem Joint Venture, mit denen Sie beide eine kommerzielle Beziehung haben, Zugang zu Projektboards innerhalb Ihres Unternehmens an. Sie können nun z.B. Karten kommentieren und auch von PLANKAs Echtzeit-Update-Funktionen profitieren.
-**NICHT ERLAUBT** unter der "Fair Use Lizenz". Das Anbieten von PLANKA als Teil eines kostenpflichtigen Dienstes für Dritte oder die Bereitstellung von PLANKA-Zugang für Drittbenutzer außerhalb Ihrer eigenen Organisation erfordert, dass Sie eine "PLANKA-Unternehmenslizenz" erwerben.
+**NICHT ERLAUBT** unter der "Fair Use Lizenz". Das Anbieten von PLANKA als Teil eines kostenpflichtigen Dienstes für Dritte oder die Bereitstellung von PLANKA-Zugang für Drittbenutzer außerhalb Ihrer eigenen juristischen Person im Kontext einer kommerziellen Beziehung erfordert, dass Sie eine "PLANKA Pro/Enterprise-Lizenz" registrieren.
##### Beispiel 5: PLANKA als gehostetes Produkt für andere Unternehmen anbieten
-Sie möchten Geld damit verdienen, indem Sie PLANKA anderen Unternehmen, Freiberuflern oder anderen Personen zur Verfügung stellen damit diese Zugriff auf bzw. Kontrolle über ein PLANKA-System erhalten.
+Sie möchten Geld damit verdienen, indem Sie PLANKA Unternehmen, Freiberuflern und anderen Personen zur Verfügung stellen. Auf diese Weise haben sie einfachen Zugang zu Projektmanagement und -kontrolle von überall.
-**NICHT ERLAUBT** unter der "Fair Use Lizenz". Der Verkauf PLANKA-basierter Dienste erfordert, dass Sie einer "PLANKA-Wiederverkäuferlizenz" (Hosting License) zustimmen. Wir haben eine spezielle Hosting-Vereinbarung und sehr günstige Hosting Lizenzpreise für diejenigen, die kostenpflichtige PLANKA-Dienste für Kunden anbieten oder einfach als Wiederverkäufer für unsere eigenen "PLANKA Corporate Hosting Services" fungieren möchten.
+**NICHT ERLAUBT** unter der "Fair Use Lizenz". Der Verkauf PLANKA-basierter Dienste erfordert, dass Sie einer "PLANKA-Wiederverkäuferlizenz" zustimmen. Wir haben eine spezielle Hosting-Vereinbarung für diejenigen, die kostenpflichtige PLANKA-Dienste für Kunden anbieten oder einfach als Wiederverkäufer für unsere eigenen "PLANKA Corporate Hosting Services" fungieren möchten.
---
+### Kurzübersicht der "Fair Use Lizenz"
+
+- Persönliche, Bildungs-, Hobby- oder interne Geschäftsnutzung:
+
+ Kostenlos nutzbar. Sie können PLANKA für sich selbst, Ihr Team oder Ihre Organisation für persönliche, Bildungs- oder interne operative Zwecke nutzen, modifizieren und hosten.
+
+- Nutzung durch gemeinnützige Organisationen und öffentliche Bildungseinrichtungen:
+
+ Kostenlos für akademische oder Forschungszwecke nutzbar, vorausgesetzt die Organisation qualifiziert sich als gemeinnützig unter den geltenden Steuergesetzen. Keine Anmeldung oder spezielle Lizenz erforderlich.
+
+- Bereitstellung von Zugang für Dritte für kommerziellen Gewinn:
+
+ Nicht erlaubt. Das Teilen von Konten oder Zugangsdaten mit Dritten für kommerziellen Gewinn ist unter der Lizenz verboten, auch wenn Sie nicht direkt für den Zugang berechnen.
+
+- Hosting von PLANKA als kommerzieller Dienst:
+
+ Nicht erlaubt. Sie dürfen PLANKA nicht als gehosteten Dienst anbieten oder in ein kostenpflichtiges Produkt oder eine Dienstleistung integrieren, bei dem der Wert wesentlich von PLANKA stammt. Dies umfasst Werbeeinnahmen, Datenmonetarisierung oder indirekten kommerziellen Nutzen oder Geschäftsvorteil.
+
### Was, wenn ich PLANKA für etwas nutzen möchte, das nicht durch die Lizenz erlaubt ist?
-Sie müssen eine separate kommerzielle Vereinbarung mit uns abschließen. Wir ermutigen Softwareintegratoren und technisches Personal dazu, PLANKA in ihre Produkte zu integrieren oder mit ihnen zu verbinden und unsere umfangreiche API zu nutzen, um Prozesse innerhalb ihres Unternehmens zu steuern - wir bitten sie nur, eine Vereinbarung zu unterzeichnen, die die Nutzungsbedingungen und die von PLANKA für die Nutzung des Produkts erforderlichen Lizenzgebühren festlegt. Durch unsere API kann PLANKA externe Systeme steuern oder auf sie reagieren. Sie können [hier](https://docs.planka.cloud/docs/category/api-reference) mehr darüber erfahren oder uns hierüber kontaktieren.
+Um die Grenzen der selbst gehosteten Community-Version zu überschreiten, benötigen Sie eine "PLANKA Pro/Enterprise-Lizenz". Sobald Sie sich bei uns registrieren, werden die Beschränkungen der "Fair Use Lizenz" nicht nur rechtlich aufgehoben, sondern Sie erhalten auch Zugang zum vollständigen Enterprise-Funktionsumfang.
+
+Alternativ können Sie diese Grenzen durch ein Abonnement einer gehosteten Version - entweder Community oder Pro/Enterprise - überschreiten. Dies ermöglicht beispielsweise die Bereitstellung von Konten für Ihre Kunden.
+
+Wenn Sie PLANKA als Dienstleistung für andere Organisationen anbieten oder PLANKA in Ihre eigene Software integrieren möchten, müssen Sie eine separate Umsatzvereinbarung mit uns abschließen. Wir ermutigen aktiv Softwareintegratoren und technisches Personal, PLANKA in ihre anderen Produkte zu integrieren und zu verbinden und unsere umfangreiche API zu nutzen, um auf Prozesse innerhalb ihres Unternehmens zu reagieren, sie zu steuern und zu beherrschen; wir bitten sie lediglich, eine Vereinbarung zu unterzeichnen, die die Nutzungsbedingungen und die von PLANKA für die Nutzung des Produkts erforderlichen Lizenzgebühren festlegt. Über PLANKAs API kann es externe Systeme steuern und auf sie reagieren. Sie können [hier](https://docs.planka.cloud/docs/category/api-reference/) mehr darüber erfahren oder uns kontaktieren.
Wenn Sie sich nicht sicher sind, ob der von Ihnen ins Auge gefasste Anwendungsfall einen internen Geschäftszweck darstellt oder nicht, werfen Sie einen Blick auf die [Beispiele oben](#können-sie-mir-einige-schnelle-beispiele-geben-um-kostenlose-nutzung-vs-unternehmensnutzung-zu-verdeutlichen), und falls Sie immer noch unsicher sind, kontaktieren Sie uns bitte unter [license@planka.group](mailto:license@planka.group).
-### Warum nutzen Sie keine Standard-Open-Source-Lizenz?
+### Warum gibt es keine kommerzielle Lizenz für eine selbst-gehostete Community-Version um die "Fair Use License"-Beschränkungen aufzuheben?
-Wir verbringen viel Zeit damit, ein einfaches, aber leistungsstarkes Tool zu erstellen, welches die Kontrolle und Beherrschung von Projekten zu einer angenehmen Erfahrung macht. Außerdem wollten wir, dass PLANKA so weit als möglich frei verfügbar ist, während wir gleichzeitig sicherstellen müssen, dass wir ein nachhaltiges und tragfähiges Geschäft aufbauen können. In dem wir unser Produkt kostenlos, und den Quellcode verfügbar und verteilbar machten, helfen wir allen, auf das Produkt zuzugreifen. Indem wir als Unternehmen tätig sind, können wir neue Funktionen entwickeln und veröffentlichen, Fehler beheben und langfristig zuverlässige Software mit wachsendem Umfang bereitstellen.
+Wir unterstützen die selbst-gehostete Community-Version über unsere GitHub- und Discord-Community-Kanäle, aber wir können nicht effizient direkten Telefon- oder E-Mail-Support für mehrere Versionen bereitstellen, während wir unseren Verpflichtungen gegenüber zahlenden Kunden nachkommen. Deshalb ist direkter Support ausschließlich Pro/Enterprise-Kunden vorbehalten.
+
+Wir bieten jedoch professionelle Installationsdienste und monatliche Serviceverträge für die Einrichtung, Aktualisierung und Überwachung aller Versionen an, um Organisationen dabei zu helfen, das Beste aus ihrer PLANKA-Bereitstellung herauszuholen - bitte kontaktieren Sie uns unter [license@planka.group](mailto:license@planka.group).
+
+### Warum nutzt PLANKA keine Standard-Open-Source-Lizenz?
+
+Wir verbringen viel Zeit damit, ein einfaches, aber leistungsstarkes Tool zu erstellen, welches die Kontrolle und Beherrschung von Projekten zu einer angenehmen Erfahrung macht. Außerdem wollten wir, dass PLANKA so weit als möglich frei verfügbar ist, während wir gleichzeitig sicherstellen müssen, dass wir ein nachhaltiges und tragfähiges Geschäft aufbauen können. Indem wir unser Produkt kostenlos nutzbar, einfach verteilbar und quelloffen machten, helfen wir allen, auf das Produkt zuzugreifen. Indem wir als Unternehmen tätig sind, können wir langfristig neue Funktionen entwickeln und veröffentlichen, Fehler beheben und zuverlässige Software in großem Maßstab bereitstellen.
### Warum haben Sie diese Lizenz gewählt?
Wir glauben, dass die "Fair Use Lizenz" sowohl für die Gemeinschaft als auch für die Entwickler von Vorteil ist. Entwicklung ist ein kostspieliges Unterfangen, und eine Community-Version kostenlos weiterzugeben, ist ein Risiko, welches viele Unternehmen nicht überleben, ohne ihre Software oder ihr Unternehmen zu veräußern. Daher leben viele Open-Source-Unternehmen von Spenden oder Finanzinvestoren. Anstatt unsere Seele zu verkaufen, verkaufen wir Dienstleistungen und Softwarelizenzen. Auf diese Weise können wir weiter wachsen, programmieren und unsere Community unterstützen. Die kurze Antwort lautet also "Leben und leben lassen" - so denken wir über PLANKA.
-Daher helfen wir dabei, [fair-code](https://faircode.io)-Software zu fördern, mit dem Ziel, sie zu einem bekannten Sammelbegriff zu machen, um Softwaremodelle wie Unseres zu beschreiben. Um jegliche Reibung um unsere proprietäre Lizenz auf ein absolutes Minimum zu beschränken, konzentrieren wir uns auf zwei Dinge:
+Daher helfen wir dabei, [fair-code](https://faircode.io)-Software zu fördern, mit dem Ziel, sie zu einem bekannten Sammelbegriff zu machen, um Softwaremodelle wie unseres zu beschreiben. Um jegliche Reibung um unsere proprietäre Lizenz auf ein absolutes Minimum zu beschränken, konzentrieren wir uns auf zwei Dinge:
-1. Klare Sprache und minimale Länge - die Lizenz ist in klarem, präzisem Englisch bzw Deutsch geschrieben, mit nur den unbedingt notwendigen Klauseln.
+1. Klare Sprache und minimale Länge - die Lizenz ist in klarem, präzisem Deutsch (eine englische Version existiert ebenfalls) geschrieben, mit nur den unbedingt notwendigen Klauseln.
2. Förderung von fair-code - wir fördern aktiv das fair-code-Modell, damit die Menschen es als unkomplizierte, nachhaltige Möglichkeit erkennen, Software wie PLANKA zu teilen und zu verbessern.
@@ -158,7 +188,7 @@ Es ist einfach, mit den Code-Beiträgen zu PLANKA auf [GitHub](https://github.co
### Ist PLANKA Open Source?
-PLANKAs Quellcode ist unter unserer "Fair Use Lizenz" frei verfügbar. Während dies nicht mit der strengen Definition der Open Source Initiative übereinstimmt (die keine Nutzungsbeschränkungen erlaubt), bietet PLANKA den meisten Benutzern, einschließlich Unternehmen, dennoch fast die selben Vorteile wie traditionelle Open-Source-Software.
+PLANKAs Quellcode ist unter der "Fair Use Lizenz" frei verfügbar. Während dies nicht mit der strengen Definition der Open Source Initiative übereinstimmt (die keine Nutzungsbeschränkungen erlaubt), bietet PLANKA den meisten Benutzern, einschließlich Unternehmen, dennoch fast die gleichen Vorteile wie traditionelle Open-Source-Software.
Wir befürworten, was oft als 'fair-code'-Modell bezeichnet wird - unser Code ist quelloffen und folgt einer einfachen "Leben und leben lassen"-Philosophie. Dieser Ansatz ermöglicht es uns, ein nachhaltiges Unternehmen zu führen und gleichzeitig Transparenz und Flexibilität für unsere Community zu bieten. Viele Unternehmen übernehmen diesen ausgewogenen Lizenzierungsansatz, der den Geist der Offenheit bewahrt und gleichzeitig die langfristige Lebensfähigkeit des Projekts sicherstellt. Wir sind stolz darauf, Teil dieser Bewegung zu sein!
@@ -174,8 +204,8 @@ Fair-code ist keine Softwarelizenz. Es beschreibt ein Softwaremodell, bei dem So
- Von ihren Autoren kommerziell eingeschränkt wird.
-Unsere "Fair Use Lizenz" ist eine fair-code-Lizenz. Sie können mehr darüber lesen und andere Beispiele für fair-code-Lizenzen [hier](https://faircode.io) sehen. Um mit uns bzgl. Lizenzfragen in Verbindung zu treten, senden Sie bitte eine E-Mail an [license@planka.group](mailto:license@planka.group).
+Die "Fair Use Lizenz" ist eine fair-code-Lizenz. Sie können mehr darüber lesen und andere Beispiele für fair-code-Lizenzen [hier](https://faircode.io) sehen. Um mit uns bzgl. Lizenzfragen in Verbindung zu treten, senden Sie bitte eine E-Mail an [license@planka.group](mailto:license@planka.group).
-### Kann ich diese Art der Lizenz für mein eigenes Projekt verwenden?
+### Kann ich die "Fair Use Lizenz" für mein eigenes Projekt verwenden?
-Ja! Wir selbst haben die "Fair Use Lizenz" genutzt, indem wir den Fußstapfen anderer folgten, die auf ihrer Website und in ihrer Lizenz offen andere dazu einladen, dem fair-code-Pfad zu folgen. Und auch wir sind gespannt darauf, mehr Software zu sehen, die unsere "Fair Use Lizenz" verwendet.
+Ja! Wir selbst haben die "Fair Use Lizenz" genutzt, indem wir den Fußstapfen anderer folgten, die auf ihrer Website und in ihrer Lizenz offen andere dazu einladen, dem fair-code-Pfad zu folgen. Wie sie sind auch wir gespannt darauf, mehr Software zu sehen, die die "Fair Use Lizenz" verwendet.
diff --git a/LICENSES/PLANKA License Guide EN.md b/LICENSES/PLANKA License Guide EN.md
index 7e77f3cd..3dc43993 100644
--- a/LICENSES/PLANKA License Guide EN.md
+++ b/LICENSES/PLANKA License Guide EN.md
@@ -1,64 +1,66 @@
**PLANKA License Guide**
-Version 1.0 - Last updated: May 2, 2025
+Version 1.1 - Last updated: May 20, 2025
Related files in English:
+
- [PLANKA Community License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20EN.md)
-- [PLANKA Enterprise License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20EN.md)
+- [PLANKA Commercial License EN.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20EN.md)
- PLANKA License Guide EN.md (this file)
Related files in German:
+
- [PLANKA Community License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20DE.md)
-- [PLANKA Enterprise License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20DE.md)
+- [PLANKA Commercial License DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20DE.md)
- [PLANKA License Guide DE.md](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20DE.md)
---
-## PLANKA's "Fair Use License" and the "PLANKA Enterprise License"
+## PLANKA's "Fair Use License" and the "PLANKA Pro/Enterprise License"
-Our [Fair Use License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20EN.md) and our [PLANKA Enterprise License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20EN.md) are based on the [fair-code](http://faircode.io) model.
+Our [Fair Use License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20EN.md) and our [PLANKA Pro/Enterprise License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20EN.md) are based on the [fair-code](http://faircode.io) model.
-#### Proprietary licenses for enterprise and the "PLANKA Educational License"
+### Special licensing for educational & non-profit organizations
-We offer proprietary licenses to our enterprise customers, but also the "PLANKA Educational License" for schools and universities. Please contact us for detailed information at [license@planka.group](mailto:license@planka.group).
+Educational institutions and non-profit organizations requiring commercial-level features or usage beyond our "Fair Use License" are encouraged to contact us for tailored licensing solutions and educational pricing. We look forward to helping your organization accomplish its mission!
# License FAQs
### What license do you use for PLANKA?
-PLANKA uses the [Fair Use License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20EN.md) and the [PLANKA Enterprise License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20EN.md). These licenses are based on the [fair-code](http://faircode.io) model.
+PLANKA uses the [Fair Use License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20EN.md) and the [PLANKA Pro/Enterprise License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20EN.md). These licenses are based on the [fair-code](http://faircode.io) model.
### What source code is covered by the PLANKA's "Fair Use License"?
The [Fair Use License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20EN.md) applies to our source code hosted in our [main GitHub repository](https://github.com/plankanban/planka) except:
-* Content of branches other than the main branch (usually "master" or "main").
+- Content of branches other than the main branch (usually "master" or "main").
-* Source code files or other files that contain ".pe." (for "PLANKA Enterprise") in their file names or folder names.
+- Source code files or other files that contain ".pe." (for "PLANKA Pro/Enterprise") in their file names or folder names.
-* Source code files that are marked as "PLANKA Enterprise" in their file headers or folders.
+- Source code files that are marked as "PLANKA Pro/Enterprise" in their file headers or folders.
-* Source code in folders that contain separate license files that clearly mark them as "PLANKA Enterprise".
+- Source code in folders that contain separate license files that clearly mark them as "PLANKA Pro/Enterprise".
-These exceptions are licensed under the [PLANKA Enterprise License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20EN.md).
+These exceptions are licensed under the [PLANKA Pro/Enterprise License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20EN.md).
### What is the "Fair Use License"?
The "Fair Use License" falls under the so-called [fair-code](http://faircode.io) licenses category. PLANKA's license is based on and extends the "Sustainable Use License" introduced by [n8n](https://n8n.io) who's advice was greatly appreciated. With similar goals in mind, we decided to follow their lead and adopt their model for our own needs. [Further below](#why-did-you-choose-this-license) you can read why we chose this license.
-The license allows you the free right to use, modify, create derivative works, and redistribute with three limitations:
+The license allows you the free right to use, modify, create derivative works, and redistribute under the following conditions:
-- You may use or modify the software (a) for personal, hobby, or educational purposes, or (b) inside your own legal entity (including wholly owned subsidiaries) for workflows that are not offered as a paid product or as a cross-company service to any third party.
+- You may use or modify PLANKA (a) for personal, hobby, or educational purposes, (b) internally within your own organization, (c) for private hosting for a typical number of friends, family, or personal projects, (d) to provide free access to non-profit organizations (as recognized by applicable tax authorities), or if you are a recognized non-profit organization yourself involving external users into your mission, and (e) public educational institutions for academic/research purposes only.
-- You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor's trademarks is subject to applicable law.
+- Sharing accounts/credentials with third parties for business purposes or operating PLANKA as a hosted service for any commercial gain whatsoever is prohibited. Commercial gain includes any form of payment, advertising revenue, data monetization, or indirect commercial benefit or business advantage.
-- "You may use the PLANKA name or logo to factually describe that your service incorporates or in other ways uses PLANKA software. Any other trademark use (e.g., in product names or brands, domain names, or marketing material) requires our prior written consent."
+- You may not alter, remove, or obscure any licensing, copyright, or other notices from the software provided by the licensor. Any use of the licensor's trademarks is subject to applicable law.
### What is and is NOT allowed under the license in the context of PLANKA's products?
-Our license restricts use to "internal business purposes". In practice this means all use is allowed unless you are selling a product, service, or module in which the value derives entirely or substantially from PLANKA functionality.
+Our license allows use for "internal business purposes", as well as for personal, hobby, educational, and limited private hosting scenarios. However, it prohibits using PLANKA for any form of commercial gain, such as selling a product or service where the value derives in any form from PLANKA's functionality, operating it as a paid hosted service, monetizing access or user data, or deriving other indirect commercial benefits.
-###### Here are some examples that would not be allowed:
+##### Here are some examples that would not be allowed:
- White-labeling PLANKA and offering it to your customers or affiliates for money.
@@ -66,25 +68,25 @@ Our license restricts use to "internal business purposes". In practice this mean
- Use PLANKA's API to power services for which money is charged.
-- Use of PLANKA to connect your own legal entity with legal entities or persons outside your own legal entity.
+- Use of PLANKA to connect your own legal entity with legal entities or persons outside your organization in the context of a commercial relationship.
- Use PLANKA to conduct or support any kind of illegal or unlawful activity.
-###### All of the following examples are allowed under our license:
+##### All of the following examples are allowed under our license:
- Using PLANKA to control your internal processes and manage your internal projects.
- Integrate PLANKA into other internally used products to enhance their capabilities.
-- Providing consulting or educational services related to PLANKA, for example, to build or integrate workflows for or in connection with PLANKA or develop custom modules to extend its functionalities.
+- Providing consulting or public educational services related to PLANKA, for example, to build or integrate workflows for or in connection with PLANKA or develop custom modules to extend its functionalities.
- Supporting PLANKA, for example, by setting it up or maintaining it on an internal company server.
### Is it allowed to use PLANKA as a backend integration?
-If you use PLANKA and its backend and related services outside the boundaries of internal operations and integrations within your own organization, you need to buy a "PLANKA Enterprise License". This also applies if you want to use PLANKA as a core backend infrastructure for third-party applications, services, or a system that is distributed to or accessed by external parties.
+If you use PLANKA and its backend services beyond internal operations within your own organization by enabling third-party access for commercial gain, such as sharing accounts with clients or selling a product or service where the value derives in any form from PLANKA's functionality, operating it as a paid hosted service, monetizing access or user data, or deriving other indirect commercial benefits, you are required to purchase a "PLANKA Pro/Enterprise License".
-Any arrangement that involves sublicensing, repackaging, or otherwise making PLANKA available to external parties or integrating PLANKA into another product to serve as the primary operational engine for that product also requires a valid "PLANKA Enterprise License" or proprietary license available for our enterprise customers.
+This includes scenarios where PLANKA serves as core infrastructure for third-party solutions, is accessed by users outside your legal entity as part of a paid or commercial offering, or where you sublicense, repackage, or otherwise make PLANKA available to external parties. Any arrangement that involves integrating PLANKA into another product to serve as the primary operational engine for that product also requires a valid "PLANKA Pro/Enterprise License" or proprietary license available for our enterprise customers.
### Can you give me some quick examples to clarify free use vs. enterprise use?
@@ -112,9 +114,9 @@ A school or charity bundles PLANKA in a free, public Docker image for students w
##### Example 4: Provide PLANKA logins to clients and affiliates
-To facilitate better project feedback, you offer your client and someone from a joint venture access to project boards inside your company. They can now comment on cards and also benefit from PLANKA's real-time update capabilities.
+To facilitate better project feedback, you offer your client and someone from a joint venture, both of whom you have a commercial relationship with, access to project boards inside your company. They can now comment on cards and also benefit from PLANKA's real-time update capabilities.
-**NOT ALLOWED** under the "Fair Use License". Offering PLANKA as part of a paid service to third parties or providing PLANKA access to third-party users outside your own legal entity requires you to register a "PLANKA Enterprise License".
+**NOT ALLOWED** under the "Fair Use License". Offering PLANKA as part of a paid service to third parties or providing PLANKA access to third-party users outside your own legal entity in the context of a commercial relationship requires you to register a "PLANKA Pro/Enterprise License".
##### Example 5: Offer PLANKA as a hosted product to other companies
@@ -124,19 +126,47 @@ You want to earn money by providing PLANKA to companies, freelancers, and other
---
+### Quick "Fair Use License" summary
+
+- Personal, educational, hobby, or internal business use:
+
+ Free to use. You can use, modify, and host PLANKA for yourself, your team, or your organization for personal, educational, or internal operational purposes.
+
+- Use by non-profits and public educational institutions:
+
+ Free to use for academic or research purposes, provided the organization qualifies as a non-profit under applicable tax laws. No application or special license is required.
+
+- Providing access to third parties for any commercial gain:
+
+ Not allowed. Sharing accounts or credentials with third parties for any commercial gain is prohibited under the license, even if you are not directly charging for access.
+
+- Hosting PLANKA as a commercial service:
+
+ Not allowed. You may not offer PLANKA as a hosted service or integrate it into a paid product or service where the value substantially comes from PLANKA. This includes advertising revenue, data monetization, or indirect commercial benefit or business advantage.
+
### What if I want to use PLANKA for something that's not permitted by the license?
-You must sign a separate commercial agreement with us. We actively encourage software integrators and technical staff to integrate and connect PLANKA within their other products and use our extensive API to respond to, control, and master processes within their company; we just ask them to sign an agreement laying out the terms of use and the license fees required by PLANKA for using the product. Through PLANKA's API, it is capable of controlling and responding to external systems. You can learn more [here](https://docs.planka.cloud/docs/category/api-reference) or contact us about it.
+To exceed the limits of the self-hosted Community version, you'll need a "PLANKA Pro/Enterprise License". Once you register with us, the "Fair Use License" restrictions are not only legally lifted, but you also gain access to the full enterprise feature set.
+
+Alternatively, you can exceed these limits by subscribing to a hosted version - either Community or Pro/Enterprise. This allows, for example, providing accounts for your customers.
+
+If you want to provide PLANKA as a service for other organizations or integrate PLANKA into your own software, you must sign a separate revenue agreement with us. We actively encourage software integrators and technical staff to integrate and connect PLANKA within their other products and use our extensive API to respond to, control, and master processes within their company; we just ask them to sign an agreement laying out the terms of use and the license fees required by PLANKA for using the product. Through PLANKA's API, it is capable of controlling and responding to external systems. You can learn more [here](https://docs.planka.cloud/docs/category/api-reference/) or contact us about it.
If you are unsure whether the use case you have in mind constitutes an internal business purpose or not, take a look at the [examples above](#can-you-give-me-some-quick-examples-to-clarify-free-use-vs-enterprise-use), and if you're still not sure, please contact us at [license@planka.group](mailto:license@planka.group).
-### Why don't you use a default open-source license?
+### Why isn't there a commercial license for a self-hosted Community version which lifts the "Fair Use License" limits?
+
+We support the self-hosted Community version through our GitHub and Discord community channels, but we can't efficiently provide direct phone or email support for multiple versions while meeting our obligations to paying customers. That's why direct support is reserved exclusively for Pro/Enterprise customers.
+
+However, we do offer professional installation services and monthly-based service contracts for setting up, updating, and monitoring all versions to help organizations get the most out of their PLANKA deployment - please contact us at [license@planka.group](mailto:license@planka.group).
+
+### Why doesn't PLANKA use a default open-source license?
We spend a lot of time creating an easy yet powerful tool that makes controlling and mastering projects a fun experience. Also, we wanted PLANKA to be as widely and freely available as possible while also ensuring that we can build a sustainable and viable business. By making our product free to use, easy to distribute, and source-available, we help everyone access the product. By operating as a business, we can develop and release new features, fix bugs, and provide reliable software at scale long-term.
### Why did you choose this license?
-We believe that the "Fair Use License" is beneficial for the community as well as for the developers. Development is a costly enterprise, and giving away a community version for free is a risk that many companies don't survive without selling software or the company. Therefore, many open-source companies live from donations or financial investors. Instead of selling our soul, we sell services and software licenses. This way we continue to grow, code, and support our community. So the short answer is "Live and let live" is how we feel about PLANKA.
+We believe that the "Fair Use License" is beneficial for the community as well as for the developers. Development is a costly enterprise, and giving away a Community version for free is a risk that many companies don't survive without selling software or the company. Therefore, many open-source companies live from donations or financial investors. Instead of selling our soul, we sell services and software licenses. This way we continue to grow, code, and support our community. So the short answer is "Live and let live" is how we feel about PLANKA.
Therefore, we are helping to promote [fair-code](https://faircode.io) software with the goal of making it a well-known umbrella term to describe software models like ours. To keep any friction around our proprietary license to an absolute minimum, we focus on two things:
diff --git a/README.md b/README.md
index 0b07a568..af2b51ab 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@ For any security issues, please do not create a public issue on GitHub - instead
## License
-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 Enterprise License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Enterprise%20License%20EN.md).
+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
From 40e107d6d40c85602a0b3ff9db96d1ad8944021c Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Tue, 27 May 2025 23:37:47 +0200
Subject: [PATCH 024/108] chore: Update version
---
charts/planka/Chart.yaml | 4 ++--
client/src/version.js | 2 +-
docker-compose.yml | 2 +-
package-lock.json | 4 ++--
package.json | 2 +-
server/version.js | 2 +-
6 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/charts/planka/Chart.yaml b/charts/planka/Chart.yaml
index 7f61edc6..8f35785c 100644
--- a/charts/planka/Chart.yaml
+++ b/charts/planka/Chart.yaml
@@ -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.0.1
+version: 1.0.2
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
-appVersion: "2.0.0-rc.2"
+appVersion: "2.0.0-rc.3"
dependencies:
- alias: postgresql
diff --git a/client/src/version.js b/client/src/version.js
index 17bd948a..c5786a1a 100644
--- a/client/src/version.js
+++ b/client/src/version.js
@@ -1 +1 @@
-export default '2.0.0-rc.2';
+export default '2.0.0-rc.3';
diff --git a/docker-compose.yml b/docker-compose.yml
index bd8f8e9c..63ec12f3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,6 @@
services:
planka:
- image: ghcr.io/plankanban/planka:2.0.0-rc.2
+ image: ghcr.io/plankanban/planka:2.0.0-rc.3
restart: on-failure
volumes:
- favicons:/app/public/favicons
diff --git a/package-lock.json b/package-lock.json
index 7c3b8dba..fe8e0fbd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "planka",
- "version": "2.0.0-rc.2",
+ "version": "2.0.0-rc.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "planka",
- "version": "2.0.0-rc.2",
+ "version": "2.0.0-rc.3",
"hasInstallScript": true,
"dependencies": {
"concurrently": "^8.2.2",
diff --git a/package.json b/package.json
index 8957c17d..569a810c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "planka",
- "version": "2.0.0-rc.2",
+ "version": "2.0.0-rc.3",
"private": true,
"scripts": {
"client:build": "npm run build --prefix client",
diff --git a/server/version.js b/server/version.js
index 95744fb9..05a5f2fc 100644
--- a/server/version.js
+++ b/server/version.js
@@ -1 +1 @@
-module.exports = '2.0.0-rc.2';
+module.exports = '2.0.0-rc.3';
From 74274e511fa063a6b2285511a6b5c52d7c56744d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niccol=C3=B2=20Pedrini?=
Date: Fri, 30 May 2025 13:42:57 +0200
Subject: [PATCH 025/108] fix: Update Italian translation (#1163)
---
client/src/locales/it-IT/core.js | 19 ++++++++++++++++++-
server/config/i18n.js | 2 +-
server/config/locales/it-IT.json | 13 +++++++++++++
3 files changed, 32 insertions(+), 2 deletions(-)
create mode 100644 server/config/locales/it-IT.json
diff --git a/client/src/locales/it-IT/core.js b/client/src/locales/it-IT/core.js
index 032cf709..8bb6a26e 100644
--- a/client/src/locales/it-IT/core.js
+++ b/client/src/locales/it-IT/core.js
@@ -284,13 +284,29 @@ export default {
unsavedChanges: 'Modifiche non salvate',
uploadedImages: 'Immagini caricate',
userActions_title: 'Azioni utente',
- userAddedThisCardToList: '<0>{{user}}0> ha aggiunto questa scheda a {{list}}',
+ userAddedCardToList: '<0>{{user}}0> ha aggiunto <2>{{card}}2> a {{list}}',
+ userAddedThisCardToList: '<0>{{user}}0> ha aggiunto questa task a {{list}}',
+ userAddedUserToCard: '<0>{{actorUser}}0> ha aggiunto {{addedUser}} a <4>{{card}}4>',
+ userAddedUserToThisCard: '<0>{{actorUser}}0> ha aggiunto {{addedUser}} a questa task',
+ userAddedYouToCard: '<0>{{user}}0> ti ha aggiunto a <2>{{card}}2>',
+ userCompletedTaskOnCard: '<0>{{user}}0> ha completato {{task}} in <4>{{card}}4>',
+ userCompletedTaskOnThisCard: '<0>{{user}}0> ha completato {{task}} in questa task',
+ userJoinedCard: `<0>{{user}}0> è entrato in <2>{{card}}2>`,
+ userJoinedThisCard: `<0>{{user}}0> è entrato in questa task`,
userLeftNewCommentToCard:
'<0>{{user}}0> ha lasciato un commento «{{comment}}» a <2>{{card}}2>',
+ userLeftCard: '<0>{{user}}0> ha lasciato <2>{{card}}2>',
+ userLeftThisCard: '<0>{{user}}0> ha lasciato questa task',
+ userMarkedTaskIncompleteOnCard:
+ '<0>{{user}}0> ha contrassegnato {{task}} come incompleta in <4>{{card}}4>',
+ userMarkedTaskIncompleteOnThisCard:
+ '<0>{{user}}0> ha contrassegnato {{task}} come incompleta in questa task',
userMovedCardFromListToList:
'<0>{{user}}0> ha spostato <2>{{card}}2> da {{fromList}} a {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}0> ha spostato questa scheda da {{fromList}} a {{toList}}',
+ userRemovedUserFromCard: '<0>{{actorUser}}0> ha rimosso {{removedUser}} da <4>{{card}}4>',
+ userRemovedUserFromThisCard: '<0>{{actorUser}}0> ha rimosso {{removedUser}} da questa task',
username: 'Username',
users: 'Utenti',
viewer: 'Visualizzatore',
@@ -354,6 +370,7 @@ export default {
deleteProject_title: 'Elimina progetto',
deleteTask: 'Elimina task',
deleteTask_title: 'Elimina task',
+ deleteTaskList: 'Elimina lista di list',
deleteUser: 'Elimina utente',
deleteUser_title: 'Elimina utente',
dismissAll: 'Ignora tutto',
diff --git a/server/config/i18n.js b/server/config/i18n.js
index 40d448f1..65ac402d 100644
--- a/server/config/i18n.js
+++ b/server/config/i18n.js
@@ -19,7 +19,7 @@ module.exports.i18n = {
*
*/
- locales: ['en-GB', 'en-US', 'ru-RU'],
+ locales: ['en-GB', 'en-US', 'ru-RU', 'it-IT'],
/**
*
diff --git a/server/config/locales/it-IT.json b/server/config/locales/it-IT.json
new file mode 100644
index 00000000..13167262
--- /dev/null
+++ b/server/config/locales/it-IT.json
@@ -0,0 +1,13 @@
+{
+ "Card Created": "Nuova task creata",
+ "Card Moved": "Task spostata",
+ "New Comment": "Nuovo commento",
+ "Test Title": "Titolo di test",
+ "This is a test text message!": "Questo è un messaggio di testo di test!",
+ "This is a *test* **markdown** `message`!": "Questo è un *test* **markdown** `messaggio`!",
+ "This is a test html message
": "Questo è un test html messaggio
",
+ "You Were Added to Card": "Sei stato aggiunto alla task",
+ "%s created %s in %s on %s": "%s ha creato %s in %s in %s",
+ "%s left a new comment to %s on %s": "%s ha commentato %s in %s",
+ "%s moved %s from %s to %s on %s": "%s ha spostato %s da %s a %s in %s"
+}
From eb2a3a2875c4bbc7fe93ff145f562e7c7f2c21e8 Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Fri, 30 May 2025 13:44:58 +0200
Subject: [PATCH 026/108] ref: Fix locale position
---
server/config/i18n.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/server/config/i18n.js b/server/config/i18n.js
index 65ac402d..d149879a 100644
--- a/server/config/i18n.js
+++ b/server/config/i18n.js
@@ -19,7 +19,7 @@ module.exports.i18n = {
*
*/
- locales: ['en-GB', 'en-US', 'ru-RU', 'it-IT'],
+ locales: ['en-GB', 'en-US', 'it-IT', 'ru-RU'],
/**
*
From c0b04368512928e8696482e0d89def486aedd154 Mon Sep 17 00:00:00 2001
From: Roman Zavarnitsyn
Date: Fri, 30 May 2025 22:01:29 +0200
Subject: [PATCH 027/108] feat: Add ability to mention users in comments
(#1162)
---
client/package-lock.json | 45 ++++++++++
client/package.json | 1 +
.../src/components/comments/Comments/Add.jsx | 89 ++++++++++++++-----
.../comments/Comments/Add.module.scss | 40 ++++++---
.../comments/Comments/Edit.module.scss | 1 -
.../notifications/NotificationsStep/Item.jsx | 25 +++++-
client/src/configs/markdown-plugins/index.js | 2 +
.../src/configs/markdown-plugins/mention.js | 61 +++++++++++++
client/src/constants/Enums.js | 1 +
client/src/locales/en-GB/core.js | 2 +
client/src/locales/en-US/core.js | 2 +
client/src/styles.module.scss | 32 +++++++
client/src/utils/formatters.js | 4 +
server/api/helpers/comments/create-one.js | 33 ++++++-
.../api/helpers/notifications/create-one.js | 47 +++++++++-
server/api/models/Notification.js | 1 +
server/config/locales/en-GB.json | 2 +
server/config/locales/en-US.json | 2 +
server/config/locales/ru-RU.json | 2 +
server/utils/formatters.js | 7 ++
20 files changed, 357 insertions(+), 42 deletions(-)
create mode 100644 client/src/configs/markdown-plugins/mention.js
create mode 100644 client/src/utils/formatters.js
create mode 100644 server/utils/formatters.js
diff --git a/client/package-lock.json b/client/package-lock.json
index 6f912566..b9891269 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -69,6 +69,7 @@
"react-i18next": "^15.5.1",
"react-input-mask": "^2.0.4",
"react-intersection-observer": "^9.16.0",
+ "react-mentions": "^4.4.10",
"react-photoswipe-gallery": "^2.2.7",
"react-redux": "^8.1.3",
"react-router-dom": "^6.30.0",
@@ -12296,6 +12297,31 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
},
+ "node_modules/react-mentions": {
+ "version": "4.4.10",
+ "resolved": "https://registry.npmjs.org/react-mentions/-/react-mentions-4.4.10.tgz",
+ "integrity": "sha512-JHiQlgF1oSZR7VYPjq32wy97z1w1oE4x10EuhKjPr4WUKhVzG1uFQhQjKqjQkbVqJrmahf+ldgBTv36NrkpKpA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/runtime": "7.4.5",
+ "invariant": "^2.2.4",
+ "prop-types": "^15.5.8",
+ "substyle": "^9.1.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.3",
+ "react-dom": ">=16.8.3"
+ }
+ },
+ "node_modules/react-mentions/node_modules/@babel/runtime": {
+ "version": "7.4.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz",
+ "integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==",
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.13.2"
+ }
+ },
"node_modules/react-onclickoutside": {
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.2.tgz",
@@ -12677,6 +12703,12 @@
"node": ">=4"
}
},
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "license": "MIT"
+ },
"node_modules/regexp-match-indices": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz",
@@ -14183,6 +14215,19 @@
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="
},
+ "node_modules/substyle": {
+ "version": "9.4.1",
+ "resolved": "https://registry.npmjs.org/substyle/-/substyle-9.4.1.tgz",
+ "integrity": "sha512-VOngeq/W1/UkxiGzeqVvDbGDPM8XgUyJVWjrqeh+GgKqspEPiLYndK+XRcsKUHM5Muz/++1ctJ1QCF/OqRiKWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.4",
+ "invariant": "^2.2.4"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.3"
+ }
+ },
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
diff --git a/client/package.json b/client/package.json
index cf8921fd..c429160a 100755
--- a/client/package.json
+++ b/client/package.json
@@ -140,6 +140,7 @@
"react-i18next": "^15.5.1",
"react-input-mask": "^2.0.4",
"react-intersection-observer": "^9.16.0",
+ "react-mentions": "^4.4.10",
"react-photoswipe-gallery": "^2.2.7",
"react-redux": "^8.1.3",
"react-router-dom": "^6.30.0",
diff --git a/client/src/components/comments/Comments/Add.jsx b/client/src/components/comments/Comments/Add.jsx
index 56e8b139..24cdf11d 100755
--- a/client/src/components/comments/Comments/Add.jsx
+++ b/client/src/components/comments/Comments/Add.jsx
@@ -3,16 +3,18 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
-import React, { useCallback, useState } from 'react';
-import { useDispatch } from 'react-redux';
+import React, { useCallback, useState, useRef } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
-import TextareaAutosize from 'react-textarea-autosize';
-import { Button, Form, TextArea } from 'semantic-ui-react';
+import { Mention, MentionsInput } from 'react-mentions';
+import { Button, Form } from 'semantic-ui-react';
import { useClickAwayListener, useDidUpdate, useToggle } from '../../../lib/hooks';
+import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { useEscapeInterceptor, useForm, useNestedRef } from '../../../hooks';
import { isModifierKeyPressed } from '../../../utils/event-helpers';
+import UserAvatar from '../../users/UserAvatar';
import styles from './Add.module.scss';
@@ -21,13 +23,16 @@ const DEFAULT_DATA = {
};
const Add = React.memo(() => {
+ const boardMemberships = useSelector(selectors.selectMembershipsForCurrentBoard);
+
const dispatch = useDispatch();
const [t] = useTranslation();
+ const [data, , setData] = useForm(DEFAULT_DATA);
const [isOpened, setIsOpened] = useState(false);
- const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const [selectTextFieldState, selectTextField] = useToggle();
- const [textFieldRef, handleTextFieldRef] = useNestedRef();
+ const mentionsInputRef = useRef(null);
+ const textFieldRef = useRef(null);
const [buttonRef, handleButtonRef] = useNestedRef();
const submit = useCallback(() => {
@@ -47,6 +52,11 @@ const Add = React.memo(() => {
}, [dispatch, data, setData, selectTextField, textFieldRef]);
const handleEscape = useCallback(() => {
+ if (mentionsInputRef.current.isOpened()) {
+ mentionsInputRef.current.clearSuggestions();
+ return;
+ }
+
setIsOpened(false);
textFieldRef.current.blur();
}, [textFieldRef]);
@@ -62,6 +72,15 @@ const Add = React.memo(() => {
setIsOpened(true);
}, []);
+ const handleFieldChange = useCallback(
+ (_, text) => {
+ setData({
+ text,
+ });
+ },
+ [setData],
+ );
+
const handleFieldKeyDown = useCallback(
(event) => {
if (isModifierKeyPressed(event) && event.key === 'Enter') {
@@ -85,6 +104,16 @@ const Add = React.memo(() => {
handleClickAwayCancel,
);
+ const suggestionRenderer = useCallback(
+ (entry, _, highlightedDisplay) => (
+
+
+ {highlightedDisplay}
+
+ ),
+ [],
+ );
+
useDidUpdate(() => {
if (isOpened) {
activateEscapeInterceptor();
@@ -99,21 +128,39 @@ const Add = React.memo(() => {
return (
(
-
+
+
+
{version}
));
diff --git a/client/src/locales/cs-CZ/login.js b/client/src/locales/cs-CZ/login.js
index 93935e00..adf8e4d4 100644
--- a/client/src/locales/cs-CZ/login.js
+++ b/client/src/locales/cs-CZ/login.js
@@ -12,7 +12,7 @@ export default {
or: 'Nebo',
pageNotFound_title: 'Stránka nenalezena',
password: 'Heslo',
- poweredByPlanka: 'Poháněno technologií PLANKA',
+ poweredByPlanka: 'Poháněno technologií <1>PLANKA1>',
serverConnectionFailed: 'Připojení k serveru selhalo',
unknownError: 'Neznámá chyba, zkuste to později',
usernameAlreadyInUse: 'Uživatelské jméno se již používá',
diff --git a/client/src/locales/de-DE/login.js b/client/src/locales/de-DE/login.js
index e357d0b5..ff2365f5 100644
--- a/client/src/locales/de-DE/login.js
+++ b/client/src/locales/de-DE/login.js
@@ -12,7 +12,7 @@ export default {
or: 'Oder',
pageNotFound_title: 'Seite nicht gefunden',
password: 'Passwort',
- poweredByPlanka: 'Powered by PLANKA',
+ poweredByPlanka: 'Powered by <1>PLANKA1>',
serverConnectionFailed: 'Serververbindung fehlgeschlagen',
unknownError: 'Unbekannter Fehler, bitte später erneut versuchen',
usernameAlreadyInUse: 'Benutzername wird bereits verwendet',
diff --git a/client/src/locales/en-GB/login.js b/client/src/locales/en-GB/login.js
index d6dd10f3..bf478f55 100644
--- a/client/src/locales/en-GB/login.js
+++ b/client/src/locales/en-GB/login.js
@@ -12,7 +12,7 @@ export default {
or: 'Or',
pageNotFound_title: 'Page Not Found',
password: 'Password',
- poweredByPlanka: 'Powered by PLANKA',
+ poweredByPlanka: 'Powered by <1>PLANKA1>',
serverConnectionFailed: 'Server connection failed',
unknownError: 'Unknown error, try again later',
usernameAlreadyInUse: 'Username already in use',
diff --git a/client/src/locales/en-US/login.js b/client/src/locales/en-US/login.js
index d6dd10f3..bf478f55 100644
--- a/client/src/locales/en-US/login.js
+++ b/client/src/locales/en-US/login.js
@@ -12,7 +12,7 @@ export default {
or: 'Or',
pageNotFound_title: 'Page Not Found',
password: 'Password',
- poweredByPlanka: 'Powered by PLANKA',
+ poweredByPlanka: 'Powered by <1>PLANKA1>',
serverConnectionFailed: 'Server connection failed',
unknownError: 'Unknown error, try again later',
usernameAlreadyInUse: 'Username already in use',
diff --git a/client/src/locales/it-IT/login.js b/client/src/locales/it-IT/login.js
index d8aa8290..6c3599c4 100644
--- a/client/src/locales/it-IT/login.js
+++ b/client/src/locales/it-IT/login.js
@@ -12,7 +12,7 @@ export default {
or: 'Oppure',
pageNotFound_title: 'Pagina non trovata',
password: 'Password',
- poweredByPlanka: 'Powered by Planka',
+ poweredByPlanka: 'Powered by <1>PLANKA1>',
serverConnectionFailed: 'Connesione al server fallita',
unknownError: 'Errore sconosciuto, prova ancora',
usernameAlreadyInUse: 'Username già in uso',
diff --git a/client/src/locales/pl-PL/login.js b/client/src/locales/pl-PL/login.js
index 5cdf23f7..1252d272 100644
--- a/client/src/locales/pl-PL/login.js
+++ b/client/src/locales/pl-PL/login.js
@@ -12,7 +12,7 @@ export default {
or: 'Lub',
pageNotFound_title: 'Strona nie znaleziona',
password: 'Hasło',
- poweredByPlanka: 'Powered by PLANKA',
+ poweredByPlanka: 'Powered by <1>PLANKA1>',
serverConnectionFailed: 'Błąd połączenia z serwerem',
unknownError: 'Nieznany błąd, spróbuj ponownie później',
usernameAlreadyInUse: 'Nazwa użytkownika nie jest dostępna',
diff --git a/client/src/locales/ru-RU/login.js b/client/src/locales/ru-RU/login.js
index b1215b05..cd55e333 100644
--- a/client/src/locales/ru-RU/login.js
+++ b/client/src/locales/ru-RU/login.js
@@ -12,7 +12,7 @@ export default {
or: 'Или',
pageNotFound_title: 'Страница не найдена',
password: 'Пароль',
- poweredByPlanka: 'Powered by PLANKA',
+ poweredByPlanka: 'Powered by <1>PLANKA1>',
serverConnectionFailed: 'Не могу подключиться к серверу',
unknownError: 'Что-то пошло не так, попробуйте позже',
usernameAlreadyInUse: 'Имя пользователя уже занято',
From fcf1fc43199368c2e665995f442765a688b73e16 Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Sun, 1 Jun 2025 20:53:35 +0200
Subject: [PATCH 029/108] ref: Little refactoring
---
.../attachments/Attachments/CsvViewer.jsx | 72 ++++++++-----------
client/src/locales/de-DE/core.js | 1 +
client/src/locales/de-DE/markdown-editor.json | 2 +-
client/src/locales/en-GB/markdown-editor.json | 2 +-
client/src/locales/en-US/markdown-editor.json | 2 +-
client/src/locales/pl-PL/markdown-editor.json | 2 +-
client/src/locales/ru-RU/core.js | 1 +
client/src/locales/ru-RU/markdown-editor.json | 4 +-
8 files changed, 37 insertions(+), 49 deletions(-)
diff --git a/client/src/components/attachments/Attachments/CsvViewer.jsx b/client/src/components/attachments/Attachments/CsvViewer.jsx
index dab29ae2..959087f0 100644
--- a/client/src/components/attachments/Attachments/CsvViewer.jsx
+++ b/client/src/components/attachments/Attachments/CsvViewer.jsx
@@ -3,20 +3,19 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
-import React, { useEffect, useState, useCallback, useMemo } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import { Pagination, Table } from 'semantic-ui-react';
import Papa from 'papaparse';
import Frame from 'react-frame-component';
+import { Loader, Pagination, Table } from 'semantic-ui-react';
import styles from './CsvViewer.module.scss';
const ROWS_PER_PAGE = 50;
-/* eslint-disable react/no-array-index-key */
const CsvViewer = React.memo(({ src, className }) => {
- const [csvData, setCsvData] = useState(null);
+ const [rows, setRows] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const frameStyles = useMemo(
@@ -34,7 +33,7 @@ const CsvViewer = React.memo(({ src, className }) => {
[],
);
- const handlePageChange = useCallback((e, { activePage }) => {
+ const handlePageChange = useCallback((_, { activePage }) => {
setCurrentPage(activePage);
}, []);
@@ -49,47 +48,46 @@ const CsvViewer = React.memo(({ src, className }) => {
Papa.parse(text, {
skipEmptyLines: true,
- complete: (results) => {
- const rows = results.data;
- setCsvData({
- rows,
- totalRows: rows.length,
- });
+ complete: ({ data }) => {
+ setRows(data);
},
});
- } catch (err) {
- setCsvData(null);
+ } catch {
+ /* empty */
}
}
fetchFile();
}, [src]);
- if (!csvData) {
- return null;
+ if (rows === null) {
+ return ;
}
- const startIdx = (currentPage - 1) * ROWS_PER_PAGE;
- const endIdx = startIdx + ROWS_PER_PAGE;
- const currentRows = csvData.rows.slice(startIdx, endIdx);
- const totalPages = Math.ceil(csvData.totalRows / ROWS_PER_PAGE);
+ const startIndex = (currentPage - 1) * ROWS_PER_PAGE;
+ const endIndex = startIndex + ROWS_PER_PAGE;
+ const currentRows = rows.slice(startIndex, endIndex);
+ const totalPages = Math.ceil(rows.length / ROWS_PER_PAGE);
- const content = (
-
+ return (
+
{frameStyles.join('')}}
+ className={classNames(styles.wrapper, className)}
+ >
- {csvData.rows[0].map((header, index) => (
- {header}
+ {rows[0].map((cell) => (
+ {cell}
))}
- {currentRows.slice(1).map((row, rowIndex) => (
-
- {row.map((cell, cellIndex) => (
- {cell}
+ {currentRows.slice(1).map((row) => (
+
+ {row.map((cell) => (
+ {cell}
))}
))}
@@ -98,30 +96,18 @@ const CsvViewer = React.memo(({ src, className }) => {
{totalPages > 1 && (
)}
-
- );
-
- return (
- {frameStyles.join('')}}
- className={classNames(styles.wrapper, className)}
- >
- {content}
);
});
-/* eslint-enable react/no-array-index-key */
CsvViewer.propTypes = {
src: PropTypes.string.isRequired,
diff --git a/client/src/locales/de-DE/core.js b/client/src/locales/de-DE/core.js
index a4798104..e1cd381a 100644
--- a/client/src/locales/de-DE/core.js
+++ b/client/src/locales/de-DE/core.js
@@ -1,5 +1,6 @@
import dateFns from 'date-fns/locale/de';
import timeAgo from 'javascript-time-ago/locale/de';
+
import markdownEditor from './markdown-editor.json';
export default {
diff --git a/client/src/locales/de-DE/markdown-editor.json b/client/src/locales/de-DE/markdown-editor.json
index e979d30d..4f2ca91e 100644
--- a/client/src/locales/de-DE/markdown-editor.json
+++ b/client/src/locales/de-DE/markdown-editor.json
@@ -63,7 +63,7 @@
"numbered-list_title": "Nummerierte Liste",
"numbered-list_hint": "1. Dein Text",
"documentation": "Dokumentation",
- "documentation_link": " https://diplodoc.com/docs/en/syntax/"
+ "documentation_link": "https://diplodoc.com/docs/en/syntax/"
},
"menubar": {
"bold": "Fett",
diff --git a/client/src/locales/en-GB/markdown-editor.json b/client/src/locales/en-GB/markdown-editor.json
index 06d3bd10..641c354a 100644
--- a/client/src/locales/en-GB/markdown-editor.json
+++ b/client/src/locales/en-GB/markdown-editor.json
@@ -63,7 +63,7 @@
"numbered-list_title": "Numbered list",
"numbered-list_hint": "1. Your text",
"documentation": "Documentation",
- "documentation_link": " https://diplodoc.com/docs/en/syntax/"
+ "documentation_link": "https://diplodoc.com/docs/en/syntax/"
},
"menubar": {
"bold": "Bold",
diff --git a/client/src/locales/en-US/markdown-editor.json b/client/src/locales/en-US/markdown-editor.json
index 06d3bd10..641c354a 100644
--- a/client/src/locales/en-US/markdown-editor.json
+++ b/client/src/locales/en-US/markdown-editor.json
@@ -63,7 +63,7 @@
"numbered-list_title": "Numbered list",
"numbered-list_hint": "1. Your text",
"documentation": "Documentation",
- "documentation_link": " https://diplodoc.com/docs/en/syntax/"
+ "documentation_link": "https://diplodoc.com/docs/en/syntax/"
},
"menubar": {
"bold": "Bold",
diff --git a/client/src/locales/pl-PL/markdown-editor.json b/client/src/locales/pl-PL/markdown-editor.json
index e71a1de1..3513607c 100644
--- a/client/src/locales/pl-PL/markdown-editor.json
+++ b/client/src/locales/pl-PL/markdown-editor.json
@@ -63,7 +63,7 @@
"numbered-list_title": "Lista numerowana",
"numbered-list_hint": "1. Twój tekst",
"documentation": "Dokumentacja",
- "documentation_link": " https://diplodoc.com/docs/en/syntax/"
+ "documentation_link": "https://diplodoc.com/docs/en/syntax/"
},
"menubar": {
"bold": "Pogrubienie",
diff --git a/client/src/locales/ru-RU/core.js b/client/src/locales/ru-RU/core.js
index 8a99fa58..23a8ebcf 100644
--- a/client/src/locales/ru-RU/core.js
+++ b/client/src/locales/ru-RU/core.js
@@ -1,5 +1,6 @@
import dateFns from 'date-fns/locale/ru';
import timeAgo from 'javascript-time-ago/locale/ru';
+
import markdownEditor from './markdown-editor.json';
export default {
diff --git a/client/src/locales/ru-RU/markdown-editor.json b/client/src/locales/ru-RU/markdown-editor.json
index 0beafbbf..1e78db5b 100644
--- a/client/src/locales/ru-RU/markdown-editor.json
+++ b/client/src/locales/ru-RU/markdown-editor.json
@@ -7,7 +7,7 @@
"bundle": {
"error-title": "Ошибка в редакторе markdown",
"settings_wysiwyg": "Визуальный редактор (wysiwyg)",
- "settings_markup": "Разметка Markdown",
+ "settings_markup": "Разметка markdown",
"markup_placeholder": "Введите разметку markdown..."
},
"codeblock": {
@@ -63,7 +63,7 @@
"numbered-list_title": "Нумерованный список",
"numbered-list_hint": "1. Ваш текст",
"documentation": "Документация",
- "documentation_link": " https://diplodoc.com/docs/en/syntax/"
+ "documentation_link": "https://diplodoc.com/docs/en/syntax/"
},
"menubar": {
"bold": "Жирный",
From 665f9998dc89ffd353f658197bf21e90c1ef47b4 Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Sun, 1 Jun 2025 21:44:14 +0200
Subject: [PATCH 030/108] fix: Prevent auto-updating translation files when key
is missing
---
server/patches/sails+1.5.14.patch | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
create mode 100644 server/patches/sails+1.5.14.patch
diff --git a/server/patches/sails+1.5.14.patch b/server/patches/sails+1.5.14.patch
new file mode 100644
index 00000000..fb950ef9
--- /dev/null
+++ b/server/patches/sails+1.5.14.patch
@@ -0,0 +1,24 @@
+diff --git a/node_modules/sails/lib/hooks/i18n/index.js b/node_modules/sails/lib/hooks/i18n/index.js
+index a0c74ed..582ff66 100644
+--- a/node_modules/sails/lib/hooks/i18n/index.js
++++ b/node_modules/sails/lib/hooks/i18n/index.js
+@@ -108,7 +108,8 @@ module.exports = function(sails) {
+ locales: sails.config.i18n.locales,
+ defaultLocale: sails.config.i18n.defaultLocale,
+ directory: resolvedLocalesDirectory,
+- extension: I18N_LOCALES_FILE_EXTENSION
++ extension: I18N_LOCALES_FILE_EXTENSION,
++ devMode: false
+ });
+
+ // Add all of the i18n prototype methods into this hook.
+@@ -176,7 +177,8 @@ module.exports = function(sails) {
+ defaultLocale: sails.config.i18n.defaultLocale,
+ directory: resolvedLocalesDirectory,
+ extension: I18N_LOCALES_FILE_EXTENSION,
+- request: req
++ request: req,
++ devMode: false
+ });
+
+ // Mix translation capabilities into res.locals.
From e0374f30dbcf1cfe67356461c3a33c607d6d73da Mon Sep 17 00:00:00 2001
From: Jason Jack
Date: Mon, 2 Jun 2025 13:39:10 -0400
Subject: [PATCH 031/108] build: Add tmpfs to compose file (#1165)
---
docker-compose.yml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docker-compose.yml b/docker-compose.yml
index 63ec12f3..a3c55bca 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -7,6 +7,10 @@ services:
- user-avatars:/app/public/user-avatars
- background-images:/app/public/background-images
- attachments:/app/private/attachments
+ # Optionally override this to your user/group
+ # user: 1000:1000
+ # tmpfs:
+ # - /app/.tmp:mode=770,uid=1000,gid=1000
ports:
- 3000:1337
environment:
From a2495b664e342c17c7bdbabd557a32d903146f6c Mon Sep 17 00:00:00 2001
From: Maksim Eltyshev
Date: Mon, 2 Jun 2025 19:44:08 +0200
Subject: [PATCH 032/108] chore: Add missing things
---
client/package-lock.json | 14 +++-----------
client/package.json | 1 -
.../boards/BoardActions/RightSide/ActionsStep.jsx | 4 ++--
client/src/selectors/modals.js | 4 ++++
4 files changed, 9 insertions(+), 14 deletions(-)
diff --git a/client/package-lock.json b/client/package-lock.json
index b9891269..8fb0dd79 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -13,7 +13,6 @@
"@gravity-ui/markdown-editor": "^15.11.0",
"@gravity-ui/uikit": "^7.11.0",
"@juggle/resize-observer": "^3.4.0",
- "@types/papaparse": "^5.3.16",
"@vitejs/plugin-react": "^4.4.1",
"browserslist-to-esbuild": "^2.1.1",
"classnames": "^2.5.1",
@@ -4748,6 +4747,7 @@
"version": "22.15.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz",
"integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==",
+ "devOptional": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -4758,15 +4758,6 @@
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
"dev": true
},
- "node_modules/@types/papaparse": {
- "version": "5.3.16",
- "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.16.tgz",
- "integrity": "sha512-T3VuKMC2H0lgsjI9buTB3uuKj3EMD2eap1MOuEQuBQ44EnDx/IkGhU6EwiTf9zG3za4SKlmwKAImdDKdNnCsXg==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
@@ -14738,7 +14729,8 @@
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "devOptional": true
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.1",
diff --git a/client/package.json b/client/package.json
index c429160a..5823b9a6 100755
--- a/client/package.json
+++ b/client/package.json
@@ -84,7 +84,6 @@
"@gravity-ui/markdown-editor": "^15.11.0",
"@gravity-ui/uikit": "^7.11.0",
"@juggle/resize-observer": "^3.4.0",
- "@types/papaparse": "^5.3.16",
"@vitejs/plugin-react": "^4.4.1",
"browserslist-to-esbuild": "^2.1.1",
"classnames": "^2.5.1",
diff --git a/client/src/components/boards/BoardActions/RightSide/ActionsStep.jsx b/client/src/components/boards/BoardActions/RightSide/ActionsStep.jsx
index f4bb8fe3..ceeb73c2 100755
--- a/client/src/components/boards/BoardActions/RightSide/ActionsStep.jsx
+++ b/client/src/components/boards/BoardActions/RightSide/ActionsStep.jsx
@@ -141,7 +141,7 @@ const ActionsStep = React.memo(({ onClose }) => {
{withTrashEmptier && (
<>
- {(withSubscribe || withCustomFieldGroups) && }
+
{t('action.emptyTrash', {
@@ -151,7 +151,7 @@ const ActionsStep = React.memo(({ onClose }) => {
>
)}
<>
- {(withSubscribe || withTrashEmptier) && }
+
{[BoardContexts.BOARD, BoardContexts.ARCHIVE, BoardContexts.TRASH].map((context) => (
Date: Mon, 2 Jun 2025 20:13:22 +0200
Subject: [PATCH 033/108] chore: Update logo assets
---
client/public/logo192.png | Bin 9621 -> 2560 bytes
client/src/assets/images/logo.png | Bin 25414 -> 28256 bytes
2 files changed, 0 insertions(+), 0 deletions(-)
diff --git a/client/public/logo192.png b/client/public/logo192.png
index ae40bd440e101947e57068e1c9ca5d99e4383ee9..690a1667a60934232c5a3e5289f7851ec1a422c7 100644
GIT binary patch
literal 2560
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
R<{JAMt!Mh`WlyYw{|_6@GED#g
delta 22923
zcmc$_by$_(w=cR-5d{Ge1f&r}Ktf3gL20DBLFsN5xp)yol$4f|?(PO@=?>{wbV=6&
z&g8p)`|P{#-S;{BoImbe{IQV?x_A
z7+&})R~UZ}4M~mG>$yYeRAZC8Cw_TTOyHp_^FKanZ8Q}gLKi7YN}-a#
ze;B%=m@c8wRdI-dYE$jt#$nP3cFBfBt?C&|wga{cwu6L|**d#6{a2)5F&I8eDb~%m
z#+}aF;A`UD{>?A=cYYWIgL#qD;(}ivF#hk){=awx*8jJS{C5}oMZdhfOv-IFTWnym
zx6ra|v^=Ojvh~P_wQ?#UFYv@Q)wSk{*R&cv<5-CX$1W8!0_GrgoBO
zVwF5lw@W8-XdWIpFHJkk9{Cy?$TEb2Qy1UuYo@}rcCMTu}2
zJ5;|hjz(8ZdbqfE9vm4Vc=#6?!#)?Yk3_<)rebyIy(~WgYE)hl*H|=PBQATLtt&e|
zg|8ldtemRXoeS2fWu@TKA8NtHsh;OAmAdo&F*slj23FQa_{a~LILIM2o8}D<8BXoY
zLE-&t0o%XxS)tJQ4;?bE9zQUz?rSm%&hreJxBxPo2g=e0@Y*-exp%)Y$gQp}UoMq=
zG$z8JYN2+8Wu45oN;XVsF1s5r1mBIhHTWTb%jF2<^9_1OS?w;slTR?W9L(r=!=f!?ofVM
zpcfb+P5bC9IQ^n_o|hkTRmH7GWvl3zH`Frxd03Au)$
z#d|`&-OF6Bo+~uplj7_Yp}{+U>@26rTwyhpYY
z&4kN4rhcHMSimC+XzO|pg}5SkG~EillSi~*Fy*)VFnOj&_b*;_J+7x!9mRUf8gYQM
zrB^4xe&I9xO6mDv|PB=`<;fH|DTT-Vw`BsQ5?Tt28tS9ih;fvm=&U
zWy#Z^*@blvmVF~9VjY5er}CHMLe#(5hzjY5vi0E%X$q4{Y8=pWx&zqDyI~l>3>Enx
z(ZJUhkR0&g1A^?&lOUTOZrY|_EWr_m;#Cpw8(!aYIXE~Vu9*qw6+EUhybXH=<{qol)#G>+x;653>g%+mO9PAH>>QpgN!s@3Pg8HqGxGU_W9sH{|0UDK`wZF
zFea?d+rRGU>IJ`cogfbeFTUc2+p;v6{w{f(yW+{ksxT#;I9o3rNN%U5`$~UQE>|#t
zFXTFD#yYdtLNydd^$ctU|1}v-J9$|*Di
zIfa0S;hF#Au*P${KWvP3Q*7L+dWS8Ku0j@Ue;dV=pg~^5VATDmp)V2lDW>b)`!gL@
zamnp}GKfBMx$KQuzV8OnNA&z;cb<4g^ok`c4P6hawHV(;i3=qsjYk`lPwk?;B?H=P
zQLLVqQm4JI7Q)YamWD59I$edg^z?q7Zs~D)w4UVt=Z?hq$wHgN^P|mIFT&CcI)Y_9
z3`^$UL6db?{`>LM{7U|JU<|-kycF$d+ut8DRet^3$KmvE=!*i1v&nAOGRr^Sc1??_
z1t_z_pe64n!xo8v0NfA~#L{MAoqVP&xiLCEJp;p&H}L77Z`@lOF@3#p8e2BIKk6~U
zB!Ic~!s{Y)6nSv^qE)T@@Al-;1v(KFO7Q__xp?+DCh5_GGfpyS>rgzQ3_n
zzAec^VFv!Xk5cEUmu&m8v$B4$IZd(;CvJ|FO98{-pg0#pO;scm9;jGO&Shm
zKK&$kH31D*X=zoMzDj}`xvVr-k;Jp@5AWTnu$pVIT}WYWyo}oKjD#|V9zMg(6qY&t
z&5)PBr~>qq@!PmHwve8oZ+M>GF*1fTuPBF%*`}HC%6*(zlG=ntaA*#yCS(;%p%l6Mmyh*;%
zU%8`sk=^j6THE^Zu;{=Z8^MD+w>){|tmhgoN7Zx&H_McuSWc^y@icWmTuO~{x8CQa
zt1KEdOoI+jLnU6$