mirror of
https://github.com/wekan/wekan.git
synced 2026-11-08 03:22:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
add47dc065 | ||
|
|
5af16df201 | ||
|
|
adaecfe8c2 | ||
|
|
08cfe2d788 | ||
|
|
ef6b2794b8 | ||
|
|
1284afc588 | ||
|
|
354b7804b5 | ||
|
|
72042b7d79 | ||
|
|
b8a21e4ce3 | ||
|
|
b71461d90c | ||
|
|
b8bb004650 | ||
|
|
bb6b0d5477 | ||
|
|
e0991d6602 | ||
|
|
077e2196f5 | ||
|
|
e010c09e87 | ||
|
|
356b1152bb | ||
|
|
66a2f8ddc8 | ||
|
|
ef81c25232 | ||
|
|
2be9253839 | ||
|
|
2cf878215d | ||
|
|
c00f24d668 | ||
|
|
95552881fc | ||
|
|
5b3394af2f | ||
|
|
d55c5e2c3c | ||
|
|
01d55828d2 | ||
|
|
219011e5eb | ||
|
|
91ccef062b | ||
|
|
97feb986a8 | ||
|
|
a184a3a717 | ||
|
|
75ec34e837 | ||
|
|
c795d13450 | ||
|
|
ea4a10c212 | ||
|
|
7691829cdb | ||
|
|
63bda22faa | ||
|
|
a9f2e9f50c | ||
|
|
861ded439e | ||
|
|
687a4af861 | ||
|
|
d67d27f0d8 | ||
|
|
3736f8431a | ||
|
|
01088ea9d4 | ||
|
|
5e014d0967 | ||
|
|
c87b4cb510 | ||
|
|
8dd6bd9db5 | ||
|
|
8cb4050cf2 | ||
|
|
a4cc0c2182 | ||
|
|
98a03bf045 | ||
|
|
4dc09c62b9 | ||
|
|
1c8ba2afc3 | ||
|
|
b027594ca8 | ||
|
|
08a2e5525e | ||
|
|
3d190ca022 | ||
|
|
aa15e72682 | ||
|
|
bd8e417be1 | ||
|
|
fc0649a756 | ||
|
|
969f96d742 | ||
|
|
0f87c3bf8b | ||
|
|
834698bb5e | ||
|
|
cd7f9f7bc9 | ||
|
|
519c04b8ef | ||
|
|
3f5173b960 | ||
|
|
c21edb160b | ||
|
|
cb80198d97 | ||
|
|
035195f2b4 | ||
|
|
197254fe16 | ||
|
|
aafe71fd19 | ||
|
|
15e756f4e0 | ||
|
|
97b0240ca4 | ||
|
|
68723ff43b | ||
|
|
cfd53cc969 | ||
|
|
bd72f5d64c | ||
|
|
e33e50d9c8 | ||
|
|
7718118654 | ||
|
|
772ae061f1 | ||
|
|
c6f960bbe1 | ||
|
|
ac20cd6442 | ||
|
|
c6aa769bc0 | ||
|
|
7699cc1e90 | ||
|
|
1312fefb12 | ||
|
|
1008561517 | ||
|
|
1cd32001d4 | ||
|
|
0535215225 | ||
|
|
8aacc22360 | ||
|
|
437c64c9a0 | ||
|
|
061eba8b7a | ||
|
|
176c5ef2d8 | ||
|
|
9a0c0613a5 | ||
|
|
22c7d5376b | ||
|
|
3c9524f61a | ||
|
|
d0a1e43f36 | ||
|
|
49285dad19 | ||
|
|
eab1546b88 | ||
|
|
46924985f8 | ||
|
|
58344db0d7 | ||
|
|
e246403bb6 | ||
|
|
e9e7071744 | ||
|
|
8287865b23 | ||
|
|
e8ced84172 | ||
|
|
9be41ab34b | ||
|
|
f3354f5cc8 | ||
|
|
3d1276fc1f | ||
|
|
765d04bf7c | ||
|
|
4ca33d29f7 | ||
|
|
f6f021f8cc | ||
|
|
26319c974b | ||
|
|
50dd05ceee | ||
|
|
6d5a594b07 | ||
|
|
ae29555862 | ||
|
|
c4784225d5 | ||
|
|
5730f7dcc5 | ||
|
|
b29a47778f | ||
|
|
7b7fada1b8 | ||
|
|
41c747ef5f | ||
|
|
99299c2a91 | ||
|
|
c29cf3b985 | ||
|
|
b1d33c5362 | ||
|
|
a76f7d28e3 | ||
|
|
d7f36d70a0 | ||
|
|
63a8657e0a | ||
|
|
b846e6d565 | ||
|
|
580276079e | ||
|
|
9a387acce0 | ||
|
|
7374b7e00d | ||
|
|
e15c920872 | ||
|
|
d1ac53b367 | ||
|
|
c951b40e5e | ||
|
|
be6ebb2503 | ||
|
|
d6ddcffcc2 |
No files matched your search
File renamed without changes.
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
| {{_ '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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) || []);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}}")
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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' }}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
+1038
-1004
File diff suppressed because it is too large.
Load diff
+508
-474
File diff suppressed because it is too large.
Load diff
+508
-474
File diff suppressed because it is too large.
Load diff
+641
-607
File diff suppressed because it is too large.
Load diff
+1150
-1116
File diff suppressed because it is too large.
Load diff
+1149
-1115
File diff suppressed because it is too large.
Load diff
+1150
-1116
File diff suppressed because it is too large.
Load diff
+856
-822
File diff suppressed because it is too large.
Load diff
+504
-470
File diff suppressed because it is too large.
Load diff
+504
-470
File diff suppressed because it is too large.
Load diff
+504
-470
File diff suppressed because it is too large.
Load diff
+2136
-2102
File diff suppressed because it is too large.
Load diff
+599
-565
File diff suppressed because it is too large.
Load diff
+659
-625
File diff suppressed because it is too large.
Load diff
+990
-956
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+991
-957
File diff suppressed because it is too large.
Load diff
+505
-471
File diff suppressed because it is too large.
Load diff
+1115
-1081
File diff suppressed because it is too large.
Load diff
+1115
-1081
File diff suppressed because it is too large.
Load diff
+506
-472
File diff suppressed because it is too large.
Load diff
+506
-472
File diff suppressed because it is too large.
Load diff
+165
-131
@@ -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",
|
||||
|
||||
+622
-588
File diff suppressed because it is too large.
Load diff
+621
-587
File diff suppressed because it is too large.
Load diff
+528
-494
File diff suppressed because it is too large.
Load diff
+623
-589
File diff suppressed because it is too large.
Load diff
+621
-587
File diff suppressed because it is too large.
Load diff
+621
-587
File diff suppressed because it is too large.
Load diff
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+605
-571
File diff suppressed because it is too large.
Load diff
+925
-891
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
Reference in new issue
Block a user