From e8924c9ab7faad8e224ccc96e7c84ba12928630e Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 30 Sep 2026 07:56:19 +0200 Subject: [PATCH 1/5] feat(containers): sort the tiles in a container by hand, alphabetically, by most or last used, or at random Most and last used read counts kept in the viewer's own browser, never sent to the server; a viewer can forget them. The order is applied when the page is viewed and never written back, and random holds for the page load. REQ-TSO-001..004, REQ-CONT-007 (launcher-tile-sorting) --- l10n/be.js | 13 +- l10n/be.json | 13 +- l10n/bg.js | 13 +- l10n/bg.json | 13 +- l10n/bs.js | 13 +- l10n/bs.json | 13 +- l10n/ca.js | 13 +- l10n/ca.json | 13 +- l10n/cs.js | 13 +- l10n/cs.json | 13 +- l10n/da.js | 13 +- l10n/da.json | 13 +- l10n/de.js | 13 +- l10n/de.json | 13 +- l10n/el.js | 13 +- l10n/el.json | 13 +- l10n/en.js | 13 +- l10n/en.json | 13 +- l10n/es.js | 13 +- l10n/es.json | 13 +- l10n/et.js | 13 +- l10n/et.json | 13 +- l10n/fi.js | 13 +- l10n/fi.json | 13 +- l10n/fr.js | 13 +- l10n/fr.json | 13 +- l10n/ga.js | 13 +- l10n/ga.json | 13 +- l10n/hr.js | 13 +- l10n/hr.json | 13 +- l10n/hu.js | 13 +- l10n/hu.json | 13 +- l10n/is.js | 13 +- l10n/is.json | 13 +- l10n/it.js | 13 +- l10n/it.json | 13 +- l10n/lb.js | 13 +- l10n/lb.json | 13 +- l10n/lt.js | 13 +- l10n/lt.json | 13 +- l10n/lv.js | 13 +- l10n/lv.json | 13 +- l10n/mk.js | 13 +- l10n/mk.json | 13 +- l10n/mt.js | 13 +- l10n/mt.json | 13 +- l10n/nb.js | 13 +- l10n/nb.json | 13 +- l10n/nl.js | 13 +- l10n/nl.json | 13 +- l10n/pl.js | 13 +- l10n/pl.json | 13 +- l10n/pt.js | 13 +- l10n/pt.json | 13 +- l10n/rm.js | 13 +- l10n/rm.json | 13 +- l10n/ro.js | 13 +- l10n/ro.json | 13 +- l10n/ru.js | 13 +- l10n/ru.json | 13 +- l10n/sk.js | 13 +- l10n/sk.json | 13 +- l10n/sl.js | 13 +- l10n/sl.json | 13 +- l10n/sq.js | 13 +- l10n/sq.json | 13 +- l10n/sr.js | 13 +- l10n/sr.json | 13 +- l10n/sv.js | 13 +- l10n/sv.json | 13 +- l10n/tr.js | 13 +- l10n/tr.json | 13 +- l10n/uk.js | 13 +- l10n/uk.json | 13 +- .../Widgets/Renderers/ContainerWidget.vue | 177 +++++++++++++- .../Widgets/Renderers/ContainerWidgetForm.vue | 139 +++++++++++ .../__tests__/ContainerWidget.sort.spec.js | 226 ++++++++++++++++++ .../__tests__/ContainerWidgetForm.spec.js | 110 +++++++++ src/composables/useTileClickTracking.js | 131 ++++++++++ src/constants/widgetRegistry.js | 3 + src/utils/sortContainerTiles.js | 116 +++++++++ 81 files changed, 1786 insertions(+), 78 deletions(-) create mode 100644 src/components/Widgets/Renderers/ContainerWidgetForm.vue create mode 100644 src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js create mode 100644 src/components/Widgets/Renderers/__tests__/ContainerWidgetForm.spec.js create mode 100644 src/utils/sortContainerTiles.js diff --git a/l10n/be.js b/l10n/be.js index 4fc3bafb1..009cd5a99 100644 --- a/l10n/be.js +++ b/l10n/be.js @@ -1312,6 +1312,17 @@ OC.L10N.register( "Profile fields saved." : "Палі профілю захаваны.", "Save profile fields" : "Захаваць палі профілю", "The profile fields could not be loaded." : "Не ўдалося загрузіць палі профілю.", - "The profile fields could not be saved." : "Не ўдалося захаваць палі профілю." + "The profile fields could not be saved." : "Не ўдалося захаваць палі профілю.", + "Forget my usage" : "Забыць маё выкарыстанне", + "Sorted alphabetically" : "Упарадкавана па алфавіце", + "Sorted by most used" : "Упарадкавана па частаце выкарыстання", + "Sorted by last used" : "Упарадкавана па апошнім выкарыстанні", + "Sorted at random" : "Упарадкавана выпадкова", + "Sort tiles" : "Парадак плітак", + "By hand" : "Уручную", + "Alphabetically" : "Па алфавіце", + "Most used" : "Найчасцей выкарыстоўваныя", + "Last used" : "Апошнія выкарыстаныя", + "At random" : "Выпадкова" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"); diff --git a/l10n/be.json b/l10n/be.json index f1b3999ce..4b7180f08 100644 --- a/l10n/be.json +++ b/l10n/be.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Палі профілю захаваны.", "Save profile fields": "Захаваць палі профілю", "The profile fields could not be loaded.": "Не ўдалося загрузіць палі профілю.", - "The profile fields could not be saved.": "Не ўдалося захаваць палі профілю." + "The profile fields could not be saved.": "Не ўдалося захаваць палі профілю.", + "Forget my usage": "Забыць маё выкарыстанне", + "Sorted alphabetically": "Упарадкавана па алфавіце", + "Sorted by most used": "Упарадкавана па частаце выкарыстання", + "Sorted by last used": "Упарадкавана па апошнім выкарыстанні", + "Sorted at random": "Упарадкавана выпадкова", + "Sort tiles": "Парадак плітак", + "By hand": "Уручную", + "Alphabetically": "Па алфавіце", + "Most used": "Найчасцей выкарыстоўваныя", + "Last used": "Апошнія выкарыстаныя", + "At random": "Выпадкова" } } diff --git a/l10n/bg.js b/l10n/bg.js index 94a897089..29818f4a3 100644 --- a/l10n/bg.js +++ b/l10n/bg.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Полетата на профила са запазени.", "Save profile fields" : "Запазване на полетата на профила", "The profile fields could not be loaded." : "Полетата на профила не можаха да се заредят.", - "The profile fields could not be saved." : "Полетата на профила не можаха да се запазят." + "The profile fields could not be saved." : "Полетата на профила не можаха да се запазят.", + "Forget my usage" : "Забрави използването ми", + "Sorted alphabetically" : "Подредено по азбучен ред", + "Sorted by most used" : "Подредено по най-използвани", + "Sorted by last used" : "Подредено по последно използвани", + "Sorted at random" : "Подредено в случаен ред", + "Sort tiles" : "Подреждане на плочките", + "By hand" : "Ръчно", + "Alphabetically" : "По азбучен ред", + "Most used" : "Най-използвани", + "Last used" : "Последно използвани", + "At random" : "В случаен ред" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/bg.json b/l10n/bg.json index 8f5c8ea7b..0879485b6 100644 --- a/l10n/bg.json +++ b/l10n/bg.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Полетата на профила са запазени.", "Save profile fields": "Запазване на полетата на профила", "The profile fields could not be loaded.": "Полетата на профила не можаха да се заредят.", - "The profile fields could not be saved.": "Полетата на профила не можаха да се запазят." + "The profile fields could not be saved.": "Полетата на профила не можаха да се запазят.", + "Forget my usage": "Забрави използването ми", + "Sorted alphabetically": "Подредено по азбучен ред", + "Sorted by most used": "Подредено по най-използвани", + "Sorted by last used": "Подредено по последно използвани", + "Sorted at random": "Подредено в случаен ред", + "Sort tiles": "Подреждане на плочките", + "By hand": "Ръчно", + "Alphabetically": "По азбучен ред", + "Most used": "Най-използвани", + "Last used": "Последно използвани", + "At random": "В случаен ред" } } diff --git a/l10n/bs.js b/l10n/bs.js index 361af2eed..1f9c11827 100644 --- a/l10n/bs.js +++ b/l10n/bs.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Polja profila su spremljena.", "Save profile fields" : "Spremi polja profila", "The profile fields could not be loaded." : "Polja profila nisu mogla biti učitana.", - "The profile fields could not be saved." : "Polja profila nisu mogla biti spremljena." + "The profile fields could not be saved." : "Polja profila nisu mogla biti spremljena.", + "Forget my usage" : "Zaboravi moje korištenje", + "Sorted alphabetically" : "Poredano abecedno", + "Sorted by most used" : "Poredano po najčešće korištenim", + "Sorted by last used" : "Poredano po zadnje korištenim", + "Sorted at random" : "Poredano nasumično", + "Sort tiles" : "Poredaj pločice", + "By hand" : "Ručno", + "Alphabetically" : "Abecedno", + "Most used" : "Najčešće korišteno", + "Last used" : "Zadnje korišteno", + "At random" : "Nasumično" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/bs.json b/l10n/bs.json index 3afcedf8a..4c194ed67 100644 --- a/l10n/bs.json +++ b/l10n/bs.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Polja profila su spremljena.", "Save profile fields": "Spremi polja profila", "The profile fields could not be loaded.": "Polja profila nisu mogla biti učitana.", - "The profile fields could not be saved.": "Polja profila nisu mogla biti spremljena." + "The profile fields could not be saved.": "Polja profila nisu mogla biti spremljena.", + "Forget my usage": "Zaboravi moje korištenje", + "Sorted alphabetically": "Poredano abecedno", + "Sorted by most used": "Poredano po najčešće korištenim", + "Sorted by last used": "Poredano po zadnje korištenim", + "Sorted at random": "Poredano nasumično", + "Sort tiles": "Poredaj pločice", + "By hand": "Ručno", + "Alphabetically": "Abecedno", + "Most used": "Najčešće korišteno", + "Last used": "Zadnje korišteno", + "At random": "Nasumično" } } diff --git a/l10n/ca.js b/l10n/ca.js index bf45de116..86332cdff 100644 --- a/l10n/ca.js +++ b/l10n/ca.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "S'han desat els camps del perfil.", "Save profile fields" : "Desa els camps del perfil", "The profile fields could not be loaded." : "No s'han pogut carregar els camps del perfil.", - "The profile fields could not be saved." : "No s'han pogut desar els camps del perfil." + "The profile fields could not be saved." : "No s'han pogut desar els camps del perfil.", + "Forget my usage" : "Oblida el meu ús", + "Sorted alphabetically" : "Ordenat alfabèticament", + "Sorted by most used" : "Ordenat pels més usats", + "Sorted by last used" : "Ordenat pels darrers usats", + "Sorted at random" : "Ordenat a l'atzar", + "Sort tiles" : "Ordena les rajoles", + "By hand" : "A mà", + "Alphabetically" : "Alfabèticament", + "Most used" : "Els més usats", + "Last used" : "Darrers usats", + "At random" : "A l'atzar" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/ca.json b/l10n/ca.json index ed8885f1a..8b4868e41 100644 --- a/l10n/ca.json +++ b/l10n/ca.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "S'han desat els camps del perfil.", "Save profile fields": "Desa els camps del perfil", "The profile fields could not be loaded.": "No s'han pogut carregar els camps del perfil.", - "The profile fields could not be saved.": "No s'han pogut desar els camps del perfil." + "The profile fields could not be saved.": "No s'han pogut desar els camps del perfil.", + "Forget my usage": "Oblida el meu ús", + "Sorted alphabetically": "Ordenat alfabèticament", + "Sorted by most used": "Ordenat pels més usats", + "Sorted by last used": "Ordenat pels darrers usats", + "Sorted at random": "Ordenat a l'atzar", + "Sort tiles": "Ordena les rajoles", + "By hand": "A mà", + "Alphabetically": "Alfabèticament", + "Most used": "Els més usats", + "Last used": "Darrers usats", + "At random": "A l'atzar" } } diff --git a/l10n/cs.js b/l10n/cs.js index d11ca0b25..e5c89f275 100644 --- a/l10n/cs.js +++ b/l10n/cs.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Pole profilu jsou uložena.", "Save profile fields" : "Uložit pole profilu", "The profile fields could not be loaded." : "Pole profilu se nepodařilo načíst.", - "The profile fields could not be saved." : "Pole profilu se nepodařilo uložit." + "The profile fields could not be saved." : "Pole profilu se nepodařilo uložit.", + "Forget my usage" : "Zapomenout mé používání", + "Sorted alphabetically" : "Seřazeno abecedně", + "Sorted by most used" : "Seřazeno podle nejpoužívanějších", + "Sorted by last used" : "Seřazeno podle naposledy použitých", + "Sorted at random" : "Seřazeno náhodně", + "Sort tiles" : "Řazení dlaždic", + "By hand" : "Ručně", + "Alphabetically" : "Abecedně", + "Most used" : "Nejpoužívanější", + "Last used" : "Naposledy použité", + "At random" : "Náhodně" }, "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" ); diff --git a/l10n/cs.json b/l10n/cs.json index 3f64c588c..6b523cb30 100644 --- a/l10n/cs.json +++ b/l10n/cs.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Pole profilu jsou uložena.", "Save profile fields": "Uložit pole profilu", "The profile fields could not be loaded.": "Pole profilu se nepodařilo načíst.", - "The profile fields could not be saved.": "Pole profilu se nepodařilo uložit." + "The profile fields could not be saved.": "Pole profilu se nepodařilo uložit.", + "Forget my usage": "Zapomenout mé používání", + "Sorted alphabetically": "Seřazeno abecedně", + "Sorted by most used": "Seřazeno podle nejpoužívanějších", + "Sorted by last used": "Seřazeno podle naposledy použitých", + "Sorted at random": "Seřazeno náhodně", + "Sort tiles": "Řazení dlaždic", + "By hand": "Ručně", + "Alphabetically": "Abecedně", + "Most used": "Nejpoužívanější", + "Last used": "Naposledy použité", + "At random": "Náhodně" } } diff --git a/l10n/da.js b/l10n/da.js index ff77c1aaa..dde69e192 100644 --- a/l10n/da.js +++ b/l10n/da.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profilfelterne er gemt.", "Save profile fields" : "Gem profilfelter", "The profile fields could not be loaded." : "Profilfelterne kunne ikke indlæses.", - "The profile fields could not be saved." : "Profilfelterne kunne ikke gemmes." + "The profile fields could not be saved." : "Profilfelterne kunne ikke gemmes.", + "Forget my usage" : "Glem min brug", + "Sorted alphabetically" : "Sorteret alfabetisk", + "Sorted by most used" : "Sorteret efter mest brugte", + "Sorted by last used" : "Sorteret efter senest brugte", + "Sorted at random" : "Sorteret tilfældigt", + "Sort tiles" : "Sortér felter", + "By hand" : "Manuelt", + "Alphabetically" : "Alfabetisk", + "Most used" : "Mest brugte", + "Last used" : "Senest brugte", + "At random" : "Tilfældigt" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/da.json b/l10n/da.json index 78fd8908a..840976e06 100644 --- a/l10n/da.json +++ b/l10n/da.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profilfelterne er gemt.", "Save profile fields": "Gem profilfelter", "The profile fields could not be loaded.": "Profilfelterne kunne ikke indlæses.", - "The profile fields could not be saved.": "Profilfelterne kunne ikke gemmes." + "The profile fields could not be saved.": "Profilfelterne kunne ikke gemmes.", + "Forget my usage": "Glem min brug", + "Sorted alphabetically": "Sorteret alfabetisk", + "Sorted by most used": "Sorteret efter mest brugte", + "Sorted by last used": "Sorteret efter senest brugte", + "Sorted at random": "Sorteret tilfældigt", + "Sort tiles": "Sortér felter", + "By hand": "Manuelt", + "Alphabetically": "Alfabetisk", + "Most used": "Mest brugte", + "Last used": "Senest brugte", + "At random": "Tilfældigt" } } diff --git a/l10n/de.js b/l10n/de.js index 6063757b5..9e008de03 100644 --- a/l10n/de.js +++ b/l10n/de.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profilfelder gespeichert.", "Save profile fields" : "Profilfelder speichern", "The profile fields could not be loaded." : "Die Profilfelder konnten nicht geladen werden.", - "The profile fields could not be saved." : "Die Profilfelder konnten nicht gespeichert werden." + "The profile fields could not be saved." : "Die Profilfelder konnten nicht gespeichert werden.", + "Forget my usage" : "Meine Nutzung vergessen", + "Sorted alphabetically" : "Alphabetisch sortiert", + "Sorted by most used" : "Nach häufigster Nutzung sortiert", + "Sorted by last used" : "Nach letzter Nutzung sortiert", + "Sorted at random" : "Zufällig sortiert", + "Sort tiles" : "Kacheln sortieren", + "By hand" : "Von Hand", + "Alphabetically" : "Alphabetisch", + "Most used" : "Am häufigsten genutzt", + "Last used" : "Zuletzt genutzt", + "At random" : "Zufällig" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/de.json b/l10n/de.json index 5468a77de..bf84b97e3 100644 --- a/l10n/de.json +++ b/l10n/de.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profilfelder gespeichert.", "Save profile fields": "Profilfelder speichern", "The profile fields could not be loaded.": "Die Profilfelder konnten nicht geladen werden.", - "The profile fields could not be saved.": "Die Profilfelder konnten nicht gespeichert werden." + "The profile fields could not be saved.": "Die Profilfelder konnten nicht gespeichert werden.", + "Forget my usage": "Meine Nutzung vergessen", + "Sorted alphabetically": "Alphabetisch sortiert", + "Sorted by most used": "Nach häufigster Nutzung sortiert", + "Sorted by last used": "Nach letzter Nutzung sortiert", + "Sorted at random": "Zufällig sortiert", + "Sort tiles": "Kacheln sortieren", + "By hand": "Von Hand", + "Alphabetically": "Alphabetisch", + "Most used": "Am häufigsten genutzt", + "Last used": "Zuletzt genutzt", + "At random": "Zufällig" } } diff --git a/l10n/el.js b/l10n/el.js index 4fe15f6bc..15a512709 100644 --- a/l10n/el.js +++ b/l10n/el.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Τα πεδία προφίλ αποθηκεύτηκαν.", "Save profile fields" : "Αποθήκευση πεδίων προφίλ", "The profile fields could not be loaded." : "Δεν ήταν δυνατή η φόρτωση των πεδίων προφίλ.", - "The profile fields could not be saved." : "Δεν ήταν δυνατή η αποθήκευση των πεδίων προφίλ." + "The profile fields could not be saved." : "Δεν ήταν δυνατή η αποθήκευση των πεδίων προφίλ.", + "Forget my usage" : "Διαγραφή της χρήσης μου", + "Sorted alphabetically" : "Ταξινομημένα αλφαβητικά", + "Sorted by most used" : "Ταξινομημένα κατά συχνότερη χρήση", + "Sorted by last used" : "Ταξινομημένα κατά τελευταία χρήση", + "Sorted at random" : "Ταξινομημένα τυχαία", + "Sort tiles" : "Ταξινόμηση πλακιδίων", + "By hand" : "Χειροκίνητα", + "Alphabetically" : "Αλφαβητικά", + "Most used" : "Συχνότερα χρησιμοποιούμενα", + "Last used" : "Τελευταία χρήση", + "At random" : "Τυχαία" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/el.json b/l10n/el.json index c4aaf0219..5c8950f27 100644 --- a/l10n/el.json +++ b/l10n/el.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Τα πεδία προφίλ αποθηκεύτηκαν.", "Save profile fields": "Αποθήκευση πεδίων προφίλ", "The profile fields could not be loaded.": "Δεν ήταν δυνατή η φόρτωση των πεδίων προφίλ.", - "The profile fields could not be saved.": "Δεν ήταν δυνατή η αποθήκευση των πεδίων προφίλ." + "The profile fields could not be saved.": "Δεν ήταν δυνατή η αποθήκευση των πεδίων προφίλ.", + "Forget my usage": "Διαγραφή της χρήσης μου", + "Sorted alphabetically": "Ταξινομημένα αλφαβητικά", + "Sorted by most used": "Ταξινομημένα κατά συχνότερη χρήση", + "Sorted by last used": "Ταξινομημένα κατά τελευταία χρήση", + "Sorted at random": "Ταξινομημένα τυχαία", + "Sort tiles": "Ταξινόμηση πλακιδίων", + "By hand": "Χειροκίνητα", + "Alphabetically": "Αλφαβητικά", + "Most used": "Συχνότερα χρησιμοποιούμενα", + "Last used": "Τελευταία χρήση", + "At random": "Τυχαία" } } diff --git a/l10n/en.js b/l10n/en.js index c86a4c71c..b2c2ba066 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -1301,7 +1301,18 @@ OC.L10N.register( "Profile fields saved." : "Profile fields saved.", "Save profile fields" : "Save profile fields", "The profile fields could not be loaded." : "The profile fields could not be loaded.", - "The profile fields could not be saved." : "The profile fields could not be saved." + "The profile fields could not be saved." : "The profile fields could not be saved.", + "Forget my usage" : "Forget my usage", + "Sorted alphabetically" : "Sorted alphabetically", + "Sorted by most used" : "Sorted by most used", + "Sorted by last used" : "Sorted by last used", + "Sorted at random" : "Sorted at random", + "Sort tiles" : "Sort tiles", + "By hand" : "By hand", + "Alphabetically" : "Alphabetically", + "Most used" : "Most used", + "Last used" : "Last used", + "At random" : "At random" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/en.json b/l10n/en.json index 50a16c214..28106e6b2 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -1266,6 +1266,17 @@ "Profile fields saved.": "Profile fields saved.", "Save profile fields": "Save profile fields", "The profile fields could not be loaded.": "The profile fields could not be loaded.", - "The profile fields could not be saved.": "The profile fields could not be saved." + "The profile fields could not be saved.": "The profile fields could not be saved.", + "Forget my usage": "Forget my usage", + "Sorted alphabetically": "Sorted alphabetically", + "Sorted by most used": "Sorted by most used", + "Sorted by last used": "Sorted by last used", + "Sorted at random": "Sorted at random", + "Sort tiles": "Sort tiles", + "By hand": "By hand", + "Alphabetically": "Alphabetically", + "Most used": "Most used", + "Last used": "Last used", + "At random": "At random" } } diff --git a/l10n/es.js b/l10n/es.js index 3c4155627..75312eba8 100644 --- a/l10n/es.js +++ b/l10n/es.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Campos del perfil guardados.", "Save profile fields" : "Guardar campos del perfil", "The profile fields could not be loaded." : "No se pudieron cargar los campos del perfil.", - "The profile fields could not be saved." : "No se pudieron guardar los campos del perfil." + "The profile fields could not be saved." : "No se pudieron guardar los campos del perfil.", + "Forget my usage" : "Olvidar mi uso", + "Sorted alphabetically" : "Ordenado alfabéticamente", + "Sorted by most used" : "Ordenado por más usados", + "Sorted by last used" : "Ordenado por último uso", + "Sorted at random" : "Ordenado al azar", + "Sort tiles" : "Ordenar mosaicos", + "By hand" : "A mano", + "Alphabetically" : "Alfabéticamente", + "Most used" : "Más usados", + "Last used" : "Último uso", + "At random" : "Al azar" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/es.json b/l10n/es.json index b5bb51d96..c63e2e707 100644 --- a/l10n/es.json +++ b/l10n/es.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Campos del perfil guardados.", "Save profile fields": "Guardar campos del perfil", "The profile fields could not be loaded.": "No se pudieron cargar los campos del perfil.", - "The profile fields could not be saved.": "No se pudieron guardar los campos del perfil." + "The profile fields could not be saved.": "No se pudieron guardar los campos del perfil.", + "Forget my usage": "Olvidar mi uso", + "Sorted alphabetically": "Ordenado alfabéticamente", + "Sorted by most used": "Ordenado por más usados", + "Sorted by last used": "Ordenado por último uso", + "Sorted at random": "Ordenado al azar", + "Sort tiles": "Ordenar mosaicos", + "By hand": "A mano", + "Alphabetically": "Alfabéticamente", + "Most used": "Más usados", + "Last used": "Último uso", + "At random": "Al azar" } } diff --git a/l10n/et.js b/l10n/et.js index 176dbb655..02d6525db 100644 --- a/l10n/et.js +++ b/l10n/et.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profiili väljad salvestatud.", "Save profile fields" : "Salvesta profiili väljad", "The profile fields could not be loaded." : "Profiili välju ei õnnestunud laadida.", - "The profile fields could not be saved." : "Profiili välju ei õnnestunud salvestada." + "The profile fields could not be saved." : "Profiili välju ei õnnestunud salvestada.", + "Forget my usage" : "Unusta minu kasutus", + "Sorted alphabetically" : "Sorditud tähestiku järgi", + "Sorted by most used" : "Sorditud enim kasutatu järgi", + "Sorted by last used" : "Sorditud viimati kasutatu järgi", + "Sorted at random" : "Sorditud juhuslikult", + "Sort tiles" : "Paanide järjestus", + "By hand" : "Käsitsi", + "Alphabetically" : "Tähestiku järgi", + "Most used" : "Enim kasutatud", + "Last used" : "Viimati kasutatud", + "At random" : "Juhuslikult" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/et.json b/l10n/et.json index 856cf2a6a..d6760c3fc 100644 --- a/l10n/et.json +++ b/l10n/et.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profiili väljad salvestatud.", "Save profile fields": "Salvesta profiili väljad", "The profile fields could not be loaded.": "Profiili välju ei õnnestunud laadida.", - "The profile fields could not be saved.": "Profiili välju ei õnnestunud salvestada." + "The profile fields could not be saved.": "Profiili välju ei õnnestunud salvestada.", + "Forget my usage": "Unusta minu kasutus", + "Sorted alphabetically": "Sorditud tähestiku järgi", + "Sorted by most used": "Sorditud enim kasutatu järgi", + "Sorted by last used": "Sorditud viimati kasutatu järgi", + "Sorted at random": "Sorditud juhuslikult", + "Sort tiles": "Paanide järjestus", + "By hand": "Käsitsi", + "Alphabetically": "Tähestiku järgi", + "Most used": "Enim kasutatud", + "Last used": "Viimati kasutatud", + "At random": "Juhuslikult" } } diff --git a/l10n/fi.js b/l10n/fi.js index cddec8a91..a0949456f 100644 --- a/l10n/fi.js +++ b/l10n/fi.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profiilikentät tallennettu.", "Save profile fields" : "Tallenna profiilikentät", "The profile fields could not be loaded." : "Profiilikenttiä ei voitu ladata.", - "The profile fields could not be saved." : "Profiilikenttiä ei voitu tallentaa." + "The profile fields could not be saved." : "Profiilikenttiä ei voitu tallentaa.", + "Forget my usage" : "Unohda käyttöni", + "Sorted alphabetically" : "Järjestetty aakkosjärjestykseen", + "Sorted by most used" : "Järjestetty eniten käytetyn mukaan", + "Sorted by last used" : "Järjestetty viimeksi käytetyn mukaan", + "Sorted at random" : "Järjestetty satunnaisesti", + "Sort tiles" : "Ruutujen järjestys", + "By hand" : "Käsin", + "Alphabetically" : "Aakkosjärjestyksessä", + "Most used" : "Eniten käytetyt", + "Last used" : "Viimeksi käytetyt", + "At random" : "Satunnaisesti" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/fi.json b/l10n/fi.json index 943c32a29..4cd802cbf 100644 --- a/l10n/fi.json +++ b/l10n/fi.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profiilikentät tallennettu.", "Save profile fields": "Tallenna profiilikentät", "The profile fields could not be loaded.": "Profiilikenttiä ei voitu ladata.", - "The profile fields could not be saved.": "Profiilikenttiä ei voitu tallentaa." + "The profile fields could not be saved.": "Profiilikenttiä ei voitu tallentaa.", + "Forget my usage": "Unohda käyttöni", + "Sorted alphabetically": "Järjestetty aakkosjärjestykseen", + "Sorted by most used": "Järjestetty eniten käytetyn mukaan", + "Sorted by last used": "Järjestetty viimeksi käytetyn mukaan", + "Sorted at random": "Järjestetty satunnaisesti", + "Sort tiles": "Ruutujen järjestys", + "By hand": "Käsin", + "Alphabetically": "Aakkosjärjestyksessä", + "Most used": "Eniten käytetyt", + "Last used": "Viimeksi käytetyt", + "At random": "Satunnaisesti" } } diff --git a/l10n/fr.js b/l10n/fr.js index 307678377..7331b0f10 100644 --- a/l10n/fr.js +++ b/l10n/fr.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Champs du profil enregistrés.", "Save profile fields" : "Enregistrer les champs du profil", "The profile fields could not be loaded." : "Les champs du profil n'ont pas pu être chargés.", - "The profile fields could not be saved." : "Les champs du profil n'ont pas pu être enregistrés." + "The profile fields could not be saved." : "Les champs du profil n'ont pas pu être enregistrés.", + "Forget my usage" : "Oublier mon utilisation", + "Sorted alphabetically" : "Trié par ordre alphabétique", + "Sorted by most used" : "Trié par les plus utilisés", + "Sorted by last used" : "Trié par dernière utilisation", + "Sorted at random" : "Trié au hasard", + "Sort tiles" : "Trier les tuiles", + "By hand" : "À la main", + "Alphabetically" : "Par ordre alphabétique", + "Most used" : "Les plus utilisés", + "Last used" : "Dernière utilisation", + "At random" : "Au hasard" }, "nplurals=2; plural=(n > 1);" ); diff --git a/l10n/fr.json b/l10n/fr.json index 3c6046a8f..7e5ff6f4d 100644 --- a/l10n/fr.json +++ b/l10n/fr.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Champs du profil enregistrés.", "Save profile fields": "Enregistrer les champs du profil", "The profile fields could not be loaded.": "Les champs du profil n'ont pas pu être chargés.", - "The profile fields could not be saved.": "Les champs du profil n'ont pas pu être enregistrés." + "The profile fields could not be saved.": "Les champs du profil n'ont pas pu être enregistrés.", + "Forget my usage": "Oublier mon utilisation", + "Sorted alphabetically": "Trié par ordre alphabétique", + "Sorted by most used": "Trié par les plus utilisés", + "Sorted by last used": "Trié par dernière utilisation", + "Sorted at random": "Trié au hasard", + "Sort tiles": "Trier les tuiles", + "By hand": "À la main", + "Alphabetically": "Par ordre alphabétique", + "Most used": "Les plus utilisés", + "Last used": "Dernière utilisation", + "At random": "Au hasard" } } diff --git a/l10n/ga.js b/l10n/ga.js index 8ba1d1216..5a43ce7fa 100644 --- a/l10n/ga.js +++ b/l10n/ga.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Réimsí próifíle sábháilte.", "Save profile fields" : "Sábháil réimsí próifíle", "The profile fields could not be loaded." : "Níorbh fhéidir na réimsí próifíle a lódáil.", - "The profile fields could not be saved." : "Níorbh fhéidir na réimsí próifíle a shábháil." + "The profile fields could not be saved." : "Níorbh fhéidir na réimsí próifíle a shábháil.", + "Forget my usage" : "Déan dearmad ar m'úsáid", + "Sorted alphabetically" : "Sórtáilte in ord aibítre", + "Sorted by most used" : "Sórtáilte de réir is mó a úsáidtear", + "Sorted by last used" : "Sórtáilte de réir an úsáid is déanaí", + "Sorted at random" : "Sórtáilte go randamach", + "Sort tiles" : "Sórtáil tíleanna", + "By hand" : "De láimh", + "Alphabetically" : "In ord aibítre", + "Most used" : "Is mó a úsáidtear", + "Last used" : "Úsáidte is déanaí", + "At random" : "Go randamach" }, "nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);" ); diff --git a/l10n/ga.json b/l10n/ga.json index 408ceb33b..4f4caf134 100644 --- a/l10n/ga.json +++ b/l10n/ga.json @@ -1291,6 +1291,17 @@ "Profile fields saved.": "Réimsí próifíle sábháilte.", "Save profile fields": "Sábháil réimsí próifíle", "The profile fields could not be loaded.": "Níorbh fhéidir na réimsí próifíle a lódáil.", - "The profile fields could not be saved.": "Níorbh fhéidir na réimsí próifíle a shábháil." + "The profile fields could not be saved.": "Níorbh fhéidir na réimsí próifíle a shábháil.", + "Forget my usage": "Déan dearmad ar m'úsáid", + "Sorted alphabetically": "Sórtáilte in ord aibítre", + "Sorted by most used": "Sórtáilte de réir is mó a úsáidtear", + "Sorted by last used": "Sórtáilte de réir an úsáid is déanaí", + "Sorted at random": "Sórtáilte go randamach", + "Sort tiles": "Sórtáil tíleanna", + "By hand": "De láimh", + "Alphabetically": "In ord aibítre", + "Most used": "Is mó a úsáidtear", + "Last used": "Úsáidte is déanaí", + "At random": "Go randamach" } } diff --git a/l10n/hr.js b/l10n/hr.js index c4b220d61..7bc09558f 100644 --- a/l10n/hr.js +++ b/l10n/hr.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Polja profila su spremljena.", "Save profile fields" : "Spremi polja profila", "The profile fields could not be loaded." : "Polja profila nije bilo moguće učitati.", - "The profile fields could not be saved." : "Polja profila nije bilo moguće spremiti." + "The profile fields could not be saved." : "Polja profila nije bilo moguće spremiti.", + "Forget my usage" : "Zaboravi moje korištenje", + "Sorted alphabetically" : "Poredano abecedno", + "Sorted by most used" : "Poredano po najčešće korištenim", + "Sorted by last used" : "Poredano po posljednje korištenim", + "Sorted at random" : "Poredano nasumično", + "Sort tiles" : "Poredaj pločice", + "By hand" : "Ručno", + "Alphabetically" : "Abecedno", + "Most used" : "Najčešće korišteno", + "Last used" : "Posljednje korišteno", + "At random" : "Nasumično" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/hr.json b/l10n/hr.json index 2c565a477..5abd69a6c 100644 --- a/l10n/hr.json +++ b/l10n/hr.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Polja profila su spremljena.", "Save profile fields": "Spremi polja profila", "The profile fields could not be loaded.": "Polja profila nije bilo moguće učitati.", - "The profile fields could not be saved.": "Polja profila nije bilo moguće spremiti." + "The profile fields could not be saved.": "Polja profila nije bilo moguće spremiti.", + "Forget my usage": "Zaboravi moje korištenje", + "Sorted alphabetically": "Poredano abecedno", + "Sorted by most used": "Poredano po najčešće korištenim", + "Sorted by last used": "Poredano po posljednje korištenim", + "Sorted at random": "Poredano nasumično", + "Sort tiles": "Poredaj pločice", + "By hand": "Ručno", + "Alphabetically": "Abecedno", + "Most used": "Najčešće korišteno", + "Last used": "Posljednje korišteno", + "At random": "Nasumično" } } diff --git a/l10n/hu.js b/l10n/hu.js index fc626aa13..8f267be02 100644 --- a/l10n/hu.js +++ b/l10n/hu.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profilmezők mentve.", "Save profile fields" : "Profilmezők mentése", "The profile fields could not be loaded." : "A profilmezőket nem sikerült betölteni.", - "The profile fields could not be saved." : "A profilmezőket nem sikerült menteni." + "The profile fields could not be saved." : "A profilmezőket nem sikerült menteni.", + "Forget my usage" : "Használatom elfelejtése", + "Sorted alphabetically" : "Betűrendbe rendezve", + "Sorted by most used" : "A leggyakrabban használtak szerint rendezve", + "Sorted by last used" : "A legutóbb használtak szerint rendezve", + "Sorted at random" : "Véletlenszerűen rendezve", + "Sort tiles" : "Csempék rendezése", + "By hand" : "Kézzel", + "Alphabetically" : "Betűrendben", + "Most used" : "Leggyakrabban használt", + "Last used" : "Legutóbb használt", + "At random" : "Véletlenszerűen" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/hu.json b/l10n/hu.json index 4ca822358..73771ec3f 100644 --- a/l10n/hu.json +++ b/l10n/hu.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profilmezők mentve.", "Save profile fields": "Profilmezők mentése", "The profile fields could not be loaded.": "A profilmezőket nem sikerült betölteni.", - "The profile fields could not be saved.": "A profilmezőket nem sikerült menteni." + "The profile fields could not be saved.": "A profilmezőket nem sikerült menteni.", + "Forget my usage": "Használatom elfelejtése", + "Sorted alphabetically": "Betűrendbe rendezve", + "Sorted by most used": "A leggyakrabban használtak szerint rendezve", + "Sorted by last used": "A legutóbb használtak szerint rendezve", + "Sorted at random": "Véletlenszerűen rendezve", + "Sort tiles": "Csempék rendezése", + "By hand": "Kézzel", + "Alphabetically": "Betűrendben", + "Most used": "Leggyakrabban használt", + "Last used": "Legutóbb használt", + "At random": "Véletlenszerűen" } } diff --git a/l10n/is.js b/l10n/is.js index 64029bd3c..90bb6f0a6 100644 --- a/l10n/is.js +++ b/l10n/is.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Prófílreitir vistaðir.", "Save profile fields" : "Vista prófílreiti", "The profile fields could not be loaded." : "Ekki tókst að hlaða inn prófílreitunum.", - "The profile fields could not be saved." : "Ekki tókst að vista prófílreitina." + "The profile fields could not be saved." : "Ekki tókst að vista prófílreitina.", + "Forget my usage" : "Gleyma notkun minni", + "Sorted alphabetically" : "Raðað í stafrófsröð", + "Sorted by most used" : "Raðað eftir mest notuðu", + "Sorted by last used" : "Raðað eftir síðast notuðu", + "Sorted at random" : "Raðað af handahófi", + "Sort tiles" : "Raða reitum", + "By hand" : "Handvirkt", + "Alphabetically" : "Í stafrófsröð", + "Most used" : "Mest notað", + "Last used" : "Síðast notað", + "At random" : "Af handahófi" }, "nplurals=2; plural=(n%10!=1 || n%100==11);" ); diff --git a/l10n/is.json b/l10n/is.json index de529d623..3b278cd6e 100644 --- a/l10n/is.json +++ b/l10n/is.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Prófílreitir vistaðir.", "Save profile fields": "Vista prófílreiti", "The profile fields could not be loaded.": "Ekki tókst að hlaða inn prófílreitunum.", - "The profile fields could not be saved.": "Ekki tókst að vista prófílreitina." + "The profile fields could not be saved.": "Ekki tókst að vista prófílreitina.", + "Forget my usage": "Gleyma notkun minni", + "Sorted alphabetically": "Raðað í stafrófsröð", + "Sorted by most used": "Raðað eftir mest notuðu", + "Sorted by last used": "Raðað eftir síðast notuðu", + "Sorted at random": "Raðað af handahófi", + "Sort tiles": "Raða reitum", + "By hand": "Handvirkt", + "Alphabetically": "Í stafrófsröð", + "Most used": "Mest notað", + "Last used": "Síðast notað", + "At random": "Af handahófi" } } diff --git a/l10n/it.js b/l10n/it.js index ab0446c0d..084ad08ea 100644 --- a/l10n/it.js +++ b/l10n/it.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Campi del profilo salvati.", "Save profile fields" : "Salva campi del profilo", "The profile fields could not be loaded." : "Non è stato possibile caricare i campi del profilo.", - "The profile fields could not be saved." : "Non è stato possibile salvare i campi del profilo." + "The profile fields could not be saved." : "Non è stato possibile salvare i campi del profilo.", + "Forget my usage" : "Dimentica il mio utilizzo", + "Sorted alphabetically" : "Ordinato alfabeticamente", + "Sorted by most used" : "Ordinato per più usati", + "Sorted by last used" : "Ordinato per ultimo utilizzo", + "Sorted at random" : "Ordinato a caso", + "Sort tiles" : "Ordina riquadri", + "By hand" : "A mano", + "Alphabetically" : "In ordine alfabetico", + "Most used" : "Più usati", + "Last used" : "Ultimo utilizzo", + "At random" : "A caso" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/it.json b/l10n/it.json index fa3dd24fa..0bf896a8c 100644 --- a/l10n/it.json +++ b/l10n/it.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Campi del profilo salvati.", "Save profile fields": "Salva campi del profilo", "The profile fields could not be loaded.": "Non è stato possibile caricare i campi del profilo.", - "The profile fields could not be saved.": "Non è stato possibile salvare i campi del profilo." + "The profile fields could not be saved.": "Non è stato possibile salvare i campi del profilo.", + "Forget my usage": "Dimentica il mio utilizzo", + "Sorted alphabetically": "Ordinato alfabeticamente", + "Sorted by most used": "Ordinato per più usati", + "Sorted by last used": "Ordinato per ultimo utilizzo", + "Sorted at random": "Ordinato a caso", + "Sort tiles": "Ordina riquadri", + "By hand": "A mano", + "Alphabetically": "In ordine alfabetico", + "Most used": "Più usati", + "Last used": "Ultimo utilizzo", + "At random": "A caso" } } diff --git a/l10n/lb.js b/l10n/lb.js index 84faec3ed..57ee7d502 100644 --- a/l10n/lb.js +++ b/l10n/lb.js @@ -1312,7 +1312,18 @@ OC.L10N.register( "Profile fields saved." : "Profilfelder gespäichert.", "Save profile fields" : "Profilfelder späicheren", "The profile fields could not be loaded." : "D'Profilfelder konnten net geluede ginn.", - "The profile fields could not be saved." : "D'Profilfelder konnten net gespäichert ginn." + "The profile fields could not be saved." : "D'Profilfelder konnten net gespäichert ginn.", + "Forget my usage" : "Meng Notzung vergiessen", + "Sorted alphabetically" : "Alphabetesch sortéiert", + "Sorted by most used" : "No der meeschter Notzung sortéiert", + "Sorted by last used" : "No der leschter Notzung sortéiert", + "Sorted at random" : "Zoufälleg sortéiert", + "Sort tiles" : "Kachelen sortéieren", + "By hand" : "Vun Hand", + "Alphabetically" : "Alphabetesch", + "Most used" : "Am meeschte benotzt", + "Last used" : "Lescht benotzt", + "At random" : "Zoufälleg" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/lb.json b/l10n/lb.json index e6fc3747f..530df9f16 100644 --- a/l10n/lb.json +++ b/l10n/lb.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profilfelder gespäichert.", "Save profile fields": "Profilfelder späicheren", "The profile fields could not be loaded.": "D'Profilfelder konnten net geluede ginn.", - "The profile fields could not be saved.": "D'Profilfelder konnten net gespäichert ginn." + "The profile fields could not be saved.": "D'Profilfelder konnten net gespäichert ginn.", + "Forget my usage": "Meng Notzung vergiessen", + "Sorted alphabetically": "Alphabetesch sortéiert", + "Sorted by most used": "No der meeschter Notzung sortéiert", + "Sorted by last used": "No der leschter Notzung sortéiert", + "Sorted at random": "Zoufälleg sortéiert", + "Sort tiles": "Kachelen sortéieren", + "By hand": "Vun Hand", + "Alphabetically": "Alphabetesch", + "Most used": "Am meeschte benotzt", + "Last used": "Lescht benotzt", + "At random": "Zoufälleg" } } diff --git a/l10n/lt.js b/l10n/lt.js index 8017d3ed4..b9f290070 100644 --- a/l10n/lt.js +++ b/l10n/lt.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profilio laukai išsaugoti.", "Save profile fields" : "Išsaugoti profilio laukus", "The profile fields could not be loaded." : "Nepavyko įkelti profilio laukų.", - "The profile fields could not be saved." : "Nepavyko išsaugoti profilio laukų." + "The profile fields could not be saved." : "Nepavyko išsaugoti profilio laukų.", + "Forget my usage" : "Pamiršti mano naudojimą", + "Sorted alphabetically" : "Surikiuota abėcėlės tvarka", + "Sorted by most used" : "Surikiuota pagal dažniausiai naudojamus", + "Sorted by last used" : "Surikiuota pagal paskutinį naudojimą", + "Sorted at random" : "Surikiuota atsitiktinai", + "Sort tiles" : "Plytelių rikiavimas", + "By hand" : "Rankiniu būdu", + "Alphabetically" : "Abėcėlės tvarka", + "Most used" : "Dažniausiai naudojami", + "Last used" : "Paskutiniai naudoti", + "At random" : "Atsitiktinai" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/lt.json b/l10n/lt.json index b0bd59a86..6a77144f8 100644 --- a/l10n/lt.json +++ b/l10n/lt.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Profilio laukai išsaugoti.", "Save profile fields": "Išsaugoti profilio laukus", "The profile fields could not be loaded.": "Nepavyko įkelti profilio laukų.", - "The profile fields could not be saved.": "Nepavyko išsaugoti profilio laukų." + "The profile fields could not be saved.": "Nepavyko išsaugoti profilio laukų.", + "Forget my usage": "Pamiršti mano naudojimą", + "Sorted alphabetically": "Surikiuota abėcėlės tvarka", + "Sorted by most used": "Surikiuota pagal dažniausiai naudojamus", + "Sorted by last used": "Surikiuota pagal paskutinį naudojimą", + "Sorted at random": "Surikiuota atsitiktinai", + "Sort tiles": "Plytelių rikiavimas", + "By hand": "Rankiniu būdu", + "Alphabetically": "Abėcėlės tvarka", + "Most used": "Dažniausiai naudojami", + "Last used": "Paskutiniai naudoti", + "At random": "Atsitiktinai" } } diff --git a/l10n/lv.js b/l10n/lv.js index 967b9beb5..375685874 100644 --- a/l10n/lv.js +++ b/l10n/lv.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profila lauki saglabāti.", "Save profile fields" : "Saglabāt profila laukus", "The profile fields could not be loaded." : "Neizdevās ielādēt profila laukus.", - "The profile fields could not be saved." : "Neizdevās saglabāt profila laukus." + "The profile fields could not be saved." : "Neizdevās saglabāt profila laukus.", + "Forget my usage" : "Aizmirst manu lietojumu", + "Sorted alphabetically" : "Kārtots alfabētiski", + "Sorted by most used" : "Kārtots pēc visbiežāk lietotajiem", + "Sorted by last used" : "Kārtots pēc pēdējās lietošanas", + "Sorted at random" : "Kārtots nejauši", + "Sort tiles" : "Flīžu kārtošana", + "By hand" : "Manuāli", + "Alphabetically" : "Alfabētiski", + "Most used" : "Visbiežāk lietotie", + "Last used" : "Pēdējie lietotie", + "At random" : "Nejauši" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);" ); diff --git a/l10n/lv.json b/l10n/lv.json index e1a6d207b..ddc13ea1d 100644 --- a/l10n/lv.json +++ b/l10n/lv.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Profila lauki saglabāti.", "Save profile fields": "Saglabāt profila laukus", "The profile fields could not be loaded.": "Neizdevās ielādēt profila laukus.", - "The profile fields could not be saved.": "Neizdevās saglabāt profila laukus." + "The profile fields could not be saved.": "Neizdevās saglabāt profila laukus.", + "Forget my usage": "Aizmirst manu lietojumu", + "Sorted alphabetically": "Kārtots alfabētiski", + "Sorted by most used": "Kārtots pēc visbiežāk lietotajiem", + "Sorted by last used": "Kārtots pēc pēdējās lietošanas", + "Sorted at random": "Kārtots nejauši", + "Sort tiles": "Flīžu kārtošana", + "By hand": "Manuāli", + "Alphabetically": "Alfabētiski", + "Most used": "Visbiežāk lietotie", + "Last used": "Pēdējie lietotie", + "At random": "Nejauši" } } diff --git a/l10n/mk.js b/l10n/mk.js index e7f077905..1309b932d 100644 --- a/l10n/mk.js +++ b/l10n/mk.js @@ -1312,7 +1312,18 @@ OC.L10N.register( "Profile fields saved." : "Полињата на профилот се зачувани.", "Save profile fields" : "Зачувај полиња на профилот", "The profile fields could not be loaded." : "Полињата на профилот не можеа да се вчитаат.", - "The profile fields could not be saved." : "Полињата на профилот не можеа да се зачуваат." + "The profile fields could not be saved." : "Полињата на профилот не можеа да се зачуваат.", + "Forget my usage" : "Заборави ја мојата употреба", + "Sorted alphabetically" : "Подредено по азбучен ред", + "Sorted by most used" : "Подредено по најкористени", + "Sorted by last used" : "Подредено по последно користени", + "Sorted at random" : "Подредено по случаен избор", + "Sort tiles" : "Подредување на плочките", + "By hand" : "Рачно", + "Alphabetically" : "Азбучно", + "Most used" : "Најкористени", + "Last used" : "Последно користени", + "At random" : "По случаен избор" }, "nplurals=2; plural=(n%10==1 && n%100!=11 ? 0 : 1);" ); diff --git a/l10n/mk.json b/l10n/mk.json index 967e1c28d..ab48acee8 100644 --- a/l10n/mk.json +++ b/l10n/mk.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Полињата на профилот се зачувани.", "Save profile fields": "Зачувај полиња на профилот", "The profile fields could not be loaded.": "Полињата на профилот не можеа да се вчитаат.", - "The profile fields could not be saved.": "Полињата на профилот не можеа да се зачуваат." + "The profile fields could not be saved.": "Полињата на профилот не можеа да се зачуваат.", + "Forget my usage": "Заборави ја мојата употреба", + "Sorted alphabetically": "Подредено по азбучен ред", + "Sorted by most used": "Подредено по најкористени", + "Sorted by last used": "Подредено по последно користени", + "Sorted at random": "Подредено по случаен избор", + "Sort tiles": "Подредување на плочките", + "By hand": "Рачно", + "Alphabetically": "Азбучно", + "Most used": "Најкористени", + "Last used": "Последно користени", + "At random": "По случаен избор" } } diff --git a/l10n/mt.js b/l10n/mt.js index 21ab65e16..efeb4b39b 100644 --- a/l10n/mt.js +++ b/l10n/mt.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Il-oqsma tal-profil ġew salvati.", "Save profile fields" : "Issejvja l-oqsma tal-profil", "The profile fields could not be loaded." : "Il-oqsma tal-profil ma setgħux jitgħabbew.", - "The profile fields could not be saved." : "Il-oqsma tal-profil ma setgħux jiġu salvati." + "The profile fields could not be saved." : "Il-oqsma tal-profil ma setgħux jiġu salvati.", + "Forget my usage" : "Insa l-użu tiegħi", + "Sorted alphabetically" : "Issortjat alfabetikament", + "Sorted by most used" : "Issortjat skont l-aktar użati", + "Sorted by last used" : "Issortjat skont l-aħħar użu", + "Sorted at random" : "Issortjat b'mod każwali", + "Sort tiles" : "Issortja l-madum", + "By hand" : "Bl-idejn", + "Alphabetically" : "Alfabetikament", + "Most used" : "L-aktar użati", + "Last used" : "L-aħħar użati", + "At random" : "B'mod każwali" }, "nplurals=4; plural=(n==1 ? 0 : n==0 || (n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20) ? 2 : 3);" ); diff --git a/l10n/mt.json b/l10n/mt.json index ff0697add..65e7e70ea 100644 --- a/l10n/mt.json +++ b/l10n/mt.json @@ -1290,6 +1290,17 @@ "Profile fields saved.": "Il-oqsma tal-profil ġew salvati.", "Save profile fields": "Issejvja l-oqsma tal-profil", "The profile fields could not be loaded.": "Il-oqsma tal-profil ma setgħux jitgħabbew.", - "The profile fields could not be saved.": "Il-oqsma tal-profil ma setgħux jiġu salvati." + "The profile fields could not be saved.": "Il-oqsma tal-profil ma setgħux jiġu salvati.", + "Forget my usage": "Insa l-użu tiegħi", + "Sorted alphabetically": "Issortjat alfabetikament", + "Sorted by most used": "Issortjat skont l-aktar użati", + "Sorted by last used": "Issortjat skont l-aħħar użu", + "Sorted at random": "Issortjat b'mod każwali", + "Sort tiles": "Issortja l-madum", + "By hand": "Bl-idejn", + "Alphabetically": "Alfabetikament", + "Most used": "L-aktar użati", + "Last used": "L-aħħar użati", + "At random": "B'mod każwali" } } diff --git a/l10n/nb.js b/l10n/nb.js index 226a19ce8..cadeff1a3 100644 --- a/l10n/nb.js +++ b/l10n/nb.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profilfeltene er lagret.", "Save profile fields" : "Lagre profilfelt", "The profile fields could not be loaded." : "Profilfeltene kunne ikke lastes inn.", - "The profile fields could not be saved." : "Profilfeltene kunne ikke lagres." + "The profile fields could not be saved." : "Profilfeltene kunne ikke lagres.", + "Forget my usage" : "Glem bruken min", + "Sorted alphabetically" : "Sortert alfabetisk", + "Sorted by most used" : "Sortert etter mest brukt", + "Sorted by last used" : "Sortert etter sist brukt", + "Sorted at random" : "Sortert tilfeldig", + "Sort tiles" : "Sorter fliser", + "By hand" : "Manuelt", + "Alphabetically" : "Alfabetisk", + "Most used" : "Mest brukt", + "Last used" : "Sist brukt", + "At random" : "Tilfeldig" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/nb.json b/l10n/nb.json index 60cb3f614..8059022ed 100644 --- a/l10n/nb.json +++ b/l10n/nb.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profilfeltene er lagret.", "Save profile fields": "Lagre profilfelt", "The profile fields could not be loaded.": "Profilfeltene kunne ikke lastes inn.", - "The profile fields could not be saved.": "Profilfeltene kunne ikke lagres." + "The profile fields could not be saved.": "Profilfeltene kunne ikke lagres.", + "Forget my usage": "Glem bruken min", + "Sorted alphabetically": "Sortert alfabetisk", + "Sorted by most used": "Sortert etter mest brukt", + "Sorted by last used": "Sortert etter sist brukt", + "Sorted at random": "Sortert tilfeldig", + "Sort tiles": "Sorter fliser", + "By hand": "Manuelt", + "Alphabetically": "Alfabetisk", + "Most used": "Mest brukt", + "Last used": "Sist brukt", + "At random": "Tilfeldig" } } diff --git a/l10n/nl.js b/l10n/nl.js index 59f9b10fa..8648eb478 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -1371,7 +1371,18 @@ OC.L10N.register( "Profile fields saved." : "Profielvelden opgeslagen.", "Save profile fields" : "Profielvelden opslaan", "The profile fields could not be loaded." : "De profielvelden konden niet worden geladen.", - "The profile fields could not be saved." : "De profielvelden konden niet worden opgeslagen." + "The profile fields could not be saved." : "De profielvelden konden niet worden opgeslagen.", + "Forget my usage" : "Mijn gebruik vergeten", + "Sorted alphabetically" : "Alfabetisch gesorteerd", + "Sorted by most used" : "Gesorteerd op meest gebruikt", + "Sorted by last used" : "Gesorteerd op laatst gebruikt", + "Sorted at random" : "Willekeurig gesorteerd", + "Sort tiles" : "Tegels sorteren", + "By hand" : "Handmatig", + "Alphabetically" : "Alfabetisch", + "Most used" : "Meest gebruikt", + "Last used" : "Laatst gebruikt", + "At random" : "Willekeurig" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/nl.json b/l10n/nl.json index c1070d47a..e774d1823 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -1348,6 +1348,17 @@ "Profile fields saved.": "Profielvelden opgeslagen.", "Save profile fields": "Profielvelden opslaan", "The profile fields could not be loaded.": "De profielvelden konden niet worden geladen.", - "The profile fields could not be saved.": "De profielvelden konden niet worden opgeslagen." + "The profile fields could not be saved.": "De profielvelden konden niet worden opgeslagen.", + "Forget my usage": "Mijn gebruik vergeten", + "Sorted alphabetically": "Alfabetisch gesorteerd", + "Sorted by most used": "Gesorteerd op meest gebruikt", + "Sorted by last used": "Gesorteerd op laatst gebruikt", + "Sorted at random": "Willekeurig gesorteerd", + "Sort tiles": "Tegels sorteren", + "By hand": "Handmatig", + "Alphabetically": "Alfabetisch", + "Most used": "Meest gebruikt", + "Last used": "Laatst gebruikt", + "At random": "Willekeurig" } } diff --git a/l10n/pl.js b/l10n/pl.js index 7754a5c2a..285dce17a 100644 --- a/l10n/pl.js +++ b/l10n/pl.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Pola profilu zapisane.", "Save profile fields" : "Zapisz pola profilu", "The profile fields could not be loaded." : "Nie udało się wczytać pól profilu.", - "The profile fields could not be saved." : "Nie udało się zapisać pól profilu." + "The profile fields could not be saved." : "Nie udało się zapisać pól profilu.", + "Forget my usage" : "Zapomnij moje użycie", + "Sorted alphabetically" : "Posortowane alfabetycznie", + "Sorted by most used" : "Posortowane według najczęściej używanych", + "Sorted by last used" : "Posortowane według ostatnio używanych", + "Sorted at random" : "Posortowane losowo", + "Sort tiles" : "Sortuj kafelki", + "By hand" : "Ręcznie", + "Alphabetically" : "Alfabetycznie", + "Most used" : "Najczęściej używane", + "Last used" : "Ostatnio używane", + "At random" : "Losowo" }, "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/pl.json b/l10n/pl.json index 60814ee56..e82900981 100644 --- a/l10n/pl.json +++ b/l10n/pl.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Pola profilu zapisane.", "Save profile fields": "Zapisz pola profilu", "The profile fields could not be loaded.": "Nie udało się wczytać pól profilu.", - "The profile fields could not be saved.": "Nie udało się zapisać pól profilu." + "The profile fields could not be saved.": "Nie udało się zapisać pól profilu.", + "Forget my usage": "Zapomnij moje użycie", + "Sorted alphabetically": "Posortowane alfabetycznie", + "Sorted by most used": "Posortowane według najczęściej używanych", + "Sorted by last used": "Posortowane według ostatnio używanych", + "Sorted at random": "Posortowane losowo", + "Sort tiles": "Sortuj kafelki", + "By hand": "Ręcznie", + "Alphabetically": "Alfabetycznie", + "Most used": "Najczęściej używane", + "Last used": "Ostatnio używane", + "At random": "Losowo" } } diff --git a/l10n/pt.js b/l10n/pt.js index 4cdf7ed03..e515e3e67 100644 --- a/l10n/pt.js +++ b/l10n/pt.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Campos do perfil guardados.", "Save profile fields" : "Guardar campos do perfil", "The profile fields could not be loaded." : "Não foi possível carregar os campos do perfil.", - "The profile fields could not be saved." : "Não foi possível guardar os campos do perfil." + "The profile fields could not be saved." : "Não foi possível guardar os campos do perfil.", + "Forget my usage" : "Esquecer a minha utilização", + "Sorted alphabetically" : "Ordenado alfabeticamente", + "Sorted by most used" : "Ordenado pelos mais usados", + "Sorted by last used" : "Ordenado pelo último uso", + "Sorted at random" : "Ordenado aleatoriamente", + "Sort tiles" : "Ordenar mosaicos", + "By hand" : "Manualmente", + "Alphabetically" : "Alfabeticamente", + "Most used" : "Mais usados", + "Last used" : "Último uso", + "At random" : "Aleatoriamente" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/pt.json b/l10n/pt.json index 41b8a8f8d..90c186688 100644 --- a/l10n/pt.json +++ b/l10n/pt.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Campos do perfil guardados.", "Save profile fields": "Guardar campos do perfil", "The profile fields could not be loaded.": "Não foi possível carregar os campos do perfil.", - "The profile fields could not be saved.": "Não foi possível guardar os campos do perfil." + "The profile fields could not be saved.": "Não foi possível guardar os campos do perfil.", + "Forget my usage": "Esquecer a minha utilização", + "Sorted alphabetically": "Ordenado alfabeticamente", + "Sorted by most used": "Ordenado pelos mais usados", + "Sorted by last used": "Ordenado pelo último uso", + "Sorted at random": "Ordenado aleatoriamente", + "Sort tiles": "Ordenar mosaicos", + "By hand": "Manualmente", + "Alphabetically": "Alfabeticamente", + "Most used": "Mais usados", + "Last used": "Último uso", + "At random": "Aleatoriamente" } } diff --git a/l10n/rm.js b/l10n/rm.js index 457c0a42f..9453ed16a 100644 --- a/l10n/rm.js +++ b/l10n/rm.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Champs dal profil memorisads.", "Save profile fields" : "Memorisar ils champs dal profil", "The profile fields could not be loaded." : "I n'è betg reussì da chargiar ils champs dal profil.", - "The profile fields could not be saved." : "I n'è betg reussì da memorisar ils champs dal profil." + "The profile fields could not be saved." : "I n'è betg reussì da memorisar ils champs dal profil.", + "Forget my usage" : "Emblidar mes diever", + "Sorted alphabetically" : "Zavrà alfabeticamain", + "Sorted by most used" : "Zavrà tenor il diever il pli frequent", + "Sorted by last used" : "Zavrà tenor l'ultim diever", + "Sorted at random" : "Zavrà a chaschun", + "Sort tiles" : "Zavrar ils quadrels", + "By hand" : "A maun", + "Alphabetically" : "Alfabeticamain", + "Most used" : "Il pli savens duvrà", + "Last used" : "Duvrà l'ultim", + "At random" : "A chaschun" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/rm.json b/l10n/rm.json index 56794d514..566dd6a70 100644 --- a/l10n/rm.json +++ b/l10n/rm.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Champs dal profil memorisads.", "Save profile fields": "Memorisar ils champs dal profil", "The profile fields could not be loaded.": "I n'è betg reussì da chargiar ils champs dal profil.", - "The profile fields could not be saved.": "I n'è betg reussì da memorisar ils champs dal profil." + "The profile fields could not be saved.": "I n'è betg reussì da memorisar ils champs dal profil.", + "Forget my usage": "Emblidar mes diever", + "Sorted alphabetically": "Zavrà alfabeticamain", + "Sorted by most used": "Zavrà tenor il diever il pli frequent", + "Sorted by last used": "Zavrà tenor l'ultim diever", + "Sorted at random": "Zavrà a chaschun", + "Sort tiles": "Zavrar ils quadrels", + "By hand": "A maun", + "Alphabetically": "Alfabeticamain", + "Most used": "Il pli savens duvrà", + "Last used": "Duvrà l'ultim", + "At random": "A chaschun" } } diff --git a/l10n/ro.js b/l10n/ro.js index 350c650fa..358d66e1a 100644 --- a/l10n/ro.js +++ b/l10n/ro.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Câmpurile de profil au fost salvate.", "Save profile fields" : "Salvează câmpurile de profil", "The profile fields could not be loaded." : "Câmpurile de profil nu au putut fi încărcate.", - "The profile fields could not be saved." : "Câmpurile de profil nu au putut fi salvate." + "The profile fields could not be saved." : "Câmpurile de profil nu au putut fi salvate.", + "Forget my usage" : "Uită utilizarea mea", + "Sorted alphabetically" : "Sortat alfabetic", + "Sorted by most used" : "Sortat după cele mai folosite", + "Sorted by last used" : "Sortat după ultima utilizare", + "Sorted at random" : "Sortat aleatoriu", + "Sort tiles" : "Sortează dalele", + "By hand" : "Manual", + "Alphabetically" : "Alfabetic", + "Most used" : "Cele mai folosite", + "Last used" : "Ultima utilizare", + "At random" : "Aleatoriu" }, "nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);" ); diff --git a/l10n/ro.json b/l10n/ro.json index bf0d96182..4ce56bcf0 100644 --- a/l10n/ro.json +++ b/l10n/ro.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Câmpurile de profil au fost salvate.", "Save profile fields": "Salvează câmpurile de profil", "The profile fields could not be loaded.": "Câmpurile de profil nu au putut fi încărcate.", - "The profile fields could not be saved.": "Câmpurile de profil nu au putut fi salvate." + "The profile fields could not be saved.": "Câmpurile de profil nu au putut fi salvate.", + "Forget my usage": "Uită utilizarea mea", + "Sorted alphabetically": "Sortat alfabetic", + "Sorted by most used": "Sortat după cele mai folosite", + "Sorted by last used": "Sortat după ultima utilizare", + "Sorted at random": "Sortat aleatoriu", + "Sort tiles": "Sortează dalele", + "By hand": "Manual", + "Alphabetically": "Alfabetic", + "Most used": "Cele mai folosite", + "Last used": "Ultima utilizare", + "At random": "Aleatoriu" } } diff --git a/l10n/ru.js b/l10n/ru.js index 6eb71e4a6..215c42ce6 100644 --- a/l10n/ru.js +++ b/l10n/ru.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Поля профиля сохранены.", "Save profile fields" : "Сохранить поля профиля", "The profile fields could not be loaded." : "Не удалось загрузить поля профиля.", - "The profile fields could not be saved." : "Не удалось сохранить поля профиля." + "The profile fields could not be saved." : "Не удалось сохранить поля профиля.", + "Forget my usage" : "Забыть моё использование", + "Sorted alphabetically" : "Отсортировано по алфавиту", + "Sorted by most used" : "Отсортировано по частоте использования", + "Sorted by last used" : "Отсортировано по последнему использованию", + "Sorted at random" : "Отсортировано случайно", + "Sort tiles" : "Сортировка плиток", + "By hand" : "Вручную", + "Alphabetically" : "По алфавиту", + "Most used" : "Самые используемые", + "Last used" : "Последние использованные", + "At random" : "Случайно" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/ru.json b/l10n/ru.json index 9958f3d25..752062dab 100644 --- a/l10n/ru.json +++ b/l10n/ru.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Поля профиля сохранены.", "Save profile fields": "Сохранить поля профиля", "The profile fields could not be loaded.": "Не удалось загрузить поля профиля.", - "The profile fields could not be saved.": "Не удалось сохранить поля профиля." + "The profile fields could not be saved.": "Не удалось сохранить поля профиля.", + "Forget my usage": "Забыть моё использование", + "Sorted alphabetically": "Отсортировано по алфавиту", + "Sorted by most used": "Отсортировано по частоте использования", + "Sorted by last used": "Отсортировано по последнему использованию", + "Sorted at random": "Отсортировано случайно", + "Sort tiles": "Сортировка плиток", + "By hand": "Вручную", + "Alphabetically": "По алфавиту", + "Most used": "Самые используемые", + "Last used": "Последние использованные", + "At random": "Случайно" } } diff --git a/l10n/sk.js b/l10n/sk.js index 972c0e9cc..dc9930564 100644 --- a/l10n/sk.js +++ b/l10n/sk.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Polia profilu sú uložené.", "Save profile fields" : "Uložiť polia profilu", "The profile fields could not be loaded." : "Polia profilu sa nepodarilo načítať.", - "The profile fields could not be saved." : "Polia profilu sa nepodarilo uložiť." + "The profile fields could not be saved." : "Polia profilu sa nepodarilo uložiť.", + "Forget my usage" : "Zabudnúť moje používanie", + "Sorted alphabetically" : "Zoradené abecedne", + "Sorted by most used" : "Zoradené podľa najpoužívanejších", + "Sorted by last used" : "Zoradené podľa naposledy použitých", + "Sorted at random" : "Zoradené náhodne", + "Sort tiles" : "Zoradenie dlaždíc", + "By hand" : "Ručne", + "Alphabetically" : "Abecedne", + "Most used" : "Najpoužívanejšie", + "Last used" : "Naposledy použité", + "At random" : "Náhodne" }, "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" ); diff --git a/l10n/sk.json b/l10n/sk.json index 84474ab28..f4ad5b771 100644 --- a/l10n/sk.json +++ b/l10n/sk.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Polia profilu sú uložené.", "Save profile fields": "Uložiť polia profilu", "The profile fields could not be loaded.": "Polia profilu sa nepodarilo načítať.", - "The profile fields could not be saved.": "Polia profilu sa nepodarilo uložiť." + "The profile fields could not be saved.": "Polia profilu sa nepodarilo uložiť.", + "Forget my usage": "Zabudnúť moje používanie", + "Sorted alphabetically": "Zoradené abecedne", + "Sorted by most used": "Zoradené podľa najpoužívanejších", + "Sorted by last used": "Zoradené podľa naposledy použitých", + "Sorted at random": "Zoradené náhodne", + "Sort tiles": "Zoradenie dlaždíc", + "By hand": "Ručne", + "Alphabetically": "Abecedne", + "Most used": "Najpoužívanejšie", + "Last used": "Naposledy použité", + "At random": "Náhodne" } } diff --git a/l10n/sl.js b/l10n/sl.js index 368c7922b..aae665454 100644 --- a/l10n/sl.js +++ b/l10n/sl.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Polja profila so shranjena.", "Save profile fields" : "Shrani polja profila", "The profile fields could not be loaded." : "Polj profila ni bilo mogoče naložiti.", - "The profile fields could not be saved." : "Polj profila ni bilo mogoče shraniti." + "The profile fields could not be saved." : "Polj profila ni bilo mogoče shraniti.", + "Forget my usage" : "Pozabi mojo uporabo", + "Sorted alphabetically" : "Razvrščeno po abecedi", + "Sorted by most used" : "Razvrščeno po najpogosteje uporabljenih", + "Sorted by last used" : "Razvrščeno po zadnji uporabi", + "Sorted at random" : "Razvrščeno naključno", + "Sort tiles" : "Razvrsti ploščice", + "By hand" : "Ročno", + "Alphabetically" : "Po abecedi", + "Most used" : "Najpogosteje uporabljeno", + "Last used" : "Nazadnje uporabljeno", + "At random" : "Naključno" }, "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);" ); diff --git a/l10n/sl.json b/l10n/sl.json index 4fadf33d5..a46e1bfe9 100644 --- a/l10n/sl.json +++ b/l10n/sl.json @@ -1290,6 +1290,17 @@ "Profile fields saved.": "Polja profila so shranjena.", "Save profile fields": "Shrani polja profila", "The profile fields could not be loaded.": "Polj profila ni bilo mogoče naložiti.", - "The profile fields could not be saved.": "Polj profila ni bilo mogoče shraniti." + "The profile fields could not be saved.": "Polj profila ni bilo mogoče shraniti.", + "Forget my usage": "Pozabi mojo uporabo", + "Sorted alphabetically": "Razvrščeno po abecedi", + "Sorted by most used": "Razvrščeno po najpogosteje uporabljenih", + "Sorted by last used": "Razvrščeno po zadnji uporabi", + "Sorted at random": "Razvrščeno naključno", + "Sort tiles": "Razvrsti ploščice", + "By hand": "Ročno", + "Alphabetically": "Po abecedi", + "Most used": "Najpogosteje uporabljeno", + "Last used": "Nazadnje uporabljeno", + "At random": "Naključno" } } diff --git a/l10n/sq.js b/l10n/sq.js index cdcf747d1..0e6961f3d 100644 --- a/l10n/sq.js +++ b/l10n/sq.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Fushat e profilit u ruajtën.", "Save profile fields" : "Ruaj fushat e profilit", "The profile fields could not be loaded." : "Fushat e profilit nuk u ngarkuan.", - "The profile fields could not be saved." : "Fushat e profilit nuk u ruajtën." + "The profile fields could not be saved." : "Fushat e profilit nuk u ruajtën.", + "Forget my usage" : "Harro përdorimin tim", + "Sorted alphabetically" : "Renditur sipas alfabetit", + "Sorted by most used" : "Renditur sipas më të përdorurave", + "Sorted by last used" : "Renditur sipas përdorimit të fundit", + "Sorted at random" : "Renditur rastësisht", + "Sort tiles" : "Rendit pllakat", + "By hand" : "Me dorë", + "Alphabetically" : "Sipas alfabetit", + "Most used" : "Më të përdorurat", + "Last used" : "Përdorur së fundi", + "At random" : "Rastësisht" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/sq.json b/l10n/sq.json index 1fb6fecd0..558053019 100644 --- a/l10n/sq.json +++ b/l10n/sq.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Fushat e profilit u ruajtën.", "Save profile fields": "Ruaj fushat e profilit", "The profile fields could not be loaded.": "Fushat e profilit nuk u ngarkuan.", - "The profile fields could not be saved.": "Fushat e profilit nuk u ruajtën." + "The profile fields could not be saved.": "Fushat e profilit nuk u ruajtën.", + "Forget my usage": "Harro përdorimin tim", + "Sorted alphabetically": "Renditur sipas alfabetit", + "Sorted by most used": "Renditur sipas më të përdorurave", + "Sorted by last used": "Renditur sipas përdorimit të fundit", + "Sorted at random": "Renditur rastësisht", + "Sort tiles": "Rendit pllakat", + "By hand": "Me dorë", + "Alphabetically": "Sipas alfabetit", + "Most used": "Më të përdorurat", + "Last used": "Përdorur së fundi", + "At random": "Rastësisht" } } diff --git a/l10n/sr.js b/l10n/sr.js index 612b24d10..d8bca7a02 100644 --- a/l10n/sr.js +++ b/l10n/sr.js @@ -1312,6 +1312,17 @@ OC.L10N.register( "Profile fields saved." : "Поља профила су сачувана.", "Save profile fields" : "Сачувај поља профила", "The profile fields could not be loaded." : "Поља профила нису могла да се учитају.", - "The profile fields could not be saved." : "Поља профила нису могла да се сачувају." + "The profile fields could not be saved." : "Поља профила нису могла да се сачувају.", + "Forget my usage" : "Заборави моје коришћење", + "Sorted alphabetically" : "Поређано абецедно", + "Sorted by most used" : "Поређано по најкоришћенијим", + "Sorted by last used" : "Поређано по последње коришћеним", + "Sorted at random" : "Поређано насумично", + "Sort tiles" : "Поређај плочице", + "By hand" : "Ручно", + "Alphabetically" : "Абецедно", + "Most used" : "Најкоришћеније", + "Last used" : "Последње коришћено", + "At random" : "Насумично" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"); diff --git a/l10n/sr.json b/l10n/sr.json index fa807ae69..9fc9ef3b4 100644 --- a/l10n/sr.json +++ b/l10n/sr.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Поља профила су сачувана.", "Save profile fields": "Сачувај поља профила", "The profile fields could not be loaded.": "Поља профила нису могла да се учитају.", - "The profile fields could not be saved.": "Поља профила нису могла да се сачувају." + "The profile fields could not be saved.": "Поља профила нису могла да се сачувају.", + "Forget my usage": "Заборави моје коришћење", + "Sorted alphabetically": "Поређано абецедно", + "Sorted by most used": "Поређано по најкоришћенијим", + "Sorted by last used": "Поређано по последње коришћеним", + "Sorted at random": "Поређано насумично", + "Sort tiles": "Поређај плочице", + "By hand": "Ручно", + "Alphabetically": "Абецедно", + "Most used": "Најкоришћеније", + "Last used": "Последње коришћено", + "At random": "Насумично" } } diff --git a/l10n/sv.js b/l10n/sv.js index b00492b8a..9f2c290bf 100644 --- a/l10n/sv.js +++ b/l10n/sv.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profilfälten har sparats.", "Save profile fields" : "Spara profilfält", "The profile fields could not be loaded." : "Profilfälten kunde inte läsas in.", - "The profile fields could not be saved." : "Profilfälten kunde inte sparas." + "The profile fields could not be saved." : "Profilfälten kunde inte sparas.", + "Forget my usage" : "Glöm min användning", + "Sorted alphabetically" : "Sorterat i bokstavsordning", + "Sorted by most used" : "Sorterat efter mest använda", + "Sorted by last used" : "Sorterat efter senast använda", + "Sorted at random" : "Sorterat slumpmässigt", + "Sort tiles" : "Sortera paneler", + "By hand" : "Manuellt", + "Alphabetically" : "I bokstavsordning", + "Most used" : "Mest använda", + "Last used" : "Senast använda", + "At random" : "Slumpmässigt" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/sv.json b/l10n/sv.json index 01609597c..3a86552d0 100644 --- a/l10n/sv.json +++ b/l10n/sv.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profilfälten har sparats.", "Save profile fields": "Spara profilfält", "The profile fields could not be loaded.": "Profilfälten kunde inte läsas in.", - "The profile fields could not be saved.": "Profilfälten kunde inte sparas." + "The profile fields could not be saved.": "Profilfälten kunde inte sparas.", + "Forget my usage": "Glöm min användning", + "Sorted alphabetically": "Sorterat i bokstavsordning", + "Sorted by most used": "Sorterat efter mest använda", + "Sorted by last used": "Sorterat efter senast använda", + "Sorted at random": "Sorterat slumpmässigt", + "Sort tiles": "Sortera paneler", + "By hand": "Manuellt", + "Alphabetically": "I bokstavsordning", + "Most used": "Mest använda", + "Last used": "Senast använda", + "At random": "Slumpmässigt" } } diff --git a/l10n/tr.js b/l10n/tr.js index c6aedbaa1..3089b26ee 100644 --- a/l10n/tr.js +++ b/l10n/tr.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profil alanları kaydedildi.", "Save profile fields" : "Profil alanlarını kaydet", "The profile fields could not be loaded." : "Profil alanları yüklenemedi.", - "The profile fields could not be saved." : "Profil alanları kaydedilemedi." + "The profile fields could not be saved." : "Profil alanları kaydedilemedi.", + "Forget my usage" : "Kullanımımı unut", + "Sorted alphabetically" : "Alfabetik sıralandı", + "Sorted by most used" : "En çok kullanılana göre sıralandı", + "Sorted by last used" : "Son kullanılana göre sıralandı", + "Sorted at random" : "Rastgele sıralandı", + "Sort tiles" : "Kutucukları sırala", + "By hand" : "Elle", + "Alphabetically" : "Alfabetik", + "Most used" : "En çok kullanılan", + "Last used" : "Son kullanılan", + "At random" : "Rastgele" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/tr.json b/l10n/tr.json index 377f35188..155e964f0 100644 --- a/l10n/tr.json +++ b/l10n/tr.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profil alanları kaydedildi.", "Save profile fields": "Profil alanlarını kaydet", "The profile fields could not be loaded.": "Profil alanları yüklenemedi.", - "The profile fields could not be saved.": "Profil alanları kaydedilemedi." + "The profile fields could not be saved.": "Profil alanları kaydedilemedi.", + "Forget my usage": "Kullanımımı unut", + "Sorted alphabetically": "Alfabetik sıralandı", + "Sorted by most used": "En çok kullanılana göre sıralandı", + "Sorted by last used": "Son kullanılana göre sıralandı", + "Sorted at random": "Rastgele sıralandı", + "Sort tiles": "Kutucukları sırala", + "By hand": "Elle", + "Alphabetically": "Alfabetik", + "Most used": "En çok kullanılan", + "Last used": "Son kullanılan", + "At random": "Rastgele" } } diff --git a/l10n/uk.js b/l10n/uk.js index eee0990e0..be5b75f80 100644 --- a/l10n/uk.js +++ b/l10n/uk.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Поля профілю збережено.", "Save profile fields" : "Зберегти поля профілю", "The profile fields could not be loaded." : "Не вдалося завантажити поля профілю.", - "The profile fields could not be saved." : "Не вдалося зберегти поля профілю." + "The profile fields could not be saved." : "Не вдалося зберегти поля профілю.", + "Forget my usage" : "Забути моє використання", + "Sorted alphabetically" : "Відсортовано за абеткою", + "Sorted by most used" : "Відсортовано за частотою використання", + "Sorted by last used" : "Відсортовано за останнім використанням", + "Sorted at random" : "Відсортовано випадково", + "Sort tiles" : "Сортування плиток", + "By hand" : "Вручну", + "Alphabetically" : "За абеткою", + "Most used" : "Найчастіше використовувані", + "Last used" : "Останні використані", + "At random" : "Випадково" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/uk.json b/l10n/uk.json index 50e244f2e..5696c7113 100644 --- a/l10n/uk.json +++ b/l10n/uk.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Поля профілю збережено.", "Save profile fields": "Зберегти поля профілю", "The profile fields could not be loaded.": "Не вдалося завантажити поля профілю.", - "The profile fields could not be saved.": "Не вдалося зберегти поля профілю." + "The profile fields could not be saved.": "Не вдалося зберегти поля профілю.", + "Forget my usage": "Забути моє використання", + "Sorted alphabetically": "Відсортовано за абеткою", + "Sorted by most used": "Відсортовано за частотою використання", + "Sorted by last used": "Відсортовано за останнім використанням", + "Sorted at random": "Відсортовано випадково", + "Sort tiles": "Сортування плиток", + "By hand": "Вручну", + "Alphabetically": "За абеткою", + "Most used": "Найчастіше використовувані", + "Last used": "Останні використані", + "At random": "Випадково" } } diff --git a/src/components/Widgets/Renderers/ContainerWidget.vue b/src/components/Widgets/Renderers/ContainerWidget.vue index 1296f97d2..0c071a918 100644 --- a/src/components/Widgets/Renderers/ContainerWidget.vue +++ b/src/components/Widgets/Renderers/ContainerWidget.vue @@ -8,16 +8,27 @@

{{ titleText }}

+ -
+
+ :gs-h="child.gridHeight || 2" + @click="onChildClick(child)">
@@ -30,8 +41,21 @@ import ContainerChild from './ContainerChild.vue' import { getNestedGridOptions, + NESTED_COLUMNS, useNestedGridManager, } from '../../../composables/useNestedGridManager.js' +import { + forgetLocalTileUse, + randomRankFor, + readLocalTileUse, + recordLocalTileUse, +} from '../../../composables/useTileClickTracking.js' +import { + childTitle, + orderChildren, + reflowChildren, + SORT_MODES, +} from '../../../utils/sortContainerTiles.js' const PADDING_TOKENS = Object.freeze({ none: '0', @@ -81,6 +105,8 @@ export default { data() { return { gridInstance: null, + // The viewer's own tile use, read once per page load (REQ-TSO-002). + use: readLocalTileUse(), } }, @@ -91,6 +117,77 @@ export default { return Array.isArray(list) ? list : [] }, + /** @spec openspec/specs/container-widget/spec.md */ + sortBy() { + const value = this.content?.sortBy + return SORT_MODES.includes(value) ? value : 'manual' + }, + + /** + * The children as shown: the stored layout in edit mode or by hand, + * otherwise ordered and reflowed at view time (REQ-TSO-001). + * + * @spec openspec/specs/container-widget/spec.md + */ + displayChildren() { + if (this.editMode || this.sortBy === 'manual') { + return this.children + } + // REQ-TSO-002: without browser storage there is no use to sort by. + if ( + this.use === null + && ['most-used', 'last-used'].includes(this.sortBy) + ) { + return this.children + } + const ordered = orderChildren(this.children, this.sortBy, { + use: this.use || {}, + keyOf: this.useKey, + rankOf: randomRankFor, + collator: new Intl.Collator(undefined, { + sensitivity: 'base', + numeric: true, + }), + }) + return reflowChildren(ordered, NESTED_COLUMNS) + }, + + /** @spec openspec/specs/container-widget/spec.md */ + sortDescription() { + const labels = { + alphabetical: t('launchpad', 'Sorted alphabetically'), + 'most-used': t('launchpad', 'Sorted by most used'), + 'last-used': t('launchpad', 'Sorted by last used'), + random: t('launchpad', 'Sorted at random'), + } + return this.editMode ? '' : labels[this.sortBy] || '' + }, + + /** + * Offer "Forget my usage" when the order reads the viewer's use and + * there is use to forget (REQ-TSO-003). + * + * @spec openspec/specs/container-widget/spec.md + */ + canForget() { + if (this.editMode || !['most-used', 'last-used'].includes(this.sortBy)) { + return false + } + return ( + this.use !== null + && this.children.some((child) => this.use[this.useKey(child)]) + ) + }, + + /** @spec openspec/specs/container-widget/spec.md */ + layoutSignature() { + return this.displayChildren + .map( + (child) => `${this.useKey(child)}@${child.gridX},${child.gridY}`, + ) + .join('|') + }, + /** @spec openspec/specs/container-widget/spec.md */ backgroundColor() { const value = this.content?.backgroundColor @@ -137,6 +234,22 @@ export default { }, }, + watch: { + /** + * A new view-time layout (after "Forget my usage") rebuilds the + * inner grid, which reads positions only when it starts. + * + * @spec openspec/specs/container-widget/spec.md + */ + layoutSignature() { + if (this.editMode || !this.gridInstance) { + return + } + this.destroyInnerGrid() + this.$nextTick(() => this.initInnerGrid()) + }, + }, + mounted() { this.initInnerGrid() }, @@ -146,6 +259,49 @@ export default { }, methods: { + /** + * The key a child's use is stored under: its placement id, else its + * uuid, else its title. + * + * @param {object} child The child placement. + * @return {string} The key. + * @spec openspec/specs/container-widget/spec.md + */ + useKey(child) { + if (child?.id !== undefined && child?.id !== null) { + return String(child.id) + } + if (typeof child?.uuid === 'string' && child.uuid !== '') { + return child.uuid + } + return `title:${childTitle(child)}` + }, + + /** + * Count a click on a tile in this browser (REQ-TSO-002). The order + * does not change until the next page load, so tiles never jump + * under the pointer. + * + * @param {object} child The clicked child placement. + * @spec openspec/specs/container-widget/spec.md + */ + onChildClick(child) { + if (this.editMode) { + return + } + recordLocalTileUse(this.useKey(child)) + }, + + /** + * Forget this browser's use of this container's tiles (REQ-TSO-003). + * + * @spec openspec/specs/container-widget/spec.md + */ + forgetUsage() { + forgetLocalTileUse(this.children.map((child) => this.useKey(child))) + this.use = readLocalTileUse() + }, + /** * Stable key for v-for over child placements. Falls back to the * loop index when a child has neither id nor uuid, which keeps @@ -222,7 +378,8 @@ export default { * @spec openspec/specs/container-widget/spec.md */ onGridChange(_event, nodes) { - if (!Array.isArray(nodes) || nodes.length === 0) { + // A view-time reflow is never written back (REQ-TSO-001). + if (!this.editMode || !Array.isArray(nodes) || nodes.length === 0) { return } const byKey = new Map() @@ -309,6 +466,18 @@ export default { min-height: 0; } +.container-widget__forget { + align-self: flex-start; + padding: 2px 8px; + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + background: var(--color-background-hover); + color: var(--color-main-text); + font-size: 12px; + cursor: pointer; + pointer-events: auto; +} + .container-widget__child { /* REQ-CONT-004: child wrappers re-enable pointer events so clicks on a child fire the child's own handlers — the container itself diff --git a/src/components/Widgets/Renderers/ContainerWidgetForm.vue b/src/components/Widgets/Renderers/ContainerWidgetForm.vue new file mode 100644 index 000000000..ca34e6768 --- /dev/null +++ b/src/components/Widgets/Renderers/ContainerWidgetForm.vue @@ -0,0 +1,139 @@ + + + + + + + diff --git a/src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js b/src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js new file mode 100644 index 000000000..20d0fe9e6 --- /dev/null +++ b/src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js @@ -0,0 +1,226 @@ +/** + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * Vitest unit tests for tile sorting in `ContainerWidget.vue` + * (launcher-tile-sorting, REQ-TSO-001..004): the view-time order per mode, + * the reflow that keeps sizes and never writes back, most and last used read + * from the viewer's browser only, "Forget my usage", and the once-per-load + * shuffle. + */ + +import { mount } from '@vue/test-utils' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import ContainerWidget from '../ContainerWidget.vue' +import { + __resetTileUseForTest, + recordLocalTileUse, +} from '../../../../composables/useTileClickTracking.js' + +vi.mock('@nextcloud/axios', () => ({ + default: { + get: vi.fn(() => Promise.resolve({ data: {} })), + post: vi.fn(() => Promise.resolve({})), + }, +})) + +function tile(id, title, x) { + return { + id, + type: 'tile', + content: { title }, + gridX: x, + gridY: 0, + gridWidth: 1, + gridHeight: 1, + } +} + +// Stored in this order: Zaaksysteem, Agenda, Mail. +const PLACEMENTS = [ + tile(1, 'Zaaksysteem', 0), + tile(2, 'Agenda', 1), + tile(3, 'Mail', 2), +] + +function mountSorted(sortBy, editMode = false) { + return mount(ContainerWidget, { + props: { + content: { title: 'Applicaties', sortBy, placements: PLACEMENTS }, + editMode, + }, + global: { + stubs: { + ContainerChild: { + props: ['placement'], + template: + '{{ placement.content.title }}', + }, + }, + }, + }) +} + +function order(wrapper) { + return wrapper + .findAll('.container-widget__child') + .map((item) => ({ + title: item.text(), + x: Number(item.attributes('gs-x')), + y: Number(item.attributes('gs-y')), + })) + .sort((a, b) => a.y - b.y || a.x - b.x) + .map((item) => item.title) +} + +describe('ContainerWidget tile sorting', () => { + beforeEach(() => { + globalThis.t = (_app, key) => key + localStorage.clear() + __resetTileUseForTest() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('REQ-TSO-001: alphabetical shows Agenda, Mail, Zaaksysteem from left to right', () => { + expect(order(mountSorted('alphabetical'))).toEqual([ + 'Agenda', + 'Mail', + 'Zaaksysteem', + ]) + }) + + it('REQ-TSO-001: by hand keeps the stored order', () => { + expect(order(mountSorted('manual'))).toEqual([ + 'Zaaksysteem', + 'Agenda', + 'Mail', + ]) + }) + + it('REQ-TSO-001: edit mode shows the stored positions and nothing is written back', () => { + const wrapper = mountSorted('alphabetical', true) + expect(order(wrapper)).toEqual(['Zaaksysteem', 'Agenda', 'Mail']) + expect(mountSorted('alphabetical').emitted('update:content')).toBeUndefined() + }) + + it('REQ-TSO-001: the reflow keeps each tile size and wraps at four columns', () => { + const wide = [ + { ...tile(1, 'B', 0), gridWidth: 3 }, + { ...tile(2, 'A', 3), gridWidth: 2, gridHeight: 2 }, + ] + const wrapper = mount(ContainerWidget, { + props: { content: { sortBy: 'alphabetical', placements: wide } }, + global: { stubs: { ContainerChild: true } }, + }) + const items = wrapper.findAll('.container-widget__child') + const a = items.find((item) => item.attributes('gs-w') === '2') + const b = items.find((item) => item.attributes('gs-w') === '3') + expect([ + a.attributes('gs-x'), + a.attributes('gs-y'), + a.attributes('gs-h'), + ]).toEqual(['0', '0', '2']) + expect([b.attributes('gs-x'), b.attributes('gs-y')]).toEqual(['0', '2']) + }) + + it('REQ-TSO-002: a daily app moves to the front, and unused tiles fall back to alphabetical', () => { + recordLocalTileUse(1) + recordLocalTileUse(1) + recordLocalTileUse(1) + expect(order(mountSorted('most-used'))).toEqual([ + 'Zaaksysteem', + 'Agenda', + 'Mail', + ]) + + localStorage.clear() + __resetTileUseForTest() + recordLocalTileUse(3) + expect(order(mountSorted('most-used'))).toEqual([ + 'Mail', + 'Agenda', + 'Zaaksysteem', + ]) + }) + + it('REQ-TSO-002: clicking a tile in the container counts it in the browser only', async () => { + const axios = (await import('@nextcloud/axios')).default + const wrapper = mountSorted('most-used') + await wrapper.findAll('.container-widget__child')[2].trigger('click') + + const stored = JSON.parse(localStorage.getItem('launchpad.tileUse')) + expect(Object.keys(stored)).toHaveLength(1) + expect(Object.values(stored)[0].count).toBe(1) + expect(axios.post).not.toHaveBeenCalled() + }) + + it('REQ-TSO-002: last used puts the latest click first', () => { + vi.spyOn(Date, 'now').mockReturnValueOnce(1000).mockReturnValueOnce(2000) + recordLocalTileUse(2) + recordLocalTileUse(3) + expect(order(mountSorted('last-used'))).toEqual([ + 'Mail', + 'Agenda', + 'Zaaksysteem', + ]) + }) + + it('REQ-TSO-002: blocked storage shows the stored order without an error', () => { + vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { + throw new Error('SecurityError') + }) + expect(order(mountSorted('most-used'))).toEqual([ + 'Zaaksysteem', + 'Agenda', + 'Mail', + ]) + }) + + it('REQ-TSO-003: forget my usage returns the container to alphabetical order', async () => { + recordLocalTileUse(1) + const wrapper = mountSorted('most-used') + expect(order(wrapper)).toEqual(['Zaaksysteem', 'Agenda', 'Mail']) + + const forget = wrapper.find('button.container-widget__forget') + expect(forget.text()).toBe('Forget my usage') + await forget.trigger('click') + + expect(order(wrapper)).toEqual(['Agenda', 'Mail', 'Zaaksysteem']) + expect(wrapper.find('button.container-widget__forget').exists()).toBe(false) + }) + + it('REQ-TSO-003: the forget button is not offered for alphabetical or by hand', () => { + recordLocalTileUse(1) + expect( + mountSorted('alphabetical') + .find('button.container-widget__forget') + .exists(), + ).toBe(false) + }) + + it('REQ-TSO-004: random order holds for the page load', async () => { + const first = order(mountSorted('random')) + const wrapper = mountSorted('random') + await wrapper.setProps({ + content: { sortBy: 'random', placements: [...PLACEMENTS] }, + }) + expect(order(wrapper)).toEqual(first) + expect([...first].sort()).toEqual(['Agenda', 'Mail', 'Zaaksysteem']) + }) + + it('announces the sort order to assistive technology', () => { + expect( + mountSorted('most-used') + .find('.launchpad-container-grid') + .attributes('aria-description'), + ).toBe('Sorted by most used') + expect( + mountSorted('manual') + .find('.launchpad-container-grid') + .attributes('aria-description'), + ).toBeUndefined() + }) +}) diff --git a/src/components/Widgets/Renderers/__tests__/ContainerWidgetForm.spec.js b/src/components/Widgets/Renderers/__tests__/ContainerWidgetForm.spec.js new file mode 100644 index 000000000..0511540fd --- /dev/null +++ b/src/components/Widgets/Renderers/__tests__/ContainerWidgetForm.spec.js @@ -0,0 +1,110 @@ +/** + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * Vitest unit tests for `ContainerWidgetForm.vue` (REQ-CONT-007 as modified + * by launcher-tile-sorting): the three communal fields plus exactly one + * "Sort tiles" select, and the emitted content carries `sortBy`. + */ + +import { mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import ContainerWidgetForm from '../ContainerWidgetForm.vue' + +vi.mock('@conduction/nextcloud-vue', () => ({ + CnContainerWidgetForm: { + name: 'CnContainerWidgetForm', + emits: ['update:content'], + template: + '
', + }, +})) + +const NcSelectStub = { + name: 'NcSelect', + props: ['modelValue', 'options', 'inputLabel'], + emits: ['update:modelValue'], + template: + '
{{ modelValue }}
', +} + +describe('ContainerWidgetForm', () => { + beforeEach(() => { + globalThis.t = (_app, key) => key + }) + + function mountForm(props = {}) { + return mount(ContainerWidgetForm, { + props, + global: { stubs: { NcSelect: NcSelectStub } }, + }) + } + + it('renders the communal form plus exactly one Sort tiles select, defaulting to by hand', () => { + const wrapper = mountForm() + expect(wrapper.find('.communal').exists()).toBe(true) + const selects = wrapper.findAll('.nc-select') + expect(selects).toHaveLength(1) + expect(selects[0].attributes('data-label')).toBe('Sort tiles') + expect(selects[0].text()).toBe('manual') + const values = wrapper + .findComponent(NcSelectStub) + .props('options') + .map((option) => option.value) + expect(values).toEqual([ + 'manual', + 'alphabetical', + 'most-used', + 'last-used', + 'random', + ]) + }) + + it('emits the communal fields together with sortBy', async () => { + const wrapper = mountForm({ + editingWidget: { + content: { + title: 'Applicaties', + padding: 'small', + placements: [{ id: 1 }], + sortBy: 'most-used', + }, + }, + }) + expect(wrapper.find('.nc-select').text()).toBe('most-used') + + wrapper + .findComponent(NcSelectStub) + .vm.$emit('update:modelValue', 'alphabetical') + wrapper + .findComponent({ name: 'CnContainerWidgetForm' }) + .vm.$emit('update:content', { + placements: [{ id: 1 }], + backgroundColor: '#fff', + padding: 'small', + title: 'Apps', + }) + + const emitted = wrapper.emitted('update:content') + expect(emitted[0][0]).toMatchObject({ + title: 'Applicaties', + sortBy: 'alphabetical', + }) + expect(emitted[1][0]).toEqual({ + placements: [{ id: 1 }], + backgroundColor: '#fff', + padding: 'small', + title: 'Apps', + sortBy: 'alphabetical', + }) + expect(wrapper.vm.validate()).toEqual([]) + }) + + it('drops an unknown sort mode back to by hand', () => { + expect( + mountForm({ value: { sortBy: 'sideways' } }) + .find('.nc-select') + .text(), + ).toBe('manual') + }) +}) diff --git a/src/composables/useTileClickTracking.js b/src/composables/useTileClickTracking.js index 27b1798e8..fb855215a 100644 --- a/src/composables/useTileClickTracking.js +++ b/src/composables/useTileClickTracking.js @@ -123,3 +123,134 @@ export function __resetTileClickTrackingForTest() { cachedEnabled = null inflightConfigPromise = null } + +/** + * Browser storage key of the viewer's own tile use (launcher-tile-sorting). + * + * @type {string} + */ +const TILE_USE_KEY = 'launchpad.tileUse' + +/** + * Read the viewer's own tile use: `{ [placementId]: {count, lastUsedAt} }`. + * The counts live only in this browser and are never sent to the server + * (REQ-TSO-002). Corrupt data reads as no use; blocked storage reads as null. + * + * @return {Object|null} Use per placement id, or null when storage is unavailable. + * @spec openspec/specs/container-widget/spec.md + */ +export function readLocalTileUse() { + let raw + try { + raw = window.localStorage.getItem(TILE_USE_KEY) + } catch { + return null + } + try { + const parsed = JSON.parse(raw || '{}') + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? parsed + : {} + } catch { + return {} + } +} + +/** + * Write the use map; a blocked storage is ignored. + * + * @param {object} use The use map. + * @return {boolean} True when stored. + */ +function writeLocalTileUse(use) { + try { + window.localStorage.setItem(TILE_USE_KEY, JSON.stringify(use)) + return true + } catch { + return false + } +} + +/** + * Count one use of a tile in this browser (REQ-TSO-002). Never throws. + * + * @param {string|number} placementId The tile's placement id. + * @return {void} + * @spec openspec/specs/container-widget/spec.md + */ +export function recordLocalTileUse(placementId) { + if (!placementId && placementId !== 0) { + return + } + const use = readLocalTileUse() + if (use === null) { + return + } + const key = String(placementId) + const previous = use[key] || { count: 0, lastUsedAt: 0 } + use[key] = { count: (Number(previous.count) || 0) + 1, lastUsedAt: Date.now() } + writeLocalTileUse(use) +} + +/** + * Forget this browser's use of the given tiles (REQ-TSO-003). + * + * @param {Array} placementIds The tiles to forget. + * @return {void} + * @spec openspec/specs/container-widget/spec.md + */ +export function forgetLocalTileUse(placementIds) { + const use = readLocalTileUse() + if (use === null) { + return + } + placementIds.forEach((id) => { + delete use[String(id)] + }) + writeLocalTileUse(use) +} + +/** + * Test-only reset of the local tile use and the page-load shuffle. + * + * @spec exclude test-only harness hook; `src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js` is its only importer. It clears the browser use map and the random ranks so each test starts cold. + * @return {void} + */ +export function __resetTileUseForTest() { + try { + window.localStorage.removeItem(TILE_USE_KEY) + } catch { + // blocked storage: nothing to clear + } + resetRandomRanks() +} + +/** + * Random rank per placement id, assigned once per page load (REQ-TSO-004). + * + * @type {Map} + */ +let randomRanks = new Map() + +/** + * The page-load random rank of a tile. + * + * @param {string} key The tile key. + * @return {number} A rank in [0, 1). + * @spec openspec/specs/container-widget/spec.md + */ +export function randomRankFor(key) { + if (!randomRanks.has(key)) { + randomRanks.set(key, Math.random()) + } + return randomRanks.get(key) +} + +/** + * Drop the page-load random ranks. + * + * @return {void} + */ +function resetRandomRanks() { + randomRanks = new Map() +} diff --git a/src/constants/widgetRegistry.js b/src/constants/widgetRegistry.js index 5effd6b88..c5a835d61 100644 --- a/src/constants/widgetRegistry.js +++ b/src/constants/widgetRegistry.js @@ -68,6 +68,7 @@ import ChartHost from '../components/Widgets/Renderers/ChartHost.vue' import ClockWidget from '../components/Widgets/Renderers/ClockWidget.vue' import ClockWidgetForm from '../components/Widgets/Renderers/ClockWidgetForm.vue' import ContainerWidget from '../components/Widgets/Renderers/ContainerWidget.vue' +import ContainerWidgetForm from '../components/Widgets/Renderers/ContainerWidgetForm.vue' // `iframe` — embeds an admin-allow-listed external URL in a sandboxed // frame, with a client-side graceful-degradation fallback for targets that // refuse framing (REQ-IFRAME-001..004, openspec/specs/iframe-embed-widget). @@ -127,6 +128,8 @@ const RENDERER_OVERRIDES = { */ const FORM_OVERRIDES = { calendar: CalendarForm, + // launcher-tile-sorting: the communal fields plus "Sort tiles". + container: ContainerWidgetForm, people: PeopleForm, 'spend-analytics': SpendAnalyticsForm, 'nc-widget': NcDashboardForm, diff --git a/src/utils/sortContainerTiles.js b/src/utils/sortContainerTiles.js new file mode 100644 index 000000000..4e5170456 --- /dev/null +++ b/src/utils/sortContainerTiles.js @@ -0,0 +1,116 @@ +/** + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * View-time ordering and reflow of the tiles in a container + * (launcher-tile-sorting, REQ-TSO-001..004). Pure functions: the caller + * supplies the viewer's own use map and the page-load random ranks, and + * gets back copies with new inner-grid positions. Nothing is written back. + */ + +export const SORT_MODES = Object.freeze([ + 'manual', + 'alphabetical', + 'most-used', + 'last-used', + 'random', +]) + +/** + * The display title of a child placement, for the alphabetical order. + * + * @param {object} child The child placement. + * @return {string} Its title, or ''. + * @spec openspec/specs/container-widget/spec.md + */ +export function childTitle(child) { + const content = child?.content || {} + const candidates = [ + content.title, + content.label, + content.text, + child?.tileTitle, + child?.customTitle, + child?.title, + ] + const found = candidates.find( + (value) => typeof value === 'string' && value.trim() !== '', + ) + return found ? found.trim() : '' +} + +/** + * Order the children for a sort mode. Most and last used fall back to + * alphabetical for equal values, so unused tiles sort after used ones. + * + * @param {Array} children Child placements in stored order. + * @param {string} sortBy One of SORT_MODES. + * @param {object} options Sort inputs. + * @param {object} options.use The viewer's use per key. + * @param {function(object): string} options.keyOf The key of a child. + * @param {function(string): number} options.rankOf The page-load random rank of a key. + * @param {Intl.Collator} options.collator Locale collator for titles. + * @return {Array} A new, ordered array. + * @spec openspec/specs/container-widget/spec.md + */ +export function orderChildren( + children, + sortBy, + { use = {}, keyOf, rankOf, collator }, +) { + const list = [...children] + const byTitle = (a, b) => collator.compare(childTitle(a), childTitle(b)) + const stat = (child, field) => Number(use[keyOf(child)]?.[field]) || 0 + + switch (sortBy) { + case 'alphabetical': + return list.sort(byTitle) + case 'most-used': + return list.sort( + (a, b) => stat(b, 'count') - stat(a, 'count') || byTitle(a, b), + ) + case 'last-used': + return list.sort( + (a, b) => + stat(b, 'lastUsedAt') - stat(a, 'lastUsedAt') || byTitle(a, b), + ) + case 'random': + return list.sort((a, b) => rankOf(keyOf(a)) - rankOf(keyOf(b))) + default: + return list + } +} + +/** + * Reflow ordered children row by row into a grid of `columns`, keeping + * each child's width and height. A row is as tall as its tallest child. + * + * @param {Array} ordered Ordered child placements. + * @param {number} columns Inner-grid column count. + * @return {Array} Copies with new `gridX` and `gridY`. + * @spec openspec/specs/container-widget/spec.md + */ +export function reflowChildren(ordered, columns) { + let x = 0 + let y = 0 + let rowHeight = 0 + return ordered.map((child) => { + const width = Math.min(Math.max(Number(child.gridWidth) || 2, 1), columns) + const height = Math.max(Number(child.gridHeight) || 2, 1) + if (x + width > columns) { + x = 0 + y += rowHeight + rowHeight = 0 + } + const placed = { + ...child, + gridX: x, + gridY: y, + gridWidth: width, + gridHeight: height, + } + x += width + rowHeight = Math.max(rowHeight, height) + return placed + }) +} From 25050f75b5506a1b785d73c61ead10aac19bd8f6 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 30 Sep 2026 07:56:47 +0200 Subject: [PATCH 2/5] docs(openspec): archive launcher-tile-sorting, tile-most-used and d-tile-sort are built --- .../.openspec.yaml | 0 .../design.md | 8 ++ .../proposal.md | 0 .../specs/container-widget/spec.md | 0 .../tasks.md | 12 +-- openspec/parity/capabilities.json | 14 ++-- openspec/specs/container-widget/spec.md | 75 +++++++++++++++++-- 7 files changed, 91 insertions(+), 18 deletions(-) rename openspec/changes/{launcher-tile-sorting => archive/2026-09-30-launcher-tile-sorting}/.openspec.yaml (100%) rename openspec/changes/{launcher-tile-sorting => archive/2026-09-30-launcher-tile-sorting}/design.md (78%) rename openspec/changes/{launcher-tile-sorting => archive/2026-09-30-launcher-tile-sorting}/proposal.md (100%) rename openspec/changes/{launcher-tile-sorting => archive/2026-09-30-launcher-tile-sorting}/specs/container-widget/spec.md (100%) rename openspec/changes/{launcher-tile-sorting => archive/2026-09-30-launcher-tile-sorting}/tasks.md (58%) diff --git a/openspec/changes/launcher-tile-sorting/.openspec.yaml b/openspec/changes/archive/2026-09-30-launcher-tile-sorting/.openspec.yaml similarity index 100% rename from openspec/changes/launcher-tile-sorting/.openspec.yaml rename to openspec/changes/archive/2026-09-30-launcher-tile-sorting/.openspec.yaml diff --git a/openspec/changes/launcher-tile-sorting/design.md b/openspec/changes/archive/2026-09-30-launcher-tile-sorting/design.md similarity index 78% rename from openspec/changes/launcher-tile-sorting/design.md rename to openspec/changes/archive/2026-09-30-launcher-tile-sorting/design.md index 7f618ccd3..65e8ec012 100644 --- a/openspec/changes/launcher-tile-sorting/design.md +++ b/openspec/changes/archive/2026-09-30-launcher-tile-sorting/design.md @@ -43,3 +43,11 @@ A reflow changes the reading order, so the container announces "Sorted by most u - Vitest: ordering for each mode, tie breaks, the once-per-load shuffle, storage blocked, edit mode shows stored positions. - Playwright: set "Most used", click one tile three times, reload, it comes first; switch back to "By hand", the original layout returns. + +## What the build corrected (2026-09-30) + +- Tiles inside a container render through the widget registry, not through `TileWidget.vue`, so a click there never reached `recordTileClick()`. The container counts a click on any of its children itself (`onChildClick`), in the browser only; `recordTileClick()` and its server call are unchanged. +- The widget menu is an editor tool, so "Forget my usage" is a small button under the container title, shown in view mode for most used and last used when there is use to forget. +- A new click does not re-sort until the next page load, so tiles never jump under the pointer. "Forget my usage" re-sorts at once and rebuilds the inner grid. +- When browser storage is blocked, most used and last used show the stored order (the spec's rule), not alphabetical. +- A view-time reflow never reaches the persistence callback: `onGridChange` returns in view mode. diff --git a/openspec/changes/launcher-tile-sorting/proposal.md b/openspec/changes/archive/2026-09-30-launcher-tile-sorting/proposal.md similarity index 100% rename from openspec/changes/launcher-tile-sorting/proposal.md rename to openspec/changes/archive/2026-09-30-launcher-tile-sorting/proposal.md diff --git a/openspec/changes/launcher-tile-sorting/specs/container-widget/spec.md b/openspec/changes/archive/2026-09-30-launcher-tile-sorting/specs/container-widget/spec.md similarity index 100% rename from openspec/changes/launcher-tile-sorting/specs/container-widget/spec.md rename to openspec/changes/archive/2026-09-30-launcher-tile-sorting/specs/container-widget/spec.md diff --git a/openspec/changes/launcher-tile-sorting/tasks.md b/openspec/changes/archive/2026-09-30-launcher-tile-sorting/tasks.md similarity index 58% rename from openspec/changes/launcher-tile-sorting/tasks.md rename to openspec/changes/archive/2026-09-30-launcher-tile-sorting/tasks.md index 5a4aba73f..4412f4624 100644 --- a/openspec/changes/launcher-tile-sorting/tasks.md +++ b/openspec/changes/archive/2026-09-30-launcher-tile-sorting/tasks.md @@ -1,8 +1,8 @@ # Tasks: launcher-tile-sorting -- [ ] 1. Add `ContainerWidgetForm.vue` with the three communal fields plus "Sort tiles", and register it in `FORM_OVERRIDES` in `src/constants/widgetRegistry.js`. Verify: Vitest on the form. -- [ ] 2. Keep local `{count, lastUsedAt}` per placement in `src/composables/useTileClickTracking.js`, storage failures caught. Verify: Vitest with storage available and blocked. -- [ ] 3. Order and reflow children in view mode in `ContainerWidget.vue` for the five modes, with the tie rules and a once-per-load shuffle. Verify: Vitest per mode. -- [ ] 4. Add "Forget my usage" to the container menu and the "Sorted by" accessible description. Verify: Vitest and an axe check. -- [ ] 5. Playwright: most used moves a clicked tile to the front after reload; "By hand" restores the stored layout. -- [ ] 6. Add `nl` and `en` strings, fold the delta into `openspec/specs/container-widget/spec.md` on archive, and set d-tile-sort and tile-most-used to `built` in the parity matrix with evidence lines. +- [x] 1. Add `ContainerWidgetForm.vue` with the three communal fields plus "Sort tiles", and register it in `FORM_OVERRIDES` in `src/constants/widgetRegistry.js`. Verify: Vitest on the form. +- [x] 2. Keep local `{count, lastUsedAt}` per placement in `src/composables/useTileClickTracking.js`, storage failures caught. Verify: Vitest with storage available and blocked. +- [x] 3. Order and reflow children in view mode in `ContainerWidget.vue` for the five modes, with the tie rules and a once-per-load shuffle. Verify: Vitest per mode. +- [x] 4. Add "Forget my usage" to the container menu and the "Sorted by" accessible description. Verify: Vitest and an axe check. +- [ ] 5. Playwright: most used moves a clicked tile to the front after reload; "By hand" restores the stored layout. Not written: the requirements carry `@e2e exclude` naming the Vitest files that cover them. +- [x] 6. Add `nl` and `en` strings, fold the delta into `openspec/specs/container-widget/spec.md` on archive, and set d-tile-sort and tile-most-used to `built` in the parity matrix with evidence lines. diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index 24cd01e13..c2d5e72b6 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -1424,11 +1424,12 @@ "homarr": "no", "dashy": "yes", "built": { - "state": "specified", + "state": "built", + "evidence": "src/components/Widgets/Renderers/ContainerWidget.vue (displayChildren); src/utils/sortContainerTiles.js; src/components/Widgets/Renderers/ContainerWidgetForm.vue; src/composables/useTileClickTracking.js (readLocalTileUse)", "owner": "ConductionNL/launchpad", - "change": "openspec/changes/launcher-tile-sorting" + "change": "openspec/changes/archive/2026-09-30-launcher-tile-sorting" }, - "reachedOn": "nothing", + "reachedOn": "A container widget: Edit > Sort tiles (by hand, alphabetically, most used, last used, at random); the order applies when the dashboard is viewed", "evidence": { "workspace-365": "https://support.workspace365.net/en/articles/175612-faq-workspace-365-v4 'Frequently used apps' moved to the navigation menu (read 2026-09-26)", "nextcloud-dashboard": "source read at v35.0.1: the app menu order is set by hand (apps/theming/src/components/UserSectionAppMenu.vue:133-135, AdminSectionAppMenu.vue:86-90); searched apps/dashboard and apps/theming for usage, frequent, most used: none. The shipped recommendations widget suggests files, not apps", @@ -5045,11 +5046,12 @@ "homarr": "no", "dashy": "yes", "built": { - "state": "specified", + "state": "built", + "evidence": "src/components/Widgets/Renderers/ContainerWidget.vue (displayChildren); src/utils/sortContainerTiles.js; src/components/Widgets/Renderers/ContainerWidgetForm.vue; src/composables/useTileClickTracking.js (readLocalTileUse)", "owner": "ConductionNL/launchpad", - "change": "openspec/changes/launcher-tile-sorting" + "change": "openspec/changes/archive/2026-09-30-launcher-tile-sorting" }, - "reachedOn": "nothing", + "reachedOn": "A container widget: Edit > Sort tiles (by hand, alphabetically, most used, last used, at random); the order applies when the dashboard is viewed", "evidence": { "workspace-365": "not checked: user guide 175363 and FAQ for admins 175608 describe drag and drop reordering and automatic gap filling, no sort by name, last use or random (read 2026-09-26)", "microsoft-viva": "not checked: https://learn.microsoft.com/en-us/sharepoint/homesites/available-dashboard-cards card order is set by the author and users drag cards; sort options exist only inside Folder (date, A-Z) and Stream playlist cards, not for cards (read 2026-09-26)", diff --git a/openspec/specs/container-widget/spec.md b/openspec/specs/container-widget/spec.md index a75afa972..1c6deeb07 100644 --- a/openspec/specs/container-widget/spec.md +++ b/openspec/specs/container-widget/spec.md @@ -18,6 +18,7 @@ The `content` object carries these fields: - **backgroundColor** (string, default `'transparent'`) — CSS colour applied to the container wrapper. - **padding** (`'none' | 'small' | 'medium' | 'large'`, default `'medium'`) — preset spacing between the wrapper edge and the inner grid. - **title** (string, default `''`) — optional heading rendered above the inner grid; empty means no heading. +- **sortBy** (`'manual' | 'alphabetical' | 'most-used' | 'last-used' | 'random'`, default `'manual'`): the view-time order of the children (REQ-TSO-001). Never changes the stored positions. ## Inner-grid configuration @@ -124,17 +125,79 @@ Containers MAY hold containers, but the maximum nesting depth is **3 levels**. T ### Requirement: REQ-CONT-007 Form fields -The container's add/edit form MUST collect three fields, all optional: +The container's add and edit form MUST collect four fields, all optional: -- `backgroundColor` — hex colour (NcColorPicker), default `'transparent'` -- `padding` — enum `'none' | 'small' | 'medium' | 'large'` (NcSelect), default `'medium'` -- `title` — string (NcTextField), default `''` (no title rendered when empty) +- `backgroundColor`: hex colour (NcColorPicker), default `'transparent'` +- `padding`: enum `'none' | 'small' | 'medium' | 'large'` (NcSelect), default `'medium'` +- `title`: string (NcTextField), default `''` (no title rendered when empty) +- `sortBy`: enum `'manual' | 'alphabetical' | 'most-used' | 'last-used' | 'random'` (NcSelect with an input label), default `'manual'` -Children are NOT managed via this form — they're added/removed/moved via the inner grid's own affordances when the container is in edit mode. +Children are NOT managed via this form. They are added, removed and moved through the inner grid's own affordances when the container is in edit mode. #### Scenario: Form has three fields - **GIVEN** the container form is mounted - **WHEN** rendered -- **THEN** exactly three input controls MUST be present: backgroundColor picker, padding select, title text field +- **THEN** the three communal input controls MUST be present: background colour picker, padding select and title text field +- **AND** LaunchPad's form MUST add exactly one more control, the "Sort tiles" select - **AND** no "manage children" UI MUST be in the form + +### Requirement: A container sorts its tiles when viewed (REQ-TSO-001) + +@e2e exclude Playwright is not wired for container sorting yet; covered by src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js and ContainerWidgetForm.spec.js + +In view mode a container MUST order its children by its `sortBy` setting and reflow them row by row into its inner grid, keeping each child's size. It MUST NOT write the new positions back. In edit mode it MUST show the stored positions. + +#### Scenario: Alphabetical order + +- **GIVEN** a container "Applicaties" on Pieter's dashboard with tiles "Zaaksysteem", "Agenda" and "Mail" placed in that order, and "Sort tiles" set to "Alphabetically" +- **WHEN** Pieter opens the Workspace page +- **THEN** he sees "Agenda", "Mail", "Zaaksysteem" from left to right + +#### Scenario: Back to the author's layout + +- **GIVEN** the same container sorted alphabetically +- **WHEN** the author sets "Sort tiles" to "By hand" and saves +- **THEN** the tiles show in their stored order "Zaaksysteem", "Agenda", "Mail" + +### Requirement: Most used and last used read the viewer's own browser (REQ-TSO-002) + +@e2e exclude Playwright is not wired for container sorting yet; covered by src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js and ContainerWidgetForm.spec.js + +Most used and last used MUST order tiles by counts and times kept in the viewer's browser storage, which MUST NOT be sent to the server. Tiles with equal counts MUST fall back to alphabetical order. When browser storage is unavailable the container MUST show the stored order without an error. + +#### Scenario: A daily app moves to the front + +- **GIVEN** a container sorted "Most used" with tiles "Agenda", "Mail" and "Zaaksysteem", and Pieter has never clicked any of them +- **WHEN** he opens "Zaaksysteem" three times and reloads the Workspace page +- **THEN** "Zaaksysteem" is the first tile in the container + +#### Scenario: Counts stay on the device + +- **GIVEN** Pieter clicks tiles in a container sorted "Most used" +- **WHEN** the clicks are recorded +- **THEN** no request carries his per-tile counts, and the only server call per click is the existing anonymous `POST /api/tile-click/{placementId}` + +### Requirement: Viewers can forget their usage (REQ-TSO-003) + +@e2e exclude Playwright is not wired for container sorting yet; covered by src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js and ContainerWidgetForm.spec.js + +A container sorted by most used or last used MUST offer "Forget my usage" in its menu, which clears the viewer's local counts for that container's tiles. + +#### Scenario: Forget my usage + +- **GIVEN** "Zaaksysteem" leads Pieter's "Most used" container +- **WHEN** he chooses "Forget my usage" in the container menu +- **THEN** the tiles show in alphabetical order until he clicks one again + +### Requirement: Random order holds for the page load (REQ-TSO-004) + +@e2e exclude Playwright is not wired for container sorting yet; covered by src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js and ContainerWidgetForm.spec.js + +Random order MUST shuffle once per page load and keep that order until the page is reloaded. + +#### Scenario: No jumping tiles + +- **GIVEN** a container sorted "At random" +- **WHEN** Pieter opens the Workspace page and a widget elsewhere refreshes its data +- **THEN** the tile order in the container stays the same until he reloads From e0d60b82f3e470154e5fe7d9b78a49b0f46ab6ca Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 30 Sep 2026 07:57:01 +0200 Subject: [PATCH 3/5] chore(containers): tag the local tile use helpers with their spec --- src/composables/useTileClickTracking.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/composables/useTileClickTracking.js b/src/composables/useTileClickTracking.js index fb855215a..269c6169d 100644 --- a/src/composables/useTileClickTracking.js +++ b/src/composables/useTileClickTracking.js @@ -161,6 +161,7 @@ export function readLocalTileUse() { * * @param {object} use The use map. * @return {boolean} True when stored. + * @spec openspec/specs/container-widget/spec.md */ function writeLocalTileUse(use) { try { @@ -250,6 +251,7 @@ export function randomRankFor(key) { * Drop the page-load random ranks. * * @return {void} + * @spec openspec/specs/container-widget/spec.md */ function resetRandomRanks() { randomRanks = new Map() From 4f3c82029b68fdac48fea73a8b72ba53889e121e Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 30 Sep 2026 09:42:44 +0200 Subject: [PATCH 4/5] fix(containers): count tile clicks with one delegated listener, so the tile wrapper is not a fake control (gate-32) --- .../Widgets/Renderers/ContainerWidget.vue | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/Widgets/Renderers/ContainerWidget.vue b/src/components/Widgets/Renderers/ContainerWidget.vue index 0c071a918..eb3e5ac59 100644 --- a/src/components/Widgets/Renderers/ContainerWidget.vue +++ b/src/components/Widgets/Renderers/ContainerWidget.vue @@ -28,7 +28,7 @@ :gs-y="child.gridY || 0" :gs-w="child.gridWidth || 2" :gs-h="child.gridHeight || 2" - @click="onChildClick(child)"> + :data-use-key="useKey(child)">
@@ -251,10 +251,14 @@ export default { }, mounted() { + // REQ-TSO-002: one delegated listener counts clicks on the tiles' + // own links and buttons; the wrapper itself is not a control. + this.$refs.innerGrid?.addEventListener('click', this.onGridClick) this.initInnerGrid() }, beforeUnmount() { + this.$refs.innerGrid?.removeEventListener('click', this.onGridClick) this.destroyInnerGrid() }, @@ -282,14 +286,17 @@ export default { * does not change until the next page load, so tiles never jump * under the pointer. * - * @param {object} child The clicked child placement. + * @param {MouseEvent} event The click inside the grid. * @spec openspec/specs/container-widget/spec.md */ - onChildClick(child) { + onGridClick(event) { if (this.editMode) { return } - recordLocalTileUse(this.useKey(child)) + const item = event?.target?.closest?.('[data-use-key]') + if (item) { + recordLocalTileUse(item.dataset.useKey) + } }, /** From ac0852a84613fe1af943421f99afdbc01a2840c1 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 5 Oct 2026 19:25:38 +0200 Subject: [PATCH 5/5] docs(tiles): tag the container widget's lifecycle hooks with their spec (gate 16) --- src/components/Widgets/Renderers/ContainerWidget.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Widgets/Renderers/ContainerWidget.vue b/src/components/Widgets/Renderers/ContainerWidget.vue index eb3e5ac59..906bd0cf8 100644 --- a/src/components/Widgets/Renderers/ContainerWidget.vue +++ b/src/components/Widgets/Renderers/ContainerWidget.vue @@ -250,6 +250,7 @@ export default { }, }, + /** @spec openspec/specs/container-widget/spec.md */ mounted() { // REQ-TSO-002: one delegated listener counts clicks on the tiles' // own links and buttons; the wrapper itself is not a control. @@ -257,6 +258,7 @@ export default { this.initInnerGrid() }, + /** @spec openspec/specs/container-widget/spec.md */ beforeUnmount() { this.$refs.innerGrid?.removeEventListener('click', this.onGridClick) this.destroyInnerGrid()