Compare commits

...

24 Commits

Author SHA1 Message Date
Hoang Pham
50f35c7880 Fix autosave focus jump in card sidebar
Signed-off-by: Hoang Pham <hoangmaths96@gmail.com>
2025-09-22 18:57:20 +07:00
Nextcloud bot
12b656dd8c fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-22 00:31:30 +00:00
github-actions[bot]
661eea3018 Merge pull request #7250 from nextcloud/automated/noid/main-update-nextcloud-ocp
[main] Update nextcloud/ocp dependency
2025-09-21 03:31:25 +00:00
nextcloud-command
ecd3cb42de chore(dev-deps): Bump nextcloud/ocp package
Signed-off-by: GitHub <noreply@github.com>
2025-09-21 02:40:50 +00:00
github-actions[bot]
f49c8f6ee4 Merge pull request #7231 from nextcloud/automated/noid/main-update-nextcloud-ocp
[main] Update nextcloud/ocp dependency
2025-09-19 17:09:19 +00:00
nextcloud-command
96b56a2447 chore(dev-deps): Bump nextcloud/ocp package
Signed-off-by: GitHub <noreply@github.com>
2025-09-19 19:03:21 +02:00
Nextcloud bot
26badb58dd fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-19 00:32:01 +00:00
Nextcloud bot
04d9433bdd fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-18 00:31:43 +00:00
Nextcloud bot
a69eb654fd fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-17 00:31:43 +00:00
Elizabeth Danzberger
ab5ccb7bc1 Merge pull request #7238 from nextcloud/bugfix/noid/activity-icons
fix(darkmode): Fix activity icon colors
2025-09-16 14:57:18 -04:00
Luka Trovic
f054cc2fbd Merge pull request #7225 from ABartelt/fix/mysql-error-1093-user-deletion
fix: resolve MySQL Error 1093 when deleting users from boards
2025-09-16 20:43:16 +02:00
Luka Trovic
1afc5cdbcc Merge pull request #7237 from nextcloud/bugfix/7207/fix-colors
fix: Fix colors from due dates and done
2025-09-16 20:16:18 +02:00
Joas Schilling
1a6e5929b2 fix(darkmode): Fix activity icon colors
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-09-16 15:17:54 +02:00
Joas Schilling
74a3ab5008 fix: Fix colors from due dates and done
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-09-16 14:42:54 +02:00
Luka Trovic
0ebd05e649 Merge pull request #7210 from nextcloud/style/noid/deleteMaterialSymbolOutline
Migrate delete icon to Material Symbol outline variant
2025-09-16 13:43:08 +02:00
Luka Trovic
3611c8f8ac Merge pull request #7159 from Somebodyisnobody/patch-1
Docs: Updating docs for API endpoints for uploading attachments
2025-09-16 10:31:31 +02:00
Luka Trovic
da3b857ab0 Merge pull request #7165 from nextcloud/get-cards-at-once
perf(cards): fetch all cards at once
2025-09-16 10:31:00 +02:00
Nextcloud bot
199b698cb7 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-16 00:31:49 +00:00
Nextcloud bot
fb1132652a fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-15 00:31:00 +00:00
Nextcloud bot
f78c3d42df fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-14 00:30:54 +00:00
Arne Bartelt
bffa4d0925 fix: resolve MySQL Error 1093 when deleting users from boards
Fixes #7125 and #7069 by implementing a two-step deletion process
that avoids MySQL's restriction on deleting from a table while
selecting from it in a subquery.

The fix separates the SELECT and DELETE operations:
1. First query: Get card IDs for assignments to delete
2. Second query: Delete assignments using the collected IDs

This approach works on all supported database systems (MySQL 5.7+,
MySQL 8.0+, MariaDB 10.x+) and follows MySQL's official best practices
for handling Error 1093: 'You can't specify target table for update in FROM clause'.

The issue occurred because the original deleteByParticipantOnBoard method
used a subquery that referenced the same table being deleted from,
which MySQL prohibits but MariaDB allows (explaining why it worked
in development but failed in production).

Signed-off-by: Arne Bartelt <arne.bartelt@gmail.com>
Signed-off-by: Arne Bartelt <Arne.Bartelt@gmail.com>
2025-09-10 16:39:51 +02:00
Andy Scherzinger
44d244f9aa style(icon): Migrate delete icon to Material Symbol outline variant
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-09-05 18:26:28 +02:00
Carl Schwan
a3fa72341d perf(cards): fetch all cards at once
Instead of one by one

Signed-off-by: Carl Schwan <carl.schwan@nextclound.com>
2025-08-13 16:01:52 +02:00
Somebodyisnobody
152181ff67 docs: Update API docs
Clarifying documentation for the attachment upload endpoints.

Signed-off-by: Somebodyisnobody <35230554+Somebodyisnobody@users.noreply.github.com>
2025-08-07 11:43:42 +02:00
30 changed files with 160 additions and 93 deletions

View File

@@ -6,6 +6,7 @@
- Adrian Missy <adrian.missy@onewavestudios.com>
- Alexandru Puiu <alexpuiu20@yahoo.com>
- Arne Bartelt <arne.bartelt@gmail.com>
- Chandi Langecker <git@chandi.it>
- Christoph Wurst <christoph@winzerhof-wurst.at>
- Gary Kim <gary@garykim.dev>

8
composer.lock generated
View File

@@ -380,12 +380,12 @@
"source": {
"type": "git",
"url": "https://github.com/nextcloud-deps/ocp.git",
"reference": "6a5219dda0583a45fb5541719de83c9b673b3efa"
"reference": "d927392a2a368c372ef80096171139d4287b2339"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/6a5219dda0583a45fb5541719de83c9b673b3efa",
"reference": "6a5219dda0583a45fb5541719de83c9b673b3efa",
"url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/d927392a2a368c372ef80096171139d4287b2339",
"reference": "d927392a2a368c372ef80096171139d4287b2339",
"shasum": ""
},
"require": {
@@ -421,7 +421,7 @@
"issues": "https://github.com/nextcloud-deps/ocp/issues",
"source": "https://github.com/nextcloud-deps/ocp/tree/master"
},
"time": "2025-09-06T00:45:32+00:00"
"time": "2025-09-16T00:45:42+00:00"
},
{
"name": "nikic/php-parser",

View File

@@ -6,7 +6,7 @@ The REST API provides access for authenticated users to their data inside the De
# Prerequisites
- All requests require a `OCS-APIRequest` HTTP header to be set to `true` and a `Content-Type` of `application/json`.
- All requests require a `OCS-APIRequest` HTTP header to be set to `true` and a `Content-Type` of `application/json`. This does not apply to the endpoint for uploading attachments, which consumes `multipart/form-data`.
- The API is located at https://nextcloud.local/index.php/apps/deck/api/v1.0
- All request parameters are required, unless otherwise specified
@@ -733,6 +733,7 @@ The board list endpoint supports setting an `If-Modified-Since` header to limit
| Parameter | Type | Description |
| --------- | ------- | --------------------------------------- |
| labelId | Integer | The label id to assign to the card |
#### Response
##### 200 Success
@@ -997,10 +998,12 @@ The request can fail with a bad request response for the following reasons:
#### Request data
| Parameter | Type | Description |
| --------- | ------- | --------------------------------------------- |
| type | String | The type of the attachement |
| file | Binary | File data to add as an attachment |
The request is performed as `multipart/form-data`.
| Parameter | Type | Description |
| --------- | ------- | ----------------------------------------------------------------------------------------------- |
| type | String | The type of the attachement. Use `file` or `deck_file`. |
| file | Binary | File data to add as an attachment together with the `filename` parameter according to RFC 7578. |
- Prior to Deck version v1.3.0 (API v1.0), attachments were stored within deck. For this type of attachments `deck_file` was used as the default type of attachments
- Starting with Deck version 1.3.0 (API v1.1) files are stored within the users regular Nextcloud files and the type `file` has been introduced for that
@@ -1022,12 +1025,13 @@ The request can fail with a bad request response for the following reasons:
#### Request data
| Parameter | Type | Description |
| --------- | ------- | --------------------------------------------- |
| type | String | The type of the attachement |
| file | Binary | File data to add as an attachment |
The request is performed as `multipart/form-data`.
| Parameter | Type | Description |
| --------- | ------- | ----------------------------------------------------------------------------------------------- |
| type | String | The type of the attachement. For now only `deck_file` is supported as an attachment type. |
| file | Binary | File data to add as an attachment together with the `filename` parameter according to RFC 7578. |
For now only `deck_file` is supported as an attachment type.
#### Response

View File

@@ -13,6 +13,7 @@ OC.L10N.register(
"Done" : "Гатова",
"File" : "Файл",
"Cancel" : "Скасаваць",
"Drop your files to upload" : "Перацягніце файлы для запампоўвання",
"File already exists" : "Файл ужо існуе",
"A file with the name {filename} already exists." : "Файл з назвай {filename} ужо існуе.",
"Do you want to overwrite it?" : "Хочаце перазапісаць яго?",
@@ -56,13 +57,18 @@ OC.L10N.register(
"Keyboard shortcuts" : "Спалучэнні клавіш",
"Keyboard shortcut" : "Спалучэнне клавіш",
"Action" : "Дзеянне",
"Shift" : "Shift",
"Search" : "Пошук",
"Enter" : "Enter",
"Shared with you" : "Абагулена з вамі",
"Cancel edit" : "Скасаваць рэдагаванне",
"An error occurred" : "Узнікла памылка",
"No notifications" : "Няма апавяшчэнняў",
"Export" : "Экспарт",
"No results found" : "Вынікаў не знойдзена",
"{stack} in {board}" : "{stack} у {board}",
"Close" : "Закрыць",
"Message from {author} in {conversationName}" : "Паведамленне ад {author} у {conversationName}",
"Failed to upload {name}" : "Не ўдалося запампаваць {name}",
"Share" : "Абагуліць",
"Today" : "Сёння",

View File

@@ -11,6 +11,7 @@
"Done" : "Гатова",
"File" : "Файл",
"Cancel" : "Скасаваць",
"Drop your files to upload" : "Перацягніце файлы для запампоўвання",
"File already exists" : "Файл ужо існуе",
"A file with the name {filename} already exists." : "Файл з назвай {filename} ужо існуе.",
"Do you want to overwrite it?" : "Хочаце перазапісаць яго?",
@@ -54,13 +55,18 @@
"Keyboard shortcuts" : "Спалучэнні клавіш",
"Keyboard shortcut" : "Спалучэнне клавіш",
"Action" : "Дзеянне",
"Shift" : "Shift",
"Search" : "Пошук",
"Enter" : "Enter",
"Shared with you" : "Абагулена з вамі",
"Cancel edit" : "Скасаваць рэдагаванне",
"An error occurred" : "Узнікла памылка",
"No notifications" : "Няма апавяшчэнняў",
"Export" : "Экспарт",
"No results found" : "Вынікаў не знойдзена",
"{stack} in {board}" : "{stack} у {board}",
"Close" : "Закрыць",
"Message from {author} in {conversationName}" : "Паведамленне ад {author} у {conversationName}",
"Failed to upload {name}" : "Не ўдалося запампаваць {name}",
"Share" : "Абагуліць",
"Today" : "Сёння",

View File

@@ -373,6 +373,7 @@ OC.L10N.register(
"Note: Only the JSON format is supported for importing back into the Deck app." : "Pozn.: Pro import zpět do aplikace Deck je podporován pouze formát JSON.",
"Export" : "Exportovat",
"Loading filtered view" : "Načítání filtrovaného pohledu",
"Search for {searchQuery} in other boards" : "Hledat {searchQuery} v ostatních tabulích",
"Search for {searchQuery} in all boards" : "Hledat {searchQuery} na všech tabulích",
"No results found" : "Nenalezeny žádné výsledky",
"Deck board {name}\n* Last modified on {lastMod}" : "Deck karta {name}\n* Naposledy změněno {lastMod}",

View File

@@ -371,6 +371,7 @@
"Note: Only the JSON format is supported for importing back into the Deck app." : "Pozn.: Pro import zpět do aplikace Deck je podporován pouze formát JSON.",
"Export" : "Exportovat",
"Loading filtered view" : "Načítání filtrovaného pohledu",
"Search for {searchQuery} in other boards" : "Hledat {searchQuery} v ostatních tabulích",
"Search for {searchQuery} in all boards" : "Hledat {searchQuery} na všech tabulích",
"No results found" : "Nenalezeny žádné výsledky",
"Deck board {name}\n* Last modified on {lastMod}" : "Deck karta {name}\n* Naposledy změněno {lastMod}",

View File

@@ -272,6 +272,7 @@ OC.L10N.register(
"{count} comments, {unread} unread" : "{count} σχόλια, {unread} μη αναγνωσμένα",
"Todo items" : "Στοιχεία todo",
"Edit card title" : "Επεξεργασία τίτλου κάρτας",
"Open link" : "Άνοιγμα συνδέσμου",
"Card deleted" : "Η καρτέλα διαγράφηκε",
"Edit title" : "Επεξεργασία τίτλου",
"Assign to me" : "Ανάθεση σε εμένα",

View File

@@ -270,6 +270,7 @@
"{count} comments, {unread} unread" : "{count} σχόλια, {unread} μη αναγνωσμένα",
"Todo items" : "Στοιχεία todo",
"Edit card title" : "Επεξεργασία τίτλου κάρτας",
"Open link" : "Άνοιγμα συνδέσμου",
"Card deleted" : "Η καρτέλα διαγράφηκε",
"Edit title" : "Επεξεργασία τίτλου",
"Assign to me" : "Ανάθεση σε εμένα",

View File

@@ -129,7 +129,7 @@ OC.L10N.register(
"Path is already shared with this card" : "A ruta xa está compartida con esta tarxeta",
"Invalid date, date format must be YYYY-MM-DD" : "Data incorrecta, o formato da data debe ser AAAA-MM-DD",
"Personal planning and team project organization" : "Planificación persoal e organización de proxectos de equipo",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in Markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your Markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Gabeta é unha ferramenta de organización de estilo kanban dirixida a planificación persoal e organización de proxectos para equipos integrados con Nextcloud. \n\n\n 📥 Engada as súas tarefas ás tarxetas e fagas ordenadas\n 📄 Escriba notas adicionais en Markdown\n 🔖 Asigne etiquetas para unha mellor organización\n 👥 Comparta co seu equipo, amigos ou a súa familia\n 📎 Anexe ficheiros e integreos na súa descrición de Markdown\n 💬 Debata co seu equipo usando os comentarios\n ⚡ Faga un seguimento dos cambios no fluxo de actividade\n 🚀 Teña o seu proxecto organizado",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in Markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your Markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Gabeta é unha ferramenta de organización de estilo kanban dirixida a planificación persoal e organización de proxectos para equipos integrados con Nextcloud. \n\n\n-- 📥 Engada as súas tarefas ás tarxetas e fagas ordenadas\n- 📄 Escriba notas adicionais en Markdown\n- 🔖 Asigne etiquetas para unha mellor organización\n- 👥 Comparta co seu equipo, amigos ou a súa familia\n- 📎 Anexe ficheiros e integreos na súa descrición de Markdown\n- 💬 Debata co seu equipo usando os comentarios\n- ⚡ Faga un seguimento dos cambios no fluxo de actividade\n- 🚀 Teña o seu proxecto organizado",
"Add board" : "Engadir taboleiro",
"Card details" : "Detalles da tarxeta",
"Select the board to link to a project" : "Seleccione o taboleiro para ligar a un proxecto",

View File

@@ -127,7 +127,7 @@
"Path is already shared with this card" : "A ruta xa está compartida con esta tarxeta",
"Invalid date, date format must be YYYY-MM-DD" : "Data incorrecta, o formato da data debe ser AAAA-MM-DD",
"Personal planning and team project organization" : "Planificación persoal e organización de proxectos de equipo",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in Markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your Markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Gabeta é unha ferramenta de organización de estilo kanban dirixida a planificación persoal e organización de proxectos para equipos integrados con Nextcloud. \n\n\n 📥 Engada as súas tarefas ás tarxetas e fagas ordenadas\n 📄 Escriba notas adicionais en Markdown\n 🔖 Asigne etiquetas para unha mellor organización\n 👥 Comparta co seu equipo, amigos ou a súa familia\n 📎 Anexe ficheiros e integreos na súa descrición de Markdown\n 💬 Debata co seu equipo usando os comentarios\n ⚡ Faga un seguimento dos cambios no fluxo de actividade\n 🚀 Teña o seu proxecto organizado",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in Markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your Markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Gabeta é unha ferramenta de organización de estilo kanban dirixida a planificación persoal e organización de proxectos para equipos integrados con Nextcloud. \n\n\n-- 📥 Engada as súas tarefas ás tarxetas e fagas ordenadas\n- 📄 Escriba notas adicionais en Markdown\n- 🔖 Asigne etiquetas para unha mellor organización\n- 👥 Comparta co seu equipo, amigos ou a súa familia\n- 📎 Anexe ficheiros e integreos na súa descrición de Markdown\n- 💬 Debata co seu equipo usando os comentarios\n- ⚡ Faga un seguimento dos cambios no fluxo de actividade\n- 🚀 Teña o seu proxecto organizado",
"Add board" : "Engadir taboleiro",
"Card details" : "Detalles da tarxeta",
"Select the board to link to a project" : "Seleccione o taboleiro para ligar a un proxecto",

View File

@@ -373,6 +373,7 @@ OC.L10N.register(
"Note: Only the JSON format is supported for importing back into the Deck app." : "Uwaga: tylko format JSON jest obsługiwany przy imporcie z powrotem do aplikacji Deck.",
"Export" : "Eksportuj",
"Loading filtered view" : "Wczytywanie przefiltrowanego widoku",
"Search for {searchQuery} in other boards" : "Szukaj {searchQuery} na innych tablicach",
"Search for {searchQuery} in all boards" : "Wyszukaj dla {searchQuery} na wszystkich tablicach",
"No results found" : "Nie znaleziono wyników",
"Deck board {name}\n* Last modified on {lastMod}" : "Tablica {name}\n* Ostatnia modyfikacja w dniu {lastMod}",

View File

@@ -371,6 +371,7 @@
"Note: Only the JSON format is supported for importing back into the Deck app." : "Uwaga: tylko format JSON jest obsługiwany przy imporcie z powrotem do aplikacji Deck.",
"Export" : "Eksportuj",
"Loading filtered view" : "Wczytywanie przefiltrowanego widoku",
"Search for {searchQuery} in other boards" : "Szukaj {searchQuery} na innych tablicach",
"Search for {searchQuery} in all boards" : "Wyszukaj dla {searchQuery} na wszystkich tablicach",
"No results found" : "Nie znaleziono wyników",
"Deck board {name}\n* Last modified on {lastMod}" : "Tablica {name}\n* Ostatnia modyfikacja w dniu {lastMod}",

View File

@@ -373,7 +373,8 @@ OC.L10N.register(
"Note: Only the JSON format is supported for importing back into the Deck app." : "Not: Yeniden Tahta uygulaması içine aktarmak için yalnızca JSON biçimi desteklenir.",
"Export" : "Dışa aktar",
"Loading filtered view" : "Süzülmüş görünüm yükleniyor",
"Search for {searchQuery} in all boards" : "Tüm panolarda {searchQuery} araması için sonuçlar",
"Search for {searchQuery} in other boards" : "Diğer panolarda {searchQuery} ara",
"Search for {searchQuery} in all boards" : "Tüm panolarda {searchQuery} ara",
"No results found" : "Herhangi bir sonuç bulunamadı",
"Deck board {name}\n* Last modified on {lastMod}" : "{name} tahta panosu\n* Son değişiklik: {lastMod}",
"* Created on {created}\n* Last modified on {lastMod}\n* {nbAttachments} attachments\n* {nbComments} comments" : "* Oluşturulma: {created}\n* Son değiştirilme: {lastMod}\n* {nbAttachments} ek dosya\n* {nbComments} yorum",

View File

@@ -371,7 +371,8 @@
"Note: Only the JSON format is supported for importing back into the Deck app." : "Not: Yeniden Tahta uygulaması içine aktarmak için yalnızca JSON biçimi desteklenir.",
"Export" : "Dışa aktar",
"Loading filtered view" : "Süzülmüş görünüm yükleniyor",
"Search for {searchQuery} in all boards" : "Tüm panolarda {searchQuery} araması için sonuçlar",
"Search for {searchQuery} in other boards" : "Diğer panolarda {searchQuery} ara",
"Search for {searchQuery} in all boards" : "Tüm panolarda {searchQuery} ara",
"No results found" : "Herhangi bir sonuç bulunamadı",
"Deck board {name}\n* Last modified on {lastMod}" : "{name} tahta panosu\n* Son değişiklik: {lastMod}",
"* Created on {created}\n* Last modified on {lastMod}\n* {nbAttachments} attachments\n* {nbComments} comments" : "* Oluşturulma: {created}\n* Son değiştirilme: {lastMod}\n* {nbAttachments} ek dosya\n* {nbComments} yorum",

View File

@@ -373,6 +373,7 @@ OC.L10N.register(
"Note: Only the JSON format is supported for importing back into the Deck app." : "Примітка: Для імпорту в додаток Deck підтримується лише формат JSON.",
"Export" : "Експортувати",
"Loading filtered view" : "Завантаження відфільтрованого перегляду",
"Search for {searchQuery} in other boards" : "Шукати {searchQuery} на інших дошках",
"Search for {searchQuery} in all boards" : "Шукати {searchQuery} на всіх дошках оголошень",
"No results found" : "Не знайдено жодного результату",
"Deck board {name}\n* Last modified on {lastMod}" : "Колода {name}\n* Востаннє змінено на {lastMod}",

View File

@@ -371,6 +371,7 @@
"Note: Only the JSON format is supported for importing back into the Deck app." : "Примітка: Для імпорту в додаток Deck підтримується лише формат JSON.",
"Export" : "Експортувати",
"Loading filtered view" : "Завантаження відфільтрованого перегляду",
"Search for {searchQuery} in other boards" : "Шукати {searchQuery} на інших дошках",
"Search for {searchQuery} in all boards" : "Шукати {searchQuery} на всіх дошках оголошень",
"No results found" : "Не знайдено жодного результату",
"Deck board {name}\n* Last modified on {lastMod}" : "Колода {name}\n* Востаннє змінено на {lastMod}",

View File

@@ -55,6 +55,7 @@ OC.L10N.register(
"Edit title" : "Sarlavhani tahrirlash",
"Delete card" : "Kartani o'chirish",
"seconds ago" : "seconds ago",
"Keyboard shortcuts" : "Klaviatura yorliqlari",
"Search" : "Qidirish",
"Archived boards" : "Arxivlangan taxtalar",
"Shared with you" : "Shared with you",

View File

@@ -53,6 +53,7 @@
"Edit title" : "Sarlavhani tahrirlash",
"Delete card" : "Kartani o'chirish",
"seconds ago" : "seconds ago",
"Keyboard shortcuts" : "Klaviatura yorliqlari",
"Search" : "Qidirish",
"Archived boards" : "Arxivlangan taxtalar",
"Shared with you" : "Shared with you",

View File

@@ -77,18 +77,33 @@ class AssignmentMapper extends DeckMapper implements IPermissionMapper {
}
public function deleteByParticipantOnBoard(string $participant, int $boardId, $type = Assignment::TYPE_USER) {
$qb = $this->db->getQueryBuilder();
// Step 1: Get all card IDs for the board that have assignments for this participant
// This avoids MySQL Error 1093 by separating the SELECT from the DELETE operation
$cardIdQuery = $this->db->getQueryBuilder();
$cardIdQuery->select('a.card_id')
->from('deck_assigned_users', 'a')
->innerJoin('a', 'deck_cards', 'c', 'c.id = a.card_id')
->innerJoin('c', 'deck_stacks', 's', 's.id = c.stack_id')
->where($cardIdQuery->expr()->eq('a.participant', $qb->createNamedParameter($participant, IQueryBuilder::PARAM_STR)))
->andWhere($cardIdQuery->expr()->eq('s.board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
->andWhere($cardIdQuery->expr()->eq('a.type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
$qb->delete('deck_assigned_users')
->where($qb->expr()->in('card_id', $qb->createFunction($cardIdQuery->getSQL()), IQueryBuilder::PARAM_INT_ARRAY));
$qb->executeStatement();
->where($cardIdQuery->expr()->eq('a.participant', $cardIdQuery->createNamedParameter($participant, IQueryBuilder::PARAM_STR)))
->andWhere($cardIdQuery->expr()->eq('s.board_id', $cardIdQuery->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
->andWhere($cardIdQuery->expr()->eq('a.type', $cardIdQuery->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
$result = $cardIdQuery->executeQuery();
$cardIds = [];
while ($row = $result->fetch()) {
$cardIds[] = $row['card_id'];
}
$result->closeCursor();
// Step 2: If we have card IDs, delete the assignments
if (!empty($cardIds)) {
$deleteQuery = $this->db->getQueryBuilder();
$deleteQuery->delete('deck_assigned_users')
->where($deleteQuery->expr()->eq('participant', $deleteQuery->createNamedParameter($participant, IQueryBuilder::PARAM_STR)))
->andWhere($deleteQuery->expr()->eq('type', $deleteQuery->createNamedParameter($type, IQueryBuilder::PARAM_INT)))
->andWhere($deleteQuery->expr()->in('card_id', $deleteQuery->createNamedParameter($cardIds, IQueryBuilder::PARAM_INT_ARRAY)));
$deleteQuery->executeStatement();
}
}

View File

@@ -131,7 +131,11 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $card;
}
public function findAll($stackId, $limit = null, $offset = null, $since = -1) {
/**
* @return Card[]
* @throws \OCP\DB\Exception
*/
public function findAll($stackId, ?int $limit = null, ?int $offset = null, int $since = -1) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_cards')
@@ -146,6 +150,32 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
/**
* @param int[] $stackIds
* @return array<int, null|Card[]>
* @throws \OCP\DB\Exception
*/
public function findAllForStacks(array $stackIds, ?int $limit = null, ?int $offset = null, int $since = -1): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_cards')
->where($qb->expr()->in('stack_id', $qb->createNamedParameter($stackIds, IQueryBuilder::PARAM_INT_ARRAY)))
->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->gt('last_modified', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT)))
->setMaxResults($limit)
->setFirstResult($offset)
->orderBy('order')
->addOrderBy('id');
$rawCards = $this->findEntities($qb);
$cards = array_fill_keys($stackIds, null);
foreach ($rawCards as $card) {
$cards[$card->getStackId()][] = $card;
}
return $cards;
}
public function queryCardsByBoard(int $boardId): IQueryBuilder {
$qb = $this->db->getQueryBuilder();
$qb->select('c.*')

View File

@@ -15,6 +15,7 @@ use Sabre\VObject\Component\VCalendar;
* @method int getDeletedAt()
* @method int getLastModified()
* @method int getOrder()
* @method Card[] getCards()
*/
class Stack extends RelationalEntity {
protected $title;

View File

@@ -77,12 +77,10 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
/**
* @param numeric $boardId
* @param int|null $limit
* @param int|null $offset
* @return Stack[]
* @throws \OCP\DB\Exception
*/
public function findAll($boardId, $limit = null, $offset = null): array {
public function findAll($boardId, ?int $limit = null, ?int $offset = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())

View File

@@ -75,19 +75,22 @@ class StackService {
$this->stackServiceValidator = $stackServiceValidator;
}
private function enrichStackWithCards($stack, $since = -1) {
$cards = $this->cardMapper->findAll($stack->getId(), null, null, $since);
/** @param Stack[] $stacks */
private function enrichStacksWithCards(array $stacks, $since = -1): void {
$cardsByStackId = $this->cardMapper->findAllForStacks(array_map(fn (Stack $stack) => $stack->getId(), $stacks), null, null, $since);
if (\count($cards) === 0) {
return;
}
foreach ($cardsByStackId as $stackId => $cards) {
if (!$cards) {
continue;
}
$stack->setCards($this->cardService->enrichCards($cards));
}
private function enrichStacksWithCards($stacks, $since = -1) {
foreach ($stacks as $stack) {
$this->enrichStackWithCards($stack, $since);
foreach ($stacks as $stack) {
if ($stack->getId() === $stackId) {
$stack->setCards($this->cardService->enrichCards($cards));
break;
}
}
}
}
@@ -124,9 +127,9 @@ class StackService {
}
/**
* @param $boardId
* @param mixed $boardId
*
* @return array
* @return Stack[]
* @throws \OCA\Deck\NoPermissionException
* @throws BadRequestException
*/
@@ -247,7 +250,7 @@ class StackService {
);
$this->changeHelper->boardChanged($stack->getBoardId());
$this->eventDispatcher->dispatchTyped(new BoardUpdatedEvent($stack->getBoardId()));
$this->enrichStackWithCards($stack);
$this->enrichStacksWithCards([$stack]);
return $stack;
}

View File

@@ -6,7 +6,7 @@
<template>
<div v-if="activity" class="activity">
<div class="activity--header">
<img :src="activity.icon" class="activity--icon">
<img :src="activity.icon" class="activity--icon" :class="applyMonochromeIconColor">
<NcRichText class="activity--subject" :text="message.subject" :arguments="message.parameters" />
<div class="activity--timestamp" :name="formatReadableDate(activity.datetime)">
{{ relativeDate(activity.datetime) }}
@@ -94,6 +94,15 @@ export default {
}
},
applyMonochromeIconColor() {
// copied from https://github.com/nextcloud/activity/blob/db919d45c45356082b17104614018e2c7e691996/js/script.js#L225
const monochromeIcon = this.activity.type !== 'file_created' && this.activity.type !== 'file_deleted' && this.activity.type !== 'favorite' && !this.activity.icon.endsWith('-color.svg')
if (monochromeIcon) {
return 'monochrome'
}
return ''
},
sanitizedMessage() {
return DOMPurify.sanitize(this.activity.message, { ALLOWED_TAGS: ['ins', 'del'], ALLOWED_ATTR: ['class'] })
},
@@ -115,6 +124,12 @@ export default {
height: 16px;
flex-shrink: 0;
flex-grow: 0;
/* colored icons, in addition to core ones */
&.monochrome {
opacity: 0.8;
filter: var(--background-invert-if-dark);
}
}
.activity--subject {
margin-left: 10px;

View File

@@ -58,7 +58,7 @@ import { NcAppSidebar, NcAppSidebarTab } from '@nextcloud/vue'
import ActivityIcon from 'vue-material-design-icons/LightningBolt.vue'
import SharingIcon from 'vue-material-design-icons/ShareVariantOutline.vue'
import TagsIcon from 'vue-material-design-icons/TagMultipleOutline.vue'
import TrashIcon from 'vue-material-design-icons/DeleteOutline.vue'
import TrashIcon from 'vue-material-design-icons/TrashCanOutline.vue'
const capabilities = window.OC.getCapabilities()
export default {

View File

@@ -178,9 +178,16 @@ export default {
},
},
watch: {
currentCard(newCard, oldCard) {
if (newCard.id === oldCard.id) return
this.focusHeader()
currentCard: {
handler(newCard, oldCard) {
if (!newCard) {
return
}
// Only refocus when actually switching cards, not during autosave updates
if (!oldCard?.id || newCard.id !== oldCard.id) {
this.focusHeader()
}
},
},
'currentCard.title': {
immediate: true,

View File

@@ -131,42 +131,6 @@ export default {
height: var(--default-clickable-area);
}
.badges .icon.due {
background-position: 4px center;
border-radius: var(--border-radius);
padding: 4px;
font-size: 13px;
display: flex;
align-items: center;
opacity: .5;
flex-shrink: 1;
.icon {
background-size: contain;
}
&.overdue {
background-color: var(--color-error);
color: var(--color-primary-element-text);
opacity: .7;
}
&.now {
background-color: var(--color-warning);
opacity: .7;
}
&.next {
background-color: var(--color-background-dark);
opacity: .7;
}
span {
margin-left: 20px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
.badge-left, .badge-right {
display: flex;
}

View File

@@ -95,16 +95,16 @@ export default {
z-index: 2;
[data-due-state='Overdue'] & {
color: var(--color-error-text);
background-color: rgba(var(--color-error-rgb), .1);
color: var(--color-element-error, var(--color-error-text));
background-color: rgba(var(--color-error-rgb), .5);
}
[data-due-state='Now'] & {
color: var(--color-warning-text);
background-color: rgba(var(--color-warning-rgb), .1);
color: var(--color-element-warning, var(--color-warning-text));
background-color: rgba(var(--color-warning-rgb), .5);
}
[data-due-state='Done'] & {
color: var(--color-success-text);
background-color: rgba(var(--color-success-rgb), .1);
color: var(--color-element-success, var(--color-success-text));
background-color: rgba(var(--color-success-rgb), .5);
}
.due--label {

View File

@@ -129,12 +129,17 @@ class StackServiceTest extends TestCase {
}
)
);
$this->cardMapper->expects($this->any())->method('findAll')->willReturn($this->getCards(222));
$this->cardMapper->expects($this->any())->method('findAllForStacks')->willReturnCallback(function (array $stackIds) {
$r = [];
foreach ($stackIds as $stackId) {
$r[$stackId] = $this->getCards(222);
}
return $r;
});
$actual = $this->stackService->findAll(123);
for ($stackId = 0; $stackId < 3; $stackId++) {
for ($cardId = 0;$cardId < 10;$cardId++) {
for ($cardId = 0; $cardId < 10; $cardId++) {
$this->assertEquals($actual[0]->getCards()[$cardId]->getId(), $cardId);
$this->assertEquals($actual[0]->getCards()[$cardId]->getStackId(), 222);
$this->assertEquals($actual[0]->getCards()[$cardId]->getLabels(), $this->getLabels()[$cardId]);
@@ -211,7 +216,7 @@ class StackServiceTest extends TestCase {
$stackToBeDeleted->setBoardId(1);
$this->stackMapper->expects($this->once())->method('find')->willReturn($stackToBeDeleted);
$this->stackMapper->expects($this->once())->method('update')->willReturn($stackToBeDeleted);
$this->cardMapper->expects($this->once())->method('findAll')->willReturn([]);
$this->cardMapper->expects($this->once())->method('findAllForStacks')->willReturn([]);
$this->stackService->delete(123);
$this->assertTrue($stackToBeDeleted->getDeletedAt() <= time(), 'deletedAt is in the past');
$this->assertTrue($stackToBeDeleted->getDeletedAt() > 0, 'deletedAt is set');