Compare commits

...
24 Commits
Author SHA1 Message Date
github-actions[bot] ac39a9a6b0 Bump versions for v13.13 2026-11-15 01:36:09 +00:00
Lauri Ojansivu 59d3fc9690 Prepare v13.13 release 2026-11-15 03:35:08 +02:00
Lauri Ojansivu 9e970b87fa Updated ChangeLog.
Thanks to ChrisMagnuson, dependabot and xet7 !
2026-11-15 03:11:44 +02:00
Lauri Ojansivu e46d7a2376 Destination pickers list the cards of a lazily loaded board.
Copy Checklist's card dropdown stayed empty for a board with more cards
than the lazy-loading threshold (CARDS_LOADING=auto above 500, or lazy).
The pickers read the chosen board's cards from the `board` publication,
which ships no cards for a lazy board, and a picker opens no list that
would fetch its window. A new `destinationCards` publication ships the
titles and places of one list's cards in one swimlane (or of the whole
board for the parent-card picker), whatever the card-loading mode, scoped
like the other card publications: the board must be readable, and an
assigned-only member gets only their own cards.

Copy / Move checklist, Copy checklist from a template card, Move / Copy
card, Link card and the parent-card picker subscribe to it. Done waits
until the chosen list's cards are there, a remembered card is checked
against them rather than against the board, and the open board's Filter
no longer empties the destination list.

Thanks to ChrisMagnuson and xet7 !

Fixes #6917.
2026-11-15 03:11:14 +02:00
Lauri Ojansivu c05be372aa Merge pull request #6914 from wekan/dependabot/npm_and_yarn/mongodb-7.8.0
Bump mongodb from 7.7.0 to 7.8.0
2026-11-15 01:48:01 +02:00
dependabot[bot] 7377f88aca Bump mongodb from 7.7.0 to 7.8.0
Bumps [mongodb](https://github.com/mongodb/node-mongodb-native) from 7.7.0 to 7.8.0.
- [Release notes](https://github.com/mongodb/node-mongodb-native/releases)
- [Changelog](https://github.com/mongodb/node-mongodb-native/blob/main/HISTORY.md)
- [Commits](https://github.com/mongodb/node-mongodb-native/compare/v7.7.0...v7.8.0)

---
updated-dependencies:
- dependency-name: mongodb
  dependency-version: 7.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-14 23:46:20 +00:00
Lauri Ojansivu 22577312a9 Merge pull request #6916 from wekan/dependabot/npm_and_yarn/sanitize-html-2.18.2
Bump sanitize-html from 2.18.1 to 2.18.2
2026-11-15 01:45:52 +02:00
Lauri Ojansivu b523e4a517 Merge pull request #6915 from wekan/dependabot/npm_and_yarn/i18next-27.1.1
Bump i18next from 26.5.11 to 27.1.1
2026-11-15 01:45:26 +02:00
Lauri Ojansivu 1810796dc6 Merge pull request #6913 from wekan/dependabot/npm_and_yarn/typescript-eslint/parser-8.72.2
Bump @typescript-eslint/parser from 8.72.1 to 8.72.2
2026-11-15 01:44:42 +02:00
Lauri Ojansivu 8cc5567649 Merge pull request #6912 from wekan/dependabot/github_actions/softprops/action-gh-release-3.0.4
Bump softprops/action-gh-release from 3.0.3 to 3.0.4
2026-11-15 01:44:27 +02:00
Lauri Ojansivu 3d96445be8 Merge pull request #6911 from wekan/dependabot/npm_and_yarn/tests/playwright/playwright/test-1.65.0
Bump @playwright/test from 1.64.1 to 1.65.0 in /tests/playwright
2026-11-15 01:44:08 +02:00
Lauri Ojansivu 26578fcc4e Merge pull request #6910 from wekan/dependabot/github_actions/docker/login-action-4.8.0
Bump docker/login-action from 4.7.0 to 4.8.0
2026-11-15 01:43:48 +02:00
Lauri Ojansivu 8cac055c59 Merge pull request #6909 from wekan/dependabot/npm_and_yarn/puppeteer-25.17.0
Bump puppeteer from 25.15.0 to 25.17.0
2026-11-15 01:43:28 +02:00
Lauri Ojansivu 30d6826564 Merge pull request #6908 from wekan/dependabot/github_actions/azure/setup-helm-5.1.0
Bump azure/setup-helm from 5.0.1 to 5.1.0
2026-11-15 01:42:56 +02:00
Lauri Ojansivu b32234cc9c Merge pull request #6907 from wekan/dependabot/github_actions/vmactions/freebsd-vm-1.6.0
Bump vmactions/freebsd-vm from 1.5.9 to 1.6.0
2026-11-15 01:42:27 +02:00
dependabot[bot] 8b213d38c9 Bump sanitize-html from 2.18.1 to 2.18.2
Bumps [sanitize-html](https://github.com/apostrophecms/apostrophe/tree/HEAD/packages/sanitize-html) from 2.18.1 to 2.18.2.
- [Changelog](https://github.com/apostrophecms/apostrophe/blob/sanitize-html@2.18.2/packages/sanitize-html/CHANGELOG.md)
- [Commits](https://github.com/apostrophecms/apostrophe/commits/sanitize-html@2.18.2/packages/sanitize-html)

---
updated-dependencies:
- dependency-name: sanitize-html
  dependency-version: 2.18.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-14 20:33:27 +00:00
dependabot[bot] 74b004333b Bump i18next from 26.5.11 to 27.1.1
Bumps [i18next](https://github.com/i18next/i18next) from 26.5.11 to 27.1.1.
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.5.11...v27.1.1)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 27.1.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-14 20:33:17 +00:00
dependabot[bot] f04beb1622 Bump @typescript-eslint/parser from 8.72.1 to 8.72.2
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.72.1 to 8.72.2.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.72.2/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.72.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-14 20:33:02 +00:00
dependabot[bot] f9b52fbd9d Bump softprops/action-gh-release from 3.0.3 to 3.0.4
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 3.0.3 to 3.0.4.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/efb35369e0ad2afab669f228072c1b0d510eae64...8a0cd651e5b2365bd640854a3fa5dfc70e4875f3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-14 20:33:00 +00:00
dependabot[bot] 74b1df33d1 Bump @playwright/test from 1.64.1 to 1.65.0 in /tests/playwright
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.64.1 to 1.65.0.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.64.1...v1.65.0)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.65.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-14 20:32:55 +00:00
dependabot[bot] fa8303dbe4 Bump docker/login-action from 4.7.0 to 4.8.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.7.0 to 4.8.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/81c6f93d4bc991e510b342a00b419b3bb4efc5c6...8aa5831fb8d48d8f76594c7ddde61ae58019f550)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-14 20:32:52 +00:00
dependabot[bot] 5346efb843 Bump puppeteer from 25.15.0 to 25.17.0
Bumps [puppeteer](https://github.com/puppeteer/puppeteer) from 25.15.0 to 25.17.0.
- [Release notes](https://github.com/puppeteer/puppeteer/releases)
- [Changelog](https://github.com/puppeteer/puppeteer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/puppeteer/puppeteer/compare/puppeteer-v25.15.0...puppeteer-v25.17.0)

---
updated-dependencies:
- dependency-name: puppeteer
  dependency-version: 25.17.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-14 20:32:49 +00:00
dependabot[bot] 164809e08b Bump azure/setup-helm from 5.0.1 to 5.1.0
Bumps [azure/setup-helm](https://github.com/azure/setup-helm) from 5.0.1 to 5.1.0.
- [Release notes](https://github.com/azure/setup-helm/releases)
- [Changelog](https://github.com/Azure/setup-helm/blob/main/CHANGELOG.md)
- [Commits](https://github.com/azure/setup-helm/compare/9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310...2f5e7bc67f1f9972ab16657bb795f36d8f04de0e)

---
updated-dependencies:
- dependency-name: azure/setup-helm
  dependency-version: 5.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-14 20:32:45 +00:00
dependabot[bot] ae6ee35bdb Bump vmactions/freebsd-vm from 1.5.9 to 1.6.0
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.5.9 to 1.6.0.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/c46abacb49f09938ca4e1702d15d836285d694cc...811efaaac0668df8bfb8308c3ec997b5ab50037b)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-14 20:32:39 +00:00
30 changed files with 853 additions and 119 deletions

No files matched your search

+1 -1
View File
@@ -147,7 +147,7 @@ jobs:
- name: Build and start in FreeBSD
if: steps.node.outputs.skip != 'true' && matrix.os == 'freebsd'
uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc
uses: vmactions/freebsd-vm@811efaaac0668df8bfb8308c3ec997b5ab50037b
with:
release: ${{ matrix.release }}
arch: ${{ matrix.vm_arch }}
+1 -1
View File
@@ -237,7 +237,7 @@ jobs:
bash releases/bun/build-target.sh'
- name: FreeBSD VM build and smoke tests
if: matrix.kind == 'freebsd'
uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1.5.9
uses: vmactions/freebsd-vm@811efaaac0668df8bfb8308c3ec997b5ab50037b # v1.6.0
with:
release: '14.5'
arch: ${{ matrix.vm_arch }}
+1 -1
View File
@@ -72,7 +72,7 @@ jobs:
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
+1 -1
View File
@@ -486,7 +486,7 @@ jobs:
- name: Install Helm
if: steps.check.outputs.skip != 'true'
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
uses: azure/setup-helm@2f5e7bc67f1f9972ab16657bb795f36d8f04de0e # v5.1.0
- name: Publish the chart for this release
if: steps.check.outputs.skip != 'true'
+2 -2
View File
@@ -538,7 +538,7 @@ jobs:
path: charts
- name: Install Helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
uses: azure/setup-helm@2f5e7bc67f1f9972ab16657bb795f36d8f04de0e # v5.1.0
- name: Fetch gh-pages branch (release-charts.sh switches to it)
run: |
@@ -1220,7 +1220,7 @@ jobs:
# loudly and verified from the release side; the next step checks the two
# base bundles from here.
- name: Create GitHub Release
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
uses: softprops/action-gh-release@8a0cd651e5b2365bd640854a3fa5dfc70e4875f3 # v3.0.4
with:
tag_name: v${{ needs.prepare.outputs.version }}
name: v${{ needs.prepare.outputs.version }}
+65
View File
@@ -124,6 +124,71 @@ its reason.
</details>
# v13.13 2026-11-15 WeKan ® release
**In short:** **Copy Checklist** and the other destination pickers list the
cards of a large board again: a board that loads its cards lazily used to leave
the card dropdown empty. Dependabot's updates, among them **i18next 27** and
the **mongodb** driver 7.8.0, are kept.
This release fixes the following bugs:
**Destination pickers** - choosing a card on another board, whatever that
board's size.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e46d7a2376ebfa413cec15843d8b7fc374fe63e0">The card dropdown lists the cards of a board that loads lazily</a>. Thanks to ChrisMagnuson and xet7.</summary>
Copy Checklist's card dropdown stayed empty for a board with more cards than
the lazy-loading threshold (Card loading `auto` above 500 cards, or `lazy`),
however long one waited ([#6917](https://github.com/wekan/wekan/issues/6917)).
The pickers read the chosen board's cards from the `board` publication, which
ships no cards for a lazy board, and a picker opens no list that would fetch
its own window. A new `destinationCards` publication ships the titles and
places of one list's cards in one swimlane, or of the whole board for the
parent-card picker, whatever the card-loading mode. It is scoped like the
other card publications: the board must be readable, and an assigned-only
member gets only the cards assigned to them.
Copy / Move checklist, Copy checklist from a template card, Move / Copy card,
Link card and the parent-card picker use it. Done waits until the chosen
list's cards are there, a remembered card is checked against them rather than
against the board, and the open board's Filter no longer empties the
destination list. `tests/destinationCards6917.test.cjs` runs the publication
on a 1,144-card board, an assigned-only member, an unreadable board and bad
arguments, and the dialog with a lazy destination;
`tests/playwright/specs/copy-checklist-lazy-destination.e2e.js` copies a
checklist to such a board.
</details>
and updates the following dependencies:
- **mongodb 7.7.0 → 7.8.0** — the server's database driver
([#6914](https://github.com/wekan/wekan/pull/6914)).
- **i18next 26.5.11 → 27.1.1** — translations
([#6915](https://github.com/wekan/wekan/pull/6915)). 27 changes defaults
WeKan does not rely on: it sets `fallbackLng` and `cleanCode` itself, turns
escaping off and registers sprintf on its own instance. An empty translation
now falls back to English instead of showing nothing.
- **sanitize-html 2.18.1 → 2.18.2** — HTML sanitizing
([#6916](https://github.com/wekan/wekan/pull/6916)).
- **puppeteer 25.15.0 → 25.17.0** and **@typescript-eslint/parser 8.72.1 →
8.72.2** — tooling ([#6909](https://github.com/wekan/wekan/pull/6909),
[#6913](https://github.com/wekan/wekan/pull/6913)).
- **@playwright/test 1.64.1 → 1.65.0** in tests/playwright — the browser tests
([#6911](https://github.com/wekan/wekan/pull/6911)).
- **softprops/action-gh-release 3.0.3 → 3.0.4**, **docker/login-action 4.7.0
→ 4.8.0**, **azure/setup-helm 5.0.1 → 5.1.0** and **vmactions/freebsd-vm
1.5.9 → 1.6.0** — CI ([#6912](https://github.com/wekan/wekan/pull/6912),
[#6910](https://github.com/wekan/wekan/pull/6910),
[#6908](https://github.com/wekan/wekan/pull/6908),
[#6907](https://github.com/wekan/wekan/pull/6907)).
Thanks to dependabot.
Thanks to above GitHub users for their contributions and translators for their translations.
# v13.12 2026-11-14 WeKan ® release
**In short:** WeKan is updated to **Meteor 3.6.1**, whose DDP server refuses
+9 -10
View File
@@ -5,6 +5,15 @@ each one changed and the project status are in
Written by `node releases/changelog-short.mjs` from CHANGELOG-big.md:
make changes there.
# v13.13 2026-11-15 WeKan ® release
**Copy Checklist** and the other destination pickers list the
cards of a large board again: a board that loads its cards lazily used to leave
the card dropdown empty. Dependabot's updates, among them **i18next 27** and
the **mongodb** driver 7.8.0, are kept.
[All changes in v13.13](CHANGELOG-big.md#v1313-2026-11-15-wekan--release)
# v13.12 2026-11-14 WeKan ® release
WeKan is updated to **Meteor 3.6.1**, whose DDP server refuses
@@ -42,13 +51,3 @@ The release's **Snap Store** credential check retries installing
snapcraft, so one Snap Store timeout no longer stops a release.
[All changes in v13.09](CHANGELOG-big.md#v1309-2026-11-12-wekan--release)
# v13.08 2026-11-12 WeKan ® release
The **REST API** creates a card under the **parent** it is given,
and a refused **integration URL** no longer leaves the other fields of the same
update written. The **single-file** WeKan for **32-bit ARM** builds again, with
an i386 Node.js of the exact version its bundle embeds. A **GitHub CodeQL**
alert in the security alert download script is fixed.
[All changes in v13.08](CHANGELOG-big.md#v1308-2026-11-12-wekan--release)
+1 -1
View File
@@ -38,7 +38,7 @@ FROM --platform=$BUILDPLATFORM debian:trixie AS bundle
ARG TARGETARCH
ARG TARGETVARIANT
ARG VERSION=13.12
ARG VERSION=13.13
ARG DEBIAN_FRONTEND=noninteractive
COPY --chmod=755 releases/check-telemetry.py /tmp/check-telemetry.py
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v13.12.0"
appVersion: "v13.13.0"
files:
userUploads:
- README.md
+17 -7
View File
@@ -2400,7 +2400,10 @@ Template.cardDestinationPicker.helpers({
destinationError() { return Template.instance().dialog.error.get(); },
destinationDisabled() {
const dialog = Template.instance().dialog;
return dialog.loading.get() || dialog.saving.get() || !dialog.selectedSwimlaneId.get() || !dialog.selectedListId.get();
// #6917: and until the chosen list's cards are there, so a move or copy
// never places the card against a dropdown that has not filled yet.
return dialog.loading.get() || dialog.saving.get() || !!dialog.cardsLoading?.get()
|| !dialog.selectedSwimlaneId.get() || !dialog.selectedListId.get();
},
boards() {
return Template.instance().dialog.boards();
@@ -2465,7 +2468,7 @@ function registerCardDialogTemplate(templateName) {
async 'click .js-done'(event, tpl) {
event.preventDefault();
const dialog = tpl.dialog;
if (dialog.loading.get() || dialog.saving.get()) return;
if (dialog.loading.get() || dialog.saving.get() || dialog.cardsLoading?.get()) return;
// Read the target board/swimlane/list from the dialog's live reactive
// selection rather than the DOM <select> elements. A reactive re-render
// (e.g. the boards() helper transiently returning [] while a 'board'
@@ -2934,6 +2937,7 @@ Template.cardMorePopup.onCreated(function () {
// is no longer blank the first time another board is picked (the cards()
// helper queries minimongo, which was empty before the subscription arrived).
this.parentBoardReady = new ReactiveVar(true);
this.view.onViewDestroyed(() => this.parentCardsSubscription?.stop());
this.parentCard = this.currentCard?.parentCard();
if (this.parentCard) {
const list = $('.js-field-parent-card');
@@ -3080,11 +3084,17 @@ Template.cardMorePopup.events({
// #3745: wait for the board subscription to be ready before showing its
// cards, so the parent-card list is populated on the first selection.
tpl.parentBoardReady.set(false);
Meteor.subscribe('board', selection, false, {
onReady() {
tpl.parentBoardReady.set(true);
},
});
// #6917: a lazily loaded board's `board` publication ships no cards, so
// the parent-card list also subscribes to the board's card titles, and
// is ready when both are.
tpl.parentCardsSubscription?.stop();
let pending = 2;
const ready = () => {
pending -= 1;
if (pending === 0 && tpl.parentBoard.get() === selection) tpl.parentBoardReady.set(true);
};
Meteor.subscribe('board', selection, false, { onReady: ready });
tpl.parentCardsSubscription = Meteor.subscribe('destinationCards', selection, null, null, { onReady: ready });
tpl.parentBoard.set(selection);
list.prop('disabled', false);
}
+1 -1
View File
@@ -347,7 +347,7 @@ template(name="copyAndMoveChecklist")
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{title}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
button.primary.confirm.js-done(type="button" disabled=doneDisabled) {{_ 'done'}}
//- Due date on a single checklist item (#4755). The badge and its edit popup
reuse the card's own date-badge markup (dateBadgeBody) and date-picker form
+11
View File
@@ -841,6 +841,11 @@ Template.copyAndMoveChecklist.helpers({
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.isTitleDefault(title) : title;
},
// #6917: Done waits for the destination list's cards.
doneDisabled() {
const dialog = getParentDialog(Template.instance());
return !dialog || dialog.loading.get() || dialog.cardsLoading.get() || dialog.cards().length === 0;
},
});
/**
@@ -865,6 +870,11 @@ function registerChecklistDialogEvents(templateName) {
? cardSelect.options[cardSelect.selectedIndex].value
: null;
// #6917: every checklist dialog needs a card, and the dropdown may still
// be filling: Done does nothing until a card of the list is chosen.
if (dialog.loading.get() || dialog.cardsLoading.get()) return;
if (!cardId || !dialog.cards().some(card => card._id === cardId)) return;
const options = { boardId, swimlaneId, listId, cardId };
try {
await dialog.setDone(cardId, options);
@@ -879,6 +889,7 @@ function registerChecklistDialogEvents(templateName) {
'change .js-select-swimlanes'(event, tpl) {
tpl.dialog.selectedSwimlaneId.set($(event.currentTarget).val());
tpl.dialog.setFirstListId();
tpl.dialog.selectedCardId.set('');
},
'change .js-select-lists'(event, tpl) {
tpl.dialog.selectedListId.set($(event.currentTarget).val());
+11
View File
@@ -1209,6 +1209,17 @@ Template.linkCardPopup.onCreated(function () {
else if (Utils.boardView() === 'board-view-lists' || !Utils.boardView)
this.swimlaneId = listData.swimlaneId || ReactiveCache.getSwimlane({ boardId: this.boardId })._id;
// #6917: the cards offered for linking come from their own subscription. A
// lazily loaded board's `board` publication ships no cards, so a large board
// picked here listed none. Inside the autorun, the previous selection's
// subscription stops when another board, swimlane or list is chosen.
this.autorun(() => {
const boardId = this.selectedBoardId.get();
if (!boardId) return;
this.subscribe('destinationCards', boardId,
this.selectedListId.get() || null, this.selectedSwimlaneId.get() || null);
});
this.getSortIndex = () => {
let ret;
if (this.position === 'top') {
@@ -265,9 +265,6 @@ export class BoardSwimlaneListDialog {
const lanes = this.swimlanes();
if (!lanes.some(lane => lane._id === this.selectedSwimlaneId.get())) this.setFirstSwimlaneId();
if (!this.lists().some(list => list._id === this.selectedListId.get())) this.setFirstListId();
if (this.selectedCardId && !this.cards().some(card => card._id === this.selectedCardId.get())) {
this.selectedCardId.set('');
}
this.loading.set(false);
},
onStop: error => {
+30 -1
View File
@@ -10,6 +10,33 @@ export class BoardSwimlaneListCardDialog extends BoardSwimlaneListDialog {
constructor(tpl, callbacks = {}) {
super(tpl, callbacks);
this.selectedCardId = new ReactiveVar(this.cardOption.cardId || '');
this.cardsLoading = new ReactiveVar(true);
// #6917: the destination's cards come from a picker-owned subscription,
// not from the `board` publication, which ships no cards at all for a
// board that loads lazily (Card loading = lazy, or 'auto' above the
// threshold). Subscribing inside the template's autorun stops the previous
// list's subscription when another board, swimlane or list is chosen, and
// the last one when the popup closes.
this.tpl.autorun?.(() => {
const boardId = this.selectedBoardId.get();
const listId = this.selectedListId.get();
const swimlaneId = this.selectedSwimlaneId.get();
const request = (this._cardsRequest = (this._cardsRequest || 0) + 1);
this.cardsLoading.set(true);
if (!boardId || !listId || !swimlaneId) return;
this.tpl.subscribe('destinationCards', boardId, listId, swimlaneId, {
onReady: () => {
if (this._disposed || request !== this._cardsRequest) return;
// A remembered card is checked here, once this list's cards are
// there - the board being ready says nothing about its cards.
const cardId = this.selectedCardId.get();
if (cardId && !this.cards().some(card => card._id === cardId)) {
this.selectedCardId.set('');
}
this.cardsLoading.set(false);
},
});
});
}
getDefaultOption() {
@@ -37,7 +64,9 @@ export class BoardSwimlaneListCardDialog extends BoardSwimlaneListDialog {
});
const swimlaneId = this.selectedSwimlaneId.get();
if (list && swimlaneId) {
return list.cards(swimlaneId).sort((a, b) => a.sort - b.sort);
// Unfiltered: the open board's Filter describes that board, and must not
// empty the dropdown of the destination (#6917).
return list.cardsUnfiltered(swimlaneId).sort((a, b) => a.sort - b.sort);
} else {
return [];
}
@@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
Right click and download files 1-4:
1. [wekan-13.12-win64.zip](https://github.com/wekan/wekan/releases/download/v13.12/wekan-13.12-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
1. [wekan-13.13-win64.zip](https://github.com/wekan/wekan/releases/download/v13.13/wekan-13.13-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
2. [node.exe](https://nodejs.org/dist/latest-v26.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-13.12-win64.zip` , inside it is directory `bundle`, to it copy other files:
7. Unzip `wekan-13.13-win64.zip` , inside it is directory `bundle`, to it copy other files:
```
bundle (directory)
+50
View File
@@ -0,0 +1,50 @@
'use strict';
// Pure, Meteor-free helpers for the `destinationCards` publication (#6917).
// Unit-tested in tests/destinationCards6917.test.cjs.
//
// The destination pickers - Copy / Move checklist, Copy checklist from a
// template card, Move / Copy card, Link card, and the parent-card picker -
// list the cards of ANOTHER board, which they used to read from the `board` publication. On a
// board that loads lazily (Admin Panel / Features / Card loading = lazy, or
// 'auto' with more cards than the threshold) that publication ships NO cards:
// each list of an open board fetches its own window. A picker opens no board
// view, so nothing ever fetched its window and the card dropdown stayed empty
// for good. `destinationCards` publishes what a picker shows, whatever the
// board's card-loading mode: the title and place of each card of one list (or,
// for the parent-card picker, of the whole board), and nothing else.
import { listCardsSelector } from './swimlaneFilter.js';
// The fields a picker needs: the option text, the sort order, what
// list.cardsUnfiltered()'s selector tests in minimongo, and `type` / `linkedId`,
// with which the link-card picker leaves out linked cards and the cards it
// already mirrors. No description, no members, no assignees - a picker reveals
// no more than a minicard's title.
const DESTINATION_CARD_FIELDS = Object.freeze({
_id: 1,
title: 1,
sort: 1,
boardId: 1,
listId: 1,
swimlaneId: 1,
archived: 1,
type: 1,
linkedId: 1,
});
// A board's biggest list stays under this; it bounds what one subscribe asks
// of the database (the windowed publication's own cap is the same).
const MAX_DESTINATION_CARDS = 5000;
// The client half of the selector: one list's cards in one swimlane, exactly as
// list.cardsUnfiltered() selects them in minimongo (orphaned cards included in
// the first swimlane), or every active card of the board when no list is
// named. The server ANDs it with the board and role scope.
function destinationCardsSelector(listId, swimlaneId, otherSwimlaneIds) {
if (!listId) return { archived: false };
// The link-card picker names a list without a swimlane: the whole list.
return listCardsSelector(listId, swimlaneId || undefined, otherSwimlaneIds);
}
export { DESTINATION_CARD_FIELDS, MAX_DESTINATION_CARDS, destinationCardsSelector };
+186 -53
View File
@@ -1,12 +1,12 @@
{
"name": "wekan",
"version": "v13.12.0",
"version": "v13.13.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wekan",
"version": "v13.12.0",
"version": "v13.13.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -51,7 +51,7 @@
"highlight.js": "^11.12.0",
"hotkeys-js": "^4.0.8",
"htmlparser2": "^12.0.0",
"i18next": "^26.5.11",
"i18next": "^27.1.1",
"i18next-sprintf-postprocessor": "^0.2.2",
"imapflow": "2.9.1",
"isomorphic-git": "1.44.10",
@@ -70,7 +70,7 @@
"pdfkit": "^0.20.2",
"pretty-ms": "^10.0.0",
"qrcode-generator": "2.0.4",
"sanitize-html": "^2.18.1",
"sanitize-html": "^2.18.2",
"sharp": "^0.35.5",
"temml": "^0.14.0",
"underscore": "^1.13.8",
@@ -87,14 +87,14 @@
"@rspack/dev-server": "^2.2.2",
"@swc/core": "^1.16.18",
"@typescript-eslint/eslint-plugin": "^8.72.1",
"@typescript-eslint/parser": "^8.72.0",
"@typescript-eslint/parser": "^8.72.2",
"chai": "^6.3.0",
"chai-as-promised": "^8.0.2",
"css-loader": "^7.1.4",
"enhanced-resolve": "5.26.0",
"minimatch": "^11.0.1",
"mongodb": "^7.7.0",
"puppeteer": "^25.15.0",
"mongodb": "^7.8.0",
"puppeteer": "^25.17.0",
"sinon": "^22.1.0",
"sinon-chai": "^4.0.1",
"style-loader": "^4.0.0"
@@ -3661,13 +3661,13 @@
"license": "MIT"
},
"node_modules/@puppeteer/browsers": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.5.tgz",
"integrity": "sha512-UoCo3Pynbm37aLEEoydSiG+t24ui7sLD40+07CKxzxNy4PpSowJMkvlLIiV67XjP1lMVynhQVnuwlTABqBH8LQ==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.7.tgz",
"integrity": "sha512-B3C/v8OEEaJYcN1IzzgbPmt+GbFANr4Mf/NrqNuGk/dIchrv6quNqxgT+A6qE0Ge0MN95NjKUddcR1JMpYRMWA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"modern-tar": "^0.8.5",
"modern-tar": "^0.8.6",
"yargs": "^18.0.0"
},
"bin": {
@@ -5485,16 +5485,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz",
"integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==",
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.2.tgz",
"integrity": "sha512-llP+T1YuJOLlxEciP6OyQGtz5SMxyn+9LA2BvZw5RmToGVa1y4beEDVQL6Gje7T5WFYh19WREjDCN9Ev4gm5AA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.72.1",
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/typescript-estree": "8.72.1",
"@typescript-eslint/visitor-keys": "8.72.1",
"@typescript-eslint/scope-manager": "8.72.2",
"@typescript-eslint/types": "8.72.2",
"@typescript-eslint/typescript-estree": "8.72.2",
"@typescript-eslint/visitor-keys": "8.72.2",
"debug": "^4.4.3"
},
"engines": {
@@ -5509,6 +5509,136 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": {
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.2.tgz",
"integrity": "sha512-6y5dLTZdHhI8Eh3yIQ/bfQhRKwn5e+TpJj64o18Cngc0iW/BCrVvrr6zGYqXPnYH31u6YXAYiFh0+qxX5mybJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.72.2",
"@typescript-eslint/types": "^8.72.2",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.2.tgz",
"integrity": "sha512-iIoMJ6/8c1/QpFNyFjU0d1YzPco4MsZjQw0k3V+vfzNWAFmUmvhlyMoYEoYYDQZXc+J6Gi1X5XcxlzW2taIIGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.2",
"@typescript-eslint/visitor-keys": "8.72.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.2.tgz",
"integrity": "sha512-+h3iCgZXuTNtoLXcnnjP12ZUHLMqoUcIHPfQrFEXTUqvb9dF8QK9b5nv5VtSo5fLkdHuks8ehbTzZj3rImAm9w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.2.tgz",
"integrity": "sha512-Xfn37OLZx03+PCU7qWx9DTv2FCRh1E6fk2+wONeNY15HXIwtcwuveaSwVpGAUptkNmPQegRxW5JyRel9T7wrog==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.2.tgz",
"integrity": "sha512-pQEGqIK/cqEvKivVWaFLYpe7XYejFxfuAOi4mu1oAWFvIo36aRHI9esRFTUxcIy/z8N482RJyH+LpZB0ZIU7Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.72.2",
"@typescript-eslint/tsconfig-utils": "8.72.2",
"@typescript-eslint/types": "8.72.2",
"@typescript-eslint/visitor-keys": "8.72.2",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.2.tgz",
"integrity": "sha512-NAGrjn0Pitjqzq/GkgMC4kBYm233PQ8GffyQ9+kTA3AlNQYEzOf25EIM02BAHZv5FRJSpHpJk+OGUxVkOB93GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.2",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz",
@@ -7546,9 +7676,9 @@
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1696802",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1696802.tgz",
"integrity": "sha512-FO5/SENfvc2K8wRnWQflKcwuoTH4dBkXCWTQ9ebIWtuX8JM5+ZvCotaEmkfh21pO7TvIKGTM3hG4xFiq9WTwmg==",
"version": "0.0.1717327",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1717327.tgz",
"integrity": "sha512-3RDLDzyvv8XAYf1L+TL1og0EEk0eOW1Jn0yi88uG6Bj8fUKO1ZKfgNtwbJkqf5SWdPDS4VGEPxwdUWz78V6Xcg==",
"dev": true,
"license": "BSD-3-Clause"
},
@@ -9685,9 +9815,9 @@
}
},
"node_modules/i18next": {
"version": "26.5.11",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.5.11.tgz",
"integrity": "sha512-Wc0klBPKxqZ2tL2qdAnF8xvvnWgr1IMGSY2ikTiAvaFVbz7xiFARtkoU4RidBDw1SyziGROp0qxu6qnJ7l2Wug==",
"version": "27.1.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-27.1.1.tgz",
"integrity": "sha512-Qq3aJizS3RXQ30crhx9BKQLrHoa6GKpgrOJjVZ9vgj3EBxAddrmVfDNUrAMxSUimw5Jc+ld4SSZUAzf1q4nGyQ==",
"funding": [
{
"type": "individual",
@@ -9703,6 +9833,9 @@
}
],
"license": "MIT",
"engines": {
"node": ">=22"
},
"peerDependencies": {
"typescript": "^5 || ^6 || ^7"
},
@@ -10968,9 +11101,9 @@
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.5.tgz",
"integrity": "sha512-aRAOI9Ia43ygL4o1EOmC/sc/0OFxrm7qcQngiIyWaqVnSnz932ybJ07zyCcgMNGSDP8Yxaf88Xj1zHEXmvQ4SA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -12872,9 +13005,9 @@
}
},
"node_modules/mongodb": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.7.0.tgz",
"integrity": "sha512-XG1xXEqyouAV8GCb3PuRaujofeXKpAJAPvXYWZgHl6Dz4QHSJ1nRLl0+GpqaLTwKvC0RFE9truOur34RNFY1nw==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.8.0.tgz",
"integrity": "sha512-p2NKagbQ41+daACG/ZSvhTzeAo+DoEyWE9Cmpf/SyP3pTC43+X0mwKE/44PpKtCxp5wOxnXz+t2pXEZRSkt9Yg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -13833,18 +13966,18 @@
}
},
"node_modules/puppeteer": {
"version": "25.15.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.15.0.tgz",
"integrity": "sha512-b+2Ye8C55Fi1u+w680WdDx4QWE4NVM1MKpB8YRXkbBngf335e/omAKI5/lPTSaAMmqHw199eEf90iFXPjJUYCw==",
"version": "25.17.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.17.0.tgz",
"integrity": "sha512-J+OQNlI7I6ZjHhF6Q8M+LgiUGldF7meer5EqMJBZMHgnFN3zcehOhpz/x0bYDEEvbEiXzKzmqSRP3QT+5accjQ==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "3.2.5",
"@puppeteer/browsers": "3.2.7",
"chromium-bidi": "157.0.8090-0",
"devtools-protocol": "0.0.1696802",
"lilconfig": "^3.1.3",
"puppeteer-core": "25.15.0",
"devtools-protocol": "0.0.1717327",
"lilconfig": "^3.1.4",
"puppeteer-core": "25.17.0",
"typed-query-selector": "^2.12.3"
},
"bin": {
@@ -13855,18 +13988,18 @@
}
},
"node_modules/puppeteer-core": {
"version": "25.15.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.15.0.tgz",
"integrity": "sha512-DEk7S2tlBDp4+iXdnHjFmMofoqVftlz1JO4eyE+oBSiQIfIVTYq8hWF1EsjmRvD49chxGX3oKQT5hX2dnIIDdg==",
"version": "25.17.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.17.0.tgz",
"integrity": "sha512-YAgBitkd+K9gxRt+wzBAFTTkGjkC8aC2o4OxofsU8JUy2nnK/RflTCJ9bRCvCMNbq9TPhqFk4Phz98otN9FrGg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "3.2.5",
"@puppeteer/browsers": "3.2.7",
"chromium-bidi": "157.0.8090-0",
"devtools-protocol": "0.0.1696802",
"devtools-protocol": "0.0.1717327",
"typed-query-selector": "^2.12.3",
"webdriver-bidi-protocol": "1.0.1",
"ws": "^8.22.0"
"webdriver-bidi-protocol": "1.0.2",
"ws": "^8.22.1"
},
"engines": {
"node": ">=22.12.0"
@@ -14417,9 +14550,9 @@
"license": "MIT"
},
"node_modules/sanitize-html": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.18.1.tgz",
"integrity": "sha512-2X5lC8fYla8YaT8g73Nj+HcLWXan5dLyCWLgVvaOmfw7JI78IRsyNtIDytTHdn7hBQbIkCah7Zbmd5jabzeDig==",
"version": "2.18.2",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.18.2.tgz",
"integrity": "sha512-LTQVNHBeKB5qIae9x2J2Fw68ORnES++8vzYaG7DFT8P4GVCJFeHffHnRUzf5Mrxfx+4GweIvMRqkBPHHodaVNg==",
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^4.0.0",
@@ -16101,9 +16234,9 @@
}
},
"node_modules/webdriver-bidi-protocol": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-1.0.1.tgz",
"integrity": "sha512-WXWs/aXF2a6Al1Z2/V/7Gp2oPZOAAmHjJOTmv4KCXRJ122IMj3tSSMc6a4q3K4QOmhQbse/Ahr+oTVJOU2dUiw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-1.0.2.tgz",
"integrity": "sha512-nghazPI+J5RrLZ+qb6vMjC6l1/tnwqr2muVEax4Ufd6eIdcjeqNnhLsAxTG9s+7Zf4umgf0DrEwDC8CoU91nAA==",
"dev": true,
"license": "Apache-2.0"
},
@@ -16371,9 +16504,9 @@
"license": "ISC"
},
"node_modules/ws": {
"version": "8.22.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
"integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
"version": "8.22.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.22.2.tgz",
"integrity": "sha512-/rEZ2ejown2N5gxVTHUu9bVVC7ttu0/Tgzf5hbTOixR3Fs7mbbXgn2DbdbcKgJOmpQMtrIUWivtYuVMnp/W/yA==",
"dev": true,
"license": "MIT",
"engines": {
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "wekan",
"version": "v13.12.0",
"version": "v13.13.0",
"description": "Open-Source kanban",
"private": true,
"repository": {
@@ -45,14 +45,14 @@
"@rspack/dev-server": "^2.2.2",
"@swc/core": "^1.16.18",
"@typescript-eslint/eslint-plugin": "^8.72.1",
"@typescript-eslint/parser": "^8.72.0",
"@typescript-eslint/parser": "^8.72.2",
"chai": "^6.3.0",
"chai-as-promised": "^8.0.2",
"css-loader": "^7.1.4",
"enhanced-resolve": "5.26.0",
"minimatch": "^11.0.1",
"mongodb": "^7.7.0",
"puppeteer": "^25.15.0",
"mongodb": "^7.8.0",
"puppeteer": "^25.17.0",
"sinon": "^22.1.0",
"sinon-chai": "^4.0.1",
"style-loader": "^4.0.0"
@@ -99,7 +99,7 @@
"highlight.js": "^11.12.0",
"hotkeys-js": "^4.0.8",
"htmlparser2": "^12.0.0",
"i18next": "^26.5.11",
"i18next": "^27.1.1",
"i18next-sprintf-postprocessor": "^0.2.2",
"imapflow": "2.9.1",
"isomorphic-git": "1.44.10",
@@ -118,7 +118,7 @@
"pdfkit": "^0.20.2",
"pretty-ms": "^10.0.0",
"qrcode-generator": "2.0.4",
"sanitize-html": "^2.18.1",
"sanitize-html": "^2.18.2",
"sharp": "^0.35.5",
"temml": "^0.14.0",
"underscore": "^1.13.8",
+3 -3
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf8" />
<title>WeKan REST API v13.12</title>
<title>WeKan REST API v13.13</title>
<!-- needed for adaptive design -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
@@ -494,11 +494,11 @@ data-styled.g652[id="sc-LDhwn"]{content:"cirFq,"}/*!sc*/
<body>
<div id="redoc"><div class="sc-jlWaFk kDulYG redoc light" style="--navbar-height:0px;--navbar-stack-height:0px"><div hidden=""><script>try{var r=document.querySelectorAll(".redoc");r=r[r.length-1];var h=document.documentElement,p=h.classList,a=[h.getAttribute("data-bs-theme"),h.getAttribute("data-theme")];var m=p.contains("dark")||a.indexOf("dark")>-1?"dark":p.contains("light")||a.indexOf("light")>-1?"light":localStorage.getItem("redoc.colorMode");if(m!=="light"&&m!=="dark")m=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";r.classList.remove("light","dark");r.classList.add(m)}catch(e){}</script></div><div class="sc-NPrTd fpSCXb menu-content"><div class="sc-bJapvR eXtYur"><div data-testid="search-trigger" class="sc-eavxOQ bStpwo"><div class="sc-dGYDtq hRGWCd"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" data-component-name="icons/SearchIcon/SearchIcon" class="sc-gJaedx dpfGSp"><path d="M14.5 13.7931L10.724 10.017C11.6313 8.9277 12.0838 7.5305 11.9872 6.11608C11.8907 4.70165 11.2525 3.37891 10.2055 2.423C9.15857 1.4671 7.78336 0.951637 6.36602 0.983845C4.94867 1.01605 3.5983 1.59345 2.59582 2.59593C1.59335 3.5984 1.01595 4.94877 0.983738 6.36612C0.95153 7.78347 1.46699 9.15867 2.4229 10.2057C3.3788 11.2526 4.70155 11.8908 6.11597 11.9873C7.53039 12.0839 8.92759 11.6314 10.0169 10.7241L13.793 14.5001L14.5 13.7931ZM2.00001 6.50012C2.00001 5.6101 2.26393 4.74007 2.7584 4.00005C3.25286 3.26003 3.95567 2.68325 4.77794 2.34266C5.6002 2.00207 6.505 1.91295 7.37792 2.08658C8.25083 2.26022 9.05266 2.6888 9.68199 3.31814C10.3113 3.94747 10.7399 4.7493 10.9135 5.62221C11.0872 6.49513 10.9981 7.39993 10.6575 8.22219C10.3169 9.04446 9.7401 9.74726 9.00008 10.2417C8.26006 10.7362 7.39003 11.0001 6.50001 11.0001C5.30694 10.9988 4.16312 10.5243 3.31949 9.68064C2.47587 8.83701 2.00133 7.69319 2.00001 6.50012Z"></path></svg><span>Loading...</span></div></div></div><div class="sc-kLsiYl kAUzfx"><ul data-component-name="Menu/Menu" class="sc-jfJzNu hoWuXt"><div data-component-name="Menu/MenuItem" class="sc-jEekwy kJsUv menu-item-type-default" role="listitem"><a class="sc-gzhUzI kKmXTr" href="/" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="true" class="sc-cToEys eWEtkC"><div class="sc-fIruOr dBwuTA"><span class="sc-gwVKcS efjekG"><span data-text="WeKan REST API" class="sc-hgJgCR dWgUkd">WeKan REST API</span></span></div></li></a></div><div data-component-name="Menu/MenuItem" class="sc-jEekwy kJsUv menu-item-type-default" role="listitem"><a class="sc-gzhUzI kKmXTr" href="/section/production-security-concerns" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-cToEys iVTRMP"><div class="sc-fIruOr dBwuTA"><span class="sc-gwVKcS efjekG"><span data-text="Production Security Concerns" class="sc-hgJgCR dlVNdD">Production Security Concerns</span></span></div></li></a></div><div data-component-name="Menu/MenuItem" class="sc-jEekwy kJsUv menu-item-type-group" role="listitem"><a class="sc-gzhUzI kKmXTr" href="/login" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-cToEys jYycQW"><div class="sc-dGPMcj PCvEP"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-kLEbxe eRqACJ"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-fIruOr dBwuTA"><span class="sc-gwVKcS efjekG"><span data-text="Login" class="sc-hgJgCR dlVNdD">Login</span></span></div></li></a><div style="display:none;height:0px;overflow:hidden" class="sc-fKDFlh joxUYR"></div></div><div data-component-name="Menu/MenuItem" class="sc-jEekwy kJsUv menu-item-type-group" role="listitem"><a class="sc-gzhUzI kKmXTr" href="/attachmentstoragesettings" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-cToEys jYycQW"><div class="sc-dGPMcj PCvEP"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-kLEbxe eRqACJ"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-fIruOr dBwuTA"><span class="sc-gwVKcS efjekG"><span data-text="AttachmentStorageSettings" class="sc-hgJgCR dlVNdD">AttachmentStorageSettings</span></span></div></li></a><div style="display:none;height:0px;overflow:hidden" class="sc-fKDFlh joxUYR"></div></div><div data-component-name="Menu/MenuItem" class="sc-jEekwy kJsUv menu-item-type-group" role="listitem"><a class="sc-gzhUzI kKmXTr" href="/settings" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-cToEys jYycQW"><div class="sc-dGPMcj PCvEP"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-defLine truncated
<div id="redoc"><div class="sc-jlWaFk kDulYG redoc light" style="--navbar-height:0px;--navbar-stack-height:0px"><div hidden=""><script>try{var r=document.querySelectorAll(".redoc");r=r[r.length-1];var h=document.documentElement,p=h.classList,a=[h.getAttribute("data-bs-theme"),h.getAttribute("data-theme")];var m=p.contains("dark")||a.indexOf("dark")>-1?"dark":p.contains("light")||a.indexOf("light")>-1?"light":localStorage.getItem("redoc.colorMode");if(m!=="light"&&m!=="dark")m=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";r.classList.remove("light","dark");r.classList.add(m)}catch(e){}</script></div><div class="sc-NPrTd fpSCXb menu-content"><div class="sc-bJapvR eXtYur"><div data-testid="search-trigger" class="sc-eavxOQ bStpwo"><div class="sc-dGYDtq hRGWCd"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" data-component-name="icons/SearchIcon/SearchIcon" class="sc-gJaedx dpfGSp"><path d="M14.5 13.7931L10.724 10.017C11.6313 8.9277 12.0838 7.5305 11.9872 6.11608C11.8907 4.70165 11.2525 3.37891 10.2055 2.423C9.15857 1.4671 7.78336 0.951637 6.36602 0.983845C4.94867 1.01605 3.5983 1.59345 2.59582 2.59593C1.59335 3.5984 1.01595 4.94877 0.983738 6.36612C0.95153 7.78347 1.46699 9.15867 2.4229 10.2057C3.3788 11.2526 4.70155 11.8908 6.11597 11.9873C7.53039 12.0839 8.92759 11.6314 10.0169 10.7241L13.793 14.5001L14.5 13.7931ZM2.00001 6.50012C2.00001 5.6101 2.26393 4.74007 2.7584 4.00005C3.25286 3.26003 3.95567 2.68325 4.77794 2.34266C5.6002 2.00207 6.505 1.91295 7.37792 2.08658C8.25083 2.26022 9.05266 2.6888 9.68199 3.31814C10.3113 3.94747 10.7399 4.7493 10.9135 5.62221C11.0872 6.49513 10.9981 7.39993 10.6575 8.22219C10.3169 9.04446 9.7401 9.74726 9.00008 10.2417C8.26006 10.7362 7.39003 11.0001 6.50001 11.0001C5.30694 10.9988 4.16312 10.5243 3.31949 9.68064C2.47587 8.83701 2.00133 7.69319 2.00001 6.50012Z"></path></svg><span>Loading...</span></div></div></div><div class="sc-kLsiYl kAUzfx"><ul data-component-name="Menu/Menu" class="sc-jfJzNu hoWuXt"><div data-component-name="Menu/MenuItem" class="sc-jEekwy kJsUv menu-item-type-default" role="listitem"><a class="sc-gzhUzI kKmXTr" href="/" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="true" class="sc-cToEys eWEtkC"><div class="sc-fIruOr dBwuTA"><span class="sc-gwVKcS efjekG"><span data-text="WeKan REST API" class="sc-hgJgCR dWgUkd">WeKan REST API</span></span></div></li></a></div><div data-component-name="Menu/MenuItem" class="sc-jEekwy kJsUv menu-item-type-default" role="listitem"><a class="sc-gzhUzI kKmXTr" href="/section/production-security-concerns" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-cToEys iVTRMP"><div class="sc-fIruOr dBwuTA"><span class="sc-gwVKcS efjekG"><span data-text="Production Security Concerns" class="sc-hgJgCR dlVNdD">Production Security Concerns</span></span></div></li></a></div><div data-component-name="Menu/MenuItem" class="sc-jEekwy kJsUv menu-item-type-group" role="listitem"><a class="sc-gzhUzI kKmXTr" href="/login" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-cToEys jYycQW"><div class="sc-dGPMcj PCvEP"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-kLEbxe eRqACJ"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-fIruOr dBwuTA"><span class="sc-gwVKcS efjekG"><span data-text="Login" class="sc-hgJgCR dlVNdD">Login</span></span></div></li></a><div style="display:none;height:0px;overflow:hidden" class="sc-fKDFlh joxUYR"></div></div><div data-component-name="Menu/MenuItem" class="sc-jEekwy kJsUv menu-item-type-group" role="listitem"><a class="sc-gzhUzI kKmXTr" href="/attachmentstoragesettings" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-cToEys jYycQW"><div class="sc-dGPMcj PCvEP"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-kLEbxe eRqACJ"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-fIruOr dBwuTA"><span class="sc-gwVKcS efjekG"><span data-text="AttachmentStorageSettings" class="sc-hgJgCR dlVNdD">AttachmentStorageSettings</span></span></div></li></a><div style="display:none;height:0px;overflow:hidden" class="sc-fKDFlh joxUYR"></div></div><div data-component-name="Menu/MenuItem" class="sc-jEekwy kJsUv menu-item-type-group" role="listitem"><a class="sc-gzhUzI kKmXTr" href="/settings" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-cToEys jYycQW"><div class="sc-dGPMcj PCvEP"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-defLine truncated
<script type="module">
import { hydrate } from "https://cdn.redocly.com/redoc/v3.1.0/bundles/redoc.standalone.js";
const __redoc_definition = {"openapi":"3.2.0","info":{"title":"WeKan REST API","version":"v13.12","description":"The REST API allows you to control and extend WeKan with ease.\n\nIf you are an end-user and not a dev or a tester, [create an issue](https://github.com/wekan/wekan/issues/new) to request new APIs.\n\n> All API calls in the documentation are made using `curl`. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.\n\n# Production Security Concerns\nWhen calling a production WeKan server, ensure it is running via HTTPS and has a valid SSL Certificate. The login method requires you to post your username and password in plaintext, which is why we highly suggest only calling the REST login api over HTTPS. Also, few things to note:\n\n* Only call via HTTPS\n* Implement a timed authorization token expiration strategy\n* Ensure the calling user only has permissions for what they are calling and no more\n"},"paths":{"/users/login":{"post":{"operationId":"login","summary":"Login with REST API","tags":["Login"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}}},"description":"Login credentials","required":true},"responses":{"200":{"description":"Successful authentication","content":{"*/*":{"schema":{"type":"object","required":["id","token","tokenExpires"],"properties":{"id":{"type":"string","description":"User ID"},"token":{"type":"string","description":"Authentication token\n"},"tokenExpires":{"type":"string","format":"date-time","description":"Token expiration date\n"}}}}}},"400":{"description":"Error in authentication\n","content":{"*/*":{"schema":{"type":"object","properties":{"error":{"type":"string"},"reason":{"type":"string"}}}}}},"default":{"description":"Error in authentication\n"}}}},"/users/register":{"post":{"operationId":"register","summary":"Register with REST API","description":"Notes:\n - You will need to provide the token for any of the authenticated methods.\n","tags":["Login"],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"},"email":{"description":"Your email\n","type":"string"}},"required":["username","password","email"]}}},"required":true},"responses":{"200":{"description":"Successful registration","content":{"*/*":{"schema":{"items":{"properties":{"id":{"type":"string"},"token":{"type":"string"},"tokenExpires":{"type":"string"}}}}}}},"400":{"description":"Error in registration\n","content":{"*/*":{"schema":{"items":{"properties":{"error":{"type":"number"},"reason":{"type":"string"}}}}}}},"default":{"description":"Error in registration\n"}}}},"/api/admin/attachment-settings":{"get":{"operationId":"get_attachment_settings","summary":"Get the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. Returns the Admin Panel >\n Attachments settings document, including `limitSettings.attachmentsUploadBlocked`\n and `limitSettings.avatarsUploadBlocked` plus the size limits. Cloud-storage\n secret keys (S3 secretAccessKey, Azure accountKey/connectionString, GCS\n credentials) are masked and never returned; a boolean `\u003cfield>Set` marker\n shows whether a value exists.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}},"put":{"operationId":"update_attachment_settings","summary":"Update the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. The request body is a\n partial settings object (for example\n `{\"limitSettings\": {\"avatarsUploadBlocked\": true}}`). It is applied through\n the same update path as the `updateAttachmentStorageSettings` Meteor method,\n so validation and `$set` semantics are identical: cloud-storage secrets left\n blank are preserved. The masked, updated settings document is returned.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}}},"/api/admin/domains":{"get":{"operationId":"get_admin_domains","summary":"List email domains with their user counts (GlobalAdmin)","description":"Only the global admin can call this. CountsLine truncated
const __redoc_definition = {"openapi":"3.2.0","info":{"title":"WeKan REST API","version":"v13.13","description":"The REST API allows you to control and extend WeKan with ease.\n\nIf you are an end-user and not a dev or a tester, [create an issue](https://github.com/wekan/wekan/issues/new) to request new APIs.\n\n> All API calls in the documentation are made using `curl`. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.\n\n# Production Security Concerns\nWhen calling a production WeKan server, ensure it is running via HTTPS and has a valid SSL Certificate. The login method requires you to post your username and password in plaintext, which is why we highly suggest only calling the REST login api over HTTPS. Also, few things to note:\n\n* Only call via HTTPS\n* Implement a timed authorization token expiration strategy\n* Ensure the calling user only has permissions for what they are calling and no more\n"},"paths":{"/users/login":{"post":{"operationId":"login","summary":"Login with REST API","tags":["Login"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}}},"description":"Login credentials","required":true},"responses":{"200":{"description":"Successful authentication","content":{"*/*":{"schema":{"type":"object","required":["id","token","tokenExpires"],"properties":{"id":{"type":"string","description":"User ID"},"token":{"type":"string","description":"Authentication token\n"},"tokenExpires":{"type":"string","format":"date-time","description":"Token expiration date\n"}}}}}},"400":{"description":"Error in authentication\n","content":{"*/*":{"schema":{"type":"object","properties":{"error":{"type":"string"},"reason":{"type":"string"}}}}}},"default":{"description":"Error in authentication\n"}}}},"/users/register":{"post":{"operationId":"register","summary":"Register with REST API","description":"Notes:\n - You will need to provide the token for any of the authenticated methods.\n","tags":["Login"],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"},"email":{"description":"Your email\n","type":"string"}},"required":["username","password","email"]}}},"required":true},"responses":{"200":{"description":"Successful registration","content":{"*/*":{"schema":{"items":{"properties":{"id":{"type":"string"},"token":{"type":"string"},"tokenExpires":{"type":"string"}}}}}}},"400":{"description":"Error in registration\n","content":{"*/*":{"schema":{"items":{"properties":{"error":{"type":"number"},"reason":{"type":"string"}}}}}}},"default":{"description":"Error in registration\n"}}}},"/api/admin/attachment-settings":{"get":{"operationId":"get_attachment_settings","summary":"Get the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. Returns the Admin Panel >\n Attachments settings document, including `limitSettings.attachmentsUploadBlocked`\n and `limitSettings.avatarsUploadBlocked` plus the size limits. Cloud-storage\n secret keys (S3 secretAccessKey, Azure accountKey/connectionString, GCS\n credentials) are masked and never returned; a boolean `\u003cfield>Set` marker\n shows whether a value exists.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}},"put":{"operationId":"update_attachment_settings","summary":"Update the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. The request body is a\n partial settings object (for example\n `{\"limitSettings\": {\"avatarsUploadBlocked\": true}}`). It is applied through\n the same update path as the `updateAttachmentStorageSettings` Meteor method,\n so validation and `$set` semantics are identical: cloud-storage secrets left\n blank are preserved. The masked, updated settings document is returned.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}}},"/api/admin/domains":{"get":{"operationId":"get_admin_domains","summary":"List email domains with their user counts (GlobalAdmin)","description":"Only the global admin can call this. CountsLine truncated
const __redoc_options = {"skipBundle":true,"disableTelemetry":false};
hydrate(__redoc_definition, __redoc_options, document.getElementById('redoc'));
+1 -1
View File
@@ -1,7 +1,7 @@
swagger: '2.0'
info:
title: WeKan REST API
version: v13.12
version: v13.13
description: |
The REST API allows you to control and extend WeKan with ease.
+2 -2
View File
@@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = (
appTitle = (defaultText = "Wekan"),
# The name of the app as it is displayed to the user.
appVersion = 1312,
appVersion = 1313,
# Increment this for every release.
appMarketingVersion = (defaultText = "13.12~2026-11-14"),
appMarketingVersion = (defaultText = "13.13~2026-11-15"),
# Human-readable presentation of the app version.
minUpgradableAppVersion = 0,
+56
View File
@@ -20,6 +20,12 @@ const {
mergeCardScope,
} = require('/models/lib/boardCardScope');
const { sortWithIdTiebreaker } = require('/models/lib/cardSortTiebreaker');
import { otherSwimlaneIdsForFirstActive } from '/models/lib/swimlaneFilter';
import {
DESTINATION_CARD_FIELDS,
MAX_DESTINATION_CARDS,
destinationCardsSelector,
} from '/models/lib/destinationCards';
const { diffCardWindow } = require('/models/lib/cardWindowDiff');
const {
effectiveBoardCardsMode,
@@ -408,3 +414,53 @@ Meteor.publish('boardCardsLoadingMode', async function(boardId) {
this.added('boardCardLoadingModes', boardId, { lazy });
this.ready();
});
// #6917: the cards a destination picker lists - Copy / Move checklist, Copy
// checklist from a template card, Move / Copy card, Link card, and the
// parent-card picker - whatever the board's card-loading mode. A lazily
// loaded board's `board` publication ships no cards, and a picker opens no
// list that would fetch its window, so the card dropdown stayed empty however
// long one waited. This ships only what the dropdown shows
// (models/lib/destinationCards.js) for one list in one swimlane, or for the
// whole board when `listId` is empty, scoped like every other card
// publication: the board must be readable, and an assigned-only member gets
// only the cards assigned to them.
Meteor.publish('destinationCards', async function(boardId, listId, swimlaneId) {
// Match.Any marks each argument checked for audit-argument-checks; a bad
// argument ends this subscription, never the server.
check(boardId, Match.Any);
check(listId, Match.Any);
check(swimlaneId, Match.Any);
const optionalId = Match.OneOf(String, null, undefined);
if (!Match.test(boardId, String) || !boardId) return this.ready();
if (!Match.test(listId, optionalId) || !Match.test(swimlaneId, optionalId)) return this.ready();
const board = await boardVisibleTo(this.userId, boardId);
if (!board) return this.ready();
// The first swimlane also lists the list's orphaned cards, as the board view
// and list.cardsUnfiltered() do (#6443); archived swimlanes stay excluded
// (#6659).
let otherSwimlaneIds;
if (listId && swimlaneId) {
const swimlanes = await ReactiveCache.getSwimlanes(
{ boardId: board._id },
{ sort: { sort: 1 }, fields: { _id: 1, archived: 1 } },
);
otherSwimlaneIds = otherSwimlaneIdsForFirstActive(swimlanes, swimlaneId);
}
const selector = mergeCardScope(
destinationCardsSelector(listId, swimlaneId, otherSwimlaneIds),
{
boardId: board._id,
archived: false,
...assignedOnlyCardScope(board, this.userId),
},
);
return Cards.find(selector, {
fields: DESTINATION_CARD_FIELDS,
sort: { sort: 1, _id: 1 },
limit: MAX_DESTINATION_CARDS,
});
});
+4 -4
View File
@@ -19,7 +19,7 @@
# base: core24 (not core26) · no build-base: devel · grade: stable
# ─────────────────────────────────────────────────────────────────────────────
name: wekan
version: '13.12'
version: '13.13'
base: core24
confinement: strict
grade: stable
@@ -703,18 +703,18 @@ parts:
armhf) WEKAN_ARCH="armhf" ;;
*) echo "Unsupported architecture: ${CRAFT_ARCH_BUILD_FOR}"; exit 1 ;;
esac
WEKAN_ZIP="wekan-13.12-${WEKAN_ARCH}.zip"
WEKAN_ZIP="wekan-13.13-${WEKAN_ARCH}.zip"
echo "Cleaning environment first"
rm -rf .build
mkdir .build
cd .build
#wget https://github.com/wekan/wekan/releases/download/v13.12/wekan-13.12-amd64.zip
#wget https://github.com/wekan/wekan/releases/download/v13.13/wekan-13.13-amd64.zip
# Retry: a release asset can 404 for a short while right after it is
# uploaded (CDN/propagation lag), and a plain wget treated that as
# fatal. --retry-on-http-error covers the 404/5xx window; a genuinely
# missing bundle still fails after the tries are spent.
wget --tries=20 --waitretry=20 --retry-on-http-error=404,403,500,502,503 \
"https://github.com/wekan/wekan/releases/download/v13.12/${WEKAN_ZIP}"
"https://github.com/wekan/wekan/releases/download/v13.13/${WEKAN_ZIP}"
unzip "${WEKAN_ZIP}"
python3 ../releases/check-telemetry.py --bundle bundle
rm "${WEKAN_ZIP}"
+9 -4
View File
@@ -159,10 +159,15 @@ check('both the window and its COUNT are narrowed', () => {
// guard that counts its own explanation counts wrong.
const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
const uses = code.match(/\.\.\.assignedOnlyCardScope\(/g) || [];
assert.strictEqual(uses.length, 2,
'boardCardsWindow AND boardListCardCount — an unrestricted count would still '
+ 'tell the member how many cards the list really holds, and offer to scroll '
+ 'in cards that never arrive');
// #6917 added a third card publication, destinationCards (the card titles a
// destination picker lists); it is narrowed the same way.
assert.strictEqual(uses.length, 3,
'boardCardsWindow, boardListCardCount AND destinationCards — an unrestricted '
+ 'count would still tell the member how many cards the list really holds, '
+ 'and an unrestricted picker would list the titles of cards they may not see');
const picker = code.slice(code.indexOf("Meteor.publish('destinationCards'"));
assert.ok(/\.\.\.assignedOnlyCardScope\(board, this\.userId\)/.test(picker),
'destinationCards narrows its cards for an assigned-only member');
// The children (comments/attachments/checklists/items) hang off windowCardIds,
// which builds on the same selector — so they cannot leak past the restriction.
+322
View File
@@ -0,0 +1,322 @@
'use strict';
// #6917: Copy Checklist's card dropdown stayed empty for a board with more
// cards than the lazy-loading threshold. The destination pickers read the
// cards of the chosen board from the `board` publication, which ships none
// for a board that loads lazily, and a picker opens no list that would fetch
// its window. The pickers now subscribe to `destinationCards`, which ships one
// list's card titles whatever the card-loading mode.
//
// This runs:
// - the pure selector and field list (models/lib/destinationCards.js);
// - the real publication handler in a vm, against a stub database: readable
// board, unreadable board, an assigned-only member, bad arguments;
// - the real picker dialog with a lazy destination board: the board's
// swimlanes and lists arrive, its cards only through destinationCards;
// - a source scan: every client picker that lists another board's cards
// subscribes to destinationCards.
//
// Run: node tests/destinationCards6917.test.cjs
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const ROOT = path.join(__dirname, '..');
const read = rel => fs.readFileSync(path.join(ROOT, rel), 'utf8');
const script = rel => read(rel).replace(/^import [\s\S]*?;\n/gm, '').replace(/^export \{[^}]*\};?\n/gm, '').replace(/^export /gm, '');
let checks = 0;
function test(name, fn) {
return Promise.resolve().then(fn).then(() => {
checks += 1;
console.log(' ok -', name);
});
}
// A small Mongo-ish matcher, enough for the selectors built here.
function matches(doc, sel) {
return Object.entries(sel).every(([key, cond]) => {
if (key === '$and') return cond.every(part => matches(doc, part));
const value = doc[key];
if (cond && typeof cond === 'object' && !Array.isArray(cond)) {
if ('$in' in cond) {
const values = Array.isArray(value) ? value : [value];
return cond.$in.some(v => values.includes(v) || (v == null && value == null));
}
if ('$nin' in cond) return !cond.$nin.includes(value);
if ('$ne' in cond) return value !== cond.$ne;
}
if (key === 'archived') return !value === !cond;
return value === cond;
});
}
(async () => {
const { DESTINATION_CARD_FIELDS, MAX_DESTINATION_CARDS, destinationCardsSelector } =
await import('../models/lib/destinationCards.js');
const { listCardsSelector } = await import('../models/lib/swimlaneFilter.js');
await test('selector: one list in one swimlane is exactly the board view\'s list selector', () => {
assert.deepEqual(destinationCardsSelector('L1', 'S2'), listCardsSelector('L1', 'S2'));
assert.deepEqual(destinationCardsSelector('L1', 'S1', ['S2']), listCardsSelector('L1', 'S1', ['S2']));
});
await test('selector: a list with no swimlane is the whole list, no list is the whole board', () => {
assert.deepEqual(destinationCardsSelector('L1', null), { listId: 'L1', archived: false });
assert.deepEqual(destinationCardsSelector(null, null), { archived: false });
assert.deepEqual(destinationCardsSelector('', 'S1'), { archived: false });
});
await test('negative: the picker fields carry no card content beyond the title', () => {
for (const field of ['description', 'members', 'assignees', 'customFields', 'receivedAt', 'userId']) {
assert.ok(!(field in DESTINATION_CARD_FIELDS), `${field} must not be published to a picker`);
}
for (const field of ['_id', 'title', 'sort', 'listId', 'swimlaneId', 'archived', 'type']) {
assert.equal(DESTINATION_CARD_FIELDS[field], 1, `${field} is needed by a picker`);
}
assert.ok(MAX_DESTINATION_CARDS >= 1144, 'the reported board\'s cards fit');
});
// ---- the publication handler, run for real -----------------------------
const src = read('server/publications/cardsWindow.js');
const start = src.indexOf("Meteor.publish('destinationCards'");
assert.ok(start > 0, 'destinationCards is published');
const open = src.indexOf('async function', start);
let depth = 0;
let end = src.indexOf('{', open);
for (; end < src.length; end += 1) {
if (src[end] === '{') depth += 1;
else if (src[end] === '}' && --depth === 0) break;
}
const handlerSource = src.slice(open, end + 1);
const { assignedOnlyCardScope, mergeCardScope } = require('../models/lib/boardCardScope.js');
const { otherSwimlaneIdsForFirstActive } = await import('../models/lib/swimlaneFilter.js');
const boards = {
big: { _id: 'big', members: [
{ userId: 'u', isActive: true },
{ userId: 'ao', isActive: true, isNormalAssignedOnly: true },
] },
secret: { _id: 'secret', members: [] },
};
const cards = [];
for (let i = 0; i < 1144; i += 1) {
cards.push({ _id: `c${i}`, title: `Card ${i}`, boardId: 'big', listId: i % 2 ? 'L2' : 'L1',
swimlaneId: 'S1', sort: i, archived: false, description: 'secret text', assignees: i === 4 ? ['ao'] : [] });
}
cards.push({ _id: 'gone', title: 'Archived', boardId: 'big', listId: 'L1', swimlaneId: 'S1', sort: 0, archived: true });
cards.push({ _id: 'orphan', title: 'Orphan', boardId: 'big', listId: 'L1', swimlaneId: 'deleted', sort: 0, archived: false });
cards.push({ _id: 'lane2', title: 'Lane 2', boardId: 'big', listId: 'L1', swimlaneId: 'S2', sort: 0, archived: false });
cards.push({ _id: 'hidden', title: 'Other board', boardId: 'secret', listId: 'L1', swimlaneId: 'S1', sort: 0, archived: false });
const Match = {
Any: 'any',
OneOf: (...types) => ({ oneOf: types }),
test(value, type) {
if (type && type.oneOf) return type.oneOf.some(t => Match.test(value, t));
// The handler runs in a vm, whose String is not this file's.
if (typeof type === 'function' && type.name === 'String') return typeof value === 'string';
if (type === null) return value === null;
if (type === undefined) return value === undefined;
return true;
},
};
const handler = vm.runInNewContext(`(${handlerSource})`, {
check() {}, Match,
boardVisibleTo: async (userId, id) => (id === 'big' && userId ? boards.big : null),
ReactiveCache: { getSwimlanes: async () => [{ _id: 'S1' }, { _id: 'S2' }] },
otherSwimlaneIdsForFirstActive, mergeCardScope, assignedOnlyCardScope,
destinationCardsSelector, DESTINATION_CARD_FIELDS, MAX_DESTINATION_CARDS,
Cards: { find: (selector, options) => ({ selector, options,
docs: cards.filter(c => matches(c, selector)).sort((a, b) => a.sort - b.sort).slice(0, options.limit) }) },
});
const publish = async (userId, ...args) => {
let readied = false;
const result = await handler.call({ userId, ready() { readied = true; } }, ...args);
return { result, readied };
};
await test('a lazily loaded board\'s list: every one of its cards, with titles only', async () => {
const { result } = await publish('u', 'big', 'L1', 'S1');
// S1 is the first swimlane: its orphaned card is listed too, S2's is not.
const ids = result.docs.map(c => c._id);
assert.equal(ids.filter(id => /^c\d+$/.test(id)).length, 572);
assert.ok(ids.includes('orphan'));
assert.ok(!ids.includes('lane2'));
assert.ok(!ids.includes('gone'), 'no archived card');
assert.equal(result.options.fields, DESTINATION_CARD_FIELDS);
assert.equal(result.options.limit, MAX_DESTINATION_CARDS);
});
await test('the second swimlane lists only its own cards', async () => {
const { result } = await publish('u', 'big', 'L1', 'S2');
assert.deepEqual(result.docs.map(c => c._id), ['lane2']);
});
await test('no list: the whole board, never another board\'s card', async () => {
const { result } = await publish('u', 'big', null, null);
assert.equal(result.docs.length, 1144 + 2);
assert.ok(!result.docs.some(c => c.boardId !== 'big'));
});
await test('negative: an assigned-only member gets only the cards assigned to them', async () => {
const { result } = await publish('ao', 'big', null, null);
assert.deepEqual(result.docs.map(c => c._id), ['c4']);
});
await test('negative: an unreadable board, no user, or bad arguments publish nothing', async () => {
for (const args of [
['u', 'secret', 'L1', 'S1'],
[null, 'big', 'L1', 'S1'],
['u', null, 'L1', 'S1'],
['u', '', 'L1', 'S1'],
['u', { $ne: null }, 'L1', 'S1'],
['u', 'big', { $ne: null }, 'S1'],
['u', 'big', 'L1', { $gt: '' }],
]) {
const { result, readied } = await publish(...args);
assert.equal(readied, true, `ready for ${JSON.stringify(args)}`);
assert.equal(result, undefined, `no cursor for ${JSON.stringify(args)}`);
}
});
// ---- the picker dialog, with a lazy destination board ------------------
class ReactiveVar {
constructor(value) { this.value = value; }
get() { return this.value; }
set(value) { this.value = value; }
}
function dialogWorld() {
const docs = { swimlanes: [{ _id: 'A-lane', boardId: 'A', sort: 0 }],
lists: [{ _id: 'A-list', boardId: 'A', swimlaneId: '', sort: 0 }], cards: [] };
const find = coll => q => ({ fetch: () => docs[coll].filter(d => matches(d, Object.fromEntries(
Object.entries(q).filter(([k]) => k !== 'deletedAt')))).sort((a, b) => a.sort - b.sort) });
const context = {
ReactiveVar,
ReactiveCache: {
getList: q => {
const l = docs.lists.find(d => d._id === q._id && d.boardId === q.boardId);
return l ? { ...l, cardsUnfiltered: sl => docs.cards.filter(c => c.listId === l._id && c.swimlaneId === sl) } : null;
},
getCurrentUser: () => ({ getTemplatesBoardId: () => 'T' }),
},
Boards: { findOne: id => ({ _id: id }), find: () => ({ fetch: () => [] }) },
Swimlanes: { find: find('swimlanes') },
Lists: { find: find('lists') },
Utils: { getCurrentBoardId: () => 'A' },
TAPi18n: { __: k => k },
Meteor: { userId: () => 'u' },
};
const Base = vm.runInNewContext(`${script('client/lib/dialogWithBoardSwimlaneList.js')}\nBoardSwimlaneListDialog`, context);
const Card = vm.runInNewContext(`${script('client/lib/dialogWithBoardSwimlaneListCard.js')}\nBoardSwimlaneListCardDialog`,
{ ...context, BoardSwimlaneListDialog: Base });
const subs = [];
const autoruns = [];
const tpl = {
view: { onViewDestroyed() {} },
subscribe(name, ...args) {
const handle = { name, args, stopped: false, stop() { this.stopped = true; }, callbacks: args.at(-1) };
subs.push(handle);
return handle;
},
// Enough of Blaze's template autorun: re-run on demand, stopping the
// subscriptions the previous run made, as Blaze does.
autorun(fn) {
const run = { fn, subs: [] };
run.go = () => {
run.subs.forEach(s => s.stop());
const before = subs.length;
fn();
run.subs = subs.slice(before);
};
autoruns.push(run);
run.go();
},
};
const rerun = () => autoruns.forEach(r => r.go());
const big = () => {
docs.swimlanes.push({ _id: 'B-lane', boardId: 'B', sort: 0 });
docs.lists.push({ _id: 'B-list', boardId: 'B', swimlaneId: '', sort: 0 });
};
const bigCards = () => {
for (let i = 0; i < 3; i += 1) docs.cards.push({ _id: `B-card${i}`, title: `T${i}`, boardId: 'B', listId: 'B-list', swimlaneId: 'B-lane', sort: i });
};
return { Card, tpl, subs, rerun, big, bigCards };
}
await test('a lazy destination board: the list\'s cards arrive through destinationCards and fill the dropdown', () => {
const w = dialogWorld();
const dialog = new w.Card(w.tpl, { getDialogOptions: () => ({ A: { boardId: 'B', swimlaneId: 'B-lane', listId: 'B-list', cardId: 'B-card2' } }) });
w.rerun();
const cardSub = () => w.subs.filter(s => s.name === 'destinationCards').at(-1);
assert.deepEqual(cardSub().args.slice(0, 3), ['B', 'B-list', 'B-lane']);
// The board is ready with its swimlanes and lists, and - being lazy - no cards.
w.big();
w.subs.filter(s => s.name === 'board').at(-1).callbacks.onReady();
assert.equal(dialog.loading.get(), false);
assert.equal(dialog.cardsLoading.get(), true, 'Done waits for the cards');
assert.equal(dialog.selectedCardId.get(), 'B-card2', 'the remembered card is not dropped before its list\'s cards arrive');
assert.deepEqual(dialog.cards(), []);
w.bigCards();
cardSub().callbacks.onReady();
assert.equal(dialog.cardsLoading.get(), false);
assert.deepEqual(dialog.cards().map(c => c._id), ['B-card0', 'B-card1', 'B-card2']);
assert.equal(dialog.selectedCardId.get(), 'B-card2');
});
await test('negative: a remembered card that is gone is cleared once the list\'s cards are there; an old list\'s answer changes nothing', () => {
const w = dialogWorld();
const dialog = new w.Card(w.tpl, { getDialogOptions: () => ({ A: { boardId: 'B', swimlaneId: 'B-lane', listId: 'B-list', cardId: 'B-deleted' } }) });
w.rerun();
const first = w.subs.filter(s => s.name === 'destinationCards').at(-1);
w.big();
w.bigCards();
w.subs.filter(s => s.name === 'board').at(-1).callbacks.onReady();
assert.equal(dialog.selectedCardId.get(), 'B-deleted');
// Another list is chosen before the first list's answer: that answer is stale.
dialog.selectedListId.set('B-other');
w.rerun();
assert.equal(first.stopped, true, 'the previous list\'s subscription stops');
first.callbacks.onReady();
assert.equal(dialog.cardsLoading.get(), true);
dialog.selectedListId.set('B-list');
w.rerun();
w.subs.filter(s => s.name === 'destinationCards').at(-1).callbacks.onReady();
assert.equal(dialog.selectedCardId.get(), '');
assert.equal(dialog.cardsLoading.get(), false);
});
await test('the open board\'s Filter does not empty the destination dropdown', () => {
const cardSrc = read('client/lib/dialogWithBoardSwimlaneListCard.js');
assert.match(cardSrc, /list\.cardsUnfiltered\(swimlaneId\)/);
assert.doesNotMatch(cardSrc, /list\.cards\(swimlaneId\)/);
});
// ---- every picker of another board's cards uses the subscription --------
await test('negative: no client picker lists another board\'s cards from the board publication alone', () => {
const base = read('client/lib/dialogWithBoardSwimlaneList.js');
const onReady = base.slice(base.indexOf("this.tpl.subscribe('board'"), base.indexOf('onStop:'));
assert.doesNotMatch(onReady, /cards\(\)/, 'the board being ready says nothing about a lazy board\'s cards');
assert.match(read('client/lib/dialogWithBoardSwimlaneListCard.js'), /subscribe\('destinationCards'/);
const details = read('client/components/cards/cardDetails.js');
const parent = details.slice(details.indexOf("'change .js-field-parent-board'"));
assert.match(parent.slice(0, parent.indexOf("'change .js-field-parent-card'")), /subscribe\('destinationCards', selection/);
assert.match(details, /destinationDisabled\(\)[\s\S]{0,400}cardsLoading/);
const listBody = read('client/components/lists/listBody.js');
const link = listBody.slice(listBody.indexOf('Template.linkCardPopup.onCreated'), listBody.indexOf('Template.linkCardPopup.helpers'));
assert.match(link, /subscribe\('destinationCards'/);
const checklists = read('client/components/cards/checklists.js');
assert.match(checklists, /if \(!cardId \|\| !dialog\.cards\(\)\.some/, 'checklist Done needs a card of the list');
assert.match(read('client/components/cards/checklists.jade'), /js-done\(type="button" disabled=doneDisabled\)/);
});
console.log(`destinationCards6917: ${checks} checks passed`);
})().catch(err => {
console.error(err);
process.exit(1);
});
@@ -56,7 +56,7 @@ function world() {
getSwimlane: q => docs.swimlanes.find(d => match(d, q)) || null,
getList: q => {
const l = docs.lists.find(d => match(d, q));
return l ? { ...l, cards: sl => docs.cards.filter(c => c.listId === l._id && c.swimlaneId === sl) } : null;
return l ? { ...l, cardsUnfiltered: sl => docs.cards.filter(c => c.listId === l._id && c.swimlaneId === sl) } : null;
},
getCurrentUser: () => ({ getTemplatesBoardId: () => 'T' }),
};
+12 -12
View File
@@ -9,7 +9,7 @@
"mongodb": "^7.8.0"
},
"devDependencies": {
"@playwright/test": "^1.64.1",
"@playwright/test": "^1.65.0",
"axe-core": "^4.14.0"
}
},
@@ -23,13 +23,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.64.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz",
"integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.65.0.tgz",
"integrity": "sha512-iGURLGuKDLakSEl7Pgfv8h7f+XLT+V861Wfhz0zlEl5nyFuKUopdkFBPKqTllTUp06aOwPuH8iuUEb3yp3MpbQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.64.1"
"playwright": "1.65.0"
},
"bin": {
"playwright": "cli.js"
@@ -138,13 +138,13 @@
}
},
"node_modules/playwright": {
"version": "1.64.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz",
"integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.65.0.tgz",
"integrity": "sha512-F3AMWcR44qMBt4kq+6Kb6mX7bjGXshktKFOPULd7reED6ZJWevjbNRqysP7q+AATE9kdPEZBibex1BYsvpVoeA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.64.1"
"playwright-core": "1.65.0"
},
"bin": {
"playwright": "cli.js"
@@ -154,9 +154,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.64.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz",
"integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.65.0.tgz",
"integrity": "sha512-vNSKgH6YqtN7cdT/L334zo0a+YwkrpicyrHJlWWPIm/Q+Nn2SWInil01oT0mbu96U4GgoRtCYQD9CzJQgju+lA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "wekan-playwright-tests",
"private": true,
"devDependencies": {
"@playwright/test": "^1.64.1",
"@playwright/test": "^1.65.0",
"axe-core": "^4.14.0"
},
"dependencies": {
@@ -0,0 +1,46 @@
'use strict';
/**
* #6917: Copy Checklist to a board that loads lazily. The destination's card
* dropdown read the `board` publication, which ships no cards for a board over
* the lazy threshold, so it stayed empty. tests/destinationCards6917.test.cjs
* runs the publication and the dialog; this runs the popup against a real
* server whose destination board stays over the threshold while it is open.
*/
const { test, expect } = require('../fixtures');
const db = require('../helpers/db');
const BoardPage = require('../pages/BoardPage');
const CardPage = require('../pages/CardPage');
test('copy a checklist to a card of a lazily loaded board', async ({ boardPage: page, board, user }) => {
const dest = db.seedBoard({ ownerId: user.id, title: 'Large destination', cardTitlesPerList: [['Target card']] });
const threshold = await page.evaluate(() => Meteor.settings.public.cardsLoadingLazyThreshold ?? 500);
const template = db.findOne('cards', { boardId: dest.boardId, title: 'Target card' });
// The padding lives in another list, so the target list holds one card.
db.insertMany('cards', Array.from({ length: threshold + 1 }, (_, i) => ({ ...template,
_id: db.uid('lazy-padding'), title: 'Lazy padding', listId: dest.listIds[1], sort: 1000 + i })));
try {
await new BoardPage(page).clickCard(board.listIds[0], 'Alpha Card');
const card = new CardPage(page);
await card.waitForOpen();
await card.addChecklist('Template steps');
await card.root.locator('.js-checklist').filter({ hasText: 'Template steps' }).first()
.locator('.js-open-checklist-details-menu').click();
const pop = page.locator('.js-pop-over');
await pop.locator('.js-copy-checklist').click();
await pop.locator('.js-select-boards').selectOption(dest.boardId);
await expect(pop.locator('.js-select-lists option')).not.toHaveCount(0);
await pop.locator('.js-select-lists').selectOption(dest.listIds[0]);
await expect(pop.locator('.js-select-cards option', { hasText: 'Target card' })).toHaveCount(1);
// Negative: the dropdown holds the chosen list's cards, not the padding.
await expect(pop.locator('.js-select-cards option', { hasText: 'Lazy padding' })).toHaveCount(0);
await expect(pop.locator('.js-done')).toBeEnabled();
await pop.locator('.js-done').click();
await expect.poll(() => db.countDocuments('checklists', { cardId: template._id, title: 'Template steps' })).toBe(1);
} finally {
db.cleanup({ boardIds: [dest.boardId] });
}
});