Compare commits

...
127 Commits
Author SHA1 Message Date
github-actions[bot] add47dc065 Bump versions for v10.01 2026-07-18 19:01:13 +00:00
Lauri Ojansivu 5af16df201 Updated ChangeLog. 2026-07-18 22:00:12 +03:00
Lauri Ojansivu adaecfe8c2 Changelog: note the zh-alias/header-cog test fixes in Upcoming. 2026-07-18 21:24:10 +03:00
Lauri Ojansivu 08cfe2d788 Tests: fix zh-alias and header-cog regressions found by the full test run.
- i18n: registering generic Chinese (zh) as its own entry made bare "zh"
  resolve to itself, so the "zh -> zh-Hans" alias is now dead code — remove it
  from LANGUAGE_ALIASES (keep wuu -> wuu-Hans) and update the two mocha tests
  (resolveTag/setLanguage) to expect "zh" instead of "zh-Hans".
- e2e (15-board-actions): the Board Settings cog was removed from the header
  (it is in the right sidebar now), so the "one click from the header cog" test
  failed in every browser. Rewrite it to open the sidebar and open the board
  menu (which still has Archived Items) from there.
2026-07-18 21:18:10 +03:00
Lauri Ojansivu ef6b2794b8 Changelog: add Upcoming section with the changes made after v10.00.
The changes committed since the v10.00 release bump had no CHANGELOG Upcoming
section. Add one above v10.00 covering all of them: the "Submit editors with
Enter" Member Setting, the right-sidebar drag-to-resize width feature, the #6481
migration NaN/Infinity fix, the #6465 Add List inline-composer options restore,
the right-sidebar width/inputs fix, and the missing copy-selection key — each
with commit links.
2026-07-18 20:37:00 +03:00
Lauri Ojansivu 1284afc588 i18n: translate drag-to-resize-sidebar tooltip to all languages.
Add the sidebar resize handle tooltip (drag-to-resize-sidebar, "Drag to resize
sidebar") to every language file, translated into each language (English-variant
locales carry the English text). All 154 files JSON-validated; value-only
additions.
2026-07-18 19:49:01 +03:00
Lauri Ojansivu 354b7804b5 Right sidebar: drag the edge to resize its width (desktop), saved per user.
Add a resize handle on the sidebar edge, like resizing a spreadsheet column
(mirrors the list resize in client/components/lists/list.js). Drag it to change
the sidebar width; the width is saved to profile.sidebarWidth for logged-in
users and to browser localStorage (wekan-sidebar-width) for anonymous users on
a public board, then re-applied on load.

- The handle uses the LOGICAL inset-inline-start, so in LTR it is the left edge
  of the right-docked sidebar and in RTL it is the RIGHT edge of the left-docked
  sidebar; the drag direction inverts for RTL so dragging always widens/narrows
  the visible panel correctly.
- Desktop only: on phones (<=800px) the handle is hidden and the sidebar stays
  full width (the 100vw rule is now !important so a desktop inline width can
  never leak into mobile).
- Width is clamped (min 280px, capped near the viewport) and re-applied on
  window resize so crossing the mobile breakpoint clears/restores it.

New profile.sidebarWidth schema field + getSidebarWidth/setSidebarWidth helpers,
setSidebarWidth Meteor method, i18n (en, fi), and source-guard test
tests/sidebarResize.test.cjs.

Thanks to xet7 !
2026-07-18 19:42:19 +03:00
Lauri Ojansivu 72042b7d79 i18n: add missing copy-selection key (Multi-Selection sidebar) in all languages.
The Multi-Selection right-sidebar "Copy selection" button used {{_ (quote)copy-selection(quote)}}
but the key was missing from every language file — including the en source —
so i18next had no value to fall back to and rendered the raw key. Add
copy-selection ("Copy selection") to en and translate it to all languages,
mirroring each language(apos)s existing "move-selection" sibling with the verb changed
to copy. All 154 files JSON-validated; value-only additions.

Thanks to xet7 !
2026-07-18 19:29:40 +03:00
Lauri Ojansivu b8a21e4ce3 Right sidebar: bound the width and make its input fields fill the width.
- .board-sidebar used width: 30vw, which scaled with the viewport: on very wide
  screens it grew to ~double the intended width (it looked right only at mid
  widths). Use width: clamp(320px, 30vw, 560px) so it keeps the mid-width look
  but is capped (and floored) instead of scaling without bound. Phones (<800px)
  still get the full-width 100vw rule.
- The sidebar filter text inputs (Filter List by Title, Filter by card title,
  advanced filter) had no width, so they rendered at the browser default narrow
  width with a big empty gap on the right. Sidebar text/search inputs now
  width: 100% + box-sizing: border-box, so they fill the padded content area
  with equal empty space on both sides (the .sidebar-content 12px padding).

Source-guard test tests/sidebarWidth.test.cjs.

Thanks to xet7 !
2026-07-18 19:18:17 +03:00
Lauri Ojansivu b71461d90c Add List: restore the previous options in the inline composer (#6465).
The #6465 per-list-header inline Add List composer (addListInline) had been
reduced to just a title input, dropping the options the old addListForm had.
Restore them:
- the "Add after which list" position <select>, listing the swimlane lists and
  pre-selecting the list whose header button opened the composer; submit reads
  it for afterListId and derives nextListId from the following option so the new
  list is inserted between them (falls back to the header target for the empty
  board case).
- the "or template" link (js-list-template) to create a list from a template.
Helpers swimlaneLists()/isDefaultAfterList() added; createListAfter already
accepts nextListId. Source-guard test tests/addListComposerOptions.test.cjs.

Thanks to csonkaoszimt and xet7 !

Fixes #6465.
2026-07-18 18:55:19 +03:00
Lauri Ojansivu b8bb004650 i18n: translate submit-on-enter and its description to all languages.
Add the two new Member Settings keys (submit-on-enter, submit-on-enter-
description) to every language file in imports/i18n/data/, translated into each
language and inserted after show-desktop-drag-handles. English-variant locales
(en-GB, en_AU, ...) carry the English text; each real language matches its own
terminology, and the keyboard-key tokens Enter, Shift+Enter and Ctrl/Cmd+Enter
are kept verbatim. All 154 files JSON-validated; value-only additions.
2026-07-18 18:25:59 +03:00
Lauri Ojansivu bb6b0d5477 Member Settings: optional "Submit editors with Enter" (per-user, off by default).
#4236 made every multi-line editor submit only on Ctrl/Cmd+Enter (plain Enter
= newline) so titles could be multi-line, but that removed the fast
Enter-to-save title workflow some users rely on (v10 email feedback). Add a
per-user preference in the Member Settings (Change Settings) popup, saved to
profile.submitOnEnter and OFF by default: when on, plain Enter submits the card
title, description and other inlined forms, and Shift+Enter inserts a newline;
when off, behaviour is unchanged (Ctrl/Cmd+Enter saves).

The rule stays in the single pure isSubmitKey(event, {submitOnEnter}); the card
title, the card-description override and the shared client/lib/inlinedform.js
all route through it and read the preference from the current user. New toggle
+ helper + toggleSubmitOnEnter method + schema field, i18n (en, fi), and
editorSubmitKey.test.cjs extended with positive + negative opt-in cases.

Thanks to xet7 !

Related to #4236.
2026-07-18 18:14:33 +03:00
Lauri Ojansivu e0991d6602 Migration: keep cards/activities whose export has NaN/Infinity doubles.
mongo 3.x mongoexport emits non-finite doubles as the bare tokens NaN,
Infinity, +Infinity and -Infinity, which are not valid JSON, so EJSON.parse
threw ("Unexpected token N/+") and migrate-mongo3-to-ferretdb.mjs dropped the
WHOLE document — leaving boards/cards missing after an otherwise "successful"
migration. sanitizeNonFinite() now rewrites those bare tokens, only outside
string literals, to the canonical EJSON v2 double form {"$numberDouble":"..."}
that EJSON.parse accepts, so the document migrates instead of being lost.
Strings that merely contain "NaN"/"Infinity" and ordinary negative numbers are
left untouched. Test tests/migrateNonFinite.test.cjs (with negative cases).

Thanks to Nissulya and xet7 !

Fixes #6481.
2026-07-18 16:42:11 +03:00
Lauri Ojansivu 077e2196f5 Updated ChangeLog. 2026-07-18 16:34:50 +03:00
Lauri Ojansivu e010c09e87 Changelog: rewrite v10.00 from what commits actually implemented.
Rewrite the v10.00 section to match the final implementation and group ALL 101
commits in the release under their own headings (each with commit links):

- Corrected "done differently" cases: Change Color is visible grouped swatches
  (not the initial two-level dropdowns); Font is font-name + size buttons with
  immediate apply (not dropdowns); the board-header "icons only" change was
  reverted so button texts stay.
- Fixed the Home board entry: it is set via the All Boards Multi-Selection
  "Selected:" home action and the after-login redirect runs on all deployments,
  so it is available, NOT disabled and NOT Sandstorm-only (an earlier draft
  wrongly said the UI was disabled).
- Every commit since v9.99 is now listed once, grouped by feature/fix area.
2026-07-18 08:46:01 +03:00
github-actions[bot] 356b1152bb Bump versions for v10.00 2026-07-18 05:11:26 +00:00
Lauri Ojansivu 66a2f8ddc8 Updated ChangeLog. 2026-07-18 08:09:43 +03:00
Lauri Ojansivu ef81c25232 ci: disable Playwright E2E Tests workflow.
Rename .github/workflows/playwright.yml -> playwright.yml.disabled so GitHub
Actions no longer runs it (only .yml/.yaml files in .github/workflows are
executed). Reversible by renaming back. NOTE: this workflow also contained the
Meteor unit-tests job and a Puppeteer regression job, so those stop running too.
2026-07-18 08:01:52 +03:00
Lauri Ojansivu 2be9253839 releases: fix push-all-translations.sh cd depth for its translations/ location.
The script lives in releases/translations/, so the repo root is two levels up
(../..), not one. With a single .. it landed in releases/ and looked for ../tx
at wekan/tx instead of the sibling ../tx, aborting with "tx not found". Match
the cd "$(dirname "$0")/../.." pattern used by the other scripts here.
2026-07-18 07:54:28 +03:00
Lauri Ojansivu 2cf878215d Changelog: add Part 12 (Waray wa-RR fix) to the 154-languages entry. 2026-07-18 07:43:33 +03:00
Lauri Ojansivu c00f24d668 i18n: fix wa-RR — translate as Waray (war), not Walloon.
The .tx config maps Transifex "war" -> local wa-RR.i18n.json, i.e. wa-RR is
Waray/Winaray (a Philippine language), NOT a regional variant of Walloon (wa).
An earlier seed wrongly copied Walloon content into it. Retranslate the file
fresh from English into Waray (2191 strings); placeholders/brand names
preserved, key set unchanged, JSON validated.
2026-07-18 07:43:10 +03:00
Lauri Ojansivu 95552881fc Moved translation script to correct directory. Thanks to xet7. 2026-07-18 07:34:17 +03:00
Lauri Ojansivu 5b3394af2f Changelog: WeKan translated to 154 languages, with commit links. 2026-07-18 07:18:22 +03:00
Lauri Ojansivu d55c5e2c3c i18n: register wuu-Hans, fix Wolof/Malay names, add wuu browser alias.
- Register Wu Chinese (wuu-Hans): it had a data file but no languages.js
  entry, so it was neither browser-detectable nor shown in Change Language.
- Add wuu -> wuu-Hans to LANGUAGE_ALIASES so a bare "wuu" browser locale
  resolves (mirrors zh -> zh-Hans).
- Fix display names that were in the wrong script for their (Latin) content:
  Wolof "ولوفل" -> "Wolof"; Malay "بهاس ملايو" -> "Bahasa Melayu" (+ Malaysia).
km_KH/ru_RU are symlinks to km-KH/ru-RU, already covered. Every data file is
now registered, so all are browser-detectable and visible in the popup.
2026-07-18 07:16:00 +03:00
Lauri Ojansivu 01d55828d2 WeKan has been translated to 154 languages.
Thanks to xet7 !
2026-07-18 07:11:49 +03:00
Lauri Ojansivu 219011e5eb i18n: translate untranslated strings (wave 7, final: 14 languages + 5 variants).
Fresh translations: vi, uz, uz-AR (Arabic-script Uzbek), ve (Venda), ve-CC
(Venetian), ve-PP (Vepsian), vo (Volapuk), wa (Walloon), wo (Wolof), xh
(Xhosa), yi (Yiddish), yo (Yoruba), zgh (Tamazight/Tifinagh), zu (Zulu).
Seeded variants uz-UZ/uz-LA, vi-VN, wa-RR, zu-ZA from bases. Completes the
full 153-language set. Placeholders/brand names preserved; JSON validated.
2026-07-18 06:58:27 +03:00
Lauri Ojansivu 91ccef062b i18n: translate untranslated strings (wave 6: 15 languages + 2 variants).
Fresh translations: nb, oc, or_IN, pa, ro, sk, sl, sr, sw, ta, te-IN, th,
tk_TM, tlh (Klingon), ug (Uyghur). Seeded variants ro-RO, sl_SI from bases.
Terminology matches each file glossary + standard Kanban terms; brand names,
acronyms, env vars, placeholders and markup preserved verbatim. JSON validated.
2026-07-18 06:28:58 +03:00
Lauri Ojansivu 97feb986a8 i18n: translate untranslated strings (wave 5: 15 languages + 5 variants).
Fresh translations: gl, gu-IN, hi, hr, hu, hy, id, ig, ka, km, lt, lv, mk, mn,
ms. Seeded variants from those bases: gl-ES; hi-IN; km-KH/km_KH; ms-MY.
Terminology matches each file glossary + standard Kanban terms; brand names,
acronyms, env vars, placeholders and markup preserved verbatim. JSON validated.
2026-07-18 05:43:34 +03:00
Lauri Ojansivu a184a3a717 i18n: translate untranslated strings (wave 4: 15 languages + 9 variants).
Fresh translations: ace, af, ast-ES, az, be, bg, br, ca, cy, el, eo, et-EE,
eu, fa, fy. Seeded regional variants from those bases: af_ZA; az-AZ/az-LA;
ca_ES/ca@valencia; cy-GB; el-GR; fa-IR; fy-NL. Terminology matches each file
glossary + standard Kanban terms; brand names, acronyms, env vars, placeholders
and markup preserved verbatim. JSON validated. ~18800 strings.
2026-07-18 04:56:25 +03:00
Lauri Ojansivu 75ec34e837 i18n: register generic Chinese (zh) so it is selectable and browser-detectable.
zh.i18n.json existed but had no entry in the languages registry, so a browser
reporting bare "zh" could not auto-detect it and it was missing from the Change
Language list. Add it (code zh, name 中文). The other two unregistered data
files (km_KH, ru_RU) are underscore duplicates already covered by the existing
km-KH / ru-RU entries, whose tag is the underscore form.
2026-07-18 04:37:28 +03:00
Lauri Ojansivu c795d13450 i18n: seed close-relative lects as baselines (cmn, wuu, yue, ja-Hira, vls, ary).
Seed Mandarin/Wu/Cantonese (cmn, wuu-Hans, yue_CN) from zh-CN, Japanese-Hiragana
locale (ja-HI) from ja, West Flemish (vl-SS) from nl, and Moroccan Arabic (ary)
from ar as machine-quality baselines sharing the written form, for the community
to refine. Placeholders preserved; JSON validated. 4074 strings.
2026-07-18 04:17:19 +03:00
Lauri Ojansivu ea4a10c212 i18n: seed 32 regional variants from their translated base languages.
Fill untranslated strings in same-language, same-script regional variants by
copying the freshly-translated base: de -> de-AT/de-CH/de_DE; es -> es-AR/-CL/
-CO/-LA/-MX/-PE/-PY/es_CO; fr -> fr-CH/fr-FR; ru -> ru-RU/ru_RU/ru-UA; pl-PL;
cs-CZ; ko-KR; uk-UA; pt -> pt-PT/pt_PT; zh-CN -> zh-Hans/zh/zh_SG/zh-GB;
zh-TW -> zh-Hant/zh-HK; he-IL; nl-NL; ar -> ar-DZ/ar-EG. Keeps variants
consistent with their base and preserves placeholders. JSON validated.
24632 strings.
2026-07-18 04:16:11 +03:00
Lauri Ojansivu 7691829cdb i18n: translate untranslated strings (wave 2: 15 major languages).
Fill previously-untranslated UI strings in de, fr, es, it, nl, ru, pl, uk, cs,
ja, ko, zh-CN, tr, ar, pt. Each language matches its existing file terminology
as a glossary plus standard Kanban terms; product/brand names, acronyms, env
vars, console commands, placeholders (__var__, %s, {{..}}) and markup preserved
verbatim. Value-only changes; every file JSON-validated.

de 524, fr 617, es 917, it 938, nl 458, ru 974, pl 1037, uk 895, cs 1108,
ja 926, ko 850, zh-CN 929, tr 1059, ar 1145, pt 500 strings.
2026-07-18 04:12:59 +03:00
Lauri Ojansivu 63bda22faa i18n: translate untranslated strings (wave 1: zh-TW, pt-BR, da, he, sv).
Fill previously-untranslated UI strings in the five near-complete languages,
matching each file existing terminology as a glossary and standard Kanban
terms (board, list, card, swimlane, label, checklist, theme, font, ...).
Product/brand names, acronyms, env vars, placeholders (__var__, %s, {{..}})
and markup preserved verbatim. Value-only changes; JSON validated.

zh-TW 29, pt-BR 28, da 159, he 262, sv 382 strings.
2026-07-18 03:59:03 +03:00
Lauri Ojansivu a9f2e9f50c releases: add translations-push-all-translations.sh (force-push all languages).
Pushes the English source, then force-pushes every language translation to
Transifex in one run (../tx push -s; ../tx push -t -f). -f is required or the
Go tx CLI skips languages whose local file is not newer than the server copy.
Warns that it overwrites newer community translations, so pull first if needed.
2026-07-18 03:49:35 +03:00
Lauri Ojansivu 861ded439e Updates 2026-07-18 03:36:21 +03:00
Lauri Ojansivu 687a4af861 i18n: translate new admin/import-export strings to Finnish.
Translate the 21 previously-English strings in fi.i18n.json: the disable-*
and anonymize-* import/export/activities/notifications/watch admin feature
labels and descriptions, plus impersonation report title, impersonated user
and reason. Terminology follows existing Finnish strings (seuraaminen,
toimet, profiilikuva, tuonti/vienti, esiintyä käyttäjänä).
2026-07-18 03:32:36 +03:00
Lauri Ojansivu d67d27f0d8 All Boards: "Selected:" star/home actions + Home board badge (#2220).
Below the "Multi-Selection is on" button a "Selected:" row now offers two
actions on the multi-selected boards:
- star icon: stars every selected board (sets them as Starred).
- home icon: sets the first selected board as the Home board (opened after
  login) via the existing toggleDefaultBoard method + router redirect, which
  brings the previously hidden Home-board feature back into use.

The Home board also always shows FIRST in the Starred list (even when it is
not itself starred), badged with a home icon centred at the top of its tile.
2026-07-18 03:19:42 +03:00
Lauri Ojansivu 3736f8431a All Boards: half-width search, hint below Multi-Selection, title beside icon.
- Search boards box is now ~half width (300px); the input flexes to fill it.
- "Click checkboxes to select boards" hint moved out of the path title and
  stacked directly below the "Multi-Selection is on" button (own column).
- Folder icon and the menu title (e.g. "Remaining") are grouped so the title
  always sits to the RIGHT of the icon instead of wrapping below it.
2026-07-18 03:12:18 +03:00
Lauri Ojansivu 01088ea9d4 Notifications menu: make the dropdown item icons black.
The Filter by Unread / View All / Delete All Notifications item icons
(envelope, copy, trash, check) were faint; force the .menu-icon and .check-icon
(and their i.fa) to black.
2026-07-18 02:59:58 +03:00
Lauri Ojansivu 5e014d0967 Notifications drawer: fix X position — cancel the global absolute .close rule.
The X dropped below the header because a global `.close { position: absolute;
top: 5px }` rule (boardBody.css) applied once my rule stopped setting position.
Set position: static on the drawer .close so it stays a flex item and aligns
with the Notifications title at the right edge of the header row.
2026-07-18 02:55:25 +03:00
Lauri Ojansivu c87b4cb510 Notifications drawer: full width + robust flex header (black hamburger + X).
- The drawer and its header are now full width (100vw) instead of 400px.
- Rebuild the header as a flex row (hamburger left, title centre via flex:1, X
  right) instead of fragile absolute positioning, so the X is aligned with the
  Notifications title at the right edge (was sitting too low).
- Force the hamburger and X to black with !important (they were inheriting the
  themed header white link colour), overriding the surrounding themed bar.
2026-07-18 02:51:40 +03:00
Lauri Ojansivu 8dd6bd9db5 Notifications drawer header: black X + hamburger, X near the top-right edge.
- The X close button is now black, moved up (top: 8px, aligned with the
  "Notifications" title instead of vertically centred lower) and to the far
  right (inset-inline-end: 4px).
- The left hamburger menu toggle is black (was #333).
2026-07-18 02:44:51 +03:00
Lauri Ojansivu 8cb4050cf2 Member menu: remove the Notifications entry (duplicates the header bell icon).
The member-menu "Notifications" item and its js-notifications-drawer-toggle
handler toggled the same notifications drawer as the header bell icon
(notifications.js), so it was a duplicate. Remove the menu entry and its
now-unused handler; the header bell is unchanged.
2026-07-18 02:41:47 +03:00
Lauri Ojansivu a4cc0c2182 Search All Boards: label the clear button "Clear" using existing trello-clear-job key.
The js-new-search button clears the search (empties the input + query), so it
is a Clear button. Replace its broom icon with the text of the existing
trello-clear-job translation (en "Clear" / fi "Tyhjennä") and use it for the
aria-label too.
2026-07-18 02:39:09 +03:00
Lauri Ojansivu 98a03bf045 Search All Boards: new-search button follows theme override colors.
The global-search button hardcoded belize #216694 (inline !important styles +
.board-color-belize CSS), ignoring the theme. Make it a .primary type="submit"
button (safe: the search form has onsubmit="return false") so it uses the
theme override colour via the existing button[type=submit].primary rules
(default blue, or the board/global theme), and remove the hardcoded belize
styles.
2026-07-18 02:36:30 +03:00
Lauri Ojansivu 4dc09c62b9 Archived boards: Restore/Delete buttons follow the theme override colors.
The theme override colours .primary buttons via `button[type=submit].primary`
(board-color-* and custom-theme rules). A <button> without an explicit type
attribute does not match [type=submit] in CSS (though it already behaves as
submit), so the archive Restore/Delete buttons kept the default blue. Add
type="submit" (no behaviour change) so they match the theme rules and use the
global theme override colour (named or custom).
2026-07-18 02:32:55 +03:00
Lauri Ojansivu 1c8ba2afc3 Archived boards modal: start below the header bar so its top is visible.
The header bars have a higher z-index than #modal, so the archive panel top
(and the close X) was hidden under the top header bar. Offset the archive
modal content 96px from the top (clears both the quick-access and board header
bars) and extend to the window bottom via min-height calc(100vh - 106px).
2026-07-18 02:29:48 +03:00
Lauri Ojansivu b027594ca8 Archived boards modal: full width + extends to the bottom of the window.
The archive modal used the default .modal-content (500px, margin: 5vh auto),
so archived boards were cramped and the panel stopped short of the window
bottom. Target the archive modal (the only one containing .archived-lists) and
make it 96vw wide with no bottom margin + min-height calc(100vh - 2vh) so it
reaches the bottom of the browser window; each archived board is a full-width
row.
2026-07-18 02:25:30 +03:00
Lauri Ojansivu 08a2e5525e Change Avatar popup: Upload button uses theme colors (blue bg, white text).
Add the .primary class to both "Upload an avatar" buttons so they use the
theme primary-button style (default #005377 blue background + white text,
overridden by the board/global theme color) instead of the plain grey/black
default button.
2026-07-18 02:21:16 +03:00
Lauri Ojansivu 3d190ca022 Font popup: style the "Unset" colour reset links as buttons.
Give .js-reset-text-color / .js-reset-bg-color a button appearance (border,
padding, background, hover) with a dimmed disabled state, matching the font/
size buttons.
2026-07-18 02:19:04 +03:00
Lauri Ojansivu aa15e72682 Member Settings / Font: show the preview pangram below the font-name buttons.
Move the "The quick brown fox ... 0123456789" preview from the bottom of the
popup to directly below the font-name buttons.
2026-07-18 02:18:25 +03:00
Lauri Ojansivu bd8e417be1 Member Settings / Font: font-name + size buttons, immediate apply (no dropdowns).
Redesign the Font popup:
- Font: one button per installed font, each shown IN ITS OWN font (via inline
  style), plus a Default button. Clicking a font applies it immediately.
- Size: a row of buttons ordered by percent — smaller on the left, 100%
  (Default) in the middle, larger on the right. Clicking a size applies it
  immediately.
- Colors: the text/background color wheels now apply immediately on change /
  reset. Removed the Save button (everything applies on click).

New .ui-font-btn / .ui-font-size-btn styling (with .active) in labels.css.
Tests updated in tests/uiFonts.test.cjs.
2026-07-18 02:16:53 +03:00
Lauri Ojansivu fc0649a756 Change Language popup: show languages in multiple columns when there is width.
Widen the language popup responsively (width: min(90vw, 1100px), max-width:
90vw) and lay the list out as an auto-filling grid (repeat(auto-fill,
minmax(170px, 1fr))) so as many languages as possible are visible at once on
wide windows, collapsing to a single column when narrow.

Guarded by tests/changeLanguageColumns.test.cjs.
2026-07-18 02:05:00 +03:00
Lauri Ojansivu 969f96d742 Member Settings / Font popup: add a title (changeFontPopup-title).
Give the Font popup a header like the Change Color popup: en "Font",
fi "Fontti".
2026-07-18 02:01:29 +03:00
Lauri Ojansivu 0f87c3bf8b Change Color popup: remove the "Preview" text from the custom-color preview swatch.
The preview swatch (shown for flat/clear custom colors) still displays the
chosen colour, but no longer shows the "Preview" text label inside it.
2026-07-18 01:59:29 +03:00
Lauri Ojansivu 834698bb5e Revert board-header icons-only; update CHANGELOG Upcoming with newest changes.
- Revert the unconditional hiding of board-header button text labels (Private,
  Muted, Sort, Filter, ...): they show again at normal widths. The pre-existing
  mobile-only (max-width:800px, #6419) icons-only rule is untouched.
- CHANGELOG Upcoming: add consolidated entries for the recent All Boards /
  board-header layout polish, the Change Color swatches / immediate-apply /
  applies-everywhere / popup title, the #4759 user-insert validator fix, and the
  rebuild-wekan.sh CURRENT-IP fix; mark the #2220 home-board toggle as currently
  disabled.
2026-07-18 01:56:47 +03:00
Lauri Ojansivu cd7f9f7bc9 All Boards: move "+ Add Template Container" text 2px down (separate from Add Board).
"+ Add Board" and "+ Add Template Container" share the .js-add-board .label
element, so add an .is-template-container class to the templates-menu variant
and give it padding 20/8/22/8 — 2px lower than "+ Add Board" (18/8/24/8) —
while keeping the same 42px total vertical padding so the tile height is
unchanged.
2026-07-18 01:51:37 +03:00
Lauri Ojansivu 519c04b8ef All Boards: move "+ Add Board" text 2px further up (padding 18/8/24/8). 2026-07-18 01:50:07 +03:00
Lauri Ojansivu 3f5173b960 All Boards: move "+ Add Board" text 4px up (keep tile height).
Shift the .js-add-board .label vertical padding (top 24->20, bottom 18->22)
so the "+ Add Board" / "+ Add Template Container" text sits 4px higher, while
keeping the same 42px total vertical padding so the grey tile stays the same
height as the board icons. Test now checks total vertical padding equality
(height) rather than an exact padding string.
2026-07-18 01:49:40 +03:00
Lauri Ojansivu c21edb160b All Boards: move the Search boards input text 2px further down (top: 8px). 2026-07-18 01:47:16 +03:00
Lauri Ojansivu cb80198d97 All Boards: move the Search boards input text 4px further down (top: 6px). 2026-07-18 01:46:41 +03:00
Lauri Ojansivu 035195f2b4 All Boards: vertically center the Search boards input text.
The search input text sat a little high in the .board-search field; nudge it
down (position: relative; top: 2px) so it reads as vertically centered.
2026-07-18 01:45:47 +03:00
Lauri Ojansivu 197254fe16 All Boards: transparent board tile drag-handle background (icon only).
The desktop board-handle had a dark rgba(0,0,0,0.4) background box; make it
transparent so only the drag-handle icon is visible. The hover highlight is
kept. Guarded in tests/addBoardTileHeight.test.cjs.
2026-07-18 01:43:32 +03:00
Lauri Ojansivu aafe71fd19 All Boards: move board tile drag handle to the right middle.
The desktop board-handle was top-centered (top:4px; left:50%; translateX);
move it to the right middle (top:50%; translateY(-50%); inset-inline-end),
matching the mobile/small-screen handle position. Guarded in
tests/addBoardTileHeight.test.cjs.
2026-07-18 01:42:47 +03:00
Lauri Ojansivu 15e756f4e0 Header/All Boards: reorder Multi-Selection/Sort/Search and swap mobile-toggle/zoom.
- All Boards path header: order is now Multi-Selection, then Sort, then Search
  boards (Search moved to the right of Multi-Selection; Multi-Selection moved to
  the left of Sort). Guarded in tests/addBoardTileHeight.test.cjs.
- Top header bar: swap the positions of the mobile/desktop toggle and the zoom
  controls (mobile toggle now right after the logo, zoom later). Both kept as
  ungated direct children (zoom is no longer hidden when the logo is hidden).
2026-07-18 01:41:48 +03:00
Lauri Ojansivu 97b0240ca4 Board header: icons only — always hide button text labels.
Make the board-header button text labels (Private, Muted, Sort, Filter,
Swimlanes, Multi-Selection, ...) always hidden (was only under a max-width
media query), so the header shows icons only. The board title and the star
count (.board-star-counter) are not button labels and stay.

Guarded in tests/headerHamburger.test.cjs.
2026-07-18 01:27:35 +03:00
Lauri Ojansivu 68723ff43b Board header: remove the Board Settings cog (it is already in the right sidebar).
The #6465 one-click board-settings cog (js-open-board-menu) sat just left of the
sidebar hamburger, duplicating Board Settings which is reachable from the right
sidebar. Remove the cog from boardHeader.jade, its now-dead click handler from
boardHeader.js, and the unused .js-open-board-menu header CSS rule. The sidebar
board-menu button (sidebar.jade/js) is unchanged, and the hamburger + separator
remain.

Guarded in tests/headerHamburger.test.cjs.
2026-07-18 01:14:28 +03:00
Lauri Ojansivu cfd53cc969 Board header: hide button text labels on narrow windows (icons only).
When the window is not wide enough, the 2nd header bar button labels (Private,
Muted, Sort, Multi-Selection, ...) wrapped to a second row. Add a max-width:
1200px media query that hides the .board-header-btn text spans (show icons
only), keeping the board title and the buttons on one line. The star count
(.board-star-counter) is kept.

Guarded in tests/headerHamburger.test.cjs.
2026-07-18 01:05:15 +03:00
Lauri Ojansivu bd72f5d64c Board header: hamburger right gap matches the user-name gap (~8px).
Previously the sidebar hamburger sat 12px from the right edge (2px margin +
10px bar right padding), more than the ~8px gap to the right of the user name
in the top bar (.header-user-bar-name margin-inline-end: 8px). Remove the board
header bar right padding and give the hamburger margin-inline-end: 8px, so its
right-edge gap is the same small amount as the user name.

Guarded in tests/headerHamburger.test.cjs.
2026-07-18 00:57:23 +03:00
Lauri Ojansivu e33e50d9c8 Board header: remove empty space around the sidebar hamburger button.
The hamburger used the general .board-header-btn margin: 0 12px, leaving a
button-sized empty gap on each side. Tighten its side margins to 2px (and the
preceding cog button right margin to 2px) so the hamburger sits snugly at the
right edge with no wasted space to its left or right.

Guarded in tests/headerHamburger.test.cjs.
2026-07-18 00:53:33 +03:00
Lauri Ojansivu 7718118654 Board header: keep the sidebar hamburger pinned to the right edge at all widths.
The board header bar (Swimlanes + Lists views) used a float layout: the right
button group (which ends with the js-toggle-sidebar hamburger) was float:
inline-end. As the window width changed, the floated groups wrapped and the
hamburger drifted to the middle of the bar. Make #header-main-bar a flex row
(align-items:center, flex-wrap:wrap), make the button groups flex (their
buttons float is ignored inside a flex container), and pin the right group with
margin-inline-start:auto so it always hugs the right edge (or the right of its
row when wrapped).

Guarded by tests/headerHamburger.test.cjs.
2026-07-18 00:49:29 +03:00
Lauri Ojansivu 772ae061f1 Change Color popup: immediate apply (no Save), left-aligned titles, title, spacing.
Feedback fixes to the Select Color picker:
- Apply IMMEDIATELY on clicking a swatch (and when a custom-color wheel is
  committed) via a shared applySelection(); removed the Save button/handler.
- Category titles (Flat/Clear/Dark/Special) are left-aligned and clear: both,
  so they drop below the previous group swatches (which float: inline-start)
  instead of the 2nd/3rd/4th title sitting mid-row.
- Remove the empty band above the first (Flat) title (first-child margin-top:0)
  and tighten the Default row.
- Member Settings Change Color popup now has a header: added changeColorPopup-title
  ("Change Color" / fi "Vaihda väri"), matching Board Settings.

Tests updated in tests/themeColorPicker.test.cjs.
2026-07-18 00:45:33 +03:00
Lauri Ojansivu c6f960bbe1 rebuild-wekan.sh: robust CURRENT-IP detection (fix http://:3000 Invalid URL).
The dev-server CURRENT-IP options detected the IP with
  ip a | grep noprefixroute | ... | grep 192.
which requires the noprefixroute flag AND a 192.x subnet. On hosts without
either (e.g. arm64 Ubuntu on a 10.x/172.x network) it returned empty, so
ROOT_URL became http://:3000 and Meteor crashed with "TypeError: Invalid URL".

Detect the primary IP subnet-agnostically via the default route
(ip route get 1.1.1.1 -> src), fall back to hostname -I, then to localhost so
the URL is always valid. macOS uses ipconfig getifaddr en0/en1 with a localhost
fallback. Applied to both CURRENT-IP dev-server options.
2026-07-17 23:52:52 +03:00
Lauri Ojansivu ac20cd6442 All Boards: hide Home-board icons and disable setting a Home board (#2220).
Remove the fa-home Home-board toggle from the board tiles (boardsList.jade),
its click handler and the isDefaultBoard tile helper (boardsList.js), so a
Home board can no longer be set from the All Boards page. The model helpers,
server toggleDefaultBoard method and the login redirect are left intact but
are no longer reachable from the UI. Test flips to guard the disabled state.
2026-07-17 23:50:39 +03:00
Lauri Ojansivu c6aa769bc0 All Boards: Multi-Selection button looks the same as in Swimlanes View.
The All Boards button showed only an icon (span.emoji-icon > fa-check-square-o)
with the reset X as a sibling, while the Swimlanes View (boardHeader.jade)
shows the check icon + a "Multi-Selection" text label with the reset X nested
inside. Match the All Boards markup to the Swimlanes View (direct icon + text
label + nested reset with fa-times-thin), and add stopPropagation to the reset
handler so the now-nested X does not bubble to the activate handler and
immediately re-enable selection.

Guarded by tests/multiSelectionButton.test.cjs.
2026-07-17 23:48:26 +03:00
Lauri Ojansivu 7699cc1e90 All Boards: My Boards bar wraps so Sort/Multi-Selection/Clear stay visible.
The .boards-path-header was a single-row flex (justify-content: space-between,
no flex-wrap) and .path-right (search box + Sort / Multi-Selection / Clear)
also did not wrap, so narrowing the window — especially with Multi-Selection
on, whose hint widens .path-left — pushed those buttons off/hid them. Add
flex-wrap: wrap to the bar, .path-left (min-width:0 so it can shrink; hint
wraps under the title) and .path-right (buttons wrap among themselves), so
everything drops to a second row and stays visible.

Guarded in tests/addBoardTileHeight.test.cjs.
2026-07-17 23:32:31 +03:00
Lauri Ojansivu 1312fefb12 All Boards: remove empty .board-list-header dead grey space above the layout.
The .board-list-header div (a leftover zoom-controls container) rendered
nothing but still reserved vertical space via margin: 1vh 0 2vh 0 + padding:
1vh 0 — an empty grey band above the boards layout, between the top and the
left menu / "My Boards" bar. Removed the empty element from boardsList.jade
and its now-unused CSS rule, so .boards-layout is the first child of .wrapper.

Guarded in tests/addBoardTileHeight.test.cjs.
2026-07-17 23:27:01 +03:00
Lauri Ojansivu 1008561517 All Boards: + Add Board / + Add Template Container tile same height as board icons.
The grey add-tile (.js-add-board .label, shared by both "+ Add Board" and
"+ Add Template Container") overrode min-height:100px, line-height:56px and
padding:36px 8px 32px 8px — taller than the #6465-thinned board tiles
(.board-list-item: min-height:72px, padding:24px 8px 18px 8px). Match the
board-tile box model so the grey background is the same height as a board icon.

Guarded by tests/addBoardTileHeight.test.cjs (asserts the add-tile min-height +
padding equal the board tile, and the old oversized values are gone).
2026-07-17 23:19:23 +03:00
Lauri Ojansivu 1cd32001d4 Select Color: global override applies everywhere + visible grouped swatches (#5778).
Two fixes from feedback that picking clearblue "did not change" and the
dropdowns were hard to use:

1) The global theme override now wins EVERYWHERE, including board pages. Before,
   the <body> autorun suppressed it whenever a board was open (onBoard gate), so
   changing the color while viewing a board did nothing. Now a set global override
   is applied regardless of board context, and #header / #header-quick-access
   (header.jade) and .board-wrapper (boardBody.jade) prefer the global override
   over the per-board color. Unset it to return to per-board colors. When no
   global is set, an open board still exposes its own custom colors.

2) The picker no longer uses category/theme dropdowns (colors were invisible).
   It now shows the actual color SWATCHES with their names, grouped by category,
   with the category name as a header above each group. The flat/clear color
   wheels still appear once such a color is selected; custom-capability is derived
   from the selected color. customTheme.css styles the group labels.

Tests updated: themeColorPicker/globalThemeColor guards for grouped swatches and
the global-wins-everywhere behaviour.
2026-07-17 23:12:55 +03:00
Lauri Ojansivu 0535215225 Fix add-list: Save did nothing (Match failed) + empty-swimlane + button (#6465).
Two fixes to the #6465 inline Add List composer:

1) Save button did nothing. The inline composer calls createListAfter WITHOUT
   nextListId, but the server check() used Match.OneOf(String,null,undefined)
   (not Match.Optional) for it. Meteor check throws "Match failed [400]" on a
   MISSING object key unless the sub-pattern is Match.Optional, so every inline
   add-list 400'd. The omittable params (swimlaneId/afterListId/nextListId/type)
   are now Match.Optional, so callers may omit them.

2) Empty swimlane/board now shows a + button, and the create-list form appears
   only after clicking it (a swimlane that already has lists keeps using the
   per-list-header add-list button — no + button needed). The reveal is scoped
   to the clicked swimlane via a wekan-add-list-empty Session flag, cleared on
   save/close.

Tests: models/lib/createListParams.js + tests/createListParams.test.cjs mirror
the method's param contract (incl. Meteor's missing-key rule) with the exact
client shapes + negatives, and a source guard that the check uses Match.Optional;
tests/upcomingUiGuards.test.cjs gains guards for the empty-state + button reveal.
Wired into test:unit:node.
2026-07-17 23:01:52 +03:00
Lauri Ojansivu 8aacc22360 Test: de-flake webkit swimlane-add popup wait (6s -> 15s).
specs/13-swimlanes.e2e.js waited only 6s for the swimlane action .js-pop-over
to appear. Under the full parallel suite that occasionally exceeds 6s on
webkit (the popup opens fine, ~3s, when the test runs alone), so the test
flaked. Bump to 15s to match the suite standard popup-visibility wait; a
genuinely broken popup still fails.
2026-07-17 22:47:37 +03:00
Lauri Ojansivu 437c64c9a0 Fix #4759 regression: uiTextColor/uiTextBgColor custom() rejected every user insert.
The optional profile.uiTextColor / profile.uiTextBgColor schema fields ran
their custom() hex validator on EVERY user insert, including when the field
was absent. /^#..$/.test(undefined) is false, so the validator returned
"notAHexColor" and collection2 threw ValidationError for any user with no
custom color set — surfacing as a SyncedCron fatal unhandledRejection and
blocking user creation (seen in ../log test-server.log; also cascaded into
ECONNREFUSED e2e failures as the server destabilized).

Guard both custom() validators to return undefined when the value is unset
(undefined/null/empty), matching the existing list-color validator. Add a
source-guard test so the guard cannot silently regress.
2026-07-17 22:05:18 +03:00
Lauri Ojansivu 061eba8b7a Finnish: translate newest UI strings (theme, font, size, colors, Sandstorm).
Translate the strings added this cycle in fi.i18n.json, reusing existing
terminology (väri, taulu, lista, uimarata, fontti, teema, Oletus): the Select
Color theme categories, the Member Settings font / size / text + background
color pickers, the default-board tooltips, and the list-move confirmation.
Fixes theme-category-clear, which had been auto-translated to the verb
"Tyhjennä" (empty) instead of the theme sense "Kirkas". font-preview-text uses
the standard Finnish pangram so the preview shows ä/ö.
2026-07-17 21:48:12 +03:00
Lauri Ojansivu 176c5ef2d8 Updates 2026-07-17 21:44:36 +03:00
Lauri Ojansivu 9a0c0613a5 Sandstorm: auto-open a single-board grain (no save) (#2220).
Restore the old "a grain opens straight into its board" behaviour as a
convenience, WITHOUT persisting anything. On the Sandstorm home route only:

- a saved Home board (explicitly chosen by the user) always wins -> redirect;
- else, once the boards subscription has loaded, exactly ONE board -> just
  open it (nothing is saved);
- else (zero or many boards, no chosen Home board) -> stay on All Boards.

Choosing a Home board stays a separate, explicit action (the All Boards home
toggle, #2220) — the auto-open never writes it.

The redirect is reactive (Tracker.autorun over a boards subscription), so it
waits out Sandstorm asynchronous grain login (Meteor.userId lands late) and
board loading instead of firing once too early. Runs at most once per page
load, and only when isSandstorm.

- models/lib/sandstormAutoOpen.js: pure, unit-tested decideSandstormAutoOpen()
  (+ tests/sandstormAutoOpen.test.cjs, wired into test:unit:node).
- config/router.js: startSandstormAutoOpen() reactive redirect, gated to
  Sandstorm; non-Sandstorm keeps the existing one-shot default-board redirect.
2026-07-17 21:40:37 +03:00
Lauri Ojansivu 22c7d5376b Member Settings / Font / Color: custom text + background color (#4759).
The Font popup now has a color wheel for the text color and one for the text
background color, each with an Unset button to go back to default. Values are
free choices (native <input type=color>), validated server-side as #rrggbb
hex (models/lib/uiFonts isHexColor6) so only a strict hex ever reaches a CSS
value — no injection surface. An unset/invalid value clears that color.

- profile.uiTextColor / profile.uiTextBgColor schema (hex-validated) + getters;
  setUiColors(textColor, bgColor) method ($set valid hex, $unset otherwise).
- changeFontPopup: two color wheels + reset buttons; live preview reflects
  font, size, text color and background color.
- Applied UI-wide via --wekan-ui-text-color / --wekan-ui-bg-color CSS variables
  + has-ui-text-color / has-ui-bg-color body classes (uiFont.js + uiFont.css).
- i18n text-color / text-background-color; tests extended (hex + negatives +
  source guards).

Part of #4759.
2026-07-17 21:15:51 +03:00
Lauri Ojansivu 3c9524f61a Member Settings / Font: pick an installed font + preset size (#4759).
New "Font" entry in the member menu opens a popup with a dropdown of fonts
DETECTED as actually installed in the browser (no free typing) and a Font
size dropdown of sensible presets (Smaller..Largest, no typed numbers). Both
have a Default option that unsets the custom choice.

Security: font names and size keys are validated server-side against a
curated whitelist / preset list (models/lib/uiFonts.js), so only a known safe
font name and a preset percentage ever reach the DOM. Labels are plain text
(Blaze-escaped) — never HTML or markdown. No CSS-injection surface.

- models/lib/uiFonts.js: UI_FONTS whitelist + SAFE_FONT_RE + isKnownFont /
  fontFamilyValue and UI_FONT_SIZES presets + isKnownFontSize / fontSizeValue.
- client/lib/fontDetector.js: canvas width-comparison detection of installed
  fonts, filtered to the whitelist.
- profile.uiFont / profile.uiFontSize schema + getters; setUiFont /
  setUiFontSize methods (validated, null/default unsets).
- changeFontPopup (font + size dropdowns, both with Default) wired in
  userHeader.js; applied UI-wide via CSS variables in uiFont.js + uiFont.css
  (marker body classes), wired into client/features/main.js + client/styles.js.
- i18n change-font/font/font-default/font-size(+presets); tests/uiFonts.test.cjs
  (pure + negatives + source guards) wired into test:unit:node.

This is a Member Settings feature (not an Admin Panel option).

Fixes #4759.
2026-07-17 21:15:39 +03:00
Lauri Ojansivu d0a1e43f36 Select Color: two-level dropdown picker + custom colors (#5778).
Both Board Settings and Member Settings Change Color now use a shared
category -> theme two-level dropdown picker with custom color wheels for the
flat (1 color) and clear (2 colors, a color slide) categories; dark/special
are fixed. Custom colors are validated server-side as #rrggbb, stored in
board.customThemeColors / profile.globalThemeCustomColors, and applied as
CSS variables (--theme-accent / --theme-accent-2) consumed by customTheme.css
to recolor the header, primary buttons and sidebar button.

- client/components/main/themeColorPicker.{jade,js}: shared picker, scope
  "board" or "global", reusing the #5514 native color wheel + themeCategories.
- board.setColor(color, custom) + setGlobalThemeColor(color, custom): validate
  via isValidCustomColors (flat/clear + count + hex) before storing.
- boardChangeColorPopup and changeColorPopup now render the shared picker;
  removed their old swatch-list helpers/events.
- globalThemeColor.js: apply custom colors as :root CSS variables (board own
  colors on a board, else the user global override) + marker body classes;
  customTheme.css consumes them.
- Wire globalThemeColor.js / themeColorPicker into client/features/main.js and
  customTheme.css into client/styles.js. This also fixes globalThemeColor.js
  never having been imported (so the #5778 override now actually loads).
- i18n theme / theme-category[-flat/clear/dark/special]; tests
  themeColorPicker.test.cjs + updated globalThemeColor.test.cjs.

Fixes #5778.
2026-07-17 20:58:39 +03:00
Lauri Ojansivu 49285dad19 Select Color: theme categories core + design doc (docs/Theme/Theme.md).
Partition the ~19 board/global theme colors into flat / clear / dark /
special and define where custom colors are allowed (flat = 1 accent,
clear = 2 for a color slide, dark/special = fixed none). This is the shared
source of truth for the redesigned Select Color picker (two-level dropdowns
+ color wheel) used by both Board Settings and Member Settings.

- models/lib/themeCategories.js: THEME_CATEGORIES + categoryOf /
  colorsInCategory / allowsCustomColor / customColorCount /
  isValidCustomColors (validates category + count + #rrggbb hex).
- tests/themeCategories.test.cjs (wired into test:unit:node): behaviour +
  negatives + a source guard that the partition exactly equals
  config/const.js ALLOWED_BOARD_COLORS.
- docs/Theme/Theme.md: full design — categories, two-level dropdown UX,
  custom-color rules, CSS-variable rendering plan, storage
  (board.customThemeColors / profile.globalThemeCustomColors), and
  server-side validation.

Completes the global per-user theme override (Member Menu / Change Color)
begun in the previous commit.

Fixes #5778.
2026-07-17 20:46:10 +03:00
Lauri Ojansivu eab1546b88 Member Menu / Change Color: global per-user theme override (#5778).
A new "Change Color" entry in the member menu works like Board Settings /
Change Color, but is a per-user GLOBAL override saved to the profile
(profile.globalThemeColor). The chosen color theme applies to the whole UI —
All Boards, Search All Boards, Admin Panel, My Cards, etc. — via a
board-color-<name> class on <body> and the header, so a user can pick a dark
theme (dark/midnight/moderndark/exodark/cleandark) or any other theme
everywhere. The first popup entry clears the override.

- models/users.js: profile.globalThemeColor field + getGlobalThemeColor /
  globalThemeColorClass / setGlobalThemeColor helpers.
- server/models/users.js: setGlobalThemeColor method, validated against
  BOARD_COLORS (rejects unknown colors so no arbitrary CSS class is injected)
  and auth-guarded; null/empty clears it.
- client/components/main/globalThemeColor.js: Tracker autorun that adds the
  class to <body> on non-board pages; on a board the board own color wins, so
  the global class is cleared there to avoid two themes clashing.
- header.jade: #header and #header-quick-access fall back to the user global
  theme class when not on a board (covers the #header-specific color rules).
- Change Color menu entry + changeColorPopup (color swatches + clear option),
  wired in userHeader.js.
- i18n change-color / theme-default; tests/globalThemeColor.test.cjs wired
  into test:unit:node.
2026-07-17 20:32:40 +03:00
Lauri Ojansivu 46924985f8 Soft delete for lists: delete = mark, restorable + undoable (#1023).
Deleting a list no longer destroys it. The list and its cards are marked
(deletedAt/deletedBy/deleteBatchId), hidden from the board, recorded as a
reversible delete in history, and restorable via lists.restore or Ctrl+Z.
This is the first slice of "no permanent delete in ordinary use": physical
removal is limited to GDPR/account erasure and a Global-Admin purge gated by
a new Admin Panel / Features / Delete "Enable permanent delete" toggle
(off by default).

- models/lib/softDelete.js: pure notDeleted/softDeleteSet/restoreModifier/
  canPurge helpers (+ tests/softDelete.test.cjs, wired into test:unit:node).
  softDeleteSet omits falsy actor/batch and restore uses $unset so no null is
  ever written to a Date/String schema field.
- enablePermanentDelete feature flag (models/lib/featureFlags.js) + Settings
  schema field.
- deletedAt/deletedBy/deleteBatchId schema fields on Lists and Cards
  (optional, no null default -> absent means live).
- board/swimlane list render paths filter deletedAt: null.
- server/models/lists.js: shared softRemoveList() + lists.softRemove /
  lists.restore / lists.purge (purge gated on canPurge = isAdmin && flag);
  client delete handler and REST DELETE now soft-remove.
- userPositionHistory undo()/redo() handle actionType delete/restore via a
  batch-consistent _applyDeleted() helper.

Design + planned rollout (other entities, Recycle Bin, admin Delete category
table with GDPR data classes) in docs/Features/Undo/Undo.md.
2026-07-17 20:20:06 +03:00
Lauri Ojansivu 58344db0d7 Undo docs: soft-delete design (delete = mark, never destroy).
Every user-facing delete becomes a soft delete (deletedAt/deletedBy), making
deletions restorable and undoable (#1023). No permanent delete in ordinary
use: physical removal is limited to GDPR/account erasure and an explicit
Global-Admin purge gated behind Admin Panel / Features / Delete ->
"Enable permanent delete for Global Admin" (off by default). Covers scope
(soft vs exempt collections), the softRemove/notDeleted mechanism, cascade
via deleteBatchId, archived-vs-deleted distinction, undo/History integration,
and migration steps.
2026-07-17 20:07:09 +03:00
Lauri Ojansivu e246403bb6 Activities already load progressively (infinite scroll), like visible cards.
The activities publication only ships the most-recent `limit` activities and
the client pages them in via loadNextPage, so large boards stay fast without
deleting activity history. Document this so the request is closeable.

Fixes #2539.
2026-07-17 20:01:52 +03:00
Lauri Ojansivu e9e7071744 Changelog: drop bogus #6483/#6484 issue links (those bugs were reported by email).
The 'Filter by date -> Overdue' and 'list disappears in swimlanes' fixes were
reported originally by email, not as GitHub issues - #6483 and #6484 do not exist
(404). Replace the dead issue links in their Upcoming entries with '(reported by
email)'. The fixes themselves are unchanged.

Thanks to xet7.
2026-07-17 19:55:40 +03:00
Lauri Ojansivu 8287865b23 Tests: cover the remaining Upcoming changes (#2220 default board, #6465 UI, #6478 confirm).
Adds tests (with negative cases) for the Upcoming changes that previously lacked
them:
- tests/defaultBoard.test.cjs (#2220): behavioural toggle/decision logic (set,
  unset the current default, switch to a different board; negatives: nothing is
  the default when unset, B is not the default when A is) + source guards that
  the schema, model + server toggleDefaultBoard, once-per-session router redirect,
  and board-tile toggle are all wired.
- tests/upcomingUiGuards.test.cjs: source guards for the #6465 Add List header
  button + inline composer (leading column replaced by empty-state gating),
  minicard white-space (min-height:0) and swimlane-header zoom clip
  (overflow:visible/height:auto), and the #6478 touch confirmation before a
  cross-swimlane list move (gated on touch/small screen; declining aborts).

The other Upcoming changes already had tests (filterDateNull, listMoveSwimlane,
cardDragSwimlaneScrollRefresh, popupAfterConfirm, undoRedoSelection,
listWidthDefaults, ferretdb* in the FerretDB repo).

Fixes #2220. Fixes #6465. Fixes #6478. Fixes #6479.

Thanks to xet7.
2026-07-17 19:48:53 +03:00
Lauri Ojansivu e8ced84172 Feature #2220: mark a board to open automatically after login (home board).
On the All Boards page each board tile gets a home (house) toggle next to the
star. Marking a board sets it as the user's default 'home' board; the next login
(or reload) lands on it instead of All Boards. Clicking again clears it.

- profile.defaultBoardId (per user) + getDefaultBoardId/isDefaultBoard/
  toggleDefaultBoard model helpers + a toggleDefaultBoard Meteor method (mirrors
  toggleBoardStar).
- home route redirects to the default board ONCE per session (so clicking All
  Boards afterwards stays on the list; honoured again next login/reload); by id
  so it works before the board subscription loads; graceful fallback if the board
  is gone.
- board tile .js-set-default-board toggle + isDefaultBoard helper; i18n
  set/unset-default-board-title.

Thanks to xet7.
2026-07-17 19:38:19 +03:00
Lauri Ojansivu 9be41ab34b Docs: unify undo/redo into one universal change-History (every change) with many scoped views.
Per request, the History design now records EVERY change a user makes (not just
card groups) in one append-only changeHistory store, and undo/redo becomes the
keyboard front-end to it (restore the current user's most recent change) rather
than a separate feature - superseding userPositionHistory.

Same table + restore is surfaced from many menus, each just a scope filter over
the one store: card group, whole card, Member settings (per-user), Board Settings
(per-board), Swimlane menu, List menu, and any future entity menu. Scopes nest
(card in list in swimlane in board) via id-column ORs. Added a scoped-views
section, the scope/scopeId page() params, per-scope permissions, and updated the
phasing (migrate position undo/redo onto changeHistory first). Undo.md now points
at History.md as the source of truth for the unified direction.

Thanks to xet7.
2026-07-17 19:27:00 +03:00
Lauri Ojansivu f3354f5cc8 Docs: undo/redo + high-impact confirmation feature doc.
docs/Features/Undo/Undo.md documents the #6478 feature: position-move undo/redo
(card/list/swimlane) via userPositionHistory with an undone/undoneAt redo stack,
undoLast/redoLast methods, Ctrl+Z/Ctrl+Y bindings, and the touch confirmation for
cross-swimlane list moves. Records the inert-recording bug (collection referenced
as an un-imported global), the write/read wiring, retention, v1 limitations
(positions only; content undo is the separate History feature) and an extension
checklist.

Thanks to xet7.
2026-07-17 19:17:10 +03:00
Lauri Ojansivu 3d1276fc1f Docs: design for per-group card History (view + restore).
Detailed design doc at docs/Features/History/History.md for the requested card
details 'History' feature: per-group history popup with a searchable, paginated
(current page only), per-contributor change table and restore.

Key point: WeKan's Activities log records what happened but NOT before/after
values, so restore needs a new append-only cardGroupHistory content-versioning
collection - the doc specifies the model, write/read (paginated+searchable+
per-user) methods, the 2-pane UI (History + contributor avatars | table with
select/change-type/content/datetime, search+pagination+restore bar), RTL
mirroring, restore semantics (re-logged to both source and restorer; history is
append-only), security, and a phased rollout starting with Description.

Thanks to xet7.
2026-07-17 19:14:40 +03:00
Lauri Ojansivu 765d04bf7c #6465: Add List as a per-list header button + inline composer, not a standing column.
Swimlanes/Lists views no longer render a permanent Add List composer column.
Each list header now has a far-right add-list button; clicking it opens the Add
List composer as a column immediately AFTER that list in the DOM - which appears
to the right of the list in LTR and to the left in RTL (the lists lane is a flex
row that mirrors under dir=rtl). An empty swimlane/board shows the composer at the
start so the first list can still be added.

- new always-open addListInline template (title input + save + close), submit ->
  createListAfter (afterListId = the clicked list, swimlaneId = the displayed
  swimlane recorded from the DOM by the header button).
- Session 'wekan-add-list-after'/'wekan-add-list-swimlane' drive which list the
  composer opens after; isAddListAfter/swimlaneHasNoLists/boardHasNoLists helpers.
- leading +addListForm removed from the non-mini swimlane and listsGroup
  branches; mini-screen path unchanged.

First cut - desktop/main views; needs live verification of styling, RTL and the
empty-board case.

Thanks to csonkaoszimt (report) and xet7.
2026-07-17 19:12:45 +03:00
Lauri Ojansivu 4ca33d29f7 #6478: undo/redo for card/list/swimlane moves + confirm high-impact list move on touch.
A user accidentally dragged a list into another swimlane on mobile (merging it
with an existing list) and spent ~15 min recovering - no confirmation, no undo.

Undo/Redo: the userPositionHistory collection modelled this but was NOT actually
recording (trackChange guarded on an un-imported global) and had no redo or key
bindings. Now:
- list moves are recorded in updateListSort (import UserPositionHistory so the
  guard passes); card moves already had the call.
- add a symmetric redo() model method + an undone/undoneAt redo stack; add
  userPositionHistory.undoLast/redoLast methods (a new action clears the redo
  stack via trackChange).
- wire Ctrl+Z / Ctrl+Y (and Cmd variants) in client/lib/keyboard.js; disabled
  inside inputs/textareas so native text undo is unaffected.
- pure selection logic models/lib/undoRedoSelection.js + tests.

Confirmation: on touch / small screens, moving a list to a DIFFERENT swimlane
(which re-homes every card) now asks for confirmation; a decline reverts the
drag. Same-swimlane reordering is unaffected.

Thanks to mueschel (report) and xet7.
2026-07-17 18:44:14 +03:00
Lauri Ojansivu f6f021f8cc Fix #6479: confirmation dialogs (remove member, etc.) did nothing on confirm.
Clicking a .js-confirm button silently did nothing. Popup.afterConfirm() stashed
the pending action as a field on the Blaze DATA CONTEXT and the confirm button
read it back as this.__afterConfirmAction. But the context is a
ReactiveCache/Minimongo document (a board member sub-doc), and Blaze re-renders
the confirmation popup with a fresh/immutable copy of that context, so the field
was gone by click time - so every afterConfirm dialog (remove member from board,
etc.) no-oped.

Store the action on the Popup INSTANCE (Popup._afterConfirmAction) instead, which
survives the re-render; it is still invoked with the confirmation popup's own data
context as this, so this.userId keeps working. The .js-confirm handler also guards
against a missing action instead of throwing. Test tests/popupAfterConfirm.test.cjs
with a negative case reproducing the old context-mutation failure.

Thanks to mueschel (report) and xet7.
2026-07-17 18:29:28 +03:00
Lauri Ojansivu 26319c974b Updated ChangeLog. 2026-07-17 18:17:18 +03:00
Lauri Ojansivu 50dd05ceee Updated Sandstorm Backup docs.
Thanks to robertdahlem and xet7 !

Fixes #6475
2026-07-17 18:13:53 +03:00
Lauri Ojansivu 6d5a594b07 Fix #6477: card drag between swimlanes broke when lists are long/overflowing.
Dragging a card from one swimlane to the SAME list in another swimlane dropped
it back in the source swimlane, especially with long overflowing lists. The card
sortable's sort handler auto-scrolls the board (#443) via .js-lists scrollLeft /
.board-canvas scrollTop, but jQuery UI sortable caches container/item geometry at
drag start and only re-caches on its own placeholder moves - never on our manual
scroll. Long lists are exactly what triggers auto-scroll, so the drop resolved
against a stale geometry map and the stop handler's ui.item.parents('.swimlane')
persisted the wrong swimlane.

Re-cache positions with sortable('refreshPositions') whenever the sort handler
actually scrolled - complementing the #2769 DOM-mutation refresh in listBody.js,
which does not fire on scroll. Source-guard test
tests/cardDragSwimlaneScrollRefresh.test.cjs.

Thanks to mueschel (report) and xet7.
2026-07-17 18:04:36 +03:00
Lauri Ojansivu ae29555862 Test: update fixed-list-width e2e for the lowered 200px minimum (#6465).
The #6465 density change lowered MIN_LIST_WIDTH 270->200 and the default
272->220, but specs/38-fixed-list-width.e2e.js still asserted the old 270 floor:
setFixedListWidth(200) was expected to be REJECTED (it is now valid), and the
anonymous default was expected to be 272. This failed on chromium/firefox/webkit.

Use 150 as the too-small value (below the new 200 floor), update the client MIN
mirror to 200 and the default to 220, and add a POSITIVE case that a width of
210 (>=200 but below the old 270) is now accepted.

Thanks to xet7.
2026-07-17 18:00:40 +03:00
Lauri Ojansivu c4784225d5 Changelog: move post-v9.99 changes to an Upcoming section above v9.99.
v9.99 was released with the LDAP connection-leak fix, the bundled-FerretDB pool
note and the snap #6476 wait-loop diagnostics. Everything committed after the
v9.99 bump - the QEMU snap build, the #6465 UI density/minicard/dock/swimlane
changes, the #6483 date-filter fix, the #6484 list-move fix, and the #6476
root-cause (FerretDB orphaned-table crash loop) - now sits under a new
# Upcoming section above v9.99 instead of being backdated into v9.99.

Thanks to xet7.
2026-07-17 17:03:13 +03:00
Lauri Ojansivu 5730f7dcc5 Changelog: bundled FerretDB now pushes date range filters down too.
Updates the #6483 note: label ($in), title ($regex) AND date range
($gt/$gte/$lt/$lte) filters are all now pushed down to SQLite in the bundled
FerretDB, so none of them full-scan the board. Dates use SQLite's ->> accessor
for numeric comparison of the stored millis, verified end-to-end.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu b29a47778f Changelog: bundled FerretDB now pushes label ($in) and title ($regex) filters down.
Notes that the filter slowness flagged earlier is addressed in the bundled
FerretDB, which now pushes {labelIds: {$in: [...]}} and literal {title: {$regex}}
down to SQLite instead of scanning + matching every card's sjson in Go.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu 7b7fada1b8 #6465: swimlane header bar no longer clips at non-100% board zoom.
At e.g. 80% zoom (transform: scale on .board-wrapper) the gray swimlane header
(.swimlane-header) looked cut off: it had overflow: hidden with a fixed
min-height, so once the scaled layout rounded, the bar/text was sliced. Use
overflow: visible + height: auto (keeping the min-height floor) so the header
grows to fit and cannot clip vertically; short swimlane names are unaffected and
a long name wraps instead of being cut.

Speculative CSS fix - verify in the running app at 80% (and check 50%/125%/200%).

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu 41c747ef5f Changelog: #6476 real root cause (FerretDB orphaned-table crash loop) and the bundled FerretDB fix.
Operator logs (uusijani, a1bert01) showed FerretDB was listening and WeKan
connected, then a startup SyncedCron upsert hit FerretDB error
'table accountslockout.connections_<hash> already exists', which became an
unhandledRejection and exited the process; systemd crash-looped it so the HTTP
port never came up. The real fix is in the bundled FerretDB (collectionCreate
now uses CREATE TABLE/INDEX IF NOT EXISTS to re-adopt an orphaned table). The
snap wait-loop timeout/diagnostics remain useful for the separate 'FerretDB
genuinely not ready' case.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu 99299c2a91 #6465: denser defaults - thinner lists (220px), board tiles and card dock (520px).
Several users reported 9.x wastes space vs the older 6.09 layout. Tighten three
density defaults (all overridable; user customizations untouched):

- List width default 272 -> 220px (models/lib/listWidth.js, models/lists.js),
  minimum 270 -> 200px here and in the server check (server/models/users.js).
  Lists already render the same width by default (resolveListWidth) on private
  and public boards; now narrower so more fit. Test updated.
- Board tiles (All Boards page): min-height 100 -> 72px, vertical padding
  36/32 -> 24/18px (boardsList.css).
- Card-detail right dock max-width 800 -> 520px (and the wide-screen centred
  variant 600 -> 520px) in cardDetails.css, so the opened card no longer takes
  ~1/4 of the screen.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu c29cf3b985 Fix #6484: nudging a list in swimlanes view hid it from other swimlanes.
A board-wide list has swimlaneId === null and renders under every swimlane. The
list-drop handler computed
  isDifferentSwimlane = targetSwimlaneId && targetSwimlaneId !== originalSwimlaneId
but targetSwimlaneId is always the swimlane the list is shown under, and
originalSwimlaneId is null for a board-wide list, so ANY small drag made this
true - it set list.swimlaneId AND moved every card in the list to that one
swimlane, so the list vanished from all other swimlanes (recoverable only by
restoring a backup).

Also require originalSwimlaneId: board-wide lists stay board-wide on a reorder,
while a genuinely swimlane-scoped list can still be moved between swimlanes. Test
tests/listMoveSwimlane.test.cjs, with a negative control that the old target-only
condition rebound a board-wide list.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu b1d33c5362 Fix #6483: 'Filter by date -> Overdue' listed cards with no due date.
Cards without a date store the field as null (that is what the sidebar's noDate
filter matches: {dueAt: null}). The comparison filters build {$lte: now}
(Overdue/past) and {$gte, $lte} (today/this/next week), and $lte MATCHES null
under both FerretDB and minimongo - null sorts before any date and neither
implements MongoDB type-bracketing - so Overdue selected every dateless card.

DateFilter._getMongoSelector now adds $ne: null to any comparison selector so
only real dates match; the noDate filter (a bare null) is unchanged. Test
tests/filterDateNull.test.cjs, with a negative control proving the old bare
{$lte: now} matched null.

The reported filter SLOWNESS and the title/label over-matching are separate
(FerretDB scanning $regex/$in/range queries not pushed down to SQLite) and need
live reproduction.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu a76f7d28e3 Fix #6465: minicards wasted half their height on empty white space.
The minicard title renders inside a .viewer, which inherited the global
.viewer { min-height: 2.5vh } (client/components/main/layouts.css). On tall
screens that reserved a tall empty band beneath a one-line title, so a large
part of every minicard was blank and fewer cards fit on screen. The minicard
title .viewer now sets min-height: 0 (the same override already applied to the
label .viewer), so a minicard collapses to fit its content.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu d7f36d70a0 Snap release: build ppc64el and s390x under QEMU on GitHub Actions, not Launchpad.
The Launchpad 'snapcraft remote-build' legs for ppc64el and s390x ended in
Launchpad state 'Stopped' with no snap produced, yet remote-build still exited 0;
the job's name-only success check let an empty/stale file through and the upload
died with ''wekan_<v>_<arch>.snap' is not a valid file' (exit 64). Build them
instead with docker/setup-qemu-action + diddlesnaps/snapcraft-multiarch-action on
a normal runner - fast, because the snap is bundle-centric (the wekan part only
downloads the prebuilt wekan-<version>-<arch>.zip and repackages it, no native
compilation).

riscv64 stays on Launchpad (the QEMU snap action does not support it) but now has
timeout-minutes: 180 so a stuck build fails with a clear message instead of
GitHub's silent 6-hour cancel.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
github-actions[bot] 63a8657e0a Bump versions for v9.99 2026-07-17 12:10:21 +00:00
Lauri Ojansivu b846e6d565 Updated ChangeLog. 2026-07-17 15:09:11 +03:00
Lauri Ojansivu 580276079e Fix #6476: snap FerretDB wait loop had no timeout, so WeKan hung with no web port.
With database=ferretdb, snap-src/bin/wekan-control waits for FerretDB to accept
connections before starting node - WeKan does not open its HTTP port until the
database answers. That wait loop had no timeout and no diagnostics, so if
FerretDB never started listening (crashed or arch-incompatible per-arch binary,
locked/corrupt SQLite, or the service left disabled by a failed migration
hand-off), WeKan blocked there forever: the service showed active but the port
never opened and the only symptom was a silent 'FerretDB not ready yet' loop.
uusijani's wekan.log stopped right after the startup env dump, confirming
control never reached node main.js.

Mirror the MongoDB branch: after WEKAN_DB_WAIT_TIMEOUT seconds (default 120,
overridable) print an actionable hint once, then keep retrying - pointing to
'snap logs <snap>.ferretdb', naming the SQLite dir to check for locks/corruption,
the arch-mismatch case, the snap start --enable recovery, and the
'snap set <snap> database=mongodb' fallback. Regression test
tests/ferretdbWaitTimeout.test.cjs, including a negative guard that the wait is
not a silent unbounded loop again.

Thanks to uusijani (report) and xet7 (fix).
2026-07-17 15:04:48 +03:00
Lauri Ojansivu 9a387acce0 Translations: auto-heal English-reverted languages on pull; force-push helpers.
tx pull -f fills any string untranslated on Transifex with the English source, so
languages whose translations live only in git silently revert to English on every
pull. Make the pull self-heal, and fix the push helpers so restored translations
actually upload.

- pull-translations.sh: after the pull, for each language file whose changes are
  ENTIRELY reverts, git checkout HEAD -- <file> to restore the committed
  translations and force-push them back (../tx push -t -l <lang> -f). Files that
  also carry a real new/updated translation from Transifex are NOT auto-healed
  (a whole-file checkout would discard it) and are left for manual handling.

- report-english-regressions.mjs: add a machine-readable --files mode and an
  otherChanges classifier, so only pure-revert files are offered for auto-heal;
  the human report tags mixed files.

- push-translation.sh: add -f (without it the Go tx CLI skips a language whenever
  the local file is not newer than the server copy - push.go shouldSkipPush - so
  a freshly reverted file uploads nothing), plus a usage guard.

- push-copy-en-gb-translation.sh (new): copy en.i18n.json to en-GB.i18n.json
  verbatim and force-push it as the en_GB translation, so British English shows
  as translated instead of reverting to the English source on each pull.

Thanks to xet7.
2026-07-17 14:14:31 +03:00
Lauri Ojansivu 7374b7e00d Fix #6467 #6469: LDAP connection leak exhausted the directory server.
Operators reported that after an update WeKan "dies really quickly", restarts
did not help, and it "kills our openldap server with too many open connections";
others saw logins hang for minutes then fail with "Must be logged in". Every
LDAP login attempt (loginHandler.js) and every background sync tick (sync.js)
created a new LDAP() and connect()ed but NEVER called disconnect() on any path,
so each attempt - including every failed login and every one-minute sync tick -
leaked a socket to the LDAP/AD server until it hit its connection limit and
started refusing connections, taking it and all WeKan logins down.

Guarantee release on every exit path: a shared runWithLdapDisconnect() helper
(connectionGuard.js) disconnects in a finally; the login handler runs its whole
flow through it; background sync() disconnects in a finally; importNewUsers()
disconnects only the connection it owns; and LDAP.disconnect() is a safe no-op
when connect() was never reached. Adds behavioural + negative + source-guard
tests (tests/ldapConnectionRelease.test.cjs, wired into test:unit:node).

Also bumps the bundled FerretDB whose SQLite pool no longer caps MaxOpenConns
at 16 (that starved WeKan's cursor load: minutes-long board loads, failed
logins).

Thanks to xet7.
2026-07-17 13:35:42 +03:00
github-actions[bot] e15c920872 Bump versions for v9.98 2026-07-17 03:16:35 +00:00
Lauri Ojansivu d1ac53b367 Updates 2026-07-17 05:03:22 +03:00
Lauri Ojansivu c951b40e5e Changelog: Upcoming security section for EscapeBleed and RandomBleed.
Document the two GitHub CodeQL code scanning fixes already committed
(alerts #423 js/incomplete-sanitization and #422
js/biased-cryptographic-random) in a new Upcoming section above v9.97,
linked to the wekan.fi Hall of Fame entries.

Thanks to GitHub CodeQL and xet7.
2026-07-17 04:57:32 +03:00
Lauri Ojansivu be6ebb2503 Fix CodeQL js/biased-cryptographic-random in schema upgrade randomId.
256 is not a multiple of the 55-character id alphabet, so the plain
byte % 55 mapping skewed generated document ids toward the first
characters. randomId now uses rejection sampling: bytes at or above the
largest multiple of the alphabet size (220) are discarded and resampled,
so every character is equally likely. Ids stay Meteor-style 17-char and
exact-length for custom lengths. Resolves code scanning alert #422.

Tests: tests/schemaUpgradeSteps.test.cjs gains a rejection-sampling
guard (negative case pins that out-of-range bytes are never wrapped).

Thanks to xet7.
2026-07-17 04:52:30 +03:00
Lauri Ojansivu d6ddcffcc2 Fix CodeQL js/incomplete-sanitization in maximizedCardPosition test.
The CSS-declaration-to-RegExp conversion escaped only parentheses;
escape the full regex metacharacter set (including backslash) instead.
Resolves code scanning alert #423. Test-only, fixed input list, but the
complete escape is correct and matches the pattern already used in
tests/archiveLinkGrouping.test.cjs.

Thanks to xet7.
2026-07-17 04:52:30 +03:00
278 changed files with 106262 additions and 94533 deletions

No files matched your search

File renamed without changes.
+81 -6
View File
@@ -1192,17 +1192,92 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "v${VERSION}" "${{ steps.build.outputs.snap }}" --clobber
snap-launchpad:
# ─── 9b. Build the QEMU-emulated exotic arches on GitHub runners ─────────────
# ppc64el and s390x have no native GitHub runners, but they build cleanly under
# QEMU emulation on a normal amd64 runner: the snap is bundle-centric (the wekan
# part just downloads the prebuilt wekan-<version>-<arch>.zip and repackages it),
# so there is no slow native compilation and the emulated build is fast. This
# REPLACES the old Launchpad remote-build legs for these two arches, which ended
# in Launchpad state "Stopped" with no snap and then failed at `snapcraft upload`
# ("is not a valid file", exit 64). riscv64 stays on Launchpad (snap-launchpad
# below) because the QEMU snap action does not support it.
# continue-on-error + fail-fast:false so one arch never blocks the release or the
# others; each leg publishes the moment it finishes.
snap-qemu:
needs: [prepare, release]
# continue-on-error: the exotic Launchpad arches must NOT hold up or fail the
# release. riscv64 in particular queues for hours / can time out; that must not
# turn the whole release red. fail-fast: false so one arch never cancels the
# others. Each leg publishes the moment it finishes.
continue-on-error: true
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
arch: [s390x, ppc64el, riscv64]
arch: [ppc64el, s390x]
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- uses: actions/checkout@v7
with:
ref: refs/tags/v${{ needs.prepare.outputs.version }}
- name: Verify snapcraft.yaml version matches the release
run: |
set -euo pipefail
if ! grep -qxF "version: '${VERSION}'" snapcraft.yaml; then
echo "::error::snapcraft.yaml is not at version '${VERSION}' ($(grep -m1 '^version:' snapcraft.yaml)). The bump job (releases/version.sh) did not update the tagged commit."
exit 1
fi
echo "snapcraft.yaml is at version '${VERSION}'."
- name: Set up QEMU for ${{ matrix.arch }} emulation
uses: docker/setup-qemu-action@v4
- name: Build the ${{ matrix.arch }} snap under QEMU
# Runs snapcraft inside a ${{ matrix.arch }} container via QEMU; snapcraft
# sees build-on: ${{ matrix.arch }} in snapcraft.yaml and builds that
# platform. Outputs the built .snap path as steps.build.outputs.snap.
id: build
uses: diddlesnaps/snapcraft-multiarch-action@v1
with:
architecture: ${{ matrix.arch }}
- name: Verify the built snap is ${{ matrix.arch }}
run: |
set -euo pipefail
snap="${{ steps.build.outputs.snap }}"
echo "Built: $snap"
case "$snap" in
*_${{ matrix.arch }}.snap) : ;;
*) echo "::error::Built snap '$snap' is not ${{ matrix.arch }}."; exit 1 ;;
esac
- name: Push ${{ matrix.arch }} to the Snap Store (stable + candidate + beta + edge)
uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_AUTH }}
with:
snap: ${{ steps.build.outputs.snap }}
release: stable,candidate,beta,edge
- name: Attach the ${{ matrix.arch }} snap to the GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "v${VERSION}" "${{ steps.build.outputs.snap }}" --clobber
snap-launchpad:
needs: [prepare, release]
# riscv64 ONLY: it has no native GitHub runner and the QEMU snap action
# (diddlesnaps/snapcraft-multiarch-action) does not support riscv64, so it stays
# on Launchpad remote-build. ppc64el and s390x moved to snap-qemu above.
# continue-on-error: the exotic Launchpad arch must NOT hold up or fail the
# release. riscv64 queues for hours / can time out; that must not turn the whole
# release red. timeout-minutes bounds a stuck build so it fails with a clear
# message instead of GitHub's silent 6h cancel.
continue-on-error: true
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
arch: [riscv64]
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
+370
View File
@@ -86,6 +86,376 @@ them up next.
same `params.user` feeds both the e-mail notification text, where the full name is intended, and the webhook payload,
where a username is expected; the safe change is to ADD a `username` field to the webhook rather than repurpose `user`).
# v10.01 2026-07-18 WeKan ® release
This release adds the following features:
- **Member Settings — optional "Submit editors with Enter"**. A new per-user setting in the Member Settings
(Change Settings) popup, saved to `profile.submitOnEnter` and **off by default**: when on, plain **Enter** saves
the card title, description and other inline editors and **Shift+Enter** inserts a new line; when off, behaviour is
unchanged (Ctrl/Cmd+Enter saves). This restores the fast Enter-to-save workflow for users who prefer it without
regressing #4236. [Setting](https://github.com/wekan/wekan/commit/bb6b0d5477368a97ba5cc1c01bdb598dcbe68cd2), [translated to all languages](https://github.com/wekan/wekan/commit/b8bb00465037389eb5829157441f2b75819353d9). Thanks to **xet7**.
- **Right board sidebar — drag the edge to resize its width** (desktop). Drag the sidebar's edge like a spreadsheet
column; the width is saved to `profile.sidebarWidth` for logged-in users and to browser `localStorage` for
anonymous users on a public board, then re-applied on load. The handle uses logical positioning, so in LTR it is
the left edge of the right-docked sidebar and in RTL the right edge of the left-docked sidebar (drag direction
inverts); on phones the sidebar stays full width. [Feature](https://github.com/wekan/wekan/commit/354b7804b577c2ca92b479b7bd2ef595cc9212d3),
[tooltip translated to all languages](https://github.com/wekan/wekan/commit/1284afc588d6a52f89fdbb80d6f6ede9bc6bdfeb). Thanks to **xet7**.
and fixes the following bugs:
- **Upgrade migration no longer drops cards/activities whose exported data holds `NaN` or `Infinity`**
([#6481](https://github.com/wekan/wekan/issues/6481)). mongo 3.x `mongoexport` emits non-finite doubles as the bare
tokens `NaN`/`+Infinity` (e.g. a card's `"sort":+Infinity`, an activity's `"value":NaN`), which are not valid JSON,
so `EJSON.parse` threw and the whole document was dropped — leaving boards/cards missing after an otherwise
"successful" 6.09 → 10.x migration. Those bare tokens are now rewritten (outside string literals) to the canonical
EJSON `{"$numberDouble":"…"}` form so the document migrates instead of being lost. [Fix](https://github.com/wekan/wekan/commit/e0991d6602c915e13d77776af36ecac410965909). Thanks to
**Nissulya** and **xet7**.
- **Add List — the inline composer's previous options are back**
([#6465](https://github.com/wekan/wekan/issues/6465)). The per-list-header inline Add List composer had been
reduced to just a title input; the **"add after which list" position selector** (pre-selecting the list whose
header opened it) and the **"or template"** link are restored. [Fix](https://github.com/wekan/wekan/commit/b71461d90c6a7d9ef9a835cf151181e15c84526d). Thanks to **csonkaoszimt**
and **xet7**.
- **Right sidebar width no longer doubles on wide screens, and its input fields fill the width**. The sidebar used
`width: 30vw`, so it grew unbounded on very wide screens (looking right only at mid widths); it is now clamped. The
filter text inputs (Filter List by Title, Filter by card title, …) had no width and rendered narrow with a big gap
on the right; they now fill the padded content width with equal spacing on both sides.
[Fix](https://github.com/wekan/wekan/commit/b8a21e4ce3acca427a25f11bcb8c0fdf6bc26ee3). Thanks to **xet7**.
- **The Multi-Selection sidebar "Copy selection" button showed its raw translation key**. The `copy-selection` key
was missing from every language file — including the English source — so i18next had no value to fall back to. It
was added ("Copy selection") and translated to all languages. [Fix](https://github.com/wekan/wekan/commit/72042b7d796ce66c9f9da637c90e84493b67b3c1). Thanks to **xet7**.
- **Follow-up fixes surfaced by the full test run.** Now that generic Chinese (`zh`) is its own registered language,
a bare `zh` browser tag resolves to that entry (Simplified Chinese) instead of the old `zh → zh-Hans` alias, which
is removed; and the board-actions end-to-end test opens the board menu from the right sidebar (the header Board
Settings cog was removed). [Fix](https://github.com/wekan/wekan/commit/08cfe2d7881aee399dd0e0a05629096e3027b9b2).
Thanks to **xet7**.
Thanks to above GitHub users for their contributions and translators for their translations.
# v10.00 2026-07-18 WeKan ® release
This release adds the following updates:
- [Updated Sandstorm Backup docs](https://github.com/wekan/wekan/commit/50dd05ceee9aa954bb09c239ed057334de63015a). Thanks to robertdahlem and xet7.
and adds the following features:
- **WeKan has been translated to 154 languages. Thanks to xet7.** Every language file in
`imports/i18n/data/` is now filled in (previously most were largely English). Each language matches its own
existing terminology as a glossary plus standard Kanban terms; product/brand names, acronyms, placeholders and
markup are preserved; regional variants were seeded from their base language; and a few wrong ones were corrected
(Wu Chinese `wuu-Hans` was registered; the `wa-RR` file is Waray/`war`, not a Walloon variant, so it was
re-translated; Wolof/Malay display names were fixed) so that **all languages are browser-detectable and visible in
the Change Language popup**. `releases/translations/push-all-translations.sh` force-pushes every language to
Transifex. Commits:
- [Finnish: translate newest UI strings (theme, font, size, colors, Sandstorm)](https://github.com/wekan/wekan/commit/061eba8b7aa4c96e356c46c0e47fb22b1b773f8d)
- [i18n: translate new admin/import-export strings to Finnish](https://github.com/wekan/wekan/commit/687a4af8617c0b06f98e20b283b0a2fd329a76a3)
- [releases: add translations-push-all-translations.sh (force-push all languages)](https://github.com/wekan/wekan/commit/a9f2e9f50cbb55eb865478ac14ce2ac1d80b5913)
- [i18n: translate untranslated strings (wave 1: zh-TW, pt-BR, da, he, sv)](https://github.com/wekan/wekan/commit/63bda22faacd430f4680c9cbe0ce521129096a5e)
- [i18n: translate untranslated strings (wave 2: 15 major languages)](https://github.com/wekan/wekan/commit/7691829cdb293796eab7794eae6c0cb4ec829e6b)
- [i18n: seed 32 regional variants from their translated base languages](https://github.com/wekan/wekan/commit/ea4a10c212c8037b833ece0010032f6a8d52b9a8)
- [i18n: seed close-relative lects as baselines (cmn, wuu, yue, ja-Hira, vls, ary)](https://github.com/wekan/wekan/commit/c795d13450fc7fea5f9cc55fbdc1ac843aeafb6d)
- [i18n: register generic Chinese (zh) so it is selectable and browser-detectable](https://github.com/wekan/wekan/commit/75ec34e83726f7792240edef70de59648c96ef35)
- [i18n: translate untranslated strings (wave 4: 15 languages + 9 variants)](https://github.com/wekan/wekan/commit/a184a3a717331239f38203fc56ed78cc9a04f11e)
- [i18n: translate untranslated strings (wave 5: 15 languages + 5 variants)](https://github.com/wekan/wekan/commit/97feb986a88f2f6128fd6bf74be922bed83c104e)
- [i18n: translate untranslated strings (wave 6: 15 languages + 2 variants)](https://github.com/wekan/wekan/commit/91ccef062b5ec866aec8ac4660235e46e14e55ab)
- [i18n: translate untranslated strings (wave 7, final: 14 languages + 5 variants)](https://github.com/wekan/wekan/commit/219011e5eb3ef3994a074fed9cc441faa734fada)
- [WeKan has been translated to 154 languages](https://github.com/wekan/wekan/commit/01d55828d23a645e1c8a841bb4e6357223399d47)
- [i18n: register wuu-Hans, fix Wolof/Malay names, add wuu browser alias](https://github.com/wekan/wekan/commit/d55c5e2c3cd544c0ad94538b689b63fe50cd6a41)
- [Changelog: WeKan translated to 154 languages, with commit links](https://github.com/wekan/wekan/commit/5b3394af2f3fad92286260d8e94158988e2f3196)
- [Moved translation script to correct directory. Thanks to xet7](https://github.com/wekan/wekan/commit/95552881fc7e7256edadf51aa84b6b4436cb0e24)
- [i18n: fix wa-RR — translate as Waray (war), not Walloon](https://github.com/wekan/wekan/commit/c00f24d668df1bfc1979a37172dadc1f2d9a77e4)
- [Changelog: add Part 12 (Waray wa-RR fix) to the 154-languages entry](https://github.com/wekan/wekan/commit/2cf878215d5cb09f8bfa7f8368e9cc36be3931d3)
- [releases: fix push-all-translations.sh cd depth for its translations/ location](https://github.com/wekan/wekan/commit/2be92538391e1e2b92c87ab4e2217ce2e9e39ec0)
- **Member menu / Change Color — global theme override, visible swatches, applies everywhere, no Save button**
([#5778](https://github.com/wekan/wekan/issues/5778), `docs/Theme/Theme.md`). The member menu gets a **Change
Color** entry that works like Board Settings / Change Color but is a **per-user global override** saved to your
profile, so the chosen theme applies to the **whole UI** — All Boards, Search, Admin Panel, My Cards, Due Cards
and board pages — via a `board-color-<name>` class on `<body>`/header (while you are on a board, that board's own
color still wins). This makes a **dark theme everywhere** possible. **Done differently from the first cut:** the
picker was initially built as **two-level dropdowns** (category then theme); that was replaced by **visible color
swatches grouped by category** (Flat / Clear / Dark / Special, category name above each group), and the **Save
button was removed — clicking a swatch applies immediately**. **Flat** themes allow **1 custom color** and
**clear** themes **2** (a gradient) via the native color wheel; **dark**/**special** are fixed. Custom colors are
validated server-side as `#rrggbb` and applied as CSS variables (`--theme-accent` / `--theme-accent-2`). Category
titles are left-aligned and the popup has a title. Thanks to **xet7**. Commits:
- [Member Menu / Change Color: global per-user theme override (#5778)](https://github.com/wekan/wekan/commit/eab1546b885336502549d9c4fcc45feec661310a)
- [Select Color: theme categories core + design doc (docs/Theme/Theme.md)](https://github.com/wekan/wekan/commit/49285dad1950a3019972dbf08d40201b256719ca)
- [Select Color: two-level dropdown picker + custom colors (#5778)](https://github.com/wekan/wekan/commit/d0a1e43f364d8ad0caef725e5d48db507887a962)
- [Select Color: global override applies everywhere + visible grouped swatches (#5778)](https://github.com/wekan/wekan/commit/1cd32001d474bed977d56ff0fdbcfbc090191974)
- [Change Color popup: immediate apply (no Save), left-aligned titles, title, spacing](https://github.com/wekan/wekan/commit/772ae061f109478a875fc6e570f0afe26e572148)
- [Change Color popup: remove the "Preview" text from the custom-color preview swatch](https://github.com/wekan/wekan/commit/0f87c3bf8b33241927ff5437c222907986ab37df)
- **Member Settings / Font — pick an installed UI font, size and text colors**
(part of [#4759](https://github.com/wekan/wekan/issues/4759)). A new **Font** member-menu entry lets you choose a
UI font that **already exists in your browser** (canvas-detected; names are plain text, never HTML/markdown,
validated server-side against a curated whitelist — only a known font name and a preset percentage ever reach the
DOM), a font **size**, and custom **text** and **text-background** colors (each unsettable). **Done differently
from the first cut:** the font and size were first built as **dropdowns**; that was replaced by **buttons** — one
**font-name button per detected font, each rendered in its own font**, and a row of **size buttons** (smaller …
100% in the middle … larger) — that **apply immediately on click** (no Save). A **preview pangram** shows below
the font buttons, the **Unset** links are styled as buttons, and the popup has a title. Stored as `profile.uiFont`
/ `profile.uiFontSize` / `profile.uiTextColor` / `profile.uiTextBgColor`. (Only the font part of #4759, so the
issue stays open.) Thanks to **xet7**. Commits:
- [Member Settings / Font: pick an installed font + preset size (#4759)](https://github.com/wekan/wekan/commit/3c9524f61a43cbf92f856f0fb2ff1f9b291d4e05)
- [Member Settings / Font / Color: custom text + background color (#4759)](https://github.com/wekan/wekan/commit/22c7d5376bcf7990890c7254c80c1e2dfd9e419c)
- [Fix #4759 regression: uiTextColor/uiTextBgColor custom() rejected every user insert](https://github.com/wekan/wekan/commit/437c64c9a0c05dbf3d12a8e45e0b8281f3c326e6)
- [Member Settings / Font popup: add a title (changeFontPopup-title)](https://github.com/wekan/wekan/commit/969f96d74219061e05093d5dff7061a684d22f65)
- [Member Settings / Font: font-name + size buttons, immediate apply (no dropdowns)](https://github.com/wekan/wekan/commit/bd8e417be18837b2e4600b3b25fa605a83910a2f)
- [Member Settings / Font: show the preview pangram below the font-name buttons](https://github.com/wekan/wekan/commit/aa15e726829a8e070634d7c04698bd251f817f08)
- [Font popup: style the "Unset" colour reset links as buttons](https://github.com/wekan/wekan/commit/3d190ca0222d657827ed03993ba542279148482a)
- **Soft delete + Undo/Redo + change-History — deleting is reversible, and moves can be undone**
([#1023](https://github.com/wekan/wekan/issues/1023), [#6478](https://github.com/wekan/wekan/issues/6478),
`docs/Features/Undo/Undo.md`). Deleting a **list** no longer destroys it: the list and its cards are **marked**
deleted (`deletedAt`/`deletedBy`/`deleteBatchId`), hidden from the board, and restorable via **`lists.restore`**
or **`Ctrl+Z`** — the first slice of a general "no permanent delete in ordinary use" principle (physical deletion
limited to GDPR/account erasure and an explicit Global-Admin purge behind an off-by-default **Admin Panel /
Features / Delete** flag). Card/list/swimlane **moves** are now undoable/redoable with `Ctrl+Z` / `Ctrl+Y` (the
`userPositionHistory` collection was silently **not recording** — its guard checked an un-imported global — and had
no redo or key bindings; all fixed, selection logic unit-tested). Design unifies this into one universal
change-History with scoped views. Activities were confirmed to already load progressively. Thanks to **xet7**.
Commits:
- [Soft delete for lists: delete = mark, restorable + undoable (#1023)](https://github.com/wekan/wekan/commit/46924985f88b473e054158ecae68af6d7cce3556)
- [Undo docs: soft-delete design (delete = mark, never destroy)](https://github.com/wekan/wekan/commit/58344db0d7e3ed9e4f8ca638e0b3e00d2813ac63)
- [#6478: undo/redo for card/list/swimlane moves + confirm high-impact list move on touch](https://github.com/wekan/wekan/commit/4ca33d29f78bd81cecc931faa5164f862823ad52)
- [Docs: design for per-group card History (view + restore)](https://github.com/wekan/wekan/commit/3d1276fc1fbd063f1c7b79467b788f13a5e62435)
- [Docs: undo/redo + high-impact confirmation feature doc](https://github.com/wekan/wekan/commit/f3354f5cc8577cda46d2789230e8d30939bd927d)
- [Docs: unify undo/redo into one universal change-History (every change) with many scoped views](https://github.com/wekan/wekan/commit/9be41ab34b1bd596a82b95f088e49e94da8b988c)
- [Activities already load progressively (infinite scroll), like visible cards](https://github.com/wekan/wekan/commit/e246403bb6969f402e066d01e256e2c56f10ff1c)
- **Home board — set a board that opens automatically after login**
([#2220](https://github.com/wekan/wekan/issues/2220)). A per-user default "home" board (`profile.defaultBoardId`)
opens automatically after login on **all deployments** (a once-per-session router redirect), and on **Sandstorm** a
grain with exactly one board again opens straight into it (**without saving anything**; a saved Home board wins, a
many-board grain with no choice shows All Boards). You **set the Home board from the All Boards page via
Multi-Selection**: turn Multi-Selection on, select a board, and click the **home** action in the new
**"Selected:"** row (which also has a **star** action); the Home board then shows a **home badge** and sits at the
**top of Starred**. **Done differently:** the earlier per-board-tile home toggle was hidden, but setting a Home
board was brought back through the Multi-Selection "Selected:" action — so the feature is **available, not
disabled, and not Sandstorm-only** — and the initial Sandstorm "save the single-board choice" was changed to
**no-save**. Thanks to **xet7**. Commits:
- [Feature #2220: mark a board to open automatically after login (home board)](https://github.com/wekan/wekan/commit/e8ced84172ec7950800becfee9860c33e964034e)
- [Sandstorm: auto-open a single-board grain (no save) (#2220)](https://github.com/wekan/wekan/commit/9a0c0613a5909f9a37f6afb20890894b8e5082bd)
- [All Boards: hide Home-board icons and disable setting a Home board (#2220)](https://github.com/wekan/wekan/commit/ac20cd64424ebe356fcd7ccb3a4f60370580e7a3)
- [All Boards: "Selected:" star/home actions + Home board badge (#2220)](https://github.com/wekan/wekan/commit/d67d27f0d872c4fc6e990262d3d3577a28783eee)
- [Tests: cover the remaining Upcoming changes (#2220 default board, #6465 UI, #6478 confirm)](https://github.com/wekan/wekan/commit/8287865b238f3ed90e60e4624e98b383e896beee)
- **Add List moved from a standing column to a per-list header button**
([#6465](https://github.com/wekan/wekan/issues/6465)). The Swimlanes/Lists views no longer show a permanent "Add
List" composer column; each list header has a far-right add-list button that opens the composer as a column
**immediately after that list** (right in LTR, left in RTL), and an **empty** swimlane/board shows a **+** button.
Also fixes the composer's Save doing nothing (`createListAfter`'s `check()` used a non-`Optional` matcher for
`nextListId`, so the inline composer 400'd with "Match failed"). *(First cut — desktop/main views.)* Thanks to
**csonkaoszimt** (report) and **xet7**. Commits:
- [#6465: Add List as a per-list header button + inline composer, not a standing column](https://github.com/wekan/wekan/commit/765d04bf7c678a9f28b663beadae7b07fd79f339)
- [Fix add-list: Save did nothing (Match failed) + empty-swimlane + button (#6465)](https://github.com/wekan/wekan/commit/0535215225a41bfe16d936a9a1b447fc77f9122e)
- **All Boards, board header, top bar and popups — a large batch of layout/theming polish.** Highlights: the
right-sidebar **hamburger is pinned to the right edge** at every width with an ~8px gap, and the duplicate **Board
Settings cog was removed**; the All Boards **My Boards toolbar wraps** (order Multi-Selection then Sort then
Search), the **Multi-Selection button matches the Swimlanes view**, the empty grey band was removed, **"+ Add …"
tiles match board-tile height**, the board-tile **drag handle sits at the right middle** (transparent, icon only),
and the **search box is half width** with the multi-selection hint beneath it; the **mobile/desktop toggle and
zoom** swapped places; the **Change Language** popup shows **multiple columns**; the **Change Avatar** upload
button and the **Search All Boards** / **Archived boards** buttons follow the **theme colors**; the **Archived
boards modal** is full width and starts below the header; the **Notifications drawer** is full width with a black
hamburger + X and black menu icons; and the member menu's duplicate **Notifications** entry was removed. **Done
differently:** an "icons only — always hide button text labels" board-header change was tried and then
**reverted**, so button **text labels stay**. Thanks to **xet7**. Commits:
- [Board header: keep the sidebar hamburger pinned to the right edge at all widths](https://github.com/wekan/wekan/commit/7718118654f35a214ea5f9b2147642eabf4a6702)
- [Board header: remove empty space around the sidebar hamburger button](https://github.com/wekan/wekan/commit/e33e50d9c84cc9125ac6e8d5e491a7419bbae883)
- [Board header: hamburger right gap matches the user-name gap (~8px)](https://github.com/wekan/wekan/commit/bd72f5d64c58ad0861f370a17969f6ee482c466c)
- [Board header: hide button text labels on narrow windows (icons only)](https://github.com/wekan/wekan/commit/cfd53cc9698bbc759bef55152ea8f74ca00f3afc)
- [Board header: remove the Board Settings cog (it is already in the right sidebar)](https://github.com/wekan/wekan/commit/68723ff43b324dda1b584e5ba4eb5d90d0490124)
- [Board header: icons only — always hide button text labels](https://github.com/wekan/wekan/commit/97b0240ca4bb5f2bf02020e94eb6d7ffc278acd9)
- [Revert board-header icons-only; update CHANGELOG Upcoming with newest changes](https://github.com/wekan/wekan/commit/834698bb5e3e926213676e499bf4338713bc43da)
- [Header/All Boards: reorder Multi-Selection/Sort/Search and swap mobile-toggle/zoom](https://github.com/wekan/wekan/commit/15e756f4e0b265a020b659fea2dc3339b2c2e767)
- [All Boards: + Add Board / + Add Template Container tile same height as board icons](https://github.com/wekan/wekan/commit/10085615179dbdd56bd5d630ee526e82a1451ef6)
- [All Boards: remove empty .board-list-header dead grey space above the layout](https://github.com/wekan/wekan/commit/1312fefb1234d6fcdcda930d93fd189e57befd37)
- [All Boards: My Boards bar wraps so Sort/Multi-Selection/Clear stay visible](https://github.com/wekan/wekan/commit/7699cc1e90f18b599f294fba774403f3b17e3749)
- [All Boards: Multi-Selection button looks the same as in Swimlanes View](https://github.com/wekan/wekan/commit/c6aa769bc0a8b01706e0f45a19b8ba8c66c77498)
- [All Boards: move board tile drag handle to the right middle](https://github.com/wekan/wekan/commit/aafe71fd191a8633012d838d00aa96abd3f092a2)
- [All Boards: transparent board tile drag-handle background (icon only)](https://github.com/wekan/wekan/commit/197254fe16c2dd2c266f2a1797d80feffccfc914)
- [All Boards: vertically center the Search boards input text](https://github.com/wekan/wekan/commit/035195f2b4b7a8601964d54645ec7d139e65dd41)
- [All Boards: move the Search boards input text 4px further down (top: 6px)](https://github.com/wekan/wekan/commit/cb80198d9748b2fbd7dee392e235f882785420c7)
- [All Boards: move the Search boards input text 2px further down (top: 8px)](https://github.com/wekan/wekan/commit/c21edb160bddfb2a9e635d5c2154c037dc464698)
- [All Boards: move "+ Add Board" text 4px up (keep tile height)](https://github.com/wekan/wekan/commit/3f5173b9602e14de3201ebc0052b2751e522f100)
- [All Boards: move "+ Add Board" text 2px further up (padding 18/8/24/8)](https://github.com/wekan/wekan/commit/519c04b8ef46497d526f4586432b132887335487)
- [All Boards: move "+ Add Template Container" text 2px down (separate from Add Board)](https://github.com/wekan/wekan/commit/cd7f9f7bc9e48c6f54b007deec13194b90d4d33b)
- [All Boards: half-width search, hint below Multi-Selection, title beside icon](https://github.com/wekan/wekan/commit/3736f8431a4b0b8224c0b7315ac8e596aa4426fa)
- [Change Language popup: show languages in multiple columns when there is width](https://github.com/wekan/wekan/commit/fc0649a756d0f6e6caf2feef659786a9908397b1)
- [Change Avatar popup: Upload button uses theme colors (blue bg, white text)](https://github.com/wekan/wekan/commit/08a2e5525e8e7b099d766643b09616e75f750fc9)
- [Archived boards modal: full width + extends to the bottom of the window](https://github.com/wekan/wekan/commit/b027594ca8ad6d9f781214e8c26aac572ed20c25)
- [Archived boards modal: start below the header bar so its top is visible](https://github.com/wekan/wekan/commit/1c8ba2afc3a51d5a0b71f5777d1e189dca5525e4)
- [Archived boards: Restore/Delete buttons follow the theme override colors](https://github.com/wekan/wekan/commit/4dc09c62b9719cbb3dd126420239f7d9da5bea55)
- [Search All Boards: new-search button follows theme override colors](https://github.com/wekan/wekan/commit/98a03bf045668a95f58ae2cfe15f6d39591c89de)
- [Search All Boards: label the clear button "Clear" using existing trello-clear-job key](https://github.com/wekan/wekan/commit/a4cc0c2182b9c4da74325bb5e21d7f13f64e9484)
- [Member menu: remove the Notifications entry (duplicates the header bell icon)](https://github.com/wekan/wekan/commit/8cb4050cf21a1fce10c08872cb8f4a4bb270565b)
- [Notifications drawer header: black X + hamburger, X near the top-right edge](https://github.com/wekan/wekan/commit/8dd6bd9db5a8f427f6863f033e7236ae80ead336)
- [Notifications drawer: full width + robust flex header (black hamburger + X)](https://github.com/wekan/wekan/commit/c87b4cb5103aa2e010f0c40b0554e4cf84126059)
- [Notifications drawer: fix X position — cancel the global absolute .close rule](https://github.com/wekan/wekan/commit/5e014d0967ec8d622857348a264f3522a7e9769d)
- [Notifications menu: make the dropdown item icons black](https://github.com/wekan/wekan/commit/01088ea9d4583301446cb021ecd8772e4d3cde6d)
- **Denser default layout: thinner lists, board tiles and card dock; taller-looking minicards**
([#6465](https://github.com/wekan/wekan/issues/6465)). List width default 272 to **220px** (minimum 270 to
**200px**), All Boards board tiles min-height 100 to **72px**, and the card-detail right dock max-width 800 to
**520px** — all overridable, user customizations untouched. Minicards no longer reserve a tall empty band under a
one-line title (the title viewer gets `min-height: 0`), and the swimlane header no longer clips at non-100% zoom.
Thanks to **csonkaoszimt** (report) and **xet7**. Commits:
- [#6465: denser defaults - thinner lists (220px), board tiles and card dock (520px)](https://github.com/wekan/wekan/commit/99299c2a91827f88f0bf091e810b8ce9d6211784)
- [Fix #6465: minicards wasted half their height on empty white space](https://github.com/wekan/wekan/commit/a76f7d28e33f79b79f89eebffcb2a2946205f5f0)
- [#6465: swimlane header bar no longer clips at non-100% board zoom](https://github.com/wekan/wekan/commit/7b7fada1b89fa21e35ebaeca26d3f85299afb33d)
- [Test: update fixed-list-width e2e for the lowered 200px minimum (#6465)](https://github.com/wekan/wekan/commit/ae29555862089cd85111aa417a892888d48124bd)
and fixes the following bugs:
- [Confirmation dialogs work again](https://github.com/wekan/wekan/commit/f6f021f8cc8de452593f499e1306dca021679325) — clicking any `.js-confirm` button (e.g. "Remove Member") did
nothing because `Popup.afterConfirm()` stashed the pending action on the Blaze **data context** (an immutable
Minimongo doc that Blaze re-creates on re-render); it is now stored on the Popup **instance**
([#6479](https://github.com/wekan/wekan/issues/6479)). Thanks to **mueschel** (report) and **xet7**.
- [Moving a card between swimlanes is no longer broken with long lists](https://github.com/wekan/wekan/commit/6d5a594b0709ad3456f91afc31d821304539a262) — jQuery UI sortable cached
geometry at drag-start and never re-cached on WeKan's manual auto-scroll, so the drop re-homed the card in the
source swimlane; fixed with `sortable('refreshPositions')` after a scroll
([#6477](https://github.com/wekan/wekan/issues/6477)). Thanks to **mueschel** (report) and **xet7**.
- [Filter by date -> Overdue no longer lists cards with no due date](https://github.com/wekan/wekan/commit/b1d33c536239d45ae5df6139beb6b5548f7e03c0) — `$lte`/range selectors
matched `null` under both FerretDB and minimongo; `DateFilter._getMongoSelector` now adds `$ne: null` (reported by
email). Thanks to the reporter and **xet7**.
- [A board-wide list no longer disappears from other swimlanes when nudged](https://github.com/wekan/wekan/commit/c29cf3b985abb670222ec25c76a055e0ff9ba53a) — the drop handler
treated any small drag of a board-wide list (`swimlaneId === null`) as a swimlane change; it now also requires an
original swimlane (reported by email). Thanks to the reporter and **xet7**.
- **FerretDB (bundled) — #6476 crash-loop root cause fixed, and label/title/date filters pushed down to SQLite**
([#6476](https://github.com/wekan/wekan/issues/6476)). A startup SyncedCron upsert hit an orphaned-table error
(`table "…connections_<hash>" already exists`) that became an unhandledRejection and crash-looped WeKan so its
port never opened; the bundled FerretDB now creates tables/indexes with `IF NOT EXISTS`. It also pushes label
(`$in`), title (`$regex`) and `dueAt` range filters down to SQLite instead of scanning every card in Go. (Details
in the fork's own CHANGELOG.) Thanks to **uusijani**, **a1bert01** and **xet7**. Changelog commits:
- [Changelog: #6476 real root cause (FerretDB orphaned-table crash loop) and the bundled FerretDB fix](https://github.com/wekan/wekan/commit/41c747ef5f7e3fac4ae37714e65e1f7b4703ab80)
- [Changelog: bundled FerretDB now pushes label ($in) and title ($regex) filters down](https://github.com/wekan/wekan/commit/b29a47778ffb3025d8c26d67427e022a6a137076)
- [Changelog: bundled FerretDB now pushes date range filters down too](https://github.com/wekan/wekan/commit/5730f7dcc54e551ca5b4cc6ede307ebe7e078310)
- **Snap release, CI and dev tooling.** The exotic `ppc64el`/`s390x` snaps now build on **GitHub Actions under
QEMU** instead of Launchpad (which returned exit 0 with no artifact); `rebuild-wekan.sh` CURRENT-IP detection is
subnet-agnostic (no more `http://:3000`); and the **Playwright E2E workflow was disabled**. Commits:
- [Snap release: build ppc64el and s390x under QEMU on GitHub Actions, not Launchpad](https://github.com/wekan/wekan/commit/d7f36d70a04ee96669d75c2dea675cf7e7b76d7a)
- [rebuild-wekan.sh: robust CURRENT-IP detection (fix http://:3000 Invalid URL)](https://github.com/wekan/wekan/commit/c6f960bbe10d5ff36a92314c0a21380e914a89e3)
- [ci: disable Playwright E2E Tests workflow](https://github.com/wekan/wekan/commit/ef81c25232c7f5f26b6b33d9e9b945b8e769dd35)
- **Changelog, docs and test housekeeping.** Commits:
- [Changelog: move post-v9.99 changes to an Upcoming section above v9.99](https://github.com/wekan/wekan/commit/c4784225d51f2d13f2f50b2fe172cb156feab861)
- [Updated ChangeLog](https://github.com/wekan/wekan/commit/26319c974b180cc2a59b55705bc5d5128f23f438)
- [Changelog: drop bogus #6483/#6484 issue links (those bugs were reported by email)](https://github.com/wekan/wekan/commit/e9e70717445afbf124fe5c536f4199ef97d8ff5b)
- [Updates](https://github.com/wekan/wekan/commit/176c5ef2d8a31f85cb41e52a85003e3560e818cc)
- [Test: de-flake webkit swimlane-add popup wait (6s -> 15s)](https://github.com/wekan/wekan/commit/8aacc2236052108576e9b0e1ac0ca68f9339463e)
- [Updates](https://github.com/wekan/wekan/commit/861ded439e08dfe16e65f19a3e150d83bbd8286f)
- [Updated ChangeLog](https://github.com/wekan/wekan/commit/66a2f8ddc8f4bc95fe08478f1d0eeda08f87cf82)
Thanks to above GitHub users for their contributions and translators for their translations.
# v9.99 2026-07-17 WeKan ® release
This release fixes the following BUG:
- **LDAP connection leak: WeKan exhausted the directory server with "too many open connections"**
([#6467](https://github.com/wekan/wekan/issues/6467), [#6469](https://github.com/wekan/wekan/issues/6469)).
Operators reported that after an update WeKan "dies really quickly", that restarting the server did not
help, and that it "kills our openldap server with too many open connections"; others saw logins hang for
minutes and then fail with *"Must be logged in"*. Root cause: every LDAP login attempt
(`packages/wekan-ldap/server/loginHandler.js`) and every background sync run
(`packages/wekan-ldap/server/sync.js`) created a fresh `new LDAP()` and called `connect()`, but the code
**never called `disconnect()` on any path** — success, failure, or fallback. Each login attempt (including
every *failed* one) and each background-sync tick (every minute by default) therefore leaked one socket to
the LDAP/AD server. Over time this grew without bound until the directory server hit its per-client
connection limit and started refusing connections, which took it — and, with it, every WeKan login — down.
- **Fixed** by guaranteeing the connection is always released:
- A small shared helper `packages/wekan-ldap/server/connectionGuard.js` (`runWithLdapDisconnect(ldap, fn)`)
runs the work and `disconnect()`s in a `finally`, on every exit path. The disconnect is best-effort and
never masks the original result or error.
- The LDAP login handler now runs its whole flow through `runWithLdapDisconnect`, so `connect()` is always
paired with a `disconnect()` — on a successful login, a thrown `Meteor.Error`, or a fallback to the
default account system.
- The background `sync()` releases its connection in a `finally`, and `importNewUsers()` disconnects only
the connection it opened itself (never a connection borrowed from `sync()`).
- The admin `ldap_test_connection` method (`packages/wekan-ldap/server/testConnection.js`), which had the
same leak, now disconnects on both the success and failure paths, so repeated "Test Connection" clicks
no longer leak either.
- `LDAP.disconnect()` is now a safe no-op when `connect()` was never reached, so cleanup can never throw.
- **Tests** (`tests/ldapConnectionRelease.test.cjs`, added to `test:unit:node`): behavioural coverage of the
guard (disconnects after success and returns the result; disconnects exactly once; disconnects *and*
re-throws when the work throws — the failed-login path that exhausted OpenLDAP), negative cases (a failing
disconnect never turns a success into a failure nor hides the real error; a missing/`null` ldap is
tolerated), plus source-level guards that the leak-prone call sites actually route through the guard and
that `connect()` lives inside the guarded region.
- Thanks to the reporting operators and **xet7** (fix).
This release also updates the bundled FerretDB (see the fork's own CHANGELOG Upcoming): the SQLite backend
connection pool no longer caps `MaxOpenConns` at 16, which had starved WeKan's cursor-heavy load and made
boards take minutes to load and logins fail with *"Must be logged in"* ([#6467](https://github.com/wekan/wekan/issues/6467),
[#6469](https://github.com/wekan/wekan/issues/6469)).
- **Snap: FerretDB never becoming ready made WeKan hang with no web port and no explanation**
([#6476](https://github.com/wekan/wekan/issues/6476)). With `database=ferretdb`, `snap-src/bin/wekan-control`
waits for FerretDB to accept connections *before* starting WeKan — WeKan does not open its HTTP port until
the database answers. That wait loop had **no timeout and no diagnostics**: if FerretDB never started
listening (a crashed or CPU/architecture-incompatible per-arch binary, a locked or corrupt SQLite database,
or the FerretDB service left disabled by a failed migration hand-off), WeKan blocked there forever. The
service showed as `active`, but the port never opened and the only symptom was a silent
`FerretDB not ready yet, retrying in 5 seconds` repeating — exactly what #6476 reported (Apache proxy could
not connect to port 3333). The reporter's `wekan.log` stopped right after the startup env dump, confirming
control never reached `node main.js`.
- **Fixed** by mirroring the MongoDB branch: after `WEKAN_DB_WAIT_TIMEOUT` seconds (default 120,
overridable) the FerretDB wait loop now prints an actionable hint **once** and keeps retrying — pointing to
`snap logs <snap>.ferretdb` for the real error, naming the SQLite directory to check for locks/corruption,
the arch-mismatch (`exec format error`) case, the `snap start --enable <snap>.ferretdb` recovery, and the
`snap set <snap> database=mongodb` fallback to keep working meanwhile. This turns "port never opens, no
clue why" into a clear message. Regression test: `tests/ferretdbWaitTimeout.test.cjs`, including a negative
guard that the FerretDB wait is not a silent unbounded loop again.
- This is a robustness/diagnostics fix; the underlying reason FerretDB was not accepting connections is
environment-specific and lives in the FerretDB service log.
- Thanks to **uusijani** (report) and **xet7** (fix).
Thanks to above GitHub users for their contributions and translators for their translations.
# v9.98 2026-07-17 WeKan ® release
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
- **[EscapeBleed](https://wekan.fi/hall-of-fame/escapebleed/): incomplete string escaping when
building a regular expression** (GitHub CodeQL code scanning alert #423, rule
`js/incomplete-sanitization`, CWE-116 Improper Encoding or Escaping of Output;
`tests/maximizedCardPosition.test.cjs`). Code that turned a CSS declaration into a
`RegExp` escaped only parentheses (`str.replace(/[()]/g, '\\$&')`) instead of the full
regex metacharacter set — an input containing other metacharacters (including a backslash)
would not be escaped correctly, so the generated pattern could match the wrong thing.
- **Fixed** by escaping the complete metacharacter set
(`str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')`), matching the correct pattern already used
elsewhere in the tests. This is test-only code with a fixed, trusted input list, so there
was no injection exposure, but the incomplete escape was genuinely wrong.
- Thanks to **GitHub CodeQL** (code scanning alert #423) and **xet7** (fix).
- **[RandomBleed](https://wekan.fi/hall-of-fame/randombleed/): biased random ids from a
cryptographically secure source** (GitHub CodeQL code scanning alert #422, rule
`js/biased-cryptographic-random`, CWE-1204 / weak randomness; `server/lib/schemaUpgradeSteps.js`).
The startup schema upgrade generates Meteor-style document ids with
`crypto.randomBytes(len)` mapped through `byte % ID_CHARS.length`. Because 256 is not a
multiple of the 55-character alphabet, that modulo skews generated ids toward the first 36
characters of the alphabet (each ~1.4% more likely than the rest) — reducing entropy of the
ids used for the swimlanes/lists/checklist-items the upgrade creates.
- **Fixed** with rejection sampling: bytes at or above the largest multiple of the alphabet
size (220) are discarded and resampled, so every character is exactly equally likely; ids
stay Meteor-style 17 characters (and exact-length for custom lengths). A negative
regression test pins that out-of-range bytes are never wrapped.
- Thanks to **GitHub CodeQL** (code scanning alert #422) and **xet7** (fix).
Thanks to above GitHub users for their contributions and translators for their translations.
# v9.97 2026-07-17 WeKan ® release
This release adds the following new features:
+1 -1
View File
@@ -7,7 +7,7 @@ LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
# TARGETARCH and TARGETVARIANT are automatically provided by Docker Buildx
ARG TARGETARCH
ARG TARGETVARIANT
ARG VERSION=9.97
ARG VERSION=10.01
ARG DEBIAN_FRONTEND=noninteractive
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
+1 -1
View File
@@ -67,7 +67,7 @@ that by providing one-click installation on various platforms.
- WeKan ® is used in [most countries of the world](https://snapcraft.io/wekan).
- WeKan ® largest user has 30k users using WeKan ® in their company.
- WeKan ® has been [translated](https://app.transifex.com/wekan/) to about 105 languages.
- WeKan ® has been [translated](https://app.transifex.com/wekan/) to 154 languages.
- [Features][features]: WeKan ® has real-time user interface.
- [Platforms](https://wekan.fi/install/): WeKan ® supports many platforms.
WeKan ® is critical part of new platforms Wekan is currently being integrated to.
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v9.97.0"
appVersion: "v10.01.0"
files:
userUploads:
- README.md
+2 -2
View File
@@ -8,10 +8,10 @@ template(name="archivedBoards")
each archivedBoards
li.archived-lists-item
div.board-header-btns
button.primary.board-header-btn.js-delete-board
button.primary.board-header-btn.js-delete-board(type="submit")
i.fa.fa-trash
| {{_ 'delete-board'}}
button.primary.board-header-btn.js-restore-board
button.primary.board-header-btn.js-restore-board(type="submit")
i.fa.fa-undo
| {{_ 'restore-board'}}
= title
+1 -1
View File
@@ -23,7 +23,7 @@ template(name="boardBody")
if debugBoardState
.debug-info(style="position: fixed; top: 0; inset-inline-start: 0; background: rgba(0,0,0,0.8); color: white; padding: 10px; z-index: 9999; font-size: 12px;")
| {{_ 'board'}}: {{currentBoard.title}} | {{_ 'view'}}: {{boardView}} | {{_ 'has-swimlanes'}}: {{hasSwimlanes}} | {{_ 'swimlanes'}}: {{currentBoard.swimlanes.length}}
.board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
.board-wrapper(class="{{#if currentUser.globalThemeColorClass}}{{currentUser.globalThemeColorClass}}{{else}}{{currentBoard.colorClass}}{{/if}}" class="{{#if isMiniScreen}}mobile-view{{/if}}")
.board-canvas.js-swimlanes.dragscroll(
class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
+2 -6
View File
@@ -159,12 +159,8 @@ template(name="boardHeaderBar")
i.fa.fa-times-thin
.separator
//- #6465: one-click board settings — the boardMenu popup used to need
//- two clicks (open sidebar, then Boardeinstellungen). The click
//- handler already exists in boardHeader.js ('click .js-open-board-menu').
if currentUser.isBoardMember
a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}")
i.fa.fa-cog
//- Board Settings cog removed: Board Settings is available in the right
//- sidebar (the hamburger), so the header no longer duplicates it.
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
i.fa.fa-bars
-1
View File
@@ -130,7 +130,6 @@ Template.boardHeaderBar.events({
Meteor.call('toggleBoardStar', boardId);
}
},
'click .js-open-board-menu': Popup.open('boardMenu'),
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
'click .js-watch-board': Popup.open('boardChangeWatch'),
'click .js-open-archived-board'() {
+91 -18
View File
@@ -1,13 +1,5 @@
@import url("../../../public/css/reset.css") print, screen;
/* Board List Header with Zoom Controls */
.board-list-header {
display: flex;
justify-content: center;
margin: 1vh 0 2vh 0;
padding: 1vh 0;
}
/* Two-column layout for All Boards */
.boards-layout {
display: grid;
@@ -260,6 +252,9 @@
align-items: center;
justify-content: space-between;
gap: 8px;
/* Wrap to a 2nd row on narrow windows so the search box and the Sort /
Multi-Selection / Clear buttons stay visible instead of overflowing. */
flex-wrap: wrap;
padding: 12px 16px;
margin-bottom: 16px;
background: #f5f5f5;
@@ -273,6 +268,46 @@
align-items: center;
gap: 8px;
flex: 1;
min-width: 0; /* allow shrinking so path-right can wrap below instead of overflowing */
flex-wrap: wrap; /* the multi-selection hint wraps under the title when tight */
}
/* Keep the folder icon and the menu title (e.g. "Remaining") on ONE line, the
title to the RIGHT of the icon — never let flex-wrap stack them vertically. */
.boards-path-header .path-title {
display: inline-flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
/* Multi-Selection button with its hint stacked underneath (own column). */
.boards-path-header .multiselection-group {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
/* "Selected:" actions row (star / home) below the Multi-Selection button. */
.boards-path-header .selected-actions {
display: flex;
align-items: center;
gap: 8px;
}
.boards-path-header .selected-label {
font-size: 13px;
color: #333;
}
.boards-path-header .selected-action {
cursor: pointer;
color: #333;
font-size: 16px;
line-height: 1;
padding: 2px 4px;
}
.boards-path-header .selected-action:hover {
color: #2980b9;
}
.boards-path-header .multiselection-hint {
@@ -295,6 +330,10 @@
display: flex;
align-items: center;
gap: 8px;
/* Buttons (Sort / Multi-Selection / Clear) wrap among themselves too, so none
is ever cut off on a narrow window. */
flex-wrap: wrap;
justify-content: flex-end;
}
/* #5799: board-name search box in the All Boards header */
@@ -306,6 +345,9 @@
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
/* Roughly half the previous width; the input flexes to fill what's left. */
width: 300px;
max-width: 100%;
}
.boards-path-header .board-search input {
@@ -313,7 +355,11 @@
outline: none;
background: transparent;
font-size: 14px;
min-width: 140px;
flex: 1 1 auto;
min-width: 0;
/* Nudge the text down so it sits at the vertical middle of the field. */
position: relative;
top: 8px;
}
.boards-path-header .board-search .board-search-clear {
@@ -517,19 +563,33 @@
overflow: hidden;
background-color: inherit; /* Inherit board color from parent li.js-board */
color: #f6f6f6;
min-height: 100px;
min-height: 72px; /* #6465: thinner tiles so more boards fit in the overview */
font-size: 16px;
line-height: 22px;
border-radius: 0; /* No border-radius - parent .js-board has it */
display: block;
font-weight: 700;
padding: 36px 8px 32px 8px; /* Top padding for drag handle, bottom for checkbox */
padding: 24px 8px 18px 8px; /* #6465: trimmed; top leaves room for the drag handle, bottom for the checkbox */
margin: 0; /* No margin - moved to parent .js-board */
position: relative;
text-decoration: none;
word-wrap: break-word;
}
/* #2220: Home-board badge — a home icon centred at the top of the board tile. */
.board-list .board-home-badge {
position: absolute;
top: 2px;
left: 50%;
transform: translateX(-50%);
z-index: 3;
color: #fff;
font-size: 14px;
line-height: 1;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
pointer-events: none;
}
.board-list .board-list-item > .js-open-board {
text-decoration: none;
color: inherit;
@@ -561,16 +621,27 @@
}
.board-list .js-add-board .label {
font-weight: normal;
line-height: 56px;
min-height: 100px;
/* Match the board tiles (#6465 thinner tiles) so the grey "+ Add Board" /
"+ Add Template Container" box is the same height as a board icon:
same min-height, padding and line-height as .board-list-item above. */
line-height: 22px;
min-height: 72px;
display: flex;
align-items: center;
justify-content: center;
background-color: #999; /* Darker background for better text contrast */
border-radius: 3px;
padding: 36px 8px 32px 8px;
/* Same total vertical padding as .board-list-item (42px -> same tile height),
but shifted up (top 24->18, bottom 18->24) so the "+ Add Board" text sits
higher. */
padding: 18px 8px 24px 8px;
color: #fff; /* White text */
}
/* "+ Add Template Container" text sits 2px lower than "+ Add Board" (same 42px
total vertical padding, so the tile height is unchanged). */
.board-list .js-add-board .label.is-template-container {
padding: 20px 8px 22px 8px;
}
.board-list .js-add-board .label i {
color: #fff; /* White icon */
}
@@ -668,12 +739,14 @@
.board-list .board-handle {
position: absolute;
padding: 4px 6px;
top: 4px;
left: 50%;
transform: translateX(-50%);
/* Right middle of the tile (matches the mobile/small-screen handle position). */
top: 50%;
inset-inline-end: 6px;
transform: translateY(-50%);
font-size: 14px;
color: #fff;
background: rgba(0,0,0,0.4);
/* Transparent background so only the drag-handle icon shows (not a box). */
background: transparent;
border-radius: 4px;
display: flex;
align-items: center;
+46 -28
View File
@@ -1,7 +1,5 @@
template(name="boardList")
.wrapper
.board-list-header
.boards-layout
// Left menu
.boards-left-menu
@@ -77,26 +75,11 @@ template(name="boardList")
.boards-right-grid
.boards-path-header
.path-left
span.path-icon.emoji-icon {{currentMenuPath.icon}}
span.path-text {{currentMenuPath.text}}
if BoardMultiSelection.isActive
span.multiselection-hint
span.emoji-icon
i.fa.fa-thumb-tack
| {{_ 'multi-selection-active'}}
span.path-title
span.path-icon.emoji-icon {{currentMenuPath.icon}}
span.path-text {{currentMenuPath.text}}
.path-right
.board-search
span.emoji-icon
i.fa.fa-search
input.js-board-search-input(type="text" placeholder="{{_ 'search-boards'}}" value="{{boardSearch}}" autocomplete="off")
if boardSearch
a.board-search-clear.js-board-search-clear(title="{{_ 'filter-clear'}}")
span.emoji-icon
i.fa.fa-times
a.board-header-btn.js-open-boards-sort(title="{{_ 'sort-boards'}}" class="{{#unless isBoardsSort 'custom'}}emphasis{{/unless}}")
span.emoji-icon
i.fa.fa-sort-alpha-asc
span {{_ 'sort'}}
//- Order (left -> right): Multi-Selection, Sort, Search boards.
if canModifyBoards
if hasBoardsSelected
button.js-archive-selected-boards.board-header-btn
@@ -107,13 +90,41 @@ template(name="boardList")
span.emoji-icon
i.fa.fa-clipboard
span {{_ 'duplicate-board'}}
a.board-header-btn.js-multiselection-activate(
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
span.emoji-icon
//- Multi-Selection button, with the "click checkboxes" hint stacked
directly BELOW it (own column) instead of beside the path title.
.multiselection-group
a.board-header-btn.js-multiselection-activate(
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
i.fa.fa-check-square-o
if BoardMultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
span {{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}
if BoardMultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
if BoardMultiSelection.isActive
span.multiselection-hint
span.emoji-icon
i.fa.fa-thumb-tack
| {{_ 'multi-selection-active'}}
//- "Selected:" actions on the multi-selected boards: star them,
or set one as the Home board (opened after login, #2220).
.selected-actions
span.selected-label {{_ 'selected-label'}}
a.selected-action.js-star-selected(title="{{_ 'set-selected-starred'}}")
i.fa.fa-star
a.selected-action.js-home-selected(title="{{_ 'set-selected-home'}}")
i.fa.fa-home
a.board-header-btn.js-open-boards-sort(title="{{_ 'sort-boards'}}" class="{{#unless isBoardsSort 'custom'}}emphasis{{/unless}}")
span.emoji-icon
i.fa.fa-sort-alpha-asc
span {{_ 'sort'}}
//- Search boards is placed to the RIGHT of the Multi-Selection button.
.board-search
span.emoji-icon
i.fa.fa-search
input.js-board-search-input(type="text" placeholder="{{_ 'search-boards'}}" value="{{boardSearch}}" autocomplete="off")
if boardSearch
a.board-search-clear.js-board-search-clear(title="{{_ 'filter-clear'}}")
span.emoji-icon
i.fa.fa-times
if boardsPaginationActive
@@ -126,7 +137,7 @@ template(name="boardList")
ul.board-list.clearfix.js-boards.dragscroll(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
li.js-add-board
if isSelectedMenu 'templates'
a.board-list-item.label(title="{{_ 'add-template-container'}}")
a.board-list-item.label.is-template-container(title="{{_ 'add-template-container'}}")
span.emoji-icon
i.fa.fa-plus
| &nbsp;{{_ 'add-template-container'}}
@@ -188,6 +199,11 @@ template(name="boardList")
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
//- #2220: the Home board (opened after login) is badged with a
home icon at the top centre of its tile.
if isDefaultBoard
span.board-home-badge(title="{{_ 'home-board-badge'}}")
i.fa.fa-home
span.board-handle.nodragscroll(title="{{boardWorkspaceDragHint}}")
span.emoji-icon
i.fa.fa-arrows
@@ -228,6 +244,8 @@ template(name="boardList")
title="{{_ 'star-board-title'}}")
span.emoji-icon
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
//- #2220 Home-board toggle is disabled: the icon and the
//- set-default-board action are intentionally not rendered.
template(name="boardsSortPopup")
ul.pop-over-list
+56 -7
View File
@@ -542,6 +542,21 @@ Template.boardList.helpers({
const boards = ReactiveCache.getBoards(query, {});
const currentUser = ReactiveCache.getCurrentUser();
// #2220: the Home board (opened after login) always appears FIRST in the
// Starred view, even when it has not been explicitly starred.
const withHomeFirst = (arr) => {
if (tpl.selectedMenu.get() !== 'starred') return arr;
if ((tpl.boardSearchVar.get() || '').trim()) return arr;
const homeId =
currentUser && typeof currentUser.getDefaultBoardId === 'function'
? currentUser.getDefaultBoardId()
: null;
if (!homeId) return arr;
const homeBoard = ReactiveCache.getBoard(homeId);
if (!homeBoard || homeBoard.archived) return arr;
return [homeBoard, ...arr.filter((b) => b && b._id !== homeId)];
};
// #5799: in a sorted (non-custom) mode the server already computed the
// current page (filtered by menu/search and sorted), so render exactly that
// ordered page of board icons. Custom (manual drag order) falls through to
@@ -552,9 +567,11 @@ Template.boardList.helpers({
: 'custom';
if (sortMode !== 'custom') {
const paged = tpl.pagedBoardsVar.get();
return (paged.ids || [])
.map((id) => ReactiveCache.getBoard(id))
.filter(Boolean);
return withHomeFirst(
(paged.ids || [])
.map((id) => ReactiveCache.getBoard(id))
.filter(Boolean),
);
}
let list = boards;
@@ -591,11 +608,11 @@ Template.boardList.helpers({
}
if (currentUser && typeof currentUser.sortBoardsForUser === 'function') {
return currentUser.sortBoardsForUser(list);
return withHomeFirst(currentUser.sortBoardsForUser(list));
}
return list
.slice()
.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
return withHomeFirst(
list.slice().sort((a, b) => (a.title || '').localeCompare(b.title || '')),
);
},
// #5174 / #4825: the board tiles' per-list card-count line and member avatar
// row. Data comes from the one-shot getAllBoardsTileData method fetch in
@@ -629,6 +646,11 @@ Template.boardList.helpers({
const user = ReactiveCache.getCurrentUser();
return user && user.hasStarred(this._id);
},
// #2220: is this the user's Home board (the one opened after login)?
isDefaultBoard() {
const user = ReactiveCache.getCurrentUser();
return user && user.isDefaultBoard(this._id);
},
isAdministrable() {
const user = ReactiveCache.getCurrentUser();
return user && user.isBoardAdmin(this._id);
@@ -916,6 +938,29 @@ Template.boardList.events({
Meteor.call('toggleBoardStar', boardId);
}
},
// "Selected:" star action: star every multi-selected board that isn't yet
// starred (so it becomes Starred, never toggled back off by this button).
'click .js-star-selected'(evt) {
evt.preventDefault();
evt.stopPropagation();
const user = ReactiveCache.getCurrentUser();
BoardMultiSelection.getSelectedBoardIds().forEach((id) => {
if (user && !user.hasStarred(id)) {
Meteor.call('toggleBoardStar', id);
}
});
},
// #2220 "Selected:" home action: set the first selected board as the Home
// board (opened after login). Clicking it again when it is already Home clears
// it (toggle), matching toggleDefaultBoard.
'click .js-home-selected'(evt) {
evt.preventDefault();
evt.stopPropagation();
const ids = BoardMultiSelection.getSelectedBoardIds();
if (ids.length) {
Meteor.call('toggleDefaultBoard', ids[0]);
}
},
// HTML5 DnD from boards to spaces
// #5850: drag a (template) board onto an Org/Team/Domain target to share it.
'dragover .js-share-target'(evt) {
@@ -1103,6 +1148,10 @@ Template.boardList.events({
},
'click .js-multiselection-reset'(evt) {
evt.preventDefault();
// The reset X is now nested inside the activate button (matching the Swimlanes
// view), so stop the click from bubbling to js-multiselection-activate — which
// would otherwise immediately re-activate what we just disabled.
evt.stopPropagation();
BoardMultiSelection.disable();
},
'click .js-toggle-board-multi-selection'(evt) {
+3 -3
View File
@@ -138,7 +138,7 @@
body.desktop-mode .card-details:not(.card-details-popup) {
position: fixed;
width: auto;
max-width: 800px;
max-width: 520px; /* #6465: narrower dock (was 800px, ~1/4 screen) */
flex-basis: auto;
border-radius: 3px;
z-index: 2001;
@@ -445,8 +445,8 @@ body.desktop-mode .card-details.card-details-collapsed {
@media screen and (min-width: 801px) {
.card-details {
top: 97px;
inset-inline-start: calc(50% - (600px / 2));
width: 600px;
inset-inline-start: calc(50% - (520px / 2)); /* #6465: narrower card (was 600px) */
width: 520px;
bottom: 0;
position: fixed;
resize: both;
+10 -6
View File
@@ -1418,7 +1418,10 @@ Template.inlinedCardDescription.events({
tpl.isOpen.set(true);
},
'keydown form textarea'(evt, tpl) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
// Ctrl/Cmd+Enter submits by default; plain Enter when the user enabled
// "submit on Enter" in Member Settings (Shift+Enter then makes a newline).
const submitOnEnter = !!ReactiveCache.getCurrentUser()?.hasSubmitOnEnter?.();
if (isSubmitKey(evt, { submitOnEnter })) {
tpl.find('button[type=submit]').click();
}
},
@@ -1529,11 +1532,12 @@ Template.editCardTitleForm.events({
Utils.showCopied(promise, $tooltip);
},
'keydown .js-edit-card-title'(event) {
// #4236: match the card description field — Ctrl/Cmd+Enter saves; plain
// Enter (and Shift+Enter) insert a new line instead of submitting. The old
// handler submitted on any Enter without Shift, so Enter could not add a
// newline in the title.
if (isSubmitKey(event)) {
// #4236: by default Ctrl/Cmd+Enter saves; plain Enter (and Shift+Enter)
// insert a new line. A user who turns on "submit on Enter" in Member
// Settings gets plain Enter to save (Shift+Enter for a newline) instead —
// the pre-#4236 fast title-editing workflow (#6480 email feedback).
const submitOnEnter = !!ReactiveCache.getCurrentUser()?.hasSubmitOnEnter?.();
if (isSubmitKey(event, { submitOnEnter })) {
$('.js-submit-edit-card-title-form').click();
}
},
+57
View File
@@ -1,3 +1,60 @@
/* #4759: Member Settings / Font popup — a button per installed font (each shown in
its own font) and a row of size buttons (smaller ... 100% ... larger). Clicking a
button applies immediately. */
.ui-font-buttons,
.ui-font-size-buttons {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin: 2px 0 10px 0;
}
.ui-font-btn,
.ui-font-size-btn {
display: inline-block;
padding: 4px 10px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
color: #333;
cursor: pointer;
font-size: 14px;
line-height: 1.4;
text-decoration: none;
}
.ui-font-btn:hover,
.ui-font-size-btn:hover {
background: #f0f0f0;
}
.ui-font-btn.active,
.ui-font-size-btn.active {
background: #2980b9;
border-color: #2980b9;
color: #fff;
}
/* The "Unset" colour reset links look like buttons. */
.color-wheel-wrap .js-reset-text-color,
.color-wheel-wrap .js-reset-bg-color {
display: inline-block;
padding: 4px 10px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
color: #333;
cursor: pointer;
font-size: 14px;
line-height: 1.4;
text-decoration: none;
}
.color-wheel-wrap .js-reset-text-color:hover,
.color-wheel-wrap .js-reset-bg-color:hover {
background: #f0f0f0;
}
.color-wheel-wrap .js-reset-text-color.disabled,
.color-wheel-wrap .js-reset-bg-color.disabled {
opacity: 0.5;
pointer-events: none;
}
/* #5514: native color-wheel (custom RGB / hex) picker wrapper, shared by the
label / list / swimlane / card color popups. */
.color-wheel-wrap {
+6
View File
@@ -203,6 +203,12 @@
.minicard .minicard-title .viewer {
display: block;
word-wrap: break-word;
/* #6465: the minicard title renders inside a .viewer, which inherits the
global `.viewer { min-height: 2.5vh }` (layouts.css). On tall screens that
reserves a big empty band under a one-line title, so half of every
minicard was white space. Collapse it to fit the text (same fix already
applied to the label .viewer above). */
min-height: 0;
}
}
.minicard .dates {
+22 -2
View File
@@ -394,6 +394,7 @@ Template.list.onRendered(function () {
// the old code compared against a scrollLeftMax of 0 and never fired, so
// dragging a card toward an off-screen list never auto-scrolled the board.
const { computeEdgeScroll, findLaneUnderPointer } = require('/imports/lib/boardAutoScroll');
let scrolled = false;
const lanes = document.querySelectorAll('.js-lists');
const rects = Array.prototype.map.call(lanes, el => el.getBoundingClientRect());
const laneIndex = findLaneUnderPointer(rects, event.clientX, event.clientY);
@@ -406,7 +407,10 @@ Template.list.onRendered(function () {
scrollSize: lanes[laneIndex].scrollWidth,
clientSize: lanes[laneIndex].clientWidth,
});
if (nextLeft !== null) lanes[laneIndex].scrollLeft = nextLeft;
if (nextLeft !== null) {
lanes[laneIndex].scrollLeft = nextLeft;
scrolled = true;
}
}
// vertical auto-scroll stays on the board canvas
const canvas = document.querySelector('.board-canvas');
@@ -420,7 +424,23 @@ Template.list.onRendered(function () {
scrollSize: canvas.scrollHeight,
clientSize: canvas.clientHeight,
});
if (nextTop !== null) canvas.scrollTop = nextTop;
if (nextTop !== null) {
canvas.scrollTop = nextTop;
scrolled = true;
}
}
// #6477: after an auto-scroll, jQuery UI sortable's cached container/item
// geometry is STALE — it snapshots positions at drag start and on its own
// placeholder moves, never on our manual scroll here. With long/overflowing
// lists (exactly what triggers auto-scroll) the drop then resolved against a
// stale map and parked the placeholder in the WRONG swimlane's copy of the
// list, so a card dragged between swimlanes snapped back into its source
// swimlane (the stop handler's ui.item.parents('.swimlane') faithfully
// persisted it). Re-cache positions so the placeholder and drop resolve
// against the scrolled layout. Complements the DOM-mutation refresh in
// listBody.js (#2769), which does not fire on scroll.
if (scrolled) {
$cards.sortable('refreshPositions');
}
},
});
+4
View File
@@ -101,6 +101,10 @@ template(name="listHeader")
i.fa.fa-plus
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
i.fa.fa-bars
//- #6465: far-right add-list button. Opens the inline Add List
//- composer AFTER this list (right in LTR / left in RTL).
a.js-add-list-here.list-header-plus-top(title="{{_ 'add-list'}}")
i.fa.fa-plus-square-o
template(name="editListTitleForm")
.list-composer
+18 -2
View File
@@ -208,6 +208,20 @@ Template.listHeader.events({
Utils.setListCollapseState(list, !status);
},
'click .js-open-list-menu': Popup.open('listAction'),
// #6465: open the inline Add List composer after this list. Record the
// swimlane it is displayed in (board-wide lists are not bound to one) so the
// new list is created in the right swimlane; the composer renders after this
// list in the DOM (right in LTR / left in RTL).
'click .js-add-list-here'(event) {
event.preventDefault();
const list = Template.currentData();
const swimlaneDom = $(event.currentTarget).closest('.js-swimlane');
const swimlaneId = swimlaneDom.length
? swimlaneDom.attr('id').replace('swimlane-', '')
: null;
Session.set('wekan-add-list-swimlane', swimlaneId);
Session.set('wekan-add-list-after', list._id);
},
'click .js-add-card.list-header-plus-top'(event) {
const listDom = $(event.target).parents(
`#js-list-${Template.currentData()._id}`,
@@ -373,8 +387,10 @@ Template.listMorePopup.events({
],
}).length === 0
) {
allCardIds.map(_id => Cards.remove(_id));
Lists.remove(list._id);
// Soft delete (docs/Features/Undo/Undo.md): mark the list + its cards deleted
// (server cascades) instead of destroying them, so the delete is restorable
// and undoable (#1023). Replaces the old hard Cards.remove / Lists.remove.
Meteor.call('lists.softRemove', list._id);
} else {
const message = `${TAPi18n.__(
'delete-linked-cards-before-this-list',
+49
View File
@@ -0,0 +1,49 @@
/* Custom theme colors (docs/Theme/Theme.md) — the flat/clear categories let the
* user pick their own colors, applied as CSS variables on :root by
* globalThemeColor.js and consumed here. This is the initial, highest-impact
* surface (the header bars, primary buttons, sidebar button); deeper theming can
* grow from here. dark/special themes never set these variables. */
/* Flat: a single accent recolors the header bars and primary controls. The class
* lives on <body>; #header / #header-quick-access are descendants. */
.has-custom-theme-color #header,
.has-custom-theme-color #header-quick-access {
background: var(--theme-accent) !important;
}
.has-custom-theme-color button[type=submit].primary,
.has-custom-theme-color input[type=submit].primary,
.has-custom-theme-color .sidebar .sidebar-content .sidebar-btn {
background-color: var(--theme-accent) !important;
}
/* Clear: a two-color slide (gradient) across the header bars. */
.has-custom-theme-slide #header,
.has-custom-theme-slide #header-quick-access {
background: linear-gradient(135deg, var(--theme-accent), var(--theme-accent-2)) !important;
}
/* Select Color picker: category groups with a visible name above each group of
* colour swatches (docs/Theme/Theme.md). */
.theme-color-picker .theme-category-group {
margin-bottom: 8px;
clear: both; /* start below the previous group's floated swatches */
}
.theme-color-picker .theme-category-label {
display: block;
width: 100%;
clear: both; /* drop below the previous group's floats... */
text-align: left; /* ...so Flat/Clear/Dark/Special all sit at the left */
font-weight: bold;
margin: 2px 0 4px;
text-transform: capitalize;
}
/* No empty band above the first (Flat) title. */
.theme-color-picker .theme-category-group:first-child .theme-category-label {
margin-top: 0;
}
/* Tighten the Default (no override) row above the groups (Member Settings). */
.theme-color-picker ul.pop-over-list {
margin: 0 0 4px;
padding: 0;
}
+3 -11
View File
@@ -88,17 +88,9 @@
cursor: pointer;
transition: background-color 0.2s;
}
.board-color-belize.js-new-search {
background-color: #216694 !important;
color: #fff !important;
border: none !important;
}
.board-color-belize.js-new-search:hover {
background-color: #1a4d6a !important;
}
.board-color-belize.js-new-search:active {
background-color: #0d334d !important;
}
/* The new-search button uses the .primary button style, so it follows the theme
override colour (default blue #005377, or the board/global theme). The old
hardcoded belize (#216694) rules were removed. */
.global-search-operator {
font-family: Courier;
}
+2 -2
View File
@@ -71,8 +71,8 @@ template(name="globalSearch")
aria-label="{{_ 'globalSearch-title'}}"
autofocus dir="auto"
)
button.js-new-search.board-color-belize(type="button" aria-label="{{_ 'globalSearch-title'}}" style="background: #216694 !important; background-color: #216694 !important; color: #fff !important; border: none !important;")
span(aria-hidden="true") 🧹
button.primary.js-new-search(type="submit" aria-label="{{_ 'trello-clear-job'}}")
| {{_ 'trello-clear-job'}}
if searching
+spinner
else if hasResults
@@ -0,0 +1,74 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { Session } from 'meteor/session';
import { ReactiveCache } from '/imports/reactiveCache';
// #5778 + docs/Theme/Theme.md: apply the user's optional GLOBAL theme color to the
// whole UI (All Boards, Search, Admin Panel, My Cards, etc.) via a `board-color-<name>`
// class on <body>, and apply CUSTOM colors (flat = 1 accent, clear = 2 = a gradient)
// as CSS variables consumed by customTheme.css.
//
// While viewing a single board, that board's own color owns the page, so we do NOT
// put the global color class on <body> there; but we DO expose the ACTIVE context's
// custom colors (the board's when on a board, the user's global override otherwise)
// so custom-colored boards and the custom global theme both render.
Meteor.startup(() => {
let appliedClass = null;
function applyClass(colorClass) {
if (colorClass === appliedClass) return;
try {
if (appliedClass) document.body.classList.remove(appliedClass);
if (colorClass) document.body.classList.add(colorClass);
} catch (_) {
// document.body may not exist yet in exotic embeddings; ignore.
}
appliedClass = colorClass;
}
function applyCustom(custom) {
try {
const root = document.documentElement;
const c1 = custom && custom[0];
const c2 = custom && custom[1];
if (c1) {
root.style.setProperty('--theme-accent', c1);
document.body.classList.add('has-custom-theme-color');
} else {
root.style.removeProperty('--theme-accent');
document.body.classList.remove('has-custom-theme-color');
}
if (c2) {
root.style.setProperty('--theme-accent-2', c2);
document.body.classList.add('has-custom-theme-slide');
} else {
root.style.removeProperty('--theme-accent-2');
document.body.classList.remove('has-custom-theme-slide');
}
} catch (_) {
// ignore
}
}
Tracker.autorun(() => {
const user = Meteor.user();
const globalColor = user && user.profile && user.profile.globalThemeColor;
if (globalColor) {
// #5778: a global override wins EVERYWHERE — including board pages. The header
// and .board-wrapper also prefer it (see header.jade / boardBody.jade), so the
// whole UI takes the chosen theme; unset it to return to per-board colors.
applyClass(`board-color-${globalColor}`);
applyCustom((user && user.profile && user.profile.globalThemeCustomColors) || []);
return;
}
// No global override: on a board expose that board's own custom colors (its color
// class already lives on .board-wrapper/#header); off a board, apply nothing.
const boardId = Session.get('currentBoard');
const board = boardId ? ReactiveCache.getBoard(boardId) : null;
applyClass(null);
applyCustom((board && board.customThemeColors) || []);
});
});
+26 -7
View File
@@ -5,8 +5,16 @@
z-index: 17;
}
#header #header-main-bar {
height: 40px;
padding: 7px 10px 0;
/* Flex row (was float-based) so the right button group — which ends with the
sidebar hamburger — is always pinned to the right edge at every window width,
instead of drifting to the middle as floats wrap. */
display: flex;
align-items: center;
flex-wrap: wrap;
min-height: 40px;
/* No right padding: the right gap is owned by the hamburger's margin so it can
match the ~8px gap to the right of the top-bar user name. (top right bottom left) */
padding: 7px 0 0 10px;
}
#header #header-main-bar h1 {
font-size: 20px;
@@ -39,15 +47,17 @@
opacity: 0.9;
}
#header #header-main-bar .board-header-btns {
display: block;
/* Flex so the buttons inside each group lay out (their float is ignored inside a
flex container) and the groups themselves are flex items of the header bar. */
display: flex;
align-items: center;
flex-wrap: wrap;
margin-top: 3px;
width: auto;
}
#header #header-main-bar .board-header-btns.left {
float: inline-start;
}
#header #header-main-bar .board-header-btns.right {
float: inline-end;
/* Always hug the right edge (and, if the row wraps, the right of its row). */
margin-inline-start: auto;
}
#header #header-main-bar .board-header-btn {
border-radius: 3px;
@@ -60,6 +70,15 @@
line-height: 28px;
margin: 0 12px;
}
/* The sidebar hamburger sits at the far right; drop its wide 12px side margins so
there is no button-sized empty space to its left or right. The cog before it also
drops its right margin so the two sit snugly together at the edge. */
#header #header-main-bar .board-header-btn.js-toggle-sidebar {
margin-inline-start: 2px;
/* 8px to the right edge — the same small gap as to the right of the user name in
the top bar (.header-user-bar-name margin-inline-end). */
margin-inline-end: 8px;
}
#header #header-main-bar .board-header-btn i.fa {
float: inline-start;
display: block;
+14 -13
View File
@@ -5,7 +5,7 @@ template(name="header")
Reddit "subreddit" bar.
The first link goes to the boards page.
if currentUser
#header-quick-access(class=currentBoard.colorClass role="navigation" aria-label="{{_ 'all-boards'}}")
#header-quick-access(class="{{#if currentUser.globalThemeColorClass}}{{currentUser.globalThemeColorClass}}{{else}}{{currentBoard.colorClass}}{{/if}}" role="navigation" aria-label="{{_ 'all-boards'}}")
// Home icon - always at left side of logo
span.home-icon.allBoards
a(href="{{pathFor 'home'}}")
@@ -24,11 +24,13 @@ template(name="header")
div#headerIsSettingDatabaseCallDone
img(src="{{pathFor '/logo-header.png'}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
// Zoom controls - always visible
.zoom-controls
span.zoom-level.js-zoom-level-click(title="{{_ 'click-to-change-zoom'}}")
span.zoom-display {{zoomLevel}}%
input.zoom-input.js-zoom-input(type="number" value=zoomLevel min="50" max="300" step="10" style="display: none;")
// Mobile/desktop mode toggle (swapped with zoom)
.mobile-mode-toggle
a.board-header-btn.js-mobile-mode-toggle(title="{{_ 'mobile-desktop-toggle'}}" class="{{#if mobileMode}}mobile-active{{else}}desktop-active{{/if}}")
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}")
i.fa.fa-mobile
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}")
i.fa.fa-desktop
// Drag handles toggle - between zoom and mobile mode toggle
a.board-header-btn.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}")
@@ -78,12 +80,11 @@ template(name="header")
//
i.fa.fa-plus(title="Create a new board")
.mobile-mode-toggle
a.board-header-btn.js-mobile-mode-toggle(title="{{_ 'mobile-desktop-toggle'}}" class="{{#if mobileMode}}mobile-active{{else}}desktop-active{{/if}}")
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}")
i.fa.fa-mobile
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}")
i.fa.fa-desktop
// Zoom controls (swapped with the mobile/desktop toggle)
.zoom-controls
span.zoom-level.js-zoom-level-click(title="{{_ 'click-to-change-zoom'}}")
span.zoom-display {{zoomLevel}}%
input.zoom-input.js-zoom-input(type="number" value=zoomLevel min="50" max="300" step="10" style="display: none;")
// Notifications
+notifications
@@ -95,7 +96,7 @@ template(name="header")
+headerUserBar
#header(class=currentBoard.colorClass)
#header(class="{{#if currentUser.globalThemeColorClass}}{{currentUser.globalThemeColorClass}}{{else}}{{currentBoard.colorClass}}{{/if}}")
//-
The main bar is a colorful bar that provide all the meta-data for the
current page. This bar is contextual based.
+21
View File
@@ -272,6 +272,27 @@ body.mobile-mode #content {
float: inline-end;
font-size: clamp(18px, 4vw, 24px);
}
/* Archived boards modal: full-width panel that extends to the bottom of the browser
window (so archived boards show full width with maximum vertical space). It is the
only modal that contains .archived-lists. */
#modal .modal-content:has(.archived-lists) {
width: 96vw;
max-width: 96vw;
/* The header bar(s) sit above the modal (higher z-index), so start the archive
panel BELOW them — otherwise its top (and the close X) is hidden under the top
header bar. ~96px clears both the quick-access bar and the board header bar; on
the All Boards page (one bar) this leaves a small top gap. Then extend to the
bottom of the browser window. */
margin: 96px auto 0;
min-height: calc(100vh - 106px);
box-sizing: border-box;
}
/* Each archived board is a full-width row. */
#modal .modal-content .archived-lists,
#modal .modal-content .archived-lists .archived-lists-item {
width: 100%;
box-sizing: border-box;
}
/* #6465: heading sizes back to the 6.09 values (22/18/16px) — the vw clamps
resolved to 24/20/18px on desktop and compounded the density regression. */
h1 {
+10 -2
View File
@@ -171,6 +171,12 @@
overflow: visible;
height: auto;
flex: 1;
/* Auto-filling columns so as many languages as possible are visible at once when
there is browser width; collapses to a single column on narrow windows. */
display: grid;
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
align-content: start;
column-gap: 4px;
}
/* Ensure content div in language popup contains all items */
@@ -197,8 +203,10 @@
top: auto !important;
inset-inline-start: auto !important;
inset-inline-end: 20px !important;
width: auto !important;
max-width: 450px !important;
/* Wide enough for multiple language columns when the browser is wide, but never
wider than ~90% of the viewport (falls back to a single narrow column). */
width: min(90vw, 1100px) !important;
max-width: 90vw !important;
height: 100vh !important;
max-height: 100vh !important;
min-height: 300px !important;
+7 -1
View File
@@ -9,7 +9,13 @@ Popup.template.events({
Popup.close();
},
'click .js-confirm'() {
this.__afterConfirmAction.call(this);
// #6479: the action is stored on the Popup instance (see Popup.afterConfirm),
// not on this data context, so a re-rendered/immutable context can't lose it.
// Fall back to the legacy per-context field for any external caller.
const action = Popup._afterConfirmAction || this.__afterConfirmAction;
if (typeof action === 'function') {
action.call(this);
}
},
// #5942: On mobile/touch, tapping inside the card-detail sub-popups (assign
// user / set due date) made the popup DISAPPEAR. The document-level
@@ -0,0 +1,32 @@
//- Shared "Select Color" picker (docs/Theme/Theme.md), used by both Board Settings
and Member Settings. Colors are shown as VISIBLE swatches grouped by category, with
the category name as a header above each group; the native color wheel(s) appear
for the flat (1) / clear (2) categories once such a color is selected. `data.scope`
is 'board' or 'global'.
template(name="themeColorPicker")
form.theme-color-picker.edit-label
if isGlobal
ul.pop-over-list
li(class="{{#if isNoneSelected}}active{{/if}}")
a.js-theme-none
i.fa.fa-ban
| {{_ 'theme-default'}}
if isNoneSelected
i.fa.fa-check
each themeGroups
.theme-category-group
.theme-category-label {{label}}
.board-backgrounds-list.clearfix
each colors
.board-background-select.js-select-theme(data-color="{{name}}")
span.background-box(class="board-color-{{name}}")
span {{name}}
if selected
i.fa.fa-check
if showCustom
.color-wheel-wrap
label {{_ 'custom-color'}}
each customWheels
input.js-theme-wheel(type="color" data-index="{{index}}" value="{{value}}" title="{{_ 'custom-color'}}")
.theme-preview
span.background-box(class="{{previewClass}}" style="{{previewStyle}}")
+166
View File
@@ -0,0 +1,166 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { ReactiveVar } from 'meteor/reactive-var';
import { TAPi18n } from '/imports/i18n';
import { Utils } from '/client/lib/utils';
import { isHexColor, toHex } from '/models/lib/contrastColor';
import { BOARD_COLORS } from '/models/metadata/colors';
import {
THEME_CATEGORY_ORDER,
categoryOf,
colorsInCategory,
allowsCustomColor,
customColorCount,
} from '/models/lib/themeCategories';
// Shared "Select Color" picker (docs/Theme/Theme.md): VISIBLE color swatches grouped
// by category (with the category name above each group) + native color wheel(s) for
// the flat/clear categories once such a color is selected. data.scope is 'board'
// (writes board.color) or 'global' (writes the user's #5778 global override).
const DEFAULT_WHEEL = ['#2980b9', '#6dd5fa']; // stock flat accent / second slide stop
function readCurrent(scope) {
if (scope === 'board') {
const b = Utils.getCurrentBoard();
return { color: (b && b.color) || BOARD_COLORS[0], custom: (b && b.customThemeColors) || [] };
}
const u = ReactiveCache.getCurrentUser();
return {
color: (u && u.getGlobalThemeColor && u.getGlobalThemeColor()) || null,
custom: (u && u.getGlobalThemeCustomColors && u.getGlobalThemeCustomColors()) || [],
};
}
Template.themeColorPicker.onCreated(function () {
const scope = this.data && this.data.scope === 'board' ? 'board' : 'global';
const cur = readCurrent(scope);
this.scope = scope;
this.color = new ReactiveVar(cur.color); // null for global = no override
this.customColors = new ReactiveVar((cur.custom || []).slice());
});
Template.themeColorPicker.helpers({
isGlobal() {
return Template.instance().scope === 'global';
},
isNoneSelected() {
const tpl = Template.instance();
return tpl.scope === 'global' && !tpl.color.get();
},
// Visible swatches grouped by category, each group labelled with its category name.
themeGroups() {
const cur = Template.instance().color.get();
return THEME_CATEGORY_ORDER.map(key => ({
key,
label: TAPi18n.__(`theme-category-${key}`),
colors: colorsInCategory(key).map(name => ({ name, selected: name === cur })),
}));
},
showCustom() {
const cur = Template.instance().color.get();
return cur ? allowsCustomColor(categoryOf(cur)) : false;
},
customWheels() {
const tpl = Template.instance();
const cur = tpl.color.get();
const n = cur ? customColorCount(categoryOf(cur)) : 0;
const cc = tpl.customColors.get();
const wheels = [];
for (let i = 0; i < n; i += 1) {
wheels.push({ index: i, value: toHex(cc[i]) || DEFAULT_WHEEL[i] || '#0079bf' });
}
return wheels;
},
previewClass() {
const color = Template.instance().color.get();
return color ? `board-color-${color}` : '';
},
// Inline preview of custom colors (the stock board-color-* CSS can't show arbitrary
// colors until the CSS-variable refactor; the preview swatch shows them directly).
previewStyle() {
const tpl = Template.instance();
const cur = tpl.color.get();
const cat = cur ? categoryOf(cur) : null;
if (!cat || !allowsCustomColor(cat)) return '';
const cc = tpl.customColors.get();
if (!cc.some(Boolean)) return '';
if (customColorCount(cat) === 2 && cc[0] && cc[1]) {
return `background: linear-gradient(135deg, ${cc[0]}, ${cc[1]}) !important;`;
}
if (cc[0]) return `background: ${cc[0]} !important;`;
return '';
},
});
// Collect the custom colors to save: [] unless the user actually engaged a wheel, in
// which case read every wheel's current value (untouched ones contribute their shown
// default) so the result is a complete set of the category's expected count.
function gatherCustom(tpl) {
const color = tpl.color.get();
const cat = color ? categoryOf(color) : null;
if (!cat || !allowsCustomColor(cat)) return [];
if (!tpl.customColors.get().some(Boolean)) return [];
const n = customColorCount(cat);
const wheels = Array.from(tpl.findAll('.js-theme-wheel'))
.map(w => w.value)
.filter(isHexColor)
.slice(0, n);
return wheels.length === n ? wheels : [];
}
// Apply the current selection immediately (no Save button): board scope writes
// board.color, global scope calls setGlobalThemeColor. `color`/`custom` are read
// from the reactive state so the same helper serves the swatch click and the wheel.
function applySelection(tpl) {
const color = tpl.color.get() || colorsInCategory(THEME_CATEGORY_ORDER[0])[0];
const custom = gatherCustom(tpl);
if (tpl.scope === 'board') {
const b = Utils.getCurrentBoard();
if (b) {
Promise.resolve(b.setColor(color, custom)).catch(e => {
if (process.env.DEBUG === 'true') console.error('board setColor error', e);
});
}
} else {
Meteor.call('setGlobalThemeColor', color, custom, err => {
if (err && process.env.DEBUG === 'true') console.error('setGlobalThemeColor error', err);
});
}
}
Template.themeColorPicker.events({
// Clicking a color swatch applies that theme IMMEDIATELY (there is no Save button).
'click .js-select-theme'(event, tpl) {
const color = event.currentTarget.dataset.color;
if (!color) return;
tpl.color.set(color);
tpl.customColors.set([]); // a fresh theme -> its stock colors
applySelection(tpl);
},
// Live-update the preview while dragging the wheel...
'input .js-theme-wheel'(event, tpl) {
const idx = parseInt(event.currentTarget.dataset.index, 10);
const val = event.currentTarget.value;
if (!isHexColor(val)) return;
const cc = tpl.customColors.get().slice();
cc[idx] = val;
tpl.customColors.set(cc);
},
// ...and apply the custom color when the wheel is committed (avoids spamming the
// server on every intermediate value during the drag).
'change .js-theme-wheel'(event, tpl) {
applySelection(tpl);
},
// Clear the global override (Default) — applies immediately too.
'click .js-theme-none'(event, tpl) {
event.preventDefault();
tpl.color.set(null);
tpl.customColors.set([]);
if (tpl.scope === 'board') {
const b = Utils.getCurrentBoard();
if (b) b.setColor(BOARD_COLORS[0], []);
} else {
Meteor.call('setGlobalThemeColor', null, null);
}
},
});
+40
View File
@@ -0,0 +1,40 @@
/* Member Settings / Font (#4759): apply the user's chosen UI font. The class lives
* on <body> and the --wekan-ui-font variable is set by uiFont.js only for a
* validated, whitelisted font. Form controls are listed explicitly because their
* UA default font-family does not inherit. Icon fonts (Font Awesome etc.) set their
* own font-family on the element, so they are unaffected. */
body.has-ui-font,
body.has-ui-font button,
body.has-ui-font input,
body.has-ui-font textarea,
body.has-ui-font select,
body.has-ui-font .wekan-form-control {
font-family: var(--wekan-ui-font) !important;
}
/* Font size preset (#4759): scale the root font size. Elements using relative units
* (em/rem/%) scale with it; the variable is a percentage set by uiFont.js only for a
* known preset. 'default'/unset removes it. */
body.has-ui-font-size {
font-size: var(--wekan-ui-font-size) !important;
}
/* Text color (#4759): the variable is a validated hex set by uiFont.js. Icon fonts
* keep their own color rules; this recolors ordinary UI text + form controls. */
body.has-ui-text-color,
body.has-ui-text-color button,
body.has-ui-text-color input,
body.has-ui-text-color textarea,
body.has-ui-text-color select,
body.has-ui-text-color .wekan-form-control {
color: var(--wekan-ui-text-color) !important;
}
/* Text background color (#4759). */
body.has-ui-bg-color,
body.has-ui-bg-color input,
body.has-ui-bg-color textarea,
body.has-ui-bg-color select,
body.has-ui-bg-color .wekan-form-control {
background-color: var(--wekan-ui-bg-color) !important;
}
+34
View File
@@ -0,0 +1,34 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { fontFamilyValue, fontSizeValue, colorValue } from '/models/lib/uiFonts';
// #4759: apply the user's chosen UI font AND font-size preset to the whole UI by
// setting CSS variables on :root and toggling marker classes on <body> (uiFont.css
// consumes them). fontFamilyValue()/fontSizeValue() return '' for anything not on the
// curated whitelist/presets, so only known, safe values can ever reach the DOM — the
// saved values are also validated server-side. Unset => nothing applied (defaults).
Meteor.startup(() => {
function toggle(varName, className, value) {
try {
if (value) {
document.documentElement.style.setProperty(varName, value);
document.body.classList.add(className);
} else {
document.documentElement.style.removeProperty(varName);
document.body.classList.remove(className);
}
} catch (_) {
// document not ready in exotic embeddings; ignore.
}
}
Tracker.autorun(() => {
const user = Meteor.user();
const profile = (user && user.profile) || {};
toggle('--wekan-ui-font', 'has-ui-font', fontFamilyValue(profile.uiFont));
toggle('--wekan-ui-font-size', 'has-ui-font-size', fontSizeValue(profile.uiFontSize));
toggle('--wekan-ui-text-color', 'has-ui-text-color', colorValue(profile.uiTextColor));
toggle('--wekan-ui-bg-color', 'has-ui-bg-color', colorValue(profile.uiTextBgColor));
});
});
@@ -2,7 +2,8 @@ section#notifications-drawer {
position: fixed;
top: 48px;
inset-inline-end: 0;
width: 400px;
width: 100vw;
max-width: 100vw;
background-color: #fafafa;
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
border-radius: 2px;
@@ -17,19 +18,20 @@ section#notifications-drawer .header {
position: fixed;
top: 48px;
inset-inline-end: 0;
width: calc(400px - 32px);
width: 100vw;
box-sizing: border-box;
padding: 8px 16px;
background: #ededed;
border-bottom: 1px solid #dbdbdb;
z-index: 2;
/* Flex row: hamburger (left), title (centre), X (right), all vertically centred. */
display: flex;
align-items: center;
}
section#notifications-drawer .header .notification-menu-toggle {
position: absolute;
inset-inline-start: 16px;
top: calc(50% - 12px);
font-size: 20px;
cursor: pointer;
color: #333;
color: #000 !important; /* black hamburger menu (override themed white link colour) */
line-height: 24px;
}
section#notifications-drawer .header .notification-menu-toggle:hover {
@@ -83,18 +85,33 @@ section#notifications-drawer .header .notification-menu .menu-item .menu-icon {
margin-inline-end: 8px;
font-size: 16px;
}
/* Menu-item icons (envelope / copy / trash / check) black. */
section#notifications-drawer .header .notification-menu .menu-item .menu-icon,
section#notifications-drawer .header .notification-menu .menu-item .menu-icon i.fa,
section#notifications-drawer .header .notification-menu .menu-item .check-icon,
section#notifications-drawer .header .notification-menu .menu-item .check-icon i.fa {
color: #000 !important;
}
section#notifications-drawer .header h5 {
flex: 1; /* fill the middle so the title is centred, X sits at the right */
text-align: center;
margin: 0;
}
section#notifications-drawer .header .close {
position: absolute;
top: calc(50% - 12px);
inset-inline-end: 12px;
/* Flex item at the right edge, aligned with the title. position: static cancels
the GLOBAL `.close { position: absolute; top: 5px }` rule (boardBody.css) that
otherwise pulls the X out of the header row and drops it below the title. */
position: static;
margin-inline-start: 8px;
font-size: 24px;
height: 24px;
line-height: 24px;
opacity: 1;
color: #000 !important; /* black X (override themed white link colour) */
}
section#notifications-drawer .header .close i.fa,
section#notifications-drawer .header .notification-menu-toggle i.fa {
color: inherit !important;
}
section#notifications-drawer ul.notifications {
+54 -3
View File
@@ -29,6 +29,16 @@
.sidebar .sidebar-content {
padding: 0 12px;
}
/* Sidebar text inputs (Filter List by Title, Filter by card title, advanced
filter, etc.) fill the padded content width, so there is equal empty space on
both sides (the .sidebar-content 12px padding) instead of the browser's
default narrow width that left a large gap on the right. box-sizing keeps the
border inside the width so it never overflows the panel. */
.sidebar .sidebar-content input[type="text"],
.sidebar .sidebar-content input[type="search"] {
width: 100%;
box-sizing: border-box;
}
.sidebar .sidebar-content .hide-btn {
display: none;
}
@@ -191,7 +201,12 @@ body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-
}
.board-sidebar {
display: none;
width: 30vw;
/* Keep a sensible sidebar width at every screen size. A bare 30vw scaled with
the viewport, so on very wide screens it grew to ~double the intended width
while looking right only at mid widths; clamp keeps the mid-width look and
caps it (and floors it) instead of scaling without bound. Phones (<800px)
are handled by the 100vw rule in the media query below. */
width: clamp(320px, 30vw, 560px);
z-index: 100;
transition: top 0.1s, right 0.1s, width 0.1s;
}
@@ -203,6 +218,36 @@ body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-
Restore/Delete links) sits unreachable UNDER the card window. */
z-index: 2002;
}
/* Right sidebar resize handle: a thin grab strip on the LEFT edge of the open
sidebar; drag left/right to change the sidebar width. Desktop only (hidden on
phones by the media query below, where the sidebar is full width). */
.sidebar-resize-handle {
display: none;
}
.board-sidebar.is-open .sidebar-resize-handle {
display: block;
position: absolute;
top: 0;
bottom: 0;
inset-inline-start: -3px;
width: 6px;
cursor: col-resize;
z-index: 2003; /* above the open sidebar (2002) and its content */
background: transparent;
transition: background-color 0.2s ease;
}
.sidebar-resize-handle:hover {
background: rgba(0, 123, 255, 0.4);
box-shadow: 0 0 4px rgba(0, 123, 255, 0.3);
}
.sidebar-resize-handle:active {
background: rgba(0, 123, 255, 0.6);
box-shadow: 0 0 6px rgba(0, 123, 255, 0.4);
}
body.sidebar-resizing-active {
user-select: none;
cursor: col-resize;
}
.board-widget h4 {
margin: 5px 0;
}
@@ -305,10 +350,16 @@ body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-
bottom: 0;
inset-inline-start: 0;
inset-inline-end: 0;
width: 100vw;
max-width: 100vw;
/* !important so a desktop drag-resize inline width never leaks into mobile,
where the sidebar must stay full width. */
width: 100vw !important;
max-width: 100vw !important;
z-index: 1000;
}
/* No dragging to resize on phones — the sidebar is full width there. */
.board-sidebar .sidebar-resize-handle {
display: none;
}
/* Normalise sidebar text on phones so the headings are not oversized and the
small text is not tiny — keep them close to the board's text size so board
and sidebar read at one consistent size. */
+7 -7
View File
@@ -1,5 +1,9 @@
template(name="sidebar")
.board-sidebar.sidebar(class="{{#if isOpen}}is-open{{/if}} {{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}")
//- Drag the sidebar's LEFT edge to resize its width (desktop only; hidden on
mobile where the sidebar is full width). Saved to the user profile (or
localStorage for anonymous users on a public board).
.sidebar-resize-handle.js-sidebar-resize-handle.nodragscroll(title="{{_ 'drag-to-resize-sidebar'}}")
//a.sidebar-tongue.js-toggle-sidebar(
//
class="{{#if isTongueHidden}}is-hidden{{/if}}",
@@ -148,13 +152,9 @@ template(name="boardDomainRow")
text(x="50%" y="11" text-anchor="middle" dominant-baseline="middle" font-size="16")= domain
template(name="boardChangeColorPopup")
.board-backgrounds-list.clearfix
each backgroundColors
.board-background-select.js-select-background
span.background-box(class="board-color-{{this}}")
span {{this}}
if isSelected
i.fa.fa-check
//- docs/Theme/Theme.md: category + theme dropdowns with custom color wheels for
the flat/clear categories, shared with Member Settings / Change Color.
+themeColorPicker(scope="board")
template(name="boardBackgroundsPopup")
.board-backgrounds-manage
+132 -20
View File
@@ -11,7 +11,6 @@ import Attachments from '/models/attachments';
import { generateUniversalAttachmentUrl } from '/models/lib/universalUrlGenerator';
import Integrations from '/models/integrations';
import Lists from '/models/lists';
import { BOARD_COLORS } from '/models/metadata/colors';
import { Filter } from '/client/lib/filter';
import { EscapeActions } from '/client/lib/escapeActions';
import { Utils } from '/client/lib/utils';
@@ -139,7 +138,137 @@ Template.sidebar.onCreated(function() {
};
});
// ── Right sidebar drag-to-resize (desktop only) ──────────────────────────────
// Drag the sidebar's left edge to change its width, like resizing a spreadsheet
// column (mirrors the list resize in client/components/lists/list.js). The width
// is saved to the user profile for logged-in users, or browser localStorage for
// anonymous users on a public board. On phones the sidebar stays full width.
const MIN_SIDEBAR_WIDTH = 280;
const SIDEBAR_WIDTH_STORAGE_KEY = 'wekan-sidebar-width';
const isMobileSidebar = () => window.innerWidth <= 800;
const maxSidebarWidth = () =>
Math.max(MIN_SIDEBAR_WIDTH, Math.min(900, window.innerWidth - 80));
function readSavedSidebarWidth() {
const user = ReactiveCache.getCurrentUser();
if (user) return user.getSidebarWidth();
try {
const v = window.localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY);
return v ? parseInt(v, 10) : undefined;
} catch (e) {
return undefined;
}
}
function saveSidebarWidth(width) {
const user = ReactiveCache.getCurrentUser();
if (user) {
Meteor.call('setSidebarWidth', width, err => {
if (err) console.error('Error saving sidebar width:', err);
});
} else {
try {
window.localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(width));
} catch (e) {
/* localStorage unavailable (private mode) — width just isn't persisted */
}
}
}
function applySavedSidebarWidth($sidebar) {
// On phones the sidebar is full width (a !important CSS rule), so never set an
// inline width there; clear any leftover so shrinking desktop -> mobile works.
if (isMobileSidebar()) {
$sidebar[0].style.removeProperty('width');
return;
}
const w = readSavedSidebarWidth();
if (typeof w === 'number' && w >= MIN_SIDEBAR_WIDTH) {
$sidebar[0].style.width = `${Math.min(w, maxSidebarWidth())}px`;
} else {
$sidebar[0].style.removeProperty('width'); // fall back to the CSS clamp
}
}
Template.sidebar.onRendered(function() {
const tpl = this;
const $sidebar = tpl.$('.board-sidebar');
const $handle = tpl.$('.js-sidebar-resize-handle');
if (!$sidebar.length || !$handle.length) return;
// Apply the saved width when the current user (and thus the saved value)
// resolves, and re-apply on window resize so crossing the mobile breakpoint
// clears/restores the inline width.
tpl.autorun(() => {
ReactiveCache.getCurrentUser(); // reactive dependency on login state
applySavedSidebarWidth($sidebar);
});
const onWinResize = () => applySavedSidebarWidth($sidebar);
window.addEventListener('resize', onWinResize);
let isResizing = false;
let startX = 0;
let startWidth = 0;
let lastWidth = 0;
const getPageX = (e) => {
const oe = e.originalEvent || e;
if (oe.touches && oe.touches.length) return oe.touches[0].pageX;
if (oe.changedTouches && oe.changedTouches.length) return oe.changedTouches[0].pageX;
return e.pageX;
};
// Direction: the sidebar docks to the logical inline-end. In LTR that is the
// right, so its left-edge handle widens the panel when dragged left; RTL puts
// the panel on the left, mirroring the direction.
const isRtl = () =>
(document.documentElement.getAttribute('dir') || document.dir) === 'rtl';
const startResize = (e) => {
if (isMobileSidebar()) return; // no resize on phones
isResizing = true;
startX = getPageX(e);
startWidth = $sidebar.outerWidth();
lastWidth = startWidth;
$('body').addClass('sidebar-resizing-active');
e.preventDefault();
e.stopPropagation();
};
const doResize = (e) => {
if (!isResizing) return;
const delta = (startX - getPageX(e)) * (isRtl() ? -1 : 1);
const w = Math.max(MIN_SIDEBAR_WIDTH, Math.min(maxSidebarWidth(), startWidth + delta));
lastWidth = w;
$sidebar[0].style.width = `${w}px`;
e.preventDefault();
e.stopPropagation();
};
const stopResize = (e) => {
if (!isResizing) return;
isResizing = false;
$('body').removeClass('sidebar-resizing-active');
saveSidebarWidth(Math.round(lastWidth));
if (e && e.preventDefault) e.preventDefault();
};
$handle.on('mousedown', startResize);
$(document).on('mousemove', doResize);
$(document).on('mouseup', stopResize);
// Native touch API so { passive: false } applies and preventDefault works.
$handle[0].addEventListener('touchstart', startResize, { passive: false });
document.addEventListener('touchmove', doResize, { passive: false });
document.addEventListener('touchend', stopResize, { passive: false });
tpl._sidebarResizeCleanup = () => {
window.removeEventListener('resize', onWinResize);
$(document).off('mousemove', doResize);
$(document).off('mouseup', stopResize);
document.removeEventListener('touchmove', doResize);
document.removeEventListener('touchend', stopResize);
};
});
Template.sidebar.onDestroyed(function() {
if (this._sidebarResizeCleanup) this._sidebarResizeCleanup();
clearSidebarInstance(this);
Sidebar = null;
});
@@ -967,25 +1096,8 @@ function draggableMembersLabelsWidgets() {
Template.membersWidget.onRendered(draggableMembersLabelsWidgets);
Template.labelsWidget.onRendered(draggableMembersLabelsWidgets);
Template.boardChangeColorPopup.helpers({
backgroundColors() {
return BOARD_COLORS;
},
isSelected() {
const currentBoard = Utils.getCurrentBoard();
return currentBoard.color === Template.currentData().toString();
},
});
Template.boardChangeColorPopup.events({
async 'click .js-select-background'(evt) {
evt.preventDefault();
evt.stopPropagation();
const currentBoard = Utils.getCurrentBoard();
const newColor = this.toString();
await currentBoard.setColor(newColor);
},
});
// docs/Theme/Theme.md: the board color popup now renders the shared themeColorPicker
// (scope="board"); its logic lives in client/components/main/themeColorPicker.js.
Template.boardChangeBackgroundImagePopup.events({
async submit(event, tpl) {
+8 -1
View File
@@ -67,8 +67,15 @@
padding: 0;
font-weight: bold;
min-height: 33px;
height: auto;
width: 100%;
overflow: hidden;
/* #6465: the gray swimlane header bar looked cut off at non-100% board zoom
(transform: scale on .board-wrapper). `overflow: hidden` clipped the header
to a slightly-too-short box once the scaled layout rounded, so the bar/text
appeared sliced. Let it grow to fit (height:auto + min-height floor) and not
clip vertically. Swimlane names are short, so dropping the ellipsis here is
fine; a long name wraps (word-wrap) instead of being cut. */
overflow: visible;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
word-wrap: break-word;
+62 -8
View File
@@ -15,12 +15,27 @@ template(name="swimlane")
each lists
+miniList(this)
else
if currentUser.isBoardMember
unless currentUser.isCommentOnly
+addListForm
//- #6465: no standing Add List column. Empty swimlane gets the inline
//- composer at the start; otherwise it opens after a list via its header
//- add-list button (see isAddListAfter).
if swimlaneHasNoLists
if currentUser.isBoardMember
unless currentUser.isCommentOnly
//- #6465: an empty swimlane shows only a + button; the create-list
//- form appears only after it is clicked. (Non-empty swimlanes use
//- the per-list-header add-list button instead.)
if isEmptyAddListOpen _id
+addListInline
else
.list.list-composer.js-list-composer.js-empty-add-list-button(class="{{#if isMiniScreen}}mini-list{{/if}}")
.list-header-add
a.open-list-composer.js-open-empty-add-list(title="{{_ 'add-list'}}" data-swimlane="{{_id}}")
i.fa.fa-plus
each lists
if visible this
+list(this)
if isAddListAfter _id
+addListInline
if currentCardIsInThisList _id ../_id
+cardDetails(currentCard)
@@ -38,14 +53,26 @@ template(name="listsGroup")
each lists
+miniList(this)
else
//- Issue #6142: allow adding a list in Lists (board-view-lists) mode too.
if defaultSwimlane
if currentUser.isBoardMember
unless currentUser.isCommentOnly
+addListForm(defaultSwimlane)
//- #6465: Lists view — empty board gets the inline composer at the start;
//- otherwise it opens after a list via that list's header add-list button.
if boardHasNoLists
if defaultSwimlane
if currentUser.isBoardMember
unless currentUser.isCommentOnly
//- #6465: empty board shows only a + button; the create-list form
//- appears only after clicking it.
if isEmptyAddListOpen defaultSwimlane._id
+addListInline(defaultSwimlane)
else
.list.list-composer.js-list-composer.js-empty-add-list-button(class="{{#if isMiniScreen}}mini-list{{/if}}")
.list-header-add
a.open-list-composer.js-open-empty-add-list(title="{{_ 'add-list'}}" data-swimlane="{{defaultSwimlane._id}}")
i.fa.fa-plus
each lists
if visible this
+list(this)
if isAddListAfter _id
+addListInline(defaultSwimlane)
if currentCardIsInThisList _id null
+cardDetails(currentCard)
@@ -77,6 +104,33 @@ template(name="addListForm")
a.open-list-composer.js-open-inlined-form(title="{{_ 'add-list'}}")
i.fa.fa-plus
//- #6465: always-open inline Add List composer, rendered AFTER a list (right in
//- LTR / left in RTL, via the flex-row lists lane) when that list's header
//- add-list button is clicked, or at the start of an empty swimlane/board.
template(name="addListInline")
.list.list-composer.js-list-composer.js-add-list-inline(class="{{#if isMiniScreen}}mini-list{{/if}}")
.list-header-add
form.js-add-list-inline-form
input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
autocomplete="off" autofocus)
//- #6465 follow-up: restore the previous "add after which list"
//- position selector, pre-selecting the list whose header button opened
//- this composer, so the position can still be chosen explicitly.
if swimlaneLists.length
| {{_ 'add-after-list'}}
select.list-position-input.full-line
each swimlaneLists
option(value="{{_id}}" selected="{{#if isDefaultAfterList _id}}selected{{/if}}") {{title}}
.edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'save'}}
a.js-close-add-list-inline(title="{{_ 'close'}}")
i.fa.fa-times-thin
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard
span.quiet
| {{_ 'or'}}
a.js-list-template {{_ 'template'}}
template(name="moveSwimlanePopup")
if currentUser
unless currentUser.isWorker
+162 -2
View File
@@ -6,6 +6,7 @@ import { CSSEvents } from '/client/lib/cssEvents';
import { Filter } from '/client/lib/filter';
import { EscapeActions } from '/client/lib/escapeActions';
import { Utils } from '/client/lib/utils';
import { TAPi18n } from '/imports/i18n';
import { defaultSwimlaneIdForBoard } from '/client/components/lists/listAddHelpers';
const { calculateIndex } = Utils;
@@ -71,11 +72,45 @@ function saveSorting(ui) {
sort: sortIndex.base,
};
// Check if the list was dropped in a different swimlane
const isDifferentSwimlane = targetSwimlaneId && targetSwimlaneId !== originalSwimlaneId;
// Check if the list was dropped in a different swimlane.
//
// #6484: only rebind a list to a swimlane when it was ALREADY swimlane-scoped
// (originalSwimlaneId set) and actually crossed into a DIFFERENT swimlane. A
// board-wide list has swimlaneId === null / getEffectiveSwimlaneId() === null
// and renders under EVERY swimlane (see filterCardsByListAndSwimlane and
// currentListIsInThisSwimlane: a null swimlaneId matches every swimlane). Since
// targetSwimlaneId is ALWAYS the swimlane the list is shown under in swimlanes
// view, the old `targetSwimlaneId !== originalSwimlaneId` was true for ANY nudge
// of a board-wide list, so it set list.swimlaneId AND moved every card to that
// one swimlane — the list then vanished from all the other swimlanes. Requiring
// originalSwimlaneId keeps board-wide lists board-wide; genuinely
// swimlane-scoped lists can still be moved between swimlanes.
const isDifferentSwimlane =
originalSwimlaneId &&
targetSwimlaneId &&
targetSwimlaneId !== originalSwimlaneId;
// If the list was dropped in a different swimlane, update the swimlaneId
if (isDifferentSwimlane) {
// #6478: moving a list to a DIFFERENT swimlane is high-impact — it re-homes
// every card in the list — and is easy to trigger accidentally on touch (the
// reporter merged lists this way and needed ~15 min to recover). Confirm on
// touch/small screens before applying; on decline, revert the drag by
// re-rendering from the server and abort. (Ctrl+Z can also undo it after the
// fact — see userPositionHistory.undoLast.)
if (
(Utils.isMiniScreen() || Utils.isTouchScreenOrShowDesktopDragHandles()) &&
typeof window !== 'undefined' &&
typeof window.confirm === 'function' &&
// eslint-disable-next-line no-alert
!window.confirm(TAPi18n.__('confirm-move-list-to-swimlane'))
) {
Meteor.subscribe('board', list.boardId, false);
$('.js-swimlane').each(function () {
$(this).addClass('dragscroll');
});
return;
}
updateData.swimlaneId = targetSwimlaneId;
// Move all cards in the list to the new swimlane
@@ -679,6 +714,24 @@ Template.swimlane.helpers({
}
return combined;
},
// #6465: the add-list composer is no longer a standing column. It opens AFTER a
// specific list when that list's header add-list button is clicked (the target
// list _id is kept in Session 'wekan-add-list-after'). Rendered after the list
// in the DOM, it appears to the RIGHT of the list in LTR and to the LEFT in RTL
// (the lists lane is a flex row that reverses under dir=rtl).
isAddListAfter(listId) {
return Session.get('wekan-add-list-after') === listId;
},
// Empty swimlane / empty board: there are no list headers to host the button,
// so offer a + button that reveals the inline composer only when clicked.
swimlaneHasNoLists() {
return !this.myLists || this.myLists().length === 0;
},
// #6465: is the empty-state create-list form open for this swimlane? (Set by the
// + button; until then only the button shows.)
isEmptyAddListOpen(swimlaneId) {
return Session.get('wekan-add-list-empty') === swimlaneId;
},
collapseSwimlane() {
return Utils.getSwimlaneCollapseState(this);
},
@@ -742,7 +795,19 @@ Template.swimlane.helpers({
},
});
// #6465: open the empty-state create-list form for a swimlane (shared by the
// Swimlanes and Lists views). Until clicked, an empty swimlane/board shows only the
// + button; this reveals the inline composer, scoped to the clicked swimlane.
function openEmptyAddList(event) {
event.preventDefault();
const swimlaneId = $(event.currentTarget).data('swimlane') || null;
Session.set('wekan-add-list-after', null);
Session.set('wekan-add-list-swimlane', swimlaneId);
Session.set('wekan-add-list-empty', swimlaneId);
}
Template.swimlane.events({
'click .js-open-empty-add-list': openEmptyAddList,
// Click-and-drag action
'mousedown .board-canvas'(evt, tpl) {
const noDragInside = ['a', 'input', 'textarea', 'p'].concat(
@@ -848,6 +913,84 @@ Template.addListForm.events({
'click .js-list-template': Popup.open('searchElement'),
});
// #6465: the inline add-list composer that opens after a specific list (or at the
// start of an empty swimlane). Its data context is the swimlane it creates the
// list in; the target list (for positioning) is Session 'wekan-add-list-after'.
Template.addListInline.onCreated(function () {
this.currentSwimlane = Template.currentData();
});
Template.addListInline.helpers({
// #6465 follow-up: the lists of the swimlane this composer opens in, so the
// "add after which list" position selector (restored) can be shown.
swimlaneLists() {
const swimlaneId =
Session.get('wekan-add-list-swimlane') ||
Template.instance().currentSwimlane?._id;
if (!swimlaneId) return [];
return ReactiveCache.getLists(
{ swimlaneId, archived: false },
{ sort: { sort: 1 } },
);
},
// Pre-select the list whose header add-list button opened this composer.
isDefaultAfterList(listId) {
return Session.get('wekan-add-list-after') === listId;
},
});
Template.addListInline.events({
async submit(evt, tpl) {
evt.preventDefault();
const titleInput = tpl.find('.list-name-input');
const title = titleInput?.value.trim();
if (!title) return;
// Position: honour the explicit "add after list" selector when present
// (its options mirror the swimlane's lists, defaulting to the one whose
// header opened this composer), and derive nextListId from the following
// option so the new list is inserted between them. Fall back to the header
// button's target (Session) for the empty-board/start case.
const positionInput = tpl.find('.list-position-input');
let afterListId;
let nextListId = null;
if (positionInput && positionInput.value) {
afterListId = positionInput.value.trim();
const next = positionInput.options[positionInput.selectedIndex + 1];
nextListId = next ? next.value : null;
} else {
afterListId = Session.get('wekan-add-list-after') || null;
}
try {
await Meteor.callAsync('createListAfter', {
title,
boardId: Session.get('currentBoard'),
// The header button records the swimlane the list is displayed in; the
// empty-swimlane composer falls back to its own swimlane data context.
swimlaneId: Session.get('wekan-add-list-swimlane') || tpl.currentSwimlane?._id,
afterListId,
nextListId,
type: 'list',
});
titleInput.value = '';
titleInput.focus();
// Empty-state flow: the swimlane now has a list, so drop the empty-composer
// flag — if it becomes empty again the + button (not the form) shows first.
Session.set('wekan-add-list-empty', null);
} catch (error) {
console.error('Failed to create list:', error);
}
},
// Restore the "or template" option: create the list from a template.
'click .js-list-template': Popup.open('searchElement'),
'click .js-close-add-list-inline'() {
Session.set('wekan-add-list-after', null);
Session.set('wekan-add-list-swimlane', null);
Session.set('wekan-add-list-empty', null);
},
});
Template.listsGroup.helpers({
// Issue #6142: the add-list composer needs a swimlane as its data context
// (it creates the list in that swimlane). In Lists mode the listsGroup data
@@ -859,6 +1002,18 @@ Template.listsGroup.helpers({
if (!swimlaneId) return null;
return ReactiveCache.getSwimlane({ _id: swimlaneId });
},
// #6465: same add-list-after-a-list behaviour as the swimlanes view.
isAddListAfter(listId) {
return Session.get('wekan-add-list-after') === listId;
},
boardHasNoLists() {
const board = Template.currentData();
return !board || !board.lists || board.lists().length === 0;
},
// #6465: is the empty-board create-list form open? (Set by the + button.)
isEmptyAddListOpen(swimlaneId) {
return Session.get('wekan-add-list-empty') === swimlaneId;
},
currentCardIsInThisList(listId, swimlaneId) {
return currentCardIsInThisList(listId, swimlaneId);
},
@@ -883,6 +1038,11 @@ Template.listsGroup.helpers({
},
});
Template.listsGroup.events({
// #6465: empty board (Lists view) + button reveals the inline composer.
'click .js-open-empty-add-list': openEmptyAddList,
});
Template.listsGroup.onRendered(function () {
const tpl = this;
const boardBodyEl2 = tpl.firstNode?.parentElement?.closest?.('.board-body') || document.querySelector('.board-body');
+2 -2
View File
@@ -98,7 +98,7 @@ template(name="changeAvatarPopup")
| {{_ 'allowed-avatar-filetypes'}} {{Meteor.settings.public.avatarsUploadMimeTypes}}
br
| {{_ 'invalid-file'}}
button.full.js-upload-avatar
button.full.primary.js-upload-avatar
i.fa.fa-upload
| {{_ 'upload-avatar' }}
@@ -137,7 +137,7 @@ template(name="adminChangeAvatarPopup")
| {{_ 'allowed-avatar-filetypes'}} {{Meteor.settings.public.avatarsUploadMimeTypes}}
br
| {{_ 'invalid-file'}}
button.full.js-upload-avatar
button.full.primary.js-upload-avatar
i.fa.fa-upload
| {{_ 'upload-avatar' }}
+52 -4
View File
@@ -44,10 +44,7 @@ template(name="memberMenuPopup")
a.board-header-btn.js-open-archived-board
i.fa.fa-archive
span {{_ 'archives'}}
li
a.js-notifications-drawer-toggle
i.fa.fa-bell
| {{_ 'notifications'}}
//- Notifications removed from the member menu: same as the header bell icon.
if currentSetting.customHelpLinkUrl
li
a(href="{{currentSetting.customHelpLinkUrl}}", title="{{_ 'help'}}", target="_blank", rel="noopener noreferrer")
@@ -79,6 +76,14 @@ template(name="memberMenuPopup")
a.js-change-settings
i.fa.fa-cog
| {{_ 'change-settings'}}
li
a.js-change-color
i.fa.fa-paint-brush
| {{_ 'change-color'}}
li
a.js-change-font
i.fa.fa-font
| {{_ 'change-font'}}
li
a.js-change-avatar
i.fa.fa-picture-o
@@ -181,6 +186,43 @@ template(name="changeLanguagePopup")
| (RTL)
if isCurrentLanguage
i.fa.fa-check
template(name="changeColorPopup")
//- #5778: global theme color override, saved to the user profile. Works like
Board Settings / Change Color, but applies to the whole UI (All Boards, Search,
Admin Panel, etc). The shared picker (docs/Theme/Theme.md) offers category +
theme dropdowns and custom color wheels for flat/clear themes.
+themeColorPicker(scope="global")
template(name="changeFontPopup")
//- #4759: pick a UI font that ALREADY EXISTS in this browser. One button per
detected font, each shown in its OWN font (names are plain text, Blaze-escaped,
validated to a curated whitelist server-side). Font size is a row of buttons
(smaller on the left, 100% in the middle, larger on the right). Clicking a font
or size applies it IMMEDIATELY (no Save button).
form.change-font.edit-label
label {{_ 'font'}}
.ui-font-buttons
a.ui-font-btn.js-ui-font-btn(data-font="" class="{{#if isNoneSelected}}active{{/if}}") {{_ 'font-default'}}
each detectedFonts
a.ui-font-btn.js-ui-font-btn(data-font="{{name}}" style="{{optionStyle}}" class="{{#if selected}}active{{/if}}") {{name}}
//- Preview (pangram + numbers) right below the font-name buttons.
.font-preview(style=previewStyle) {{_ 'font-preview-text'}}
label {{_ 'font-size'}}
.ui-font-size-buttons
each fontSizes
a.ui-font-size-btn.js-ui-font-size-btn(data-size="{{key}}" class="{{#if selected}}active{{/if}}") {{label}}
//- #4759: text + text-background color from a color wheel, each unsettable.
//- Applied immediately when the wheel is committed / reset.
label {{_ 'text-color'}}
.color-wheel-wrap
input.js-ui-text-color(type="color" value=textColorHex title="{{_ 'text-color'}}")
a.js-reset-text-color(class="{{#unless hasTextColor}}disabled{{/unless}}") {{_ 'unset-color'}}
label {{_ 'text-background-color'}}
.color-wheel-wrap
input.js-ui-bg-color(type="color" value=bgColorHex title="{{_ 'text-background-color'}}")
a.js-reset-bg-color(class="{{#unless hasBgColor}}disabled{{/unless}}") {{_ 'unset-color'}}
template(name="changeSettingsPopup")
ul.pop-over-list
li
@@ -189,6 +231,12 @@ template(name="changeSettingsPopup")
| {{_ 'show-desktop-drag-handles'}}
if isShowDesktopDragHandles
i.fa.fa-check
li
a.js-toggle-submit-on-enter(title="{{_ 'submit-on-enter-description'}}")
i.fa.fa-level-down
| {{_ 'submit-on-enter'}}
if isSubmitOnEnter
i.fa.fa-check
unless currentUser.isWorker
li
label.bold.clear
+141 -3
View File
@@ -4,6 +4,9 @@ import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import AccountSettings from '/models/accountSettings';
import Users from '/models/users';
import { Utils } from '/client/lib/utils';
import { ReactiveVar } from 'meteor/reactive-var';
import { detectAvailableFonts } from '/client/lib/fontDetector';
import { fontFamilyValue, fontSizeValue, UI_FONT_SIZES, isHexColor6, colorValue } from '/models/lib/uiFonts';
Template.headerUserBar.events({
'click .js-open-header-member-menu': Popup.open('memberMenu'),
@@ -72,13 +75,12 @@ Template.memberMenuPopup.events({
'click .js-invite-people': Popup.open('invitePeople'),
'click .js-edit-profile': Popup.open('editProfile'),
'click .js-change-settings': Popup.open('changeSettings'),
'click .js-change-color': Popup.open('changeColor'),
'click .js-change-font': Popup.open('changeFont'),
'click .js-change-avatar': Popup.open('changeAvatar'),
'click .js-change-password': Popup.open('changePassword'),
'click .js-change-language': Popup.open('changeLanguage'),
'click .js-support': Popup.open('support'),
'click .js-notifications-drawer-toggle'() {
Session.set('showNotificationsDrawer', !Session.get('showNotificationsDrawer'));
},
'click .js-toggle-grey-icons'(event) {
event.preventDefault();
const currentUser = ReactiveCache.getCurrentUser();
@@ -320,6 +322,10 @@ Template.changeLanguagePopup.events({
});
Template.changeSettingsPopup.helpers({
isSubmitOnEnter() {
const currentUser = ReactiveCache.getCurrentUser();
return currentUser ? currentUser.hasSubmitOnEnter() : false;
},
rescueCardDescription() {
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser) {
@@ -379,6 +385,12 @@ Template.changeSettingsPopup.events({
window.localStorage.setItem('showDesktopDragHandles', 'true');
}
},
'click .js-toggle-submit-on-enter'() {
// Saved to the user profile (Member Settings only; requires a logged-in user).
if (ReactiveCache.getCurrentUser()) {
Meteor.call('toggleSubmitOnEnter');
}
},
'click .js-rescue-card-description'() {
Meteor.call('toggleRescueCardDescription')
},
@@ -413,3 +425,129 @@ Template.changeSettingsPopup.events({
Popup.back();
},
});
// #5778 + docs/Theme/Theme.md: the Member Menu / Change Color popup renders the
// shared themeColorPicker (scope="global"); all of its logic lives there.
// #4759: Member Menu / Font. A dropdown of fonts detected as installed in this
// browser; the value is stored to the profile and validated server-side. The first
// option unsets the custom font.
Template.changeFontPopup.onCreated(function () {
const user = ReactiveCache.getCurrentUser();
this.available = detectAvailableFonts(); // ordered subset of the curated whitelist
this.selected = new ReactiveVar((user && user.getUiFont && user.getUiFont()) || '');
this.selectedSize = new ReactiveVar((user && user.getUiFontSize && user.getUiFontSize()) || 'default');
// null = use default (unset); a hex string = custom color.
this.textColor = new ReactiveVar((user && user.getUiTextColor && user.getUiTextColor()) || null);
this.bgColor = new ReactiveVar((user && user.getUiTextBgColor && user.getUiTextBgColor()) || null);
});
Template.changeFontPopup.helpers({
detectedFonts() {
const tpl = Template.instance();
const sel = tpl.selected.get();
return tpl.available.map(name => ({
name,
selected: name === sel,
optionStyle: `font-family: "${name}", sans-serif;`,
}));
},
isNoneSelected() {
return !Template.instance().selected.get();
},
fontSizes() {
const cur = Template.instance().selectedSize.get();
// Ordered by percent so the buttons read: smaller ... 100% (default) ... larger.
return [...UI_FONT_SIZES].sort((a, b) => a.percent - b.percent).map(s => ({
key: s.key,
label: TAPi18n.__(`font-size-${s.key}`),
selected: s.key === cur,
}));
},
// Wheel <input type=color> needs a hex value even when unset — show a sensible
// default so the wheel opens somewhere reasonable.
textColorHex() {
return Template.instance().textColor.get() || '#000000';
},
bgColorHex() {
return Template.instance().bgColor.get() || '#ffffff';
},
hasTextColor() {
return !!Template.instance().textColor.get();
},
hasBgColor() {
return !!Template.instance().bgColor.get();
},
// Preview reflects the chosen font, size, text color and background color.
previewStyle() {
const tpl = Template.instance();
const family = fontFamilyValue(tpl.selected.get());
const size = fontSizeValue(tpl.selectedSize.get());
const color = colorValue(tpl.textColor.get());
const bg = colorValue(tpl.bgColor.get());
return [
family && `font-family: ${family};`,
size && `font-size: ${size};`,
color && `color: ${color};`,
bg && `background-color: ${bg};`,
].filter(Boolean).join('');
},
});
// Apply the current text/background colors immediately (no Save button).
function applyUiColors(tpl) {
Meteor.call('setUiColors', tpl.textColor.get(), tpl.bgColor.get(), err => {
if (err && process.env.DEBUG === 'true') console.error('setUiColors error', err);
});
}
Template.changeFontPopup.events({
// Click a font-name button -> apply that font immediately ('' = default/unset).
'click .js-ui-font-btn'(event, tpl) {
event.preventDefault();
const font = event.currentTarget.dataset.font || null;
tpl.selected.set(font || '');
Meteor.call('setUiFont', font, err => {
if (err && process.env.DEBUG === 'true') console.error('setUiFont error', err);
});
},
// Click a size button -> apply that size immediately.
'click .js-ui-font-size-btn'(event, tpl) {
event.preventDefault();
const size = event.currentTarget.dataset.size || 'default';
tpl.selectedSize.set(size);
Meteor.call('setUiFontSize', size, err => {
if (err && process.env.DEBUG === 'true') console.error('setUiFontSize error', err);
});
},
// Live-preview while dragging the wheel...
'input .js-ui-text-color'(event, tpl) {
const v = event.currentTarget.value;
if (isHexColor6(v)) tpl.textColor.set(v);
},
'input .js-ui-bg-color'(event, tpl) {
const v = event.currentTarget.value;
if (isHexColor6(v)) tpl.bgColor.set(v);
},
// ...and apply the color when the wheel is committed.
'change .js-ui-text-color'(event, tpl) {
const v = event.currentTarget.value;
if (isHexColor6(v)) tpl.textColor.set(v);
applyUiColors(tpl);
},
'change .js-ui-bg-color'(event, tpl) {
const v = event.currentTarget.value;
if (isHexColor6(v)) tpl.bgColor.set(v);
applyUiColors(tpl);
},
'click .js-reset-text-color'(event, tpl) {
event.preventDefault();
tpl.textColor.set(null); // back to default
applyUiColors(tpl);
},
'click .js-reset-bg-color'(event, tpl) {
event.preventDefault();
tpl.bgColor.set(null);
applyUiColors(tpl);
},
});
+4
View File
@@ -5,6 +5,7 @@ import '/client/components/main/dueCards.jade';
import '/client/components/main/editor.jade';
import '/client/components/main/globalSearch.jade';
import '/client/components/main/header.jade';
import '/client/components/main/themeColorPicker.jade';
import '/client/components/main/keyboardShortcuts.jade';
import '/client/components/main/layouts.jade';
import '/client/components/main/myCards.jade';
@@ -29,6 +30,9 @@ import '/client/components/main/dueCards.js';
import '/client/components/main/editor.js';
import '/client/components/main/globalSearch.js';
import '/client/components/main/header.js';
import '/client/components/main/globalThemeColor.js';
import '/client/components/main/themeColorPicker.js';
import '/client/components/main/uiFont.js';
import '/client/components/main/layouts.js';
import '/client/components/main/myCards.js';
import '/client/components/main/popup.js';
+11
View File
@@ -185,6 +185,17 @@ class DateFilter {
_getMongoSelector() {
this._dep.depend();
// #6483: cards with NO due/end/received date store the field as null — that
// is exactly what the noDate filter matches ({dueAt: null}). A comparison
// filter such as Overdue/"past" is `{$lte: now}`, and `$lte` MATCHES null
// under FerretDB and minimongo (null sorts before dates; neither implements
// MongoDB's type-bracketing), so "Overdue" listed every card that has no due
// date at all. Require a real, non-null value for any comparison selector so
// only actual dates match. The noDate filter (this._filter === null) returns
// null unchanged and is unaffected.
if (this._filter && typeof this._filter === 'object') {
return { ...this._filter, $ne: null };
}
return this._filter;
}
+36
View File
@@ -0,0 +1,36 @@
import { UI_FONTS } from '/models/lib/uiFonts';
// #4759: detect which curated UI fonts are actually installed in THIS browser, using
// the canvas text-width comparison technique. A candidate font is "available" if
// rendering a probe string in it produces a different width than the generic base
// families — i.e. the browser actually had the font and didn't fall back.
//
// Returns an ordered subset of UI_FONTS. Purely a detection helper; the value the
// user picks is still validated server-side against the whitelist.
const BASE_FONTS = ['monospace', 'sans-serif', 'serif'];
const PROBE = 'mmmmmmmmmmlliWWWWWW';
const PROBE_SIZE = '72px';
export function detectAvailableFonts(candidates = UI_FONTS) {
let ctx;
try {
ctx = document.createElement('canvas').getContext('2d');
} catch (_) {
return [];
}
if (!ctx) return [];
const baseWidths = {};
BASE_FONTS.forEach(base => {
ctx.font = `${PROBE_SIZE} ${base}`;
baseWidths[base] = ctx.measureText(PROBE).width;
});
return candidates.filter(font =>
BASE_FONTS.some(base => {
ctx.font = `${PROBE_SIZE} "${font}", ${base}`;
return ctx.measureText(PROBE).width !== baseWidths[base];
}),
);
}
+6 -1
View File
@@ -16,6 +16,8 @@ import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { EscapeActions } from '/client/lib/escapeActions';
import { ReactiveCache } from '/imports/reactiveCache';
import { isSubmitKey } from '/models/lib/editorSubmitKey';
import {
openForm,
closeForm,
@@ -77,7 +79,10 @@ Template.inlinedForm.events({
openForm(currentlyOpenedForm, tpl);
},
'keydown form textarea'(evt, tpl) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
// Ctrl/Cmd+Enter submits by default; a user who enabled "submit on Enter"
// in Member Settings submits on plain Enter (Shift+Enter for a newline).
const submitOnEnter = !!ReactiveCache.getCurrentUser()?.hasSubmitOnEnter?.();
if (isSubmitKey(evt, { submitOnEnter })) {
tpl.find('button[type=submit]').click();
}
},
+22
View File
@@ -1,4 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { ReactiveCache } from '/imports/reactiveCache';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
@@ -307,6 +308,27 @@ hotkeys('-', archiveCard);
// https://github.com/wekan/wekan/pull/5589#issuecomment-2516776519
hotkeys('\xf7', archiveCard);
// #6478: Undo / Redo the caller's last position change (card / list / swimlane
// move) with Ctrl+Z / Ctrl+Y (also Cmd+Z / Cmd+Shift+Z on macOS). The hotkeys
// filter above disables shortcuts inside inputs/textareas/contentEditable, so
// native text undo/redo keeps working while typing. Only enabled for members who
// can modify the board.
function undoRedoLast(method) {
const boardId = Session.get('currentBoard');
if (!boardId || !Utils.canModifyBoard()) {
return;
}
Meteor.call(method, boardId, () => {});
}
hotkeys('ctrl+z, command+z', event => {
event.preventDefault();
undoRedoLast('userPositionHistory.undoLast');
});
hotkeys('ctrl+y, ctrl+shift+z, command+shift+z', event => {
event.preventDefault();
undoRedoLast('userPositionHistory.redoLast');
});
hotkeys('n', (event) => {
event.preventDefault();
const cardId = getSelectedCardId();
+10 -1
View File
@@ -136,7 +136,16 @@ window.Popup = new (class {
return function(evt, tpl) {
const context = (this.currentData && this.currentData()) || this;
context.__afterConfirmAction = action;
// #6479: store the pending confirm action on the Popup instance, NOT on the
// Blaze data context. The old code stashed it as a field on the context and
// the confirm button read it back as `this.__afterConfirmAction`; but the
// context is a ReactiveCache/Minimongo document (e.g. a board member sub-doc),
// and once Blaze re-renders the confirmation popup with a fresh/immutable copy
// of that context the field is gone, so clicking "Remove member" (and every
// other confirm dialog) silently did nothing. The instance field survives the
// re-render; the action is still invoked with the confirmation popup's own data
// context as `this`, so `this.userId` etc. keep working.
self._afterConfirmAction = action;
self.open(name).call(context, evt, tpl);
};
}
+2
View File
@@ -26,7 +26,9 @@ import '/client/components/import/import.css';
import '/client/components/lists/list.css';
import '/client/components/main/accessibility.css';
import '/client/components/main/brokenCards.css';
import '/client/components/main/customTheme.css';
import '/client/components/main/dueCards.css';
import '/client/components/main/uiFont.css';
import '/client/components/main/editor.css';
import '/client/components/main/fonts.css';
import '/client/components/main/globalSearch.css';
+83
View File
@@ -1,8 +1,10 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { ReactiveCache } from '/imports/reactiveCache';
import { decideSandstormAutoOpen } from '/models/lib/sandstormAutoOpen';
import Settings from '/models/settings';
import { EscapeActions } from '/client/lib/escapeActions';
import { Filter } from '/client/lib/filter';
@@ -70,10 +72,91 @@ function renderBoardList(ctx, menu) {
});
}
// #2220: on the FIRST landing on '/' this session (i.e. right after login), send
// the user to their chosen default "home" board. Only once per session, so a
// later click on "All Boards" stays on the list, and the choice is honoured again
// on the next login / full reload. Returns true when it redirected.
function maybeRedirectToDefaultBoard() {
if (Session.get('defaultBoardRedirectDone')) return false;
Session.set('defaultBoardRedirectDone', true);
if (!Meteor.userId()) return false;
const user = ReactiveCache.getCurrentUser();
const boardId = user && user.getDefaultBoardId && user.getDefaultBoardId();
if (!boardId) return false;
// Redirect by id right away — at login the board's own subscription may not have
// loaded yet, so use its real slug when it happens to be cached and a harmless
// placeholder otherwise (the board route resolves by :id). If the board was
// deleted / access was removed, the board route degrades gracefully and the
// "All Boards" link still works (this only redirects once per session).
const board = ReactiveCache.getBoard(boardId);
FlowRouter.go('board', { id: boardId, slug: (board && board.slug) || 'board' });
return true;
}
// #2220 on Sandstorm: a grain historically opened straight into its single board.
// Restore that convenience — reactively, because a grain's login (Meteor.userId())
// and its boards subscription both arrive asynchronously after '/' first renders.
// Runs at most once per page load (grain session): a saved Home board wins; else
// exactly one board just opens (nothing saved — choosing a Home board is the separate
// explicit All Boards toggle); else (zero or many boards, nothing saved) we stay on
// the All Boards list. The decision itself is the pure, unit-tested
// decideSandstormAutoOpen(). Sandstorm-only (see the isSandstorm gate in the route).
let sandstormAutoOpenStarted = false;
function startSandstormAutoOpen() {
if (sandstormAutoOpenStarted) return;
sandstormAutoOpenStarted = true;
const boardsHandle = Meteor.subscribe('boards');
Tracker.autorun(computation => {
const userReady = !!Meteor.userId();
const user = userReady ? ReactiveCache.getCurrentUser() : null;
const savedDefaultId = user && user.getDefaultBoardId && user.getDefaultBoardId();
const boardsReady = boardsHandle.ready();
let boardCount = 0;
let onlyBoardId;
if (userReady && boardsReady) {
const boards = ReactiveCache.getBoards({
archived: false,
type: 'board',
'members.userId': Meteor.userId(),
});
boardCount = boards.length;
if (boardCount === 1) onlyBoardId = boards[0]._id;
}
const decision = decideSandstormAutoOpen({
userReady,
savedDefaultId,
boardsReady,
boardCount,
onlyBoardId,
});
if (decision.action === 'wait') return; // inputs not ready — keep watching
if (decision.action === 'redirect') {
// Just open the board — nothing is persisted here.
const board = ReactiveCache.getBoard(decision.boardId);
FlowRouter.go('board', { id: decision.boardId, slug: (board && board.slug) || 'board' });
}
computation.stop(); // decided (redirected or staying) — done for this page load
});
}
FlowRouter.route('/', {
name: 'home',
triggersEnter: [ensureSignedInUnlessSandstorm],
action() {
// On Sandstorm, render All Boards immediately and let the reactive auto-open
// (above) redirect once the grain login + boards have loaded, if appropriate.
if (isSandstorm) {
startSandstormAutoOpen();
renderBoardList(this, 'starred');
return;
}
if (maybeRedirectToDefaultBoard()) return;
renderBoardList(this, 'starred');
},
});
+6 -3
View File
@@ -1,8 +1,11 @@
[Sandstorm](../Platforms/FOSS/Sandstorm/Sandstorm.md) - [Sandstorm Backup](../Platforms/FOSS/Sandstorm/Export-from-Wekan-Sandstorm-grain-.zip-file.md)
# Backup Sandstorm
# Related
1. Please first backup your Sandstorm https://docs.sandstorm.io/en/latest/administering/backups/
2. Please first download all of your grains to .zip files from top menu row down arrow button, just in case.
[Transferring attachments from MongoDB to filesystem, and text from MongoDB to SQLite](https://github.com/wekan/minio-metadata)
# Upgrade Sandstorm WeKan
[Upgrade Sandstorm WeKan ](../Platforms/FOSS/Sandstorm/Sandstorm.md)
# Backup Docker
+334
View File
@@ -0,0 +1,334 @@
# Design: Universal change History (view + restore) — the basis for Undo/Redo
Status: **Draft for approval** · Owner: xet7 · Related: card details view, Member settings,
`Activities`, `userPositionHistory`, `docs/Features/Undo/Undo.md`
This document specifies one unified **change-history** subsystem that records **every change a user
makes**, keeps it **append-only**, and lets changes be **restored**. It is surfaced from **many menus**, but every one is the **same table + restore** over the **same
store**, differing only by the **scope filter** it passes to `changeHistory.page`. A **History**
option is added to each of these menus (and the pattern generalises to any future entity menu — the
UI and method don't change, only the scope):
| Menu / location | View shows the history of… | Scope filter | Section |
| --- | --- | --- | --- |
| Card group menu (open card) | that **group** on that card, with a per-contributor avatar list | `{ cardId, group }` | 7 |
| Card menu (open card) | the **whole card** (all its groups) | `{ cardId }` | 7a |
| Member settings menu | that **user's** own changes | `{ userId }` | 7a |
| Board Settings | the whole **board** (all users/entities) | `{ boardId }` | 7a |
| Swimlane menu | that **swimlane** and its contents | `{ scope:'swimlane', scopeId }` | 7a |
| List menu | that **list** and its cards | `{ scope:'list', scopeId }` | 7a |
| *(future)* Checklist menu, Attachment menu, … | that entity | `{ entityType, entityId }` | 7a |
Scopes **nest**: a card's history ⊂ its list's ⊂ its swimlane's ⊂ the board's. Container scopes
(board/swimlane/list/card) therefore mean "this entity **and its descendants**" — implemented as an
OR over the relevant id columns (`boardId` / `swimlaneId` / `listId` / `cardId` / `entityId`), which
is why the write side (section 5) stores all the applicable id columns on every row.
Plus one keyboard front-end:
- **Undo / Redo** — `Ctrl+Z` / `Ctrl+Y` = "restore the current user's most recent change from this
history" / "re-apply it". Undo/Redo is therefore **not a separate feature**; it is the keyboard
restore of the newest own change. (v1 undo/redo of *position moves* already shipped in #6478 via
`userPositionHistory`; this design **generalises** that to every change and merges the two.)
It is a design doc only — no code is implied as final until this is approved.
> **Scope change (this revision):** the earlier draft covered only card *groups*. Per request, the
> model now records **every change** (all card fields/groups **and** board/list/swimlane/etc.
> structural changes), per user, and adds the Member-settings per-user view. `userPositionHistory`
> becomes a special case that this unified store supersedes.
---
## 1. Goal (from the request)
On the card open-details view, each group menu gains an option **History**. Clicking it opens a
big popup:
- **Left column** (always visible):
- Top: **History** entry — the current, newest history for that group.
- Below: an **avatar** (or initials when there is no avatar) for **each user** who has added or
changed something in that group. Clicking an avatar shows **all changes that user made to that
group**, newest first.
- **Right area** (always the same table layout):
- **Above the table** (LTR, left → right): **Search** (left), **pagination controls** (middle),
**Restore** (right).
- **Table columns** (LTR, left → right): **select checkbox** (per row), **what was changed**
(translatable — added / removed / edited / …), **content of change** (e.g. the new text),
**datetime** (formatted with the card's selected date format).
- Only the **current page** is loaded/rendered (server-side pagination).
- **RTL**: every element and the text order is mirrored (search right, pagination middle, restore
left; columns reversed).
- **Restore**: restores the selected change to that group. The restore is itself recorded in
history — on **both** the source contributor's history *and* the restorer's history (who restored
what to become current). History is **append-only**: you can restore *from* it, but never edit it.
## 2. The core blocker (why this is more than a UI)
WeKan already logs actions in the **`Activities`** collection, but that collection records *what
happened* (an `activityType` plus references: `cardId`, `listId`, `memberId`, …) — **not the
before/after values**. Example: editing a description logs `activityType: 'changedDescription'`
with the card id, but **not the previous text**.
Consequences:
- The **viewer** (what changed, when, by whom) can largely be built from `Activities`.
- **Restore** generally **cannot** be built from `Activities`, because the previous content is not
stored. Only `userPositionHistory` stores before/after (for positions).
Therefore the feature splits into two efforts:
1. A **content-versioning subsystem** that captures before/after content per group change (the large,
invasive part; prerequisite for restore).
2. The **history viewer + restore UI**.
## 3. Groups and change types
"Group" maps to the sections of the card details view. Proposed coverage (phase order later):
| Group | Entity / field | Change types |
| --- | --- | --- |
| Description | `cards.description` | edited |
| Title | `cards.title` | edited |
| Labels | `cards.labelIds[]` | added, removed |
| Members / Assignees | `cards.members[]`, `cards.assignees[]` | added, removed |
| Dates | received/start/due/end | added, edited, removed |
| Checklists | checklist + items | added, removed, edited, checked/unchecked, moved |
| Subtasks | linked subtask cards | added, removed, moved |
| Attachments | files/avatars | added, removed, renamed |
| Comments | `card_comments` | added, edited, removed |
| Custom fields | `cards.customFields[]` | edited |
Change types are a small closed set with i18n keys, e.g. `history-change-added`,
`history-change-removed`, `history-change-edited`, `history-change-moved`,
`history-change-restored`. Added to `imports/i18n/data/en.i18n.json` only (translations follow via
Transifex; see the translation-pull auto-heal note in the changelog).
## 4. Data model
One new append-only Mongo collection **`changeHistory`** (working name) covering **every** change,
whatever the entity. One document per change:
```js
{
_id,
boardId, // for permission scoping + publications (null for non-board changes, if any)
// What was changed — general, not card-only:
entityType, // 'card' | 'list' | 'swimlane' | 'board' | 'checklist' | 'checklistItem'
// | 'comment' | 'attachment' | 'customField' | ...
entityId, // the changed entity's _id
cardId, // set when the change belongs to a card (drives the card-group view); optional
group, // logical group for the card view: 'description' | 'labels' | 'members'
// | 'dates' | 'checklists' | 'title' | ... (optional for non-card changes)
changeType, // 'added' | 'removed' | 'edited' | 'moved' | 'restored'
// Content for display + restore — structured, not just strings:
previousContent, // blackbox; null for 'added'
newContent, // blackbox; null for 'removed'
userId, // WHO made the change — the axis the Member-settings view filters on
createdAt, // Date; formatted client-side with the viewer's/card's date format
// Undo/redo stack (folds in #6478's userPositionHistory fields):
undone, // Boolean — restored/undone, redoable until superseded
undoneAt, // Date — orders the redo stack
batchId, // groups a multi-entity change (e.g. multi-select move / multi-restore)
// Restore provenance (set only when changeType === 'restored'):
restoredFromId, // the changeHistory _id whose content was restored
restoredByUserId, // who performed the restore
}
```
Notes:
- **Append-only.** No client-exposed update/remove (except the internal `undone`/`undoneAt` flip for
the undo/redo stack). Retention cap via a server cron (à la `userPositionHistory.cleanup`).
- `previousContent`/`newContent` are `blackbox` so each entity/group stores what it needs
(`{ text }` for description, `{ labelId }` for a label, `{ millis }` for a date, `{ sort,
swimlaneId, listId, boardId }` for a move, …).
- **Supersedes `userPositionHistory`.** That collection's move rows map 1:1 onto this schema
(`entityType` card/list/swimlane, `changeType: 'moved'`, previous/new = the position). Migration:
keep `userPositionHistory` writing during transition, or one-time copy its rows in; the undo/redo
methods move to read `changeHistory`.
- Alternative considered: **extend `Activities`** with before/after content. Rejected for v1 —
`Activities` is deliberately schemaless/high-volume and drives notifications/webhooks; overloading
it risks those paths. A dedicated collection keeps concerns separate and independently cappable.
## 5. Write side (recording every change)
Record on the **server**, in **every** mutation path (not only card groups), capturing the value
**before** and **after**. A single helper:
```js
ChangeHistory.record({ boardId, entityType, entityId, cardId?, group?, changeType, previousContent, newContent, userId, batchId? });
```
called from the existing setters/methods — card fields (`Cards.setDescription`, title, label
add/remove, member/assignee add/remove, date setters, custom fields), card sub-entities
(checklist/checklist-item, comment, attachment mutations), and **structural** changes (list/swimlane
create/rename/move/archive, board-level changes). Position **moves** come in via the same helper
(replacing `userPositionHistory.trackChange`).
- **Best-effort:** never fail the mutation if recording throws (try/catch).
- **Import the collection** — do not reference it as an assumed global. The shipped position history
was inert precisely because its guard `typeof UserPositionHistory !== 'undefined'` was false
without an import (fixed in #6478). This is the single most important implementation lesson.
- Consider a thin, central choke point: many mutations already emit an `Activity`; recording history
next to `Activities.insert` (with the extra before/after content) avoids sprinkling calls
everywhere. Evaluate during phase 1.
## 6. Read side (one paginated/searchable method for all three views)
Only the current page is loaded. One method (not a naive reactive publication of the whole log)
serves the card-group view, the per-user Member-settings view, and any filter combination:
```js
Meteor.call('changeHistory.page', {
// scope (any subset; container scopes match the entity AND its descendants):
scope, // 'board' | 'swimlane' | 'list' | 'card' — the container kind
scopeId, // that container's _id (boardId / swimlaneId / listId / cardId)
group, // narrow a card scope to one group (card-group view)
userId, // one contributor — Member view, or an avatar click within another scope
// list controls:
search, // matches changeType label + content text
page, pageSize, // 1-based page, server clamps pageSize
}) -> { rows, total, page, pageSize, contributors: [{ userId, count }] }
```
The server turns `{scope, scopeId}` into the id-column filter (`board`→`boardId`; `swimlane`→
`swimlaneId` OR its lists'/cards' rows; `list`→`listId` OR its cards'; `card`→`cardId`), then applies
`userId`/`group`/`search` on top. Member-settings view passes just `{ userId }` (optionally
`+ scope:'board'` to limit to the current board).
- **Permission:** caller must have board-visible access to the scoped board(s) (reuse
`requireBoardVisible`). The Member-settings view is scoped to boards the **caller** can see; it
never leaks a user's changes on boards the caller can't access.
- **Search:** case-insensitive over the rendered change-type label and a text projection of
`newContent`/`previousContent` (same cross-environment numeric/text caveat as card search).
- **contributors** powers the card view's left-column avatar list (distinct `userId` + counts);
unused when the view is already pinned to one `userId`.
- Pure, unit-testable helpers (mirroring `models/lib/undoRedoSelection.js`): `paginate(rows, page,
size)`, `matchesSearch(row, term)`, `selectionToIds(selected)` — in `models/lib/…` with tests.
## 7. UI
Templates (Blaze/jade), all inside one popup opened from the group menu's **History** item:
- `cardGroupHistoryPopup` — 2-pane layout.
- Left: `historyNav` — a **History** button (default view = newest, all users) + a list of
`+userAvatar` (fallback initials) from `contributors`. Selecting one sets the `userId` filter.
- Right: `historyTable` —
- top bar: `.js-history-search` (left) · pagination `.js-history-prev/next` + "page X / N"
(middle) · `.js-history-restore` (right).
- table: header row + one row per `rows[]`: `input[type=checkbox].js-history-select`,
change-type label (`{{_ changeTypeKey}}`), content, `{{ formatWithCardDateFormat createdAt }}`.
- State: current `group`, `userId` filter, `search`, `page`, and a `Set` of selected row ids kept in
a `ReactiveDict` on the template instance (not the data context — see #6479: don't stash state on
Blaze data contexts).
- **RTL:** rely on `dir=rtl` + logical CSS (`margin-inline`, `text-align: start`, flex order) so the
top bar and columns mirror without duplicated markup. Verify live.
- **Date format:** reuse the card's configured date format helper so the datetime column matches the
rest of the card.
## 7a. Scoped views (card / member / board / swimlane / list / …)
Every non-card-group surface in the table above is **the same `historyTable`** with a different
`changeHistory.page` scope; there is **one** implementation, parametrised by scope. They share the
columns, search, pagination, RTL, and restore (+ dual re-logging) of section 7/8.
- **Contributor avatars** (left column) appear whenever the scope can span more than one user —
card, board, swimlane, list. They are omitted for the Member view (already one user). Clicking an
avatar adds `{ userId }` to the current scope (that user's changes **within** this scope).
- **Permission** per scope, reusing existing guards:
- card / swimlane / list → board-visible (write access to restore);
- board (Board Settings) → board **admin**;
- member (Member settings) → the user themselves, or an admin over boards the admin can see. A
Member view **never** leaks changes on boards the caller can't access.
- **Nesting** (see the table note): container scopes match the entity **and its descendants** via an
OR over id columns, so a swimlane's view includes its lists'/cards' changes, etc.
Concretely, adding "History" to a new menu = (1) a menu item that opens `historyPopup` with a scope,
(2) — nothing else. No new method, table, or restore code.
## 7c. Undo / Redo = restore the last own change
`Ctrl+Z` / `Ctrl+Y` are the keyboard front-end to this history for the **current user on the current
board**:
- **Undo** = restore the caller's **most recent, not-yet-undone** `changeHistory` row (mark it
`undone`), for **any** `entityType`/`changeType` — not just moves.
- **Redo** = re-apply the caller's **most-recently-undone** row.
- A **new change clears the redo stack** (delete/flag this user+board's `undone` rows).
This **generalises** the shipped #6478 methods: `userPositionHistory.undoLast/redoLast` become
`changeHistory.undoLast/redoLast` reading the unified store; the selection rule stays the pure,
tested `pickUndo`/`pickRedo`; the key bindings in `client/lib/keyboard.js` are unchanged. "Restore
selected row" (History UI) and "undo last" (keyboard) are the **same operation** on the same data.
> Undo restores content via the **same setters** as a normal edit (so validation/Activities run),
> and the restore is itself appended to history (see section 8) — so undo is auditable and itself
> undoable/redoable.
## 8. Restore
`Meteor.call('cardGroupHistory.restore', historyId)`:
1. Load the target history row; require board write access.
2. Apply `previousContent` (or `newContent`, per the row's semantics) back to the live group via the
**same setters** used for normal edits (so validation/activities still run).
3. Append **two** `cardGroupHistory` rows with `changeType: 'restored'` and `restoredFromId`:
- one attributed to the **source contributor** (`userId` of the restored row) — "their" data was
restored,
- one attributed to the **restorer** (`Meteor.userId()`) — who restored what to become current.
4. Restore of **multiple selected** rows applies in a defined order (oldest→newest of the selection)
and is a single logical batch (shared `batchId`).
5. History rows themselves are **never edited or deleted** by restore.
Per-change-type restore functions live next to each group's setter (description/labels/dates/…), so
each knows how to re-apply its own `previousContent`.
## 9. Security & integrity
- All read/restore methods gate on board access (`requireBoardVisible` / write access), like
`updateListSort` and `userPositionHistory.*`.
- No client-exposed update/delete on `cardGroupHistory` (append-only invariant).
- Retention cap per card/board via a server cron (reuse the `userPositionHistory.cleanup` pattern).
## 10. Phasing (each phase verified live before the next)
1. **`changeHistory` model + write helper + pure helpers (paging/search/selection/pick undo-redo) +
unit tests.** Migrate the shipped position undo/redo onto it: point
`changeHistory.undoLast/redoLast` at the new store and record card/list/swimlane **moves** there
(this both proves the model and keeps #6478 working). Ctrl+Z/Ctrl+Y now read `changeHistory`.
2. **First content group — Description:** record before/after on edit; **read method**
(`changeHistory.page`) + the **viewer UI** (table, search, pagination, avatars); LTR then RTL.
Ctrl+Z now also undoes a description edit.
3. **Restore** UI for Description (single + multi-select) with dual re-logging (identical to the
keyboard undo path).
4. **Member-settings "History"** (per-user) and **Board-settings "History"** (per-board) views —
both are just the same `changeHistory.page` method + `historyTable` UI with a different scope
(`{ userId }` vs `{ boardId }`), so they land together once step 3's table/restore exist.
5. **Roll out** to every remaining group/entity (title, labels, dates, members/assignees, checklists,
subtasks, attachments, comments, custom fields, board/swimlane structural changes), one per PR,
reusing the shared write helper + `page` method + UI.
6. **"History" menu item** wired into every card group menu (and the Member-settings menu from
step 4).
## 11. Open questions (need product decisions)
- Scope of "content of change" for non-text groups (label = its name/color? member = avatar+name?
attachment = filename?). Proposal above; confirm.
- Retention: how many history rows per card/board before pruning?
- Does history need to survive card **archive/restore** and **copy/move** across boards?
- Should restore of a **removed** entity re-create it (attachments, comments) or only text/scalar
fields in v1?
- Per-board on/off setting for history (storage cost)?
---
### Appendix: lessons already banked from this session
- **Import your collection helpers.** `userPositionHistory` recording was dead because
`trackChange` was guarded by `typeof … !== 'undefined'` without importing the collection (fixed in
#6478). The new history helper must be a real import.
- **Don't stash state on Blaze data contexts.** #6479 showed a re-render drops ad-hoc fields; keep
popup/table state on the template instance / `ReactiveDict`.
- **Pagination/search/selection as pure functions** (à la `models/lib/undoRedoSelection.js`) so the
logic is unit-testable without the Meteor/Blaze runtime.
+371
View File
@@ -0,0 +1,371 @@
# Feature: Undo / Redo (position moves) + high-impact confirmation
Status: **Implemented (v1)** in #6478 · Owner: xet7 · Related: `userPositionHistory`,
card/list/swimlane drag-and-drop, `docs/Features/History/History.md`
This documents the undo/redo feature **shipped in v1** for #6478 ("UI: Confirmation for actions with
impact") — which covers **position moves** (card / list / swimlane) only.
> **Direction (approved design):** undo/redo is being folded into one **universal change-history**
> subsystem that records **every** change and exposes it as History views on card groups, the whole
> card, Member settings, Board Settings, swimlane/list menus, etc. In that model, **Undo/Redo is not
> a separate feature** — `Ctrl+Z`/`Ctrl+Y` simply restore the current user's most recent change from
> that history, for **any** change type, and `userPositionHistory` is superseded by the unified
> `changeHistory` store. The single source of truth for that direction is
> **`docs/Features/History/History.md`**; this doc describes the current v1 (positions) and the
> migration path. See History.md §7c (Undo/Redo) and §10 (phasing: position undo/redo migrates onto
> `changeHistory` first).
---
## 1. Motivation
A user accidentally dragged a **list** into another swimlane on mobile, merging it with an existing
list, and spent ~15 minutes recovering — there was **no confirmation** and **no undo**. This feature
adds both for the move operations, using the already-present (but previously **inert**) position
history.
## 2. What ships in v1
- **Undo** with `Ctrl+Z` (`⌘Z`) and **Redo** with `Ctrl+Y` / `Ctrl+Shift+Z` (`⌘⇧Z`) for the last
**position change** on the current board: moving a **card**, **list**, or **swimlane**.
- A **confirmation** prompt before moving a **list to a different swimlane** on **touch / small
screens** (the exact, card-re-homing accident). Declining reverts the drag. Everyday same-swimlane
reordering is unaffected.
## 3. Data model — `userPositionHistory`
Pre-existing collection (`models/userPositionHistory.js`), one document per position change:
```
userId, boardId, entityType ('card'|'list'|'swimlane'|'checklist'|'checklistItem'),
entityId, actionType ('move'|'create'|'delete'|'restore'|'archive'),
previousState / newState (blackbox) + flattened previous*/new* (Sort, SwimlaneId, ListId, BoardId),
createdAt, isCheckpoint, checkpointName, batchId,
undone (Boolean), undoneAt (Date) // <- added in #6478 for the redo stack
```
Helper methods on a document:
- `canUndo()` — the entity still exists.
- `undo()` — restore the **previous** position (per entityType).
- `redo()` — **added in #6478**: re-apply the **new** position (mirror of `undo()`).
## 4. The bug that made undo inert (fixed in #6478)
`UserPositionHistory.trackChange(...)` is server-only, but callers referenced it as an **assumed
global** guarded by `typeof UserPositionHistory !== 'undefined'`. It is an ES-module default export,
**not** a global, so the guard was **false** and **nothing was ever recorded** — undo silently did
nothing. Fix: **import the collection** where `trackChange` is called.
> Lesson (also in the History doc): import your collection helpers; never gate on an assumed global.
## 5. Recording changes (write side)
`trackChange({ userId, boardId, entityType, entityId, actionType, previousState, newState })` inserts
a history row. Wired at:
- **Card moves** — `models/cards.js` (`Card.move`), already present (now actually runs).
- **List moves** — `server/models/lists.js` `updateListSort` (**added in #6478**; imports the
collection). Captures `{ sort, swimlaneId, boardId }` before the update.
- **Swimlane moves** — the model supports `entityType: 'swimlane'`; wire the swimlane reorder path
the same way when needed (follow-up).
Recording is **best-effort**: a failure to record never fails the move (wrapped in try/catch).
## 6. Undo / redo stack semantics
The undo stack is the board's real (non-checkpoint) changes in `createdAt` order; the redo stack is
the changes that have been undone.
- **Undo** picks the **most recent not-yet-undone** change, calls `undo()`, sets
`undone: true, undoneAt: now`.
- **Redo** picks the **most-recently-undone** change (largest `undoneAt`), calls `redo()`, clears
`undone`.
- **A new real change clears the redo stack** — `trackChange` deletes this user+board's `undone`
rows, so an undone-then-superseded change can never be redone into stale state.
Server methods (`server/models/userPositionHistory.js`):
- `userPositionHistory.undoLast(boardId)` → `{ undone, entityType, entityId }`
- `userPositionHistory.redoLast(boardId)` → `{ redone, entityType, entityId }`
Both gate on `requireBoardVisible`. The selection rule is a **pure, unit-tested** helper
`models/lib/undoRedoSelection.js` (`pickUndo` / `pickRedo`, tests in
`tests/undoRedoSelection.test.cjs`); the server applies the same rule via Mongo `sort`/`limit`.
## 7. Client bindings
`client/lib/keyboard.js` binds `ctrl+z, command+z` → `undoLast` and
`ctrl+y, ctrl+shift+z, command+shift+z` → `redoLast`, calling the method for
`Session.get('currentBoard')`, only when `Utils.canModifyBoard()`. The global hotkeys **filter**
already disables shortcuts inside inputs / textareas / contentEditable, so **native text undo/redo
keeps working** while typing.
## 8. High-impact confirmation
In `client/components/swimlanes/swimlanes.js` `saveSorting` (the list-drop handler): when the drop
is a move to a **different** swimlane (`isDifferentSwimlane`) **and** the viewport is touch/small
(`Utils.isMiniScreen() || Utils.isTouchScreenOrShowDesktopDragHandles()`), a `window.confirm`
(i18n `confirm-move-list-to-swimlane`) is shown; declining re-subscribes to the board (reverting the
drag) and aborts before any card re-homing. This complements undo (belt-and-suspenders for the
riskiest, hardest-to-notice action).
> Interaction with #6484: board-wide lists (`swimlaneId === null`) are no longer *accidentally*
> bound to a swimlane on a nudge, so this confirm only fires for genuinely swimlane-scoped list moves
> across swimlanes.
## 9. Retention
`userPositionHistory.cleanup` (server cron) caps history per user/board (keeps ~1000 newest
non-checkpoint rows). Toggle via `Meteor.settings.public.enableHistoryCleanup`.
## 10. Limitations / not in v1
- Undo/redo covers **position moves only** — not text edits, labels, members, dates, etc. Those need
before/after content versioning (see `docs/Features/History/History.md`).
- No on-screen Undo/Redo **buttons** yet (keyboard only) — a board-header control + a post-move
"Undo" toast are natural follow-ups.
- Swimlane-move recording is modelled but the reorder path is not yet wired.
- Cross-user concurrency: undo restores the previous stored position; if another user moved the same
entity meanwhile, the restore uses this history row's previous values (last-writer-wins, as
elsewhere in WeKan).
## 11. Tests
- `tests/undoRedoSelection.test.cjs` — pure `pickUndo`/`pickRedo` incl. LIFO symmetry, checkpoint
and already-undone skipping, and the redo-by-`undoneAt` order.
- Runtime paths (keyboard, Meteor methods, Blaze) require a live app to verify.
## 12. Extension checklist (adding a new undoable move)
1. In the mutation path, capture `previousState` **before** the write and call
`UserPositionHistory.trackChange({ … })` **after** — importing the collection, wrapped in
try/catch.
2. Ensure `undo()`/`redo()` and `canUndo()` handle the `entityType` (add a `case` if new).
3. Nothing else — `undoLast`/`redoLast`, the redo-stack clearing, and the key bindings are generic.
---
# Soft delete (delete = mark, never destroy)
Status: **Lists implemented (v1) · rest design** · Owner: xet7 · Related: #1023 ("undo button for
deleted lists etc"), History.md, the `archived` flag, `userPositionHistory`
(`actionType: 'delete'|'restore'`)
> **Implemented so far (v1):** deleting a **list** is now a soft delete — the list and its cards are
> marked (`deletedAt`/`deletedBy`/`deleteBatchId`), hidden from the board, recorded as a reversible
> `delete` in history, and restorable via `lists.restore` **or** undone with `Ctrl+Z`. This delivers
> **#1023** for lists. Core pieces that are live: `models/lib/softDelete.js` (pure helpers +
> `tests/softDelete.test.cjs`), the `enablePermanentDelete` feature flag + `Settings` field, the
> `deletedAt/deletedBy/deleteBatchId` schema fields on Lists & Cards, `deletedAt: null` filtering on
> the board/swimlane list render paths, the `lists.softRemove` / `lists.restore` / `lists.purge`
> methods (purge gated by `canPurge`), the client + REST delete paths, and `undo()/redo()` handling
> of `delete`/`restore`. **Still design:** extending the same pattern to swimlanes/boards/cards/
> checklists/comments/attachments, the Recycle Bin UI, and the Admin Panel / Features / Delete table
> (§16b–§16c).
> **Principle:** *Every* user-facing delete is a **soft delete** — it **marks** the document deleted
> instead of removing it. There is **no permanent delete in ordinary use**. Physical removal exists
> only behind **one narrow, guarded purge path** (below). This is the foundation that makes
> **undoing a deletion** possible (#1023): if nothing is destroyed, anything can be restored.
## 13. Why this exists / relation to undo & history
Undo v1 (§1–§12) restores **positions**. It cannot bring back a **deleted** list/card/swimlane
because the delete path physically `removeAsync`-es the rows — there is nothing left to restore. Soft
delete closes that gap: a delete becomes a reversible state change, so:
- **Undo of a delete** = clear the deleted mark (exactly like `undo()` clears a move), satisfying
**#1023**.
- **History** (History.md) records the `delete` and `restore` events like any other change; the
`userPositionHistory` schema **already** allows `actionType: 'delete'|'restore'|'archive'`.
- A **Recycle Bin** view can list soft-deleted items per board/user for explicit restore, independent
of the keyboard undo stack.
## 14. `archived` vs `deleted` — two distinct states
WeKan already has **archive** (`archived: true` + `archivedAt`) as a *user-visible, first-class*
"put it aside" state with its own UI (the Archive). **Soft delete is a separate, deeper state** —
"the user asked to delete this":
| State | Field(s) | Visible where | Meaning |
|--------------|-----------------------------|------------------------------|----------------------------------|
| Active | `deletedAt: null` | Board | Normal |
| Archived | `archived: true` | Archive view | Set aside, still "kept" |
| Soft-deleted | `deletedAt`, `deletedBy` | Recycle Bin only | Deleted, awaiting restore/purge |
Do **not** overload `archived` for delete — they have different UIs, different retention, and an item
can be archived *and* then deleted. New fields on every soft-deletable collection:
```
deletedAt (Date, default null) // null = live; set = soft-deleted
deletedBy (String userId, optional)
deleteBatchId (String, optional) // groups a cascade (list + its cards) into one restorable unit
```
## 15. Scope — what gets soft delete, what is exempt
**Soft-deletable (user content):** Boards, Swimlanes, Lists, Cards (incl. subtasks), Checklists,
ChecklistItems, CardComments, Attachments, Rules/Triggers/Actions, Labels, CustomFields,
Integrations, plus board-scoped settings the user can delete.
**Exempt (transient / infrastructure — hard delete stays):** `SessionData`, the `CronJob*`
collections, `TrelloImportJobs`, `InvitationCodes`, `UserPositionHistory`/`changeHistory` retention
cron, and *cascade bookkeeping* like `Activities` cleanup. These are not user content; soft-deleting
them would only bloat storage with no restore value. `log()`-style note: exemptions are **explicit**,
not accidental — each exempt `removeAsync` should carry a short `// hard-delete OK: transient` comment
so the audit is greppable.
## 16. The one narrow purge path (the only permanent delete)
Physical `removeAsync` of user content is allowed from **exactly two** places, both guarded and
audited:
1. **GDPR / account erasure** — deleting a user (`Users` erasure) may hard-delete or **anonymize**
that user's personal data. Legally required; cannot be soft-only, so it is **not** gated by the
toggle below.
2. **Explicit Global-Admin "Permanently delete"** — an admin action on a soft-deleted item (or
"empty Recycle Bin") that consciously destroys it, **gated by an Admin Panel setting** (below).
### 16a. Admin Panel / Features / Delete → "Enable permanent delete for Global Admin"
Permanent delete of user content is **off by default**. A new toggle lives at **Admin Panel /
Features / Delete**:
```
Admin Panel
└─ Features
└─ Delete
[ ] Enable permanent delete for Global Admin
(When off, nothing can be physically deleted from the Recycle Bin —
all deletes stay soft/restorable. GDPR account erasure is unaffected.)
```
- Stored as a feature flag alongside the existing ones (`models/lib/featureFlags.js` /
`getFeatureFlags`, the same mechanism the activities publication reads), e.g.
`enablePermanentDelete` on `Settings`.
- **Only a Global Admin** sees the "Permanently delete" / "Empty Recycle Bin" actions, and **only
when the flag is on**. Board admins never get it. Both the client (hide the action) and the server
(the `purge()` method re-checks `Meteor.user().isAdmin` **and** the flag) enforce it — client
hiding alone is not a guard.
- The physical-delete helper (`purge()`) is the *only* code allowed to call `removeAsync` on user
content; it hard-fails unless `isAdmin && enablePermanentDelete`, and logs an audit `Activity`
(`activityType: 'permanentDelete'`).
### 16b. The Delete panel is a History-style category table
**Admin Panel / Features / Delete** is not just the toggle — it renders a **History-design table**
(same layout as History.md: paginated, searchable, actor avatar, timestamp, RTL-aware) that lists
**everything currently soft-deleted, grouped by category**. It works whether or not permanent delete
is enabled:
- **Flag off:** the table is **read-only insight** — the admin can see what has been deleted (and how
much storage it holds) and could **Restore**, but the "Permanently delete" / "Empty" actions are
hidden/disabled.
- **Flag on:** each row and each category header gains a **checkbox**; the Global Admin **selects
what to delete permanently** and confirms. Selection is per-row or whole-category ("select all in
*Cards*").
Columns: category · title/description · board · deleted by (avatar) · deleted at · size · actions
(Restore / Permanently delete). A server method `softDelete.listDeleted(category, boardId, page)`
feeds it with the same pagination discipline as the activities feed (#2539) — only the visible page
is loaded, so a huge trash never slows the panel.
### 16c. GDPR categories — what a Global Admin may permanently delete, and what each purge must include
The category table is organized around the data classes GDPR erasure and data-minimization actually
require an operator to be able to remove. **Permanently deleting a category deletes the whole object
graph below it** — a purge that leaves orphaned children behind is a data leak, so each category
lists what *must* be included:
| Category | What it is | A permanent delete MUST also remove |
|----------|------------|-------------------------------------|
| **User account (PII)** | A person's identity: profile (name, email, avatar), login/OAuth/LDAP identifiers, sessions, API tokens, notification/subscription rows | Their avatar blob; memberships stripped from every board; **authored content anonymized or reassigned** (comments/activities → "Deleted user") per the board owner's choice; personal settings, invitation codes, password-reset tokens |
| **Boards** | A whole board | Its swimlanes, lists, cards (+ subtasks), checklists & items, card comments, labels, custom-field *values*, rules/triggers/actions, activities, and **all attachments' physical blobs** (S3/GridFS), board background images |
| **Swimlanes / Lists** | A column or row | Their cards and everything under those cards (as in Boards), via the `deleteBatchId` cascade |
| **Cards** | A single card (+ subtasks) | Its checklists & items, comments, activities, attachments (physical blobs), custom-field values, votes, time entries |
| **Attachments** | Files | The **physical bytes** in the storage adapter (S3 / GCS / Azure / GridFS / filesystem), not just the metadata row |
| **Comments & Activities** | Free-text that may contain PII | The rows themselves; referenced attachment blobs if any |
| **Custom-field values** | Per-card values (may hold PII) | The values on cards; definitions remain unless the field itself is deleted |
Rules for the purge implementation:
- **Cascade completeness:** deleting a parent purges the entire subtree; never leave orphaned cards,
checklist items, comments, or (critically) **attachment blobs** — those live in external storage and
are the easiest thing to leak.
- **PII beyond content:** account erasure must also clear the identity/auth surface (sessions, tokens,
OAuth/LDAP links, avatar), not only board content.
- **Anonymize vs. delete:** for authored content that others still rely on (a comment thread), the
operator chooses **anonymize** (reassign to a tombstone "Deleted user", scrub the PII) instead of a
hard delete — GDPR is satisfied by removing the *personal data*, not necessarily the row.
- **Audit, not silent:** every permanent delete writes an audit `Activity` (who, what category, how
many objects, when). The trash is never emptied on a timer.
Everything else **must** soft-delete. To keep this enforceable:
- Route user-content deletes through a single helper (§17), never a bare `removeAsync`.
- **No time-based auto-purge.** Trash is emptied only by the flag-gated Global-Admin action or GDPR
erasure — the "one narrow purge path" decision, chosen over a retention-window auto-purge, so
nothing ever disappears on a timer.
## 17. Mechanism — a `softRemove` helper + query default
Replace user-content `removeAsync` call sites (see the ~40 sites in `models/` and `server/models/`)
with a soft-delete helper that mirrors today's cascade but *marks* instead of destroys:
```js
// document method, e.g. on Lists / Cards / Swimlanes …
async softRemove(userId, batchId) {
const at = /* server time */;
await this.collection.updateAsync(this._id, {
$set: { deletedAt: at, deletedBy: userId, deleteBatchId: batchId },
});
// cascade: soft-remove children with the SAME batchId (a list marks its cards, etc.)
// record history: UserPositionHistory.trackChange({ actionType: 'delete', … })
}
```
Reads must hide soft-deleted docs **by default**. Two enforcement layers:
- **Publications / server queries** add `deletedAt: null` to their selectors (a shared
`notDeleted(selector)` helper keeps this uniform and testable). The Recycle Bin publication is the
one place that *opts in* to `deletedAt: { $ne: null }`.
- **Cascade & counts** (list card counts, board "N cards", exports) filter `deletedAt: null` too, so a
soft-deleted card doesn't inflate totals.
Restore is the inverse: `$unset` the three fields for the whole `deleteBatchId` (so restoring a list
brings back exactly the cards deleted with it, not ones deleted separately later).
## 18. Undo/redo & History integration
- **Undo of a delete:** `trackChange({ actionType: 'delete' })` on soft-remove; `undo()` gains a
`delete` case that clears the delete mark for the `deleteBatchId`; `redo()` re-applies it. This
extends the generic stack in §6 — `undoLast`/`redoLast` need no change.
- **History:** delete/restore appear in History.md's unified feed with actor avatar + timestamp; the
Recycle Bin is essentially a History view filtered to `actionType: 'delete'` with a Restore button.
## 19. Migration
1. Add `deletedAt`/`deletedBy`/`deleteBatchId` to the soft-deletable schemas (nullable; no backfill
needed — absent ⇒ live).
2. Introduce `notDeleted()` and `softRemove()` helpers; convert user-content `removeAsync` sites,
leaving exempt sites with an explicit `// hard-delete OK` comment.
3. Add `deletedAt: null` to the affected publications/queries **in the same change** as each
conversion (a converted delete that publications don't filter would make "deleted" items still
show — the highest-risk step; do it collection-by-collection).
4. Add the Recycle Bin view + the admin purge action + GDPR erasure purge.
5. Wire `delete`/`restore` into undo and History.
## 20. Risks / notes
- **Storage growth** is accepted (the chosen trade for "never lose data"); the admin purge and GDPR
erasure are the only relief valves. Document this for operators.
- **Query drift:** any *new* query that forgets `deletedAt: null` will leak deleted rows. The
`notDeleted()` helper + a source-guard test (grep that user-content publications include the filter)
mitigate this.
- **Unique constraints / slugs:** a soft-deleted board/label still occupies its slug; decide whether
restore or a new item wins the name (recommend: soft-deleted items release user-facing uniqueness,
re-acquire on restore or get suffixed).
- **Third-party storage (Attachments):** soft-deleting an attachment keeps the blob in S3/GridFS;
only the admin purge / GDPR erasure removes the physical bytes.
+5 -5
View File
@@ -1,14 +1,14 @@
- At some cases [Hardened kernel may prevent creating new Wekan boards at Sandstorm](https://github.com/wekan/wekan/issues/1398)
# Sandstorm at Debian and Ubuntu
### New Sandstorm WeKan at 2026-07
Backup your current Sandstorm first !
## 1. Backup Sandstorm
1. Please first backup your Sandstorm https://docs.sandstorm.io/en/latest/administering/backups/
2. Please first download all of your grains to .zip files from top menu row down arrow button, just in case.
## 2. Upgrade Sandstorm WeKan
Backup your Sandstorm first ! See above !
This new Sandstorm WeKan uses fork of FerretDB v1 SQLite at https://github.com/wekan/FerretDB
- FerretDB is drop-in replacement for MongoDB 7 Server
+2 -2
View File
@@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
Right click and download files 1-4:
1. [wekan-9.97-win64.zip](https://github.com/wekan/wekan/releases/download/v9.97/wekan-9.97-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
1. [wekan-10.01-win64.zip](https://github.com/wekan/wekan/releases/download/v10.01/wekan-10.01-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
2. [node.exe](https://nodejs.org/dist/latest-v24.x/win-x64/node.exe)
@@ -22,7 +22,7 @@ Right click and download files 1-4:
6. Double click `mongodb-windows-x86_64-7.0.34-signed.msi` . In installer, uncheck downloading MongoDB compass.
7. Unzip `wekan-9.97-win64.zip` , inside it is directory `bundle`, to it copy other files:
7. Unzip `wekan-10.01-win64.zip` , inside it is directory `bundle`, to it copy other files:
```
bundle (directory)
+131
View File
@@ -0,0 +1,131 @@
# Select Color — theme categories + custom colors
Status: **Core implemented · picker UI design** · Owner: xet7 · Related: #5778 (global
per-user theme), Board Settings / Change Color, Member Settings / Change Color, #5514
(the native color-wheel `<input type="color">` already used for list colors).
WeKan ships ~19 board theme colors. Many are near-duplicates that differ only by **one or
two accent colors**; a few (`clearblue`) are **two-color "color slides"** (gradients). This
document defines how the **Select Color** picker — used in **both** Board Settings and
Member Settings (the global override) — is reorganized into **categories** with **two-level
dropdowns** and **custom colors** where it makes sense.
## 1. Categories
The colors partition into four categories (single source of truth:
`models/lib/themeCategories.js`, guarded by `tests/themeCategories.test.cjs` to exactly equal
`config/const.js` `ALLOWED_BOARD_COLORS`):
| Category | Colors | Custom colors |
|----------|--------|---------------|
| **flat** | belize, nephritis, pomegranate, pumpkin, wisteria, moderatepink, strongcyan, limegreen, natural | **1** (single accent) |
| **clear** | clearblue | **2** (color slide / gradient) |
| **dark** | midnight, dark, moderndark, exodark, cleandark | **none** (fixed) |
| **special** | relax, corteza, modern, cleanlight | **none** (fixed) |
Rationale: **flat** designs are one accent color over a flat surface, so a single custom color
fully re-skins them. **clear** designs are a two-color gradient ("slide"), so they take two
custom colors. **dark** and **special** are hand-tuned, multi-color designs where a naive
custom color would break contrast/readability — so they are **fixed** (pick the named theme, no
custom colors).
## 2. Picker UX — two-level dropdowns + color wheel
The Select Color popup (identical in Board Settings and Member Settings) is:
```
Category: [ flat ▼ ] <- 1st-level dropdown (themeCategories order)
Theme: [ belize ▼ ] <- 2nd-level dropdown (colors in the chosen category)
(flat) Custom color: [🎨 color wheel] <- shown only for flat
(clear) Colors: [🎨 wheel 1] [🎨 wheel 2] <- shown only for clear
(dark/special) — no custom color controls —
[ Preview swatch ] [ Save ] [ Default / Unset ]
```
- **1st dropdown** picks the category; **2nd dropdown** repopulates with that category's themes.
- The **color wheel(s)** are the native `<input type="color">` (the #5514 mechanism already used
for list colors — reuse `isHexColor`/`toHex` from `models/lib/contrastColor`). They are shown
**only** when `allowsCustomColor(category)` is true (flat → 1 wheel, clear → 2).
- Selecting a **dark**/**special** category hides the wheels entirely.
- A live **preview swatch** shows the resulting theme (named or custom).
- **Default / Unset**: Board → falls back to the instance default; Member → clears the global
override (the existing #5778 behaviour).
Member Settings adds nothing new structurally — it is the same picker writing to the user profile
instead of the board, and its result themes the whole UI (`board-color-<name>` on `<body>`/header,
see #5778).
## 3. Storage model
Named theme (today): `board.color` / `profile.globalThemeColor` = a color name.
Custom colors (new): stored alongside, as an ordered array of `#rrggbb` hex strings whose length
matches the category's `customColorCount`:
- Board: `board.customThemeColors: [String]` (1 for flat, 2 for clear).
- User: `profile.globalThemeCustomColors: [String]`.
A custom flat/clear theme is therefore `{ color: 'belize', customThemeColors: ['#123456'] }` — the
**named theme selects the CSS design**, and the **custom colors override its accent(s)**. Clearing
`customThemeColors` returns the theme to its stock colors.
## 4. Rendering custom colors — CSS variables (the remaining work)
Today each theme is a hardcoded class (`.board-color-belize { … #2980b9 … }`). Custom colors require
the flat/clear theme rules to be driven by **CSS custom properties** so arbitrary colors can be
injected inline:
1. Refactor the **flat** theme rules to reference `var(--theme-accent, <stock hex>)` (stock hex as
the fallback, so a theme with no custom color is unchanged).
2. Refactor the **clear** theme rules to reference `var(--theme-accent-1, …)` and
`var(--theme-accent-2, …)` (the two gradient stops).
3. When a custom theme is active, set those variables **inline** on the themed element:
- Board: on `#header` / the board wrapper (where `board-color-*` already goes).
- Global (#5778): on `<body>` / header via the `globalThemeColor.js` autorun.
e.g. `element.style.setProperty('--theme-accent', customThemeColors[0])`.
4. **dark**/**special** rules are left as-is (no variables, no custom colors).
This CSS refactor is the largest and most visual part; it is the natural next implementation step
and must be verified in a running app across the flat/clear themes.
## 5. Validation (security)
Custom colors are user input that ends up as a CSS value, so they are validated on the server before
storage. `models/lib/themeCategories.isValidCustomColors(color, customColors)` enforces:
- the named `color` belongs to a category that **allows** custom colors (flat/clear only), and
- `customColors` length equals that category's count, and
- every entry matches `^#[0-9a-fA-F]{6}$`.
The `setColor` (board) and `setGlobalThemeColor` (user, #5778) methods reject anything else, so no
arbitrary string can be injected as an inline style. Because only `#rrggbb` passes, there is no CSS
injection surface.
## 6. What is implemented now vs. next
- **Implemented:**
- `models/lib/themeCategories.js` — the categorization + helpers + `isValidCustomColors` (tested).
- The shared **two-level dropdown picker** `client/components/main/themeColorPicker.{jade,js}`
(category → theme + custom color wheels for flat/clear), used by **both** Board Settings
(`boardChangeColorPopup`) and Member Settings (`changeColorPopup`) via `scope="board"|"global"`.
- **Storage + validation**: `board.customThemeColors` and `profile.globalThemeCustomColors`
(each a validated `#rrggbb` array), written by `board.setColor(color, custom)` and the
`setGlobalThemeColor(color, custom)` method, both gated by `isValidCustomColors`.
- **Custom-color application**: `globalThemeColor.js` sets `--theme-accent` / `--theme-accent-2`
on `:root` (board's colors when on a board, the user's global override otherwise) and toggles
the `has-custom-theme-color` / `has-custom-theme-slide` body classes; `customTheme.css` consumes
them to recolor the header bars, primary buttons, and sidebar button.
- **Next (iterative):** broaden `customTheme.css` beyond the header/buttons to the full flat/clear
surface (minicards, pop-overs, board canvas), ideally by refactoring those theme rules to read the
`--theme-accent*` variables directly (§4) so custom colors cover everything a named theme does.
This part is CSS-heavy and best iterated in a running app.
## 7. Extension checklist (adding a theme color)
1. Add the color name to `config/const.js` `ALLOWED_BOARD_COLORS`.
2. Add it to the correct category in `models/lib/themeCategories.js` (the test enforces the union
stays equal to `ALLOWED_BOARD_COLORS`).
3. Add its `.board-color-<name>` CSS. If it is a **flat**/**clear** theme, drive its accent(s) with
the `--theme-accent[-1/-2]` variables so it supports custom colors for free.
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+165 -131
View File
@@ -1,7 +1,7 @@
{
"accept": "Accepter",
"activity-changedTitle": "ændrede titel til %s af %s",
"activity-changedDescription": "changed description of %s",
"activity-changedDescription": "ændrede beskrivelsen af %s",
"act-deleteCard": "slettede kortet __card__ fra listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-removeBoard": "slettede tavlen __board__",
"act-removeList": "slettede listen __list__ fra tavlen __board__",
@@ -109,6 +109,10 @@
"allboards.delete-workspace-confirm": "Er du sikker på, at du vil slette dette arbejdsområde?",
"allboards.delete-workspace-confirm-check": "Bekræft sletning af arbejdsområdet",
"multi-selection-active": "Klik på tjekboksene for at vælge tavler",
"selected-label": "Valgt:",
"set-selected-starred": "Stjernemarkér de valgte tavler",
"set-selected-home": "Angiv som hjemtavle (åbnes efter login)",
"home-board-badge": "Hjemtavle (åbnes efter login)",
"activity-dueDate": "Redigerede forfaldsdato til %s af %s",
"activity-endDate": "Redigerede slutdato til %s af %s",
"add-attachment": "Tilføj vedhæftning",
@@ -118,18 +122,18 @@
"add-card-to-top-of-list": "Tilføj kort øverst i listen",
"add-card-to-bottom-of-list": "Tilføj kort nederst i listen",
"addListPopup-title": "Tilføj liste",
"setListWidthPopup-title": "Set width",
"set-list-width": "Set width",
"set-list-width-value": "List width (pixels)",
"list-width-error-message": "List width must be an integer greater than 270",
"list-width-shared-note": "This width is shared with everyone on this board.",
"list-width-personal-note": "This width is only for you.",
"personal-list-width": "Personal list widths",
"personal-list-width-description": "When on, each user keeps their own list widths. When off, list widths are shared with everyone on the board.",
"fixed-list-width": "Same width for all lists",
"click-to-enable-fixed-list-width": "Same width for all lists disabled. Click to enable.",
"click-to-disable-fixed-list-width": "Same width for all lists enabled. Click to disable.",
"fixed-list-width-note": "When on, all lists have the same width, and resizing one list resizes all lists. This setting is only for you.",
"setListWidthPopup-title": "Angiv bredde",
"set-list-width": "Angiv bredde",
"set-list-width-value": "Listebredde (pixels)",
"list-width-error-message": "Listebredden skal være et heltal større end 270",
"list-width-shared-note": "Denne bredde deles med alle på denne tavle.",
"list-width-personal-note": "Denne bredde er kun for dig.",
"personal-list-width": "Personlige listebredder",
"personal-list-width-description": "Når slået til, beholder hver bruger sine egne listebredder. Når slået fra, deles listebredderne med alle på tavlen.",
"fixed-list-width": "Samme bredde for alle lister",
"click-to-enable-fixed-list-width": "Samme bredde for alle lister deaktiveret. Klik for at aktivere.",
"click-to-disable-fixed-list-width": "Samme bredde for alle lister aktiveret. Klik for at deaktivere.",
"fixed-list-width-note": "Når slået til, har alle lister samme bredde, og ændring af én listes bredde ændrer bredden på alle lister. Denne indstilling er kun for dig.",
"keyboard-shortcuts-enabled": "Tastaturgenveje aktiverede. Klik for at deaktivere.",
"keyboard-shortcuts-disabled": "Tastaturgenveje deaktiverede. Klik for at aktivere.",
"setSwimlaneHeightPopup-title": "Angiv højde på svømmebane",
@@ -212,6 +216,8 @@
"board-drag-drop-reorder-or-click-open": "Træk og slip for at arrangere tavleikoner. Klik på tavleikon for at åbne tavle.",
"board-open-and-move-between-remaining-and-workspaces": "Klik for at åbne tavle. Træk-og-slip for at tildele tavle til __workspaces__ (slip på Arbejdsområde i sidemenuen).",
"boardChangeColorPopup-title": "Skift tavlens baggrund",
"changeColorPopup-title": "Skift farve",
"changeFontPopup-title": "Skrifttype",
"boardChangeBackgroundImagePopup-title": "Skift baggrundsbillede",
"allBoardsChangeColorPopup-title": "Skift farve",
"allBoardsChangeBackgroundImagePopup-title": "Skift baggrundsbillede",
@@ -236,7 +242,7 @@
"board-view-collapse": "Sammenfold",
"board-view-gantt": "Gantt",
"board-view-lists": "Lister",
"board-view-table": "Table",
"board-view-table": "Tabel",
"bucket-example": "F.eks. \"Bucket List\"",
"calendar-previous-month-label": "Forgående måned",
"calendar-next-month-label": "Næste måned",
@@ -339,9 +345,30 @@
"cardType-linkedBoard": "Sammenkædet tavle",
"change": "Tilpas",
"change-avatar": "Tilpas avatar",
"change-color": "Skift farve",
"change-password": "Skift kodeord",
"change-permissions": "Tilpas tilladelser",
"change-settings": "Tilpas indstillinger",
"theme-default": "Standard (ingen tilsidesættelse)",
"theme": "Tema",
"theme-category": "Kategori",
"theme-category-flat": "Flad",
"theme-category-clear": "Ryd",
"theme-category-dark": "Mørk",
"theme-category-special": "Speciel",
"change-font": "Skrifttype",
"font": "Skrifttype",
"font-default": "Standard (ingen brugerdefineret skrifttype)",
"font-preview-text": "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon 0123456789",
"font-size": "Skriftstørrelse",
"font-size-default": "Standard",
"font-size-smaller": "Mindre",
"font-size-small": "Lille",
"font-size-large": "Stor",
"font-size-larger": "Større",
"font-size-largest": "Størst",
"text-color": "Tekstfarve",
"text-background-color": "Tekstbaggrundsfarve",
"changeAvatarPopup-title": "Tilpas avatar",
"delete-avatar-confirm": "Er du sikker på, at du vil slette denne avatar?",
"deleteAvatarPopup-title": "Slet avatar?",
@@ -422,6 +449,7 @@
"confirm-subtask-delete-popup": "Er du sikker på du vil slette delopgaven?",
"confirm-checklist-delete-popup": "Er du sikker på, at du vil slette tjeklisten?",
"confirm-checklist-item-delete-popup": "Er du sikker på, at du vil slette dette element på tjeklisten?",
"confirm-move-list-to-swimlane": "Flyt denne liste og alle dens kort til den anden svømmebane?",
"subtaskDeletePopup-title": "Slet delopgave?",
"checklistDeletePopup-title": "Slet tjeklisten?",
"checklistItemDeletePopup-title": "Slet element på tjeklisten?",
@@ -442,7 +470,7 @@
"createCustomField": "Opret felt",
"createCustomFieldPopup-title": "Opret felt",
"current": "nuværende",
"custom-color": "Custom color",
"custom-color": "Brugerdefineret farve",
"custom-field-delete-pop": "Du kan ikke fortryde handlingen. Dette vil fjerne dette brugerdefinerede felt fra alle kort og tilintetgøre dens historik.",
"custom-field-checkbox": "Afkrydsningsfelt",
"custom-field-currency": "Valuta",
@@ -510,8 +538,8 @@
"error-csv-schema": "Din CSV (Comma Separated Values)/TSV (Tab Separated Values) indeholder ikke gyldig information i korrekt format",
"error-import-empty-board": "Denne eksport indeholder intet tavleindhold (ingen svømmebaner, lister eller kort). Den kan være oprettet af en ældre WeKan-version med en kendt eksportfejl. Eksporter tavlen igen med den aktuelle version, og importer den nye fil.",
"error-list-doesNotExist": "Listen findes ikke",
"error-linked-card-not-allowed": "You can only link to a normal card. Linking to another linked card, or to a card that links back to this board, is not allowed because it would make the cards inaccessible.",
"error-user-disabled": "This user account is disabled",
"error-linked-card-not-allowed": "Du kan kun linke til et normalt kort. At linke til et andet sammenkædet kort eller til et kort, der linker tilbage til denne tavle, er ikke tilladt, da det ville gøre kortene utilgængelige.",
"error-user-disabled": "Denne brugerkonto er deaktiveret",
"error-user-doesNotExist": "Brugeren findes ikke",
"error-user-notAllowSelf": "Du kan ikke invitere dig selv",
"error-user-notCreated": "Brugeren er ikke oprettet",
@@ -584,12 +612,12 @@
"fullname": "Fuldt navn",
"header-logo-title": "Gå tilbage til siden med dine tavler",
"show-activities": "Visa aktiviteter",
"hide-activities": "Hide Activities",
"hide-activities": "Skjul aktiviteter",
"headerBarCreateBoardPopup-title": "Opret tavle",
"home": "Hjem",
"import": "Importér",
"imported-member-no-account": "Imported member — no matching account yet",
"inactive-member": "Inactive member",
"imported-member-no-account": "Importeret medlem — ingen matchende konto endnu",
"inactive-member": "Inaktivt medlem",
"impersonate-user": "Log ind som bruger",
"link": "Link",
"import-board": "importer tavle",
@@ -706,6 +734,7 @@
"members": "Medlemmer",
"menu": "Menu",
"move-selection": "Flyt valgte",
"copy-selection": "Kopiér valgte",
"moveCardPopup-title": "Flyt kort",
"moveListPopup-title": "Flytta lista",
"moveCardToBottom-title": "Flyt til bunden",
@@ -753,7 +782,7 @@
"remove-label": "Fjern etikette",
"listDeletePopup-title": "Slet liste?",
"remove-member": "Fjern medlem",
"deleted-user": "Deleted user",
"deleted-user": "Slettet bruger",
"remove-member-from-card": "Fjern fra kort",
"remove-member-pop": "Fjern __name__ (__username__) fra __boardTitle__? Medlemmet vil blive fjernet fra alle kort på denne tavle. De vil modtage en notifikation.",
"removeMemberPopup-title": "Fjern medlem?",
@@ -788,6 +817,8 @@
"sidebar-close": "Luk sidebjælke",
"signupPopup-title": "Opret en konto",
"star-board-title": "Klik for at stjernemarkere denne tavle. Den vil blive vist i toppen af din liste over tavler.",
"set-default-board-title": "Klik for at åbne denne tavle automatisk, efter du logger ind.",
"unset-default-board-title": "Denne tavle åbnes, efter du logger ind. Klik for at stoppe automatisk åbning.",
"starred-boards": "Tavler med stjerner",
"starred-boards-description": "Tavler med stjerner vises i toppen af din liste over tavler.",
"subscribe": "Abonnér",
@@ -900,12 +931,12 @@
"no-name": "(Ukendt)",
"Node_version": "Node-version",
"Meteor_version": "Meteor-version",
"Database_type": "Database type",
"Database_type": "Databasetype",
"Database_commit": "Database commit",
"FerretDB_version": "FerretDB version",
"FerretDB_version": "FerretDB-version",
"FerretDB_commit": "FerretDB commit",
"Reactivity_mode": "Reactivity mode (changeStreams / oplog / polling)",
"MongoDB_version": "MongoDB compatible version",
"Reactivity_mode": "Reaktivitetstilstand (changeStreams / oplog / polling)",
"MongoDB_version": "MongoDB-kompatibel version",
"MongoDB_storage_engine": "Lagringsmotor for MongoDB",
"MongoDB_Oplog_enabled": "MongoDB Oplog er slået til",
"OS_Arch": "OS - Arch",
@@ -985,10 +1016,10 @@
"cover-attachment-on-minicard": "Omslagsbild på minikort",
"badge-attachment-on-minicard": "Antal bilagor på minikort",
"card-sorting-by-number-on-minicard": "Sortering av kort efter nummer på minikort",
"checklist-count-on-minicard": "Checklist item count (0/0) on minicard",
"checklist-count": "Checklist item count (0/0)",
"attachment-count": "Count of attachments",
"cover-image": "Cover image",
"checklist-count-on-minicard": "Antal tjeklisteelementer (0/0) på minikort",
"checklist-count": "Antal tjeklisteelementer (0/0)",
"attachment-count": "Antal vedhæftninger",
"cover-image": "Coverbillede",
"prefix-with-full-path": "Præfiks med fuld sti",
"prefix-with-parent": "Præfiks med overordnede",
"subtext-with-full-path": "Undertekst med fuld sti",
@@ -1026,17 +1057,17 @@
"r-workflow-help": "Træk en trigger og en handling ind i byggeren, og tilføj derefter reglen. Træk en handling over på en eksisterende regel for at ændre, hvad den gør.",
"r-drop-trigger": "træk en udløser hertil",
"r-drop-action": "træk en handling hertil",
"r-w-card-created": "Card is created",
"r-w-card-archived": "Card is archived",
"r-w-card-unarchived": "Card is unarchived",
"r-w-label-added": "Any label is added",
"r-w-label-removed": "Any label is removed",
"r-w-member-added": "A member is added",
"r-w-member-removed": "A member is removed",
"r-w-checklist-added": "A checklist is added",
"r-w-attachment-added": "An attachment is added",
"r-w-every-day-at": "Every day at __time__",
"r-w-set-received-now": "Set received date to now",
"r-w-card-created": "Kort oprettes",
"r-w-card-archived": "Kort flyttes til arkiv",
"r-w-card-unarchived": "Kort genskabes fra arkiv",
"r-w-label-added": "En etikette tilføjes",
"r-w-label-removed": "En etikette fjernes",
"r-w-member-added": "Et medlem tilføjes",
"r-w-member-removed": "Et medlem fjernes",
"r-w-checklist-added": "En tjekliste tilføjes",
"r-w-attachment-added": "En vedhæftning tilføjes",
"r-w-every-day-at": "Hver dag kl. __time__",
"r-w-set-received-now": "Angiv modtaget dato til nu",
"r-export-json": "Eksporter regler til JSON",
"r-export-csv": "Eksporter regler til CSV",
"r-import-json": "Importer regler fra JSON",
@@ -1090,12 +1121,12 @@
"r-mark-incomplete": "Markér kort som ikke fuldført",
"r-move-all-cards": "Flyt alle kort i en liste",
"r-set-date-relative": "Angiv dato i forhold til dags dato",
"r-later": "later",
"r-unit-minutes": "minute(s)",
"r-unit-hours": "hour(s)",
"r-unit-days": "day(s)",
"r-unit-weeks": "week(s)",
"r-unit-months": "month(s)",
"r-later": "senere",
"r-unit-minutes": "minut(ter)",
"r-unit-hours": "time(r)",
"r-unit-days": "dag(e)",
"r-unit-weeks": "uge(r)",
"r-unit-months": "måned(er)",
"r-trigger": "Udløser",
"r-action": "Handling",
"r-when-a-card": "Når et kort",
@@ -1260,6 +1291,9 @@
"accounts-allowUserDelete": "Tillad brugere at slette deres egen konto",
"hide-minicard-label-text": "Skjul etiketteteksten for minikort",
"show-desktop-drag-handles": "Vis trække-håndtag for skrivebord",
"drag-to-resize-sidebar": "Træk for at ændre sidepanelets bredde",
"submit-on-enter": "Send editorer med Enter",
"submit-on-enter-description": "Når slået til gemmer et tryk på Enter kortets titel, beskrivelse og andre indbyggede editorer; Shift+Enter indsætter en ny linje. Når slået fra gemmer Ctrl/Cmd+Enter, og Enter indsætter en ny linje.",
"assignee": "Tildelt til",
"assignees": "Tilldelade",
"cardAssigneesPopup-title": "Tildelt til",
@@ -1341,7 +1375,7 @@
"myCardsViewChange-title": "Mina korts vy",
"myCardsViewChangePopup-title": "Mina korts vy",
"myCardsViewChange-choice-boards": "Tavler",
"myCardsViewChange-choice-table": "Table",
"myCardsViewChange-choice-table": "Tabel",
"myCardsSortChange-title": "Mina Tavlor sortering",
"myCardsSortChangePopup-title": "Mina Tavlor sortering",
"myCardsSortChange-choice-board": "På tavla",
@@ -1399,7 +1433,7 @@
"operator-limit": "gräns",
"operator-debug": "feilsøk",
"operator-org": "org",
"operator-team": "team",
"operator-team": "hold",
"operator-title": "titel",
"operator-description": "beskrivning",
"operator-customfield": "anpassat fält",
@@ -1479,11 +1513,11 @@
"label-names": "Märkningsnamn",
"archived-at": "arkiverad den",
"sort-cards": "Sortera kort",
"sort-boards": "Sort Boards",
"search-boards": "Search boards",
"sort-boards-custom": "Custom (drag order)",
"sort-boards-title-asc": "Title (A → Z)",
"sort-boards-title-desc": "Title (Z → A)",
"sort-boards": "Sorter tavler",
"search-boards": "Søg tavler",
"sort-boards-custom": "Brugerdefineret (trækkerækkefølge)",
"sort-boards-title-asc": "Titel (A → Z)",
"sort-boards-title-desc": "Titel (Z → A)",
"sort-is-on": "Sortering er slået til",
"cardsSortPopup-title": "Sortera kort",
"due-date": "Förfallodatum",
@@ -1557,10 +1591,10 @@
"rulesReportTitle": "Regelrapport",
"boardsReportTitle": "Tavlan rapport",
"cardsReportTitle": "Kort rapport",
"impersonationReportTitle": "Impersonation Report",
"impersonationReportTitle": "Rapport om brugerefterligning",
"impersonation-admin": "Administrator",
"impersonation-user": "Impersonated user",
"reason": "Reason",
"impersonation-user": "Efterlignet bruger",
"reason": "Årsag",
"copy-swimlane": "Kopiera simbana",
"copySwimlanePopup-title": "Kopiera simbana",
"display-card-creator": "Visa kortskaparen",
@@ -1721,7 +1755,7 @@
"invalid-file": "Om filnamnet är ogiltigt avbryts uppladdningen eller omdöps filen.",
"preview-pdf-not-supported": "Din enhet stöder inte förhandsgranskning av PDF. Försök istället att ladda ner.",
"drag-board": "Dra tavla",
"drag-board-to-workspace": "Drag board to assign to __workspaces__ (drop on workspace in sidebar)",
"drag-board-to-workspace": "Træk tavle for at tildele til __workspaces__ (slip på arbejdsområde i sidemenuen)",
"translation-number": "Antalet översatta fraser är:",
"delete-translation-confirm-popup": "Är du säker att du vill radera denna översatta fras? Det går inte att ångra.",
"newTranslationPopup-title": "Ny anspassad översättning",
@@ -1754,7 +1788,7 @@
"accounts-lockout-info": " Dessa inställningar styr hur inloggningsförsök skyddas mot brute force-attacker.",
"accounts-lockout-known-users": " Inställningar för kända användare (korrekt användarnamn, fel lösenord)",
"accounts-lockout-unknown-users": " Inställningar för okända användare (obefintligt användarnamn)",
"accounts-lockout-failures-before": "Failures before lockout",
"accounts-lockout-failures-before": "Mislykkede forsøg før udelukkelse",
"accounts-lockout-period": " Utelåsningsperiod (sekunder)",
"accounts-lockout-failure-window": " Tidsperiod för felräkning (sekunder)",
"accounts-lockout-settings-updated": " Inställningar för brute force-skydd har uppdaterats",
@@ -1841,82 +1875,82 @@
"azure-container": "Blobcontainer",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"database-migration": "Database migration",
"database-migration-description": "Migrate text-based data (everything except attachments and avatars, which stay on the filesystem) between MongoDB and FerretDB v1 (SQLite). The other database must be running and reachable (default ports: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; override with WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). After it finishes, point MONGO_URL at the other database and restart WeKan (on Snap: snap set wekan database=ferretdb or =mongodb).",
"database-migrate-to-ferretdb": "Migrate text data to FerretDB v1 (SQLite)",
"database-migrate-to-mongodb": "Migrate text data to MongoDB",
"database-migration-confirm": "Copy all text-based collections to __db__? Attachments and avatars on the filesystem are not affected. The target database must be running.",
"database-migration-phase": "Phase",
"database-migration-done": "Text-data migration completed.",
"sandstorm-migration-description": "When WeKan runs inside a Sandstorm grain, its old MongoDB 3 database is migrated to FerretDB v1 (SQLite) on first launch, and attachments and avatars are moved from the database to the grain's files/attachments and files/avatars directories. This shows whether that migration succeeded and how much disk space the old raw MongoDB database files still use inside the grain, and lets you delete those raw files to free space.",
"sandstorm-migration-status": "Migration status",
"sandstorm-migration-success": "Succeeded",
"sandstorm-migration-failed": "Failed",
"sandstorm-migration-pending": "Not migrated yet",
"database-migration": "Databasemigrering",
"database-migration-description": "Migrer tekstbaseret data (alt undtagen vedhæftninger og avatarer, som forbliver på filsystemet) mellem MongoDB og FerretDB v1 (SQLite). Den anden database skal køre og være tilgængelig (standardporte: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; tilsidesæt med WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). Når den er færdig, peg MONGO_URL mod den anden database og genstart WeKan (på Snap: snap set wekan database=ferretdb eller =mongodb).",
"database-migrate-to-ferretdb": "Migrer tekstdata til FerretDB v1 (SQLite)",
"database-migrate-to-mongodb": "Migrer tekstdata til MongoDB",
"database-migration-confirm": "Kopier alle tekstbaserede samlinger til __db__? Vedhæftninger og avatarer på filsystemet påvirkes ikke. Måldatabasen skal køre.",
"database-migration-phase": "Fase",
"database-migration-done": "Migrering af tekstdata er fuldført.",
"sandstorm-migration-description": "Når WeKan kører inde i et Sandstorm-grain, migreres dens gamle MongoDB 3-database til FerretDB v1 (SQLite) ved første start, og vedhæftninger og avatarer flyttes fra databasen til grainets mapper files/attachments og files/avatars. Dette viser, om migreringen lykkedes, og hvor meget diskplads de gamle rå MongoDB-databasefiler stadig bruger inde i grainet, og lader dig slette disse rå filer for at frigøre plads.",
"sandstorm-migration-status": "Migreringsstatus",
"sandstorm-migration-success": "Lykkedes",
"sandstorm-migration-failed": "Mislykkedes",
"sandstorm-migration-pending": "Ikke migreret endnu",
"sandstorm-storage-item": "Lagring",
"sandstorm-disk-usage": "Disk usage",
"sandstorm-raw-mongodb": "Raw MongoDB 3 database files",
"sandstorm-delete-raw-mongodb": "Delete raw MongoDB database files",
"sandstorm-delete-raw-mongodb-description": "After a successful migration the data lives in FerretDB and on the filesystem, so the old raw MongoDB 3 database files can be deleted to free grain disk space. This cannot be undone.",
"sandstorm-delete-raw-mongodb-confirm": "Delete the raw MongoDB 3 database files to free disk space? The data has already been migrated to FerretDB and the filesystem. This cannot be undone.",
"sandstorm-raw-mongodb-deleted": "Raw MongoDB database files deleted, freed",
"collections": "collections",
"features": "Features",
"features-performance": "Performance",
"cards-loading": "Card loading",
"cards-loading-all": "All cards (default)",
"cards-loading-lazy": "Lazy (only visible cards)",
"cards-loading-description": "How each board loads its cards. All cards: every card is loaded into the browser (simplest, all features work, but a board with thousands of cards sends a lot of data to each viewer). Lazy: each list loads only the cards currently visible (infinite scroll) plus a live count, so very large boards use less memory and bandwidth. Also set by the CARDS_LOADING environment variable.",
"cards-loading-lazy-note": "Lazy card loading is experimental: card counters and WIP limits are accurate, but the Calendar/Table/Gantt views and multi-select reflect only the cards loaded so far. Reload open boards after changing this.",
"features-security": "Security",
"render-links-as-plain-text": "Render links as plain text",
"render-links-as-plain-text-description": "When enabled, all links (both markdown links like [label](url) and raw HTML <a href> tags) are always shown as plain, non-clickable text in every rich text field (board and card titles, descriptions, comments, checklists, etc.). This prevents misleading clickable links. Default is off, so links stay clickable.",
"always-show-code-as-text": "Always show all code as plain text",
"always-show-code-as-text-description": "When enabled, rich text is never rendered as markdown or HTML: the entire source is always shown as plain text in every rich text field (board and card titles, descriptions, comments, checklists, etc.). This reveals hidden content — HTML comments (<!-- -->), the target of markdown links, JavaScript and any other code — so all code is always visible, not clickable, and not running. Default is off, so markdown is rendered normally.",
"sandstorm-disk-usage": "Diskforbrug",
"sandstorm-raw-mongodb": "Rå MongoDB 3-databasefiler",
"sandstorm-delete-raw-mongodb": "Slet rå MongoDB-databasefiler",
"sandstorm-delete-raw-mongodb-description": "Efter en vellykket migrering ligger dataene i FerretDB og på filsystemet, så de gamle rå MongoDB 3-databasefiler kan slettes for at frigøre diskplads i grainet. Dette kan ikke fortrydes.",
"sandstorm-delete-raw-mongodb-confirm": "Slet de rå MongoDB 3-databasefiler for at frigøre diskplads? Dataene er allerede migreret til FerretDB og filsystemet. Dette kan ikke fortrydes.",
"sandstorm-raw-mongodb-deleted": "Rå MongoDB-databasefiler slettet, frigjort",
"collections": "samlinger",
"features": "Funktioner",
"features-performance": "Ydeevne",
"cards-loading": "Indlæsning af kort",
"cards-loading-all": "Alle kort (standard)",
"cards-loading-lazy": "Doven (kun synlige kort)",
"cards-loading-description": "Hvordan hver tavle indlæser sine kort. Alle kort: hvert kort indlæses i browseren (enklest, alle funktioner virker, men en tavle med tusindvis af kort sender meget data til hver seer). Doven: hver liste indlæser kun de kort, der er synlige lige nu (uendelig rulning) plus et live-antal, så meget store tavler bruger mindre hukommelse og båndbredde. Angives også af miljøvariablen CARDS_LOADING.",
"cards-loading-lazy-note": "Doven indlæsning af kort er eksperimentel: korttællere og WIP-begrænsninger er præcise, men Kalender-/Tabel-/Gantt-visningerne og multivalg afspejler kun de kort, der er indlæst indtil videre. Genindlæs åbne tavler efter at have ændret dette.",
"features-security": "Sikkerhed",
"render-links-as-plain-text": "Vis links som almindelig tekst",
"render-links-as-plain-text-description": "Når slået til, vises alle links (både markdown-links som [label](url) og rå HTML <a href>-tags) altid som almindelig, ikke-klikbar tekst i alle rich text-felter (tavle- og korttitler, beskrivelser, kommentarer, tjeklister osv.). Dette forhindrer vildledende klikbare links. Standard er fra, så links forbliver klikbare.",
"always-show-code-as-text": "Vis altid al kode som almindelig tekst",
"always-show-code-as-text-description": "Når slået til, gengives rich text aldrig som markdown eller HTML: hele kildeteksten vises altid som almindelig tekst i alle rich text-felter (tavle- og korttitler, beskrivelser, kommentarer, tjeklister osv.). Dette afslører skjult indhold — HTML-kommentarer (<!-- -->), målet for markdown-links, JavaScript og enhver anden kode — så al kode altid er synlig, ikke klikbar og ikke kørende. Standard er fra, så markdown gengives normalt.",
"anonymized-user": "användare",
"disable-all-import": "Disable all import",
"disable-all-import-description": "When enabled, all import features are turned off: importing a board from WeKan JSON, Trello, CSV/Excel, Jira, Kanboard, NextCloud Deck, OpenProject, GitHub, GitLab, Gitea and Forgejo, as well as cloning a board. The import menu options are hidden and the server rejects any import request. Default is off.",
"disable-all-export": "Disable all export",
"disable-all-export-description": "When enabled, all export features are turned off: exporting a board as WeKan JSON, CSV, Excel, Kanboard, or to NextCloud Deck / OpenProject / GitHub / GitLab / Gitea / Forgejo, and single-attachment export. The export menu options are hidden and the server rejects any export request. Default is off.",
"disable-import-avatars": "Disable import avatars",
"disable-import-avatars-description": "When enabled, avatars (user profile pictures) are never imported. This covers WeKan JSON import, Trello import, and avatar sync from external identity providers on login (LDAP, OIDC/OAuth2). Names and other data are still imported; only avatars are skipped. Default is off.",
"disable-export-avatars": "Disable export avatars",
"disable-export-avatars-description": "When enabled, avatars (user profile pictures) are never included when exporting a board (WeKan JSON and CSV export). Usernames and other data are still exported; only avatars are omitted. Default is off.",
"anonymize-import-users": "Anonymize import users",
"anonymize-import-users-description": "When enabled, importing a WeKan JSON board replaces every imported user's username, full name and initials with counter placeholders (user1, user2, ...), drops their avatar, and rewrites @username mentions (and the requested-by / assigned-by fields) inside imported card and comment content. No real user identity from the source board is stored. The word \"user\" follows the language of the person importing. Default is off.",
"anonymize-export-users": "Anonymize export users",
"anonymize-export-users-description": "When enabled, exporting a board replaces every exported user's username, full name and initials with counter placeholders (user1, user2, ...), drops their avatar, and rewrites @username mentions (and the requested-by / assigned-by fields) inside card and comment content. The exported file carries no real user identity. The word \"user\" follows the language of the person exporting. Default is off.",
"disable-all-import": "Deaktiver al import",
"disable-all-import-description": "Når slået til, er alle importfunktioner deaktiveret: import af en tavle fra WeKan JSON, Trello, CSV/Excel, Jira, Kanboard, NextCloud Deck, OpenProject, GitHub, GitLab, Gitea og Forgejo samt kloning af en tavle. Importmenupunkterne er skjult, og serveren afviser enhver importanmodning. Standard er fra.",
"disable-all-export": "Deaktiver al eksport",
"disable-all-export-description": "Når slået til, er alle eksportfunktioner deaktiveret: eksport af en tavle som WeKan JSON, CSV, Excel, Kanboard eller til NextCloud Deck / OpenProject / GitHub / GitLab / Gitea / Forgejo samt eksport af enkelt vedhæftning. Eksportmenupunkterne er skjult, og serveren afviser enhver eksportanmodning. Standard er fra.",
"disable-import-avatars": "Deaktiver import af avatarer",
"disable-import-avatars-description": "Når slået til, importeres avatarer (brugerprofilbilleder) aldrig. Dette omfatter WeKan JSON-import, Trello-import og avatarsynkronisering fra eksterne identitetsudbydere ved login (LDAP, OIDC/OAuth2). Navne og andre data importeres stadig; kun avatarer springes over. Standard er fra.",
"disable-export-avatars": "Deaktiver eksport af avatarer",
"disable-export-avatars-description": "Når slået til, medtages avatarer (brugerprofilbilleder) aldrig ved eksport af en tavle (WeKan JSON- og CSV-eksport). Brugernavne og andre data eksporteres stadig; kun avatarer udelades. Standard er fra.",
"anonymize-import-users": "Anonymiser importerede brugere",
"anonymize-import-users-description": "Når slået til, erstatter import af en WeKan JSON-tavle hver importeret brugers brugernavn, fulde navn og initialer med tællepladsholdere (user1, user2, ...), fjerner deres avatar og omskriver @brugernavn-omtaler (samt felterne anmodet-af / tildelt-af) i importeret kort- og kommentarindhold. Ingen reel brugeridentitet fra kildetavlen gemmes. Ordet \"user\" følger sproget hos den person, der importerer. Standard er fra.",
"anonymize-export-users": "Anonymiser eksporterede brugere",
"anonymize-export-users-description": "Når slået til, erstatter eksport af en tavle hver eksporteret brugers brugernavn, fulde navn og initialer med tællepladsholdere (user1, user2, ...), fjerner deres avatar og omskriver @brugernavn-omtaler (samt felterne anmodet-af / tildelt-af) i kort- og kommentarindhold. Den eksporterede fil indeholder ingen reel brugeridentitet. Ordet \"user\" følger sproget hos den person, der eksporterer. Standard er fra.",
"features-notifications": "Notifikationer",
"disable-activities": "Disable all activities",
"disable-activities-description": "When enabled, activity-feed entries are never recorded and never shown anywhere (board sidebar and card activity tab). No history of who did what is kept. Default is off.",
"disable-notifications": "Disable all notifications",
"disable-notifications-description": "When enabled, WeKan never sends watch notifications (in-app, email or other services) for any activity. Activities can still be recorded (unless also disabled above); only the notifications are suppressed. Default is off.",
"disable-watch": "Disable watch",
"disable-watch-description": "When enabled, the watch feature is turned off — users cannot subscribe to boards, lists or cards to receive notifications, and any attempt to change a watch level is rejected. Default is off.",
"disable-activities": "Deaktiver alle aktiviteter",
"disable-activities-description": "Når slået til, registreres poster i aktivitetsfeedet aldrig og vises aldrig nogen steder (tavlens sidemenu og kortets aktivitetsfane). Der gemmes ingen historik over, hvem der gjorde hvad. Standard er fra.",
"disable-notifications": "Deaktiver alle notifikationer",
"disable-notifications-description": "Når slået til, sender WeKan aldrig følgenotifikationer (i appen, e-mail eller andre tjenester) for nogen aktivitet. Aktiviteter kan stadig registreres (medmindre de også er deaktiveret ovenfor); kun notifikationerne undertrykkes. Standard er fra.",
"disable-watch": "Deaktiver følgefunktion",
"disable-watch-description": "Når slået til, er følgefunktionen deaktiveret — brugere kan ikke abonnere på tavler, lister eller kort for at modtage notifikationer, og ethvert forsøg på at ændre et følgeniveau afvises. Standard er fra.",
"backup": "Backup",
"backup-description": "Back up Attachments, Avatars and/or Data (all text-based collections that are not attachments or avatars). The .zip is streamed directly to the selected storage — no temporary file — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip, containing YYYY_MM_DD-HH_MM_SS/attachments, /avatars and /data. Cloud storages (S3/MinIO, Azure, GCS) use the credentials configured in their storage tabs.",
"backup-data": "Data (text)",
"backup-storage": "Save to storage",
"backup-now": "Backup now",
"backup-done": "Backup completed",
"backup-schedule": "Scheduled backups",
"backup-frequency": "Frequency",
"backup-frequency-off": "Off",
"backup-frequency-daily": "Daily",
"backup-frequency-weekly": "Weekly",
"backup-frequency-monthly": "Monthly",
"backup-time": "Time (HH:MM)",
"backup-day-of-week": "Day of week (weekly)",
"backup-day-of-month": "Day of month (monthly, 1-28)",
"backup-description": "Tag backup af vedhæftninger, avatarer og/eller data (alle tekstbaserede samlinger, der ikke er vedhæftninger eller avatarer). .zip-filen streames direkte til det valgte lager — ingen midlertidig fil — som backup/YYYY/MM/DD/HH_MM_SS/backup.zip, der indeholder YYYY_MM_DD-HH_MM_SS/attachments, /avatars og /data. Cloud-lagre (S3/MinIO, Azure, GCS) bruger de legitimationsoplysninger, der er konfigureret i deres lagerfaner.",
"backup-data": "Data (tekst)",
"backup-storage": "Gem til lager",
"backup-now": "Tag backup nu",
"backup-done": "Backup fuldført",
"backup-schedule": "Planlagte backups",
"backup-frequency": "Hyppighed",
"backup-frequency-off": "Fra",
"backup-frequency-daily": "Dagligt",
"backup-frequency-weekly": "Ugentligt",
"backup-frequency-monthly": "Månedligt",
"backup-time": "Tidspunkt (TT:MM)",
"backup-day-of-week": "Ugedag (ugentligt)",
"backup-day-of-month": "Dag i måneden (månedligt, 1-28)",
"backup-restore": "Genskab",
"backup-list": "List backups",
"backup-datetime": "Date and time",
"backup-path": "File path",
"backup-restore-mode": "Restore mode",
"backup-restore-add-missing": "Add missing data only",
"backup-restore-replace-all": "Replace all data",
"backup-restore-select-first": "Select a backup from the list first.",
"backup-restore-confirm": "Restore the selected backup? Attachments/avatars are written to the filesystem and data to the database according to the chosen mode.",
"backup-list": "Vis backups",
"backup-datetime": "Dato og tid",
"backup-path": "Filsti",
"backup-restore-mode": "Gendannelsestilstand",
"backup-restore-add-missing": "Tilføj kun manglende data",
"backup-restore-replace-all": "Erstat alle data",
"backup-restore-select-first": "Vælg først en backup fra listen.",
"backup-restore-confirm": "Gendan den valgte backup? Vedhæftninger/avatarer skrives til filsystemet og data til databasen i henhold til den valgte tilstand.",
"gcs-project-id": "Projekt ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Sti til servicekontoens nøglefil",
@@ -2064,7 +2098,7 @@
"migration-progress-note": "Vänta medan vi migrerar din tavla till den senaste strukturen...",
"steps": "steg",
"view": "Visa",
"has-swimlanes": "Has Swimlanes",
"has-swimlanes": "Har svømmebaner",
"step-analyze-board-structure": "Analysera tavlans struktur",
"step-fix-orphaned-cards": "Fixa övergivna kort",
"step-convert-shared-lists": "Konvertera delade listor",
@@ -2073,7 +2107,7 @@
"step-fix-avatar-urls": "Fixa avatar-URL:er",
"step-fix-attachment-urls": "Fixa bilage-URL:er",
"step-analyze-lists": "Analysera listor",
"step-create-missing-lists": "Create Missing Lists",
"step-create-missing-lists": "Opret manglende lister",
"step-update-cards": "Uppdatera kort",
"step-finalize": "Slutför",
"step-delete-duplicate-empty-lists": "Ta bort duplicerade tomma listor",
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+34
View File
@@ -109,6 +109,10 @@
"allboards.delete-workspace-confirm": "Are you sure you want to delete this workspace?",
"allboards.delete-workspace-confirm-check": "Confirm workspace deletion",
"multi-selection-active": "Click checkboxes to select boards",
"selected-label": "Selected:",
"set-selected-starred": "Star the selected boards",
"set-selected-home": "Set as Home board (opened after login)",
"home-board-badge": "Home board (opened after login)",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@@ -212,6 +216,8 @@
"board-drag-drop-reorder-or-click-open": "Drag and drop to reorder board icons. Click board icon to open board.",
"board-open-and-move-between-remaining-and-workspaces": "Click to open board. Use drag handle to assign board to __workspaces__ (drop on workspace in sidebar).",
"boardChangeColorPopup-title": "Change Board Background",
"changeColorPopup-title": "Change Color",
"changeFontPopup-title": "Font",
"boardChangeBackgroundImagePopup-title": "Change Background Image",
"allBoardsChangeColorPopup-title": "Change color",
"allBoardsChangeBackgroundImagePopup-title": "Change Background Image",
@@ -339,9 +345,30 @@
"cardType-linkedBoard": "Linked Board",
"change": "Change",
"change-avatar": "Change Avatar",
"change-color": "Change Color",
"change-password": "Change Password",
"change-permissions": "Change permissions",
"change-settings": "Change Settings",
"theme-default": "Default (no override)",
"theme": "Theme",
"theme-category": "Category",
"theme-category-flat": "Flat",
"theme-category-clear": "Clear",
"theme-category-dark": "Dark",
"theme-category-special": "Special",
"change-font": "Font",
"font": "Font",
"font-default": "Default (no custom font)",
"font-preview-text": "The quick brown fox jumps over the lazy dog 0123456789",
"font-size": "Font size",
"font-size-default": "Default",
"font-size-smaller": "Smaller",
"font-size-small": "Small",
"font-size-large": "Large",
"font-size-larger": "Larger",
"font-size-largest": "Largest",
"text-color": "Text color",
"text-background-color": "Text background color",
"changeAvatarPopup-title": "Change Avatar",
"delete-avatar-confirm": "Are you sure you want to delete this avatar?",
"deleteAvatarPopup-title": "Delete Avatar?",
@@ -422,6 +449,7 @@
"confirm-subtask-delete-popup": "Are you sure you want to delete subtask?",
"confirm-checklist-delete-popup": "Are you sure you want to delete the checklist?",
"confirm-checklist-item-delete-popup": "Are you sure you want to delete the checklist item?",
"confirm-move-list-to-swimlane": "Move this list and all its cards to the other swimlane?",
"subtaskDeletePopup-title": "Delete Subtask?",
"checklistDeletePopup-title": "Delete Checklist?",
"checklistItemDeletePopup-title": "Delete Checklist Item?",
@@ -706,6 +734,7 @@
"members": "Members",
"menu": "Menu",
"move-selection": "Move selection",
"copy-selection": "Copy selection",
"moveCardPopup-title": "Move Card",
"moveListPopup-title": "Move List",
"moveCardToBottom-title": "Move to Bottom",
@@ -788,6 +817,8 @@
"sidebar-close": "Close Sidebar",
"signupPopup-title": "Create an Account",
"star-board-title": "Click to star this board. It will show up at top of your boards list.",
"set-default-board-title": "Click to open this board automatically after you log in.",
"unset-default-board-title": "This board opens after you log in. Click to stop opening it automatically.",
"starred-boards": "Starred Boards",
"starred-boards-description": "Starred boards show up at the top of your boards list.",
"subscribe": "Subscribe",
@@ -1260,6 +1291,9 @@
"accounts-allowUserDelete": "Allow users to self delete their account",
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"drag-to-resize-sidebar": "Drag to resize sidebar",
"submit-on-enter": "Submit editors with Enter",
"submit-on-enter-description": "When on, pressing Enter saves the card title, description and other inline editors; Shift+Enter inserts a new line. When off, Ctrl/Cmd+Enter saves and Enter inserts a new line.",
"assignee": "Assignee",
"assignees": "Assignees",
"cardAssigneesPopup-title": "Assignee",
+34
View File
@@ -109,6 +109,10 @@
"allboards.delete-workspace-confirm": "Are you sure you want to delete this workspace?",
"allboards.delete-workspace-confirm-check": "Confirm workspace deletion",
"multi-selection-active": "Click checkboxes to select boards",
"selected-label": "Selected:",
"set-selected-starred": "Star the selected boards",
"set-selected-home": "Set as Home board (opened after login)",
"home-board-badge": "Home board (opened after login)",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@@ -212,6 +216,8 @@
"board-drag-drop-reorder-or-click-open": "Drag and drop to reorder board icons. Click board icon to open board.",
"board-open-and-move-between-remaining-and-workspaces": "Click to open board. Use drag handle to assign board to __workspaces__ (drop on workspace in sidebar).",
"boardChangeColorPopup-title": "Change Board Background",
"changeColorPopup-title": "Change Color",
"changeFontPopup-title": "Font",
"boardChangeBackgroundImagePopup-title": "Change Background Image",
"allBoardsChangeColorPopup-title": "Change color",
"allBoardsChangeBackgroundImagePopup-title": "Change Background Image",
@@ -339,9 +345,30 @@
"cardType-linkedBoard": "Linked Board",
"change": "Change",
"change-avatar": "Change Avatar",
"change-color": "Change Color",
"change-password": "Change Password",
"change-permissions": "Change permissions",
"change-settings": "Change Settings",
"theme-default": "Default (no override)",
"theme": "Theme",
"theme-category": "Category",
"theme-category-flat": "Flat",
"theme-category-clear": "Clear",
"theme-category-dark": "Dark",
"theme-category-special": "Special",
"change-font": "Font",
"font": "Font",
"font-default": "Default (no custom font)",
"font-preview-text": "The quick brown fox jumps over the lazy dog 0123456789",
"font-size": "Font size",
"font-size-default": "Default",
"font-size-smaller": "Smaller",
"font-size-small": "Small",
"font-size-large": "Large",
"font-size-larger": "Larger",
"font-size-largest": "Largest",
"text-color": "Text color",
"text-background-color": "Text background color",
"changeAvatarPopup-title": "Change Avatar",
"delete-avatar-confirm": "Are you sure you want to delete this avatar?",
"deleteAvatarPopup-title": "Delete Avatar?",
@@ -422,6 +449,7 @@
"confirm-subtask-delete-popup": "Are you sure you want to delete subtask?",
"confirm-checklist-delete-popup": "Are you sure you want to delete the checklist?",
"confirm-checklist-item-delete-popup": "Are you sure you want to delete the checklist item?",
"confirm-move-list-to-swimlane": "Move this list and all its cards to the other swimlane?",
"subtaskDeletePopup-title": "Delete Subtask?",
"checklistDeletePopup-title": "Delete Checklist?",
"checklistItemDeletePopup-title": "Delete Checklist Item?",
@@ -706,6 +734,7 @@
"members": "Members",
"menu": "Menu",
"move-selection": "Move selection",
"copy-selection": "Copy selection",
"moveCardPopup-title": "Move Card",
"moveListPopup-title": "Move List",
"moveCardToBottom-title": "Move to Bottom",
@@ -788,6 +817,8 @@
"sidebar-close": "Close Sidebar",
"signupPopup-title": "Create an Account",
"star-board-title": "Click to star this board. It will show up at top of your boards list.",
"set-default-board-title": "Click to open this board automatically after you log in.",
"unset-default-board-title": "This board opens after you log in. Click to stop opening it automatically.",
"starred-boards": "Starred Boards",
"starred-boards-description": "Starred boards show up at the top of your boards list.",
"subscribe": "Subscribe",
@@ -1260,6 +1291,9 @@
"accounts-allowUserDelete": "Allow users to self delete their account",
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"drag-to-resize-sidebar": "Drag to resize sidebar",
"submit-on-enter": "Submit editors with Enter",
"submit-on-enter-description": "When on, pressing Enter saves the card title, description and other inline editors; Shift+Enter inserts a new line. When off, Ctrl/Cmd+Enter saves and Enter inserts a new line.",
"assignee": "Assignee",
"assignees": "Assignees",
"cardAssigneesPopup-title": "Assignee",
+34
View File
@@ -109,6 +109,10 @@
"allboards.delete-workspace-confirm": "Are you sure you want to delete this workspace?",
"allboards.delete-workspace-confirm-check": "Confirm workspace deletion",
"multi-selection-active": "Click checkboxes to select boards",
"selected-label": "Selected:",
"set-selected-starred": "Star the selected boards",
"set-selected-home": "Set as Home board (opened after login)",
"home-board-badge": "Home board (opened after login)",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@@ -212,6 +216,8 @@
"board-drag-drop-reorder-or-click-open": "Drag and drop to reorder board icons. Click board icon to open board.",
"board-open-and-move-between-remaining-and-workspaces": "Click to open board. Use drag handle to assign board to __workspaces__ (drop on workspace in sidebar).",
"boardChangeColorPopup-title": "Change Board Background",
"changeColorPopup-title": "Change Color",
"changeFontPopup-title": "Font",
"boardChangeBackgroundImagePopup-title": "Change Background Image",
"allBoardsChangeColorPopup-title": "Change color",
"allBoardsChangeBackgroundImagePopup-title": "Change Background Image",
@@ -339,9 +345,30 @@
"cardType-linkedBoard": "Linked Board",
"change": "Change",
"change-avatar": "Change Avatar",
"change-color": "Change Color",
"change-password": "Change Password",
"change-permissions": "Change permissions",
"change-settings": "Change Settings",
"theme-default": "Default (no override)",
"theme": "Theme",
"theme-category": "Category",
"theme-category-flat": "Flat",
"theme-category-clear": "Clear",
"theme-category-dark": "Dark",
"theme-category-special": "Special",
"change-font": "Font",
"font": "Font",
"font-default": "Default (no custom font)",
"font-preview-text": "The quick brown fox jumps over the lazy dog 0123456789",
"font-size": "Font size",
"font-size-default": "Default",
"font-size-smaller": "Smaller",
"font-size-small": "Small",
"font-size-large": "Large",
"font-size-larger": "Larger",
"font-size-largest": "Largest",
"text-color": "Text color",
"text-background-color": "Text background color",
"changeAvatarPopup-title": "Change Avatar",
"delete-avatar-confirm": "Are you sure you want to delete this avatar?",
"deleteAvatarPopup-title": "Delete Avatar?",
@@ -422,6 +449,7 @@
"confirm-subtask-delete-popup": "Are you sure you want to delete subtask?",
"confirm-checklist-delete-popup": "Are you sure you want to delete the checklist?",
"confirm-checklist-item-delete-popup": "Are you sure you want to delete the checklist item?",
"confirm-move-list-to-swimlane": "Move this list and all its cards to the other swimlane?",
"subtaskDeletePopup-title": "Delete Subtask?",
"checklistDeletePopup-title": "Delete Checklist?",
"checklistItemDeletePopup-title": "Delete Checklist Item?",
@@ -706,6 +734,7 @@
"members": "Members",
"menu": "Menu",
"move-selection": "Move selection",
"copy-selection": "Copy selection",
"moveCardPopup-title": "Move Card",
"moveListPopup-title": "Move List",
"moveCardToBottom-title": "Move to Bottom",
@@ -788,6 +817,8 @@
"sidebar-close": "Close Sidebar",
"signupPopup-title": "Create an Account",
"star-board-title": "Click to star this board. It will show up at top of your boards list.",
"set-default-board-title": "Click to open this board automatically after you log in.",
"unset-default-board-title": "This board opens after you log in. Click to stop opening it automatically.",
"starred-boards": "Starred Boards",
"starred-boards-description": "Starred boards show up at the top of your boards list.",
"subscribe": "Subscribe",
@@ -1260,6 +1291,9 @@
"accounts-allowUserDelete": "Allow users to self delete their account",
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"drag-to-resize-sidebar": "Drag to resize sidebar",
"submit-on-enter": "Submit editors with Enter",
"submit-on-enter-description": "When on, pressing Enter saves the card title, description and other inline editors; Shift+Enter inserts a new line. When off, Ctrl/Cmd+Enter saves and Enter inserts a new line.",
"assignee": "Assignee",
"assignees": "Assignees",
"cardAssigneesPopup-title": "Assignee",
+34
View File
@@ -109,6 +109,10 @@
"allboards.delete-workspace-confirm": "Are you sure you want to delete this workspace?",
"allboards.delete-workspace-confirm-check": "Confirm workspace deletion",
"multi-selection-active": "Click checkboxes to select boards",
"selected-label": "Selected:",
"set-selected-starred": "Star the selected boards",
"set-selected-home": "Set as Home board (opened after login)",
"home-board-badge": "Home board (opened after login)",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@@ -212,6 +216,8 @@
"board-drag-drop-reorder-or-click-open": "Drag and drop to reorder board icons. Click board icon to open board.",
"board-open-and-move-between-remaining-and-workspaces": "Click to open board. Use drag handle to assign board to __workspaces__ (drop on workspace in sidebar).",
"boardChangeColorPopup-title": "Change Board Background",
"changeColorPopup-title": "Change Color",
"changeFontPopup-title": "Font",
"boardChangeBackgroundImagePopup-title": "Change Background Image",
"allBoardsChangeColorPopup-title": "Change color",
"allBoardsChangeBackgroundImagePopup-title": "Change Background Image",
@@ -339,9 +345,30 @@
"cardType-linkedBoard": "Linked Board",
"change": "Change",
"change-avatar": "Change Avatar",
"change-color": "Change Color",
"change-password": "Change Password",
"change-permissions": "Change permissions",
"change-settings": "Change Settings",
"theme-default": "Default (no override)",
"theme": "Theme",
"theme-category": "Category",
"theme-category-flat": "Flat",
"theme-category-clear": "Clear",
"theme-category-dark": "Dark",
"theme-category-special": "Special",
"change-font": "Font",
"font": "Font",
"font-default": "Default (no custom font)",
"font-preview-text": "The quick brown fox jumps over the lazy dog 0123456789",
"font-size": "Font size",
"font-size-default": "Default",
"font-size-smaller": "Smaller",
"font-size-small": "Small",
"font-size-large": "Large",
"font-size-larger": "Larger",
"font-size-largest": "Largest",
"text-color": "Text color",
"text-background-color": "Text background color",
"changeAvatarPopup-title": "Change Avatar",
"delete-avatar-confirm": "Are you sure you want to delete this avatar?",
"deleteAvatarPopup-title": "Delete Avatar?",
@@ -422,6 +449,7 @@
"confirm-subtask-delete-popup": "Are you sure you want to delete subtask?",
"confirm-checklist-delete-popup": "Are you sure you want to delete the checklist?",
"confirm-checklist-item-delete-popup": "Are you sure you want to delete the checklist item?",
"confirm-move-list-to-swimlane": "Move this list and all its cards to the other swimlane?",
"subtaskDeletePopup-title": "Delete Subtask?",
"checklistDeletePopup-title": "Delete Checklist?",
"checklistItemDeletePopup-title": "Delete Checklist Item?",
@@ -706,6 +734,7 @@
"members": "Members",
"menu": "Menu",
"move-selection": "Move selection",
"copy-selection": "Copy selection",
"moveCardPopup-title": "Move Card",
"moveListPopup-title": "Move List",
"moveCardToBottom-title": "Move to Bottom",
@@ -788,6 +817,8 @@
"sidebar-close": "Close Sidebar",
"signupPopup-title": "Create an Account",
"star-board-title": "Click to star this board. It will show up at top of your boards list.",
"set-default-board-title": "Click to open this board automatically after you log in.",
"unset-default-board-title": "This board opens after you log in. Click to stop opening it automatically.",
"starred-boards": "Starred Boards",
"starred-boards-description": "Starred boards show up at the top of your boards list.",
"subscribe": "Subscribe",
@@ -1260,6 +1291,9 @@
"accounts-allowUserDelete": "Allow users to self delete their account",
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"drag-to-resize-sidebar": "Drag to resize sidebar",
"submit-on-enter": "Submit editors with Enter",
"submit-on-enter-description": "When on, pressing Enter saves the card title, description and other inline editors; Shift+Enter inserts a new line. When off, Ctrl/Cmd+Enter saves and Enter inserts a new line.",
"assignee": "Assignee",
"assignees": "Assignees",
"cardAssigneesPopup-title": "Assignee",
+34
View File
@@ -109,6 +109,10 @@
"allboards.delete-workspace-confirm": "Are you sure you want to delete this workspace?",
"allboards.delete-workspace-confirm-check": "Confirm workspace deletion",
"multi-selection-active": "Click checkboxes to select boards",
"selected-label": "Selected:",
"set-selected-starred": "Star the selected boards",
"set-selected-home": "Set as Home board (opened after login)",
"home-board-badge": "Home board (opened after login)",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@@ -212,6 +216,8 @@
"board-drag-drop-reorder-or-click-open": "Drag and drop to reorder board icons. Click board icon to open board.",
"board-open-and-move-between-remaining-and-workspaces": "Click to open board. Use drag handle to assign board to __workspaces__ (drop on workspace in sidebar).",
"boardChangeColorPopup-title": "Change Board Background",
"changeColorPopup-title": "Change Color",
"changeFontPopup-title": "Font",
"boardChangeBackgroundImagePopup-title": "Change Background Image",
"allBoardsChangeColorPopup-title": "Change color",
"allBoardsChangeBackgroundImagePopup-title": "Change Background Image",
@@ -339,9 +345,30 @@
"cardType-linkedBoard": "Linked Board",
"change": "Change",
"change-avatar": "Change Avatar",
"change-color": "Change Color",
"change-password": "Change Password",
"change-permissions": "Change permissions",
"change-settings": "Change Settings",
"theme-default": "Default (no override)",
"theme": "Theme",
"theme-category": "Category",
"theme-category-flat": "Flat",
"theme-category-clear": "Clear",
"theme-category-dark": "Dark",
"theme-category-special": "Special",
"change-font": "Font",
"font": "Font",
"font-default": "Default (no custom font)",
"font-preview-text": "The quick brown fox jumps over the lazy dog 0123456789",
"font-size": "Font size",
"font-size-default": "Default",
"font-size-smaller": "Smaller",
"font-size-small": "Small",
"font-size-large": "Large",
"font-size-larger": "Larger",
"font-size-largest": "Largest",
"text-color": "Text color",
"text-background-color": "Text background color",
"changeAvatarPopup-title": "Change Avatar",
"delete-avatar-confirm": "Are you sure you want to delete this avatar?",
"deleteAvatarPopup-title": "Delete Avatar?",
@@ -422,6 +449,7 @@
"confirm-subtask-delete-popup": "Are you sure you want to delete subtask?",
"confirm-checklist-delete-popup": "Are you sure you want to delete the checklist?",
"confirm-checklist-item-delete-popup": "Are you sure you want to delete the checklist item?",
"confirm-move-list-to-swimlane": "Move this list and all its cards to the other swimlane?",
"subtaskDeletePopup-title": "Delete Subtask?",
"checklistDeletePopup-title": "Delete Checklist?",
"checklistItemDeletePopup-title": "Delete Checklist Item?",
@@ -706,6 +734,7 @@
"members": "Members",
"menu": "Menu",
"move-selection": "Move selection",
"copy-selection": "Copy selection",
"moveCardPopup-title": "Move Card",
"moveListPopup-title": "Move List",
"moveCardToBottom-title": "Move to Bottom",
@@ -788,6 +817,8 @@
"sidebar-close": "Close Sidebar",
"signupPopup-title": "Create an Account",
"star-board-title": "Click to star this board. It will show up at top of your boards list.",
"set-default-board-title": "Click to open this board automatically after you log in.",
"unset-default-board-title": "This board opens after you log in. Click to stop opening it automatically.",
"starred-boards": "Starred Boards",
"starred-boards-description": "Starred boards show up at the top of your boards list.",
"subscribe": "Subscribe",
@@ -1260,6 +1291,9 @@
"accounts-allowUserDelete": "Allow users to self delete their account",
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"drag-to-resize-sidebar": "Drag to resize sidebar",
"submit-on-enter": "Submit editors with Enter",
"submit-on-enter-description": "When on, pressing Enter saves the card title, description and other inline editors; Shift+Enter inserts a new line. When off, Ctrl/Cmd+Enter saves and Enter inserts a new line.",
"assignee": "Assignee",
"assignees": "Assignees",
"cardAssigneesPopup-title": "Assignee",
+34
View File
@@ -109,6 +109,10 @@
"allboards.delete-workspace-confirm": "Are you sure you want to delete this workspace?",
"allboards.delete-workspace-confirm-check": "Confirm workspace deletion",
"multi-selection-active": "Click checkboxes to select boards",
"selected-label": "Selected:",
"set-selected-starred": "Star the selected boards",
"set-selected-home": "Set as Home board (opened after login)",
"home-board-badge": "Home board (opened after login)",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@@ -212,6 +216,8 @@
"board-drag-drop-reorder-or-click-open": "Drag and drop to reorder board icons. Click board icon to open board.",
"board-open-and-move-between-remaining-and-workspaces": "Click to open board. Use drag handle to assign board to __workspaces__ (drop on workspace in sidebar).",
"boardChangeColorPopup-title": "Change Board Background",
"changeColorPopup-title": "Change Color",
"changeFontPopup-title": "Font",
"boardChangeBackgroundImagePopup-title": "Change Background Image",
"allBoardsChangeColorPopup-title": "Change color",
"allBoardsChangeBackgroundImagePopup-title": "Change Background Image",
@@ -339,9 +345,30 @@
"cardType-linkedBoard": "Linked Board",
"change": "Change",
"change-avatar": "Change Avatar",
"change-color": "Change Color",
"change-password": "Change Password",
"change-permissions": "Change permissions",
"change-settings": "Change Settings",
"theme-default": "Default (no override)",
"theme": "Theme",
"theme-category": "Category",
"theme-category-flat": "Flat",
"theme-category-clear": "Clear",
"theme-category-dark": "Dark",
"theme-category-special": "Special",
"change-font": "Font",
"font": "Font",
"font-default": "Default (no custom font)",
"font-preview-text": "The quick brown fox jumps over the lazy dog 0123456789",
"font-size": "Font size",
"font-size-default": "Default",
"font-size-smaller": "Smaller",
"font-size-small": "Small",
"font-size-large": "Large",
"font-size-larger": "Larger",
"font-size-largest": "Largest",
"text-color": "Text color",
"text-background-color": "Text background color",
"changeAvatarPopup-title": "Change Avatar",
"delete-avatar-confirm": "Are you sure you want to delete this avatar?",
"deleteAvatarPopup-title": "Delete Avatar?",
@@ -422,6 +449,7 @@
"confirm-subtask-delete-popup": "Are you sure you want to delete subtask?",
"confirm-checklist-delete-popup": "Are you sure you want to delete the checklist?",
"confirm-checklist-item-delete-popup": "Are you sure you want to delete the checklist item?",
"confirm-move-list-to-swimlane": "Move this list and all its cards to the other swimlane?",
"subtaskDeletePopup-title": "Delete Subtask?",
"checklistDeletePopup-title": "Delete Checklist?",
"checklistItemDeletePopup-title": "Delete Checklist Item?",
@@ -706,6 +734,7 @@
"members": "Members",
"menu": "Menu",
"move-selection": "Move selection",
"copy-selection": "Copy selection",
"moveCardPopup-title": "Move Card",
"moveListPopup-title": "Move List",
"moveCardToBottom-title": "Move to Bottom",
@@ -788,6 +817,8 @@
"sidebar-close": "Close Sidebar",
"signupPopup-title": "Create an Account",
"star-board-title": "Click to star this board. It will show up at top of your boards list.",
"set-default-board-title": "Click to open this board automatically after you log in.",
"unset-default-board-title": "This board opens after you log in. Click to stop opening it automatically.",
"starred-boards": "Starred Boards",
"starred-boards-description": "Starred boards show up at the top of your boards list.",
"subscribe": "Subscribe",
@@ -1260,6 +1291,9 @@
"accounts-allowUserDelete": "Allow users to self delete their account",
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"drag-to-resize-sidebar": "Drag to resize sidebar",
"submit-on-enter": "Submit editors with Enter",
"submit-on-enter-description": "When on, pressing Enter saves the card title, description and other inline editors; Shift+Enter inserts a new line. When off, Ctrl/Cmd+Enter saves and Enter inserts a new line.",
"assignee": "Assignee",
"assignees": "Assignees",
"cardAssigneesPopup-title": "Assignee",
+34
View File
@@ -109,6 +109,10 @@
"allboards.delete-workspace-confirm": "Are you sure you want to delete this workspace?",
"allboards.delete-workspace-confirm-check": "Confirm workspace deletion",
"multi-selection-active": "Click checkboxes to select boards",
"selected-label": "Selected:",
"set-selected-starred": "Star the selected boards",
"set-selected-home": "Set as Home board (opened after login)",
"home-board-badge": "Home board (opened after login)",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@@ -212,6 +216,8 @@
"board-drag-drop-reorder-or-click-open": "Drag and drop to reorder board icons. Click board icon to open board.",
"board-open-and-move-between-remaining-and-workspaces": "Click to open board. Use drag handle to assign board to __workspaces__ (drop on workspace in sidebar).",
"boardChangeColorPopup-title": "Change Board Background",
"changeColorPopup-title": "Change Color",
"changeFontPopup-title": "Font",
"boardChangeBackgroundImagePopup-title": "Change Background Image",
"allBoardsChangeColorPopup-title": "Change color",
"allBoardsChangeBackgroundImagePopup-title": "Change Background Image",
@@ -339,9 +345,30 @@
"cardType-linkedBoard": "Linked Board",
"change": "Change",
"change-avatar": "Change Avatar",
"change-color": "Change Color",
"change-password": "Change Password",
"change-permissions": "Change permissions",
"change-settings": "Change Settings",
"theme-default": "Default (no override)",
"theme": "Theme",
"theme-category": "Category",
"theme-category-flat": "Flat",
"theme-category-clear": "Clear",
"theme-category-dark": "Dark",
"theme-category-special": "Special",
"change-font": "Font",
"font": "Font",
"font-default": "Default (no custom font)",
"font-preview-text": "The quick brown fox jumps over the lazy dog 0123456789",
"font-size": "Font size",
"font-size-default": "Default",
"font-size-smaller": "Smaller",
"font-size-small": "Small",
"font-size-large": "Large",
"font-size-larger": "Larger",
"font-size-largest": "Largest",
"text-color": "Text color",
"text-background-color": "Text background color",
"changeAvatarPopup-title": "Change Avatar",
"delete-avatar-confirm": "Are you sure you want to delete this avatar?",
"deleteAvatarPopup-title": "Delete Avatar?",
@@ -422,6 +449,7 @@
"confirm-subtask-delete-popup": "Are you sure you want to delete subtask?",
"confirm-checklist-delete-popup": "Are you sure you want to delete the checklist?",
"confirm-checklist-item-delete-popup": "Are you sure you want to delete the checklist item?",
"confirm-move-list-to-swimlane": "Move this list and all its cards to the other swimlane?",
"subtaskDeletePopup-title": "Delete Subtask?",
"checklistDeletePopup-title": "Delete Checklist?",
"checklistItemDeletePopup-title": "Delete Checklist Item?",
@@ -706,6 +734,7 @@
"members": "Members",
"menu": "Menu",
"move-selection": "Move selection",
"copy-selection": "Copy selection",
"moveCardPopup-title": "Move Card",
"moveListPopup-title": "Move List",
"moveCardToBottom-title": "Move to Bottom",
@@ -788,6 +817,8 @@
"sidebar-close": "Close Sidebar",
"signupPopup-title": "Create an Account",
"star-board-title": "Click to star this board. It will show up at top of your boards list.",
"set-default-board-title": "Click to open this board automatically after you log in.",
"unset-default-board-title": "This board opens after you log in. Click to stop opening it automatically.",
"starred-boards": "Starred Boards",
"starred-boards-description": "Starred boards show up at the top of your boards list.",
"subscribe": "Subscribe",
@@ -1260,6 +1291,9 @@
"accounts-allowUserDelete": "Allow users to self delete their account",
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"drag-to-resize-sidebar": "Drag to resize sidebar",
"submit-on-enter": "Submit editors with Enter",
"submit-on-enter-description": "When on, pressing Enter saves the card title, description and other inline editors; Shift+Enter inserts a new line. When off, Ctrl/Cmd+Enter saves and Enter inserts a new line.",
"assignee": "Assignee",
"assignees": "Assignees",
"cardAssigneesPopup-title": "Assignee",
+34
View File
@@ -109,6 +109,10 @@
"allboards.delete-workspace-confirm": "Are you sure you want to delete this workspace?",
"allboards.delete-workspace-confirm-check": "Confirm workspace deletion",
"multi-selection-active": "Click checkboxes to select boards",
"selected-label": "Selected:",
"set-selected-starred": "Star the selected boards",
"set-selected-home": "Set as Home board (opened after login)",
"home-board-badge": "Home board (opened after login)",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@@ -212,6 +216,8 @@
"board-drag-drop-reorder-or-click-open": "Drag and drop to reorder board icons. Click board icon to open board.",
"board-open-and-move-between-remaining-and-workspaces": "Click to open board. Use drag handle to assign board to __workspaces__ (drop on workspace in sidebar).",
"boardChangeColorPopup-title": "Change Board Background",
"changeColorPopup-title": "Change Color",
"changeFontPopup-title": "Font",
"boardChangeBackgroundImagePopup-title": "Change Background Image",
"allBoardsChangeColorPopup-title": "Change color",
"allBoardsChangeBackgroundImagePopup-title": "Change Background Image",
@@ -339,9 +345,30 @@
"cardType-linkedBoard": "Linked Board",
"change": "Change",
"change-avatar": "Change Avatar",
"change-color": "Change Color",
"change-password": "Change Password",
"change-permissions": "Change permissions",
"change-settings": "Change Settings",
"theme-default": "Default (no override)",
"theme": "Theme",
"theme-category": "Category",
"theme-category-flat": "Flat",
"theme-category-clear": "Clear",
"theme-category-dark": "Dark",
"theme-category-special": "Special",
"change-font": "Font",
"font": "Font",
"font-default": "Default (no custom font)",
"font-preview-text": "The quick brown fox jumps over the lazy dog 0123456789",
"font-size": "Font size",
"font-size-default": "Default",
"font-size-smaller": "Smaller",
"font-size-small": "Small",
"font-size-large": "Large",
"font-size-larger": "Larger",
"font-size-largest": "Largest",
"text-color": "Text color",
"text-background-color": "Text background color",
"changeAvatarPopup-title": "Change Avatar",
"delete-avatar-confirm": "Are you sure you want to delete this avatar?",
"deleteAvatarPopup-title": "Delete Avatar?",
@@ -422,6 +449,7 @@
"confirm-subtask-delete-popup": "Are you sure you want to delete subtask?",
"confirm-checklist-delete-popup": "Are you sure you want to delete the checklist?",
"confirm-checklist-item-delete-popup": "Are you sure you want to delete the checklist item?",
"confirm-move-list-to-swimlane": "Move this list and all its cards to the other swimlane?",
"subtaskDeletePopup-title": "Delete Subtask?",
"checklistDeletePopup-title": "Delete Checklist?",
"checklistItemDeletePopup-title": "Delete Checklist Item?",
@@ -706,6 +734,7 @@
"members": "Members",
"menu": "Menu",
"move-selection": "Move selection",
"copy-selection": "Copy selection",
"moveCardPopup-title": "Move Card",
"moveListPopup-title": "Move List",
"moveCardToBottom-title": "Move to Bottom",
@@ -788,6 +817,8 @@
"sidebar-close": "Close Sidebar",
"signupPopup-title": "Create an Account",
"star-board-title": "Click to star this board. It will show up at top of your boards list.",
"set-default-board-title": "Click to open this board automatically after you log in.",
"unset-default-board-title": "This board opens after you log in. Click to stop opening it automatically.",
"starred-boards": "Starred Boards",
"starred-boards-description": "Starred boards show up at the top of your boards list.",
"subscribe": "Subscribe",
@@ -1260,6 +1291,9 @@
"accounts-allowUserDelete": "Allow users to self delete their account",
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"drag-to-resize-sidebar": "Drag to resize sidebar",
"submit-on-enter": "Submit editors with Enter",
"submit-on-enter-description": "When on, pressing Enter saves the card title, description and other inline editors; Shift+Enter inserts a new line. When off, Ctrl/Cmd+Enter saves and Enter inserts a new line.",
"assignee": "Assignee",
"assignees": "Assignees",
"cardAssigneesPopup-title": "Assignee",
+34
View File
@@ -109,6 +109,10 @@
"allboards.delete-workspace-confirm": "Are you sure you want to delete this workspace?",
"allboards.delete-workspace-confirm-check": "Confirm workspace deletion",
"multi-selection-active": "Click checkboxes to select boards",
"selected-label": "Selected:",
"set-selected-starred": "Star the selected boards",
"set-selected-home": "Set as Home board (opened after login)",
"home-board-badge": "Home board (opened after login)",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@@ -212,6 +216,8 @@
"board-drag-drop-reorder-or-click-open": "Drag and drop to reorder board icons. Click board icon to open board.",
"board-open-and-move-between-remaining-and-workspaces": "Click to open board. Use drag handle to assign board to __workspaces__ (drop on workspace in sidebar).",
"boardChangeColorPopup-title": "Change Board Background",
"changeColorPopup-title": "Change Color",
"changeFontPopup-title": "Font",
"boardChangeBackgroundImagePopup-title": "Change Background Image",
"allBoardsChangeColorPopup-title": "Change color",
"allBoardsChangeBackgroundImagePopup-title": "Change Background Image",
@@ -339,9 +345,30 @@
"cardType-linkedBoard": "Linked Board",
"change": "Change",
"change-avatar": "Change Avatar",
"change-color": "Change Color",
"change-password": "Change Password",
"change-permissions": "Change permissions",
"change-settings": "Change Settings",
"theme-default": "Default (no override)",
"theme": "Theme",
"theme-category": "Category",
"theme-category-flat": "Flat",
"theme-category-clear": "Clear",
"theme-category-dark": "Dark",
"theme-category-special": "Special",
"change-font": "Font",
"font": "Font",
"font-default": "Default (no custom font)",
"font-preview-text": "The quick brown fox jumps over the lazy dog 0123456789",
"font-size": "Font size",
"font-size-default": "Default",
"font-size-smaller": "Smaller",
"font-size-small": "Small",
"font-size-large": "Large",
"font-size-larger": "Larger",
"font-size-largest": "Largest",
"text-color": "Text color",
"text-background-color": "Text background color",
"changeAvatarPopup-title": "Change Avatar",
"delete-avatar-confirm": "Are you sure you want to delete this avatar?",
"deleteAvatarPopup-title": "Delete Avatar?",
@@ -422,6 +449,7 @@
"confirm-subtask-delete-popup": "Are you sure you want to delete subtask?",
"confirm-checklist-delete-popup": "Are you sure you want to delete the checklist?",
"confirm-checklist-item-delete-popup": "Are you sure you want to delete the checklist item?",
"confirm-move-list-to-swimlane": "Move this list and all its cards to the other swimlane?",
"subtaskDeletePopup-title": "Delete Subtask?",
"checklistDeletePopup-title": "Delete Checklist?",
"checklistItemDeletePopup-title": "Delete Checklist Item?",
@@ -706,6 +734,7 @@
"members": "Members",
"menu": "Menu",
"move-selection": "Move selection",
"copy-selection": "Copy selection",
"moveCardPopup-title": "Move Card",
"moveListPopup-title": "Move List",
"moveCardToBottom-title": "Move to Bottom",
@@ -788,6 +817,8 @@
"sidebar-close": "Close Sidebar",
"signupPopup-title": "Create an Account",
"star-board-title": "Click to star this board. It will show up at top of your boards list.",
"set-default-board-title": "Click to open this board automatically after you log in.",
"unset-default-board-title": "This board opens after you log in. Click to stop opening it automatically.",
"starred-boards": "Starred Boards",
"starred-boards-description": "Starred boards show up at the top of your boards list.",
"subscribe": "Subscribe",
@@ -1260,6 +1291,9 @@
"accounts-allowUserDelete": "Allow users to self delete their account",
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"drag-to-resize-sidebar": "Drag to resize sidebar",
"submit-on-enter": "Submit editors with Enter",
"submit-on-enter-description": "When on, pressing Enter saves the card title, description and other inline editors; Shift+Enter inserts a new line. When off, Ctrl/Cmd+Enter saves and Enter inserts a new line.",
"assignee": "Assignee",
"assignees": "Assignees",
"cardAssigneesPopup-title": "Assignee",
+34
View File
@@ -109,6 +109,10 @@
"allboards.delete-workspace-confirm": "Are you sure you want to delete this workspace?",
"allboards.delete-workspace-confirm-check": "Confirm workspace deletion",
"multi-selection-active": "Click checkboxes to select boards",
"selected-label": "Selected:",
"set-selected-starred": "Star the selected boards",
"set-selected-home": "Set as Home board (opened after login)",
"home-board-badge": "Home board (opened after login)",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@@ -212,6 +216,8 @@
"board-drag-drop-reorder-or-click-open": "Drag and drop to reorder board icons. Click board icon to open board.",
"board-open-and-move-between-remaining-and-workspaces": "Click to open board. Use drag handle to assign board to __workspaces__ (drop on workspace in sidebar).",
"boardChangeColorPopup-title": "Change Board Background",
"changeColorPopup-title": "Change Color",
"changeFontPopup-title": "Font",
"boardChangeBackgroundImagePopup-title": "Change Background Image",
"allBoardsChangeColorPopup-title": "Change color",
"allBoardsChangeBackgroundImagePopup-title": "Change Background Image",
@@ -339,9 +345,30 @@
"cardType-linkedBoard": "Linked Board",
"change": "Change",
"change-avatar": "Change Avatar",
"change-color": "Change Color",
"change-password": "Change Password",
"change-permissions": "Change permissions",
"change-settings": "Change Settings",
"theme-default": "Default (no override)",
"theme": "Theme",
"theme-category": "Category",
"theme-category-flat": "Flat",
"theme-category-clear": "Clear",
"theme-category-dark": "Dark",
"theme-category-special": "Special",
"change-font": "Font",
"font": "Font",
"font-default": "Default (no custom font)",
"font-preview-text": "The quick brown fox jumps over the lazy dog 0123456789",
"font-size": "Font size",
"font-size-default": "Default",
"font-size-smaller": "Smaller",
"font-size-small": "Small",
"font-size-large": "Large",
"font-size-larger": "Larger",
"font-size-largest": "Largest",
"text-color": "Text color",
"text-background-color": "Text background color",
"changeAvatarPopup-title": "Change Avatar",
"delete-avatar-confirm": "Are you sure you want to delete this avatar?",
"deleteAvatarPopup-title": "Delete Avatar?",
@@ -422,6 +449,7 @@
"confirm-subtask-delete-popup": "Are you sure you want to delete subtask?",
"confirm-checklist-delete-popup": "Are you sure you want to delete the checklist?",
"confirm-checklist-item-delete-popup": "Are you sure you want to delete the checklist item?",
"confirm-move-list-to-swimlane": "Move this list and all its cards to the other swimlane?",
"subtaskDeletePopup-title": "Delete Subtask?",
"checklistDeletePopup-title": "Delete Checklist?",
"checklistItemDeletePopup-title": "Delete Checklist Item?",
@@ -706,6 +734,7 @@
"members": "Members",
"menu": "Menu",
"move-selection": "Move selection",
"copy-selection": "Copy selection",
"moveCardPopup-title": "Move Card",
"moveListPopup-title": "Move List",
"moveCardToBottom-title": "Move to Bottom",
@@ -788,6 +817,8 @@
"sidebar-close": "Close Sidebar",
"signupPopup-title": "Create an Account",
"star-board-title": "Click to star this board. It will show up at top of your boards list.",
"set-default-board-title": "Click to open this board automatically after you log in.",
"unset-default-board-title": "This board opens after you log in. Click to stop opening it automatically.",
"starred-boards": "Starred Boards",
"starred-boards-description": "Starred boards show up at the top of your boards list.",
"subscribe": "Subscribe",
@@ -1260,6 +1291,9 @@
"accounts-allowUserDelete": "Allow users to self delete their account",
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"drag-to-resize-sidebar": "Drag to resize sidebar",
"submit-on-enter": "Submit editors with Enter",
"submit-on-enter-description": "When on, pressing Enter saves the card title, description and other inline editors; Shift+Enter inserts a new line. When off, Ctrl/Cmd+Enter saves and Enter inserts a new line.",
"assignee": "Assignee",
"assignees": "Assignees",
"cardAssigneesPopup-title": "Assignee",
+34
View File
@@ -109,6 +109,10 @@
"allboards.delete-workspace-confirm": "Are you sure you want to delete this workspace?",
"allboards.delete-workspace-confirm-check": "Confirm workspace deletion",
"multi-selection-active": "Click checkboxes to select boards",
"selected-label": "Selected:",
"set-selected-starred": "Star the selected boards",
"set-selected-home": "Set as Home board (opened after login)",
"home-board-badge": "Home board (opened after login)",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@@ -212,6 +216,8 @@
"board-drag-drop-reorder-or-click-open": "Drag and drop to reorder board icons. Click board icon to open board.",
"board-open-and-move-between-remaining-and-workspaces": "Click to open board. Use drag handle to assign board to __workspaces__ (drop on workspace in sidebar).",
"boardChangeColorPopup-title": "Change Board Background",
"changeColorPopup-title": "Change Color",
"changeFontPopup-title": "Font",
"boardChangeBackgroundImagePopup-title": "Change Background Image",
"allBoardsChangeColorPopup-title": "Change color",
"allBoardsChangeBackgroundImagePopup-title": "Change Background Image",
@@ -339,9 +345,30 @@
"cardType-linkedBoard": "Linked Board",
"change": "Change",
"change-avatar": "Change Avatar",
"change-color": "Change Color",
"change-password": "Change Password",
"change-permissions": "Change permissions",
"change-settings": "Change Settings",
"theme-default": "Default (no override)",
"theme": "Theme",
"theme-category": "Category",
"theme-category-flat": "Flat",
"theme-category-clear": "Clear",
"theme-category-dark": "Dark",
"theme-category-special": "Special",
"change-font": "Font",
"font": "Font",
"font-default": "Default (no custom font)",
"font-preview-text": "The quick brown fox jumps over the lazy dog 0123456789",
"font-size": "Font size",
"font-size-default": "Default",
"font-size-smaller": "Smaller",
"font-size-small": "Small",
"font-size-large": "Large",
"font-size-larger": "Larger",
"font-size-largest": "Largest",
"text-color": "Text color",
"text-background-color": "Text background color",
"changeAvatarPopup-title": "Change Avatar",
"delete-avatar-confirm": "Are you sure you want to delete this avatar?",
"deleteAvatarPopup-title": "Delete Avatar?",
@@ -422,6 +449,7 @@
"confirm-subtask-delete-popup": "Are you sure you want to delete subtask?",
"confirm-checklist-delete-popup": "Are you sure you want to delete the checklist?",
"confirm-checklist-item-delete-popup": "Are you sure you want to delete the checklist item?",
"confirm-move-list-to-swimlane": "Move this list and all its cards to the other swimlane?",
"subtaskDeletePopup-title": "Delete Subtask?",
"checklistDeletePopup-title": "Delete Checklist?",
"checklistItemDeletePopup-title": "Delete Checklist Item?",
@@ -706,6 +734,7 @@
"members": "Members",
"menu": "Menu",
"move-selection": "Move selection",
"copy-selection": "Copy selection",
"moveCardPopup-title": "Move Card",
"moveListPopup-title": "Move List",
"moveCardToBottom-title": "Move to Bottom",
@@ -788,6 +817,8 @@
"sidebar-close": "Close Sidebar",
"signupPopup-title": "Create an Account",
"star-board-title": "Click to star this board. It will show up at top of your boards list.",
"set-default-board-title": "Click to open this board automatically after you log in.",
"unset-default-board-title": "This board opens after you log in. Click to stop opening it automatically.",
"starred-boards": "Starred Boards",
"starred-boards-description": "Starred boards show up at the top of your boards list.",
"subscribe": "Subscribe",
@@ -1260,6 +1291,9 @@
"accounts-allowUserDelete": "Allow users to self delete their account",
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"drag-to-resize-sidebar": "Drag to resize sidebar",
"submit-on-enter": "Submit editors with Enter",
"submit-on-enter-description": "When on, pressing Enter saves the card title, description and other inline editors; Shift+Enter inserts a new line. When off, Ctrl/Cmd+Enter saves and Enter inserts a new line.",
"assignee": "Assignee",
"assignees": "Assignees",
"cardAssigneesPopup-title": "Assignee",
+34
View File
@@ -109,6 +109,10 @@
"allboards.delete-workspace-confirm": "Are you sure you want to delete this workspace?",
"allboards.delete-workspace-confirm-check": "Confirm workspace deletion",
"multi-selection-active": "Click checkboxes to select boards",
"selected-label": "Selected:",
"set-selected-starred": "Star the selected boards",
"set-selected-home": "Set as Home board (opened after login)",
"home-board-badge": "Home board (opened after login)",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@@ -212,6 +216,8 @@
"board-drag-drop-reorder-or-click-open": "Drag and drop to reorder board icons. Click board icon to open board.",
"board-open-and-move-between-remaining-and-workspaces": "Click to open board. Use drag handle to assign board to __workspaces__ (drop on workspace in sidebar).",
"boardChangeColorPopup-title": "Change Board Background",
"changeColorPopup-title": "Change Color",
"changeFontPopup-title": "Font",
"boardChangeBackgroundImagePopup-title": "Change Background Image",
"allBoardsChangeColorPopup-title": "Change color",
"allBoardsChangeBackgroundImagePopup-title": "Change Background Image",
@@ -339,9 +345,30 @@
"cardType-linkedBoard": "Linked Board",
"change": "Change",
"change-avatar": "Change Avatar",
"change-color": "Change Color",
"change-password": "Change Password",
"change-permissions": "Change permissions",
"change-settings": "Change Settings",
"theme-default": "Default (no override)",
"theme": "Theme",
"theme-category": "Category",
"theme-category-flat": "Flat",
"theme-category-clear": "Clear",
"theme-category-dark": "Dark",
"theme-category-special": "Special",
"change-font": "Font",
"font": "Font",
"font-default": "Default (no custom font)",
"font-preview-text": "The quick brown fox jumps over the lazy dog 0123456789",
"font-size": "Font size",
"font-size-default": "Default",
"font-size-smaller": "Smaller",
"font-size-small": "Small",
"font-size-large": "Large",
"font-size-larger": "Larger",
"font-size-largest": "Largest",
"text-color": "Text color",
"text-background-color": "Text background color",
"changeAvatarPopup-title": "Change Avatar",
"delete-avatar-confirm": "Are you sure you want to delete this avatar?",
"deleteAvatarPopup-title": "Delete Avatar?",
@@ -422,6 +449,7 @@
"confirm-subtask-delete-popup": "Are you sure you want to delete subtask?",
"confirm-checklist-delete-popup": "Are you sure you want to delete the checklist?",
"confirm-checklist-item-delete-popup": "Are you sure you want to delete the checklist item?",
"confirm-move-list-to-swimlane": "Move this list and all its cards to the other swimlane?",
"subtaskDeletePopup-title": "Delete Subtask?",
"checklistDeletePopup-title": "Delete Checklist?",
"checklistItemDeletePopup-title": "Delete Checklist Item?",
@@ -706,6 +734,7 @@
"members": "Members",
"menu": "Menu",
"move-selection": "Move selection",
"copy-selection": "Copy selection",
"moveCardPopup-title": "Move Card",
"moveListPopup-title": "Move List",
"moveCardToBottom-title": "Move to Bottom",
@@ -788,6 +817,8 @@
"sidebar-close": "Close Sidebar",
"signupPopup-title": "Create an Account",
"star-board-title": "Click to star this board. It will show up at top of your boards list.",
"set-default-board-title": "Click to open this board automatically after you log in.",
"unset-default-board-title": "This board opens after you log in. Click to stop opening it automatically.",
"starred-boards": "Starred Boards",
"starred-boards-description": "Starred boards show up at the top of your boards list.",
"subscribe": "Subscribe",
@@ -1260,6 +1291,9 @@
"accounts-allowUserDelete": "Allow users to self delete their account",
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"drag-to-resize-sidebar": "Drag to resize sidebar",
"submit-on-enter": "Submit editors with Enter",
"submit-on-enter-description": "When on, pressing Enter saves the card title, description and other inline editors; Shift+Enter inserts a new line. When off, Ctrl/Cmd+Enter saves and Enter inserts a new line.",
"assignee": "Assignee",
"assignees": "Assignees",
"cardAssigneesPopup-title": "Assignee",
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Loaded 100 of 278 files, more files were not shown because too many files have changed in this diff. Show more