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 klicken 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>Apprise, 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}}<1> hat diese Karte hinzugefügt zu {{list}}', 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": "![Dein Text](URL)", + "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}}<1> added this card to {{list}}', + userAddedUserToThisCard: '<0>{{actorUser}}<1> added {{addedUser}} to this card', + userAddedYouToCard: '{{user}} added you to <2>{{card}}', + userJoinedThisCard: `<0>{{user}}<1> joined this card`, userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}', userMovedCardFromListToList: '{{user}} moved <2>{{card}} 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}}<1> added this card to {{list}}', + userAddedUserToThisCard: '<0>{{actorUser}}<1> added {{addedUser}} to this card', + userAddedYouToCard: '{{user}} added you to <2>{{card}}', + userJoinedThisCard: `<0>{{user}}<1> joined this card`, userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}', userMovedCardFromListToList: '{{user}} moved <2>{{card}} 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}}', userJoinedThisCard: `<0>{{user}}<1> joined this card`, userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}', + userLeftThisCard: '<0>{{user}}<1> left this card', userMovedCardFromListToList: '{{user}} moved <2>{{card}} from {{fromList}} to {{toList}}', userMovedThisCardFromListToList: '<0>{{user}}<1> moved this card from {{fromList}} to {{toList}}', + userRemovedUserFromThisCard: + '<0>{{actorUser}}<1> 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 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}}', userJoinedThisCard: `<0>{{user}}<1> joined this card`, userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}', + userLeftThisCard: '<0>{{user}}<1> left this card', userMovedCardFromListToList: '{{user}} moved <2>{{card}} from {{fromList}} to {{toList}}', userMovedThisCardFromListToList: '<0>{{user}}<1> moved this card from {{fromList}} to {{toList}}', + userRemovedUserFromThisCard: + '<0>{{actorUser}}<1> removed {{removedUser}} from this card', 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** -![Version](https://img.shields.io/github/package-json/v/plankanban/planka?style=flat-square) [![Docker Pulls](https://img.shields.io/badge/docker_pulls-5M%2B-%23066da5?style=flat-square&color=red)](https://github.com/plankanban/planka/pkgs/container/planka) [![Contributors](https://img.shields.io/github/contributors/plankanban/planka?style=flat-square&color=blue)](https://github.com/plankanban/planka/graphs/contributors) [![Chat](https://img.shields.io/discord/1041440072953765979?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/WqqYNd7Jvt) +![Version](https://img.shields.io/github/package-json/v/plankanban/planka?style=flat-square) [![Docker Pulls](https://img.shields.io/badge/docker_pulls-6M%2B-%23066da5?style=flat-square&color=red)](https://github.com/plankanban/planka/pkgs/container/planka) [![Contributors](https://img.shields.io/github/contributors/plankanban/planka?style=flat-square&color=blue)](https://github.com/plankanban/planka/graphs/contributors) [![Chat](https://img.shields.io/discord/1041440072953765979?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/WqqYNd7Jvt) ![Demo](https://raw.githubusercontent.com/plankanban/planka/master/assets/demo.gif) 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}}<1> added this card to {{list}}', userAddedUserToThisCard: '<0>{{actorUser}}<1> added {{addedUser}} to this card', userAddedYouToCard: '{{user}} added you to <2>{{card}}', + userCompletedTaskOnThisCard: '<0>{{user}}<1> completed {{task}} on this card', userJoinedThisCard: `<0>{{user}}<1> joined this card`, userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}', userLeftThisCard: '<0>{{user}}<1> left this card', + userMarkedTaskIncompleteOnThisCard: + '<0>{{user}}<1> marked {{task}} incomplete on this card', userMovedCardFromListToList: '{{user}} moved <2>{{card}} from {{fromList}} to {{toList}}', userMovedThisCardFromListToList: '<0>{{user}}<1> moved this card from {{fromList}} to {{toList}}', 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}}<1> added this card to {{list}}', userAddedUserToThisCard: '<0>{{actorUser}}<1> added {{addedUser}} to this card', userAddedYouToCard: '{{user}} added you to <2>{{card}}', + userCompletedTaskOnThisCard: '<0>{{user}}<1> completed {{task}} on this card', userJoinedThisCard: `<0>{{user}}<1> joined this card`, userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}', userLeftThisCard: '<0>{{user}}<1> left this card', + userMarkedTaskIncompleteOnThisCard: + '<0>{{user}}<1> marked {{task}} incomplete on this card', userMovedCardFromListToList: '{{user}} moved <2>{{card}} from {{fromList}} to {{toList}}', userMovedThisCardFromListToList: '<0>{{user}}<1> moved this card from {{fromList}} to {{toList}}', 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}
+ + + +