Compare commits

..
367 Commits
Author SHA1 Message Date
Lauri Ojansivu 58ad81e301 continuousBackup integration suite: stop each engine before its directory is removed.
Three tests registered the hook that removes their temp directory before the hook
that stops their engine. node:test runs after-hooks in the order they were
registered, not last-in first-out, so the directory was gone when stop() sealed
into it, and the ENOENT (files/index.ndjson) surfaced after the test as
"generated asynchronous activity after the test ended" - which failed the whole
suite although all 8 tests passed. One hook now stops the engine first and then
removes the directory. Five runs in a row, and the suite runner, pass.

Thanks to xet7 !
2026-11-04 19:20:58 +02:00
Lauri Ojansivu 2912f4b080 Close #6870: attachments are already off site-wide in Admin Panel / Attachments / Transfer limits.
How: Admin Panel / Attachments / Transfer limits, set "Attachments upload" and
"API upload" to Blocked. That is the site-wide "Disable attachments" the issue
asks for, as two settings instead of one checkbox.

Comment for the issue:

  A site-wide switch for this already exists, so a new "Disable attachments"
  setting is not needed. Open Admin Panel / Attachments / Transfer limits and set
  "Attachments upload" and "API upload" to Blocked. While they are on, the server
  refuses an upload on every path - the upload pipeline, the api.attachment.upload
  and api.attachment.copy methods, and the /api/attachment upload,
  upload-background and copy routes - on every board, including boards created
  later, and no board's allowsAttachments flag has to be touched. Download, move
  and delete are not affected, so files uploaded earlier stay reachable. What was
  missing is that a card still showed the "+" button and accepted a drop for an
  upload that would then be refused; since d8b3579117 the card asks the server
  once a minute and hides both. There is no second setting, so nothing can
  disagree with what the server enforces. Two settings instead of one checkbox
  is the one difference from the request.

Thanks to Raymond-Voogt and xet7 !

Fixes #6870,
2026-11-04 19:17:53 +02:00
Lauri Ojansivu 97d1eca09d CHANGELOG: In short for the upcoming release. 2026-11-04 19:12:14 +02:00
Lauri Ojansivu 95b75177b8 CHANGELOG: continuous backup runs on one server at a time, with a lease. 2026-11-04 19:12:06 +02:00
Lauri Ojansivu 880ec2d707 Continuous backup runs on one server at a time, with a lease.
With several WeKan servers on one database, every server applied the continuous
backup setting, so each of them tailed the oplog and wrote its own archives into
the same target directory (#6861, item 3).

The leader holds a lease: one continuousBackupLease document with its id and an
expiry. Taking it is one atomic operation (an insert while there is none, else
an update that matches only an unheld, own or expired lease), the leader renews
it every 15 seconds and it lapses after 45, so another server takes over within
a minute when the leader stops. A server that loses the lease stops its stream.
Startup, saving the settings and a change saved on another server all converge
through that loop, and a restore or a fetch from the cloud takes the lease and
waits one renewal so it is the only writer. One server is always its own leader.

Checked against the real driver on FerretDB (SQLite): 24 clients at once give
exactly one leader; renewal, expiry, loss of the old leader and release behave.
tests/continuousBackupLease.test.cjs pins the same and the wiring in
server/continuousBackup.js. docs/Backup/Continuous-Backup.md no longer says
single-server only.

Thanks to Raymond-Voogt and xet7 !

Refs #6861, not closed: the remaining items of the issue were fixed in 4e28465964.
2026-11-04 19:11:32 +02:00
Lauri Ojansivu dbc7cccea3 CHANGELOG: a card no longer offers the attachment plus when uploads are blocked for the whole site. 2026-11-04 19:07:11 +02:00
Lauri Ojansivu d8b3579117 A card no longer offers the attachment "+" when uploads are blocked for the whole site.
#6870 asks for a site-wide switch that turns attachments off, because the
per-board allowsAttachments flag has to be set on every board. That switch is
there already: Admin Panel / Attachments / Transfer limits, "Attachments upload"
and "API upload" set to Blocked, and the server refuses on every path - the
upload pipeline, api.attachment.upload and .copy, and the /api/attachment
upload, upload-background and copy routes. What was missing is that the card
still showed the "+" button and accepted a drop for an upload that would be
refused.

attachmentUploadPolicy tells a signed-in client the one boolean (nothing about
the limits), asked at most once a minute, and the "+" and the drop/paste upload
follow it. No second setting is added, so there is nothing that could disagree
with what the server enforces. tests/attachmentUploadPolicy.test.cjs pins the
client, the method, and the server enforcement it relies on, and that no
"disableAttachments" setting exists.

Thanks to Raymond-Voogt and xet7 !

Refs #6870, not closed: the reporter asked for a single Features checkbox, and the
two Blocked limits are two settings.
2026-11-04 19:07:09 +02:00
Lauri Ojansivu c7e53e9f38 CHANGELOG: an integration update for a missing or wrong-board integration returns 404. 2026-11-04 19:04:14 +02:00
Lauri Ojansivu 6012521ccc An integration update for a missing or wrong-board integration returns 404.
PUT /api/boards/:boardId/integrations/:intId answered 200 with the requested id
when no integration matched that id and board (PR #6871). The two activities
routes (POST and DELETE .../activities) had the same fault: they updated
nothing and answered 200 with `data: null`. All three now look the integration
up on that board after board-admin authorization and answer the JSON 404 the GET
route already uses (#6862), before validating or writing anything.

tests/restIntegrationUpdate.test.cjs and the Playwright spec are the PR's.
tests/restIntegrationWriteRoutes404.test.cjs adds the activities routes and
pins, for the whole file, that every handler that updates an integration looks
it up first and only after authorization.

Thanks to AhmedLukman and xet7 !
2026-11-04 19:04:12 +02:00
Lauri Ojansivu 0195a5b3d4 Merge remote-tracking branch 'origin/main' 2026-11-04 15:02:36 +02:00
Lauri Ojansivu b12e761afb CHANGELOG: missing-board exports return 404 and an over-long export token 400. 2026-11-04 14:50:49 +02:00
Lauri Ojansivu 0c0d666ffa Missing-board exports return 404, and an over-long export token returns 400.
The board Excel export answered a missing board with HTTP 200 and the text
"Board not found" (PR #6869). The two PDF export routes had the same fault, and
the Excel and PDF routes answered an over-long auth token with 200 too, while
export.js already used 404 and 400. They now choose their status the same way.

tests/boardExcelNotFound.test.cjs and the Playwright spec are the PR's.
tests/exportErrorStatuses.test.cjs scans every models/export*.js route for a
text answer without a status, so the fault cannot return anywhere there.

Thanks to AhmedLukman and xet7 !
2026-11-04 14:50:33 +02:00
Lauri Ojansivu fc2ad0a48b CHANGELOG: nodemailer, body-parser, qs, uuid and postcss-selector-parser raised in the bundle. 2026-11-04 14:45:57 +02:00
Lauri Ojansivu dad03959d9 Raise nodemailer, body-parser, qs, uuid and postcss-selector-parser in the bundle.
A Grype scan of the image found advisories in packages that Meteor's own
packages carry in programs/server/npm/node_modules/meteor/<package>, which
package.json cannot reach.

nodemailer 8.0.11 -> 10.3.1: meteor/email only calls createTransport, the
'stream' plugin hook, nodemailer/lib/well-known and nodemailer/lib/mail-composer,
and all four work in 10.3.1 (checked with the bundle's own package specifiers,
through its exports map). body-parser 1.20.6/1.20.8 -> 1.20.9 and 2.3.0 ->
2.4.0: a minimum may now be "name@MAJOR", so each major is raised inside
itself. qs -> 6.16.1, postcss-selector-parser 6.1.4 -> 6.1.5.

uuid 8.3.2 and 9.0.1 -> 11.1.1: 11 moved its entry point, which is why it was
refused before. The bumper now writes the recorded dist/index.js back as an
ESM re-export of dist/esm/index.js, which require() loads and which leaves
uuid's own ESM and CJS entry points as they are.

glob 14 and minimatch 11 are already in package.json. Run against a copy of
the published image's bundle, the bumper raises all of these and every raised
copy loads.

Thanks to the Grype scan reporter and xet7 !
2026-11-04 14:45:04 +02:00
github-actions[bot] ac960e7d81 Bump versions for v12.94 2026-11-04 01:07:15 +00:00
Lauri Ojansivu 3213d7fa1d Prepare v12.94 release 2026-11-04 03:06:07 +02:00
Lauri Ojansivu 4757676abf CHANGELOG: on a phone in desktop mode the opened card starts below the header bar. 2026-11-04 03:04:05 +02:00
Lauri Ojansivu efaf94242e On a phone in desktop mode the opened card starts below the header bar.
The header wraps to several rows on a phone, far taller than the 8px the
floating card docks at, so the card's top row sat among the header's rows and
was only seen by dragging. Below 801px a card without an inline top, popup
cards included, starts at --wekan-header-height + 8px; a dragged place is an
inline top and is never overridden.

Thanks to the iPhone 12 mini reporter and xet7 !
2026-11-04 03:03:59 +02:00
Lauri Ojansivu d87baa0244 CHANGELOG: the opened card covers the header bar on an iPad. 2026-11-04 02:56:44 +02:00
Lauri Ojansivu 006d24502c The opened card covers the header bar on an iPad: the header drops to the content's level.
On iPad Chrome and Safari (both WebKit) the opened card's top row - the collapse
caret, the card number, the menu, maximize and X - was behind the header bar,
although the card has z-index 2001 and the header 1000. iOS WebKit makes every
scrolling element (#content, the board wrapper) a stacking context of its own, so
the card's z-index is compared only inside it while the header bar, a flex item of
body and a stacking context too, is painted over all of it. While a card is open
the header bar (#header-quick-access) is now at z-index 0, the level of the
content, which comes after it in the page and so paints over it; with no card open
it keeps 1000.

A browser spec makes #content and the board wrapper stacking contexts with
isolation: isolate, which is the same trap, and shows the card over the header open
and collapsed, its close button reachable, in Chromium and WebKit; without the rule
it fails with the card's top at 8 behind a header 88 high, as in the screenshot.

Thanks to xet7 !
2026-11-04 02:56:33 +02:00
Lauri Ojansivu 8f4efbca57 Merge remote-tracking branch 'origin/main' 2026-11-04 01:35:33 +02:00
Lauri Ojansivu dde09dead5 CHANGELOG: where the time goes when a WeKan board is opened. 2026-11-04 01:21:48 +02:00
Lauri Ojansivu 3621413d6f Speed test: where the time goes when a WeKan board is opened, on FerretDB and on MongoDB.
app/board-open.cjs seeds a board of 200, 2000 or 5000 cards, opens it in Chromium and
records the wall clock to the first card and until the DDP WebSocket is quiet, what
crossed it, the Meteor server's CPU by who spends it (Node inspector) and FerretDB's
CPU (pprof). Two runs on each database: the Meteor server's CPU is the same on both
(0.1-0.5 s) and the DDP server is 1% of it; a 5000-card board takes FerretDB (SQLite)
400-600 ms longer to open, the data sent to the browser being the same ~210 KB, a
small board 70-100 ms. FerretDB uses about 4 s of CPU for the 5000-card open: SQLite
39%, filtering in Go 34%, find/getMore/aggregate 0.7-0.8 s each. The README says what
was measured and what was not: the development build, one machine, the browser's own
time not separated, and the aggregate's source not identified.

Thanks to Raymond-Voogt and xet7 !
2026-11-04 01:21:16 +02:00
github-actions[bot] 3c3fc07b3c Bump versions for v12.93 2026-11-03 23:04:07 +00:00
Lauri Ojansivu c1e8976b28 Prepare v12.93 release 2026-11-04 01:02:51 +02:00
Lauri Ojansivu dcce935310 CHANGELOG: the opened card and popups move under a finger. 2026-11-04 01:00:21 +02:00
Lauri Ojansivu 1b05673200 The opened card and every popup move under a finger: Pointer Events and touch-action.
On an iPad the opened card did not move when dragged by a finger from the collapse
caret, the space beside it, the card number, or the drag handle, while the page
behind it scrolled. The card was moved by mousedown / mousemove / mouseup, and a
finger sends none of them while it moves; the drag areas also lacked
touch-action: none, so the browser took the gesture for a scroll. The card window
is now moved by Pointer Events: a press on the header row or the handle moves it, a
press on a link or button in the row (the caret, close, the menu) waits for a real
drag so a tap still does its job and a drag does not also collapse or close, the
title still edits, and with drag handles on the handle alone moves the window. The
card number is above the title's edit zone so it drags too, the window is kept
within reach, and touch-action: none is on the card header, its handle and every
popup's title bar and handle. Blaze hands over a jQuery event without isPrimary, so
the native event is read.

A browser spec drags with a real touch (DevTools protocol, Pointer Events) from the
caret, the number and the space beside the title, with handles on and off, a tap
that still collapses, a mouse that still drags and a popup; without the fix the
finger tests fail.

Thanks to xet7 !
2026-11-04 01:00:19 +02:00
Lauri Ojansivu dafb63299f CHANGELOG: a popup card covers the header bars. 2026-11-04 00:42:59 +02:00
Lauri Ojansivu 84b0480aa8 An opened card opened as a popup covers the header bars, in every browser.
A card opened as a popup (a search result, a table row, a Roadmap/Gantt bar) got
the generic wide-layout rule top: 97px and z-index 30, because the desktop-mode
rules that dock an opened card at the top (top: 8px, z-index 2001) skip a popup.
So it opened BELOW the header, and collapsed it was a bar floating over the
swimlane, while the same file's popup wrapper rule says it must cover the header
bars. The popup card now docks at top: 8px with z-index 2001 in the wide layout;
mobile mode keeps its own full-window rule. A browser spec shows the card over
the header, open and collapsed, in Chromium and WebKit, and fails without the fix
(top 97 against a header 88 high).

Thanks to xet7 !
2026-11-04 00:42:57 +02:00
Lauri Ojansivu f6fc97e77a The full screen button says Minimize while the page is in full screen.
The same button leaves full screen; its title and name were still Maximize, which
does not say so. The existing Minimize translation is used, so no language file
changes.
2026-11-04 00:29:00 +02:00
Lauri Ojansivu d7105e531d CHANGELOG: a full screen button in the header bar. 2026-11-04 00:27:30 +02:00
Lauri Ojansivu f25957dcf3 A full screen button in the header bar, to the right of the logo.
It puts the whole page in full screen with the Fullscreen API (the unprefixed one
and the webkit-prefixed one) and takes it out again; the icon follows the
browser's own events, so Esc is understood. The label is the existing Maximize
translation, so no language file changes. It is drawn only where the browser
offers the API and the page may use it: not on iPhone, not in an iframe without
allowfullscreen. Unit tests drive the pure part with fake documents, including a
refusal; a browser spec checks the position, the label, entering, leaving, Esc,
and that nothing is drawn where the API is not enabled, in Chromium and WebKit.

Thanks to xet7 !
2026-11-04 00:27:28 +02:00
Lauri Ojansivu 984ea93aa8 CHANGELOG: Dependabot alerts 180 and 183-187, and the SQLite membership test fix. 2026-11-04 00:13:58 +02:00
Lauri Ojansivu 2441563008 Speed test: SQLite after the membership test fix (68.3 ms, 4.1x), the Due Cards profile and what was tried.
Thanks to Raymond-Voogt and xet7 !
2026-11-04 00:13:34 +02:00
Lauri Ojansivu f3950b9adc Dependabot alerts 180 and 183-187: minimatch in the other lockfiles, and the Go modules of the embedded benchmark.
minimatch before 11.0.1 (GHSA-g3hv-75wg-m249): npm-packages/exceljs still had
10.2.6 through readdir-glob 3, and the vendored jade lockfile under
meteor-jade-loader had three copies through glob 13 and test-exclude; both get
an override to minimatch 11, as the root lockfile has. A test now reads every
package-lock.json of the repository, not only the reported ones.

The benchmark module of embedded engines pulled klauspost/compress 1.18.0,
golang.org/x/net 0.23.0 and golang/glog 1.0.0 (GHSA-259r-337f-4rfw,
GHSA-5cv4-jp36-h3mw, GHSA-qxp5-gwg8-xv66, GHSA-vvgc-356p-c3xw,
GHSA-6wxm-mpqj-6jpf). They are raised to 1.18.7, 0.62.0 and 1.2.4, and
govulncheck then found more in x/net, x/text and OpenTelemetry, which are raised
too (x/net 0.62.0, x/text 0.43.0, otel 1.42.0): it now reports none. A test
holds every go.mod of the repository at or above these floors.

Thanks to dependabot and xet7 !
2026-11-04 00:12:45 +02:00
Lauri Ojansivu 5e07b8db6c Docs: the speed test, version history and embedded engines READMEs link directories, not README.md files. 2026-11-03 23:59:03 +02:00
Lauri Ojansivu eeeb0dbec9 CHANGELOG: Go databases compared with FerretDB's SQLite. 2026-11-03 23:47:33 +02:00
Lauri Ojansivu 96d56ecc6c Go databases that could be linked into FerretDB, against its SQLite: none is faster where it counts.
A benchmark of its own (docs/Databases/FerretDB/1/SpeedTest/embedded, a Go module,
no cgo) loads the speed test's cards into SQLite set up as FerretDB sets it and
into Pebble, Badger, bbolt, BuntDB, Nutsdb and Clover, and asks each the six
questions WeKan asks of cards, at 15,377 and 153,768 cards. A board's cards take
the same time on every engine that is not slower, because decoding the JSON
dominates; the key-value stores win single reads and writes by microseconds that
a FerretDB call hides; Pebble is 12x slower on fetching a board, Clover 90-190x
slower in total, and bbolt with its default fsync 100-160x slower on writes. The
README accounts for every entry of the awesome-go section.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 23:47:13 +02:00
Lauri Ojansivu aadfd9b6b3 CHANGELOG: Dolt in the speed test and the version history experiment. 2026-11-03 23:19:00 +02:00
Lauri Ojansivu 1c44876beb Speed test: Dolt, and what a database with version history would give WeKan's undo and redo.
Dolt runs under the existing MySQL handler with no new handler and gives the same
answers, but is by far the slowest: 20,470 ms in total, every card query a
2.8 s whole-table scan. The VersionHistory scripts drive WeKan-shaped changes
through FerretDB onto Dolt (a commit per write, undo and redo with DOLT_REVERT,
AS OF, +0.4 ms a write, a conflict when the same document is restored again) and
through the REST API of TerminusDB (author and message per commit, field-level
restore). ClickHouse and StarRocks were tried with the MySQL handler and need a
handler of their own; the README says where each fails.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 23:18:51 +02:00
Lauri Ojansivu bd86b518ea CHANGELOG and tests: the small-scale speed test, pgrust, TiDB and the databases that cannot run FerretDB. 2026-11-03 22:47:16 +02:00
Lauri Ojansivu f576a0cca9 Speed test: TiDB added, and the README says what was tried: pgrust, TiDB, Noria, YugabyteDB, CockroachDB.
TiDB (MySQL protocol, Apache-2.0) runs FerretDB after its metadata table's
generated columns became VIRTUAL, and gives the same answers as MongoDB; it is
a speed test row and opt-in for the conformance run (WEKAN_CONFORMANCE_TIDB=1).
Noria has no JSON type, triggers or generated columns, which the MySQL handler
needs; YugabyteDB refuses a CREATE INDEX on a jsonb expression, which every
FerretDB v1 PostgreSQL index is; CockroachDB is not open source. The README
also records that analyzing a PostgreSQL table after its indexes are created
took the PostgreSQL row from 454 to 145 ms, and that the same on MySQL and
MariaDB did nothing.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 22:45:56 +02:00
Lauri Ojansivu 46c0ad43e8 Speed test: small-scale results ordered fastest to slowest, with times slower, and pgrust.
Results.md now opens with an At a glance table (rank, total, how many times
slower than the fastest, the same for the slowest user), every table is ordered
by its own numbers, the header says the real data size and the real operating
system, and the load table is ordered by the whole load time. pgrust
(PostgreSQL in Rust, malisper/pgrust:v0.4-beta) is one more row through the
same PostgreSQL handler; in the conformance run it is opt-in with
WEKAN_CONFORMANCE_PGRUST=1. The run is 1/20 of a production WeKan so it takes
minutes.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 22:36:56 +02:00
Lauri Ojansivu 499772e4c2 Speed test README: sorting in PostgreSQL was tried and was not faster.
The ten newest of 30,116 users took 52 ms with an ORDER BY in the database and
38-49 ms decoded and sorted in Go, so the change was not kept; the README says
what was measured so it is not repeated.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 21:53:31 +02:00
Lauri Ojansivu 90fe133363 Merge remote-tracking branch 'origin/main' 2026-11-03 21:37:36 +02:00
Lauri Ojansivu b812d36871 CHANGELOG: the speed test README. 2026-11-03 21:24:06 +02:00
Lauri Ojansivu d05a42a5af Speed test README: the FerretDB v1.100-v1.105 and WeKan improvements since the last run.
SQLite sorts and stores JSONB, PostgreSQL uses a GIN index for membership and
pushes down $or, $elemMatch and $and, MySQL and MariaDB read through their
indexes with multikey side tables, and WeKan stopped scanning whole collections
every hour or at every start. Results.md still shows v1.99.0, which the README
now says, and the open items are updated to match.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 21:24:03 +02:00
github-actions[bot] 42507de1d0 Bump versions for v12.92 2026-11-03 18:51:08 +00:00
Lauri Ojansivu 131b0cc509 Prepare v12.92 release 2026-11-03 20:48:47 +02:00
Lauri Ojansivu 93ae5ac6ee Merge remote-tracking branch 'origin/main' 2026-11-03 20:46:48 +02:00
Lauri Ojansivu 34bb91d1f5 CHANGELOG: a missing REST checklist answers a JSON 404. 2026-11-03 20:43:21 +02:00
Lauri Ojansivu d231df56fc REST API: a missing checklist, or one under another card, answers a JSON 404, not an empty 500.
GET /api/boards/:boardId/cards/:cardId/checklists/:checklistId answered 500 with
no body when the checklist did not exist or belonged to another card. It now
answers 404 with the same error text the checklist update and delete routes
use; board access and the parent-card check are unchanged. Route tests drive
the registered handler (found, missing, other card, other board, denied, lookup
failure) and a browser spec reads the checklist over REST and after a reload.

Thanks to AhmedLukman and xet7 !
2026-11-03 20:43:06 +02:00
github-actions[bot] e422ab691e Bump versions for v12.91 2026-11-03 18:01:08 +00:00
Lauri Ojansivu 3f712a0445 Prepare v12.91 release 2026-11-03 19:59:16 +02:00
Lauri Ojansivu d99dadadee Merge remote-tracking branch 'origin/main' 2026-11-03 19:42:04 +02:00
Lauri Ojansivu a58b7bee2c CHANGELOG: code scanning alert 600, a test replacing getLabel with itself. 2026-11-03 19:17:12 +02:00
Lauri Ojansivu 62da564a1e Code scanning #600: a test no longer replaces getLabel with itself, and the guard knows anchored patterns.
tests/boardLabelLookup.test.cjs sliced the getLabel method out of models/boards.js
and then ran .replace(/^getLabel/, 'getLabel') on it, which does nothing: the
slice already starts at getLabel. tests/noIdentityReplacement.test.cjs skipped
every regular expression with an anchor, so it could not see it; a leading ^ or
a trailing $ over plain characters is now compared like any other.

Thanks to GitHub CodeQL and xet7 !
2026-11-03 19:16:44 +02:00
github-actions[bot] 2ada2e2d61 Bump versions for v12.90 2026-11-03 15:58:18 +00:00
Lauri Ojansivu 5de1acf496 Prepare v12.90 release 2026-11-03 17:57:04 +02:00
Lauri Ojansivu 3008bb5606 Merge remote-tracking branch 'origin/main' 2026-11-03 17:55:41 +02:00
Lauri Ojansivu 4f7521ae3c Tigre: the login page background label uses the Tigre word for color, not the Tigrinya one.
Thanks to Raymond-Voogt and xet7 !
2026-11-03 17:31:40 +02:00
Lauri Ojansivu 1c63049913 CHANGELOG: login page background color and a readable support page. 2026-11-03 17:20:07 +02:00
Lauri Ojansivu 6b27764452 Admin Panel: a login page background colour, and a readable layout for the login text and the support page.
Lay-out has a new Login page background color (loginBackgroundColor). Only a
hex colour is accepted, so the value in the CSS custom property can never carry
CSS; the text under the login logo turns white on a dark colour and black on a
light one. The colour is on body only while the sign-in layout shows. The
support page and the text under the login logo get a centred column with sized,
bold headings and space between paragraphs. The label is translated into every
language file.

Thanks to Raymond-Voogt and xet7 !

Fixes #6866,
2026-11-03 17:19:22 +02:00
Lauri Ojansivu ae49937b44 Tests: the Playwright specs expect the accessibility-audit colours and drop below the last checklist row.
The belize and nephritis buttons, the limegreen header, the red-string and
typed-relation lines are now the darker shades that keep their contrast, and
the checklist drag released on the sortable's own bottom edge.
2026-11-03 16:48:44 +02:00
Lauri Ojansivu 7f27ea1f26 CHANGELOG: Show focus outline, and Escape closes the add-list composer. 2026-11-03 16:04:58 +02:00
Lauri Ojansivu 144777ae6b Member Settings: a per-member switch to hide the focus outline.
Change Settings has Show focus outline, on by default: the blue ring round the
focused control is an accessibility feature (WCAG 2.4.7), and it also shows
after a click on a minicard. A member who turns it off - profile.hideFocusOutline,
a toggle method that takes the signed-in user only - gets a class on body that
hides the outline and its halo for them and nobody else. The label is translated
into every language file (best effort; the low-confidence ones are listed in the
changelog).

Thanks to mimZD and xet7 !

Refs #6755,
2026-11-03 16:04:44 +02:00
Lauri Ojansivu f4b5025a7b Escape closes the inline add-list composer.
The composer opened after a list (or in an empty swimlane) stays open after a
list is created, so several can be added, and only its X closed it. Escape now
closes it at the level of an inlined form, with the same reset as the X, and not
on a click elsewhere.

Thanks to mimZD and xet7 !

Fixes #6865,
2026-11-03 15:39:24 +02:00
github-actions[bot] cdcf8ec39d Bump versions for v12.89 2026-11-03 13:35:18 +00:00
Lauri Ojansivu 739f9ed7a0 Prepare v12.89 release 2026-11-03 15:33:40 +02:00
Lauri Ojansivu f06d082e34 Merge remote-tracking branch 'origin/main' 2026-11-03 15:33:13 +02:00
Lauri Ojansivu 4036b16774 CHANGELOG: shortcuts run once on a non-Latin keyboard. 2026-11-03 15:30:41 +02:00
Lauri Ojansivu 0110e84ce5 Keyboard shortcuts run once on a non-Latin layout.
On a Farsi (or Cyrillic) keyboard the d shortcut opened the Due date popup and
closed it at a glance. keyboard.js re-dispatched every key typed on another
script as its Latin key, a leftover from Mousetrap; hotkeys-js resolves the key
from event.code itself, so the shortcut ran twice and the repeat clicked the
same control again, closing it. The re-dispatch is removed, and a test drives
real key events for Latin, Farsi and Cyrillic at the library.

Thanks to mimZD and xet7 !

Fixes #6840,
2026-11-03 15:30:39 +02:00
github-actions[bot] 00a50ae139 Bump versions for v12.88 2026-11-03 12:58:20 +00:00
Lauri Ojansivu c67a67bc9a Prepare v12.88 release 2026-11-03 14:57:20 +02:00
Lauri Ojansivu 0b8a7face3 CHANGELOG: every theme and chosen color is readable; padded REST labels. 2026-11-03 12:37:32 +02:00
Lauri Ojansivu 8b2114cd0f Themes: every board theme and every color a member chooses is readable.
Eight themes (nephritis, pumpkin, moderatepink, strongcyan, limegreen, relax,
corteza, modern) had white text on an accent at 2.1 to 4.3:1: their accents and
hover, selected and emphasis shades are darkened until it reads at 4.5:1. The
filter sidebar's headings, the clear themes' quiet text and cleanlight's
add-dependency link are darker. A color a member chooses is made legible: a
custom theme accent or slide end is darkened under the header's white text, the
text color is moved to read on the theme's surfaces and no longer reaches the
header, labels and primary buttons, a dependency line is drawn at 3:1, Kanri's
tag text is the better of dark and light, and a custom label, card, list or
swimlane color already derives its text. All 25 themes show no colour contrast
failure on the board, the sidebars, Card Settings, the card and its popups.

Thanks to Samunosuke and xet7 !
2026-11-03 12:37:10 +02:00
Lauri Ojansivu 0426c7741a Board labels: a padded name finds the stored label, in every way a label is added.
A label is looked up by name and color before one is added, and the lookup
compared the name as sent while the schema stores it trimmed: a padded ' Bug '
missed the stored 'Bug', and each repeat created another label with the same
name and color. The REST builder now trims, as the pull request did, and so
does the lookup itself, which the card's label form, the importers and the list
sync share. A legacy label stored with padding is still found.

Thanks to AhmedLukman and xet7 !
2026-11-03 12:37:10 +02:00
Lauri Ojansivu c2a28a9233 CHANGELOG: the dark board themes are readable again. 2026-11-03 11:42:09 +02:00
Lauri Ojansivu a6b10e3592 Dark themes: the quiet greys, Card Settings, popups and sidebars are readable again.
The accessibility audit made the quiet text greys dark for every theme, and
old rules never followed the dark themes: Card Settings drew white text on
light grey blocks, popup items, the sidebar's shortcuts row, quiet buttons and
sidebar labels were black on dark, and an !important list header beat each
theme's own. The quiet text is a variable with a light value on the dark
surfaces; Card Settings' sections, popup items and the other rules follow the
theme. axe-core finds no colour contrast failure in cleandark, moderndark, dark
and exodark on the board, sidebar, Card Settings, an opened card and the custom
fields popup.

Thanks to Samunosuke and xet7 !

Fixes #6863,
2026-11-03 11:41:58 +02:00
github-actions[bot] 1ac4d1b1f9 Bump versions for v12.87 2026-11-02 22:34:10 +00:00
Lauri Ojansivu 222492f313 Prepare v12.87 release 2026-11-03 00:33:10 +02:00
Lauri Ojansivu 4f95b10f0d Merge remote-tracking branch 'origin/main' 2026-11-03 00:26:43 +02:00
Lauri Ojansivu 86a18665f0 CHANGELOG: REST single-item GETs answer 404 when nothing is found. 2026-11-03 00:22:26 +02:00
Lauri Ojansivu 1ded34dfdd REST API: a single-item GET that finds nothing answers 404, not 200 with an empty body.
The scoped card GET (/api/boards/:boardId/lists/:listId/cards/:cardId) sent
200 with no body for a missing, wrong-list, wrong-board or archived card. The
list, swimlane, custom field, comment and integration GETs did the same, and
the user GET threw a TypeError for an unknown name. Each now answers 404 after
the board check, so a stranger still learns nothing about which ids exist.

Thanks to AhmedLukman and xet7 !
2026-11-03 00:22:24 +02:00
github-actions[bot] db791e1d40 Bump versions for v12.86 2026-11-02 17:03:48 +00:00
Lauri Ojansivu e8e22dc25e Prepare v12.86 release 2026-11-02 19:02:52 +02:00
Lauri Ojansivu 08d20930de Merge remote-tracking branch 'origin/main' 2026-11-02 18:56:24 +02:00
Lauri Ojansivu 10f244de77 CHANGELOG: daily jobs run once per installation. 2026-11-02 18:49:22 +02:00
Lauri Ojansivu 4e28465964 Daily jobs run once per installation: notification cleanup and the file integrity audit.
The notification cleanup ran at every start and daily on every server, reading
and rewriting the same users 24 times at a rollout; a notification added by
another server between a read and its write was lost. A run now claims a
document per job and day in dailyJobRuns, and the timer looks hourly for an
unclaimed day. The file integrity audit read 'lastRun', checked it, then wrote
it, so two servers could both scan; one atomic update now claims the run.

Continuous backup has no leader: that is documented as single-server, and the
lease it needs is not built, so #6861 item 3 stays open and this commit does
not close it.

Thanks to Raymond-Voogt and xet7 !
2026-11-02 18:49:19 +02:00
github-actions[bot] 2936a682ce Bump versions for v12.85 2026-11-02 15:27:08 +00:00
Lauri Ojansivu ab80aaf9a3 Prepare v12.85 release 2026-11-02 17:26:05 +02:00
Lauri Ojansivu 0fb943b6fb Merge remote-tracking branch 'origin/main' 2026-11-02 17:25:22 +02:00
Lauri Ojansivu 6d8b0c15ec CHANGELOG: a cleared connection pool is a lost connection; the board key backfill uses the key index. 2026-11-02 17:18:17 +02:00
Lauri Ojansivu abc23a7eda Database problems: a cleared connection pool is a lost connection; the board key backfill uses the key index.
collection2 wraps a failed write as 'ValidationError: Failed validation <driver
text>', and the driver's 'Connection pool for <host> was cleared' names a
replica set member that MONGO_URL does not list, so the schema rule matched.
The text is now a lost connection. The board key backfill looked for boards
without a key with an $or including an exists-false branch, a scan of every
board at every start of every server; one $in on the key index finds the same
boards.

Thanks to Raymond-Voogt and xet7 !

Fixes #6860,
2026-11-02 17:17:58 +02:00
Lauri Ojansivu d9aa210012 Accessibility: the results of the fixes in Audit.md, page by page. 2026-11-02 17:17:07 +02:00
Lauri Ojansivu 2e8e1e2ce2 CHANGELOG: the accessibility audit findings are fixed in the default theme. 2026-11-02 16:08:47 +02:00
Lauri Ojansivu c8bcc4817d Accessibility: fix the findings of the axe-core audit in the default theme.
The default theme's blue is #2573a7, so the header's white and #f2f2f2 text
passes WCAG AA; label colours, quiet greys and grey tiles have text that
passes too. The Starred menu, the user menu and initials avatars carry their
visible text in their accessible name. The card's Pomodoro field and the
comment and description editors have a name. A card section heading is an h3
holding a real button with aria-expanded, and its menu and layout toggle sit
beside it. The sign-in form is the main landmark with a level-one title, every
page has a level-one heading, and the shortcuts list is reachable by keyboard.

After the fixes axe-core finds no violation on 22 pages. Spec 45 runs the scan
and tests/accessibilityAuditFixes.test.cjs pins each fault by its shape.

Thanks to xet7 !
2026-11-02 16:08:32 +02:00
Lauri Ojansivu 2dd25c733e Accessibility: the result of an automated axe-core audit of seven pages.
Thanks to xet7 !
2026-11-02 15:03:54 +02:00
github-actions[bot] cb6c6533c5 Bump versions for v12.84 2026-11-02 12:45:02 +00:00
Lauri Ojansivu 292b804474 Prepare v12.84 release 2026-11-02 14:43:46 +02:00
Lauri Ojansivu 623fccf06e Merge remote-tracking branch 'origin/main' 2026-11-02 14:39:48 +02:00
Lauri Ojansivu cfd7ad2e5f Catch two unhandled rejections found by the wider promise lint.
The instance-editing hook looked up the board of an edit without a catch,
and the service worker registration had none.

Thanks to xet7 !
2026-11-02 14:37:12 +02:00
Lauri Ojansivu 762e5a9432 CHANGELOG: npm run lint:promises. 2026-11-02 14:30:59 +02:00
Lauri Ojansivu f1d2a497bc Lint for promises that are not awaited, handled or marked void.
npm run lint:promises runs typescript-eslint's no-floating-promises and
no-misused-promises over models/lib, server/lib and imports/lib, with type
information read from tsconfig.lint.json (nothing is type-checked). The
eleven findings were promises that never reject (their own catch) and now
say so with void, and three checks that used a promise as a boolean.

Thanks to xet7 !
2026-11-02 14:30:49 +02:00
Lauri Ojansivu 2f9c972d46 CHANGELOG: the code-quality findings. 2026-11-02 14:15:17 +02:00
Lauri Ojansivu 582a22c291 Fix GitHub code scanning code-quality findings.
Real faults: a card's original-position description formatted a promise
because it did not await the history; the attachment swipe distances and
two drag-scroll locals were undeclared globals; a file that cannot be read
during a GridFS upload left the upload open; api.py called exit() instead of
sys.exit(); a rules test redefined a property and a security-log test kept
an unreachable branch. Also 31 missing semicolons, unused imports in the
card details, exporters and attachment API, unused Python imports, and
comments on empty except clauses. The list-sync scanner test cut its source
at the last Meteor.startup, which the new index startup of #6855 had moved.

Thanks to GitHub CodeQL and xet7 !
2026-11-02 14:15:02 +02:00
github-actions[bot] d200234535 Bump versions for v12.83 2026-11-02 11:40:29 +00:00
Lauri Ojansivu 8548bae527 Prepare v12.83 release 2026-11-02 13:38:58 +02:00
Lauri Ojansivu d6b2d55c36 Merge remote-tracking branch 'origin/main' 2026-11-02 13:35:16 +02:00
Lauri Ojansivu f7e3533b13 CHANGELOG: LDAP account binding. 2026-11-02 13:32:03 +02:00
Lauri Ojansivu e91c14cd9a LDAP: a reused login name no longer opens the previous owner's account.
An account found by username or e-mail that records another directory
entry's identifier (same attribute, other value) is refused at login and
skipped by the import. An existing account now records its identifier and
its attribute, so LDAP_UNIQUE_IDENTIFIER_FIELD takes effect for it.
LDAP_EMAIL_MATCH_REQUIRE looks users up by username instead of _id, which
never matched. The background sync skips an account whose username matches
several entries instead of binding it to the first.

Thanks to Raymond-Voogt and xet7 !

Fixes #6859,
2026-11-02 13:31:40 +02:00
github-actions[bot] e7439e1d45 Bump versions for v12.82 2026-11-02 11:00:45 +00:00
Lauri Ojansivu 4a34b91ecf Prepare v12.82 release 2026-11-02 12:59:36 +02:00
Lauri Ojansivu d9984d2069 Merge remote-tracking branch 'origin/main' 2026-11-02 12:54:11 +02:00
Lauri Ojansivu e982282954 CHANGELOG: card numbers on a move, a restore and a set number. 2026-11-02 12:42:43 +02:00
Lauri Ojansivu e68938698c Card numbers: a card moved to another board, restored or given a number cannot take one another card has.
#6815 checked the number of every new card, but a card also enters a board's
numbering by an update: a move from the browser, which computed the
destination number from the cards it had loaded (often none, so 1), a restore
from the archive, and a number set by the REST API or an import. A before-update
hook now does what the insert hook does: a taken number is replaced by a fresh
one from the board's counter, a free one is kept and the counter raised past it.

Thanks to Raymond-Voogt and xet7 !

Fixes #6858,
2026-11-02 12:42:33 +02:00
github-actions[bot] 55299a5172 Bump versions for v12.81 2026-11-02 10:22:59 +00:00
Lauri Ojansivu 8bbc6fcda1 Prepare v12.81 release 2026-11-02 12:20:06 +02:00
Lauri Ojansivu 71463fb445 Merge remote-tracking branch 'origin/main' 2026-11-02 12:17:05 +02:00
Lauri Ojansivu b60a1e5a85 CHANGELOG: the database health probe reports a finding once. 2026-11-02 12:15:29 +02:00
Lauri Ojansivu 114e9ecd5a Database health: one server probes the database per period, so a finding is reported once.
Every server read the same MongoDB log and counters and recorded each
finding itself: one collection scan became up to 24 identical rows in Admin
Panel / Problems on 24 replicas. A probe now claims the period in a shared
document, and the state that compares one probe with the next lives there.
A claim that cannot be made at all does not stop the probe.

Thanks to Raymond-Voogt and xet7 !

Fixes #6857,
2026-11-02 12:15:09 +02:00
github-actions[bot] c6739fe452 Bump versions for v12.80 2026-11-02 10:01:31 +00:00
Lauri Ojansivu efd45c651e Prepare v12.80 release 2026-11-02 12:00:25 +02:00
Lauri Ojansivu fdc2c57b8f Merge remote-tracking branch 'origin/main' 2026-11-02 11:52:22 +02:00
Lauri Ojansivu 2c75b95908 CHANGELOG: minimatch 11.0.4 for GHSA-g3hv-75wg-m249. 2026-11-02 11:42:44 +02:00
Lauri Ojansivu a3630d96ce Update minimatch to 11.0.4, GHSA-g3hv-75wg-m249 (Dependabot alerts #180 and #182).
minimatch before 11.0.1 grows its glob parts without bound while it
preprocesses a pattern, then compares them pairwise. archiver (readdir-glob),
@typescript-eslint/typescript-estree and @babel/cli's glob used 10.2.6; they
now use the root minimatch 11.0.4.

Thanks to dependabot and xet7 !
2026-11-02 11:41:52 +02:00
github-actions[bot] db2aafe16c Bump versions for v12.79 2026-11-02 09:35:57 +00:00
Lauri Ojansivu c4b90d08a0 Prepare v12.79 release 2026-11-02 11:34:58 +02:00
Lauri Ojansivu cb5278ddb3 CHANGELOG: a collection-scan row says which query it is. 2026-11-02 11:31:54 +02:00
Lauri Ojansivu bc79de8c57 Database health: a collection scan row names the command, the operators per field and the queryHash.
A row said only which fields a slow query filtered on, which is not enough
to find the query in the code or to run explain(). It now shows the command
(for a slow getMore, the query it continues, instead of 'filter on nothing'),
the operators used on each field - never values - and MongoDB's queryHash.

Thanks to Raymond-Voogt and xet7 !

Fixes #6856,
2026-11-02 11:31:47 +02:00
Lauri Ojansivu 81714cd695 CHANGELOG: the schema re-check and due-date reminders run once. 2026-11-02 11:28:59 +02:00
Lauri Ojansivu d42369e96a Full collection scans run once, not at every start of every server; due-date reminders are sent once.
The startup schema re-check claims a run in _wekan_migration so one server
re-checks while the others skip (a claim older than 2 hours is taken over),
and a run that leaves work unresolved is retried once a day instead of at
every start. The daily due-date reminder scan is claimed per day in
dueReminderRuns, so 24 replicas no longer send 24 reminders per card.
A partial index on lists serves the 15-minute list sync scan, and a sparse
index on boards.dueReminderDays serves the due-date lookup.

Thanks to Raymond-Voogt and xet7 !

Fixes #6855,
2026-11-02 11:28:44 +02:00
Lauri Ojansivu efff590f74 CHANGELOG: the announcement shows on Home and All Boards. 2026-11-02 11:26:12 +02:00
Lauri Ojansivu 64b5762020 Announcement: shown on Home and every All Boards list, not only on a board.
The banner waited for membership of the current board, and Home, Remaining,
Starred, Templates and the other All Boards pages have none. It now shows to
every signed-in user there, and to the members of the open board on a board.
Signed-out visitors still do not see it.

Thanks to titver968 and xet7 !

Fixes #6854,
2026-11-02 11:25:57 +02:00
Lauri Ojansivu c29a59eb95 CHANGELOG: any year by number in the date picker. 2026-11-02 11:17:04 +02:00
Lauri Ojansivu dcaa316a01 Date picker popup: the year is a number field, any year by typing or stepping.
A list of years had to end somewhere. The year is now a whole number
(0 is 1 BC) found by jumping through the calendar in use, so thousands of
years back or ahead are one entry away, up to the range a date holds.

Thanks to brlin-tw and xet7 !

Fixes #6853,
2026-11-02 11:17:02 +02:00
Lauri Ojansivu 7721c5f655 CHANGELOG: year, month and day drop-downs in the date picker. 2026-11-02 11:10:08 +02:00
Lauri Ojansivu 492895200b Date picker popup: year, month and day drop-downs under the calendar.
The heading above the calendar shows the month and year only, not the
YYYY-MM-DD of the selected day. Under the calendar, year, month and day
drop-downs pick any past, present or future date, in every calendar
system.

Thanks to brlin-tw and xet7 !

Fixes #6853,
2026-11-02 11:09:34 +02:00
Lauri Ojansivu 1b346dc465 CHANGELOG: the date picker heading names the year. 2026-11-02 10:50:22 +02:00
Lauri Ojansivu 45c291b406 Date picker popup: the heading names the year as well as the month.
Thanks to brlin-tw and xet7 !

Fixes #6853,
2026-11-02 10:50:13 +02:00
Lauri Ojansivu c3af2a08a9 CHANGELOG: the macOS sandbox keychain and Claude Code token. 2026-11-02 10:16:03 +02:00
Lauri Ojansivu 21c94ce098 Fix macOS Sandbox for VSCodium.
Thanks to xet7 !
2026-11-02 10:14:47 +02:00
Lauri Ojansivu 2d8a28c2f6 Merge remote-tracking branch 'origin/main' 2026-11-02 09:44:15 +02:00
Lauri Ojansivu c58b2896d1 Fix macOS Sandbox for VSCodium.
Thanks to xet7 !
2026-11-02 09:43:32 +02:00
github-actions[bot] 6b98272272 Bump versions for v12.78 2026-11-02 07:14:42 +00:00
Lauri Ojansivu ee22fb1a3b Prepare v12.78 release 2026-11-02 09:13:43 +02:00
Lauri Ojansivu 154c9a6e52 Updated Changelog. 2026-11-02 09:11:57 +02:00
Lauri Ojansivu 121f37be91 Tests: drawing refit, Due Cards board scope, dependency import counts, and SAML's one Issuer.
Changes since October whose commits lacked a unit, negative or browser
test:
- 2929b61c85: a new drawing refits when its window changes, and is left
  alone once drawn on, edited, or zoomed or resized by hand.
- c64b3a3b1d: Due Cards, in "me" and "all users", shows an assigned-only
  member only their assigned cards; the owner sees them all.
- 8a6aeba7d1: the All Boards dependency import says how many lines were
  added, already there and unmatched.
- 5c50f38e25: a signed SAML Response with two Issuers is refused, which
  node-saml 5.1.0 accepted; the same Response with one Issuer logs in.
The browser specs pass in Chromium.

Thanks to xet7 !
2026-11-02 00:54:59 +02:00
Lauri Ojansivu 79c40407d1 Tests: Admin Panel pane titles, audit log alignment, GCS help, Data health "Never", card reports.
October Admin Panel changes whose commits had no test of their own:
- 85af731c1b and 2017f2871f: the Password length and Audit log panes
  name themselves in the title bar, the audit log has its own URL, and
  its cells align to the start - right in Hebrew - with no left/right
  rule left in its CSS.
- e6f7d226ca: no help line of a storage field is shown twice.
- 77956c98ed: Data health's "Never" is its own key, also when an
  administrator translates the List Sync one differently.
- 69f3358154: the card reports stay instance-wide and admin-only.
tests/playwright/specs/admin-pane-titles-and-help.e2e.js passes in
Chromium.

Thanks to xet7 !
2026-11-02 00:31:24 +02:00
Lauri Ojansivu 148d18d456 Tests: OpenSSL 4 decrypt refusal, the Scrum rollover lease, and the backup catch-up scan.
October changes whose commits had no node test of their own:
- ffe49dd2fd: decryptFile() refuses both the OpenSSL 4 and the OpenSSL 3
  wording of a failed authentication as "changed or the key is wrong",
  on any OpenSSL; an unrelated error (ENOSPC) is passed through.
- 0218cd823f: the rollover and batch-job leases are claimed before every
  write, another server's live lease is refused, and each lease field is
  written in one place in the whole tree.
- 8cbe2605fe: on macOS, a stopped engine and one with watching off run no
  catch-up scan.

Thanks to xet7 !
2026-11-02 00:30:57 +02:00
Lauri Ojansivu 15877dd1bc Source server: a local package fork that a prebuilt package implies is loaded first.
ecmascript implies the local dynamic-import fork, so compiling ddp-server,
listed before it in .meteor/versions, stopped the source-loaded server
with "isopack dynamic-import not yet loaded". The loader now loads the
local package asked for and retries. The source server still stops later,
on "Cannot find module 'meteor/accounts-base'"; the bundle server is what
the browser tests ran against.

Thanks to xet7 !
2026-11-02 00:24:55 +02:00
Lauri Ojansivu 3ccb465c3f Tests: the #6755 browser test focuses the minicard link itself.
Chromium does not leave the focus on the minicard after its menu popup
is closed with Escape - the menu's opener is a link without href, which
cannot take the focus back - so the test asserted a state the browser
had not reached. It now focuses the minicard's link, the state of the
report, and checks that d still opens the Due date editor from there.
Passes in Chromium and WebKit.

Thanks to mimZD and xet7 !
2026-11-02 00:24:55 +02:00
Lauri Ojansivu 71d9776b61 CHANGELOG: the Rules page has a Back to the board.
Thanks to mimZD and xet7 !
2026-11-02 00:07:50 +02:00
Lauri Ojansivu 34d9817110 Rules page: Back to the board.
The Rules page lost its Back with its own sidebar; only the trigger, action
and rule details pages kept one, to the rules list, so the way back to the
board was the Board View menu. The rules list and its Workflow, Bots,
Blocks and History views now have a Back link to the board at the top. The
three sub-pages keep their own Back and show no second one.

Thanks to mimZD and xet7 !

Fixes #6852.
2026-11-02 00:07:42 +02:00
Lauri Ojansivu aa939d7bec Tests: CrossBoardExportBleed and UnassignedAttachBleed are in the security regression inventory.
Both are published on the Hall of Fame, so the inventory counts 143 and
names each one's unit and browser regression tests. CrossBoardExportBleed
was published without being added, which failed the whole node-suite run.

Thanks to xet7 !
2026-11-02 00:05:44 +02:00
Lauri Ojansivu 194e5b0a51 CHANGELOG: UnassignedAttachBleed, and shortcuts after a click on a minicard or a closed popup.
Thanks to Javier García Herrera, mimZD and xet7 !
2026-11-02 00:05:10 +02:00
Lauri Ojansivu 532a96da11 Security: UnassignedAttachBleed - attachment API writes obey assigned-only card scope.
An assigned-only member whose card assignment was removed kept the card and
attachment ids and, through POST /api/attachment/upload and
DELETE /api/attachment/delete/:id, still added a file to that card and
soft-deleted its existing one: both asked only for board write access. The
DDP methods api.attachment.upload/delete, and copy and move onto a target
card over REST and DDP, had the same shape.

mayWriteCardAttachment (server/lib/assignedOnlyAttachments.js) applies the
same card rule the ordinary card Delete already used to all eight handlers.
A refusal is recorded in Admin Panel / Problems as UnassignedAttachBleed
(authz.assigned-attachment-write, medium: never disables the account).
tests/unassignedAttachBleed.test.cjs reproduces the reported sequence and
fails if any attachment API write lacks the card check;
tests/playwright/specs/unassigned-attach-bleed.e2e.js replays it over REST.

GHSA-w26g-rc43-q6cm.

Thanks to Javier García Herrera and xet7 !
2026-11-02 00:04:03 +02:00
Lauri Ojansivu ba0de93f79 Keyboard shortcuts keep working after a click on a minicard or a closed popup.
The shortcut filter refused every key while a link or button had the focus.
The minicard is itself a link, so a click on its date badge or its menu left
that link focused, and closing a popup returns focus to the button that
opened it: hovering a card and pressing d then did nothing until a reload.

A focused control now keeps only the keys it uses itself - Enter for any of
them, Space for a button, summary or checkbox, End/Home/arrows for a tab -
so Space still activates a focused button (audit A01) and every other key
is a shortcut again.

Thanks to mimZD and xet7 !

Fixes #6755.
2026-11-02 00:03:54 +02:00
Lauri Ojansivu 9a15491985 CHANGELOG: sign-in errors kept through a redraw, and the Firefox full-run test fixes.
Thanks to xet7 !
2026-11-01 20:35:06 +02:00
Lauri Ojansivu c8ed3e0fd0 Tests: the bulk export popup is opened until it stays, in all five Haitian Creole tests.
Each test navigates to All Boards and at once opens Export all boards.
All Boards is still drawing just after the route changes, and in
EVERYTHING's full Firefox run the popup opened then was closed again: no
header title to read (the first test; it passed in the run before).

All five tests open the popup again until its title is there. The spec in
Firefox, three times beside the running suite: 15 of 15.

Thanks to xet7 !
2026-11-01 18:56:50 +02:00
Lauri Ojansivu 4a9c3f21d2 Tests: the drawing surface's width is waited for, not read once after its viewBox.
The test waited until the drawing's viewBox had grown past the old 800,
then read the surface's box once. The element's layout follows the
viewBox a frame later, and in EVERYTHING's full Firefox run it was still
800 px against more than 1088 expected (it passed in the run before).

It polls the surface's width as well. The spec in Firefox, three times
beside the running suite: 9 of 9.

Thanks to xet7 !
2026-11-01 18:23:13 +02:00
Lauri Ojansivu 3a2fef4dc7 Sign-in: login errors are rendered from template state, and the SAML error test clicks a finished form.
saml-popup-error's first test failed in EVERYTHING's full Firefox run and
2 of 3 times alone: #login-error-message stayed empty after the SAML
button was clicked with a stub that fails at once.

The test clicked #at-saml (force) as soon as Meteor connected, while the
sign-in form was still drawing: the click landed on a button being
replaced, or - in another attempt - below the window ("Element is
outside of the viewport"), and no handler ran. It now waits for the form,
scrolls the button into view and clicks until the error shows. With one
worker both tests pass 6 of 6 in Firefox against a rebuilt bundle.

Found on the way: #login-error-message sits inside `if isLoading`, and
showLoginError wrote into the element - when that block is drawn again
the element is new and empty, so an error shown just before was lost. The
template now renders the message from a ReactiveVar (loginErrorText) as
escaped text, so a redraw keeps it and it can still never be HTML.
oauthProviders.test.cjs pins the same region, the translated codes and
text-only output in that form.

The other sign-in specs (44 files) pass in Firefox with it, apart from
runs where two copies of a settings-changing spec ran at once.

Thanks to xet7 !
2026-11-01 17:49:21 +02:00
Lauri Ojansivu f241681e80 Tests: background notification recovery checks its own mail, not the first one the sink caught.
The SMTP sink captures every mail the shared test server sends while it
listens. The test waited until user2 had exactly one mail, then read
sink.messages[0] - in EVERYTHING's full Firefox run another worker's
comment notification ("Please look", to another user) had arrived first,
so "Frozen recovery body 0" was looked for in the wrong mail.

It reads the mail addressed to user2. The whole spec in Firefox, twice:
24 passed, 2 conditionally skipped.

Thanks to xet7 !
2026-11-01 17:32:09 +02:00
Lauri Ojansivu c71c875649 Tests: the custom field settings popup is opened until its Create button shows.
The String Template help test opened a card and at once its custom field
settings popup. The opened card was still drawing, and in Firefox under
the whole suite the popup sometimes closed again before Create could be
clicked: ve-CC and rup failed in EVERYTHING's full Firefox run, 42
others passed.

The test opens the popup again until its Create button is there. ve-CC,
rup and en, twice each in Firefox beside the running suite: 6 of 6.

Thanks to xet7 !
2026-11-01 17:06:02 +02:00
Lauri Ojansivu 0da86171e9 CHANGELOG: TODO Later - the whole EVERYTHING run has passed in the browsers, conformance and FerretDB.
Thanks to xet7 !
2026-11-01 14:15:06 +02:00
Lauri Ojansivu b80e21fd48 CHANGELOG: Android bundles start again.
Thanks to xet7 !
2026-11-01 14:11:20 +02:00
Lauri Ojansivu 24a7270b7f Android bundles start again: npm 12 runs argon2's and bcrypt's install scripts.
All four Android bundle jobs failed: WeKan crash-looped on

  No native build was found for platform=android arch=x64 runtime=node
  abi=147 ... loaded from: .../meteor/accounts-password/node_modules/argon2

until the 240 s smoke test gave up. Meteor's npm rebuild had printed
"install scripts blocked because they are not covered by allowScripts",
though prepare-bundle-npm.mjs writes argon2 and bcrypt into every
rebuild directory's allowScripts.

npm 12 applies allowScripts to a project's DECLARED dependencies only.
The package.json that prepare-bundle-npm writes there declared none, so
argon2 and bcrypt were "extraneous" and their scripts stayed blocked -
reproduced with npm 12.2.1 on this release's bundle, and with the two
declared the same rebuild runs both install scripts. Linux, macOS and
Windows never noticed: prebuilds load there, and the Windows jobs compile
the two themselves. Android has no prebuild, and check-addon-arch passed
the Linux ones it cannot load.

prepare-bundle-npm.mjs now declares each reviewed installer a rebuild
directory has installed, at its installed version, and leaves a declared
one as it is. The Android build runs it itself first - the Linux bundle
it rebuilds may predate this - and loads argon2 and bcrypt with Android's
Node.js right after the install, so a missing addon stops the build with
its name instead of a smoke-test timeout.

prepareBundleNpm.test.cjs and androidBundles.test.cjs pin both; they fail
on the code before. Not run on Android here: the build needs Termux on
an Android-CPU runner.

Thanks to xet7 !
2026-11-01 14:11:10 +02:00
Lauri Ojansivu 73ea323da1 CHANGELOG: CrossBoardExportBleed, GHSA-8885-xrq4-j47h.
Thanks to ccsimplyspolit and xet7 !
2026-11-01 14:01:07 +02:00
Lauri Ojansivu be6727607c CrossBoardExportBleed: a checklist scope stays on its board, and every export obeys Disable all export and Anonymize export users.
GHSA-8885-xrq4-j47h, reported by ccsimplyspolit, both findings confirmed
in the current code:

1. GET /api/boards/<board>/export?checklistId=<id> (and exportZip)
   selected the scoped checklist and its items by id alone. A checklist
   of another board - a private one too - was streamed with every item
   and its creator, whose profile the anonymization map did not cover.
   Both selectors are now bound to the exported board's cards, and a
   checklist of another board is recorded in Admin Panel / Problems
   (authz.export-checklist-scope).

2. The streaming CSV/TSV export asked neither Disable all export nor
   Anonymize export users. Neither did, it turned out, Excel (board,
   card, all boards), PDF (board and card), the chart exports, the
   calendar feed or the external formats: only the WeKan JSON, ZIP and
   Trello exports did, though the setting promises that "the server
   rejects any export request" and names CSV and Excel.
   - refuseDisabledExport() answers 403 before anything is written, and
     all 13 HTTP export routes call it first; assertExportEnabled() still
     guards the builders. Both record the attempt (authz.export-disabled,
     never blocking the account: a page opened before the switch still
     shows the menus).
   - anonymizeExportIdentities() anonymizes what one exporter writes -
     users, @mentions, requested-by / assigned-by, comments - with the map
     build() uses, board members included. The CSV/TSV stream, Excel,
     PDF, chart exports (forExport: the chart views keep names), the
     calendar feed and every external format call it.

tests/crossBoardExportBleed.test.cjs drives the gate and the anonymizer
and scans the tree: no scoped checklist selected by id alone, no export
route without the gate, no exporter naming people without anonymizing.
All 7 fail on the code before. The new spec
security-cross-board-export.e2e.js replays the reporter's requests and
checks every export route while export is disabled and the CSV and
Kanboard exports while anonymizing; it passes with export-access and
scrum-export-anonymization against a rebuilt bundle.

Thanks to ccsimplyspolit and xet7 !
2026-11-01 14:00:19 +02:00
Lauri Ojansivu 9c5fa760c3 CHANGELOG: glob 14.0.3, checked: no WeKan code loads it, no telemetry, tarballs match the lockfile.
Thanks to dependabot and xet7 !
2026-11-01 13:41:40 +02:00
Lauri Ojansivu 27040c6f2f Merge pull request #6851 from wekan/dependabot/npm_and_yarn/npm_and_yarn-0e36e05768
Bump glob from 13.0.6 to 14.0.3 in the npm_and_yarn group across 1 directory
2026-11-01 13:36:11 +02:00
Lauri Ojansivu c15e1ab4b2 Tests: the Card Settings splitter is dragged once the popup holds still.
The splitter test measured the splitter straight after opening Card
Settings, while the popup was still placing itself. In WebKit the press
landed where the splitter had been and dragged nothing: the pane stayed
526 px against more than 606 expected (the first attempt in EVERYTHING's
WebKit stage; the retry passed).

It now measures once two reads of the splitter and the panes 150 ms apart
agree. WebKit 5 of 5, Chromium 3 of 3.

Thanks to xet7 !
2026-11-01 11:58:06 +02:00
Lauri Ojansivu b676cdc169 CHANGELOG: the WebKit date-range refusal and the language and push fixes of the browser tests.
Thanks to xet7 !
2026-11-01 11:25:20 +02:00
Lauri Ojansivu 4ad13db5ef Filters: the date-range forms are checked by WeKan, not refused by WebKit after Clear.
After Clear emptied a refused range, WebKit kept a half-edited date
segment in the From field and fired `invalid` on it at the next submit -
though its validity read valid - so the form never submitted and the new
range never applied, with no message (card-date-range.e2e.js in WebKit,
also on retry: 0 submit events, `invalid` on .js-card-date-from). The old
Clear handler emptied the fields the same way, so this was there before.

Both range forms are novalidate now. The submit handler already decides
whether a range is valid (rangeRefused), marks the end date invalid with
the reason and says it in the form, so nothing is lost.

filterDateRangeRefusal.test.cjs pins novalidate on both forms. Against a
rebuilt bundle card-date-range and card-movement-filter pass twice each
in WebKit, Firefox and Chromium.

Thanks to xet7 !
2026-11-01 11:24:59 +02:00
Lauri Ojansivu efe0e9287c Tests: a language switch is waited for, and WebKit does not freeze on asking for a push subscription.
Two causes behind most of EVERYTHING's WebKit failures so far, both in the
test pages rather than in WeKan:

- 94 specs switch language with setLanguage and then click at once. The
  client loads that language afterwards and draws everything again, which
  closes a popup opened before that: the board view popup of the flow and
  report help specs (Latin monte-carlo in WebKit; Corsican and Scottish
  Gaelic in earlier Firefox runs) had no entry left to click. The
  fixture's context now wraps Meteor.callAsync in each page so that a
  setLanguage call resolves once <html lang> - which client/lib/i18n.js
  sets in the same flush that redraws - names the language. It waits at
  most 10 s and then resolves anyway, so it can only make a spec patient,
  never fail one. A page of a context a spec makes itself uses
  untilLanguageShown().
- Playwright's WebKit freezes the page's main thread on
  PushManager.getSubscription(): after it not even a timer runs. Member
  Notification Settings asks it on opening, to show whether this device
  gets push (client/lib/webPush.js), so every WebKit spec that opened it
  hung in its next click - notification-activity-mute and the member
  scope of notification-delivery-settings, in every language. In WebKit
  the page now answers as a device with no subscription does. Chromium,
  Firefox and Safari answer the real call; no spec subscribes.

WebKit: those notification specs 10 of 10, and the Latin, Corsican and
Product Backlog specs 13 of 13; Chromium and Firefox 11 of 11 each with
the same subset. playwrightPageInit.test.cjs runs both page scripts on a
fake page: setLanguage waits for the language and keeps its result,
other methods are not held, a page whose Meteor arrives later is
wrapped, the wait is capped; WebKit's getSubscription answers null and
Chromium's and Firefox's are left alone; the fixture installs both.

Thanks to xet7 !
2026-11-01 11:21:45 +02:00
Lauri Ojansivu cfcead6a1a CHANGELOG: the Firefox fixes - date range refusal, history Retry, whiteboard line width - and the browser test fixes.
Thanks to xet7 !
2026-11-01 09:23:34 +02:00
Lauri Ojansivu 40f60582b7 Whiteboard: a line is drawn at its own width, not the whiteboard's 800.
Inside `each strokes` the template drew stroke-width=width, and Spacebars
looks a name up among the template's helpers before the line's own
fields: the width helper is the drawing's width, so every line was drawn
800 wide - one black shape over the whole whiteboard, in every browser.
The lines are mapped with strokeWidth now.

whiteboard.test.cjs: every name the line's path uses is a field of the
line and none is a helper of the template, which would win - it fails on
the code before. whiteboard.e2e.js checks each drawn path is the pen's
width (4) in both browsers.

The same spec drew the second person in another context of the same
browser. In Firefox two contexts share the pointer capture the first pen
takes: one line zig-zagged between both paths and the other page drew
nothing, 4 of 4. The second person now has a browser of their own, as
two people do; both pens are still down at once. Firefox 6 of 6 and
Chromium 4 of 4 against a rebuilt bundle.

Thanks to xet7 !
2026-11-01 09:22:05 +02:00
Lauri Ojansivu 96845f6f4b History recovery: Retry is enabled only once the board lets this person write on it.
After a reload the notice for an unanswered keyboard undo or redo shows
as soon as its board is open - before the board and its members have
arrived. undoRedoLast does nothing while Utils.canModifyBoard() is false,
so a Retry clicked in that moment was dropped without a word and the
notice stayed (history-recovery-notice.e2e.js, Firefox).

Retry is disabled until the retry can act; Forget needs no right and
stays usable. For someone who may only read the board Retry stays
disabled, as it could never do anything for them.

historyRecoveryNotice.test.cjs pins the binding and the guard it mirrors,
and that Forget is not gated - it fails on the code before. The spec
waits for Retry to be enabled before it clicks; the spec passes in
Firefox against a rebuilt bundle.

Thanks to xet7 !
2026-11-01 09:22:05 +02:00
Lauri Ojansivu c5f5ad2338 Filters: a refused date range says why in the form, so Clear still works in Firefox.
The filter sidebar's two date-range forms (card dates, card movement)
reported a reversed range with reportValidity(). Firefox opens that
bubble just under the end date - over the Apply and Clear buttons - and
while the field has focus it takes every click on them: no mousedown and
no click reached the page, twice in a row, so a refused range could not
be cleared at all (card-date-range.e2e.js, 4 of 4 in Firefox; with the
bubble suppressed it passed).

The end date is still marked invalid with the reason (setCustomValidity,
for :invalid and screen readers), and the reason is shown in the form as
a role="alert" line under the buttons. Typing in the range or Clear takes
it back.

filterDateRangeRefusal.test.cjs: both forms report through rangeRefused,
show the line and take it back on input and Clear, and nothing in the
filter sidebar calls reportValidity() - it fails on the code before. The
card-date-range and card-movement-filter specs check the line appears
with the reason, is gone after Clear or an edit, and is absent for an
accepted range; both pass in Firefox against a rebuilt bundle.

Thanks to xet7 !
2026-11-01 09:22:05 +02:00
Lauri Ojansivu 0c8c254dc5 Tests: the viewport matrix cleans up only what it seeded.
viewport-matrix is Chromium-only (device emulation), so in Firefox and
WebKit every test of a viewport is skipped and its beforeAll never runs -
but its afterAll still does, and read board.boardId of a board that was
never seeded: each of the twelve viewports failed in EVERYTHING's Firefox
stage, on the last test of the group, in 0 ms.

afterAll returns when nothing was seeded. Firefox: 132 skipped, none
failed. Chromium: phone-portrait 11 of 11, and its board is cleaned up.

Thanks to xet7 !
2026-11-01 09:09:14 +02:00
Lauri Ojansivu 0a1f8584ee Tests: the board view popup is opened until it stays open for the Product Backlog.
The test switches language and then opens the board, and when the language
arrives the board header draws again and closes a popup opened before
that. In Firefox the click on the board view toggle often came first: no
popup at all, and no Product Backlog entry to open - 2 of 6 runs alone,
the st variant in EVERYTHING's Firefox stage.

The test opens the popup again until its Product Backlog entry is there.
9 of 9 in Firefox (st, en, haw, three times each).

Thanks to xet7 !
2026-11-01 09:01:09 +02:00
Lauri Ojansivu 2042fd5102 Tests: the Blocks drag test grabs the block's body, not its edge.
In Firefox a Khmer label overflows its Blocks block and widens the box the
test measures, so the first point of the block it found, scanning from the
left, was the block's outline - and a press there dragged nothing: km and
km-KH failed every time (4 of 4), every other language passed.

The grab point is now one with the block's own body on both sides of it.
The whole spec in Firefox: 104 of 104, Khmer, Georgian, Fijian,
Kinyarwanda (the long labels the search was written for) and Arabic
right-to-left among them.

Thanks to xet7 !
2026-11-01 08:23:41 +02:00
Lauri Ojansivu 0403e27c62 Tests: the notification delivery sections are opened until they stay open.
The member-scope test switches language just before it opens Notification
Settings, and when the language arrives the popup draws again with every
section closed. Under a whole-suite run in Firefox that came after the
click on the section's summary, so the text checks passed and the select
the test then used was hidden: vi and zh_SG failed, 101 others passed,
and vi passed 3 of 3 alone.

expand() opens the section when it is closed and waits until its content
shows, again if a redraw closes it. vi, zh_SG, en and fi twice each, and
the board-scope, webhook-form and tray tests: all pass.

Thanks to xet7 !
2026-11-01 08:04:55 +02:00
Lauri Ojansivu d9e48a909d Tests: the list-sync specs' mock servers are reachable from WeKan when the browsers run in Docker on macOS.
The list-sync specs answer as Jira or GitLab from a server in the test
process, and WeKan's server fetches from it. With the browsers in Docker
on macOS that server was on the container's loopback, which Docker
Desktop keeps from the host: "fetch failed", and every test of
list-sync-estimate, -planning, -source and -time-estimates failed in
EVERYTHING's Firefox stage.

build.sh already forwarded the test SMTP port from the host into the
container through docker exec; the same forwarder carries any TCP. On
macOS it now also forwards a small pool, 39101-39104 by default
(WEKAN_TEST_REVERSE_PORTS), and names it to the specs, which listen
through helpers/reachable-server.cjs: the first free port of the pool,
or any port when there is none, as before. Linux forwards nothing.

30 of 30 of those specs pass in Firefox in Docker now.
playwrightReachableServer.test.cjs: the pool is read strictly, a port in
use is skipped, no pool listens anywhere, and when every forwarded port
is taken it fails clearly instead of listening where WeKan cannot reach.
playwrightDockerHost.test.cjs: macOS forwards the SMTP port and the pool
(or the one configured), names it to the container and removes its ready
files; Linux forwards nothing.

Thanks to xet7 !
2026-11-01 07:50:40 +02:00
Lauri Ojansivu 0cea141702 Tests: the duplicate relation picker is opened once the card has opened.
The test clicked Add dependency straight after the minicard. Under a
whole-suite run in Firefox that click landed while the card was still
opening, and the picker it opened was closed again when the card's route
and details settled - no popup, and the wait for its type select ran out
(the ve-PP variant; 59 of 60 passed, and it passed 3 of 3 alone).

The test waits for the opened card, then opens the picker again until its
type select shows. 6 of 6 (en, ve-PP, tlh, twice each) beside the suite.

Thanks to xet7 !
2026-11-01 07:21:43 +02:00
Lauri Ojansivu 9188843369 Tests: the checklist drag tests drag once the opened card's rows hold still.
The opened card draws the sections above its checklist for a moment after
it opens, and they push the rows down: in Firefox they moved 50 px between
the test measuring them and releasing the mouse. With handles off the
release landed on item 1's own title and opened its editor, so the titles
read 2,3 (EVERYTHING's Firefox stage); with handles on the enabled drag
dropped item 1 between 2 and 3. 2 of 6 in a row failed that way.

drag() waits until two reads of the rows' positions 150 ms apart agree,
then measures and drags. 10 of 10, both variants five times, while the
whole suite ran beside it.

Thanks to xet7 !
2026-11-01 06:57:18 +02:00
Lauri Ojansivu dbf07aadf9 Tests: the activity recovery report is searched again until no worker lease covers it.
The report is a snapshot taken when the search runs, and the live
one-second recovery worker leases each pending intent while it looks at
it. A search made during that lease shows the seeded orphan as "Delivery
reservation active" - correct - and the page keeps showing it, so the
wait for "Original activity is missing" ran out in EVERYTHING's Firefox
stage (the en variant).

The test searches again until a snapshot falls between leases, up to 30 s,
and then asserts the same status and the disabled Retry. 10 of 10 in
Firefox, every test twice, while the whole suite ran beside it.

Thanks to xet7 !
2026-11-01 06:40:42 +02:00
Lauri Ojansivu e001c4f9c2 CHANGELOG: the Docker test proxy redials an unanswered connect.
Thanks to xet7 !
2026-11-01 06:23:55 +02:00
Lauri Ojansivu ffe207e191 Tests: the Docker test proxy dials the host again when a connect is never answered.
Even on the bridge network, EVERYTHING's Firefox stage lost a test now and
then to a page whose document never came (RuleBleed's cross-board test,
Red Strings, shared templates). Inside the container the cause was in
plain sight: a connect to host.docker.internal:3000 in SYN_SENT after nine
retransmits, while the next connection went straight through. Through the
container's proxy 9 of 1000 requests went unanswered; from the Mac itself
none did.

The proxy no longer waits on such a connect. It reads nothing from the
browser until a connection to the host is up (pauseOnConnect), drops a
dial that has not connected in 1.5 s - a normal one takes milliseconds -
and dials again, five times, so no byte is lost or sent twice. If none
connects the browser's connection is closed rather than left open.

playwrightDockerHostProxy.test.cjs: two unanswered dials, then one that
connects - the request arrives once and is answered, and the abandoned
dials are closed; when every dial is unanswered the client is closed after
exactly the attempts, in well under the old 30-60 s wait.

Thanks to xet7 !
2026-11-01 06:23:34 +02:00
Lauri Ojansivu b2198cb61b CHANGELOG: the Firefox stage's Docker network, host paths and phone invitation test.
Thanks to xet7 !
2026-11-01 06:10:30 +02:00
Lauri Ojansivu a6bec88266 Tests: the phone invitation tile is measured in one layout with its buttons.
43-mobile-allboards read the invitation tile's box, then each control's
box, one round trip at a time. Firefox draws the row above the board list
later than Chromium, and in between it moved the whole list down 29.4 px:
the Decline button (1102-1162) was compared with where the tile had been
(853-1139) and failed, 2 of 2, in EVERYTHING's Firefox stage. The tile was
never clipped - settled, it runs 882-1168 around all three controls.

The tile and its message and two buttons are measured in one layout now,
polled until they hold, and a missing or zero-height control fails too.
Firefox: 3 of 3. With the original #6488 bug put back (the tile forced to
4rem) it still fails, naming the message and both buttons.

Thanks to xet7 !
2026-11-01 06:10:03 +02:00
Lauri Ojansivu 8a1f7a14a9 Tests: the Playwright container sees the repo and the files root at their own host paths.
A spec and the server share files by path. admin-continuous-backup gives
the server a key file it wrote under .tools/tmp, and reads the stream.json
the server wrote to its target - and with the repository mounted at /repo
and the files root at /wekan-files, every such path named a file only one
side had: "The encryption key file cannot be read", and no stream.json, in
both of its tests in EVERYTHING's Firefox stage (the only browser there that
runs in Docker on this Mac, and on Linux arm64 every browser does).

Both are mounted at the path they have on the host now, the specs are told
that path in WEKAN_FILES_PATH, and the in-container script finds the proxy
and the Playwright binaries from its working directory. Both tests pass in
the container now.

playwrightDockerHost.test.cjs drives run_playwright_docker on macOS and
Linux: the repo and the files root are mounted at their own paths,
WEKAN_FILES_PATH and HOME follow, and the function names no container-only
path. It fails on the code before.

Thanks to xet7 !
2026-11-01 06:08:05 +02:00
Lauri Ojansivu 7f948bf438 Tests: the macOS Playwright container uses Docker's bridge network, not the host network.
Three Firefox tests of one EVERYTHING run waited the full 60 s in
waitForMeteor: their page's own document never arrived. Measured from
containers during that run, on Docker Desktop for macOS:

- host network, through the WeKan proxy:          3 of 600 requests lost
- host network, straight to host.docker.internal: 8 of 600 connects never
  completed
- bridge network, through the same proxy:          0 of 1000
- from the Mac itself:                             0 of 300

So it is Docker Desktop's host network, not WeKan and not the proxy. On
macOS nothing needs that network - WeKan and MongoDB are reached through
host.docker.internal and SMTP through docker exec - so the container runs
on the bridge network, where the proxy still serves localhost:3000 and the
browser keeps ROOT_URL's origin. Linux keeps the host network, which is
the real one there.

playwrightDockerHost.test.cjs drives run_playwright_docker with uname
stubbed: Darwin gets bridge and the proxy, Linux gets host and none, and
the function names no network of its own. It fails on the code before.

Thanks to xet7 !
2026-11-01 06:06:04 +02:00
Lauri Ojansivu 6d0ab6cf11 CHANGELOG: continuous backup, single-file builds and the EVERYTHING test fixes.
The seven commits since the Dependabot entries: two bug fixes (continuous
backup watching before its first scan; single-file Mach-O signing and
start/backup waiting for WeKan) and five test-run fixes, grouped by topic.
The short changelog guard is a group of its own now, so the developer-
tooling subsection is grouped throughout. CHANGELOG.md regenerated.

Thanks to xet7 !
2026-11-01 05:47:52 +02:00
Lauri Ojansivu 89b64966c6 Tests: the password minimum specs wait until the server has the policy they wrote.
password-min-length-signup-login.e2e.js writes the password policy straight
into the settings document. The server learns it from its settings observer,
which polls on the standalone test MongoDB EVERYTHING runs: for up to ten
seconds it still answered the old policy. A sign-up form loaded meanwhile kept
the old minimum (6 instead of 10), and an administrator's Set password was
refused under the minimum the test before had left enforced - two failures in
EVERYTHING's Chromium stage that never showed on a dev server, whose MongoDB
is a replica set.

untilServerHas() asks the server (getPasswordPolicy) until it has the policy
the test wrote; the sign-up form is loaded again after it. 6 of 6 with two
workers, each test twice.

Thanks to xet7 !
2026-11-01 05:01:18 +02:00
Lauri Ojansivu d5e3325e87 Tests: Card Settings section order waits for the published board before reading the card.
"with every field allowed, the card draws exactly the Card pane's
sections" writes the board's Scrum visibility straight to the database and
reloads. The board publication's cursor has limit: 1, which Meteor's
change-stream driver does not follow, so it polls: a reload could still be
served the board as it was for up to ten seconds, and the card drew no
Scrum section - failing every time in EVERYTHING's Chromium stage. Live in
the browser the update arrived after 9.8 s; in the app itself a write
through a method re-polls at once, so only this direct write waited. The
test waits for the published board (up to 15 s) before it reads the card.

Thanks to xet7 !
2026-11-01 03:27:46 +02:00
Lauri Ojansivu 897376ba54 Tests: the Board Settings button theme test checks the settings that still have action buttons.
button-theme-foreground.e2e.js opened Board Settings / Swimlane, List and
Card and asked each for its column-actions buttons. Card Settings has had
none since 3e5bc58105 ("Card Settings: a checkbox per section, ...":
card actions live in the card's own section hamburgers), so every theme
failed on Card, in EVERYTHING's Chromium stage. Swimlane and List are
checked, in all four themes, and the test says why Card is not.

Thanks to xet7 !
2026-11-01 03:17:38 +02:00
Lauri Ojansivu 16103f9ad2 Tests: the whole-suite run keeps its test database in .tools/, not beside the checkout.
build.sh and build.bat started the test mongod on :3001 with its dbpath
in the checkout's parent directory (../mongodb-test-3001). That is the
one thing in a test run that was written outside the repository, against
the rule that everything WeKan's scripts write stays in .tools/ - and a
sandboxed editor, which may write only the repository, could not create
it, so EVERYTHING's browser stages had no database to run against. It is
.tools/mongodb-test-<port> now, which git and Meteor already ignore.

toolsDirOnly.test.cjs pins the new path in both scripts and fails on a
test database beside the checkout.

Thanks to xet7 !
2026-11-01 02:51:41 +02:00
Lauri Ojansivu 8cbe2605fe Continuous backup: files are watched before the first scan, and a macOS start catches up once.
The file engine scanned its sources first and started watching them
after, so a change made between the two - after the scan had passed a
file, before the watcher began - waited for the next scan, five minutes
by default. And macOS reports file events through FSEvents, which
delivers nothing for a moment after a watcher is created: an EVERYTHING
run on a loaded Mac lost a log line appended right after the start
(tests/integration/continuousBackup.test.cjs, "the appended log line was
streamed by watching", 4 !== 5).

The watchers start first now; a change seen by the scan and a watcher
too is recorded once, as they share one queue. On macOS one catch-up
scan runs two seconds after the start. Not elsewhere: inotify is
watching when fs.watch returns, and a second full scan of a large store
would double the start-up cost.

A new test, on macOS and Linux, with a watcher that never reports:
watching starts before the first scan; on macOS the catch-up scan
records a change made right after the start; on Linux no second scan
runs. It fails on the code before this change.

Thanks to xet7 !
2026-11-01 02:42:03 +02:00
Lauri Ojansivu f5eb4ecf61 Mac smoke test: a process group's live members come from libproc, not ps.
When macOS refuses to signal a process group (EPERM, which it returns for
a group of zombies), the smoke test checked with `ps` whether any member
was still alive. A Mac that will not let the build run `ps` - "Operation
not permitted", as here, where EVERYTHING's node suites run - failed the
check itself, and with it a correct app: macAppPackaging.test.cjs, every
EVERYTHING run on this machine.

On macOS it asks the kernel through libproc now (proc_listpids for the
group, proc_pidinfo for each member: a zombie's info cannot be read, a
live one's status is not SZOMB), with no process started. Elsewhere it
uses ps exactly as before.

tests/mac-smoke-app.py: the ps cases run as the non-macOS path; on macOS
an EPERM asks libproc and never ps; and a real group - a live child, then
a zombie leader alone - is told apart. macAppPackaging.test.cjs passes,
three runs in a row.

Thanks to xet7 !
2026-11-01 02:32:31 +02:00
Lauri Ojansivu 8d4f4caa2f Single file: a macOS build is signed whatever its target is called, and start/backup return once WeKan runs.
EVERYTHING's node suites run under Meteor's Node.js, which has single
executable applications, so singleFile.test.cjs built and ran a real
single executable - and it failed twice:

- The builder ad-hoc signed only a target named mac*/darwin*. Built from
  this Mac's Node.js under another label (the test builds 'amd64'), the
  file is still a Mach-O binary, the injected archive had invalidated its
  signature, and macOS killed it (SIGKILL) before it printed anything:
  status null. It signs every Mach-O output now, read from the file's
  magic number.
- `start --background` and `backup` returned as soon as they had spawned
  WeKan, before it wrote its pid file - which it does only after reading
  its settings and extracting FerretDB. A `stop` right after them said
  "WeKan was not running" and left it running. Both wait now
  (status.waitUntilStarted) until the pid file names the process they
  started, give up at once if it ended, and say so if it did not start.

singleFile.test.cjs: 24 of 24 under Meteor's Node.js, the real single
executable included; a new test pins waitUntilStarted, and that both
commands use it.

Thanks to xet7 !
2026-11-01 02:23:36 +02:00
dependabot[bot] 9a74138a84 Bump glob
Bumps the npm_and_yarn group with 1 update in the / directory: [glob](https://github.com/isaacs/node-glob).


Updates `glob` from 13.0.6 to 14.0.3
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v13.0.6...v14.0.3)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 14.0.3
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 23:51:54 +00:00
Lauri Ojansivu fa7e447536 CHANGELOG: ten Dependabot updates, checked; node-saml 5.2.0 where logins run; @smithy/core 3.38.1.
Thanks to dependabot and xet7 !
2026-10-31 23:06:40 +02:00
Lauri Ojansivu 05cc2460ea Tests: the short changelog guard holds between releases too.
changelogShort.test.cjs asserted an Upcoming section always, and one
Upcoming in CHANGELOG.md - but right after a release there is none until
the next real entry (CLAUDE.md, Making a release), so it failed after
every release, v12.77 included. With no Upcoming it checks that the
newest release is first and that nothing links an Upcoming that does
not exist; with one, what it checked before.

Thanks to xet7 !
2026-10-31 23:06:08 +02:00
Lauri Ojansivu 5c50f38e25 SAML logins are verified by @node-saml/node-saml 5.2.0, as package.json says.
Dependabot's 5.1.0 -> 5.2.0 bump (#6850) changed the app's package.json,
but a SAML login is verified by the wekan-accounts-saml package's own
copy, pinned by its Npm.depends to 5.1.0 - so nothing that runs changed.
5.2.0 is a hardening release a login relies on: it rejects a message
with more than one Issuer, checks idpIssuer on the Response and its
assertion, rejects a parsed query whose RelayState is not the signed
one, and checks a POST LogoutResponse's status and issuer.

The package pins 5.2.0 now. samlLogin.test.cjs required '5.1.0' in one
place and '^5.1.0' in the other; it requires the package and package.json
to name the SAME version, and at least 5.2.0, so a bump of one alone
fails.

A login roundtrip with the package's own copy (node-saml 5.2.0,
xml-crypto 6.4.0) and WeKan's options: a signed Response and a signed
Assertion log in; a NameID changed after signing, a signature by another
key, an unsigned response, and an answer to a request WeKan never made
are refused. saml-popup-error.e2e.js completes a real signed login in
the browser.

Thanks to xet7 !
2026-10-31 23:06:08 +02:00
Lauri Ojansivu 338afc15f8 @smithy/core 3.38.1: the AWS SDK's logger redaction fix, and the telemetry gate passes again.
The @aws-sdk/client-s3 3.1159.0 bump (#6844) resolved @smithy/core to
3.37.0, which npm marks deprecated: "a bug in custom logger data
redaction" (aws/aws-sdk-js-v3#8393). With a custom logger passed to an
AWS client, sensitive fields nested in a list or map reached the logger
unredacted. @smithy/core 3.24.0 to 3.38.0 have it, so 3.36.2 before the
bump did too; 3.38.1 is the fix.

WeKan's only AWS client is the S3 backup (server/methods/backup.js), and
it passes no logger, so nothing was logged. Updated all the same, in the
lockfile only and within the ranges the SDK already allows - one package,
nothing else moved. Its tarball matches the lockfile's integrity hash,
and compared with 3.37.0 it adds no network host, telemetry, process
spawning or install script. A multipart upload to a local S3 stand-in
still arrives signed AWS4-HMAC-SHA256, whole.

releases/check-telemetry.py --source failed on it: the deprecation text
links github.com, a URL origin new to package-lock.json. It passes now.

Thanks to xet7 !
2026-10-31 23:06:08 +02:00
Lauri Ojansivu dd2082b0ea Merge pull request #6846 from wekan/dependabot/npm_and_yarn/isomorphic-git-1.44.8
Bump isomorphic-git from 1.44.3 to 1.44.8
2026-10-31 22:39:27 +02:00
Lauri Ojansivu 4a86e9c19e Merge pull request #6850 from wekan/dependabot/npm_and_yarn/node-saml/node-saml-5.2.0
Bump @node-saml/node-saml from 5.1.0 to 5.2.0
2026-10-31 22:38:24 +02:00
dependabot[bot] 568cd9b6ec Bump isomorphic-git from 1.44.3 to 1.44.8
Bumps [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git) from 1.44.3 to 1.44.8.
- [Release notes](https://github.com/isomorphic-git/isomorphic-git/releases)
- [Commits](https://github.com/isomorphic-git/isomorphic-git/compare/v1.44.3...v1.44.8)

---
updated-dependencies:
- dependency-name: isomorphic-git
  dependency-version: 1.44.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:37:49 +00:00
Lauri Ojansivu fea468471d Merge pull request #6849 from wekan/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-8.72.1
Bump @typescript-eslint/eslint-plugin from 8.72.0 to 8.72.1
2026-10-31 22:37:36 +02:00
Lauri Ojansivu 83199b9af7 Merge pull request #6848 from wekan/dependabot/npm_and_yarn/rspack/dev-server-2.2.2
Bump @rspack/dev-server from 2.2.1 to 2.2.2
2026-10-31 22:37:10 +02:00
Lauri Ojansivu 3839ee698d Merge pull request #6845 from wekan/dependabot/npm_and_yarn/mailparser-3.12.4
Bump mailparser from 3.12.2 to 3.12.4
2026-10-31 22:36:48 +02:00
dependabot[bot] a60b996c92 Bump @node-saml/node-saml from 5.1.0 to 5.2.0
Bumps [@node-saml/node-saml](https://github.com/node-saml/node-saml) from 5.1.0 to 5.2.0.
- [Release notes](https://github.com/node-saml/node-saml/releases)
- [Changelog](https://github.com/node-saml/node-saml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/node-saml/node-saml/compare/v5.1.0...v5.2.0)

---
updated-dependencies:
- dependency-name: "@node-saml/node-saml"
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:36:39 +00:00
Lauri Ojansivu 1230164c16 Merge pull request #6847 from wekan/dependabot/npm_and_yarn/i18next-26.5.11
Bump i18next from 26.5.4 to 26.5.11
2026-10-31 22:36:29 +02:00
dependabot[bot] 8008f410fe Bump @typescript-eslint/eslint-plugin from 8.72.0 to 8.72.1
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 8.72.0 to 8.72.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.72.1/packages/eslint-plugin)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:36:27 +00:00
Lauri Ojansivu bd90782da2 Merge pull request #6844 from wekan/dependabot/npm_and_yarn/aws-sdk/client-s3-3.1159.0
Bump @aws-sdk/client-s3 from 3.1155.0 to 3.1159.0
2026-10-31 22:35:45 +02:00
dependabot[bot] 5eadc45563 Bump @rspack/dev-server from 2.2.1 to 2.2.2
Bumps [@rspack/dev-server](https://github.com/rstackjs/rspack-dev-server) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/rstackjs/rspack-dev-server/releases)
- [Commits](https://github.com/rstackjs/rspack-dev-server/compare/v2.2.1...v2.2.2)

---
updated-dependencies:
- dependency-name: "@rspack/dev-server"
  dependency-version: 2.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:35:25 +00:00
Lauri Ojansivu 3785a13f6a Merge pull request #6842 from wekan/dependabot/npm_and_yarn/imapflow-2.9.1
Bump imapflow from 2.8.0 to 2.9.1
2026-10-31 22:35:18 +02:00
Lauri Ojansivu 97cabdeca4 Merge pull request #6843 from wekan/dependabot/github_actions/helm/chart-releaser-action-1.8.0
Bump helm/chart-releaser-action from 1.7.0 to 1.8.0
2026-10-31 22:34:58 +02:00
Lauri Ojansivu 2802326888 Merge pull request #6841 from wekan/dependabot/github_actions/docker/login-action-4.7.0
Bump docker/login-action from 4.6.0 to 4.7.0
2026-10-31 22:34:17 +02:00
dependabot[bot] 1f46af0639 Bump i18next from 26.5.4 to 26.5.11
Bumps [i18next](https://github.com/i18next/i18next) from 26.5.4 to 26.5.11.
- [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.4...v26.5.11)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.5.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:33:51 +00:00
dependabot[bot] 1bcb1da92f Bump mailparser from 3.12.2 to 3.12.4
Bumps [mailparser](https://github.com/nodemailer/mailparser) from 3.12.2 to 3.12.4.
- [Release notes](https://github.com/nodemailer/mailparser/releases)
- [Changelog](https://github.com/nodemailer/mailparser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/mailparser/compare/v3.12.2...v3.12.4)

---
updated-dependencies:
- dependency-name: mailparser
  dependency-version: 3.12.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:33:31 +00:00
dependabot[bot] 067efac7f8 Bump @aws-sdk/client-s3 from 3.1155.0 to 3.1159.0
Bumps [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) from 3.1155.0 to 3.1159.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1159.0/clients/client-s3)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1159.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:33:21 +00:00
dependabot[bot] 2bdd16060e Bump imapflow from 2.8.0 to 2.9.1
Bumps [imapflow](https://github.com/postalsys/imapflow) from 2.8.0 to 2.9.1.
- [Release notes](https://github.com/postalsys/imapflow/releases)
- [Changelog](https://github.com/postalsys/imapflow/blob/master/CHANGELOG.md)
- [Commits](https://github.com/postalsys/imapflow/compare/v2.8.0...v2.9.1)

---
updated-dependencies:
- dependency-name: imapflow
  dependency-version: 2.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:32:54 +00:00
dependabot[bot] 433bc62211 Bump helm/chart-releaser-action from 1.7.0 to 1.8.0
Bumps [helm/chart-releaser-action](https://github.com/helm/chart-releaser-action) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/helm/chart-releaser-action/releases)
- [Commits](https://github.com/helm/chart-releaser-action/compare/cae68fefc6b5f367a0275617c9f83181ba54714f...df287104677278f869ca5480bd260358397cc3f9)

---
updated-dependencies:
- dependency-name: helm/chart-releaser-action
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:32:54 +00:00
dependabot[bot] 29f9c62a64 Bump docker/login-action from 4.6.0 to 4.7.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.6.0 to 4.7.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/dbcb813823bdd20940b903addbd779551569679f...81c6f93d4bc991e510b342a00b419b3bb4efc5c6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:32:36 +00:00
github-actions[bot] 21766cf38d Bump versions for v12.77 2026-10-31 19:47:43 +00:00
Lauri Ojansivu 101cdb7579 Prepare v12.77 release 2026-10-31 21:46:40 +02:00
Lauri Ojansivu 54cf951d9c Merge remote-tracking branch 'origin/main' 2026-10-31 21:46:38 +02:00
Lauri Ojansivu 57a43ee8e1 CHANGELOG: the date-popup guards follow movable popups and the date picker's empty cells.
Thanks to xet7 !
2026-10-31 21:44:13 +02:00
Lauri Ojansivu ff2c0ed7f6 Tests: the date-popup guards follow movable popups and the date picker's empty cells.
Two node guards still described v12.76's behaviour before it changed on
purpose. calendarPopupResize asserted that a popup without a date editor
does not drag; every popup drags by its title bar now (#6839), and with
drag handles on only by its handle - both pinned instead, its mock no
longer answering every selector as if a drag handle were there.
calendarTodayMarker picked a month's days with filter(Boolean), which an
empty cell passes now that it is an object with its own _id (#6838); it
skips the cells marked empty.

Thanks to xet7 !
2026-10-31 21:44:11 +02:00
github-actions[bot] 63d022fe5f Bump versions for v12.76 2026-10-31 19:44:06 +00:00
Lauri Ojansivu e9cf825b22 Prepare v12.76 release 2026-10-31 21:43:03 +02:00
Lauri Ojansivu 4ce8514e36 CHANGELOG: open issues #6834-#6840 - Today in the date picker, the card title and movable popups, Change Settings, the d shortcut, minicard dates.
#6834 is fixed in part and #6837 needs a decision: both in TODO Later.

Thanks to mimZD and xet7 !
2026-10-31 21:32:28 +02:00
Lauri Ojansivu 429fdc5319 Tests: a card-button rule is a button on the minicard (#6836 was already there).
"Show rule button on minicard" is Board Settings / Card, Card buttons,
Show on Minicard (allowsCardButtonsOnMinicard), in WeKan since v12.16:
the board's card-button rules are drawn under each minicard, and a click
runs the rule on that card without opening it. Off by default, which is
why it was not seen. It had no test.

minicard-rule-button-6836.e2e.js (Chromium and WebKit): with it on, a
card-button rule's button is on the minicard and a click archives the
card without opening it; with it off, no minicard has one.

Thanks to mimZD and xet7 !

Fixes #6836,
2026-10-31 21:31:36 +02:00
Lauri Ojansivu ffd5cbd2ad Change Settings: icons in a column on the first line, compact wrapped lines, rows divided.
Member Settings / Change Settings used the shared popup list as it is:
a flex row whose every line is 33px high with its icon centred. Its
settings are sentences, and most wrap - in Persian, in German - so a
setting became two tall lines with its icon floating between them and
the next setting right under it, with nothing to tell where one ended.
Each row is now an icon column, the words and the check mark at the
end; the icon sits on the first line, a wrapped line is an ordinary
line high, a rule divides the rows, and the Card settings checkbox has
the same space beside its words as an icon has.

Scoped to the popup's own list (change-settings-list): a popup keeps
the screens before it - the member menu - in the same .pop-over, so its
data-popup would have restyled those too.

change-settings-popup-6835.e2e.js, in Persian and English, Chromium and
WebKit: each row's icon is on its first line, a line is under 28px high
and a row only as tall as its lines, rows are divided, and the member
menu beside it is untouched.

Thanks to mimZD and xet7 !

Fixes #6835,
2026-10-31 21:29:31 +02:00
Lauri Ojansivu cf4b48df09 Minicard: a due date uses the card's width instead of three lines beside an empty strip.
The dates row of a minicard reserved 84px at its trailing edge - a third
of a 250px card - for nothing: the avatars float after it, and clear:
both starts it below the menu. A due date with its "1 day left" was
broken over three lines beside that empty strip, in a Persian board of
the report. It reserves the drag handle's room now, 24px, and the same
badge takes two lines at the default list width.

The board header bar wrapping to a second row, and the list header icons
under the card count, in the same report, did not reproduce on the
current code at the same window width, in Persian, with the card count
on; they are in CHANGELOG TODO Later until the reporter can say which
WeKan and which browser zoom showed them.

minicard-dates-width-6834.e2e.js (Chromium and WebKit, English and
Persian): the dates row reserves at most 24px, and the due date badge is
at most two lines high.

Thanks to mimZD and xet7 !
2026-10-31 21:26:15 +02:00
Lauri Ojansivu 73c21dfe3e Tests: typing d in the card title types at the end of the title in WebKit too.
WebKit puts the caret at the start of a field on focus and Chromium at
the end, so the d typed into the title landed at its start in WebKit and
the check for a title ending in dd failed there - while what the test is
for, that d typed in a field opens no Due date editor, held in both.
The test puts the caret at the end itself now.

Thanks to xet7 !
2026-10-31 21:23:04 +02:00
Lauri Ojansivu 22a12dc7f8 Opened card: the whole title edits; the window and every popup move by their bar or handle.
The opened card's title was split like a minicard's: the leading half
opened the editor and the trailing half dragged the window, so clicking
the right of the title, or beside a short last line, did not edit. Now
all of the title edits. With drag handles off, the window is moved from
the empty space of its header row above the title - between the collapse
caret and the menu; with drag handles on, only by the drag handle. A
reader who may not edit has no edit zone and still drags by the title.

Every popup now moves the same way - by its title bar with drag handles
off, by the handle before its title alone with them on - where only the
Drawing, Board View and Board Card settings popups and the date popups
moved before. A dragged popup is kept inside the window as it grows. The
drag pins a popup where it is before moving it: one placed from the
right edge (the member menu) jumped across the window, and one with a
margin by that margin, as the drag began.

card-title-edit-and-move-6839.e2e.js, in Chromium and WebKit: a two-line
title edits from its right, middle and left; the empty header row moves
the window by exactly the drag with handles off and not at all with them
on, when the handle does; and the card menu, Labels and the member menu
move by their title bar, or with handles on by the handle alone.
titleClickToEdit, popupMoveMaximize and popupBounds pin it.

Thanks to mimZD and xet7 !

Fixes #6839,
2026-10-31 21:23:04 +02:00
Lauri Ojansivu b1403ca7af The d shortcut opens the Due date editor of a card that is still being opened.
On a slower server a card's details render a few hundred milliseconds
after the click that opens it. `d` pressed in that moment found no Due
date control yet and did nothing - only the hovered-minicard path (#6755)
waited for it. Both paths now wait for THAT card's control, stop if
another card was opened meanwhile, click it once, and give up after 3
seconds.

The card details also closed any popup of their card when they were
destroyed (#6686), which a re-render of the card still open does too: a
popup could open and shut again at once. They close it now only when
the card is no longer the open one - closed, or another card opened.

The Playwright test emulates a 300 ms connection in Chromium, clicks
each card open and presses d at once: the editor opens and is still
open 1.5 s later, and closing the card still closes it. It fails on the
code before this change. dueDateKeyboardShortcut and
labelsPopupStaleCardContext pin both halves.

Thanks to mimZD and xet7 !

Fixes #6840,
2026-10-31 21:06:53 +02:00
Lauri Ojansivu bc21c901f4 Date picker: a Today button, and a month navigated back to is drawn whole.
The board Calendar view already has Today (and Calendar mode too), but
the date picker of every date popup - Due, Start, End, Received and the
rest - had only « ‹ › », so a user who paged to another month had to
page back. Today, between the month and ›, shows today's month and
focuses today; it changes nothing until a day is clicked and saved.

Testing it found a picker bug that was already there: going back to a
month drew it wrong - October came back as "29, empty, 30, empty", with
no 31st. Blaze matches list items by _id, and the weeks and days had
none; the empty cells, all alike, were moved around. Every week and
cell, empty ones too, now has an _id that names its month.

calendarPickerControls.test.cjs checks the ids, that Today shows today's
month and focuses today, and that it selects nothing; the Playwright test
pages two months ahead, presses Today, and checks every day of the month
in order, today focused, and the card's due date unchanged.

Thanks to mimZD and xet7 !

Fixes #6838,
2026-10-31 21:06:53 +02:00
Lauri Ojansivu 88fef5dc08 CHANGELOG: the Password length strings in every language, and its title bar.
Thanks to xet7 !
2026-10-31 15:18:23 +02:00
Lauri Ojansivu 3f9379ac86 Translations: the Password length page and Change your password, in 234 languages.
The 18 new strings of Admin Panel / Problems / Password length and of the
"Change your password" dialog, written in the language and script each
locale tag names, with the terms each file already uses for password,
account, save and sign in, and __min__ kept exactly. Only those 18 keys
changed in each file, written with fill-translations.mjs --apply, which
fills English placeholders only.

Low confidence, for a speaker to correct on Transifex: ace, ak, an, ary,
ast-ES, ay, ba, bho, bi, bm, bo, br, bua, chr, co, csb, cv, cy, dz, ee,
ff, fj, fo, fur, ga, gd, gn, gv, ha, haw, hsb, ig, iu, jv, kl, kok, ks,
kw, lg, lld, mg, mi, mt, nah, nap, nd, nso, ny, om, pap, qu, rm, rn,
rup, rw, sah, sc, scn, sd, se, si, sm, sn, so, ss, st, szl, te-IN, tg,
ti, tig, tk_TM, tlh, tn, to, tpi, ts, tt, uz-AR, ve, ve-CC, ve-PP, vo,
wa, wa-RR, wal, wo, wuu-Hans, xh, yi, zgh.

Thanks to xet7 !
2026-10-31 15:18:13 +02:00
Lauri Ojansivu 85af731c1b Admin Panel / Problems / Password length names itself in the title bar.
The title bar keeps its own copy of the menu labels, and the new pane
had none: tests/adminUrls.test.cjs failed on problems/password-length.

Thanks to xet7 !
2026-10-31 15:18:11 +02:00
Lauri Ojansivu bb32a50550 CHANGELOG: sign-up works again, Admin Panel / Problems / Password length, and a changed password signs every session out.
Thanks to xet7 !
2026-10-31 14:56:22 +02:00
Lauri Ojansivu c99aa7f845 Admin Panel / Problems / Password length, and a changed password signs every session out.
PASSWORD_MIN_LENGTH (default 8) is now the length below which a sign-in
is REPORTED, in the new Admin Panel / Problems / Password length, and
PASSWORD_MIN_LENGTH_ENFORCE (off by default) is what makes it a rule: a
shorter new password is refused, and an account that signs in with an
older shorter one sees only "Change your password" until it has a long
enough one. Signing in is never refused, enforced or not, so nobody is
locked out by a minimum set after their password. LDAP, OAuth2, OIDC,
SAML, CAS and Sandstorm accounts are neither reported nor asked: their
password is their provider's. An administrator's Set password no longer
applies WeKan's policy to such an account.

Meteor sends a password hashed, so only the browser knows its length: it
notes it on the sign-in form and, after a sign-in that worked, says "it
was short" - never the password or its length.

The page has both settings and a table, one row per account: username,
first and last short-password login, the latest IPv4 and IPv6 address
with the country's flag and city when a CDN says where it is, the count,
and when it was fixed, with the Too short (default) / Fixed / All filter.
Fixed is a changed password not seen short since; a short one seen
again reopens the row.

Changing one's own password - Member Settings, the forced change, a
reset link - now signs EVERY session of the account out, the one that
changed it too, and only the new password signs in again. The browser
goes to the sign-in page as soon as it is signed out, since the server
closes the connection before the answer can arrive.

Documented in docs/Features/Admin-Panel/Problems/Password-length.md, the
settings on every platform. The new strings are in every locale in
English for now. Tests: passwordPolicy, passwordChangeRequired and
shortPasswordLogins (node); password-min-length-signup-login,
password-length-report and password-change-other-sessions (Playwright,
Chromium and WebKit), which change each setting on the page and check
what it does, and that sign-up with no mail server registers and signs
the user in.

Thanks to xet7 !
2026-10-31 14:54:22 +02:00
Lauri Ojansivu 508484ef1e Sign-up, and every method and publication, pass audit-argument-checks again.
Registering answered "Internal server error" - and created the account
anyway, so the next try said it already existed - and the
starredPublicBoards and translation publications failed for everybody:
"Did not check() all arguments". audit-argument-checks asks,
synchronously, the moment a handler returns, whether check() saw every
argument. The REQUIRE_2FA wrapper of every method and publication, and
the Audit log's wrapper of the administrators' methods, were async and
awaited before they called the handler - even with REQUIRE_2FA off - so
the handler's own check() came after the audit had already failed the
call, and the handler went on running.

A wrapper now calls the handler at once when it needs no lookup
(REQUIRE_2FA off, nobody signed in, a cached answer). When it must await
first, it hands the audit over (server/lib/argumentChecks.js): the
arguments count as seen for the outer audit, and the handler runs under
an audit of its own that it must satisfy exactly as unwrapped.

tests/wrappedHandlerArgumentChecks.test.cjs runs both wrappers under a
model of the check package's audit: a checking handler passes, off and
on, a refusal stays a refusal, a handler that does not check still fails
on every path, and no wrapper in server/ awaits before it has checked.

Thanks to xet7 !
2026-10-31 14:13:40 +02:00
Lauri Ojansivu 548196c9a9 CHANGELOG: Meteor in .tools/.meteor no longer springboards forever.
Thanks to xet7 !
2026-10-31 13:23:36 +02:00
Lauri Ojansivu 9a118c83b2 Meteor in .tools/.meteor no longer springboards forever.
A build from the menu printed uname -p and sysctl hw.cpu64bit_capable
over and over and never reached its first package. With
METEOR_WAREHOUSE_DIR set and no SANDBOX, Meteor takes the running tool
to be wherever the warehouse's "meteor" link points - 3.5.4, the release
it was installed with - so with .meteor/release at 3.6-rc.1 every start
springboarded to 3.6.0-rc.1, found itself the wrong tool again and
springboarded again: 28 times in 20 seconds of meteor --version. The
earlier 1.1 GB build log was the same loop, its module trace printed
anew on every exec.

tools-env.sh and tools-env.bat now set SANDBOX=true beside the
warehouse, as Meteor's own self-test does; that one check is all it
changes. The Nextcloud test scripts and the dev-source runner, which set
the warehouse themselves, do the same. meteor --version now springboards
once, from the linked 3.5.4 to 3.6.0-rc.1.

tests/meteorSpringboardLoop.test.cjs checks both env scripts, that
nothing sets the warehouse without SANDBOX, and runs the installed
Meteor to count its springboards.

Thanks to xet7 !
2026-10-31 13:23:24 +02:00
Lauri Ojansivu d442bd8a7e CHANGELOG: builds trace module resolution only with WEKAN_BUILD_TRACE_MODULES=1.
Thanks to xet7 !
2026-10-31 13:02:19 +02:00
Lauri Ojansivu 5a13e927bc Builds trace module resolution only with WEKAN_BUILD_TRACE_MODULES=1.
Turning the NODE_DEBUG=module trace off for EVERYTHING alone was not
enough: a dev build from the build.sh menu wrote 1.1 GB, three million
lines, in its first three minutes, before it was interrupted. The trace
is now opt-in in build.sh and build.bat alike, and the diagnostics line
says whether it is on and how to turn it on. The other build diagnostics
(resolver commands, npm verbose, METEOR_PROFILE) are unchanged.

Thanks to xet7 !
2026-10-31 13:01:57 +02:00
Lauri Ojansivu c78af24a36 CHANGELOG: EVERYTHING builds without the per-require module trace.
Thanks to xet7 !
2026-10-31 12:46:14 +02:00
Lauri Ojansivu 6551062492 EVERYTHING builds without the per-require module trace.
The build's NODE_DEBUG=module trace wrote over 12 GB in the first half
hour of an EVERYTHING run on macOS - twice, through tee - and slowed the
build it traced, so the run would fill the disk before reaching the tests.
A build from the menu still traces by default; WEKAN_BUILD_TRACE_MODULES=0
turns it off, and EVERYTHING sets that unless the caller asked for 1. The
other build diagnostics (resolver commands, npm verbose, METEOR_PROFILE)
are unchanged.

Thanks to xet7 !
2026-10-31 04:15:01 +02:00
Lauri Ojansivu a0ed5de6f1 CHANGELOG: the breached-password check's lookup digest (CodeQL #599).
Thanks to GitHub CodeQL and xet7 !
2026-10-31 03:52:47 +02:00
Lauri Ojansivu f406d46fe7 Breached-password check: one Web Crypto lookup digest for the browser and the server.
CodeQL #599 (js/insufficient-password-hash, CWE-916) flagged the SHA-1
the server computed for PASSWORD_CHECK_PWNED. That digest is not password
storage - accounts-password keeps passwords with bcrypt - and it is never
stored or logged: Have I Been Pwned's range API is keyed by SHA-1, its
first 5 hex digits are the question and the rest is compared in memory.
A slow hash cannot be used, because the service could not match it.

The browser and the server now compute it with one implementation,
breachLookupDigest in models/lib/passwordPolicy.js, which says what it is
and is not; the server no longer hashes a password with a hash of its
own, and a digest that cannot be computed lets the password through.
Tests pin that the digest is SHA-1 for ASCII and UTF-8, that it reaches
only the lookup and the comparison, and that nothing in WeKan hashes a
password-named value with a fast hash. Unreleased hardening, so no Hall
of Fame entry: docs/Security/Password-Breach-Check-Digest-2026-10-31.md.

Thanks to GitHub CodeQL and xet7 !
2026-10-31 03:52:32 +02:00
Lauri Ojansivu 4b0aa715eb Merge remote-tracking branch 'origin/main' 2026-10-31 03:36:43 +02:00
Lauri Ojansivu f5af33ac38 CHANGELOG: Audit log documentation.
Thanks to xet7 !
2026-10-31 00:44:17 +02:00
Lauri Ojansivu f165a24192 docs: Admin Panel / People / Audit log.
Thanks to xet7 !
2026-10-31 00:44:14 +02:00
Lauri Ojansivu e6ea524541 CHANGELOG: password policy, compulsory 2FA, idle sign-out, audit log, HSTS, cross-site guard and S3 encryption.
Thanks to xet7 !
2026-10-31 00:33:31 +02:00
Lauri Ojansivu de23dece66 Translations: the password policy, two-factor, audit log and S3 encryption strings in 234 languages.
The 16 strings the security features added were English placeholders in
every locale; each is now in the language and script its locale tag
names, with the terms the file already uses for password, account,
attachment, backup and two-factor authentication. __min__ is kept
character for character. Where the right word is spelled like the
English ("Administrator"), the column header says "Admin" or "Who"
instead, because a value equal to English is not a translation.

fy and fy-NL: the existing two-factor strings were Dutch in a Frisian
file, and are Frisian now; tests/frisianTwoFactorLanguage.test.cjs.
tig: Tigre spells storage መክዘን, not Tigrinya's መኽዘን.

Low confidence, for a speaker to correct on Transifex: ace, ak, an, ary,
as, ast-ES, ay, ba, bho, bi, bm, bo, br, bua, chr, ckb, co, csb, cv, dz,
ee, ff, fj, fy, gn, gv, haw, hsb, ht, ig, iu, kl, kok, ks, kw, la, lg,
lld, mg, mi, nah, nd, nso, ny, om, pap, ps, qu, rm, rn, rup, rw, sah, sc,
scn, sd, se, si, sm, sn, so, ss, st, szl, tig, tlh, tn, to, tpi, ts,
uz-AR, ve, ve-CC, ve-PP, vo, wa, wa-RR, wal, wo, xh, yi, zgh.

Thanks to xet7 !
2026-10-31 00:32:43 +02:00
Lauri Ojansivu 2017f2871f Audit log: its own Admin Panel URL and title, and right-to-left table alignment.
/admin/people/audit-log opens the pane and names it in the title bar like
every other People pane, and the table aligns to the start of the line,
so it reads right to left in Arabic, Hebrew, Persian and the rest.

Thanks to xet7 !
2026-10-31 00:32:33 +02:00
Lauri Ojansivu 302b47db6f docs: Security settings, every hardening option in one page.
Thanks to xet7 !
2026-10-31 00:15:13 +02:00
Lauri Ojansivu 482c2c29f4 Attachments, avatars and backups in S3 can be encrypted at rest.
Admin Panel / Attachments / S3 / Server-side encryption: SSE-S3 (AES256)
or SSE-KMS (aws:kms, with a KMS key or the account's default). S3
encrypts each object as it is written and decrypts it for WeKan on read,
so reads, range requests and moves are unchanged. Every attachment and
avatar upload, every renamed object and every backup written to S3 now
carries the choice; a value that is not one of them, or a malformed key,
adds nothing.

On the filesystem, encryption at rest stays with the disk or volume
(LUKS, an encrypted cloud volume): exports, PDF, thumbnails, virus scans,
backups and WebDAV all read the stored files directly, and encrypting
them inside WeKan would change every one of those and convert every file
already stored.

The new interface strings are pending translation.

Thanks to xet7 !
2026-10-31 00:12:09 +02:00
Lauri Ojansivu 4d5b832499 Admin Panel / People / Audit log: what site administrators did.
Nothing recorded who set somebody's password, made somebody an admin,
changed a login setting or restored a backup. Every call of those
administrative methods by a site administrator is now kept, one document
each, for a year: when, who, which action, the outcome - a refused call
too - its arguments and the address. A password or a secret never
reaches the trail: values under a credential-named key, the arguments
that are a password by position, and a password inside a URL are
replaced by [redacted]. An administrator editing cards is not in it,
the trail is shown to site administrators only, and a trail that cannot
be written never changes what the method did. It and the REQUIRE_2FA
wrapper keep each other's marks, so neither wraps the other twice.

The new interface strings are pending translation.

Thanks to xet7 !
2026-10-31 00:09:20 +02:00
Lauri Ojansivu 41173f0f6b Sign a browser session out after it has been idle for LOGOUT_IDLE_MINUTES.
LOGOUT_WITH_TIMER ends a login at a deadline however busy its user is;
nothing ended a session somebody walked away from. LOGOUT_IDLE_MINUTES
(Admin Panel / People / Login, every platform; empty or 0 is off) does:
the browser reports a key press, click, touch or scroll at most once a
minute, the server keeps the time per login token in idleSessions, and
once a minute every token idle past the limit is removed, which signs its
connections out. A tab that reconnects and resumes its login by itself -
a laptop waking up - is not activity and does not extend the session.
REST API tokens have no session record and are never removed. The time
is kept beside the user rather than on the token, because updating one
element of the token array needs the positional $ operator, which not
every database WeKan runs on supports.

Thanks to xet7 !
2026-10-31 00:08:20 +02:00
Lauri Ojansivu d7abe9910a Password policy for new passwords, and compulsory two-factor authentication.
New passwords: PASSWORD_MIN_LENGTH (default 8) and PASSWORD_CHECK_PWNED
(off by default), in Admin Panel / People / Login and on every platform.
The browser sends a password to Meteor hashed, so the server checks every
password that reaches it as text - Admin Panel Set password and Add user,
POST /users/register - and the sign-up, Change Password, reset and
enrolment forms check in the browser. Signing in is never checked, so an
existing shorter password keeps working until it is changed. A breached
password is found with Have I Been Pwned's k-anonymity range API: only 5
hex digits of the SHA-1 leave WeKan, and an unreachable service never
refuses a password.

REQUIRE_2FA: off (default), admins or all. It covers accounts that sign in
with a WeKan password; LDAP, OAuth2, SAML, CAS and Sandstorm accounts sign
in through an identity provider and are not asked. Such an account can
still sign in, sees only the 2FA set-up until it has 2FA, and until then
the server refuses every method and publication but signing in and
enrolling, HTTP routes do not accept its login and the REST API gives it
no token. Once required, 2FA cannot be switched off.

The new interface strings are pending translation.

Thanks to xet7 !
2026-10-31 00:07:58 +02:00
Lauri Ojansivu 48a2e97ea3 A state-changing request that logs in only with the cookie must come from WeKan itself.
POST /import-trello and other HTTP routes accept the login cookie as the
login, so a page on another site could make a signed-in browser send one.
The cookie is SameSite=Strict, which keeps it off such a request in
today's browsers; this is the second line, for older browsers, same-site
sibling hosts and routes added later. A POST, PUT, PATCH or DELETE whose
only login is the cookie must carry an Origin, or Referer, naming
ROOT_URL's host or the Host / X-Forwarded-Host it was sent to, which on
Sandstorm is the grain's own host. Token-authenticated API calls, Meteor's
cookie routes and DDP are not affected. A refusal is recorded in Admin
Panel / Problems as csrf.cross-site-cookie and never blocks the account
whose cookie it was.

Thanks to xet7 !
2026-10-30 23:52:09 +02:00
Lauri Ojansivu 4993242c5f Strict-Transport-Security on every response of an HTTPS site.
WeKan sent no HSTS header, so a link typed as plain HTTP or a downgrade on
a hostile network could strip TLS from the next visit unless the reverse
proxy added it. It is now sent whenever ROOT_URL is https: 180 days, this
host only. HSTS_MAX_AGE, HSTS_INCLUDE_SUBDOMAINS and HSTS_PRELOAD (Admin
Panel / People / Login, every platform) change it; 0 stops it, and preload
is sent only with includeSubDomains and at least a year. Every response
also carries X-Content-Type-Options: nosniff and
Referrer-Policy: strict-origin-when-cross-origin.

Thanks to xet7 !
2026-10-30 23:50:02 +02:00
Lauri Ojansivu 12eef31162 CHANGELOG: snap backup and restore instructions work on AlmaLinux, Rocky Linux and Fedora.
Thanks to Group-Deed and xet7 !
2026-10-30 23:14:01 +02:00
Lauri Ojansivu 0b5c855820 Snap backup and restore instructions work on AlmaLinux, Rocky Linux and Fedora.
snapd keeps the snaps in /var/lib/snapd/snap on Fedora, RHEL, AlmaLinux,
Rocky Linux, CentOS Stream and Arch Linux, which have no /snap, so the
instructions that exported /snap/wekan/current/bin and its lib directory
did not find mongodump and mongorestore there. They now pick the directory
with one S= line and the CPU's library directory with $(uname -m), and the
scripts that can be run from a host shell find the snap in either place.

Thanks to Group-Deed and xet7 !
2026-10-30 23:13:50 +02:00
Lauri Ojansivu 0c53ef6960 CHANGELOG: boards from before the templates feature are shown on All Boards again.
Thanks to xet7 !
2026-10-30 22:56:38 +02:00
Lauri Ojansivu 2ab0afdc93 Old boards without a type are shown on All Boards again.
Boards, swimlanes, lists and cards from before the templates feature have
no `type`: the schemas' defaultValue fills it on insert only. All Boards
publishes `type: 'board'` and the card queries ask for 'cardType-card', so
a Sandstorm grain migrated from MongoDB 3 opened onto an empty All Boards
page although its board was intact in the database. The new
type-defaults-backfill startup step sets the schema default on every
document whose type is missing, null or empty, and never touches an
explicit type.

Thanks to xet7 !
2026-10-30 22:56:24 +02:00
github-actions[bot] f2a1d7b9ba Bump versions for v12.75 2026-10-30 20:26:45 +00:00
Lauri Ojansivu cd1f1b8f05 Prepare v12.75 release 2026-10-30 22:25:30 +02:00
Lauri Ojansivu b941f36e4c Merge remote-tracking branch 'origin/main' 2026-10-30 22:25:01 +02:00
Lauri Ojansivu 487a528806 CHANGELOG: a changed password signs the account out in every other browser.
Thanks to xet7 !
2026-10-30 21:39:35 +02:00
Lauri Ojansivu dc9a7bb1ac Tests: a changed password signs the account out in every other browser.
A report described "failure to invalidate session on password change": the
same account signed in on two browsers, the password changed in one, and
the other's session "updated" rather than ended. WeKan does not do that.
Member Settings / Change Password is Meteor's changePassword, which removes
every login token but the changing session's; the admin's Set password is
setPasswordAsync, which removes them all, as resetPassword does; and a
session whose token is gone is closed, while WeKan's HTTP routes accept only
a token still in the list.

password-change-other-sessions.e2e.js runs the report in two browser
contexts: both signed in with the password, the password changed through
the form in one, and the other is signed out within seconds, stays signed
out after a reload, and only the new password signs in. It passes three
times in a row in Chromium and in WebKit. passwordChangeSessions.test.cjs
pins those paths and fails on a password change with logout: false, the
LDAP sign-in's password sync being the one stated exception.

Thanks to xet7 !
2026-10-30 21:39:20 +02:00
github-actions[bot] 696a1a2c8a Bump versions for v12.74 2026-10-30 08:15:31 +00:00
Lauri Ojansivu ef1e683cdd Prepare v12.74 release 2026-10-30 10:14:18 +02:00
Lauri Ojansivu 55163d0ce6 Merge remote-tracking branch 'origin/main' 2026-10-30 10:14:15 +02:00
Lauri Ojansivu b974ba426d CHANGELOG: Caddy and FerretDB run with no MPTCP listener.
Thanks to xet7 !
2026-10-30 04:31:14 +02:00
Lauri Ojansivu e9751b65d4 Caddy and FerretDB run with no MPTCP listener (CVE-2026-98432).
CVE-2026-98432 is a Linux kernel use-after-free in IPv4 TCP connection
handling, shown to give remote code execution through an MPTCP listener
(golang-announce, 2026-10). Since Go 1.24 every Linux listen socket of a
Go program is MPTCP by default, and WeKan ships two Go servers: Caddy, the
snap's web server on ports 80 and 443, and FerretDB, the default database.

Every place WeKan starts one sets GODEBUG=multipathtcp=0: the snap's
environment (every app, Caddy and FerretDB among them), the Docker
entrypoint and the bundle launcher - which keep an operator's own
multipathtcp setting - every docker-compose FerretDB service (the commented
FerretDB 2 example too), and the FerretDB the Sandstorm grain starts. Caddy
is an upstream binary, so no rebuild is needed: Go reads GODEBUG at start.
wekan/FerretDB builds with `godebug multipathtcp=0` as well. The real fix
is a patched host kernel.

Checked on Linux: a Go server's default listener reports MPTCP true;
GODEBUG=multipathtcp=0, or the go.mod setting, makes it false.
tests/goMptcpOff.test.cjs pins every launcher, runs the launcher line, and
fails when something turns MPTCP back on.

Thanks to xet7 !
2026-10-30 04:30:39 +02:00
github-actions[bot] 5abc2809b9 Bump versions for v12.73 2026-10-30 02:18:18 +00:00
Lauri Ojansivu 03f3a197cf Prepare v12.73 release 2026-10-30 04:17:07 +02:00
Lauri Ojansivu 6ed3f5cfd0 CHANGELOG: v12.73 was not released, so its section is Upcoming again.
"Prepare v12.73 release" renamed Upcoming, but its push was refused and
the release workflow was never dispatched: there is no v12.73 tag and no
v12.73 GitHub Release. As CLAUDE.md says for a release that published
nothing, the heading goes back to "# Upcoming WeKan ® release", so the next
release-all.sh takes v12.73 again. It also gains the entry for the fix that
keeps this from happening again.

Thanks to xet7 !
2026-10-30 04:16:04 +02:00
Lauri Ojansivu e372016826 Release All pulls before releasing, and pulls and retries when its push is refused.
v12.73 was prepared on a main that origin had moved past: the release
bot's v12.72 version bump had landed meanwhile. release-all.sh renamed
Upcoming and committed "Prepare v12.73 release", its bare git push was
refused, and set -e stopped it before gh workflow run - so no release ran,
and no tag was made, while the CHANGELOG already said v12.73.

release-all.sh now runs build.sh's git pull right after its checks, before
it chooses the version or renames anything, and checks the Upcoming section
again after it; and it pushes with build.sh's git push, which repairs moved
CHANGELOG links and pulls and pushes once more when origin moved meanwhile.
If the push still does not finish, it says that the workflow was NOT
dispatched, and the command to dispatch it.

releaseUpcomingPreflight pins the order - the pull before the version and
the rename, the push before the dispatch - and fails on a bare git push;
releaseTelemetry and the version-selection fixture follow.

Thanks to xet7 !
2026-10-30 04:14:48 +02:00
Lauri Ojansivu fd699b67e9 Merge remote-tracking branch 'origin/main' 2026-10-30 04:07:50 +02:00
Lauri Ojansivu 827641193f Prepare v12.73 release 2026-10-30 04:07:36 +02:00
Lauri Ojansivu fb7bd9c6de CHANGELOG: CodeQL alert #598, the Sandstorm host test.
Thanks to xet7 !
2026-10-30 04:02:02 +02:00
Lauri Ojansivu fcd995095d Tests: the Sandstorm host check names no host but sandstorm.org.
CodeQL alert #598 (incomplete URL substring sanitization, CWE-20) flagged
tests/sandstormOrgHosts.test.cjs, which skipped files that did not include
the old Sandstorm host name as a substring. The file no longer names that
host at all. It finds every Sandstorm host on a top-level domain other than
org by its shape, written as a URL - a scheme or // before it, or a port or
path after it - so property paths such as services.sandstorm.id, file names
such as sandstorm.js and the domains reserved for tests are not hosts. The
demo and the store workflow's older-market publish step stay allowed by
their subdomain and place.

A new self-test checks what it finds and what it does not; run against the
docs and workflows from before the sandstorm.org switch it finds 13 old
hosts, the installer address of release-all.yml among them.

Thanks to xet7 !
2026-10-30 04:01:36 +02:00
Lauri Ojansivu b7b5ec3711 CHANGELOG: the short CHANGELOG.md starts with the upcoming release.
Thanks to xet7 !
2026-10-30 03:56:32 +02:00
Lauri Ojansivu 1af642069c The short CHANGELOG.md starts with the upcoming release's In short.
CHANGELOG.md showed only the five newest releases, so what was about to be
released was only in CHANGELOG-big.md. changelog-short.mjs now puts the
unreleased # Upcoming WeKan ® release first - its In short and a link to
its full section - and the five newest releases below it; right after a
release, when there is no Upcoming yet, none is invented.

CLAUDE.md and AGENTS.md say what "update wekan changelog" means: find the
commits not in the changelog yet, add an Upcoming section when a release
was cut meanwhile, write the entries and the In short into
CHANGELOG-big.md, run changelog-short.mjs, run the changelog tests, and
commit both files.

tests/changelogShort.test.cjs: Upcoming first, whole, above the releases,
its link resolves, and no Upcoming appears after a release.

Thanks to xet7 !
2026-10-30 03:56:11 +02:00
Lauri Ojansivu 0343e6614b CHANGELOG: the short CHANGELOG.md, written from CHANGELOG-big.md.
Thanks to xet7 !
2026-10-30 03:30:23 +02:00
Lauri Ojansivu f1c0db247a CHANGELOG.md is the short changelog, written from CHANGELOG-big.md.
CHANGELOG-big.md is the changelog as it was: # Status, # Upcoming and every
release in full, with its <details> entries. Everything that writes or edits
the changelog uses it - release-all.sh, release-notes.sh and the release
workflow, the commit-link repair in build.sh and fix-changelog-hashes.sh,
the archive, the wrapper, the version scripts - and the release notes'
"More details at ChangeLog" link goes to its section. remote-release.py,
shared with FerretDB and the patch repositories, takes the file from its
config, and only WeKan's names CHANGELOG-big.md.

CHANGELOG.md is new: no Status and no Upcoming, only the In short paragraph
of each of the five newest releases and a link to its full section, in plain
Markdown - no <details>, <summary>, </summary> or </details>, and HTML links
written as Markdown links. releases/changelog-short.mjs writes it;
release-all.sh runs it right after naming the release, so it is in the
release commit, and changelog.sh does too. The Sandstorm App Market keeps
embedding CHANGELOG.md, now the short one.

old-CHANGELOG/'s headers say they moved out of CHANGELOG-big.md. References
to Upcoming or TODO Later in code comments and docs name CHANGELOG-big.md.
CLAUDE.md and AGENTS.md describe both files: CHANGELOG-big.md is written,
CHANGELOG.md is generated and never edited by hand.

tests/changelogShort.test.cjs: CHANGELOG.md is exactly what the generator
writes, the five newest releases with their whole In short, links that
resolve, and none of the four tags or an HTML link; the changelog tests
read CHANGELOG-big.md.

Thanks to xet7 !
2026-10-30 03:29:48 +02:00
Lauri Ojansivu 61e838df46 Rename CHANGELOG.md to CHANGELOG-big.md.
Only the rename, in a commit of its own, so that git log --follow and blame
keep the changelog's history; the next commit adds the new short
CHANGELOG.md and points the release tooling at CHANGELOG-big.md.

Thanks to xet7 !
2026-10-30 03:29:34 +02:00
github-actions[bot] 057cbc26d9 Bump versions for v12.72 2026-10-30 00:37:33 +00:00
Lauri Ojansivu 22e8b9bc86 Prepare v12.72 release 2026-10-30 02:36:31 +02:00
Lauri Ojansivu 602b47f06a CHANGELOG: new Sandstorm app market screenshots.
Thanks to xet7 !
2026-10-30 02:35:31 +02:00
Lauri Ojansivu a086323a1f Updated Sandstorm WeKan screenshots.
Thanks to xet7 !
2026-10-30 02:33:12 +02:00
Lauri Ojansivu 22140739ab CHANGELOG: card links never launch an OS handler, the Apache guide and Sandstorm publishing.
Thanks to ocdtrekkie and xet7 !
2026-10-30 01:26:40 +02:00
Lauri Ojansivu 55b71ce19e Tests: the Sandstorm store workflow may publish to apps.sandstorm.io too.
#6833 publishes WeKan to both Sandstorm app markets, apps.sandstorm.org
and the older apps.sandstorm.io, whose API is alpha-api.sandstorm.io.
sandstormOrgHosts.test.cjs refused every sandstorm.io host; it allows
that publish step in sandstorm-store.yml only, and checks that both
markets are published to.

Thanks to ocdtrekkie and xet7 !
2026-10-30 01:25:31 +02:00
Lauri Ojansivu 1a97c8bd86 Apache guide: no request captures in proxy targets, and httpd 2.4.60 or newer.
Checked against the 2024 Confusion Attacks on Apache httpd
(blog.orange.tw, CVE-2024-38472 to -38477 and CVE-2024-39573, fixed in
2.4.60). The guide's examples were reverse proxies only, with no
RewriteRule, but each passed the WebSocket with ProxyPassMatch and $1 - a
request capture in a proxy target, the shape those attacks abuse - and in
the sub-URL example that rule looked for ^/sockjs while the requests come
to /wekan/sockjs, so its WebSocket never connected.

- Both examples pass the WebSocket with "ProxyPass ... upgrade=websocket"
  on their fixed prefix (2.4.47+); no ProxyPassMatch remains.
- The guide asks for httpd 2.4.60 or newer, drops NameVirtualHost (a no-op
  in 2.4) and the path in the sub-URL ServerName, which is not valid.
- A "What not to add" section: no RewriteRule or ProxyPassMatch with a
  request capture in the target, no <Files>/<Location> ACL in front of
  WeKan, AllowEncodedSlashes Off and MergeSlashes On, <Directory /> denied.

Verified with Apache 2.4.67: both virtual hosts parse, and in front of a
WebSocket server both pass HTTP and the WebSocket, at / and at /wekan;
the old sub-URL example fails its WebSocket ("Unexpected server response:
200"). tests/apacheReverseProxy.test.cjs pins it and fails on the old guide.

WeKan's own redirects stay on its origin (CAS checks the callback's origin
against ROOT_URL; the file and avatar redirects build their own paths), so
a backend-chosen Location cannot be steered elsewhere.

Thanks to xet7 !
2026-10-30 01:24:48 +02:00
Lauri Ojansivu b4e5f90afb Card links never launch an OS handler with a code-execution history.
Custom URL schemes in card text link only when Admin Panel -> Features ->
URL lists them, and never javascript:, data: or another scheme that runs
code in the page. A listed operating-system handler was linked, though, and
some have a public code-execution history: ms-msdt (Follina,
CVE-2022-30190), search-ms and search, ms-officecmd, ms-appinstaller,
ms-cxh and ms-cxh-full, and telnet - the last step of the 2026 Edge full
chain, where Windows ran telnet.exe from a user-writable PATH directory.
An administrator listing one would let any board member plant a link that
runs something on whoever clicks it.

- They are never linked, listed or not, by the app's parser and by the
  markdown package's copy of the rule (the two lists must be identical),
  and a click on one does nothing should such a link reach the page.
- A card title or description, comment, checklist or checklist item that
  ADDS a link to one is shown in Admin Panel -> Problems (HandlerLink,
  CWE-939), with who wrote it; the text is saved as written and the writer
  is told nothing. The word in prose, or a link the text already had, is
  not recorded.
- docs/Features/Admin-Panel/Settings/Visibility.md lists both kinds of
  never-linked scheme, and says what a listed file: does on Windows (an SMB
  link outside the network sends the clicker's NTLM hash).

Tests: tests/urlSchemeAllowlist.test.cjs (positive and negative, through
the real DOMPurify, both sanitizer passes and the detector; fails on the
old code), and custom-url-schemes.e2e.js, which passes in Chromium and
WebKit: listed handlers do not link, a description saved with one through
the editor is recorded for its writer, and a kept link or a comment that
only mentions the word is not. Firefox does not start in this
environment.

Thanks to xet7 !
2026-10-30 01:19:16 +02:00
Lauri Ojansivu 2c1fc39f61 Fix Sandstorm store spk publish syntax.
Thanks to ocdtrekkie and xet7 !
2026-10-30 01:14:55 +02:00
Lauri Ojansivu ee82c21718 Merge pull request #6833 from ocdtrekkie/patch-1
Sandstorm: Publish to both app markets
2026-10-30 01:08:19 +02:00
Jacob Weisz 10851cd3f4 Sandstorm: Publish to both app markets 2026-10-29 18:04:45 -05:00
github-actions[bot] a937ed2180 Bump versions for v12.71 2026-10-29 22:39:44 +00:00
Lauri Ojansivu 22be2f2dc0 Prepare v12.71 release 2026-10-30 00:38:34 +02:00
Lauri Ojansivu 833c0bc021 CHANGELOG: two entries made after v12.70 go to Upcoming, not into v12.70.
The v12.70 release renamed Upcoming to v12.70 at 23:59, and the entries for
the Hall of Fame guards (43c4d41877) and the Sandstorm install address
(ca2dc5fae5), committed after it, were added to its Tests and Releases
groups - inside a released section, for commits that release does not
contain, and with no Upcoming section left for release-all.sh to take the
next version from. They move to a new Upcoming section; the v12.70 section is
again exactly what v12.70 shipped. changelogEntriesBelongToTheirRelease
passes.

Thanks to xet7 !
2026-10-30 00:36:01 +02:00
Lauri Ojansivu 1251c1c8b3 Merge remote-tracking branch 'origin/main' 2026-10-30 00:33:58 +02:00
Lauri Ojansivu 5b428faea6 CHANGELOG: the Sandstorm build installs from install.sandstorm.org.
Thanks to xet7 !
2026-10-30 00:28:08 +02:00
Lauri Ojansivu ca2dc5fae5 Sandstorm: build from install.sandstorm.org, and link sandstorm.org everywhere.
v12.69's build-sandstorm in release-all.yml stopped because the
certificate of install.sandstorm.io had expired. It fetches the installer
from install.sandstorm.org, as sandstorm.yml and sandstorm-store.yml
already did, and so does releases/install-sandstorm.sh. That installer
downloads Sandstorm from dl.sandstorm.org and configures a development
server at local.sandstorm.org, so the docs follow: docs, apps, dl, install
and local.sandstorm.io links and addresses become sandstorm.org in the docs,
SECURITY.md, code comments and the accounts-sandstorm README, each URL
checked to answer there. demo.sandstorm.io stays, as it has no sandstorm.org
name; released CHANGELOG sections are records and stay as they are. The
release audit's baseline allows the two code comments' docs.sandstorm.org.

tests/sandstormOrgHosts.test.cjs: every Sandstorm build installs from
install.sandstorm.org, and no tracked file names a sandstorm.io host
outside those kept on purpose; it fails on the old release-all.yml.

Thanks to xet7 !
2026-10-30 00:27:54 +02:00
Lauri Ojansivu b9f6eff3d9 CHANGELOG: the Hall of Fame guards read linked vulnerability names.
Thanks to xet7 !
2026-10-30 00:20:59 +02:00
Lauri Ojansivu 43c4d41877 Tests: the Hall of Fame guards read a linked vulnerability name too.
hallOfFameProblemsCoverage and securityRegressionCoverage collected the
names from <td valign="top"><b>Name</b></td>, so a name that was a link to
its page was never seen. Now that every name is one, the match takes the
link; the four it had missed - ImportPlacementBleed, SamlSubjectBleed,
StampBleed and UploadBleed - are listed as guarded by the suites that name
them, and the inventory count goes from 137 to 141.

Thanks to xet7 !
2026-10-30 00:20:47 +02:00
github-actions[bot] 609c6d3d64 Bump versions for v12.70 2026-10-29 22:00:25 +00:00
Lauri Ojansivu 8097935f5d Prepare v12.70 release 2026-10-29 23:59:10 +02:00
Lauri Ojansivu 4f7222f541 CHANGELOG: AppImages, Launchpad snap source, certificate errors and three test fixes.
Thanks to xet7 !
2026-10-29 23:50:04 +02:00
Lauri Ojansivu 6bbb4d39bc Downloads: an unverifiable TLS certificate is named as that, not as an outage.
v12.69's build-sandstorm stopped because install.sandstorm.io's certificate
expired that day (Let's Encrypt, notAfter 2026-10-29 19:02 GMT), and
fetch.sh reported "The server is having an outage" for curl's exit 60. It
now says the certificate could not be verified and that only that server's
owner can fix it. dl.sandstorm.io, where Sandstorm itself is downloaded
from, is valid.

Thanks to xet7 !
2026-10-29 23:49:36 +02:00
Lauri Ojansivu a1f2c006b4 Snap on Launchpad: fetch.sh is uploaded with the tools-env.sh it sources.
releases/prepare-launchpad-source.sh uploads only the files the snap recipe
reads, and listed releases/fetch.sh without releases/tools-env.sh, which
fetch.sh sources for its TMPDIR. In v12.69's armhf build the caddy part's
fetch.sh had no TMPDIR, mktemp made nothing, and curl got an empty -o seven
times ("option -o: is badly used here"), with pauses of up to 480s, before
it was reported as a server outage.

- tools-env.sh is uploaded, and it makes .tools/tmp in a tree that has only
  snapcraft.yaml and releases/, as the Launchpad source does.
- fetch.sh stops at once when it has no temporary file, and does not retry
  curl's exit 2 (it could not start) or call it an outage.
- tests: every releases/ file a listed script sources or runs must be
  listed; tools-env.sh and fetch.sh work in such a tree with no .git and no
  TMPDIR; both fail on the old list. fetch.sh's two new refusals are tested.

s390x failed earlier, on Launchpad: even a new build answered "Could not
find repository at path" three times.

Thanks to xet7 !
2026-10-29 23:48:30 +02:00
Lauri Ojansivu dfdadaa8ca AppImages: FerretDB and mongo-tools are readable by every user again.
releases/fetch.sh downloaded into a mktemp file and moved it into place, so
every download kept mktemp's 0600, and the bundled ferretdb and mongo-tools
were 0711 after chmod +x. A user other than the owner gets "Permission
denied" from such a file, and since today's appimagetool checks for it, all
six v12.69 AppImages (i686, x86_64, aarch64, armhf and both musl ones)
failed: "has mode 0711, but it must be readable for all users".

A download now gets the mode its umask gives a new file, and the AppDir is
made a+rX before it is packed. tests/releaseDownloads.test.cjs fails on the
old fetch.sh with 711 instead of 755 and checks that a stricter umask is
still followed; tests/appImageRuntime.test.cjs pins both.

Thanks to xet7 !
2026-10-29 23:44:34 +02:00
Lauri Ojansivu 856a32968b Tests: board drag settings open Card's Draggable table in its section hamburger.
Since Board Settings / Card became the card's Card Settings (52de57c844),
the Card Draggable table is in the Card section's hamburger, not in the
popup, so board-drag-settings.e2e.js failed in every language waiting for a
table that is no longer there. It opens Card the way the cog menu does and
then that hamburger, as board-settings-columns.e2e.js already does.

Thanks to xet7 !
2026-10-29 23:42:36 +02:00
Lauri Ojansivu e4b83d2274 Tests: the keyboard card move waits for the reset sorts to reach the client.
F02 F06 reset three cards to sort 0 in the database and waited only for the
minicards to show in id order. gamma 0, alpha .125, beta .25 already is id
order one time in six, so the wait passed while the client still had the old
sorts, the move was computed from them, and the card landed last instead of
second (1 in 3 runs here). The test waits for the client's own copy of all
three cards to read sort 0; 8 repeats pass.

Thanks to xet7 !
2026-10-29 23:28:31 +02:00
Lauri Ojansivu d06b468629 Tests: ReplyBleed's mocha test counts the attempt in every ReplyBleed row.
ReplyBleed is one Problems row per source, and the webhook's source is the
client address, so the bounce and board-address tests leave rows of their
own in the test database, which persists between runs. The test's findOne
picked one of those (source "ses"), whose count the spoofed reply never
moves, and failed although the attempt was recorded under 127.0.0.1. It sums
the count over all ReplyBleed rows, the way emailBounces.tests.js already
does.

Thanks to xet7 !
2026-10-29 23:07:04 +02:00
Lauri Ojansivu 379058bc9d CHANGELOG: OpenSSL 4 backup errors, Bun assets on Python 3.9, snap build logs, macOS test fixes.
Thanks to xet7 !
2026-10-29 22:52:42 +02:00
Lauri Ojansivu 5534c8830c Tests: the macOS suites run inside the sandboxed editor too.
mac-smoke-app.py checked that a killed descendant was gone with ps, which a
sandboxed macOS app may not run; it waits for os.kill(pid, 0) to find the
process gone and uses ps only to tell a zombie from a live one. macosSandbox
skips its live sandbox-exec check only when even an (allow default) profile
cannot be applied, which is the case inside the sandbox it is testing.

Thanks to xet7 !
2026-10-29 22:51:29 +02:00
Lauri Ojansivu 48a8937ff0 Snap on Launchpad: this attempt's build log is found by name, not only by mtime.
Whether a Launchpad build log belonged to the current attempt was decided by
[ "$log" -nt "$attempt_marker" ] alone. bash 3.2 compares whole seconds, so a
log written in the marker's second was not "newer": it was neither printed
nor checked for an infrastructure failure. this_attempts_log() counts a log
whose name did not exist when the attempt started, or one rewritten after the
marker; an earlier attempt's untouched log still does not count. Used by
infra_failure() and by the log printing, and tested for all three cases.

Thanks to xet7 !
2026-10-29 22:51:29 +02:00
Lauri Ojansivu 2dcccf0d4a Bun release assets: runs on Python 3.9, which macOS ships as python3.
hashlib.file_digest is Python 3.11+ and Path.write_text(newline=) is 3.10+,
so releases/bun/assets.py failed with AttributeError/TypeError on macOS's
python3 3.9. It hashes in chunks and writes the LF-only .sha256sum through
open(newline='\n'), and the test still pins LF on every platform.

Thanks to xet7 !
2026-10-29 22:51:29 +02:00
Lauri Ojansivu bf266fb4da DB conformance: a live run is still recognised where ps is not allowed.
In a sandboxed macOS app ps fails with "operation not permitted", so every
live conformance run looked dead and a second run would remove its
containers. command_line_of() falls back to pgrep -lf, which still works
there. The test pins the fallback.

Thanks to xet7 !
2026-10-29 22:51:29 +02:00
Lauri Ojansivu ffe49dd2fd Continuous backup: a changed encrypted file is reported as changed, also with OpenSSL 4.
A Node.js linked to OpenSSL 4 reports an AES-GCM tag mismatch as
ERR_OSSL_BAD_DECRYPT ("bad decrypt") instead of OpenSSL 3's "Unsupported
state or unable to authenticate data", so decryptFile() let the raw OpenSSL
error through instead of "An encrypted backup file has changed or the key is
wrong". Both are recognised now; tests/continuousBackupEncryption.test.cjs
passes with OpenSSL 4.1.0 and 3.5.9.

Thanks to xet7 !
2026-10-29 22:51:29 +02:00
github-actions[bot] ee16bdce6b Bump versions for v12.69 2026-10-29 20:03:10 +00:00
Lauri Ojansivu 44bae91f8e Prepare v12.69 release 2026-10-29 22:02:00 +02:00
Lauri Ojansivu ae13eab23e SECURITY.md, CLAUDE.md, AGENTS.md: a report without a GHSA goes from 3 to 5 stars when its GHSA arrives.
A researcher who sent a report to security@wekan.fi, or some other way, and
is later able to add it as a GitHub security advisory: the maintainer
requests the CVE through that GHSA, and the Hall of Fame row's 3 stars
become 5. SECURITY.md says so to researchers. CLAUDE.md and AGENTS.md tell
an AI updating or auditing the Hall of Fame to check each such 3-star row
against the advisories, match by the report's subject and reporter, and
upgrade the index row, its own page, the CVE cell, the Process cell and the
star audit together.

Thanks to xet7 !
2026-10-29 21:52:15 +02:00
Lauri Ojansivu f5f6ad4d4d CHANGELOG: ImportPlacementBleed, and Release all missing builds after a failed rerun.
Thanks to Zachariah Mithani (AploSec) and xet7 !
2026-10-29 20:27:59 +02:00
Lauri Ojansivu 0d6d481fcb ImportPlacementBleed: an import into a board is placed only beside that board's own list, swimlane or card.
The scoped importer, reached by POST /api/import/zip and the importScoped
method, authorized the import on boardId alone. The listId and swimlaneId
beside it were written onto the new cards as given, and a cardId was looked
up with no board filter, so a writer of board A could create cards on A
whose list and swimlane were board B's, and could probe whether a card id
existed on a board they cannot see.

Before anything is written, the list, swimlane and card are now looked up
together with the board's id; an id of another board is refused (400 over
REST, error-import-placement from the method), with the same answer for a
foreign card and a missing one. Cards are placed only by what was found on
the board. A refusal is recorded in Admin Panel / Problems as
ImportPlacementBleed (authz.import-placement), without blocking the account.

Tests: the decision with the reporter's ids, the check before every write,
a negative scan for a target id written or looked up without the board, and
a browser test of the reporter's attack by both routes, which fails on the
previous code.

Thanks to Zachariah Mithani (AploSec) and xet7 !
2026-10-29 20:27:14 +02:00
Lauri Ojansivu 99d9958e39 Release all missing: the missing assets are built even when the rerun of failed jobs fails again.
The planner needed recover, so one job failing again in the rerun of the
full release skipped the planner and every build after it, and assets that
could be filled were not. For v12.68 the rerun failed on four Launchpad
snaps (503 / timeout) and on the OpenBSD bundle, whose start-wekan.sh fault
is in the tagged code and so in every rerun of it. The planner now runs
unless the run is cancelled, and each job after it checks the planner's own
result, since an implicit success() also counts recover. recover stays red
and now names the jobs that failed again.

Thanks to xet7 !
2026-10-29 20:15:32 +02:00
Lauri Ojansivu daae36b313 CHANGELOG: Invite via Email adds the invited new user, not the first user of the database.
Thanks to momosari and xet7 !
2026-10-29 17:36:01 +02:00
Lauri Ojansivu 8556459f97 Invite via Email adds the invited new user, not the first user of the database.
Meteor 3's server Accounts.createUser is createUserAsync. inviteUserToBoard
called it without await, so the new user's id was a Promise; findOneAsync
read the Promise as the empty selector {} and returned the FIRST user,
usually the site administrator. That user was added to the board and got
the invitee's language and invitedBoards, while the invitee was created but
not added. It was not FerretDB: any database did the same.

createUser and sendEnrollmentEmail are awaited (an enrollment email that
fails is logged, as the invitation email is), and the user found must be
the one just created. Every server ReactiveCache getter now refuses a
Promise as its id or selector, so a missing await fails loudly instead of
picking the first document. Tests: the guard and the reporter's case in
memory, a scan that no server file calls an async Accounts API without
await, and a browser test that inviting a new address adds exactly that new
account - it fails on the previous code.

Thanks to momosari and xet7 !

Fixes #6831.
2026-10-29 17:35:21 +02:00
Lauri Ojansivu 7f9e8ea7a3 CHANGELOG: History on FerretDB, date paths as strings, migration reports refused documents.
Thanks to vijlautens and xet7 !
2026-10-29 16:58:42 +02:00
Lauri Ojansivu 01968fc637 History on FerretDB: date paths are stored as strings, and the migration reports refused documents.
A History row of a date-valued custom field held datePaths as an array of
path arrays, [["3","value"]]. FerretDB v1 does not store an array inside an
array, so on FerretDB the change was not recorded, and migrateTextDatabase
to FerretDB dropped those rows while it swallowed the per-document error and
still reported success.

Each path is now a JSON string, '["3","value"]'. Rows already written in
the array form on MongoDB still restore, and the migration to FerretDB
rewrites them to strings. The migration counts every document the target
refuses and ends as failed with the count per collection and the first
error, instead of success: true.

Thanks to vijlautens and xet7 !

Fixes #6832.
2026-10-29 16:58:21 +02:00
Lauri Ojansivu 04c2598cf4 Merge remote-tracking branch 'origin/main' 2026-10-29 10:55:23 +02:00
Lauri Ojansivu 77ded27994 CHANGELOG: OpenBSD bundle starts again.
Thanks to xet7 !
2026-10-29 08:25:49 +02:00
Lauri Ojansivu ec6cf66bff OpenBSD bundle starts again: start-wekan.sh takes only a number from sysctl.
OpenBSD's sysctl prints nothing and exits 0 for a name it does not have, so
`sysctl -n hw.memsize || sysctl -n hw.physmem` stopped at the empty
hw.memsize and $(( / 1048576 )) ended start-wekan.sh before WeKan answered.
Both start-wekan.sh copies now try hw.memsize, hw.physmem64 (NetBSD, whose
hw.physmem is 32-bit) and hw.physmem in turn and take the first numeric one.
The test runs the lines against fake OpenBSD, NetBSD and macOS sysctls, and
shows the old chain failing on the OpenBSD one.

Thanks to xet7 !
2026-10-29 08:25:38 +02:00
github-actions[bot] 8561f49ed9 Bump versions for v12.68 2026-10-29 05:16:53 +00:00
Lauri Ojansivu 749e9b76a7 Prepare v12.68 release 2026-10-29 07:15:43 +02:00
Lauri Ojansivu ad43de3c01 CHANGELOG: Card Settings, two panes, the card and the minicard follow its order, menus inline.
Thanks to xet7 !
2026-10-29 02:03:11 +02:00
Lauri Ojansivu 2bd5d1180a Card Settings: two panes, the card and the minicard follow their order, menus inline.
The opened card and the minicard now draw Card Settings' sections, in
Card Settings' order, each section's fields in the order of its rows:
- Section order is per side: cardSettingsSectionOrder for the card,
  the new minicardSettingsSectionOrder for the minicard. A board that
  stores none: the card starts with Labels, Date, Members, Card, ...
  (the card's historical order as far as the grouping allows), the
  minicard with what it drew first before.
- CARD_LAYOUT's and MINICARD_LAYOUT's reorderable sections are Card
  Settings' sections, so dragging a section or a row in Card Settings
  moves it on the card or the minicard. Stored field orders map: a
  field keeps its place relative to the others of its new section.
- On the card every section has the shared heading (its icon, title,
  caret and hamburger) - Voting and Planning Poker too, which had a
  heading of their own. The minicard stays compact: no headings.

Card Settings shows both sides at once: the minicard's pane first (on
the left), the card's second, each with a caret that folds it, each
scrolling on its own, stacked when the popup is too narrow for two.
The Show radio is gone. A splitter between the panes changes their
widths by dragging, like spreadsheet columns (clamped, kept per user
as a fraction, reset by a double-click, moved by the arrow keys). The
popup is wider again for two panes and its corner grip resizes it,
the panes following.

A hamburger's content opens under its line inside the pane, not in a
popup: the hamburger shows it is open (aria-expanded, highlighted, the
panel attached to it); its button, the panel's X or Escape closes it.
A custom field's edit form and the + create form open inline the same
way and close only their panel when saved. The popup layer added in
9c9742e09d is removed, as nothing uses it now.

Docs: Card-Settings.md, Card-Fields.md and Board Settings / Card.md.
New tests cardSectionsOnCard, minicardSections, cardSettingsSplit,
cardSettingsInlinePanels and card-settings-sections-order.e2e.js; the
guards of the old layouts and orders say why they changed.

Thanks to xet7 !
2026-10-29 02:02:54 +02:00
Lauri Ojansivu eddfd515ea CHANGELOG: Card Settings, hamburger menus open on top of it, next to their button.
Thanks to xet7 !
2026-10-28 23:23:18 +02:00
Lauri Ojansivu 9c9742e09d Card Settings: hamburger menus open on top of it, next to their button.
A hamburger of Card Settings opened its menu in Card Settings' place:
the popup slid to the menu and Card Settings disappeared until its back
arrow. Its menus, and the forms it opens (a custom field's edit form,
the + under Custom Fields, Subtask settings), now open as a popup
LAYER: on top of Card Settings, next to the button that opened them,
with Card Settings visible under it and scrolled where it was.

Popup.open(name, { layer: true }), from inside a popup, pushes the
layer on the stack as before, so Popup.back() - the layer's X, Escape,
a form's Save - closes the layer alone. The popup template draws the
popup under it with the layer on top, outside the scrolling content;
depth counts the sliding popups only, so a confirmation opened from a
layer still slides. One layer at a time: another replaces it, its own
button closes it, a press elsewhere in the popup closes it.

A layer opens toward the middle of the popup: a hamburger at the end of
a line gets its right edge, the + on the left its left edge, so the
create form no longer ran out of the popup's left side where its Save
could not be clicked; below the button, or above when there is more
room, its height and width capped to the room
(models/lib/popupLayerPosition.js). A menu's checkboxes start at its
edge rather than .check-div's form indent.

tests/popupLayer.test.cjs runs the placement and pins the Popup wiring
and that every Card Settings menu opens as a layer;
card-add-fields.e2e.js checks Card Settings stays visible under the
menu, the menu sits by its button, toggles, closes on a press elsewhere
and by its X alone, and that the + form opens inside Card Settings.

Thanks to xet7 !
2026-10-28 23:23:08 +02:00
Lauri Ojansivu 0250e54a07 CHANGELOG: Card Settings, a checkbox per section, every field always shown, only in Board Settings.
Thanks to xet7 !
2026-10-28 22:44:02 +02:00
Lauri Ojansivu 3e5bc58105 Card Settings: a checkbox per section, always every field, only in Board Settings.
Card Settings is now only Board Settings / Card. The opened card has no
Card Settings button any more: it simply shows every field its board
shows on cards, empty or not.

- On this card, Show empty and Show empty fields are removed, with the
  hamburgers' Select all / Select none rows, their handlers, helpers,
  board methods (setCardFieldAlwaysShown, setShowEmptyCardFields), CSS
  and the three translation keys. The card no longer reads its own
  shownFields / hiddenFields, nor the board's cardFieldsAlwaysShown /
  showEmptyCardFields; the schema keeps them so old documents validate.
- Every section has a checkbox before its title: whether the whole
  section is shown on the chosen side, whatever its rows are ticked.
  Stored per side in cardSettingsHiddenOnCard / cardSettingsHiddenOnMinicard
  (setCardSettingsSectionShown, board admins). The opened card's view
  of the board and a new minicard view read false for every row of a
  hidden section, Scrum fields included; Card Settings itself reads the
  raw board, so the rows keep their ticks.
- A section with one row on the chosen side is one line: one checkbox,
  the name, one hamburger.
- A hamburger is drawn only when it holds something.
- Custom Fields: its items are the board's custom fields, in their sort
  order, reordered by drag and drop within the section, with a + under
  them that creates one. Show on Minicard sets a field's showOnCard,
  Show on Card its alwaysOnCard; its hamburger edits the field. The
  section's checkbox is the board's Custom Fields flag of that side.
- Card actions (Filter: Subtasks, Edit voting, Card kind, ...) live in
  the card's own section hamburgers; Card Settings keeps only board
  actions.

Docs: docs/Features/Cards/Card-Settings.md replaces
Card-Settings-On-Card.md; Card-Fields.md and Board Settings / Card.md
rewritten. New tests cardSettingsSectionShown and
cardSettingsCustomFields; the guards of the removed behaviour say why
they changed; card-add-fields.e2e.js covers section hiding per side, an
empty card showing every field, empty hamburgers, the one-row line, the
Custom Fields items and +, and that the opened card has no Card
Settings.

Thanks to xet7 !
2026-10-28 22:43:29 +02:00
Lauri Ojansivu 039935dd0b CHANGELOG: Card Settings, a space between a checkbox and its label.
Thanks to xet7 !
2026-10-28 20:46:50 +02:00
Lauri Ojansivu 5adfc1b9db Card Settings: a space between a checkbox and its label.
On the rows of Card Settings - on the opened card and in Board Settings /
Card - and in their hamburgers' popups, a checkbox touched its label:
the rows' grid had lost its column gap with the old pane's CSS, and a
hamburger's option put the box and the text side by side with none.
Both now have a space's width (0.3em) between them.

A ticked box still touched the label, because a tick is drawn rotated
and reaches about 3px past its box. Left to right it is drawn 3px
further left, so a ticked box keeps the same space and the label does
not move when it is ticked; right to left the tick leans away from the
label and stays as it was.

tests/cardSettingsOnCard.test.cjs pins the gaps and the tick rule, and
card-add-fields.e2e.js measures at least 3px for ticked and unticked
boxes on the rows and in Card's hamburger, with every label in the same
place.

Thanks to xet7 !
2026-10-28 20:46:42 +02:00
Lauri Ojansivu f269b1ef16 CHANGELOG: Board Settings / Card shows the same checklists as the opened card, by the same templates.
Thanks to xet7 !
2026-10-28 20:23:42 +02:00
Lauri Ojansivu 52de57c844 Board Settings / Card is the opened card's Card Settings, same templates.
Board Settings / Card had its own layout: an Admin Panel left menu of
sections and a pane with two lists, column headings and per-column Select
all. It now renders the same template as the opened card's Card Settings
(cardSettingsInline), with no card: Show on Minicard / Show on Card, the
sections as checklists, their rows as the items, and the same section and
row hamburgers. The popup's header is the title and the close button, so
the inline's own title and X are not drawn there, and a row's hamburger
has no On this card. The popup is 600px wide, about an opened card,
instead of 1200px.

The popup no longer registers the Card Settings handlers itself: the
template inside it does, and registering them on both would handle every
click twice. The left menu, the pane, the column Select all, the
in-place action forms and the personal-only mode are removed with their
helpers and CSS. A non-admin sees what a card shows them: the board's
rows disabled, their own rows (Labels text, List title) usable.

Dragging, on the card and in Board Settings / Card:
- With drag handles on, a row or a section is dragged only by its
  handle; the rest of the line scrolls the page. With them off, by its
  icon and name.
- A row dropped on another section goes back where it was and nothing
  is saved: the sections are unrelated kinds of fields. jQuery UI fires
  a delayed "out" just before "stop", so where it was dropped is read
  in beforeStop.

Found while testing both open at once:
- Each Card Settings has its own Show radio group name; one name made
  the card's and the popup's radios one group.
- A section hamburger's Select all acts on the Card Settings it was
  opened from.
- Rows are keyed, so switching Show moves each row with its side instead
  of relabelling rows in place a flush before they reorder, when a
  click could land on the neighbouring field.

Docs: Card-Settings-On-Card.md, Card-Fields.md and Board Settings /
Card.md. The node guards that pinned the old layout now pin the shared
template, with negatives that the old layout and the double handler
registration are gone; card-add-fields.e2e.js compares the two views
section by section and row by row on both sides, and tests the drag
rules, and the specs that drove the old pane drive the new one.

Thanks to xet7 !
2026-10-28 20:23:05 +02:00
Lauri Ojansivu 81e6988491 Southern Sotho: "dikarolo", not "masimo", for data fields.
Three Southern Sotho strings called a data field "masimo", which means
farm fields: Show empty fields in Card Settings, the five fields of a
cron schedule, and the webhook's optional fields. They now use
"dikarolo", the word the file already uses for card and custom fields.
sesotho-popup-titles.e2e.js, which checks that Card Settings names data
fields as such, now reads Card's "Show empty fields" in Board Settings /
Card and caught the first. The cron and webhook wording is low
confidence; a Southern Sotho speaker's correction on Transifex replaces
it.

Thanks to xet7 !
2026-10-28 20:22:56 +02:00
Lauri Ojansivu 8b4987d5bd CHANGELOG: the Data health entry no longer repeats its group's name.
Thanks to xet7 !
2026-10-28 18:50:35 +02:00
Lauri Ojansivu e12106ce7a CHANGELOG: Meteor 3.6-rc.1, with WeKan's six Meteor package forks rebased onto it.
Thanks to xet7 !
2026-10-28 18:50:00 +02:00
Lauri Ojansivu 120f484a30 Meteor 3.6-rc.1, with WeKan's six Meteor package forks rebased onto it.
meteor update --release 3.6-rc.1 failed with "No version of
shell-server satisfies all constraints" (and the same for
dynamic-import, allow-deny, ddp-server, facebook-oauth and
google-oauth): those six are WeKan's forks in packages/, made because
Meteor 3.6-rc.0 lacked the 3.5.3 security fixes, and pinned to rc.0's
versions.

Meteor 3.6-rc.1 carries the security fixes of 3.5.3 and 3.5.4, but not
what is WeKan's own in the forks: the Admin Panel / Problems reports of
refused attempts, and the crypto-RNG keys of dynamic-import and meteor
shell (rc.1 still makes them with Math.random()). So the forks are kept
and rebased: each is rc.1's package with WeKan's changes over rc.0's
package merged three-way onto it. The 3.5.3 hunks merged away, as rc.1
has them; what remains over rc.1 is only the hooks and the keys. The
versions are rc.1's with a _1 wrap number.

@meteorjs/rspack moves to 3.0.0-rc.2 with the release. The Dockerfile's
METEOR_RELEASE and the snap description follow. The experimental Bun
build's Meteor patch applies unchanged to 3.6-rc.1; prepare-meteor.sh
now checks for rc.1's tag commit and applies meteor-3.6-rc.1.patch, and
the rc.0 copy is removed.

tests/meteor353SecurityFixes.test.cjs and meteorForkRandomKeys.test.cjs
are moved to rc.1 with their assertions kept;
meteor-353-security.e2e.js passes on rc.1 in Chromium and Firefox.

Thanks to xet7 !
2026-10-28 18:49:59 +02:00
Lauri Ojansivu 08ff3327c4 Card fields guard: the Scrum section filter asks the board first.
tests/cardShownFields.test.cjs still pinned the Scrum filter of the
opened card's sections as it was before #6830, which now requires the
board's Scrum visibility before asking the card.

Thanks to jullbo and xet7 !
2026-10-28 18:49:18 +02:00
Lauri Ojansivu d74dbbe409 CHANGELOG: repoint commit links after history moved.
A rebase rewrote the commits these entries link to, so the links named commits
that no longer exist and would have 404ed on GitHub. Repointed by
releases/fix-changelog-hashes.sh, which only touches a link no ref in this clone
can reach.

Thanks to xet7 !
2026-10-28 18:43:49 +02:00
Lauri Ojansivu 2fd0c77164 Merge remote-tracking branch 'origin/main' 2026-10-28 18:43:48 +02:00
Lauri Ojansivu 6e18251e5f CHANGELOG: Show empty fields shows no Scrum section the board has turned off (#6830).
Thanks to jullbo and xet7 !
2026-10-28 18:28:57 +02:00
Lauri Ojansivu 0a224b3cea Show empty fields shows no Scrum section the board has turned off.
With Board Settings / Card's "Show empty fields" on, every opened card
showed the "Scrum settings" heading and its sections although Board
Settings / Scrum had none of its fields turned on. The Scrum fields are
the card's only sections with no board flag in the template, and
orderedSectionsOf asked only whether the card shows the field, which
Show empty fields answers yes for every field. A Scrum section now
needs the board's Scrum visibility (boardAllows) first, as Members,
Vote and Poker already do.

Thanks to jullbo and xet7 !

Fixes #6830.
2026-10-28 18:28:31 +02:00
Lauri Ojansivu a834211ab6 CHANGELOG: Card Settings on the opened card, its sections as checklists.
Thanks to xet7 !
2026-10-28 14:39:48 +02:00
Lauri Ojansivu c0ec28e82f Card Settings on the opened card, its sections as checklists.
The opened card's "+ Add" button is now "Card Settings". Clicking it no
longer opens the Board Settings / Card popup: the button and everything
below it on the card are replaced by Card Settings itself, until the X at
the end of its title puts the card back. Top to bottom:

- Card Settings, with its X (close) at the right of the title.
- "Show" and two radio buttons, Show on Minicard and Show on Card, which
  choose what the checkboxes below are.
- The sections (Card, Date, Time, ... Comments) drawn like checklists: the
  section is the checklist's title, with a count of the ticked rows and a
  hamburger; its rows are the checklist items. An item's checkbox is the
  board-wide Show on Card or Show on Minicard of that field.
- A row's hamburger holds On this card and Show empty; a section's holds
  Select all / Select none of each column, the section's card actions and,
  for Card, Show empty fields, Ask for a reason and the Draggable table.

Sections and rows reorder by drag and drop like checklists and their items,
and by the Up and Down keys. A section's order is the board's new
cardSettingsSectionOrder (normalised by sectionOrder() in
models/lib/cardSettingsSections.js); a row's is the card's or minicard's
field order, as in the popup. Sections collapse like checklists, kept per
user and board. The rows, checkboxes and handlers are the popup's, shared
rather than copied, and Board Settings / Card still opens the popup.

Design: docs/Features/Cards/Card-Settings-On-Card.md.

New strings: "Show" (card-settings-show) and the two hamburgers' titles,
"Actions". Every language takes its existing translation of the same
words; "Show" was written directly, with low confidence, for an, ary, bi,
csb, cy, cy-GB, fur, hsb, hy, jv, ka, kk, km, km-KH, km_KH, kok, lb, lld,
mai, mn, oc, or_IN, pa, sc, sn, so, szl, te-IN, tg, tk_TM, tl, tpi and tt.

Tests: tests/cardSettingsOnCard.test.cjs (the order rules, the layout, the
shared handlers, the translations) and
tests/playwright/specs/card-add-fields.e2e.js rewritten for the card's Card
Settings, the popup's own tests now opening it from Board Settings. The
guards that pinned the card template's indentation and the old opener say
why they changed.

Thanks to xet7 !
2026-10-28 14:39:29 +02:00
Lauri Ojansivu 6dcb9fa6e2 CHANGELOG: Data health cleans up custom fields whose boards were all deleted (#6829).
Thanks to Raymond-Voogt and xet7 !
2026-10-28 13:51:17 +02:00
Lauri Ojansivu 9dc3fc3f31 Data health: custom fields whose boards were all deleted get their own check.
customfield-dead-board counted every custom field that names a deleted
board, but its Repair only removes dead boards from a field that is also on
a live board; a field whose boards were ALL deleted was skipped on purpose,
since removing them would leave it on no board. So those fields stayed in
the count with a Repair button that could never change it: one instance
showed 510 and "Repaired: 0" after every click.

- New check customfield-boards-deleted: fields whose boards no longer exist
  (through boardIds or the legacy boardId). No existing board shows them.
  Repair deletes the definitions after the confirming second click, like
  the other repairs that delete data. A value a card on a live board still
  holds is left alone and stays listed under card-dead-field-values.
- customfield-dead-board counts only fields that still have a live board,
  so its count goes to zero when its Repair has run.

docs/Features/Admin-Panel/Problems/Data-Health.md lists the new check.
Test: tests/dataHealth6818.test.cjs.

Thanks to Raymond-Voogt and xet7 !

Fixes #6829.
2026-10-28 13:50:49 +02:00
986 changed files with 47844 additions and 11805 deletions

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": [
"./tsconfig.lint.json"
]
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error"
}
}
+4
View File
@@ -405,6 +405,10 @@ jobs:
APPRUN
sed -i "s/@APPIMAGE_ARCH@/$arch/" AppDir/AppRun
chmod +x AppDir/AppRun
# The AppImage runs as whoever starts it, not as this runner, so
# everything in it must be readable by all. v12.69's FerretDB and
# mongo-tools were 0711 and appimagetool refused to pack them.
chmod -R a+rX AppDir
- name: Pack the AppImage
if: ${{ env.BUILD_THIS == 'true' }}
+1 -1
View File
@@ -103,7 +103,7 @@ jobs:
app/.tools/meteor-bun/dev_bundle
app/.tools/meteor-bun/.babel-cache
app/.tools/meteor-bun/packages/*/.npm
key: bun-meteor-${{ runner.os }}-${{ hashFiles('app/.meteor/release', 'app/.meteor/versions', 'build-tools/releases/bun/meteor-3.6-rc.0.patch') }}
key: bun-meteor-${{ runner.os }}-${{ hashFiles('app/.meteor/release', 'app/.meteor/versions', 'build-tools/releases/bun/meteor-3.6-rc.1.patch') }}
- name: Build the release's ESM application once
working-directory: app
env:
+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@dbcb813823bdd20940b903addbd779551569679f
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
+13 -7
View File
@@ -97,6 +97,12 @@ jobs:
plan:
needs: recover
# The planner runs whatever recover's rerun ended with. A rerun that still
# fails - Launchpad answering 503, a fault that is in the tagged code and
# so in every rerun of it (v12.68's OpenBSD start) - used to skip the
# planner and every build after it, so not even the assets that COULD be
# filled were. recover stays red and names the jobs; the rest goes on.
if: ${{ !cancelled() }}
timeout-minutes: 15
name: what the release is missing
runs-on: ubuntu-latest
@@ -211,7 +217,7 @@ jobs:
contents: write
name: ${{ matrix.arch }}
needs: plan
if: ${{ needs.plan.outputs.extra != '[]' }}
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.extra != '[]' }}
runs-on: ubuntu-24.04
timeout-minutes: 300
strategy:
@@ -331,7 +337,7 @@ jobs:
bun:
name: Missing Bun runtime bundles
needs: plan
if: ${{ needs.plan.outputs.bun != '[]' }}
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.bun != '[]' }}
uses: ./.github/workflows/bun.yml
permissions:
contents: write
@@ -345,7 +351,7 @@ jobs:
appimage:
name: AppImages
needs: plan
if: ${{ needs.plan.outputs.appimage != '[]' }}
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.appimage != '[]' }}
uses: ./.github/workflows/AppImage.yml
permissions:
contents: write
@@ -357,7 +363,7 @@ jobs:
mac:
name: Mac apps
needs: plan
if: ${{ needs.plan.outputs.mac != '[]' }}
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.mac != '[]' }}
uses: ./.github/workflows/mac.yml
permissions:
contents: write
@@ -369,7 +375,7 @@ jobs:
flatpak:
name: Flatpaks
needs: plan
if: ${{ needs.plan.outputs.flatpak != '[]' }}
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.flatpak != '[]' }}
uses: ./.github/workflows/Flatpak.yml
permissions:
contents: write
@@ -381,7 +387,7 @@ jobs:
windows-exe:
name: Windows single EXE
needs: plan
if: ${{ needs.plan.outputs.windows != '[]' }}
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.windows != '[]' }}
uses: ./.github/workflows/windows.yml
permissions:
contents: write
@@ -425,7 +431,7 @@ jobs:
timeout-minutes: 30
name: Helm chart
needs: plan
if: ${{ !cancelled() && needs.plan.outputs.version != '' }}
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.version != '' }}
runs-on: ubuntu-latest
steps:
- name: Checkout wekan (for releases/release-charts.sh)
+23 -13
View File
@@ -38,7 +38,7 @@ run-name: Release v${{ inputs.new_version }}
# only the ferretdb-<arch> binary it needs from the newest wekan/FerretDB
# release (FerretDB is built + released separately by its own build.sh —
# not compiled here) and embeds it
# - release: writes the newest CHANGELOG.md notes to the GitHub Release and
# - release: writes the newest CHANGELOG-big.md notes to the GitHub Release and
# checks the core wekan-<version>-{amd64,arm64}.zip bundles are on
# it. It uploads nothing: EVERY job that builds a release file
# attaches it itself, as its last step, the moment it is built and
@@ -281,8 +281,8 @@ jobs:
# No `changelog:` output. The notes are 170 KB and more, which is past the
# 128 KiB MAX_ARG_STRLEN a single environment variable may hold, so any job
# consuming them through `env:` fails to start its shell. Jobs read
# CHANGELOG.md from their own checkout instead - see
# "Check the release notes exist in CHANGELOG.md" below.
# CHANGELOG-big.md from their own checkout instead - see
# "Check the release notes exist in CHANGELOG-big.md" below.
steps:
# Shallow: the only thing history was fetched for was finding out whether
# the release tag already exists, which the tag step now asks GitHub.
@@ -310,8 +310,8 @@ jobs:
# string at MAX_ARG_STRLEN - 128 KiB - so execve refused to start bash at
# all ("Argument list too long") before a line of the step ran. Each job
# that needs the notes now extracts them from its own checkout of
# CHANGELOG.md with the same script, straight into a file.
- name: Check the release notes exist in CHANGELOG.md
# CHANGELOG-big.md with the same script, straight into a file.
- name: Check the release notes exist in CHANGELOG-big.md
env:
STEP_VERSION_VERSION: ${{ steps.version.outputs.version }}
run: |
@@ -1170,7 +1170,7 @@ jobs:
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
# ref: main, the same ref `prepare` read CHANGELOG.md from, so the notes
# ref: main, the same ref `prepare` read CHANGELOG-big.md from, so the notes
# composed below are the ones prepare validated. (bump pushes to main and
# prepare tags it; nothing else pushes during a release.)
#
@@ -1203,7 +1203,7 @@ jobs:
# published nothing, "Incorrect: command not found"); and `env:` put a
# 172 KB string in envp, past the 128 KiB MAX_ARG_STRLEN, so execve
# refused to start bash at all (v10.92, "Argument list too long"). Read
# from CHANGELOG.md, appended with cat, the text is data all the way.
# from CHANGELOG-big.md, appended with cat, the text is data all the way.
run: |
set -euo pipefail
bash releases/release-notes.sh "$VERSION" > release-notes.md
@@ -3672,7 +3672,7 @@ jobs:
# Ubuntu 24.04's AppArmor defaults kernel.apparmor_restrict_unprivileged_userns=1,
# which blocks the unprivileged user namespaces that the Sandstorm install and
# the spk supervisor rely on (the "fragile part" noted above). Relax it so
# `install.sandstorm.io` and meteor-spk can create sandboxes on the runner.
# `install.sandstorm.org` and meteor-spk can create sandboxes on the runner.
# `|| true`: on kernels/runners without these knobs, setting them is a no-op.
run: |
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
@@ -3696,11 +3696,11 @@ jobs:
run: |
# Downloaded, then run: a `curl | sh` cannot be retried, because by the
# time the download fails sh is already half way through the script.
bash "$GITHUB_WORKSPACE/releases/fetch.sh" -o /tmp/install-sandstorm.sh https://install.sandstorm.io
bash "$GITHUB_WORKSPACE/releases/fetch.sh" -o /tmp/install-sandstorm.sh https://install.sandstorm.org
sudo bash /tmp/install-sandstorm.sh -d
- name: Assemble modern meteor-spk.deps (Node 26 + FerretDB + Mongo 3.x CLIs)
# Base = upstream meteor-spk 0.6.0 from dl.sandstorm.io (has mongod 3.0 +
# Base = upstream meteor-spk 0.6.0 from dl.sandstorm.org (has mongod 3.0 +
# niscud). build-deps.sh swaps in Node 26, adds FerretDB + the Mongo 3.x
# CLIs + the migration launcher/importer, keeps niscud, and swaps in the
# modern sandstorm-http-bridge from the Sandstorm install above. No dependency
@@ -5061,11 +5061,20 @@ jobs:
# infrastructure earns another one, up to this many, after a pause
# that grows each time.
max_attempts=6
# A Launchpad build log is THIS attempt's when its name did not exist
# before the attempt, or it was rewritten after the marker. The name
# alone decides it where -nt compares whole seconds (bash 3.2): a log
# written in the marker's second was not "newer", and was missed.
logs_before=" "
this_attempts_log() {
[ -e "$1" ] || return 1
case "$logs_before" in *" $1 "*) [ "$1" -nt "$attempt_marker" ] ;; *) return 0 ;; esac
}
infra_failure() { # did THIS attempt fail on Launchpad, not on WeKan?
grep -qsE 'HTTP Error 5[0-9][0-9]|Error: Timeout|Service Unavailable' "$remote_log" && return 0
local log
for log in snapcraft-wekan-*.txt; do
[ -e "$log" ] && [ "$log" -nt "$attempt_marker" ] || continue
this_attempts_log "$log" || continue
grep -qsE 'RPC failed;|GnuTLS recv error|fatal: early EOF|Gateway error \[IP: ' "$log" && return 0
done
return 1
@@ -5090,6 +5099,7 @@ jobs:
# earlier attempts' logs stay in the directory for the diagnostics
# below, but must not be printed again as if they were this one's.
touch "$attempt_marker"
logs_before=" $(ls snapcraft-wekan-*.txt 2>/dev/null | tr '\n' ' ')"
if [ "$fresh_next" = 1 ]; then
echo "The previous attempt's Launchpad build finished without a snap; submitting a new build instead of recovering it."
run_remote_build && rc=0 || rc=$?
@@ -5146,7 +5156,7 @@ jobs:
# and threw it away.
new_logs=0
for log in snapcraft-wekan-*.txt; do
[ -e "$log" ] && [ "$log" -nt "$attempt_marker" ] || continue
this_attempts_log "$log" || continue
new_logs=1
echo "::group::Launchpad build log: $log (what failed, then the last 150 lines)"
# The tail alone is not where the cause is. snapcraft's own error is
@@ -5684,7 +5694,7 @@ jobs:
# rewrites the notes of a release that is already published.
timeout-minutes: 15
steps:
# ref: main, so this job reads the same CHANGELOG.md the release job did.
# ref: main, so this job reads the same CHANGELOG-big.md the release job did.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
+1 -1
View File
@@ -32,6 +32,6 @@ jobs:
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Run chart-releaser
uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0
uses: helm/chart-releaser-action@df287104677278f869ca5480bd260358397cc3f9 # v1.8.0
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+4 -1
View File
@@ -140,4 +140,7 @@ jobs:
fi
echo "Publishing $SPK with $SPK_BIN ..."
# Same command as releases/release-sandstorm.sh: spk publish wekan-<VERSION>.spk
"$SPK_BIN" publish "$SPK"
# Publish to apps.sandstorm.org
"$SPK_BIN" publish --webkey=https://api-b72c46193b266fd6c33e1299dccc9bfa.oasis.sandstorm.org#iCDu7VYl4qZxVbDLXLIE2XSU0bEadaBgApEMqgCkLmP "$SPK"
# Publish to apps.sandstorm.io
"$SPK_BIN" publish --webkey=https://alpha-api.sandstorm.io/#Rs-0TT13YrNSbv7Fiz5K9bBkLaJn3E5TB0PU1GSn1HE "$SPK"
+27 -27
View File
@@ -6,12 +6,12 @@
meteor-base@1.5.2
# Build system
ecmascript@0.19.2-rc360.0
standard-minifier-js@3.3.0-rc360.0
rspack@1.4.0-rc360.0
ecmascript@0.19.2-rc360.1
standard-minifier-js@3.3.0-rc360.1
rspack@1.4.0-rc360.1
# Declared explicitly because server/modernBrowsers.js imports it, to say that
# Yandex Browser is modern and must not be served the legacy bundle (#6557).
modern-browsers@0.2.4-rc360.0
modern-browsers@0.2.4-rc360.1
blaze@3.0.2
@@ -19,11 +19,11 @@ blaze@3.0.2
aldeed:collection2
aldeed:schema-index@4.0.0-beta.5
reywood:publish-composite
mongo@2.5.2-rc360.0
mongo@2.5.2-rc360.1
# Account system
accounts-password@3.3.2-rc360.0
accounts-2fa@3.1.1-rc360.0
accounts-password@3.3.2-rc360.1
accounts-2fa@3.1.1-rc360.1
accounts-express@1.0.0
communitypackages:core
communitypackages:unstyled
@@ -39,19 +39,19 @@ wekan-oidc
wekan-accounts-oidc
# Utilities
check@1.5.1-rc360.0
random@1.2.3-rc360.0
reactive-dict@1.3.3-rc360.0
session@1.2.4-rc360.0
tracker@1.3.5-rc360.0
check@1.5.1-rc360.1
random@1.2.3-rc360.1
reactive-dict@1.3.3-rc360.1
session@1.2.4-rc360.1
tracker@1.3.5-rc360.1
audit-argument-checks@1.0.8
# UI components
ostrio:i18n
reactive-var@1.0.13
shell-server@0.7.1-rc360.0
email@3.2.1-rc360.0
dynamic-import@0.7.5-rc360.0
shell-server@0.7.2-rc360.1
email@3.2.1-rc360.1
dynamic-import@0.8.1-rc360.1
wekan-markdown
quave:synced-cron
ostrio:cookies
@@ -59,21 +59,21 @@ ostrio:files
# lmieulet:meteor-coverage
meteortesting:mocha@3.0.0
matb33:collection-hooks
service-configuration@1.3.6-rc360.0
service-configuration@1.3.6-rc360.1
minifier-css@2.0.2
ejson@1.2.1-rc360.0
logging@1.3.7-rc360.0
ejson@1.4.1-rc360.1
logging@1.3.7-rc360.1
wekan-fullcalendar
wekan-fontawesome
accounts-google@1.4.2-rc360.0
accounts-github@1.5.2-rc360.0
accounts-facebook@1.3.5-rc360.0
accounts-twitter@1.5.3-rc360.0
accounts-meteor-developer@1.5.2-rc360.0
accounts-weibo@1.4.2-rc360.0
accounts-meetup@1.5.2-rc360.0
accounts-passwordless@3.1.2-rc360.0
instrumentation@0.0.1-rc360.0
accounts-google@1.4.2-rc360.1
accounts-github@1.5.2-rc360.1
accounts-facebook@1.3.5-rc360.1
accounts-twitter@1.5.3-rc360.1
accounts-meteor-developer@1.5.2-rc360.1
accounts-weibo@1.4.2-rc360.1
accounts-meetup@1.5.2-rc360.1
accounts-passwordless@3.1.2-rc360.1
instrumentation@0.0.1-rc360.1
# Prevent Meteor build/run tools from sending application package statistics.
package-stats-opt-out@1.0.8
+1 -1
View File
@@ -1 +1 @@
METEOR@3.6-rc.0
METEOR@3.6-rc.1
+74 -74
View File
@@ -1,130 +1,130 @@
accounts-2fa@3.1.1-rc360.0
accounts-base@3.4.0-rc360.0
accounts-2fa@3.1.1-rc360.1
accounts-base@3.4.0-rc360.1
accounts-express@1.0.0
accounts-facebook@1.3.5-rc360.0
accounts-github@1.5.2-rc360.0
accounts-google@1.4.2-rc360.0
accounts-meetup@1.5.2-rc360.0
accounts-meteor-developer@1.5.2-rc360.0
accounts-oauth@1.4.8-rc360.0
accounts-password@3.3.2-rc360.0
accounts-passwordless@3.1.2-rc360.0
accounts-twitter@1.5.3-rc360.0
accounts-weibo@1.4.2-rc360.0
accounts-facebook@1.3.5-rc360.1
accounts-github@1.5.2-rc360.1
accounts-google@1.4.2-rc360.1
accounts-meetup@1.5.2-rc360.1
accounts-meteor-developer@1.5.2-rc360.1
accounts-oauth@1.4.8-rc360.1
accounts-password@3.3.2-rc360.1
accounts-passwordless@3.1.2-rc360.1
accounts-twitter@1.5.3-rc360.1
accounts-weibo@1.4.2-rc360.1
aldeed:collection2@4.2.2
aldeed:schema-index@4.0.0
aldeed:simple-schema@1.13.1
allow-deny@2.1.1-rc360.0_1
allow-deny@2.1.2-rc360.1_1
audit-argument-checks@1.0.8
autoupdate@2.0.2-rc360.0
babel-compiler@7.15.2-rc360.0
autoupdate@2.0.2-rc360.1
babel-compiler@7.15.2-rc360.1
babel-runtime@1.5.2
base64@1.0.15-rc360.0
binary-heap@1.0.14-rc360.0
base64@1.0.15-rc360.1
binary-heap@1.0.14-rc360.1
blaze@3.0.3
blaze-tools@2.0.1
boilerplate-generator@2.2.0-rc360.0
boilerplate-generator@2.2.0-rc360.1
caching-compiler@2.0.1
caching-html-compiler@2.0.1
callback-hook@1.8.1-rc360.0
check@1.5.1-rc360.0
callback-hook@1.8.1-rc360.1
check@1.5.1-rc360.1
communitypackages:core@2.0.3
communitypackages:flow-routing@2.1.0
communitypackages:unstyled@2.0.1
core-runtime@1.0.0
ddp@1.4.3-rc360.0
ddp-client@3.4.2-rc360.0
ddp-common@1.4.6-rc360.0
ddp-rate-limiter@1.3.1-rc360.0
ddp-server@3.4.1-rc360.0_1
diff-sequence@1.1.4-rc360.0
dynamic-import@0.7.5-rc360.0_1
ecmascript@0.19.2-rc360.0
ddp@1.4.3-rc360.1
ddp-client@3.4.2-rc360.1
ddp-common@1.4.6-rc360.1
ddp-rate-limiter@1.3.1-rc360.1
ddp-server@3.6.0-rc360.1_1
diff-sequence@1.2.1-rc360.1
dynamic-import@0.8.1-rc360.1_1
ecmascript@0.19.2-rc360.1
ecmascript-runtime@0.8.3
ecmascript-runtime-client@0.13.0
ecmascript-runtime-server@0.11.1
ejson@1.2.1-rc360.0
email@3.2.1-rc360.0
ejson@1.4.1-rc360.1
email@3.2.1-rc360.1
es5-shim@4.8.1
facebook-oauth@1.11.7-rc360.0_1
facts-base@1.0.3-rc360.0
fetch@0.2.1-rc360.0
geojson-utils@1.0.13-rc360.0
github-oauth@1.4.3-rc360.0
google-oauth@1.4.6-rc360.0_1
facebook-oauth@1.12.1-rc360.1_1
facts-base@1.0.3-rc360.1
fetch@0.2.1-rc360.1
geojson-utils@1.0.13-rc360.1
github-oauth@1.4.3-rc360.1
google-oauth@1.5.1-rc360.1_1
hot-code-push@1.0.5
html-tools@2.0.1
htmljs@2.0.2
id-map@1.2.1-rc360.0
instrumentation@0.0.1-rc360.0
inter-process-messaging@0.1.4-rc360.0
id-map@1.2.1-rc360.1
instrumentation@0.0.1-rc360.1
inter-process-messaging@0.1.4-rc360.1
jquery@3.0.2
lai:collection-extensions@1.0.0
localstorage@1.2.1
logging@1.3.7-rc360.0
logging@1.3.7-rc360.1
matb33:collection-hooks@2.1.0
meetup-oauth@1.1.4-rc360.0
meteor@2.3.2-rc360.0
meetup-oauth@1.1.4-rc360.1
meteor@2.3.2-rc360.1
meteor-base@1.5.2
meteor-developer-oauth@1.3.4-rc360.0
meteor-developer-oauth@1.3.4-rc360.1
meteortesting:browser-tests@1.8.0
meteortesting:mocha@3.4.0
meteortesting:mocha-core@8.2.0
minifier-css@2.0.2
minifier-js@3.3.2-rc360.0
minimongo@2.2.1-rc360.0
modern-browsers@0.2.4-rc360.0
minifier-js@3.3.2-rc360.1
minimongo@2.2.1-rc360.1
modern-browsers@0.2.4-rc360.1
modules@0.20.3
modules-runtime@0.13.2
mongo@2.5.2-rc360.0
mongo@2.5.2-rc360.1
mongo-decimal@0.2.0
mongo-dev-server@1.1.1
mongo-id@1.0.10-rc360.0
npm-mongo@6.16.3
oauth@3.0.5-rc360.0
oauth1@1.5.4-rc360.0
mongo-id@1.0.10-rc360.1
npm-mongo@6.18.1-rc360.1
oauth@3.0.5-rc360.1
oauth1@1.5.4-rc360.1
oauth2@1.3.3
observe-sequence@2.0.1
ordered-dict@1.2.1-rc360.0
ordered-dict@1.2.1-rc360.1
ostrio:cookies@2.9.2
ostrio:cstorage@4.0.1
ostrio:files@3.0.1
ostrio:flow-router-extra@3.15.3
ostrio:i18n@3.2.1
package-stats-opt-out@1.0.8
promise@1.0.1-rc360.0
promise@1.0.1-rc360.1
quave:synced-cron@2.4.0
raix:eventemitter@1.0.0
random@1.2.3-rc360.0
rate-limit@1.2.1-rc360.0
random@1.2.3-rc360.1
rate-limit@1.2.1-rc360.1
react-fast-refresh@0.3.0
reactive-dict@1.3.3-rc360.0
reactive-dict@1.3.3-rc360.1
reactive-var@1.0.13
reload@1.3.3-rc360.0
retry@1.1.2-rc360.0
reload@1.3.3-rc360.1
retry@1.1.2-rc360.1
reywood:publish-composite@1.9.0
routepolicy@1.1.3-rc360.0
rspack@1.4.0-rc360.0
service-configuration@1.3.6-rc360.0
session@1.2.4-rc360.0
sha@1.0.11-rc360.0
shell-server@0.7.1-rc360.0_1
socket-stream-client@0.7.2-rc360.0
routepolicy@1.1.3-rc360.1
rspack@1.4.0-rc360.1
service-configuration@1.3.6-rc360.1
session@1.2.4-rc360.1
sha@1.0.11-rc360.1
shell-server@0.7.2-rc360.1_1
socket-stream-client@0.7.2-rc360.1
spacebars@2.0.1
spacebars-compiler@2.0.1
standard-minifier-js@3.3.0-rc360.0
standard-minifier-js@3.3.0-rc360.1
templating@1.4.5
templating-compiler@2.0.1
templating-runtime@2.0.2
templating-tools@2.0.1
tools-core@1.4.0-rc360.0
tracker@1.3.5-rc360.0
twitter-oauth@1.3.5-rc360.0
typescript@5.11.2-rc360.0
url@1.3.7-rc360.0
webapp@2.3.1-rc360.0
webapp-hashing@1.1.3-rc360.0
weibo-oauth@1.3.4-rc360.0
tools-core@1.4.0-rc360.1
tracker@1.3.5-rc360.1
twitter-oauth@1.3.5-rc360.1
typescript@5.11.2-rc360.1
url@1.3.7-rc360.1
webapp@2.3.1-rc360.1
webapp-hashing@1.1.3-rc360.1
weibo-oauth@1.3.4-rc360.1
wekan-accounts-cas@0.2.0
wekan-accounts-lockout@1.1.0
wekan-accounts-oidc@1.0.10
+73 -12
View File
@@ -35,7 +35,7 @@ contributor — that offer is the mistake, not the rule.
The maintainer's half is the release flow, not taste. Releases here are frequent —
several a day when a fault is being chased — and `releases/release-all.sh` cuts one
from whatever is on `main`, taking the version from the `# Upcoming WeKan ® release`
section of `CHANGELOG.md`. Work parked on a branch is work that is not in the next
section of `CHANGELOG-big.md`. Work parked on a branch is work that is not in the next
release, and `tests/changelogEntriesBelongToTheirRelease.test.cjs` fails when a
released section links a commit that release does not contain.
@@ -340,7 +340,7 @@ directly after the merge.
the two patch repositories under `.tools/` — node-patches and mongo-tools-patches.
- Directory structure:
- `wekan` — this repo (https://github.com/wekan/wekan); see
`docs/DeveloperDocs/Directory-Structure.md`; `CHANGELOG.md` at root. It is at
`docs/DeveloperDocs/Directory-Structure.md`; `CHANGELOG-big.md` and `CHANGELOG.md` at root. It is at
`~/repos/wekan` on Linux, `~/Documents/repos/wekan` on macOS and
`Downloads\repos\wekan` on Windows. The maintainer commits on `main`; a
contributor works on a branch and opens a pull request — see "Where this
@@ -414,7 +414,7 @@ directly after the merge.
| Repository | File | Format |
| --- | --- | --- |
| `wekan/wekan` | `CHANGELOG.md` | the WeKan format this section describes: `# Status` (Platforms/Version/TODO Later nested as `<details>`), then `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` sections of `<details>` entries whose `<summary>` links the commit |
| `wekan/wekan` | `CHANGELOG-big.md` (and the generated `CHANGELOG.md`, below) | the WeKan format this section describes: `# Status` (Platforms/Version/TODO Later nested as `<details>`), then `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` sections of `<details>` entries whose `<summary>` links the commit |
| `wekan/node-patches` | `CHANGELOG.md` | the same WeKan format, with `# Upcoming node-patches release` |
| `wekan/mongo-tools-patches` | `CHANGELOG.md` | the same WeKan format, with `# Upcoming mongo-tools-patches release` |
| `wekan/FerretDB` | `CHANGELOG.md` | **upstream FerretDB's** format, not WeKan's: `## [v1.48.0](tag URL) (YYYY-MM-DD)` and `### New Features 🎉` / `### Fixed 🐛` / `### Other Changes 🤖` bullets ending `by @xet7. Thanks to xet7.` |
@@ -426,6 +426,52 @@ directly after the merge.
out and would break the tooling that parses them. When this file and the file being
edited disagree, **the file being edited wins** — and everything below in this
section is about the WeKan format specifically.
- **WeKan has TWO changelogs, and only one is written.** `CHANGELOG-big.md` is
the changelog: `# Status`, `# Upcoming WeKan ® release` and every release in
full, in the `<details>` format below - every rule in the rest of this section
is about it, and every entry, fix and release goes into it. `CHANGELOG.md` is
the SHORT changelog, generated from it by `node releases/changelog-short.mjs`
and never edited by hand:
| | `CHANGELOG.md` |
| --- | --- |
| Status, TODO Later | none |
| First | the unreleased `# Upcoming WeKan ® release`, when there is one: its **In short** paragraph only, then `[All changes so far](CHANGELOG-big.md#upcoming-wekan--release)` |
| Below it | the five newest `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` |
| Per release | its **In short** paragraph only, then `[All changes in v…](CHANGELOG-big.md#anchor)` |
| Markup | plain Markdown: no `<details>`, `<summary>`, `</summary>` or `</details>`, and every HTML link a Markdown link |
It exists because the big file is for whoever writes and audits a release,
and most readers - GitHub's front page, the Sandstorm App Market
(`sandstorm-pkgdef.capnp` embeds it), the Helm chart's changelog link - want
what changed lately, at a glance. `releases/release-all.sh` regenerates it
right after naming the new release, so it is in the same release commit, and
`releases/changelog.sh` does the same; `tests/changelogShort.test.cjs` fails
when it is not exactly what the generator writes. So: write
`CHANGELOG-big.md`, run the generator, commit both - and never edit
`CHANGELOG.md` to fix something, because the next run overwrites it.
- **"Update wekan changelog" means both files, in this order:**
1. Find what is not in the changelog yet: `git log` since the newest commit
`CHANGELOG-big.md`'s Upcoming section (or, when it has none, the newest
release's `Prepare v… release` commit) already covers, by anybody - a
merged pull request and the maintainer's own commits alike.
2. Check whether a release was cut since the last update (`release-all.sh`
renames Upcoming to `# v<NEW> …` and opens no new one). If there is no
`# Upcoming WeKan ® release` heading, add it from
[docs/DeveloperDocs/Changelog-Upcoming-Template.md](docs/DeveloperDocs/Changelog-Upcoming-Template.md)
above the newest release - never add an entry to a released section.
3. Write the entries into that Upcoming section of `CHANGELOG-big.md`, in the
format described below (subsections, topic groups, `<details>` entries
linking each commit), and update its **In short** paragraph so it still
names the release's main outcomes.
4. Run `node releases/changelog-short.mjs`: `CHANGELOG.md` then shows that
Upcoming **In short** first and the five newest releases below it, with the
longer details only in `CHANGELOG-big.md`.
5. Run the changelog tests - `tests/changelogShort.test.cjs`,
`tests/changelogFormat.test.cjs`,
`tests/changelogEntriesBelongToTheirRelease.test.cjs` (or
`node tests/run-node-suites.cjs changelog`) - and commit
`CHANGELOG-big.md` and `CHANGELOG.md` together.
- During development, add entries under a new `# Upcoming WeKan ® release` section above
the newest release (FerretDB uses `## Upcoming FerretDB release`; the patch repos use
`# Upcoming <repo> release`). Do **not** hand-edit
@@ -438,10 +484,10 @@ directly after the merge.
add and syntax-check/register the test and state clearly what could not be run.
Do not duplicate an existing test merely to satisfy the audit — extend it when
that keeps one behavior in one suite.
- **CHANGELOG.md stays small enough for GitHub to show it.** GitHub renders
- **CHANGELOG-big.md stays small enough for GitHub to show it.** GitHub renders
Markdown up to 500 KiB; above that it shows only plain source text, and above
about 2 MB nothing at all ("we can't show files that are this big right
now"). CHANGELOG.md reached 2.6 MB over 1,100 releases back to 2015 (#6580),
now"). The changelog reached 2.6 MB over 1,100 releases back to 2015 (#6580),
and keeping only the current month still came to 2.1 MB, because releases
here are frequent and large. So the cut is by SIZE: 450,000 bytes
(`BUDGET` in `releases/changelog-archive.mjs`), under GitHub's limit with
@@ -449,7 +495,7 @@ directly after the merge.
| | |
| --- | --- |
| `CHANGELOG.md` | `# Status` (Platforms/TODO Later inside it), `# Upcoming`, and the newest releases that fit the budget |
| `CHANGELOG-big.md` | `# Status` (Platforms/TODO Later inside it), `# Upcoming`, and the newest releases that fit the budget |
| `old-CHANGELOG/<year>/<MM>.md` | a month's releases, newest first; a month over the budget continues in `<MM>-part2.md`, `<MM>-part3.md`, ... |
| `old-CHANGELOG/<year>.md` | 2015–2025, whole |
@@ -464,11 +510,11 @@ directly after the merge.
still in git (`gitk`, `git-gui`, `git log --follow`), and being small enough
to open is worth more.
Run `node releases/changelog-archive.mjs` whenever CHANGELOG.md has grown -
Run `node releases/changelog-archive.mjs` whenever CHANGELOG-big.md has grown -
after a release is the natural moment. It is idempotent — a run with nothing
to move only refreshes the tables and parts — and it decides from the FILE
rather than the clock, so two people running it agree.
`tests/changelogArchive.test.cjs` fails when CHANGELOG.md is over the budget.
`tests/changelogArchive.test.cjs` fails when CHANGELOG-big.md is over the budget.
An archived section is never edited, for the same reason a released one is
not.
- **The file's shape, top to bottom** — keep it exactly as it is now. There is
@@ -564,7 +610,7 @@ directly after the merge.
`<details>` blocks below carry that information. Keep the paragraph current as
topics change, and shorten it when added commits make it grow. A finished release
keeps the paragraph it was written with.
- **Neither CHANGELOG.md nor release notes may contain a binaries provenance
- **Neither changelog nor the release notes may contain a binaries provenance
section or a Platform/Bundle/Binary/From/Version/Checked/SHA256 table.**
Do not add "Binaries in these bundles" at the top or bottom of either.
Release notes summarize each Translations group as only a list of updated
@@ -577,7 +623,7 @@ directly after the merge.
no security fixes - is left out rather than added empty.
Release notes have only In short, Security, Translations (language list),
the standard thanks line and More details at ChangeLog with the release
anchor link. Other details stay in CHANGELOG.md. Keep download
anchor link into CHANGELOG-big.md. Other details stay in CHANGELOG-big.md. Keep download
verification and `provenance.tsv` build artifacts for diagnostics; do not
append their contents or `releases/provenance-table.sh` output to notes.
- **Inside a subsection, entries are GROUPED BY TOPIC/AREA.** A release touches a handful
@@ -636,7 +682,7 @@ directly after the merge.
Because CRITICAL comes first, it keeps the `This release ` prefix; a following non-security
subsection becomes `and …` per the rule above.
- **TODO Later** — a `<details>` nested inside `# Status` near the TOP of
`CHANGELOG.md` (above the version sections), a triage backlog for open
`CHANGELOG-big.md` (above the version sections), a triage backlog for open
issues that were **investigated but not fixed here**, each
recorded with a concrete REASON so whoever picks it up next knows why. Use it when working
through open issues (the "Fix open issues" process): for each issue, either **fix it** (commit
@@ -717,7 +763,7 @@ have cost a released section its accuracy:
- **Work continues immediately after a release**, so an entry written right
after one has to go somewhere. `release-all.sh` renames `# Upcoming WeKan ®
release` to `# v<NEW> …` and stops there — it does **not** create a new,
empty `# Upcoming` section. CHANGELOG.md never carries an
empty `# Upcoming` section. CHANGELOG-big.md never carries an
`**In short:** nothing here yet.` placeholder sitting between releases; it
used to (via a now-deleted `releases/changelog-open-next.mjs`), and that
meant the file always had a section saying nothing, from the moment a
@@ -869,6 +915,21 @@ have cost a released section its accuracy:
sizes them at 24px, and keeps the icon and its red drop on one line at every width
(they are one icon) — the table itself stays percentage-sized with no fixed or
minimum width, so it fills the page at every browser width.
- **A report that arrives WITHOUT a GHSA is upgraded when its GHSA arrives.** The
stars follow `.tools/wekan.fi/hall-of-fame/Audit-Stars.md`: a GHSA with a
suggested fix is 5, a report sent to security@wekan.fi or any other way, with a
suggested fix, is 3. When that researcher is later able to add a GitHub security
advisory for the same report, the maintainer requests the CVE through the GHSA
process, and the row's 3 stars become 5. So whenever an AI updates or audits the
Hall of Fame, it checks each 3-star row whose Process says the report came
without a GHSA against [WeKan Security
Advisories](https://github.com/wekan/wekan/security/advisories): when a GHSA for
that report now exists, it sets the row to 5 stars on the index AND on the row's
own page, links the GHSA (and the CVE in the CVE cell once one is assigned),
rewrites the Process cell to say the report later arrived as a GHSA, and records
the change in Audit-Stars.md. Match by the report's subject and reporter, not by
a nearby advisory - a GHSA about something else is no upgrade. Until the GHSA
exists, the row stays at 3 and its Process cell says the GHSA is awaited.
- **On the Hall of Fame index page, the Process and Vulnerabilities cells are
collapsed.** Those two columns carry the prose — how the report arrived, and the
whole story of the vulnerability — so the page would be one wall of text and the
+8121
View File
File diff suppressed because it is too large. Load diff
+44 -5275
View File
File diff suppressed because it is too large. Load diff
+73 -12
View File
@@ -35,7 +35,7 @@ contributor — that offer is the mistake, not the rule.
The maintainer's half is the release flow, not taste. Releases here are frequent —
several a day when a fault is being chased — and `releases/release-all.sh` cuts one
from whatever is on `main`, taking the version from the `# Upcoming WeKan ® release`
section of `CHANGELOG.md`. Work parked on a branch is work that is not in the next
section of `CHANGELOG-big.md`. Work parked on a branch is work that is not in the next
release, and `tests/changelogEntriesBelongToTheirRelease.test.cjs` fails when a
released section links a commit that release does not contain.
@@ -340,7 +340,7 @@ directly after the merge.
the two patch repositories under `.tools/` — node-patches and mongo-tools-patches.
- Directory structure:
- `wekan` — this repo (https://github.com/wekan/wekan); see
`docs/DeveloperDocs/Directory-Structure.md`; `CHANGELOG.md` at root. It is at
`docs/DeveloperDocs/Directory-Structure.md`; `CHANGELOG-big.md` and `CHANGELOG.md` at root. It is at
`~/repos/wekan` on Linux, `~/Documents/repos/wekan` on macOS and
`Downloads\repos\wekan` on Windows. The maintainer commits on `main`; a
contributor works on a branch and opens a pull request — see "Where this
@@ -414,7 +414,7 @@ directly after the merge.
| Repository | File | Format |
| --- | --- | --- |
| `wekan/wekan` | `CHANGELOG.md` | the WeKan format this section describes: `# Status` (Platforms/Version/TODO Later nested as `<details>`), then `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` sections of `<details>` entries whose `<summary>` links the commit |
| `wekan/wekan` | `CHANGELOG-big.md` (and the generated `CHANGELOG.md`, below) | the WeKan format this section describes: `# Status` (Platforms/Version/TODO Later nested as `<details>`), then `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` sections of `<details>` entries whose `<summary>` links the commit |
| `wekan/node-patches` | `CHANGELOG.md` | the same WeKan format, with `# Upcoming node-patches release` |
| `wekan/mongo-tools-patches` | `CHANGELOG.md` | the same WeKan format, with `# Upcoming mongo-tools-patches release` |
| `wekan/FerretDB` | `CHANGELOG.md` | **upstream FerretDB's** format, not WeKan's: `## [v1.48.0](tag URL) (YYYY-MM-DD)` and `### New Features 🎉` / `### Fixed 🐛` / `### Other Changes 🤖` bullets ending `by @xet7. Thanks to xet7.` |
@@ -426,6 +426,52 @@ directly after the merge.
out and would break the tooling that parses them. When this file and the file being
edited disagree, **the file being edited wins** — and everything below in this
section is about the WeKan format specifically.
- **WeKan has TWO changelogs, and only one is written.** `CHANGELOG-big.md` is
the changelog: `# Status`, `# Upcoming WeKan ® release` and every release in
full, in the `<details>` format below - every rule in the rest of this section
is about it, and every entry, fix and release goes into it. `CHANGELOG.md` is
the SHORT changelog, generated from it by `node releases/changelog-short.mjs`
and never edited by hand:
| | `CHANGELOG.md` |
| --- | --- |
| Status, TODO Later | none |
| First | the unreleased `# Upcoming WeKan ® release`, when there is one: its **In short** paragraph only, then `[All changes so far](CHANGELOG-big.md#upcoming-wekan--release)` |
| Below it | the five newest `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` |
| Per release | its **In short** paragraph only, then `[All changes in v…](CHANGELOG-big.md#anchor)` |
| Markup | plain Markdown: no `<details>`, `<summary>`, `</summary>` or `</details>`, and every HTML link a Markdown link |
It exists because the big file is for whoever writes and audits a release,
and most readers - GitHub's front page, the Sandstorm App Market
(`sandstorm-pkgdef.capnp` embeds it), the Helm chart's changelog link - want
what changed lately, at a glance. `releases/release-all.sh` regenerates it
right after naming the new release, so it is in the same release commit, and
`releases/changelog.sh` does the same; `tests/changelogShort.test.cjs` fails
when it is not exactly what the generator writes. So: write
`CHANGELOG-big.md`, run the generator, commit both - and never edit
`CHANGELOG.md` to fix something, because the next run overwrites it.
- **"Update wekan changelog" means both files, in this order:**
1. Find what is not in the changelog yet: `git log` since the newest commit
`CHANGELOG-big.md`'s Upcoming section (or, when it has none, the newest
release's `Prepare v… release` commit) already covers, by anybody - a
merged pull request and the maintainer's own commits alike.
2. Check whether a release was cut since the last update (`release-all.sh`
renames Upcoming to `# v<NEW> …` and opens no new one). If there is no
`# Upcoming WeKan ® release` heading, add it from
[docs/DeveloperDocs/Changelog-Upcoming-Template.md](docs/DeveloperDocs/Changelog-Upcoming-Template.md)
above the newest release - never add an entry to a released section.
3. Write the entries into that Upcoming section of `CHANGELOG-big.md`, in the
format described below (subsections, topic groups, `<details>` entries
linking each commit), and update its **In short** paragraph so it still
names the release's main outcomes.
4. Run `node releases/changelog-short.mjs`: `CHANGELOG.md` then shows that
Upcoming **In short** first and the five newest releases below it, with the
longer details only in `CHANGELOG-big.md`.
5. Run the changelog tests - `tests/changelogShort.test.cjs`,
`tests/changelogFormat.test.cjs`,
`tests/changelogEntriesBelongToTheirRelease.test.cjs` (or
`node tests/run-node-suites.cjs changelog`) - and commit
`CHANGELOG-big.md` and `CHANGELOG.md` together.
- During development, add entries under a new `# Upcoming WeKan ® release` section above
the newest release (FerretDB uses `## Upcoming FerretDB release`; the patch repos use
`# Upcoming <repo> release`). Do **not** hand-edit
@@ -438,10 +484,10 @@ directly after the merge.
add and syntax-check/register the test and state clearly what could not be run.
Do not duplicate an existing test merely to satisfy the audit — extend it when
that keeps one behavior in one suite.
- **CHANGELOG.md stays small enough for GitHub to show it.** GitHub renders
- **CHANGELOG-big.md stays small enough for GitHub to show it.** GitHub renders
Markdown up to 500 KiB; above that it shows only plain source text, and above
about 2 MB nothing at all ("we can't show files that are this big right
now"). CHANGELOG.md reached 2.6 MB over 1,100 releases back to 2015 (#6580),
now"). The changelog reached 2.6 MB over 1,100 releases back to 2015 (#6580),
and keeping only the current month still came to 2.1 MB, because releases
here are frequent and large. So the cut is by SIZE: 450,000 bytes
(`BUDGET` in `releases/changelog-archive.mjs`), under GitHub's limit with
@@ -449,7 +495,7 @@ directly after the merge.
| | |
| --- | --- |
| `CHANGELOG.md` | `# Status` (Platforms/TODO Later inside it), `# Upcoming`, and the newest releases that fit the budget |
| `CHANGELOG-big.md` | `# Status` (Platforms/TODO Later inside it), `# Upcoming`, and the newest releases that fit the budget |
| `old-CHANGELOG/<year>/<MM>.md` | a month's releases, newest first; a month over the budget continues in `<MM>-part2.md`, `<MM>-part3.md`, ... |
| `old-CHANGELOG/<year>.md` | 2015–2025, whole |
@@ -464,11 +510,11 @@ directly after the merge.
still in git (`gitk`, `git-gui`, `git log --follow`), and being small enough
to open is worth more.
Run `node releases/changelog-archive.mjs` whenever CHANGELOG.md has grown -
Run `node releases/changelog-archive.mjs` whenever CHANGELOG-big.md has grown -
after a release is the natural moment. It is idempotent — a run with nothing
to move only refreshes the tables and parts — and it decides from the FILE
rather than the clock, so two people running it agree.
`tests/changelogArchive.test.cjs` fails when CHANGELOG.md is over the budget.
`tests/changelogArchive.test.cjs` fails when CHANGELOG-big.md is over the budget.
An archived section is never edited, for the same reason a released one is
not.
- **The file's shape, top to bottom** — keep it exactly as it is now. There is
@@ -564,7 +610,7 @@ directly after the merge.
`<details>` blocks below carry that information. Keep the paragraph current as
topics change, and shorten it when added commits make it grow. A finished release
keeps the paragraph it was written with.
- **Neither CHANGELOG.md nor release notes may contain a binaries provenance
- **Neither changelog nor the release notes may contain a binaries provenance
section or a Platform/Bundle/Binary/From/Version/Checked/SHA256 table.**
Do not add "Binaries in these bundles" at the top or bottom of either.
Release notes summarize each Translations group as only a list of updated
@@ -577,7 +623,7 @@ directly after the merge.
no security fixes - is left out rather than added empty.
Release notes have only In short, Security, Translations (language list),
the standard thanks line and More details at ChangeLog with the release
anchor link. Other details stay in CHANGELOG.md. Keep download
anchor link into CHANGELOG-big.md. Other details stay in CHANGELOG-big.md. Keep download
verification and `provenance.tsv` build artifacts for diagnostics; do not
append their contents or `releases/provenance-table.sh` output to notes.
- **Inside a subsection, entries are GROUPED BY TOPIC/AREA.** A release touches a handful
@@ -636,7 +682,7 @@ directly after the merge.
Because CRITICAL comes first, it keeps the `This release ` prefix; a following non-security
subsection becomes `and …` per the rule above.
- **TODO Later** — a `<details>` nested inside `# Status` near the TOP of
`CHANGELOG.md` (above the version sections), a triage backlog for open
`CHANGELOG-big.md` (above the version sections), a triage backlog for open
issues that were **investigated but not fixed here**, each
recorded with a concrete REASON so whoever picks it up next knows why. Use it when working
through open issues (the "Fix open issues" process): for each issue, either **fix it** (commit
@@ -717,7 +763,7 @@ have cost a released section its accuracy:
- **Work continues immediately after a release**, so an entry written right
after one has to go somewhere. `release-all.sh` renames `# Upcoming WeKan ®
release` to `# v<NEW> …` and stops there — it does **not** create a new,
empty `# Upcoming` section. CHANGELOG.md never carries an
empty `# Upcoming` section. CHANGELOG-big.md never carries an
`**In short:** nothing here yet.` placeholder sitting between releases; it
used to (via a now-deleted `releases/changelog-open-next.mjs`), and that
meant the file always had a section saying nothing, from the moment a
@@ -869,6 +915,21 @@ have cost a released section its accuracy:
sizes them at 24px, and keeps the icon and its red drop on one line at every width
(they are one icon) — the table itself stays percentage-sized with no fixed or
minimum width, so it fills the page at every browser width.
- **A report that arrives WITHOUT a GHSA is upgraded when its GHSA arrives.** The
stars follow `.tools/wekan.fi/hall-of-fame/Audit-Stars.md`: a GHSA with a
suggested fix is 5, a report sent to security@wekan.fi or any other way, with a
suggested fix, is 3. When that researcher is later able to add a GitHub security
advisory for the same report, the maintainer requests the CVE through the GHSA
process, and the row's 3 stars become 5. So whenever an AI updates or audits the
Hall of Fame, it checks each 3-star row whose Process says the report came
without a GHSA against [WeKan Security
Advisories](https://github.com/wekan/wekan/security/advisories): when a GHSA for
that report now exists, it sets the row to 5 stars on the index AND on the row's
own page, links the GHSA (and the CVE in the CVE cell once one is assigned),
rewrites the Process cell to say the report later arrived as a GHSA, and records
the change in Audit-Stars.md. Match by the report's subject and reporter, not by
a nearby advisory - a GHSA about something else is no upgrade. Until the GHSA
exists, the row stays at 3 and its Process cell says the GHSA is awaited.
- **On the Hall of Fame index page, the Process and Vulnerabilities cells are
collapsed.** Those two columns carry the prose — how the report arrived, and the
whole story of the vulnerability — so the page would be one wall of text and the
+11 -3
View File
@@ -38,7 +38,7 @@ FROM --platform=$BUILDPLATFORM debian:trixie AS bundle
ARG TARGETARCH
ARG TARGETVARIANT
ARG VERSION=12.67
ARG VERSION=12.94
ARG DEBIAN_FRONTEND=noninteractive
COPY --chmod=755 releases/check-telemetry.py /tmp/check-telemetry.py
@@ -148,8 +148,8 @@ ARG DEBIAN_FRONTEND=noninteractive
ENV \
DEBUG=false \
DDP_TRANSPORT=sockjs \
NODE_VERSION=v26.12.1 \
METEOR_RELEASE=METEOR@3.6-rc.0 \
NODE_VERSION=v26.13.0 \
METEOR_RELEASE=METEOR@3.6-rc.1 \
USE_EDGE=false \
NPM_VERSION=12.2.1 \
SRC_PATH=./ \
@@ -292,6 +292,14 @@ ENV \
LOGOUT_ON_HOURS="" \
LOGOUT_ON_MINUTES="" \
USERS_DISABLE_INACTIVE_AFTER_DAYS="" \
LOGOUT_IDLE_MINUTES="" \
REQUIRE_2FA="" \
PASSWORD_MIN_LENGTH="" \
PASSWORD_MIN_LENGTH_ENFORCE="" \
PASSWORD_CHECK_PWNED="" \
HSTS_MAX_AGE="" \
HSTS_INCLUDE_SUBDOMAINS="" \
HSTS_PRELOAD="" \
ATTACHMENTS_RETENTION_DAYS="" \
ATTACHMENTS_RETENTION_MODE="" \
ATTACHMENTS_PURGE_DELETED_AFTER_DAYS="" \
+13 -5
View File
@@ -29,7 +29,15 @@ because WeKan is NOT Big Tech. WeKan is FLOSS.
- **Remediation**: Please wait for a new WeKan release that addresses the issue.
Fixes are announced at the top of the [ChangeLog](https://github.com/wekan/wekan/blob/main/CHANGELOG.md).
- **Recognition**: We will acknowledge your contribution by adding you to our
[Hall of Fame](https://wekan.fi/hall-of-fame/).
[Hall of Fame](https://wekan.fi/hall-of-fame/). The stars follow the
[Stars Audit](https://github.com/wekan/wekan.fi/blob/main/hall-of-fame/Audit-Stars.md):
a GitHub security advisory (GHSA) with details and a suggested fix gets 5 stars,
a report sent to security@wekan.fi or some other way, with a suggested fix, gets 3.
- **Adding the GHSA later**: if you sent your report to security@wekan.fi, or some
other way, and are later able to add it as a "New draft security advisory" at
[WeKan Security Advisories](https://github.com/wekan/wekan/security/advisories),
please do. The maintainer of WeKan then requests the CVE through that GHSA, and
your Hall of Fame row is upgraded from 3 stars to 5.
- **CVE Policy**: WeKan Security requests CVE at GitHub when releasing Security Advisory.
### 3. Post-Release and Public Disclosure:
@@ -264,13 +272,13 @@ Meteor.startup(() => {
On the Sandstorm platform, Standalone WeKan features like the Admin Panel are turned off using environment variables, because Sandstorm platform provides SSO for all apps running on Sandstorm.
[Sandstorm](https://sandstorm.io) is a separate Open Source platform that has been
[security audited](https://sandstorm.io/news/2017-03-02-security-review) and found bugs fixed.
[Sandstorm](https://sandstorm.org) is a separate Open Source platform that has been
[security audited](https://sandstorm.org/news/2017-03-02-security-review) and found bugs fixed.
Sandstorm also has passwordless login, LDAP, SAML, Google, and other auth options already.
On Sandstorm, code is read-only and signed by app maintainers; only grain content can be modified.
WeKan on Sandstorm runs in a sandboxed grain; it does not have access elsewhere without a user-visible PowerBox request or opening a randomly-generated API key URL.
Also read [Sandstorm Security Practices](https://docs.sandstorm.io/en/latest/using/security-practices/) and
[Sandstorm Security non-events](https://docs.sandstorm.io/en/latest/using/security-non-events/).
Also read [Sandstorm Security Practices](https://docs.sandstorm.org/en/latest/using/security-practices/) and
[Sandstorm Security non-events](https://docs.sandstorm.org/en/latest/using/security-non-events/).
For Sandstorm specific security issues you can contact [kentonv](https://github.com/kentonv) by email.
## What WeKan bugs are eligible?
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v12.67.0"
appVersion: "v12.94.0"
files:
userUploads:
- README.md
+45 -52
View File
@@ -8,13 +8,6 @@
# TODO:
# addcustomfieldtoboard: There is error: Settings must be object. So adding does not work yet.
try:
# python 3
from urllib.parse import urlencode
except ImportError:
# python 2
from urllib import urlencode
import json
import requests
import sys
@@ -163,7 +156,7 @@ If *nix: chmod +x api.py => ./api.py users
if arguments == 0:
print(syntax)
exit
sys.exit()
# TODO:
# print(" python3 api.py attachmentjson BOARDID ATTACHMENTID # One attachment as JSON base64")
@@ -329,7 +322,7 @@ if arguments == 8:
if new_card_id:
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + new_card_id
put_data = {'labelIds': labelIds}
body = requests.put(edcard, data=put_data, headers=headers)
requests.put(edcard, data=put_data, headers=headers)
print("=== EDIT CARD ===\n")
body = requests.get(edcard, headers=headers)
data2 = body.text.replace('}', "}\n")
@@ -372,7 +365,7 @@ if arguments == 6:
print(edcard)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {'title': '{}'.format(newcardtitle), 'description': '{}'.format(newcarddescription)}
body = requests.put(edcard, data=put_data, headers=headers)
requests.put(edcard, data=put_data, headers=headers)
print("=== EDIT CARD ===\n")
body = requests.get(edcard, headers=headers)
data2 = body.text.replace('}',"}\n")
@@ -411,7 +404,7 @@ if arguments == 5:
print(edcard)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {'labelIds': labelIds}
body = requests.put(edcard, data=put_data, headers=headers)
requests.put(edcard, data=put_data, headers=headers)
print("=== ADD LABEL ===\n")
body = requests.get(edcard, headers=headers)
data2 = body.text.replace('}',"}\n")
@@ -437,7 +430,7 @@ if arguments == 5:
print(edcard)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {'color': '{}'.format(newcolor)}
body = requests.put(edcard, data=put_data, headers=headers)
requests.put(edcard, data=put_data, headers=headers)
print("=== EDIT CARD COLOR ===\n")
body = requests.get(edcard, headers=headers)
data2 = body.text.replace('}', "}\n")
@@ -456,7 +449,7 @@ if arguments >= 4:
print(edlist)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {'title': '{}'.format(newlisttitle)}
body = requests.put(edlist, data=put_data, headers=headers)
requests.put(edlist, data=put_data, headers=headers)
print("=== EDIT LIST ===\n")
body = requests.get(edlist, headers=headers)
data2 = body.text.replace('}', "}\n")
@@ -482,7 +475,7 @@ if arguments >= 4:
print(edlist)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {'color': '{}'.format(newcolor)}
body = requests.put(edlist, data=put_data, headers=headers)
requests.put(edlist, data=put_data, headers=headers)
print("=== EDIT LIST COLOR ===\n")
body = requests.get(edlist, headers=headers)
data2 = body.text.replace('}', "}\n")
@@ -1050,7 +1043,7 @@ if arguments >= 1:
if arguments < 6:
print("Usage: python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND]")
print("Storage backends: fs, gridfs, s3")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
swimlaneid = sys.argv[3]
@@ -1064,7 +1057,7 @@ if arguments >= 1:
import os, mimetypes
if not os.path.isfile(filepath):
print(f"Error: File '{filepath}' not found")
exit(1)
sys.exit(1)
filename = os.path.basename(filepath)
file_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
params = {'boardId': boardid, 'swimlaneId': swimlaneid, 'listId': listid, 'cardId': cardid,
@@ -1092,7 +1085,7 @@ if arguments >= 1:
# ------- DOWNLOAD ATTACHMENT START -----------
if arguments < 3:
print("Usage: python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH")
exit(1)
sys.exit(1)
attachmentid = sys.argv[2]
outputpath = sys.argv[3]
@@ -1121,7 +1114,7 @@ if arguments >= 1:
# ------- ATTACHMENT INFO START -----------
if arguments < 2:
print("Usage: python3 api.py attachmentinfo ATTACHMENTID")
exit(1)
sys.exit(1)
attachmentid = sys.argv[2]
@@ -1160,7 +1153,7 @@ if arguments >= 1:
# ------- LIST CARD ATTACHMENTS START -----------
if arguments < 5:
print("Usage: python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
swimlaneid = sys.argv[3]
@@ -1199,7 +1192,7 @@ if arguments >= 1:
# ------- COPY/MOVE ATTACHMENT START -----------
if arguments < 6:
print("Usage: python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move]")
exit(1)
sys.exit(1)
attachmentid = sys.argv[2]
targetboardid = sys.argv[3]
@@ -1210,7 +1203,7 @@ if arguments >= 1:
if operation not in ['copy', 'move']:
print("Operation must be 'copy' or 'move'")
exit(1)
sys.exit(1)
# Prepare request data
request_data = {
@@ -1253,7 +1246,7 @@ if arguments >= 1:
# ------- DELETE ATTACHMENT START -----------
if arguments < 2:
print("Usage: python3 api.py deleteattachment ATTACHMENTID")
exit(1)
sys.exit(1)
attachmentid = sys.argv[2]
@@ -1282,7 +1275,7 @@ if arguments >= 1:
# ------- BOARD MEMBER / CARD FIELD API ENDPOINTS START -----------
# These commands wrap REST endpoints that already exist in Wekan.
# See CHANGELOG.md "Upcoming" section for related issues and design notes.
# See CHANGELOG-big.md "Upcoming" section for related issues and design notes.
# Map a human readable ROLE to the board member permission boolean flags
# used by /api/boards/:boardId/members/:userId/add and .../members/:memberId
@@ -1322,7 +1315,7 @@ if arguments >= 1:
# ------- ADD BOARD MEMBER START -----------
if arguments < 3:
print("Usage: python3 api.py addboardmember BOARDID USERID ROLE")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
userid = sys.argv[3]
role = sys.argv[4] if arguments > 3 else 'normal'
@@ -1339,7 +1332,7 @@ if arguments >= 1:
# ------- REMOVE BOARD MEMBER START -----------
if arguments < 3:
print("Usage: python3 api.py removeboardmember BOARDID USERID")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
userid = sys.argv[3]
remove_member_url = wekanurl + apiboards + boardid + s + 'members' + s + userid + s + 'remove'
@@ -1354,7 +1347,7 @@ if arguments >= 1:
# ------- SET BOARD MEMBER ROLE START -----------
if arguments < 4:
print("Usage: python3 api.py setboardmemberrole BOARDID USERID ROLE")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
userid = sys.argv[3]
role = sys.argv[4]
@@ -1370,7 +1363,7 @@ if arguments >= 1:
# ------- LIST BOARD DOMAINS START -----------
if arguments < 2:
print("Usage: python3 api.py boarddomains BOARDID")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
domains_url = wekanurl + apiboards + boardid + s + 'domains'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
@@ -1383,7 +1376,7 @@ if arguments >= 1:
# ------- ADD BOARD DOMAIN START -----------
if arguments < 3:
print("Usage: python3 api.py addboarddomain BOARDID DOMAIN")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
domain = sys.argv[3]
add_domain_url = wekanurl + apiboards + boardid + s + 'domains'
@@ -1398,7 +1391,7 @@ if arguments >= 1:
# ------- REMOVE BOARD DOMAIN START -----------
if arguments < 3:
print("Usage: python3 api.py removeboarddomain BOARDID DOMAIN")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
domain = sys.argv[3]
remove_domain_url = wekanurl + apiboards + boardid + s + 'domains' + s + domain
@@ -1413,7 +1406,7 @@ if arguments >= 1:
if arguments < 4:
print("Usage: python3 api.py {} BOARDID LISTID CARDID IDS".format(sys.argv[1]))
print("IDS = comma-separated userIds, or '' to clear")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
@@ -1435,7 +1428,7 @@ if arguments >= 1:
print("Usage: python3 api.py setcarddate BOARDID LISTID CARDID DATETYPE DATEVALUE")
print("DATETYPE: received, start, due, end")
print("DATEVALUE: ISO 8601, e.g. 2026-06-07T12:00:00.000Z")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
@@ -1453,7 +1446,7 @@ if arguments >= 1:
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {datemap[datetype]: datevalue}
body = requests.put(edcard, data=put_data, headers=headers)
requests.put(edcard, data=put_data, headers=headers)
print("=== SET CARD DATE {} ===\n".format(datetype.upper()))
print(body.text)
# ------- SET CARD DATE END -----------
@@ -1463,7 +1456,7 @@ if arguments >= 1:
if arguments < 4:
print("Usage: python3 api.py setcardlabels BOARDID LISTID CARDID LABELIDS")
print("LABELIDS = comma-separated labelIds, or '' to clear")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
@@ -1472,7 +1465,7 @@ if arguments >= 1:
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
# NOTE: this REPLACES the card's labels. Bulk add/remove (merge) is not yet
# implemented server-side, see Issue #5819 in CHANGELOG.md "Upcoming".
# implemented server-side, see Issue #5819 in CHANGELOG-big.md "Upcoming".
put_data = {'labelIds': labelids if labelids else []}
body = requests.put(edcard, json=put_data, headers=headers)
print("=== SET CARD LABELS ===\n")
@@ -1483,7 +1476,7 @@ if arguments >= 1:
# ------- MOVE CARD START -----------
if arguments < 7:
print("Usage: python3 api.py movecard BOARDID LISTID CARDID NEWBOARDID NEWSWIMLANEID NEWLISTID")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
@@ -1497,7 +1490,7 @@ if arguments >= 1:
'newSwimlaneId': newswimlaneid,
'newListId': newlistid,
}
body = requests.put(edcard, data=put_data, headers=headers)
requests.put(edcard, data=put_data, headers=headers)
print("=== MOVE CARD ===\n")
print(body.text)
# ------- MOVE CARD END -----------
@@ -1508,7 +1501,7 @@ if arguments >= 1:
print("Usage: python3 api.py bulkaddcards BOARDID LISTID AUTHORID SWIMLANEID CARDSJSONFILE")
print("CARDSJSONFILE = JSON file with a list of titles, e.g. [\"Card A\", \"Card B\"],")
print(" or a list of card objects [{\"title\": \"Card A\", \"description\": \"...\"}, ...]")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
authorid = sys.argv[4]
@@ -1516,16 +1509,16 @@ if arguments >= 1:
cardsjsonfile = sys.argv[6] if arguments > 5 else None
if not cardsjsonfile:
print("CARDSJSONFILE is required")
exit(1)
sys.exit(1)
try:
with open(cardsjsonfile, 'r') as f:
raw_cards = json.load(f)
except Exception as e:
print("Error reading CARDSJSONFILE: {}".format(e))
exit(1)
sys.exit(1)
if not isinstance(raw_cards, list) or len(raw_cards) == 0:
print("CARDSJSONFILE must contain a non-empty JSON list")
exit(1)
sys.exit(1)
cards = []
for item in raw_cards:
if isinstance(item, str):
@@ -1534,7 +1527,7 @@ if arguments >= 1:
cards.append(item)
else:
print("Each card must be a string (title) or an object")
exit(1)
sys.exit(1)
bulk_url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + 'bulk'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
post_data = {'authorId': authorid, 'swimlaneId': swimlaneid, 'cards': cards}
@@ -1548,7 +1541,7 @@ if arguments >= 1:
if arguments < 3:
print("Usage: python3 api.py bulkdeletecards BOARDID CARDIDS")
print("CARDIDS = comma-separated cardIds")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
cardids_raw = sys.argv[3]
cardids = [x for x in cardids_raw.split(',') if x]
@@ -1565,7 +1558,7 @@ if arguments >= 1:
if arguments < 5:
print("Usage: python3 api.py bulkcardlabels BOARDID CARDIDS ADDLABELIDS REMOVELABELIDS")
print("CARDIDS / ADDLABELIDS / REMOVELABELIDS = comma-separated, or '' for none")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
cardids = [x for x in sys.argv[3].split(',') if x]
add_labelids = [x for x in sys.argv[4].split(',') if x]
@@ -1582,7 +1575,7 @@ if arguments >= 1:
# ------- CREATE LINKED CARD START -----------
if arguments < 6:
print("Usage: python3 api.py linkcard BOARDID LISTID SWIMLANEID AUTHORID LINKEDCARDID")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
swimlaneid = sys.argv[4]
@@ -1600,7 +1593,7 @@ if arguments >= 1:
# ------- ADD/REMOVE CARD MEMBER / ASSIGNEE START -----------
if arguments < 5:
print("Usage: python3 api.py {} BOARDID LISTID CARDID USERID".format(sys.argv[1]))
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
@@ -1620,14 +1613,14 @@ if arguments >= 1:
# ------- COPY CARD START -----------
if arguments < 4:
print("Usage: python3 api.py copycard BOARDID LISTID CARDID TOSWIMLANEID [TOBOARDID] [TOLISTID] [POSITION]")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
toswimlaneid = sys.argv[5] if arguments > 4 else None
if not toswimlaneid:
print("TOSWIMLANEID is required")
exit(1)
sys.exit(1)
toboardid = sys.argv[6] if arguments > 5 else boardid
tolistid = sys.argv[7] if arguments > 6 else listid
url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid + s + 'copy'
@@ -1644,7 +1637,7 @@ if arguments >= 1:
# ------- COPY/MOVE SWIMLANE START -----------
if arguments < 3:
print("Usage: python3 api.py {} BOARDID SWIMLANEID [TOBOARDID] [POSITION]".format(sys.argv[1]))
exit(1)
sys.exit(1)
boardid = sys.argv[2]
swimlaneid = sys.argv[3]
toboardid = sys.argv[4] if arguments > 3 else boardid
@@ -1663,7 +1656,7 @@ if arguments >= 1:
# ------- COPY/MOVE LIST START -----------
if arguments < 3:
print("Usage: python3 api.py {} BOARDID LISTID [TOSWIMLANEID] [TOBOARDID] [POSITION]".format(sys.argv[1]))
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
toswimlaneid = sys.argv[4] if arguments > 3 else None
@@ -1702,7 +1695,7 @@ if arguments >= 1:
# ------- GET CARD SETTINGS START -----------
if arguments < 2:
print("Usage: python3 api.py getcardsettings BOARDID")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
url = wekanurl + apiboards + boardid + s + 'cardSettings'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
@@ -1717,7 +1710,7 @@ if arguments >= 1:
if arguments < 4:
print("Usage: python3 api.py setcardsetting BOARDID KEY VALUE")
print("e.g. python3 api.py setcardsetting BOARDID allowsDueDate true")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
key = sys.argv[3]
value = sys.argv[4]
@@ -1945,7 +1938,7 @@ if arguments >= 3 and sys.argv[1] == 'uploadbackground':
response = requests.post(url, headers=headers, params=params, data=f)
except Exception as e:
print(f"Error reading file: {e}")
exit(1)
sys.exit(1)
print(response.text)
if arguments >= 3 and sys.argv[1] == 'downloadbackground':
+9 -6
View File
@@ -989,15 +989,18 @@ exit /b %errorlevel%
:buildcommon
REM The same diagnostics as build.sh build_wekan: npm verbose output and
REM foreground install scripts, METEOR_PROFILE, Node module resolution and the
REM resolver command/output tracer, so a failed Windows build leaves the same
REM evidence in its log. WEKAN_BUILD_HEAP_SNAPSHOT=1 caps the heap at
REM foreground install scripts, METEOR_PROFILE and the resolver command/output
REM tracer, so a failed Windows build leaves the same evidence in its log. Node
REM module resolution (one line per require, gigabytes a minute) only with
REM WEKAN_BUILD_TRACE_MODULES=1. WEKAN_BUILD_HEAP_SNAPSHOT=1 caps the heap at
REM WEKAN_BUILD_HEAP_SNAPSHOT_MB (4096) and writes a heap snapshot near the limit.
setlocal
set "npm_config_loglevel=verbose"
set "npm_config_foreground_scripts=true"
if not defined METEOR_PROFILE set "METEOR_PROFILE=100"
if defined NODE_DEBUG (set "NODE_DEBUG=%NODE_DEBUG%,module") else (set "NODE_DEBUG=module")
if "%WEKAN_BUILD_TRACE_MODULES%"=="1" (
if defined NODE_DEBUG (set "NODE_DEBUG=!NODE_DEBUG!,module") else (set "NODE_DEBUG=module")
)
if "%WEKAN_BUILD_HEAP_SNAPSHOT%"=="1" (
if not defined WEKAN_BUILD_HEAP_SNAPSHOT_MB set "WEKAN_BUILD_HEAP_SNAPSHOT_MB=4096"
set "TOOL_NODE_FLAGS=--max-old-space-size=!WEKAN_BUILD_HEAP_SNAPSHOT_MB! --heapsnapshot-near-heap-limit=1"
@@ -1005,7 +1008,7 @@ if "%WEKAN_BUILD_HEAP_SNAPSHOT%"=="1" (
echo Heap snapshot: ON, heap capped at !WEKAN_BUILD_HEAP_SNAPSHOT_MB! MB; the build will fail sooner and write Heap.*.heapsnapshot.
)
set "NODE_OPTIONS=%NODE_OPTIONS% --require="%REPO%\tools\build-command-output.cjs""
echo Build diagnostics: resolver command/output tracing, Node module resolution, npm verbose output, foreground install scripts, METEOR_PROFILE=%METEOR_PROFILE% ^(milliseconds^).
echo Build diagnostics: resolver command/output tracing, npm verbose output, foreground install scripts, METEOR_PROFILE=%METEOR_PROFILE% ^(milliseconds^).
call :buildcommon_steps
set "BUILD_RC=%errorlevel%"
if not "%BUILD_RC%"=="0" if defined WEKAN_BUILD_LOG (
@@ -1977,7 +1980,7 @@ netstat -ano | findstr /r /c:":3001 .*LISTENING" >nul 2>&1
if not errorlevel 1 (
echo ==^> Reusing the MongoDB already listening on :3001 ^(not started or stopped by this run^).
) else (
for %%i in ("%REPO%\..\mongodb-test-3001") do set "DBPATH=%%~fi"
for %%i in ("%REPO%\.tools\mongodb-test-3001") do set "DBPATH=%%~fi"
if not exist "!DBPATH!" md "!DBPATH!"
echo ==^> Starting MongoDB ^(Meteor's mongod^) on :3001, dbpath !DBPATH!.
start "WekanTestMongo" /MIN "%MONGOD_BIN%" --port 3001 --dbpath "!DBPATH!" --bind_ip 127.0.0.1 --logpath "%RUN_LOGDIR%\wekan-test-mongod.log"
+73 -33
View File
@@ -468,10 +468,17 @@ function build_wekan(){
echo "===== wekan build started $(date '+%F %T') ====="
export npm_config_loglevel=verbose npm_config_foreground_scripts=true
export METEOR_PROFILE="${METEOR_PROFILE:-100}"
export NODE_DEBUG="${NODE_DEBUG:+$NODE_DEBUG,}module"
# Node module resolution, one line per require. OFF unless
# WEKAN_BUILD_TRACE_MODULES=1: a Meteor build writes it at gigabytes a
# minute - 1.1 GB in the first three minutes of a menu dev build, over
# 12 GB in the first half hour of EVERYTHING on macOS, written twice
# through tee - and slows the build it is tracing.
if [ "${WEKAN_BUILD_TRACE_MODULES:-0}" = 1 ]; then
export NODE_DEBUG="${NODE_DEBUG:+$NODE_DEBUG,}module"
fi
# NODE_OPTIONS parses quoted paths, including checkouts with spaces.
export NODE_OPTIONS="${NODE_OPTIONS:-} --require=\"$(pwd)/tools/build-command-output.cjs\""
echo "Build diagnostics: resolver command/output tracing, Node module resolution, npm verbose output, foreground install scripts, METEOR_PROFILE=$METEOR_PROFILE (milliseconds)."
echo "Build diagnostics: resolver command/output tracing, npm verbose output, foreground install scripts, METEOR_PROFILE=$METEOR_PROFILE (milliseconds), Node module resolution $([ "${WEKAN_BUILD_TRACE_MODULES:-0}" = 1 ] && echo on || echo 'off (WEKAN_BUILD_TRACE_MODULES=1 turns it on)')."
build_stage "1/4 Remove dependencies and build caches" rm -rf node_modules node_modules/.cache .meteor/local .build _build || return $?
# Updating npm metadata has historically been best effort; installation is required.
build_stage "2/4 Compile app to resolve Meteor plugin npm dependencies" meteor update --npm || echo "WARNING: Meteor plugin npm dependency compilation failed; trying dependency installation."
@@ -645,8 +652,18 @@ function run_playwright_docker(){
local docker_base_url="${WEKAN_BASE_URL:-http://127.0.0.1:3000}"
local docker_mongo_url="${WEKAN_MONGO_URL:-mongodb://127.0.0.1:3001/meteor}"
local docker_host_proxy_port=""
local docker_network="host"
# Docker Desktop on macOS keeps the host outside the container's loopback.
if [ "$(uname -s)" = Darwin ]; then
# Its host network loses connections to host.docker.internal: from a
# container on that network about 1 connect in 75 never completed, so a
# test page waited 60 s for a document that never came (Firefox, 2026-11-01:
# 8 of 600 direct connects, against 0 of 1000 from the bridge network).
# Nothing here needs the host network on macOS - WeKan and Mongo are reached
# through host.docker.internal and SMTP through docker exec - so the
# container uses Docker's bridge network, where the proxy below still
# serves localhost.
docker_network="bridge"
# Meteor's dynamic imports use ROOT_URL (localhost in the test server).
# Keep the browser on that same origin and bridge its loopback to the host.
# Browsing host.docker.internal while ROOT_URL is localhost makes Firefox
@@ -670,35 +687,53 @@ function run_playwright_docker(){
mkdir -p "$filesroot"
echo "Running Playwright $browser in Docker ($image)."
echo "Expecting WeKan at $docker_base_url."
local smtp_proxy_pid="" smtp_proxy_ready="" container_name="wekan-playwright-${browser}-$$"
if [ "$(uname -s)" = Darwin ] && [ -n "${WEKAN_TEST_SMTP_PORT:-}" ]; then
smtp_proxy_ready="$(mktemp "$WEKAN_DIR/.tools/tmp/smtp-proxy.XXXXXX")"
node "$pwdir/helpers/docker-smtp-proxy.cjs" "$container_name" "$WEKAN_TEST_SMTP_PORT" "$smtp_proxy_ready" &
smtp_proxy_pid=$!
for i in 1 2 3 4 5 6 7 8 9 10; do
[ -s "$smtp_proxy_ready" ] && break
kill -0 "$smtp_proxy_pid" 2>/dev/null || break
sleep 0.1
local container_name="wekan-playwright-${browser}-$$"
# Docker Desktop keeps the container's loopback from the host, so a server a
# spec starts in the container cannot be reached by WeKan outside it. Each
# port below is forwarded from the host into the container through docker
# exec (helpers/docker-smtp-proxy.cjs, which carries any TCP): the test SMTP
# server, and a small pool for the mock Jira/GitLab servers of the list-sync
# specs, which take a free port from WEKAN_TEST_REVERSE_PORTS
# (helpers/reachable-server.cjs). Elsewhere they listen on any port.
local forward_pids="" forward_ready="" reverse_ports=""
if [ "$(uname -s)" = Darwin ]; then
reverse_ports="${WEKAN_TEST_REVERSE_PORTS:-39101 39102 39103 39104}"
local fport fready fpid i
for fport in ${WEKAN_TEST_SMTP_PORT:-} $reverse_ports; do
fready="$(mktemp "$WEKAN_DIR/.tools/tmp/docker-port-proxy.XXXXXX")"
node "$pwdir/helpers/docker-smtp-proxy.cjs" "$container_name" "$fport" "$fready" &
fpid=$!
forward_pids="$forward_pids $fpid"; forward_ready="$forward_ready $fready"
for i in 1 2 3 4 5 6 7 8 9 10; do
[ -s "$fready" ] && break
kill -0 "$fpid" 2>/dev/null || break
sleep 0.1
done
if [ ! -s "$fready" ]; then
kill $forward_pids 2>/dev/null; wait $forward_pids 2>/dev/null
rm -f $forward_ready
echo "ERROR: local test proxy for port $fport did not start."
return 1
fi
done
if [ ! -s "$smtp_proxy_ready" ]; then
kill "$smtp_proxy_pid" 2>/dev/null; wait "$smtp_proxy_pid" 2>/dev/null
rm -f "$smtp_proxy_ready"
echo "ERROR: local SMTP test proxy did not start."
return 1
fi
fi
# Mount the whole repo so specs that reach the repo-root node_modules
# (e.g. @wekanteam/exceljs) and .tools resolve; run from tests/playwright.
# The repo and the files root are mounted at their OWN host paths, so a path
# names the same file for the specs in here and for the server outside:
# admin-continuous-backup gives the server a key file it wrote and reads the
# stream the server wrote, and with the repo at /repo neither side found the
# other's file.
# Run as the host user (--user) with a writable HOME so the container does
# not leave root-owned files under test-results/ (which would later make
# native Chromium/Firefox runs fail with "EACCES: permission denied, mkdir
# .../test-results/.playwright-artifacts-N").
docker_exec run --rm --init --ipc=host --network host \
docker_exec run --rm --init --ipc=host --network "$docker_network" \
--name "$container_name" \
--label org.wekan.test-run=everything \
--user "$(id -u):$(id -g)" \
-e HOME=/repo/.tools/tmp \
-e TMPDIR=/repo/.tools/tmp \
-e HOME="$reporoot/.tools/tmp" \
-e TMPDIR="$reporoot/.tools/tmp" \
-e WEKAN_BASE_URL="$docker_base_url" \
-e WEKAN_DOCKER_HOST_PROXY_PORT="$docker_host_proxy_port" \
-e WEKAN_MONGO_URL="$docker_mongo_url" \
@@ -710,16 +745,17 @@ function run_playwright_docker(){
-e WEKAN_TEST_SERIAL="${WEKAN_TEST_SERIAL:-0}" \
-e WEKAN_TEST_SERVER_MODE="${WEKAN_TEST_SERVER_MODE:-bundle}" \
-e WEKAN_TEST_SMTP_PORT="${WEKAN_TEST_SMTP_PORT:-}" \
-e WEKAN_TEST_REVERSE_PORTS="$reverse_ports" \
-e PLAYWRIGHT_HTML_OPEN=never \
-e WEKAN_FILES_PATH=/wekan-files \
-e WEKAN_FILES_PATH="$filesroot" \
-e PLAYWRIGHT_JSON_OUTPUT_NAME="${PLAYWRIGHT_JSON_OUTPUT_NAME:-}" \
-v "$filesroot":/wekan-files \
-v "$reporoot":/repo -w /repo/tests/playwright \
-v "$filesroot":"$filesroot" \
-v "$reporoot":"$reporoot" -w "$pwdir" \
"$image" \
sh -c '
export PATH=/repo/tests/playwright/node_modules/.bin:$PATH
export PATH="$PWD/node_modules/.bin:$PATH"
if [ -n "$WEKAN_DOCKER_HOST_PROXY_PORT" ]; then
node /repo/tests/playwright/helpers/docker-host-proxy.cjs &
node "$PWD/helpers/docker-host-proxy.cjs" &
proxy_pid=$!
trap '\''kill "$proxy_pid" 2>/dev/null || true; wait "$proxy_pid" 2>/dev/null || true'\'' EXIT
ready=0
@@ -732,9 +768,9 @@ function run_playwright_docker(){
npx playwright test --project="$0" "$@"
' "$browser" "$@"
local playwright_rc=$?
if [ -n "$smtp_proxy_pid" ]; then
kill "$smtp_proxy_pid" 2>/dev/null; wait "$smtp_proxy_pid" 2>/dev/null
rm -f "$smtp_proxy_ready"
if [ -n "$forward_pids" ]; then
kill $forward_pids 2>/dev/null; wait $forward_pids 2>/dev/null
rm -f $forward_ready
fi
return "$playwright_rc"
}
@@ -1506,7 +1542,11 @@ function run_all_tests(){
echo "ERROR: nothing is listening on :$TEST_DB_PORT and no mongod was found (Meteor dev_bundle or PATH)."
echo " Meteor's bundled mongod is normally at <dev_bundle>/mongodb/bin/mongod."; rm -rf "$STATDIR"; return 1
fi
local DBPATH="../mongodb-test-$TEST_DB_PORT"
# In .tools/, where everything this script writes lives (CLAUDE.md;
# tests/toolsDirOnly.test.cjs). It was in the checkout's PARENT directory,
# which a sandboxed editor cannot write - the test database never
# started there.
local DBPATH="$WEKAN_DIR/.tools/mongodb-test-$TEST_DB_PORT"
mkdir -p "$DBPATH"
echo "==> Starting MongoDB (Meteor's mongod) on :$TEST_DB_PORT, dbpath $DBPATH."
{ echo "===== mongod :$TEST_DB_PORT - started $(date '+%Y-%m-%d %H:%M:%S %Z') ====="; exec "$MONGOD_BIN" --port "$TEST_DB_PORT" --dbpath "$DBPATH" --bind_ip 127.0.0.1 --nounixsocket; } > "$RUN_LOGDIR/wekan-test-mongod.log" 2>&1 &
@@ -1534,7 +1574,7 @@ function run_all_tests(){
export WEKAN_FILES_PATH_HOST="$WRITABLE_ABS/files"
# ...and the same path for a LOCAL playwright run, not only the Docker one.
# Only WEKAN_FILES_PATH_HOST was exported, which is read when the browsers run
# in a container (it is mounted there as /wekan-files). A local run fell back
# in a container (it is mounted there at the same path). A local run fell back
# to the spec's own default of .build/bundle/files, so a spec that writes a
# stored attachment wrote it OUTSIDE the root this server reads - and then
# tested what WeKan does with a file it does not own.
@@ -2393,8 +2433,8 @@ function git_fix_changelog_links(){
local script="$WEKAN_DIR/releases/fix-changelog-hashes.sh"
[ -f "$script" ] || return 1
bash "$script" || return 1
if [ -n "$(git status --porcelain -- CHANGELOG.md 2>/dev/null)" ]; then
git add CHANGELOG.md
if [ -n "$(git status --porcelain -- CHANGELOG-big.md CHANGELOG.md 2>/dev/null)" ]; then
git add CHANGELOG-big.md CHANGELOG.md
git commit -q -m "CHANGELOG: repoint commit links after history moved.
A rebase rewrote the commits these entries link to, so the links named commits
@@ -2605,7 +2645,7 @@ choose() {
# snap/bundle, not on a maintainer's machine) and the superseded old-*.sh and
# translations/fill_translations.py.
# Release notes summarize translations as affected languages; other changelog
# details stay in CHANGELOG.md. Notes include only In short, Security,
# details stay in CHANGELOG-big.md. Notes include only In short, Security,
# Translations language list, thanks and the changelog link. Binary provenance remains
# a build artifact and must not be appended by any release entry point.
RELEASE_SCRIPTS=( "Release|Release All: audit, commit, push and build with Actions|releases/release-all.sh|||"
+26 -1
View File
@@ -62,7 +62,10 @@ try {
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
const swPath = new URL('pwa-service-worker.js', Meteor.absoluteUrl()).pathname;
navigator.serviceWorker.register(swPath);
navigator.serviceWorker.register(swPath).catch(error => {
// No service worker (a private window, an old browser): the app works without the offline cache.
if (process.env.DEBUG === 'true') console.warn('service worker registration failed:', error);
});
});
}
@@ -111,6 +114,28 @@ Meteor.startup(() => {
}
});
// LOGOUT_IDLE_MINUTES: report the user's activity while signed in, if the
// limit is set (models/lib/idleLogout.js).
const { createIdleActivityReporter } = require('/client/lib/idleActivity');
const idleReporter = createIdleActivityReporter({
target: document,
touch: () => Meteor.call('touchActivity', () => {}),
});
let idleCheckedFor = null;
Tracker.autorun(() => {
const userId = Meteor.userId();
if (!userId) {
idleCheckedFor = null;
idleReporter.stop();
return;
}
if (idleCheckedFor === userId) return;
idleCheckedFor = userId;
Meteor.call('getIdleLogout', (error, result) => {
if (!error && result && result.enabled && Meteor.userId() === userId) idleReporter.start();
});
});
// Initialize mobile mode on startup for iOS devices
// This ensures mobile mode is applied correctly on page load
Tracker.afterFlush(() => {
+1 -1
View File
@@ -53,7 +53,7 @@
background-color: #fff;
border: 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.23);
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
height: 36px;
margin-block: 4px 6px;
margin-inline: 0 4px;
+2 -2
View File
@@ -15,7 +15,7 @@ template(name="commentForm")
a.js-cancel-reply(title="{{_ 'close' }}")
i.fa.fa-times-thin
form.js-new-comment-form
+editor(class="js-new-comment-input")
+editor(class="js-new-comment-input" labelKey="comment")
| {{getUnsavedValue 'cardComment' currentCard._id}}
.add-controls
button.primary.confirm.clear.js-add-comment(type="submit") {{_ 'comment'}}
@@ -40,7 +40,7 @@ template(name="comment")
= parentComment.text
+inlinedForm(classNames='js-edit-comment')
+editor(autofocus=true)
+editor(autofocus=true labelKey="comment")
= text
.edit-controls
button.primary(type="submit" title="{{_ 'edit'}}" aria-label="{{_ 'edit'}}")
+203 -97
View File
@@ -6,9 +6,9 @@
/* Ordinary solid-colour themes. */
.board-color-nephritis {
--board-theme-accent: #27ae60;
--board-theme-dark: #1f8b4d;
--board-theme-hover: #2cc66d;
--board-theme-accent: #217d49;
--board-theme-dark: #1c6c3f;
--board-theme-hover: #1f7645;
--board-theme-emphasis: #ae2775;
--board-theme-emphasis-hover: #9d2369;
--board-theme-emphasis-close: #8b1f5e;
@@ -36,7 +36,10 @@
}
.board-color-belize {
--board-theme-accent: #2980b9;
/* The default theme. The earlier blue #2980b9 gave the header's white text a ratio of 4.30 and
its #f2f2f2 button labels 3.84, below the 4.5 WCAG 2.1 AA (1.4.3) asks of
small text; this blue keeps the hue and gives 5.14 and 4.59. */
--board-theme-accent: #2573a7;
--board-theme-dark: #216694;
--board-theme-hover: #2e90d0;
--board-theme-emphasis: #b96229;
@@ -47,7 +50,7 @@
--board-theme-selected-hover-bg: #f8fbfd;
--board-theme-bright: #459cd6;
--board-theme-canvas-scrollbar: #2573a7f2;
--board-theme-sidebar-scrollbar: #2980b9f2;
--board-theme-sidebar-scrollbar: #2573a7f2;
}
.board-color-wisteria {
@@ -81,12 +84,12 @@
}
.board-color-pumpkin {
--board-theme-accent: #e67e22;
--board-theme-dark: #be6415;
--board-theme-hover: #e98b38;
--board-theme-emphasis: #228ae6;
--board-theme-emphasis-hover: #187dd5;
--board-theme-emphasis-close: #156fbe;
--board-theme-accent: #a75919;
--board-theme-dark: #904d15;
--board-theme-hover: #9d5417;
--board-theme-emphasis: #196ec2;
--board-theme-emphasis-hover: #1765b2;
--board-theme-emphasis-close: #155ca3;
--board-theme-checklist-bg: #f9e5d1;
--board-theme-selected-bg: #fdf2e9;
--board-theme-selected-hover-bg: #fefbf8;
@@ -96,12 +99,12 @@
}
.board-color-moderatepink {
--board-theme-accent: #cd5a91;
--board-theme-dark: #b53773;
--board-theme-hover: #d26b9c;
--board-theme-emphasis: #5acd96;
--board-theme-emphasis-hover: #43c688;
--board-theme-emphasis-close: #37b579;
--board-theme-accent: #bd3a79;
--board-theme-dark: #a33268;
--board-theme-hover: #b23772;
--board-theme-emphasis: #217b54;
--board-theme-emphasis-hover: #1e714d;
--board-theme-emphasis-close: #1c6747;
--board-theme-checklist-bg: #f4dde8;
--board-theme-selected-bg: #faeef4;
--board-theme-selected-hover-bg: #fefafc;
@@ -111,9 +114,9 @@
}
.board-color-strongcyan {
--board-theme-accent: #00aecc;
--board-theme-dark: #008ba3;
--board-theme-hover: #00c8eb;
--board-theme-accent: #00798d;
--board-theme-dark: #006879;
--board-theme-hover: #007285;
--board-theme-emphasis: #cc1e00;
--board-theme-emphasis-hover: #b81b00;
--board-theme-emphasis-close: #a31800;
@@ -126,12 +129,12 @@
}
.board-color-limegreen {
--board-theme-accent: #4bbf6b;
--board-theme-dark: #389d54;
--board-theme-hover: #5dc57a;
--board-theme-emphasis: #bf4b9f;
--board-theme-emphasis-hover: #b13f91;
--board-theme-emphasis-close: #9d3881;
--board-theme-accent: #277c48;
--board-theme-dark: #226b3e;
--board-theme-hover: #257544;
--board-theme-emphasis: #b34093;
--board-theme-emphasis-hover: #a53b87;
--board-theme-emphasis-close: #96367b;
--board-theme-checklist-bg: #daf4de;
--board-theme-selected-bg: #edf9f0;
--board-theme-selected-hover-bg: #fafdfb;
@@ -141,12 +144,12 @@
}
.board-color-corteza {
--board-theme-accent: #568ba2;
--board-theme-dark: #456f82;
--board-theme-hover: #6597ad;
--board-theme-emphasis: #a26d56;
--board-theme-emphasis-hover: #92624d;
--board-theme-emphasis-close: #825745;
--board-theme-accent: #4a738a;
--board-theme-dark: #406377;
--board-theme-hover: #466c82;
--board-theme-emphasis: #94614f;
--board-theme-emphasis-hover: #885949;
--board-theme-emphasis-close: #7c5142;
--board-theme-checklist-bg: #dce6ec;
--board-theme-selected-bg: #eef3f6;
--board-theme-selected-hover-bg: #fafcfc;
@@ -901,10 +904,10 @@ THEME - Relax
.board-list li.board-color-relax,
.board-color-relax .setting-content .content-body .side-menu ul li.active,
.board-color-relax .setting-content .content-body .side-menu ul li.active:hover {
background-color: #27ae61;
background-color: #217d4a;
}
.board-color-relax .is-selected .minicard {
border-inline-start: 3px solid #27ae61;
border-inline-start: 3px solid #217d4a;
}
.board-color-relax .minicard {
border-radius: 7px;
@@ -915,17 +918,17 @@ THEME - Relax
.board-color-relax input[type=submit].primary,
.board-color-relax .sidebar .sidebar-content .sidebar-btn,
.board-color-relax .selected-calendar-picker button.primary {
background-color: #1f8b4e;
background-color: #1c6c40;
border-radius: 7px;
}
.board-color-relax.pop-over .pop-over-list li a:not(.disabled):hover,
.board-color-relax .sidebar .sidebar-content .sidebar-btn:hover,
.board-color-relax .sidebar-list li a:hover {
background-color: #2cc66f;
background-color: #1f7646;
}
.board-color-relax#header ul li.current,
.board-color-relax#header-quick-access ul li.current {
border-bottom: 2px solid #2cc66f;
border-bottom: 2px solid #1f7646;
}
.board-color-relax#header-quick-access {
color: #fff;
@@ -941,14 +944,14 @@ THEME - Relax
background: #8b1f5d;
}
.board-color-relax .materialCheckBox.is-checked {
border-bottom: 2px solid #27ae61;
border-right: 2px solid #27ae61;
border-bottom: 2px solid #217d4a;
border-right: 2px solid #217d4a;
}
.board-color-relax .checklist-progress-bar {
background-color: #d4f1dd !important;
}
.board-color-relax .checklist-progress-bar .checklist-progress {
background-color: #27ae61 !important;
background-color: #217d4a !important;
}
.board-color-relax .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #e7faef;
@@ -957,17 +960,17 @@ THEME - Relax
background: #f8fdfa;
}
.board-color-relax .toggle-label:after {
background-color: #1f8b4e;
background-color: #1c6c40;
}
.board-color-relax .toggle-switch:checked ~ .toggle-label {
background-color: #3dd37e;
}
.board-color-relax .toggle-switch:checked ~ .toggle-label:after {
background-color: #1f8b4e;
background-color: #1c6c40;
}
@media screen and (max-width: 800px) {
.board-color-relax.pop-over .header {
background: #27ae61;
background: #217d4a;
color: #fff;
}
}
@@ -1554,7 +1557,7 @@ THEME - Clear Blue
.board-backgrounds-list .board-color-clearblue.background-box,
.public-board-row.board-color-clearblue,
.board-list li.board-color-clearblue {
background: linear-gradient(180deg, #499bea 0%, #00aecc 100%);
background: linear-gradient(180deg, #499bea 0%, #00798d 100%);
}
.board-color-clearblue .is-selected .minicard {
border-inline-start: 3px solid #499bea;
@@ -1566,7 +1569,7 @@ THEME - Clear Blue
}
.board-color-clearblue#header ul li.current,
.board-color-clearblue#header-quick-access ul li.current {
border-bottom: 4px solid #00c8eb;
border-bottom: 4px solid #007285;
}
.board-color-clearblue#header-quick-access {
color: #fff;
@@ -1585,14 +1588,14 @@ THEME - Clear Blue
.board-color-clearblue#header-quick-access,
.board-color-clearblue .setting-content .content-body .side-menu ul li.active,
.board-color-clearblue .setting-content .content-body .side-menu ul li.active:hover {
background-color: #00aecc;
border-bottom: 1px solid #008ba3;
background-color: #00798d;
border-bottom: 1px solid #006879;
border-top: 1px solid #00687a;
}
/* clearblue's bar is a colour SLIDE, and the slide has to be on the bar that is
actually on screen. It was only ever on `#header #header-main-bar` - the
inner element of the SECOND bar - because that is where the theme used to
live. The first bar got the flat `background-color: #00aecc` from the rule
live. The first bar got the flat `background-color: #00798d` from the rule
above instead, which is exactly strongcyan's colour: picking clearblue
painted the header strongcyan.
`#header-quick-access` has no inner bar of its own, so the slide goes
@@ -1601,7 +1604,7 @@ THEME - Clear Blue
The All Boards left menu's selected row is named here too. Every other theme
paints that row through `--theme-accent`, which is one colour and is exactly
right for them; a slide is not a colour, so the variable cannot carry it and
clearblue's row came out flat #00aecc while the Admin Panel's row beside it
clearblue's row came out flat #00798d while the Admin Panel's row beside it
slid. The Admin Panel's row has always been named in this rule; this is the
All Boards row joining it. docs/Features/General/Theme/Change-Color.md */
.board-color-clearblue#header #header-main-bar,
@@ -1610,13 +1613,13 @@ THEME - Clear Blue
.board-color-clearblue .setting-content .content-body .side-menu ul li.active:hover,
.board-color-clearblue .boards-left-menu .menu-item.active a,
.board-color-clearblue .boards-left-menu .menu-item.active a:hover {
background: linear-gradient(180deg, #499bea 0%, #00aecc 100%);
background: linear-gradient(180deg, #499bea 0%, #00798d 100%);
}
.board-color-clearblue#header #header-main-bar p {
margin-bottom: 6px;
}
.board-color-clearblue#header #header-main-bar .board-header-btn.emphasis {
background: #00c8eb;
background: #007285;
}
.board-color-clearblue#header #header-main-bar .board-header-btn.emphasis:hover,
.board-color-clearblue#header #header-main-bar .board-header-btn.emphasis .board-header-btn-close {
@@ -1648,7 +1651,7 @@ THEME - Clear Blue
background-color: #197ddc;
}
.board-color-clearblue.board-wrapper {
background: linear-gradient(135deg, #499bea 0%, #00aecc 100%);
background: linear-gradient(135deg, #499bea 0%, #00798d 100%);
}
.board-color-clearblue .swimlane {
background: none;
@@ -1740,7 +1743,7 @@ THEME - Clear Blue
}
.board-color-clearblue .quiet,
.board-color-clearblue .quiet a {
color: rgba(0,0,0,0.4);
color: rgba(0,0,0,0.68);
}
.board-color-clearblue .list-header .list-header-watch-icon {
color: rgba(0,0,0,0.5);
@@ -1767,7 +1770,7 @@ THEME - Clear Blue
}
.board-color-clearblue .sidebar-shadow {
box-shadow: none;
border-inline-start: 9px solid #00aecc;
border-inline-start: 9px solid #00798d;
}
.board-color-clearblue .is-open .sidebar-shadow {
box-shadow: -10px 0 8px rgba(0,0,0,0.3);
@@ -1833,7 +1836,7 @@ THEME - Clear Blue
/* Apply scrollbar to sidebar content*/
.board-color-clearblue .sidebar .sidebar-content {
scrollbar-color: #00aecc #ffffff00;
scrollbar-color: #00798d #ffffff00;
}
/* Remove margins in between columns/fix spacing */
@@ -1984,10 +1987,10 @@ THEME - Modern
.board-list li.board-color-modern,
.board-color-modern .setting-content .content-body .side-menu ul li.active,
.board-color-modern .setting-content .content-body .side-menu ul li.active:hover {
background-color: #2a80b8;
background-color: #2372a4;
}
.board-color-modern .is-selected .minicard {
border-inline-start: 3px solid #2a80b8;
border-inline-start: 3px solid #2372a4;
}
.board-color-modern .minicard {
border-radius: 7px;
@@ -1998,40 +2001,40 @@ THEME - Modern
.board-color-modern input[type=submit].primary,
.board-color-modern .sidebar .sidebar-content .sidebar-btn,
.board-color-modern .selected-calendar-picker button.primary {
background-color: #226693;
background-color: #1e628d;
border-radius: 7px;
}
.board-color-modern.pop-over .pop-over-list li a:not(.disabled):hover,
.board-color-modern .sidebar .sidebar-content .sidebar-btn:hover,
.board-color-modern .sidebar-list li a:hover {
background-color: #2f90cf;
background-color: #216b9a;
}
.board-color-modern#header ul li.current,
.board-color-modern#header-quick-access ul li.current {
border-bottom: 2px solid #2f90cf;
border-bottom: 2px solid #216b9a;
}
.board-color-modern#header-quick-access {
color: #fff;
}
.board-color-modern#header #header-main-bar .board-header-btn.emphasis {
background: #b8622a;
background: #a45b23;
}
.board-color-modern#header #header-main-bar .board-header-btn.emphasis:hover,
.board-color-modern#header #header-main-bar .board-header-btn.emphasis .board-header-btn-close {
background: #a65826;
background: #975420;
}
.board-color-modern#header #header-main-bar .board-header-btn.emphasis:hover .board-header-btn-close {
background: #934e22;
background: #8a4c1d;
}
.board-color-modern .materialCheckBox.is-checked {
border-bottom: 2px solid #2a80b8;
border-right: 2px solid #2a80b8;
border-bottom: 2px solid #2372a4;
border-right: 2px solid #2372a4;
}
.board-color-modern .checklist-progress-bar {
background-color: #d1e7f5 !important;
}
.board-color-modern .checklist-progress-bar .checklist-progress {
background-color: #2a80b8 !important;
background-color: #2372a4 !important;
}
.board-color-modern .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #e8f3fa;
@@ -2040,17 +2043,17 @@ THEME - Modern
background: #f8fbfd;
}
.board-color-modern .toggle-label:after {
background-color: #226693;
background-color: #1e628d;
}
.board-color-modern .toggle-switch:checked ~ .toggle-label {
background-color: #469cd5;
}
.board-color-modern .toggle-switch:checked ~ .toggle-label:after {
background-color: #226693;
background-color: #1e628d;
}
@media screen and (max-width: 800px) {
.board-color-modern.pop-over .header {
background: #2a80b8;
background: #2372a4;
color: #fff;
}
}
@@ -2379,7 +2382,7 @@ THEME - Modern Dark
.board-color-moderndark button[type=submit].primary,
.board-color-moderndark .board-color-modern input[type=submit].primary,
.board-color-moderndark .selected-calendar-picker button.primary {
background-color: #777;
background-color: #6e6e6e;
border-radius: 7px;
}
.board-color-moderndark .toggle-switch:checked~.toggle-label {
@@ -3012,7 +3015,15 @@ THEME - Exodark
.pop-over.board-color-exodark {
background: #2b2b2b;
color: #fff;
--wekan-quiet-text: #b4b4b4;
/* Card Settings opens in this popup: dark sections for its white text. */
--card-settings-section-bg: rgba(255, 255, 255, 0.08);
--card-settings-helper-bg: #3a3a46;
--card-settings-notch-bg: #2c3340;
}
/* The popup's own links keep the light popup's black otherwise: black on
#2b2b2b was 1.48:1 (#6863). */
.pop-over.board-color-exodark .header {
background: #2b2b2b;
color: #fff;
@@ -3791,7 +3802,7 @@ THEME - Clean Light
font-size: calc(16px * var(--wekan-ui-font-scale, 1));
font-weight: 400;
line-height: calc(24px * var(--wekan-ui-font-scale, 1));
color: rgba(10, 10, 20, 0.5);
color: rgba(10, 10, 20, 0.7);
}
.board-color-cleanlight .card-details-header,
@@ -4614,7 +4625,7 @@ THEME - Clean Light
/* ===============
THEME - cleargreen
A colour SLIDE, like clearblue: #8ad59f down to #4bbf6b.
A colour SLIDE, like clearblue: #8ad59f down to #277c48.
The base is limegreen's own colour, so the flat theme and the slide theme are
the same colour family and a reader picking either gets what they expected.
@@ -4631,7 +4642,7 @@ docs/Features/General/Theme/Change-Color.md
.board-backgrounds-list .board-color-cleargreen.background-box,
.public-board-row.board-color-cleargreen,
.board-list li.board-color-cleargreen {
background: linear-gradient(180deg, #8ad59f 0%, #4bbf6b 100%);
background: linear-gradient(180deg, #8ad59f 0%, #277c48 100%);
}
.board-color-cleargreen .is-selected .minicard {
border-inline-start: 3px solid #8ad59f;
@@ -4658,7 +4669,7 @@ docs/Features/General/Theme/Change-Color.md
.board-color-cleargreen#header-quick-access,
.board-color-cleargreen .setting-content .content-body .side-menu ul li.active,
.board-color-cleargreen .setting-content .content-body .side-menu ul li.active:hover {
background-color: #4bbf6b;
background-color: #277c48;
border-bottom: 1px solid #3c9956;
border-top: 1px solid #2d7340;
}
@@ -4669,7 +4680,7 @@ docs/Features/General/Theme/Change-Color.md
.board-color-cleargreen .setting-content .content-body .side-menu ul li.active:hover,
.board-color-cleargreen .boards-left-menu .menu-item.active a,
.board-color-cleargreen .boards-left-menu .menu-item.active a:hover {
background: linear-gradient(180deg, #8ad59f 0%, #4bbf6b 100%);
background: linear-gradient(180deg, #8ad59f 0%, #277c48 100%);
}
.board-color-cleargreen#header #header-main-bar p {
margin-bottom: 6px;
@@ -4707,7 +4718,7 @@ docs/Features/General/Theme/Change-Color.md
background-color: #42a85e;
}
.board-color-cleargreen.board-wrapper {
background: linear-gradient(135deg, #8ad59f 0%, #4bbf6b 100%);
background: linear-gradient(135deg, #8ad59f 0%, #277c48 100%);
}
.board-color-cleargreen .swimlane {
background: none;
@@ -4799,7 +4810,7 @@ docs/Features/General/Theme/Change-Color.md
}
.board-color-cleargreen .quiet,
.board-color-cleargreen .quiet a {
color: rgba(0,0,0,0.4);
color: rgba(0,0,0,0.68);
}
.board-color-cleargreen .list-header .list-header-watch-icon {
color: rgba(0,0,0,0.5);
@@ -4826,7 +4837,7 @@ docs/Features/General/Theme/Change-Color.md
}
.board-color-cleargreen .sidebar-shadow {
box-shadow: none;
border-inline-start: 9px solid #4bbf6b;
border-inline-start: 9px solid #277c48;
}
.board-color-cleargreen .is-open .sidebar-shadow {
box-shadow: -10px 0 8px rgba(0,0,0,0.3);
@@ -4891,7 +4902,7 @@ docs/Features/General/Theme/Change-Color.md
/* Apply scrollbar to sidebar content*/
.board-color-cleargreen .sidebar .sidebar-content {
scrollbar-color: #4bbf6b #ffffff00;
scrollbar-color: #277c48 #ffffff00;
}
/* Remove margins in between columns/fix spacing */
@@ -4910,7 +4921,7 @@ docs/Features/General/Theme/Change-Color.md
/* ===============
THEME - clearorange
A colour SLIDE, like clearblue: #efab6f down to #e67e22.
A colour SLIDE, like clearblue: #efab6f down to #a75919.
The base is pumpkin's own colour, so the flat theme and the slide theme are
the same colour family and a reader picking either gets what they expected.
@@ -4927,7 +4938,7 @@ docs/Features/General/Theme/Change-Color.md
.board-backgrounds-list .board-color-clearorange.background-box,
.public-board-row.board-color-clearorange,
.board-list li.board-color-clearorange {
background: linear-gradient(180deg, #efab6f 0%, #e67e22 100%);
background: linear-gradient(180deg, #efab6f 0%, #a75919 100%);
}
.board-color-clearorange .is-selected .minicard {
border-inline-start: 3px solid #efab6f;
@@ -4954,7 +4965,7 @@ docs/Features/General/Theme/Change-Color.md
.board-color-clearorange#header-quick-access,
.board-color-clearorange .setting-content .content-body .side-menu ul li.active,
.board-color-clearorange .setting-content .content-body .side-menu ul li.active:hover {
background-color: #e67e22;
background-color: #a75919;
border-bottom: 1px solid #b8651b;
border-top: 1px solid #8a4c14;
}
@@ -4965,7 +4976,7 @@ docs/Features/General/Theme/Change-Color.md
.board-color-clearorange .setting-content .content-body .side-menu ul li.active:hover,
.board-color-clearorange .boards-left-menu .menu-item.active a,
.board-color-clearorange .boards-left-menu .menu-item.active a:hover {
background: linear-gradient(180deg, #efab6f 0%, #e67e22 100%);
background: linear-gradient(180deg, #efab6f 0%, #a75919 100%);
}
.board-color-clearorange#header #header-main-bar p {
margin-bottom: 6px;
@@ -5003,7 +5014,7 @@ docs/Features/General/Theme/Change-Color.md
background-color: #ca6f1e;
}
.board-color-clearorange.board-wrapper {
background: linear-gradient(135deg, #efab6f 0%, #e67e22 100%);
background: linear-gradient(135deg, #efab6f 0%, #a75919 100%);
}
.board-color-clearorange .swimlane {
background: none;
@@ -5095,7 +5106,7 @@ docs/Features/General/Theme/Change-Color.md
}
.board-color-clearorange .quiet,
.board-color-clearorange .quiet a {
color: rgba(0,0,0,0.4);
color: rgba(0,0,0,0.68);
}
.board-color-clearorange .list-header .list-header-watch-icon {
color: rgba(0,0,0,0.5);
@@ -5122,7 +5133,7 @@ docs/Features/General/Theme/Change-Color.md
}
.board-color-clearorange .sidebar-shadow {
box-shadow: none;
border-inline-start: 9px solid #e67e22;
border-inline-start: 9px solid #a75919;
}
.board-color-clearorange .is-open .sidebar-shadow {
box-shadow: -10px 0 8px rgba(0,0,0,0.3);
@@ -5187,7 +5198,7 @@ docs/Features/General/Theme/Change-Color.md
/* Apply scrollbar to sidebar content*/
.board-color-clearorange .sidebar .sidebar-content {
scrollbar-color: #e67e22 #ffffff00;
scrollbar-color: #a75919 #ffffff00;
}
/* Remove margins in between columns/fix spacing */
@@ -5206,7 +5217,7 @@ docs/Features/General/Theme/Change-Color.md
/* ===============
THEME - clearpink
A colour SLIDE, like clearblue: #df94b8 down to #cd5a91.
A colour SLIDE, like clearblue: #df94b8 down to #bd3a79.
The base is moderatepink's own colour, so the flat theme and the slide theme are
the same colour family and a reader picking either gets what they expected.
@@ -5223,7 +5234,7 @@ docs/Features/General/Theme/Change-Color.md
.board-backgrounds-list .board-color-clearpink.background-box,
.public-board-row.board-color-clearpink,
.board-list li.board-color-clearpink {
background: linear-gradient(180deg, #df94b8 0%, #cd5a91 100%);
background: linear-gradient(180deg, #df94b8 0%, #bd3a79 100%);
}
.board-color-clearpink .is-selected .minicard {
border-inline-start: 3px solid #df94b8;
@@ -5250,7 +5261,7 @@ docs/Features/General/Theme/Change-Color.md
.board-color-clearpink#header-quick-access,
.board-color-clearpink .setting-content .content-body .side-menu ul li.active,
.board-color-clearpink .setting-content .content-body .side-menu ul li.active:hover {
background-color: #cd5a91;
background-color: #bd3a79;
border-bottom: 1px solid #a44874;
border-top: 1px solid #7b3657;
}
@@ -5261,7 +5272,7 @@ docs/Features/General/Theme/Change-Color.md
.board-color-clearpink .setting-content .content-body .side-menu ul li.active:hover,
.board-color-clearpink .boards-left-menu .menu-item.active a,
.board-color-clearpink .boards-left-menu .menu-item.active a:hover {
background: linear-gradient(180deg, #df94b8 0%, #cd5a91 100%);
background: linear-gradient(180deg, #df94b8 0%, #bd3a79 100%);
}
.board-color-clearpink#header #header-main-bar p {
margin-bottom: 6px;
@@ -5299,7 +5310,7 @@ docs/Features/General/Theme/Change-Color.md
background-color: #b44f80;
}
.board-color-clearpink.board-wrapper {
background: linear-gradient(135deg, #df94b8 0%, #cd5a91 100%);
background: linear-gradient(135deg, #df94b8 0%, #bd3a79 100%);
}
.board-color-clearpink .swimlane {
background: none;
@@ -5391,7 +5402,7 @@ docs/Features/General/Theme/Change-Color.md
}
.board-color-clearpink .quiet,
.board-color-clearpink .quiet a {
color: rgba(0,0,0,0.4);
color: rgba(0,0,0,0.68);
}
.board-color-clearpink .list-header .list-header-watch-icon {
color: rgba(0,0,0,0.5);
@@ -5418,7 +5429,7 @@ docs/Features/General/Theme/Change-Color.md
}
.board-color-clearpink .sidebar-shadow {
box-shadow: none;
border-inline-start: 9px solid #cd5a91;
border-inline-start: 9px solid #bd3a79;
}
.board-color-clearpink .is-open .sidebar-shadow {
box-shadow: -10px 0 8px rgba(0,0,0,0.3);
@@ -5483,7 +5494,7 @@ docs/Features/General/Theme/Change-Color.md
/* Apply scrollbar to sidebar content*/
.board-color-clearpink .sidebar .sidebar-content {
scrollbar-color: #cd5a91 #ffffff00;
scrollbar-color: #bd3a79 #ffffff00;
}
/* Remove margins in between columns/fix spacing */
@@ -5687,7 +5698,7 @@ docs/Features/General/Theme/Change-Color.md
}
.board-color-clearpurple .quiet,
.board-color-clearpurple .quiet a {
color: rgba(0,0,0,0.4);
color: rgba(0,0,0,0.68);
}
.board-color-clearpurple .list-header .list-header-watch-icon {
color: rgba(0,0,0,0.5);
@@ -5983,7 +5994,7 @@ docs/Features/General/Theme/Change-Color.md
}
.board-color-clearred .quiet,
.board-color-clearred .quiet a {
color: rgba(0,0,0,0.4);
color: rgba(0,0,0,0.68);
}
.board-color-clearred .list-header .list-header-watch-icon {
color: rgba(0,0,0,0.5);
@@ -6090,3 +6101,98 @@ docs/Features/General/Theme/Change-Color.md
}
/* === END clearred THEME === */
/* The quiet text colour (#595959 on the light themes' greys, WCAG 1.4.3) is a
variable, so the themes whose popups and sidebars are DARK can have a light
one: a dark grey on a dark surface was nearly invisible (#6863). cleandark and
moderndark are those; dark and exodark keep light popups and sidebars, where
the dark grey is right. Popups carry the theme class themselves, so the
variable reaches them. */
:is(
.board-color-cleandark,
.board-color-moderndark
) {
--wekan-quiet-text: #b4b4b4;
/* Card Settings: its sections are light grey blocks (cardAddFields.css), and
these themes' text on them is white (1.07:1 in cleandark). */
--card-settings-section-bg: rgba(255, 255, 255, 0.08);
--card-settings-helper-bg: #3a3a46;
--card-settings-notch-bg: #2c3340;
}
/* moderndark's surfaces are lighter greys (#545454): the quiet text needs more. */
.board-color-moderndark {
--wekan-quiet-text: #dcdcdc;
}
/* The board sidebar's "Keyboard shortcuts" row is outside .sidebar-content, so
the white of its buttons does not reach it, and sidebar.css makes it black:
black on these sidebars was 1.34:1. */
:is(
.board-color-cleandark,
.board-color-moderndark
) .sidebar .sidebar-shortcuts {
color: #fff;
}
/* moderndark: grey (#808080) on its near-black sidebar was 4.02. */
.board-color-moderndark :is(.js-open-board-menu, h3[data-fold], .activity-title) {
color: var(--wekan-quiet-text);
}
/* dark: the swimlane header's text was black on its #2c3e50 bar (1.91:1). */
.board-color-dark .swimlane-header {
color: #fff;
}
/* dark and exodark: the board, the opened card and the sidebar are dark surfaces
(#222, #2b2b2b, #2c3e50). Their popups differ: exodark's is dark too (above),
dark's is light, so the dark theme's popups keep the dark quiet text. */
:is(
.board-color-dark,
.board-color-exodark
) :is(.board-canvas, .card-details, .sidebar) {
--wekan-quiet-text: #b4b4b4;
}
/* A "quiet button" (Add custom field, Link fields to another card, ...) is #222 in
forms.css, for a light surface; on a dark popup or sidebar it was 1.18:1 (#6863). */
:is(
.board-color-cleandark,
.board-color-moderndark
) .quiet-button,
:is(
.board-color-cleandark,
.board-color-moderndark,
.board-color-exodark
).pop-over .quiet-button {
color: #e0e0e0;
}
/* The board sidebar of the dark themes: what is not a button or a heading
(a form label, the list of swimlanes' lists and cards) kept the light
sidebar's black: 1.34:1 on cleandark's #23232b (#6863). */
:is(
.board-color-cleandark,
.board-color-moderndark
) .sidebar .sidebar-content :is(label, .minilists, .minicards) {
color: #fff;
}
/* Popup items and sidebar lists: popup.css forces `.pop-over-list li > a` to black
with !important, which is right on a light surface. moderndark's popups and
sidebar and exodark's popups are dark: black on #454545 was 2.19:1, on #2b2b2b
1.48:1 (#6863). */
:is(
.board-color-moderndark,
.board-color-exodark
).pop-over .pop-over-list li > a,
:is(
.board-color-moderndark,
.board-color-exodark
).pop-over .pop-over-list li > a *,
.board-color-moderndark .sidebar .pop-over-list li > a,
.board-color-moderndark .sidebar .pop-over-list li > a * {
color: #fff !important;
}
/* moderndark: the sidebar's headings were #808080 on #222 (4.02:1). */
.board-color-moderndark .sidebar .sidebar-content h3 {
color: var(--wekan-quiet-text);
}
/* moderndark: the selected minicard's title was #ccc on #666 (3.57:1). */
.board-color-moderndark .is-selected > .minicard .minicard-title,
.board-color-moderndark .is-selected > .minicard .minicard-title * {
color: #fff;
}
+7 -7
View File
@@ -173,7 +173,7 @@
crossed it - which is the bug that had to be fixed in the Admin Panel's. */
.boards-left-menu .menu-item.active a,
.boards-left-menu .menu-item.active a:hover {
background: var(--theme-accent, #2980b9);
background: var(--theme-accent, #2573a7);
}
/* The Admin Panel puts the selected background on the LI, not the anchor, so
the fill reaches the row's full width. Here the anchor is what fills, so it
@@ -215,7 +215,7 @@
docs/Features/Pages/Starred.md */
.board-list .board-list-item-bookmark .board-list-item {
border: 4px solid #fff;
background: var(--theme-accent, #2980b9);
background: var(--theme-accent, #2573a7);
color: #fff;
/* Room at the end for the unstar button, which sits over the tile. */
padding-inline-end: 34px;
@@ -481,7 +481,7 @@
content: '';
display: block;
height: 30px;
border: 2px dashed var(--theme-accent, #2980b9);
border: 2px dashed var(--theme-accent, #2573a7);
border-radius: 4px;
background: rgba(41, 128, 185, 0.08);
margin: 2px 0;
@@ -694,7 +694,7 @@
as the pointer crossed it. */
.workspace-node.active > .workspace-node-content .js-select-space,
.workspace-node.active > .workspace-node-content:hover .js-select-space {
background: var(--theme-accent, #2980b9);
background: var(--theme-accent, #2573a7);
}
.workspace-node.active > .workspace-node-content .js-select-space,
.workspace-node.active > .workspace-node-content .js-select-space i {
@@ -1046,7 +1046,7 @@
display: flex;
align-items: center;
justify-content: center;
background-color: #999; /* Darker background for better text contrast */
background-color: #767676; /* Grey with white text: 4.54, above the 4.5 of WCAG 1.4.3 (#999 gave 2.84) */
border-radius: 3px;
/* 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
@@ -1661,7 +1661,7 @@
.js-board {
display: block;
background-color: #999; /* Default gray background if no color class is applied */
background-color: #767676; /* Default gray background if no color class is applied */
border-radius: 3px; /* Rounded corners for board items */
overflow: hidden; /* Ensure children respect rounded corners */
margin: 0; /* grid gap spaces the tiles now (#6523) */
@@ -1724,7 +1724,7 @@
chose, not a colour, and painting over it would be losing something rather
than calming it down. */
body.has-theme-board-tiles .board-list > li.js-board {
background: var(--theme-accent-fill, var(--theme-accent, #2980b9));
background: var(--theme-accent-fill, var(--theme-accent, #2573a7));
}
body.has-theme-board-tiles .board-list > li.js-board .board-list-item:not(.has-background-image) {
background-color: transparent;
+10 -3
View File
@@ -1,6 +1,9 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { ReactiveVar } from 'meteor/reactive-var';
import { Utils } from '/client/lib/utils';
import { ensureContrast } from '/models/lib/contrastColor';
import { surfacesFor } from '/models/lib/uiFonts';
import { activeThemeName } from '/client/lib/activeTheme';
import {
DEPENDENCY_ICON_CHOICES,
DEPENDENCY_TYPES,
@@ -138,13 +141,17 @@ Template.dependencyOverlay.onCreated(function () {
const y2 = dependent.y + dependent.h / 2;
const dx = Math.max(40, Math.abs(x2 - x1) / 2);
const path = `M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`;
const markerId = markerIdForColor(dep.color);
// The colour is the member's free choice (a colour wheel), drawn over the
// board: made visible against the theme's surface - a line is a graphic
// that says something, 3:1 (WCAG 1.4.11). The stored colour is untouched.
const drawColor = ensureContrast(dep.color, surfacesFor(activeThemeName()), 3);
const markerId = markerIdForColor(drawColor);
if (meta.directed) {
markerColors.add(dep.color);
markerColors.add(drawColor);
}
lines.push({
path,
color: dep.color,
color: drawColor,
type: dep.type,
icon: dep.icon,
directed: meta.directed,
+2 -2
View File
@@ -23,9 +23,9 @@
* so the row keeps the table's white background: with the light text below that
* is white on white, and only the row's emoji was visible. The All Boards tile
* has the same problem and solves it the same way — `.board-list-item` falls
* back to a grey (#999) so its white text always has something to sit on. */
* back to a grey (#767676) so its white text always has something to sit on. */
.public-board-row {
background-color: #999;
background-color: #767676;
background-size: cover;
background-position: center;
cursor: pointer;
@@ -52,8 +52,14 @@ const boardFor = context => ReactiveCache.getBoard(context?.record?.boardId || S
// `field` (a definition suffix, 'Sprint') draws one field alone: on a card and
// a minicard each Scrum field sits where the board's Board Settings / Card
// order puts it (models/lib/cardFieldOrder.js SCRUM_FIELD_KEYS).
// Read through the board as that side sees it (models/lib/cardShownFields.js
// boardForSide): a Scrum section hidden whole on Show on Card or Show on
// Minicard in Card Settings hides these fields there too. The settings above
// (scrumFieldSettings) read the board itself.
function visibleFields(context) {
const board = boardFor(context);
const { boardForSide } = require('/models/lib/cardShownFields');
const side = context?.kind === 'card' || context?.kind === 'minicard' ? context.kind : null;
const board = side ? boardForSide(boardFor(context), side) : boardFor(context);
return (definitions[context?.kind] || []).filter(([suffix]) => (!context.field || context.field === suffix) &&
board?.scrum?.visibility?.[`${context.kind}${suffix}`] === true);
}
+1 -1
View File
@@ -87,7 +87,7 @@ template(name="attachmentGallery")
.attachment-gallery
//- #6796: a small "+" like every other section's, not a full-width box.
if canModifyCard
if canAddAttachment
a.card-label.add-label.add-attachment.js-add-attachment(title="{{_ 'add-attachment'}}" aria-label="{{_ 'add-attachment'}}")
i.fa.fa-plus
+10 -2
View File
@@ -16,6 +16,7 @@ import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManag
import Attachments from '/models/attachments';
import { Utils } from '/client/lib/utils';
import { buildAttachmentUploadConfig } from '/client/lib/attachmentUploadConfig';
import { attachmentUploadsBlocked } from '/client/lib/attachmentUploadPolicy';
const { cleanFileName } = require('/imports/lib/fileNameDisplay');
const { attachmentTileCompact } = require('/models/lib/attachmentTile.cjs');
import { formatDateTime } from '/imports/lib/dateUtils';
@@ -338,8 +339,8 @@ function openPrevAttachment() {
function processTouch(){
xDist = touchEndCoords.x - touchStartCoords.x;
yDist = touchEndCoords.y - touchStartCoords.y;
const xDist = touchEndCoords.x - touchStartCoords.x;
const yDist = touchEndCoords.y - touchStartCoords.y;
// Left swipe
if (Math.abs(xDist) > Math.abs(yDist) && xDist < 0) {
@@ -467,6 +468,10 @@ Template.attachmentGallery.onCreated(function () {
});
Template.attachmentGallery.helpers({
// #6870: the "+" button - a card the user may edit, unless uploads are blocked site-wide
canAddAttachment() {
return Utils.canModifyCard() && !attachmentUploadsBlocked();
},
// #6796: the small file button instead of the preview tile
// (models/lib/attachmentTile.cjs).
attachmentTileCompact(file) {
@@ -672,6 +677,9 @@ export async function handleFileUpload(card, files) {
return [];
}
// #6870: the administrator blocked uploads for the whole site; the server refuses them too
if (attachmentUploadsBlocked()) return [];
// Check if board allows attachments
const board = card.board();
if (!board || !board.allowsAttachments) {
+285 -5
View File
@@ -1,5 +1,285 @@
/* "+ Add" on the opened card (docs/Features/Cards/Card-Fields.md): it opens
Board Settings / Card for this card (sidebar.css .card-settings-sections). */
.card-details-add-fields { padding: 4px 0 2px; }
.card-details-add-fields-button { display: inline-flex; align-items: center; gap: 0.3em; padding: 2px 8px; border-radius: 3px; }
.card-details-add-fields-button:hover { background: rgba(0, 0, 0, 0.06); }
/* Card Settings (Board Settings / Card, docs/Features/Cards/Card-Settings.md):
"Show" and its two choices, then the sections drawn like checklists. */
.card-settings-inline { padding: 4px 0 8px; }
/* Two panes, Minicard first (the start: left in a left-to-right page, right in
a right-to-left one) and Card after it (sidebar.jade boardCardSettingsPopup).
STACKED by default - Minicard above Card - and the container scrolls as one;
SIDE BY SIDE once the popup is wide enough for two panes of about 20em,
and then each pane's body scrolls on its own while the container does not.
A collapsed pane is its header only, and the other pane takes the width.
The popup's content wrapper does not scroll here (popup.css), so the
container is the one scroller in the stacked layout. */
.card-settings-panes-container {
container-type: inline-size;
container-name: card-settings-panes;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
.card-settings-panes {
display: flex;
flex-direction: column;
flex: 0 0 auto;
gap: 8px;
}
.card-settings-pane { min-width: 0; }
.card-settings-pane-head {
display: flex;
align-items: center;
gap: 6px;
padding-block: 4px 6px;
border-block-end: 1px solid #ddd;
margin-block-end: 8px;
}
.card-settings-pane-collapse {
padding: 2px 4px;
color: var(--wekan-quiet-text, #595959);
text-decoration: none;
cursor: pointer;
}
.card-settings-pane-collapse:hover,
.card-settings-pane-collapse:focus-visible { color: #2573a7; }
.card-settings-pane-title {
margin: 0;
font-size: calc(17px * var(--wekan-ui-font-scale, 1));
font-weight: bold;
}
.card-settings-pane-body[hidden] { display: none !important; }
/* The splitter: only side by side with neither pane collapsed (the container
query below shows it). A thin line with a wider grab area, the Admin Panel
left menu's grip (settingBody.css .left-menu-resize-handle) in look. */
.card-settings-splitter {
display: none;
flex: 0 0 10px;
align-self: stretch;
cursor: col-resize;
touch-action: none;
position: relative;
border-radius: 3px;
transition: background-color 0.2s ease;
}
.card-settings-splitter::before {
content: '';
position: absolute;
inset-block: 0;
inset-inline-start: 4px;
width: 2px;
background: #ddd;
}
.card-settings-splitter:hover,
.card-settings-splitter:focus-visible {
background: rgba(0, 123, 255, 0.4);
outline: none;
}
.card-settings-splitter:active { background: rgba(0, 123, 255, 0.6); }
body.card-settings-splitting-active {
user-select: none;
cursor: col-resize;
}
.card-settings-pane.is-pane-collapsed .card-settings-pane-head { margin-block-end: 0; }
@container card-settings-panes (min-width: 40em) {
.card-settings-panes {
flex-direction: row;
align-items: stretch;
flex: 1 1 auto;
min-height: 0;
gap: 0;
}
.card-settings-pane {
flex: 1 1 0;
display: flex;
flex-direction: column;
min-height: 0;
}
/* The Minicard pane takes its share of the width (the splitter between
them, models/lib/cardSettingsSplit.js); the Card pane takes the rest. */
.card-settings-panes:not(.has-collapsed-pane) > .card-settings-pane-minicard {
flex: 0 0 calc((100% - 10px) * var(--card-settings-split, 0.5));
}
.card-settings-pane.is-pane-collapsed { flex: 0 0 auto; }
.card-settings-panes:not(.has-collapsed-pane) > .card-settings-splitter { display: block; }
.card-settings-pane-body {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding-inline-end: 4px;
}
}
/* A section: the checklist's grey block, its title line and its items. */
.card-settings-inline-section {
/* The checklist's grey block; a dark theme has its own (#6863: white text on
this light grey was 1.07:1 in cleandark). */
background: var(--card-settings-section-bg, #f7f7f7);
border-radius: 3px;
margin-block-end: 8px;
padding: 4px 8px 6px;
}
.card-settings-inline-section.ui-sortable-helper {
box-shadow: -2px 2px 8px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.5);
cursor: grabbing;
}
.card-settings-inline-section-head {
display: flex;
align-items: center;
gap: 6px;
padding-block: 4px;
}
.card-settings-inline-section-title {
flex: 1 1 auto;
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0;
font-size: calc(16px * var(--wekan-ui-font-scale, 1));
min-width: 0;
overflow-wrap: anywhere;
}
.card-settings-inline-section-title.js-card-settings-section-handle { cursor: grab; }
.card-settings-inline-section-count { color: var(--wekan-quiet-text, #595959); white-space: nowrap; }
.card-settings-inline-collapse,
.card-settings-inline-section-move,
.card-settings-inline-menu {
padding: 2px 4px;
color: var(--wekan-quiet-text, #595959);
text-decoration: none;
cursor: pointer;
}
.card-settings-inline-collapse:hover,
.card-settings-inline-collapse:focus-visible,
.card-settings-inline-menu:hover,
.card-settings-inline-menu:focus-visible { color: #2573a7; }
/* The section's own checkbox, left of its title: the rows' look. A section
hidden whole on this side keeps its rows' ticks, drawn faded. */
.card-settings-section-side { flex: 0 0 auto; }
.card-settings-section-side > a.flex { margin: 0; }
.card-settings-inline-section.is-section-hidden .card-settings-inline-items { opacity: 0.55; }
/* One row on this side: one line, no caret - its checkbox where the other
sections' checkboxes are. */
.card-settings-inline-section.is-single .card-settings-inline-section-head {
padding-inline-start: calc(0.5em + 8px);
}
/* "+" under Custom Fields' items: a new custom field. */
.card-settings-add-custom-field { padding: 2px 0 0 22px; }
.card-settings-add-custom-field-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
min-height: 28px;
border-radius: 3px;
color: var(--wekan-quiet-text, #595959);
text-decoration: none;
}
.card-settings-add-custom-field-button:hover,
.card-settings-add-custom-field-button:focus-visible { background: rgba(0, 0, 0, 0.06); color: #2573a7; }
/* Collapsed: the items hidden, not removed, so their drag stays bound. */
.card-settings-inline-section.is-collapsed .card-settings-inline-items,
.card-settings-inline-section.is-collapsed .card-settings-add-custom-field { display: none; }
/* An item: the checkbox, the name, and its hamburger at the end
(sidebar.css .card-settings-line is the line itself). */
.card-settings-inline .card-settings-line {
grid-template-columns: auto minmax(0, 1fr) auto;
/* A space's width between the checkbox and the name, as in text. */
column-gap: 0.3em;
border-bottom: 0;
padding-inline-start: 22px;
}
.card-settings-inline .card-settings-line.ui-sortable-helper { background: var(--card-settings-helper-bg, #fff); }
.card-settings-inline .materialCheckBox { transition: none; }
.card-settings-inline .card-field-order-toggle.is-animating .materialCheckBox { transition: 0.2s; }
/* The hamburgers' checkboxes, likewise: only the one just clicked animates. */
.card-settings-section-menu .materialCheckBox,
.card-settings-item-menu .materialCheckBox { transition: none; }
.card-settings-section-menu .is-animating .materialCheckBox,
.card-settings-item-menu .is-animating .materialCheckBox { transition: 0.2s; }
/* The hamburgers' popups. */
.card-settings-item-menu,
.card-settings-section-menu { min-width: 16em; }
/* The hamburgers' checkboxes: a space's width before their label, as on
the rows. */
.card-settings-item-menu .card-settings-board-option > a.flex,
.card-settings-section-menu .card-settings-board-option > a.flex {
align-items: center;
column-gap: 0.3em;
}
/* A menu's options start at its edge, like its table: not .check-div's
indent for a form's checkboxes. */
.card-settings-item-menu .card-settings-board-option,
.card-settings-section-menu .card-settings-board-option {
margin-inline-start: 0;
}
/* A tick is drawn rotated (forms.css #6786) and reaches about 3px past its
box toward the label, which ate the space: drawn 3px further left, it keeps
it - the box, and so the label, do not move when it is ticked. Right to
left the label is on the other side of the box and the tick leans away
from it, so it stays as it is. */
:root:not([dir="rtl"]) .card-settings-inline .materialCheckBox.is-checked,
:root:not([dir="rtl"]) .card-settings-item-menu .materialCheckBox.is-checked,
:root:not([dir="rtl"]) .card-settings-section-menu .materialCheckBox.is-checked {
left: -6px;
}
/* A hamburger's panel (sidebar.jade cardSettingsInlinePanel): what the
hamburger holds, opened UNDER its line inside the pane, pushing the rest
down - not a popup. The open hamburger is tinted, and the panel hangs from
it in the same tint, with a notch pointing up at the line it belongs to. */
.card-settings-inline-menu.is-open,
.card-settings-add-custom-field-button.is-open {
background: rgba(41, 128, 185, 0.15);
color: #2573a7;
border-radius: 3px;
}
.card-settings-inline-panel {
position: relative;
margin-block: 6px 8px;
padding: 10px 12px 8px;
border: 1px solid rgba(41, 128, 185, 0.45);
border-radius: 4px;
background: rgba(41, 128, 185, 0.06);
/* Its own block, the width of what it hangs from. */
flex: 0 0 100%;
box-sizing: border-box;
}
.card-settings-inline-panel::before {
content: '';
position: absolute;
inset-block-start: -7px;
inset-inline-end: 10px;
width: 12px;
height: 12px;
background: inherit;
border-inline-start: 1px solid rgba(41, 128, 185, 0.45);
border-block-start: 1px solid rgba(41, 128, 185, 0.45);
transform: rotate(45deg);
background-color: var(--card-settings-notch-bg, #eef4f9);
}
/* The + is at the start of its line: the notch is under it there. */
.card-settings-inline-panel-create::before {
inset-inline-end: auto;
inset-inline-start: 6px;
}
.card-settings-inline-panel-close {
/* Above the notch, which is drawn at the same end of the panel's top. */
z-index: 1;
position: absolute;
inset-block-start: 4px;
inset-inline-end: 6px;
color: var(--wekan-quiet-text, #595959);
padding: 2px 4px;
}
.card-settings-inline-panel-close:hover,
.card-settings-inline-panel-close:focus-visible { color: #2573a7; }
.card-settings-section-action-title {
display: flex;
align-items: center;
gap: 6px;
margin: 0 0 6px;
}
+16 -79
View File
@@ -1,69 +1,29 @@
import './cardDetails.jade';
import { ReactiveCache } from '/imports/reactiveCache';
import { Utils } from '/client/lib/utils';
// docs/Features/Cards/Card-Fields.md: an opened card shows only the fields it
// has data in - and what its board always shows (by default its description,
// attachments and comments), empty or not - and "+ Add" chooses the rest. The rules are models/lib/cardShownFields.js.
// docs/Features/Cards/Card-Fields.md: an opened card shows every field its
// board shows on cards, empty or not, less the Card Settings sections the
// board hides on Show on Card. The rules are models/lib/cardShownFields.js.
const f = require('/models/lib/cardShownFields');
// What lives in other collections, for "has data".
function countOf(list) {
if (!list) return 0;
if (typeof list.length === 'number') return list.length;
return typeof list.count === 'function' ? list.count() : 0;
}
export function cardFieldCounts(card) {
if (!card || !card._id) return {};
return {
checklists: typeof card.checklists === 'function' ? countOf(card.checklists()) : 0,
subtasks: typeof card.subtasks === 'function' ? countOf(card.subtasks()) : 0,
textNotes: countOf(ReactiveCache.getCardTextNotes({ cardId: card._id })),
// For a board that does not always show these: shown when there are some.
attachments: typeof card.attachments === 'function' ? countOf(card.attachments()) : 0,
comments: typeof card.comments === 'function' ? countOf(card.comments()) : 0,
};
}
// The card's document, with its methods, for a template's data.
function cardOf(data) {
if (!data || !data._id) return null;
return ReactiveCache.getCard(data._id) || data;
}
// What decides which fields `card` shows: its DATA, which a linked card keeps
// on its source card (members, labels, dates, custom fields, checklists ...),
// and its own choices (shownFields / hiddenFields, set on the card opened).
// Reading the linked card's own empty document hid every field its source
// has (the #1946/#6613 browser test). Returns { card, counts }.
export function shownFieldsOf(card) {
if (!card) return { card: null, counts: {} };
const real = typeof card.getRealCard === 'function' ? (card.getRealCard() || card) : card;
if (real === card) return { card, counts: cardFieldCounts(card) };
return {
card: { ...real, shownFields: card.shownFields, hiddenFields: card.hiddenFields },
counts: cardFieldCounts(real),
};
}
// The board as this card sees it (cardShownFields.js boardForCard).
export function cardBoardView(data) {
const board = Utils.getCurrentBoard();
const card = cardOf(data);
if (!board || !card || card.boardId !== board._id) return board;
const view = shownFieldsOf(card);
return f.boardForCard(board, view.card, view.counts);
// The board as its opened cards see it (cardShownFields.js boardForCard): the
// same for every card of the board, empty or not, so `data` (the card) does
// not change it - it is kept so the callers read as before.
export function cardBoardView(data) { // eslint-disable-line no-unused-vars
return f.boardForCard(Utils.getCurrentBoard());
}
// The opened card and each of its section templates read `currentBoard`; this
// template-level helper answers before the global one, so every
// `currentBoard.allowsX` in them asks this card's view - and a template not
// listed here (the minicard, the board) is unchanged.
// `currentBoard.allowsX` in them asks the card's view - and a template not
// listed here (the board, Card Settings itself) is unchanged. The minicard has
// its own (client/components/cards/minicard.js MINICARD_FIELD_TEMPLATES).
export const CARD_FIELD_TEMPLATES = Object.freeze([
'cardDetails', 'cardFieldSectionLabels', 'cardFieldSectionDates', 'cardFieldSectionMembers',
'cardFieldSectionDependencies', 'cardFieldSectionSort', 'cardFieldSectionCustomFields',
'cardFieldSectionVoteAndPoker', 'cardFieldSectionDescription', 'cardCommentsSection',
// One per Card Settings section the opened card draws (cardDetails.jade).
'cardDetails', 'cardFieldSectionLabels', 'cardFieldSectionDate', 'cardFieldSectionMembers',
'cardFieldSectionCard', 'cardFieldSectionTime', 'cardFieldSectionCustomFields',
'cardFieldSectionVote', 'cardFieldSectionPoker', 'cardFieldSectionDescription', 'cardFieldSectionScrum',
'cardFieldSectionChecklists', 'cardFieldSectionSubtasks', 'cardFieldSectionAttachments', 'cardCommentsSection',
]);
CARD_FIELD_TEMPLATES.forEach(name => {
Template[name].helpers({
@@ -73,26 +33,3 @@ CARD_FIELD_TEMPLATES.forEach(name => {
},
});
});
// Whether this card shows the field `key` - for a section with no board flag
// of its own to read (the Scrum fields, cardDetails.js orderedCardFieldSections).
export function cardShowsField(data, key) {
const card = cardOf(data);
if (!card) return true;
const view = shownFieldsOf(card);
return f.isShown(key, view.card, view.counts, Utils.getCurrentBoard());
}
// "+ Add" opens Board Settings / Card - the same popup - for this card: its
// sections, each row's "Show on Minicard" and "Show on Card", and whether this
// card shows the field (client/components/sidebar/sidebar.js). Overriding
// `currentData` is what reaches the popup's data context (as Board Settings'
// own opener does).
Template.cardDetails.events({
'click .js-open-card-add-fields'(event) {
const card = cardOf(Template.currentData());
if (!card) return;
const data = { card };
Popup.open('boardCardSettings').call({ currentData: () => data }, event);
},
});
+1 -1
View File
@@ -38,7 +38,7 @@
background-color: #fff;
border: 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.23);
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
height: 36px;
margin: 4px 4px 6px 0;
width: 92%;
+1 -1
View File
@@ -3,5 +3,5 @@ template(name="descriptionForm")
.new-description.js-new-description(
class="{{#if descriptionFormIsOpen}}is-open{{/if}}")
form.js-new-description-form
+editor(class="js-new-description-input" autofocus="autofocus")
+editor(class="js-new-description-input" autofocus="autofocus" labelKey="description")
| {{getUnsavedValue 'cardDescription' _id getDescription}}
+120 -45
View File
@@ -185,6 +185,23 @@ body.desktop-mode .card-details:not(.card-details-popup):not(.card-details-colla
bottom: 8px;
}
/* A phone in desktop mode: the header bar wraps to several rows (up to five with
the board controls open), far taller than the 8px the floating window docks
at, so the card's own top row (caret, number, menu, X) sat among the header's
rows and was only seen by dragging the card down. Below 801px the card starts
BELOW the header, at the height Utils publishes as --wekan-header-height.
Only a card WITHOUT an inline top matches (`:not([style*="top"])`), so the
place a finger dragged it to is an inline top and is kept when the finger is
lifted - this rule never competes with it. */
@media screen and (max-width: 800px) {
body.desktop-mode .card-details:not(.card-details-popup):not([style*="top"]),
/* a card opened as a popup (search result, table, Roadmap bar) docks the same way */
body.desktop-mode .card-details.card-details-popup:not([style*="top"]),
body.desktop-mode .card-details.card-details-maximized:not(.card-details-popup):not(.card-details-collapsed) {
top: calc(var(--wekan-header-height, 0px) + 8px) !important;
}
}
/* #4822: the MAXIMIZED card must be anchored to the VIEWPORT (position: fixed
with viewport insets), never laid out inside — or measured from — the
scrolled swimlane/board container. Historically the maximized pane was an
@@ -250,6 +267,24 @@ body.desktop-mode .card-details.card-details-collapsed {
color: #b3b3b3;
display: inline-block;
margin-inline-end: 6px;
/* Above the title's edit zone (z-index 1, over the whole title): a press on the
card number moves the window like the rest of the header row; only the title
itself edits. */
position: relative;
z-index: 2;
}
/* The areas that move the window by a finger. Without touch-action: none the browser
takes a finger drag for a scroll and cancels the pointer, so the window never moves
(the page behind it scrolls instead). Only in desktop mode, where the card is a
window; the full-screen card of mobile mode is not moved. */
body.desktop-mode .card-details .card-details-header,
body.desktop-mode .card-details .card-details-header .card-drag-handle {
touch-action: none;
}
body.card-window-dragging {
user-select: none;
-webkit-user-select: none;
}
/* Collapse toggle triangle */
@@ -366,43 +401,43 @@ body.desktop-mode .card-details.card-details-collapsed {
position: relative;
}
/* The title is two things at once, split like a minicard's: the LEADING half
opens the editor, the trailing half drags the window. With drag handles OFF
there is no handle, so the title bar is what the window is moved by - and a
heading that is entirely an edit target leaves nothing to take hold of.
/* #6839: the whole title edits - every word of it and the space beside a
short last line, which is what anybody clicks to change the title. The window
is moved from the empty space ABOVE the title, in the row of the collapse
caret and the menu (cardDetails.js), or by the drag handle when drag handles
are on. It was split like a minicard's - the trailing half dragged - and a
click on the right of the title did nothing anybody expected.
An overlay rather than a split of the text, because the text is a `.viewer`
and cutting it in two would cut the sentence in two. `inset-inline-start`, so
the edit half is the left in English and the right in Arabic from one rule.
The drag handler steps aside for this zone (cardDetails.js). */
An overlay rather than a link around the text, because the text is a
`.viewer` with its own links. */
.card-details .card-details-header .card-details-title .card-details-title-edit-zone {
position: absolute;
top: 0;
bottom: 0;
inset-inline-start: 0;
/* Half of the heading - and the heading is stretched to the row below, so
this is half of the BAR however long the title happens to be. */
width: 50%;
width: 100%;
cursor: text;
z-index: 1;
}
/* The heading fills the row rather than shrinking to its text, so "half the
title" and "half the bar" are the same half however long the title is. A
heading only as wide as its words leaves the empty space beside it belonging
to nothing - and that space is where anybody would grab to move the window.
It stays inline-block so the complete-toggle beside it keeps its place; the
buttons floating at the trailing edge take their room out of this width. */
/* The heading fills the row rather than shrinking to its text, so the edit
zone covers the space beside a short title too. It stays inline-block so
the complete-toggle beside it keeps its place. */
.card-details .card-details-header .card-details-title {
min-width: 60%;
}
/* With drag handles ON the handle is the only drag source, so the whole title
is the edit target - the same rule the minicard follows, from the same
question: `card-details-with-handle` is set by the template from
isTouchScreenOrShowDesktopDragHandles, which is Utils.showDragHandles(). */
.card-details.card-details-with-handle .card-details-header .card-details-title .card-details-title-edit-zone {
width: 100%;
/* The empty part of the header row is what moves the window when there is no
drag handle; with one, the handle alone does. */
.card-details:not(.card-details-with-handle) .card-details-header {
cursor: move;
}
.card-details .card-details-header .card-details-title,
.card-details .card-details-header a {
cursor: auto;
}
.card-details .card-details-header a {
cursor: pointer;
}
.card-details .card-details-header .linked-card-location {
font-style: italic;
@@ -536,6 +571,19 @@ body.desktop-mode .card-details.card-details-collapsed {
resize: both;
}
/* A card opened as a POPUP (Session popupCardId: from a search result, a table
or a Roadmap/Gantt bar) docks at the very top of the window like every other
opened card, and covers the header bars, as the .pop-over[data-popup=
"cardDetailsPopup"] rule near the end of this file says it must. The 97px
above is the room the header takes for a card that is NOT docked by the
desktop-mode rules (those skip popups), and it opened the popup BELOW the
header: collapsed, it was a bar floating over the swimlane. Mobile mode keeps
its own full-window rule (top: 0 !important). */
.card-details.card-details-popup {
top: 8px;
z-index: 2001;
}
/* Override for mobile mode even on larger screens */
body.mobile-mode .card-details {
width: 100vw !important;
@@ -846,15 +894,6 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
.voted {
opacity: 0.7;
}
.vote-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.vote-title .js-edit-date {
align-self: flex-start;
margin-inline-start: 6px;
}
.vote-result {
display: flex;
gap: 6px;
@@ -865,15 +904,6 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
.poker-voted {
opacity: 0.7;
}
.poker-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.poker-title .js-edit-date {
align-self: flex-start;
margin-inline-start: 6px;
}
.poker-result {
display: flex;
flex-wrap: wrap;
@@ -1114,7 +1144,7 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
}
.card-details-item-location .card-location-remove {
margin-inline-start: 8px;
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
cursor: pointer;
}
.card-details-item-location .card-location-remove:hover {
@@ -1230,8 +1260,8 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
and Activities - has one rule above it and a caret in its heading that
collapses it. The rule comes from the same template as the caret, so eleven
sections cannot end up with ten separators.
The heading is a button: it says so to a screen reader (role/tabindex in the
template) and it looks like one to a mouse. */
The heading holds a real button (cardSectionHeader in the template) that a
screen reader and the keyboard reach, and it looks like one to a mouse. */
/* The GROUP is the full-width row of the card, and the fields inside it share
rows within it - which is what puts Assignee and Creator beside Members
rather than under it. The class that made an individual field full width is
@@ -1266,11 +1296,32 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
changes length, so it gets a fixed box. `text-align: center` rather than a
side, because the glyph itself differs between the open and closed states
(down, and right or left) and a fixed side would make it jump. */
.card-details h3.card-details-item-title.js-toggle-card-section > .fa:first-child {
.card-details h3.card-details-item-title.js-toggle-card-section .card-details-section-toggle > .fa:first-child {
width: 12px;
text-align: center;
flex: 0 0 auto;
}
/* The button inside the heading looks like the heading: no button chrome, the
heading's own size, weight and colour, the caret, icon and label in a row. */
.card-details h3.card-details-item-title .card-details-section-toggle {
align-items: center;
background: none;
border: 0;
border-radius: 3px;
color: inherit;
cursor: pointer;
display: inline-flex;
font: inherit;
gap: 6px;
margin: 0;
padding: 0;
text-align: start;
width: auto;
}
.card-details h3.card-details-item-title .card-details-section-toggle:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.card-details h3.card-details-item-title.js-toggle-card-section:hover {
opacity: 0.85;
}
@@ -1449,3 +1500,27 @@ body.desktop-mode .card-details.card-details-position-pending:not(.card-details-
/* Card kind: the selected one is marked, at the end of its line, and bold. */
.card-kind-list .js-select-card-kind.is-active { font-weight: bold; }
.card-kind-list .card-kind-selected { margin-inline-start: 0.5em; color: #3cb500; }
/* ONE SECTION MODEL (2026-10-28): the opened card draws Card Settings'
sections (cardDetails.jade `.card-details-section`), each a full-width row
of the items, headed by cardSectionHeader with the rule above it. The FIRST
section has nothing above it to be separated from, so its rule is not drawn
- whichever section the board put first (the template cannot know). */
.card-details .card-details-items > .card-details-section {
flex: 0 0 100%;
width: 100%;
min-width: 0;
}
.card-details .card-details-items > .card-details-section:first-child > hr.card-details-section-rule:first-child,
.card-details .card-details-items > .card-details-section:first-child > * > hr.card-details-section-rule:first-child,
.card-details .card-details-items > .card-details-section:first-child > * > .comment-title > hr.card-details-section-rule:first-child,
.card-details .card-details-items > .card-details-section:first-child > .card-details-group-vote-poker > hr:first-child {
display: none;
}
/* Lists that were sections of their own and are a field of one now: a row of
their own inside the section. */
.card-details .card-details-group-body > .card-details-item-similar-cards,
.card-details .card-details-group-body > .card-details-item-buttons,
.card-details .card-details-section .card-details-item-text-notes {
flex: 0 0 100%;
}
File diff suppressed because it is too large. Load diff
+233 -188
View File
@@ -24,6 +24,7 @@ import { isTextSelectionInsideCard } from '/client/lib/cardCloseGuard';
import { placeCardDetailsX, MARGIN } from '/client/lib/cardDetailsPlacement';
import { dueFromTitle } from '/client/components/forms/titleDueHint';
import { riskOf } from '/client/lib/cardRiskOf';
import Rules from '/models/rules';
import { suggestedMembersFor } from '/client/lib/suggestedMembers';
import '/client/components/cards/similarCards';
@@ -37,26 +38,7 @@ import {
datePickerRendered,
datePickerHelpers,
} from '/client/lib/datepicker';
import {
formatDateTime,
formatDate,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
} from '/imports/lib/dateUtils';
import { formatDateTime, add, now } from '/imports/lib/dateUtils';
import Cards from '/models/cards';
import Boards from '/models/boards';
import Checklists from '/models/checklists';
@@ -99,6 +81,7 @@ const editingLocationId = new ReactiveVar(null);
// Shared with Admin Panel, which links the places accounts log in from the same
// way - models/lib/mapLink.js.
const { mapLinkFor, GCJ } = require('/models/lib/mapLink');
const { cardHeaderPress, dragThreshold, passedThreshold, clampWindow } = require('/models/lib/windowDrag');
@@ -700,9 +683,12 @@ Template.cardDetails.onDestroyed(function () {
// #6686: see the comment in onCreated above. Only close a popup that is
// still showing THIS card's data at the base of its stack - a popup opened
// for an unrelated card/context must not be touched.
// for an unrelated card/context must not be touched. #6840: and only when
// this card is no longer the open one - closed, or another card opened. A
// card whose details are merely rendered again is still open, and its popup
// (the Due date one `d` just opened) must not flash shut.
const openedCardId = this.openedCardId;
if (openedCardId && Popup.isOpen()) {
if (openedCardId && Popup.isOpen() && Utils.getCurrentCardId() !== openedCardId) {
const baseStackEntry = Popup._stack?.[0];
if (baseStackEntry?.dataContext?._id === openedCardId) {
Popup.close();
@@ -754,14 +740,15 @@ Template.registerHelper('cardSwimlaneTitle', function cardSwimlaneTitle() {
return swimlane && swimlane.title;
});
// The Custom Fields section: on a card that has a custom field value, or that
// ticked Custom Fields in "+ Add" (docs/Features/Cards/Card-Fields.md) - not on
// every card, where it was an empty heading. "+ Add" is how a card with none
// gets the section, and its hamburger then adds the fields.
// The Custom Fields section: on every card the reader may change whose board
// shows Custom Fields on cards, empty or not (docs/Features/Cards/Card-Fields.md)
// - its hamburger is where the fields are added. The board as its cards see it,
// so a board that hides the whole Custom Fields section on Show on Card
// (models/lib/cardShownFields.js boardForCard) hides it here too.
Template.registerHelper('canShowCustomFieldsOnCard', function canShowCustomFieldsOnCard() {
const { cardShowsField } = require('/client/components/cards/cardAddFields');
const board = this?.board?.();
return Utils.canModifyCard(this) && board?.allowsCustomFields !== false && cardShowsField(this, 'customFields');
const { boardForCard } = require('/models/lib/cardShownFields');
const board = boardForCard(this?.board?.());
return Utils.canModifyCard(this) && board?.allowsCustomFields !== false;
});
Template.registerHelper('stickers', function stickers() {
@@ -891,54 +878,122 @@ Template.registerHelper('isCurrentListId', function isCurrentListId(listId) {
return data.listId == listId;
});
// #4448: the fields INSIDE a reorderable section (the four dates, the people
// of Members, ...) render in the board's order too. One argument-less helper
// per section, registered globally like isDateFormat above because each
// section is its own template. The card is the data context (`each field in`
// keeps it), so `this.board()` is the board. models/lib/cardFieldOrder.js
// The opened card draws Card Settings' sections (models/lib/cardSettingsSections.js)
// in the board's Show on Card section order, and the fields INSIDE each in the
// order Card Settings lists that section's rows (board.cardFieldOrder through
// models/lib/cardFieldOrder.js and cardSettingsSections.sectionFieldsOn) - one
// order, so the card and Card Settings cannot disagree. One argument-less
// helper per section, registered globally because each section is its own
// template. The card is the data context (`each field in` keeps it), so
// `this.board()` is the board.
const cardSettingsSections = require('/models/lib/cardSettingsSections');
const { applyCardOrder } = require('/models/lib/cardFieldOrder');
function cardSectionFields(card, sectionId) {
const board = card?.board?.();
return cardSettingsSections.sectionFieldsOn('card', applyCardOrder(board?.cardFieldOrder), sectionId);
}
{
const { orderedCardFieldsOf } = require('/models/lib/cardFieldOrder');
const { boardAllows, boardForCard } = require('/models/lib/cardShownFields');
const sectionHelper = section => function orderedFields() {
const board = this?.board?.();
return orderedCardFieldsOf(board?.cardFieldOrder, section);
return cardSectionFields(this, section);
};
Template.registerHelper('orderedLabelsFields', sectionHelper('labels'));
Template.registerHelper('orderedDatesFields', sectionHelper('dates'));
Template.registerHelper('orderedDateFields', sectionHelper('date'));
Template.registerHelper('orderedMembersFields', sectionHelper('members'));
Template.registerHelper('orderedSortFields', sectionHelper('sort'));
Template.registerHelper('orderedVoteAndPokerFields', sectionHelper('voteAndPoker'));
Template.registerHelper('orderedCardFields', sectionHelper('card'));
Template.registerHelper('orderedTimeFields', sectionHelper('time'));
Template.registerHelper('orderedChecklistsFields', sectionHelper('checklists'));
Template.registerHelper('orderedAttachmentsFields', sectionHelper('attachments'));
// Only the Scrum fields the board shows on cards (Board Settings / Scrum,
// #6830): a Scrum field has no board flag in the template to read.
Template.registerHelper('orderedScrumFields', function orderedScrumFields() {
const board = boardForCard(this?.board?.());
return cardSectionFields(this, 'scrum').filter(key => boardAllows(board, key));
});
}
// The card's sections in the board's order, less the Scrum fields this card
// does not show - a Scrum field is its own section with no board flag in the
// template to read (docs/Features/Cards/Card-Fields.md).
function orderedSectionsOf(card) {
const board = card?.board?.();
let sections;
if (board && typeof board.getCardFieldOrder === 'function') {
sections = board.getCardFieldOrder();
} else {
const { applyCardFieldOrder } = require('/models/lib/cardFieldOrder');
sections = applyCardFieldOrder(board?.cardFieldOrder);
// Whether the card has anything to show in section `id`, read through the
// board as its cards see it (cardShownFields.js boardForCard, which already
// takes away the rows of a section hidden whole on Show on Card): a section
// with nothing in it draws no heading. The same gates its template had before
// the card used Card Settings' sections (docs/Features/Cards/Card-Fields.md).
function cardSectionHasContent(card, board, id) {
const { boardAllows } = require('/models/lib/cardShownFields');
if (!board) return false;
const may = Utils.canModifyCard(card);
switch (id) {
case 'labels': return Boolean(board.allowsLabels || board.allowsStickers || board.allowsLocation);
case 'date':
return Boolean(board.allowsReceivedDate || board.allowsStartDate || board.allowsDueDate || board.allowsEndDate
|| (board.allowsAtRiskOnCard !== false && riskOf(card, { withMoves: true })));
case 'members':
return Boolean(board.allowsMembers || board.allowsAssignee || board.allowsCreator
|| board.allowsRequestedBy || board.allowsAssignedBy);
case 'card': {
const swimlane = typeof card?.swimlane === 'function' ? card.swimlane() : null;
return Boolean(board.allowsDependencies || board.allowsCardSortingByNumber || board.allowsShowLists
|| (board.allowsSwimlaneNameOnCard && swimlane && swimlane.title)
|| (board.allowsCardButtonsOnCard !== false && cardHasButtons(card)));
}
case 'time': return Boolean(board.allowsSpentTime || board.allowsFlowtime || board.allowsPomodoro);
case 'customFields': return may && board.allowsCustomFields !== false;
case 'vote': return Boolean(board.allowsVote);
case 'poker': return Boolean(board.allowsPoker);
case 'description': {
const shows = board.allowsDescriptionTitle || board.allowsDescriptionText;
const writer = may && !ReactiveCache.getCurrentUser()?.isWorker?.();
return Boolean(shows && (writer || (card && card.getDescription && card.getDescription())));
}
case 'scrum': return cardSectionFields(card, 'scrum').some(key => boardAllows(board, key));
case 'checklists': return Boolean(board.allowsChecklists);
case 'subtasks': return Boolean(board.allowsSubtasks);
case 'attachments': return Boolean(board.allowsAttachments || board.allowsTextNotes);
case 'comments': return Boolean(board.allowsComments);
default: return false;
}
const { cardShowsField } = require('/client/components/cards/cardAddFields');
return sections.filter(s => !/^scrum[A-Z]/.test(s) || cardShowsField(card, s));
}
Template.cardFieldSectionSort.helpers({
// The card's own view of the board (docs/Features/Cards/Card-Fields.md), as
// the fields inside read it: reading the board itself drew the group's rule
// for fields the card does not show - an empty group, and a second line
// under "+ Add".
hasVisibleSortFields() {
const board = cardBoardView(this);
return !!(board && (
board.allowsCardSortingByNumber || board.allowsShowLists ||
board.allowsFlowtime || board.allowsPomodoro ||
// The card's own view already says whether Spent time is shown: with
// hours, or ticked "On this card" to be filled in.
board.allowsSpentTime
));
// Whether the board has card buttons (rules) - what the cardButtons template
// draws (client/components/rules/cardButtons.js buttonRulesForBoard); without
// any, Card is not headed for them alone.
function cardHasButtons(card) {
return Boolean(card && card.boardId && Rules.find({ boardId: card.boardId, buttonType: 'card' }).count());
}
// The card's sections, in the board's Show on Card section order
// (cardSettingsSections.sectionOrderOf), less those hidden whole on Show on
// Card and those with nothing to show for this card - and less Comments on a
// maximized card, which has them at the top of the right column.
function orderedSectionsOf(card, { maximized = false } = {}) {
const { boardForCard } = require('/models/lib/cardShownFields');
const raw = card?.board?.();
const board = boardForCard(raw);
const hidden = cardSettingsSections.hiddenSectionsOf(raw, 'card');
return cardSettingsSections.sectionOrderOf(raw, 'card')
.filter(id => !hidden.includes(id))
.filter(id => !(maximized && id === 'comments'))
.filter(id => cardSectionHasContent(card, board, id));
}
function isCardMaximized() {
const currentUser = ReactiveCache.getCurrentUser();
const maximized = currentUser
? currentUser.hasCardMaximized()
: window.localStorage.getItem('cardMaximized') === 'true';
return !Utils.getPopupCardId() && maximized;
}
// What the Date and Checklists sections read that used to be the opened
// card's own helpers.
Template.cardFieldSectionDate.helpers({
cardAtRisk() {
return riskOf(Template.currentData(), { withMoves: true });
},
});
Template.cardFieldSectionChecklists.helpers({
checklistSectionOpen() {
return ReactiveCache.getBoard(this.boardId)?.allowsChecklistTitle === false || isCardSectionOpen('checklists');
},
});
@@ -950,18 +1005,10 @@ Template.cardDetails.helpers({
? Meteor.connection._lastSessionId
: null;
},
// #4448: the order the reorderable card-detail sections (Labels, Dates,
// Members, Dependencies, Sort, Custom Fields, Vote/Poker, Description)
// render in, resolved from the board's stored setting - the section order
// of the flat field order Board Settings / Card writes.
// models/lib/cardFieldOrder.js
// The sections the card draws, in Card Settings' Show on Card section order
// (orderedSectionsOf above): the ids of models/lib/cardSettingsSections.js.
orderedCardFieldSections() {
return orderedSectionsOf(this);
},
// The first Scrum field this card shows: where the one "Scrum settings"
// heading goes, over all of them (docs/Features/Cards/Card-Fields.md).
firstScrumSection() {
return orderedSectionsOf(this).find(s => /^scrum[A-Z]/.test(s)) || null;
return orderedSectionsOf(this, { maximized: isCardMaximized() });
},
// Placed beside its minicard: card-details-position-pending is off for good.
@@ -971,39 +1018,41 @@ Template.cardDetails.helpers({
},
cardMaximized() {
const currentUser = ReactiveCache.getCurrentUser();
const maximized = currentUser
? currentUser.hasCardMaximized()
: window.localStorage.getItem('cardMaximized') === 'true';
return !Utils.getPopupCardId() && maximized;
return isCardMaximized();
},
// List title, Show on Card: the card's own choice, unless the
// administrator unticked List title in Admin Panel / Settings / Visibility /
// Features (models/lib/cardFieldVisibility.js). The card keeps its choice.
// Its section (Card) hidden whole on Show on Card hides it as well.
showListOnCard() {
const board = this?.board?.();
return Boolean(this?.showListOnCard) && !isCardFieldHiddenOnBoard(board, 'listTitle');
const { hidesRow } = require('/models/lib/cardShownFields');
return Boolean(this?.showListOnCard) && !isCardFieldHiddenOnBoard(board, 'listTitle')
&& !hidesRow(board, 'card', 'listTitle');
},
// The header's own flags, read through the board as its cards see it
// (cardBoardView), so a Card Settings section hidden whole on Show on Card
// takes them away too.
// Card color, Show on Card: the header's color, on unless turned off.
showCardColorOnCard() {
return Utils.getCurrentBoard()?.allowsCardColorOnCard !== false;
return cardBoardView(this)?.allowsCardColorOnCard !== false;
},
// Collapse, Show on Card: the header's collapse caret, on unless turned off.
allowsCardCollapse() {
return Utils.getCurrentBoard()?.allowsCardCollapse !== false;
return cardBoardView(this)?.allowsCardCollapse !== false;
},
// Labels title, Show on Card: the labels above the title, as on a minicard.
showLabelsAboveTitleOnCard() {
return Utils.getCurrentBoard()?.labelsAboveTitleOnCard === true;
return cardBoardView(this)?.labelsAboveTitleOnCard === true;
},
// ...as long as the board shows labels at all.
labelsShownAboveTitle() {
return Utils.getCurrentBoard()?.allowsLabels !== false;
return cardBoardView(this)?.allowsLabels !== false;
},
cardCollapsed() {
// A board that turned the caret off has no collapsed cards.
if (Utils.getCurrentBoard()?.allowsCardCollapse === false) return false;
if (cardBoardView(this)?.allowsCardCollapse === false) return false;
const user = ReactiveCache.getCurrentUser();
if (user && user.profile) {
return !!user.profile.cardCollapsed;
@@ -1045,10 +1094,6 @@ Template.cardDetails.helpers({
return user && user.isVerticalScrollbars();
},
checklistSectionOpen() {
return ReactiveCache.getBoard(this.boardId)?.allowsChecklistTitle === false || isCardSectionOpen('checklists');
},
isLoaded() {
return Template.instance().isLoaded;
},
@@ -1056,9 +1101,9 @@ Template.cardDetails.helpers({
// WHICH SECTIONS OF A CARD ARE OPEN.
//
// Every section of an opened card - Labels, Date Format, Members, Dependencies,
// Sort, Custom Fields, Description, Checklists, Subtasks, Attachments, Comments
// and Activities - carries a caret that collapses it, and one rule above it.
// Every section of an opened card - Card Settings' sections, Labels, Date,
// Members, Card, Time, Custom Fields, ..., Comments, and Activities - carries a
// caret that collapses it, and one rule above it.
// Before this there was one caret for the whole card and an EYE on Activities
// that showed and hid exactly what a caret would: two controls for one idea,
// able to disagree with each other.
@@ -1069,9 +1114,11 @@ Template.cardDetails.helpers({
//
// ACTIVITIES starts CLOSED and everything else open. A card is opened to read
// the card; its history is the thing you go looking for.
// The sections are Card Settings' (models/lib/cardSettingsSections.js ids,
// 2026-10-28), plus the right column's Activities and the Similar cards list
// inside Card.
const CARD_SECTIONS = [
'labels', 'date-format', 'members', 'dependencies', 'sort', 'custom-fields',
'description', 'checklists', 'subtasks', 'attachments', 'comments', 'activities',
...require('/models/lib/cardSettingsSections').SECTION_IDS, 'activities', 'similar-cards',
];
const COLLAPSED_BY_DEFAULT = ['activities'];
@@ -1144,18 +1191,68 @@ Template.cardSectionHeader.events({
if (!section) return;
cardSectionState.set(section, !isCardSectionOpen(section));
},
// A heading that can be operated with the mouse can be operated with the
// keyboard: it carries role="button" and tabindex, so Enter and Space are
// what a screen reader user presses.
'keydown .js-toggle-card-section'(event) {
if (event.target !== event.currentTarget) return;
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
const section = event.currentTarget.dataset.section;
if (section) cardSectionState.set(section, !isCardSectionOpen(section));
},
});
// Moves the card window under a finger or a mouse. `press` is cardHeaderPress's answer.
// A press on a control waits until the pointer has really moved, so a tap still
// collapses, closes or opens the menu; once it moved, the click that follows the drag
// is swallowed.
function beginCardWindowDrag(jqEvent, press) {
// Blaze hands over a jQuery event, which does not copy `isPrimary`; the browser's
// own event has it.
const event = jqEvent.originalEvent || jqEvent;
if (!press.start || !event.isPrimary) return;
if (event.pointerType === 'mouse' && event.button !== 0) return;
const $card = $(event.target).closest('.card-details');
if (!$card.length) return;
const startX = event.clientX;
const startY = event.clientY;
const offset = $card.offset();
const startLeft = offset.left - window.scrollX;
const startTop = offset.top - window.scrollY;
const threshold = dragThreshold(event.pointerType, press.deferred);
const pointerId = event.pointerId;
let moving = false;
const onMove = (e) => {
if (e.pointerId !== pointerId) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (!moving) {
if (!passedThreshold(dx, dy, threshold)) return;
moving = true;
// #6465: where the user put it wins over where the minicard is, so a later
// resize re-places every other window but only clamps this one into view.
markCardDetailsUserMoved($card);
document.body.classList.add('card-window-dragging');
}
e.preventDefault();
const placed = clampWindow(startLeft + dx, startTop + dy, $card.outerWidth(),
{ width: window.innerWidth, height: window.innerHeight });
$card.css({ left: `${placed.left}px`, top: `${placed.top}px` });
};
const onEnd = (e) => {
if (e.pointerId !== pointerId) return;
document.removeEventListener('pointermove', onMove);
document.removeEventListener('pointerup', onEnd);
document.removeEventListener('pointercancel', onEnd);
document.body.classList.remove('card-window-dragging');
if (moving && press.deferred) {
// The drag started on a link or a button: its click is not a tap.
const swallow = (click) => { click.preventDefault(); click.stopPropagation(); };
document.addEventListener('click', swallow, { capture: true, once: true });
setTimeout(() => document.removeEventListener('click', swallow, { capture: true }), 400);
}
};
document.addEventListener('pointermove', onMove, { passive: false });
document.addEventListener('pointerup', onEnd);
document.addEventListener('pointercancel', onEnd);
}
Template.cardDetails.events({
// #1591 folded each FIELD by its own title, storing a class per card in the
// profile. The section carets replaced it: one handle per section, on the
@@ -1185,85 +1282,31 @@ Template.cardDetails.events({
Users.setPublicCardCollapsed(!currentState);
}
},
'mousedown .js-card-drag-handle'(event) {
event.preventDefault();
const $card = $(event.target).closest('.card-details');
// #6465: where the user put it wins over where the minicard is, so a later
// resize re-places every other window but only clamps this one into view.
markCardDetailsUserMoved($card);
const startX = event.clientX;
const startY = event.clientY;
const startLeft = $card.offset().left;
const startTop = $card.offset().top;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
$card.css({
left: startLeft + deltaX + 'px',
top: startTop + deltaY + 'px'
});
};
const onMouseUp = () => {
$(document).off('mousemove', onMouseMove);
$(document).off('mouseup', onMouseUp);
};
$(document).on('mousemove', onMouseMove);
$(document).on('mouseup', onMouseUp);
// The opened card's window is moved by a finger as well as a mouse, so these are
// Pointer Events (a finger sends no mouse events while it moves, which left the card
// fixed on an iPad while the page behind it scrolled) and the areas that start a
// drag are `touch-action: none` (cardDetails.css). With drag handles ON the handle
// is the only thing that moves the window; with them off the header row moves it,
// except the title (it edits). A link or button in the row - the collapse caret, the
// menu, the card number's neighbours - still does its job when tapped, and moves the
// window when dragged. models/lib/windowDrag.js
'pointerdown .js-card-drag-handle'(event) {
beginCardWindowDrag(event, { start: true, deferred: false });
},
'mousedown .js-card-title-drag-handle'(event) {
// The title bar drags the window. Don't interfere with text selection.
if (
$(event.target).closest(
'a, input, textarea, button, select, option, .js-card-details-title',
).length > 0
) {
// #6641: while the editor replaces the header, a textarea press still
// bubbles to this drag bar. Leave it to the browser so mouse dragging
// selects text instead of moving the card window.
return;
}
// ...and don't drag from the half that EDITS. The title is split like a
// minicard's: the leading half opens the editor, the trailing half is what
// you take hold of. With drag handles ON the zone covers the whole title
// (cardDetails.css), so the window is dragged by its handle alone - which is
// what turning handles on means.
if ($(event.target).closest('.card-details-title-edit-zone').length > 0) {
return;
}
// The drag handle has its own handler; without this both would run and the
// window would be moved twice by one press.
if ($(event.target).closest('.js-card-drag-handle').length > 0) {
return;
}
event.preventDefault();
const $card = $(event.target).closest('.card-details');
// #6465: same as the drag handle above — a dragged window keeps its place.
markCardDetailsUserMoved($card);
const startX = event.clientX;
const startY = event.clientY;
const startLeft = $card.offset().left;
const startTop = $card.offset().top;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
$card.css({
left: startLeft + deltaX + 'px',
top: startTop + deltaY + 'px'
});
};
const onMouseUp = () => {
$(document).off('mousemove', onMouseMove);
$(document).off('mouseup', onMouseUp);
};
$(document).on('mousemove', onMouseMove);
$(document).on('mouseup', onMouseUp);
'pointerdown .js-card-title-drag-handle'(event) {
const target = event.target;
const closest = selector => (target.closest ? target.closest(selector) : null);
const press = cardHeaderPress({
onEditable: !!closest('input, textarea, select, option, .js-card-details-title'),
onTitleEditZone: !!closest('.card-details-title-edit-zone'),
// The handle has its own handler above; this one steps aside for it.
onHandle: false,
cardHasHandle: !!closest('.js-card-drag-handle')
|| !!(closest('.card-details') && closest('.card-details').querySelector('.js-card-drag-handle')),
onControl: !!closest('a, button'),
});
if (closest('.js-card-drag-handle')) return;
beginCardWindowDrag(event, press);
},
'click .js-close-card-details'(event) {
event.preventDefault();
@@ -1912,8 +1955,10 @@ Template.cardSubtasksMenuPopup.events({
Popup.open('boardSubtaskSettings')(event);
},
});
Template.cardFieldSectionVoteAndPoker.events({
Template.cardFieldSectionVote.events({
'click .js-section-edit-voting': Popup.open('cardStartVoting'),
});
Template.cardFieldSectionPoker.events({
'click .js-section-edit-poker': Popup.open('cardStartPlanningPoker'),
});
Template.cardSpentTimeMenu.events({
@@ -2328,7 +2373,7 @@ const filterMembers = (filterTerm) => {
.map(({ member }) => member);
}
return members;
}
};
Template.editCardRequesterForm.onRendered(function () {
autosize(this.$('.js-edit-card-requester'));
+1 -1
View File
@@ -9,5 +9,5 @@ template(name="cardPomodoro")
button.js-stop-pomodoro.pomodoro-btn.negate(type="button") {{_ 'stop'}}
else
if canControlPomodoro
input.js-pomodoro-work-minutes.pomodoro-minutes(type="number" min="1" value="{{workMinutesInput}}")
input.js-pomodoro-work-minutes.pomodoro-minutes(type="number" min="1" value="{{workMinutesInput}}" aria-label="{{_ 'pomodoro-work'}} ({{_ 'minutes'}})")
button.js-start-pomodoro.pomodoro-btn.primary(type="button") {{_ 'pomodoro-start'}}
+6 -6
View File
@@ -1,5 +1,5 @@
.js-add-checklist {
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
}
textarea.js-add-checklist-item,
textarea.js-edit-checklist-item {
@@ -11,7 +11,7 @@ textarea.js-edit-checklist-item {
.delete-text,
.js-delete-checklist-item,
.js-convert-checklist-item-to-card {
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
text-decoration: underline;
word-wrap: break-word;
float: inline-end;
@@ -139,7 +139,7 @@ textarea.js-edit-checklist-item {
overflow-wrap: anywhere;
}
.checklist-item .item-title.is-checked {
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
font-style: italic;
text-decoration: line-through;
}
@@ -167,11 +167,11 @@ textarea.js-edit-checklist-item {
.checklist-item .checklist-item-due-date-add {
align-self: center;
margin-inline-start: 6px;
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
text-decoration: none;
}
.checklist-item .checklist-item-due-date-add:hover {
color: #2980b9;
color: #2573a7;
}
.js-delete-checklist-item,
.js-convert-checklist-item-to-card {
@@ -235,7 +235,7 @@ textarea.js-edit-checklist-item {
.checklist-title .checklist-collapse:hover,
.checklist-title .checklist-collapse:focus-visible {
color: #2980b9;
color: #2573a7;
}
.checklist-title .checklist-due-date-add {
+7 -2
View File
@@ -27,8 +27,8 @@
}
.ui-font-btn.active,
.ui-font-size-btn.active {
background: #2980b9;
border-color: #2980b9;
background: #2573a7;
border-color: #2573a7;
color: #fff;
}
/* The "Unset" colour reset links look like buttons. */
@@ -155,6 +155,7 @@
}
.card-label-green {
background-color: #3cb500;
color: #000;
}
.card-label-green:hover {
color: #000;
@@ -169,6 +170,7 @@
}
.card-label-red {
background-color: #eb4646;
color: #000;
}
.card-label-purple {
background-color: #a632db;
@@ -182,6 +184,7 @@
}
.card-label-sky {
background-color: #00c2e0;
color: #000;
}
.card-label-black {
background-color: #4d4d4d;
@@ -213,6 +216,7 @@
}
.card-label-magenta {
background-color: #f0f;
color: #000;
}
.card-label-gold {
background-color: #ffd700;
@@ -223,6 +227,7 @@
}
.card-label-gray {
background-color: #808080;
color: #000;
}
.card-label-saddlebrown {
background-color: #8b4513;
+25 -3
View File
@@ -374,7 +374,12 @@
flex-wrap: wrap;
position: relative;
z-index: 5;
margin-inline-end: 84px;
/* #6834: room for the drag handle beside it (20px, 1px from the edge), and
no more. It reserved 84px - a third of a 250px minicard - for nothing:
the avatars float AFTER this row and `clear: both` starts it below the
menu, so a due date "1405-08-10 12:00 (1 day left)" was broken over three
lines beside an empty strip. */
margin-inline-end: 24px;
clear: both;
}
.minicard .date {
@@ -1006,7 +1011,7 @@
.minicard-list-name {
clear: inline-start;
font-size: 0.75em;
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
margin-top: 2px;
display: flex;
align-items: center;
@@ -1018,7 +1023,7 @@
.minicard-swimlane-name {
clear: inline-start;
font-size: 0.75em;
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
margin-top: 2px;
display: flex;
align-items: center;
@@ -1214,3 +1219,20 @@
.minicard.minicard-unread-changes {
box-shadow: inset 4px 0 0 #2b6cb0;
}
/* Card Settings' sections on the minicard (2026-10-28, models/lib/minicardGroups.js):
At risk and Forecast are badges drawn in the dates line, after the date
their row follows; and a section boundary can start a second badge strip,
which floats beside the first with the gap two badges have. */
.minicard .dates .badge {
align-self: center;
margin-inline-end: 6px;
font-size: 0.9em;
color: #808080;
}
.minicard .dates .badge .badge-text {
padding-inline-start: 4px;
}
.minicard .badges + .badges {
margin-inline-start: 21px;
}
+254 -251
View File
@@ -119,22 +119,30 @@ template(name="minicard")
+editMinicardTitleForm
else
each section in orderedMinicardSections
//- Each Scrum field where the board's order puts it ("Scrum settings:
Sprint" ... in Board Settings / Card).
if $eq section "scrumSprint"
+scrumMetadata(kind="minicard" record=this field="Sprint")
if $eq section "scrumPastSprints"
+scrumMetadata(kind="minicard" record=this field="PastSprints")
if $eq section "scrumRelease"
+scrumMetadata(kind="minicard" record=this field="Release")
if $eq section "scrumAcceptanceCriteria"
+scrumMetadata(kind="minicard" record=this field="AcceptanceCriteria")
if $eq section "scrumBacklogRank"
+scrumMetadata(kind="minicard" record=this field="BacklogRank")
if $eq section "dates"
//- The cover is fixed, under the title (MINICARD_LAYOUT head).
if cover
if currentBoard.allowsCoverAttachmentOnMinicard
//- An img, not a CSS background: it can fall back to the original
//- once (attachments.js previewFallback), and it is drawn only
//- when there is a URL - never url('?dummyReload...').
if attachmentPreviewUrl cover
.minicard-cover
img.minicard-cover-image(alt="" src="{{attachmentPreviewUrl cover}}?dummyReloadAfterSessionEstablished={{sess}}" width="512" height="120" decoding="async")
//- Card Settings' sections (models/lib/cardSettingsSections.js) in the
board's Show on Minicard section order (minicardSettingsSectionOrder), each
section's fields in the order of its rows there (board.minicardFieldOrder,
models/lib/cardFieldOrder.js) - with no headings: the minicard is compact.
What shares a container still does: consecutive date fields are one
`.dates` line and consecutive badges one `.badges` strip, also across a
section boundary (minicard.js minicardGroups), so a board that never set
an order looks as it did. Every gate (the show* helpers, the
currentBoard.allows* flags) is unchanged; only where a block sits is
decided by the loop.
each group in minicardGroups
if group.dates
.dates
each field in orderedMinicardDates
each field in group.fields
if $eq field "receivedDate"
if showReceived
if getReceived
@@ -159,136 +167,36 @@ template(name="minicard")
if getSpentTime
.date
+cardSpentTime
if $eq section "cover"
if cover
if currentBoard.allowsCoverAttachmentOnMinicard
//- An img, not a CSS background: it can fall back to the original
//- once (attachments.js previewFallback), and it is drawn only
//- when there is a URL - never url('?dummyReload...').
if attachmentPreviewUrl cover
.minicard-cover
img.minicard-cover-image(alt="" src="{{attachmentPreviewUrl cover}}?dummyReloadAfterSessionEstablished={{sess}}" width="512" height="120" decoding="async")
if $eq section "labels"
unless labelsAboveTitle
+minicardLabels(show=showLabels labels=labels hideText=hiddenMinicardLabelText)
if $eq section "customFields"
if showCustomFieldsOnMinicard
.minicard-custom-fields
each customFieldsWD
if definition.showOnCard
//- #6811: only a field with something in it, as before the
minicard's sections were rewritten - a field a card carries
because it is on every card is empty until filled in.
if minicardCustomFieldHasValue
.minicard-custom-field
// If there is custom field label, show label at left,
// and value at right
if definition.showLabelOnMiniCard
.minicard-custom-field-item
+viewer
= definition.name
.minicard-custom-field-item
if $eq definition.type "currency"
+viewer
= formattedCurrencyCustomFieldValue(definition)
else if $eq definition.type "date"
.date
+minicardCustomFieldDate
else if $eq definition.type "checkbox"
i.fa.minicard-custom-field-checkbox-icon(class="{{#if value}}fa-check-circle minicard-custom-field-checkbox-true{{else}}fa-times-circle minicard-custom-field-checkbox-false{{/if}}" title="{{#if value}}{{_ 'yes'}}{{else}}{{_ 'no'}}{{/if}}")
else if $eq definition.type "stringtemplate"
+viewer
= formattedStringtemplateCustomFieldValue(definition)
else
+viewer
= trueValue
else
// If there is no custom field label,
// show value full width
.minicard-custom-field-item-fullwidth
if $eq definition.type "currency"
+viewer
= formattedCurrencyCustomFieldValue(definition)
else if $eq definition.type "date"
.date
+minicardCustomFieldDate
else if $eq definition.type "checkbox"
i.fa.minicard-custom-field-checkbox-icon(class="{{#if value}}fa-check-circle minicard-custom-field-checkbox-true{{else}}fa-times-circle minicard-custom-field-checkbox-false{{/if}}" title="{{#if value}}{{_ 'yes'}}{{else}}{{_ 'no'}}{{/if}}")
else if $eq definition.type "stringtemplate"
+viewer
= formattedStringtemplateCustomFieldValue(definition)
else
+viewer
= trueValue
if $eq section "assignee"
if showAssignee
if getAssignees
.minicard-assignees.js-minicard-assignees
each getAssignees
+userAvatar(userId=this)
if $eq section "members"
if showMembers
if getMembers
.minicard-members.js-minicard-members
each getMembers
+userAvatar(userId=this)
if $eq section "creator"
if showCreatorOnMinicard
.minicard-creator
+userAvatar(userId=getCreatorId noRemove=true)
if $eq section "checklists"
if shouldShowChecklistAtMinicard
each shouldShowChecklistAtMinicard
+minicardChecklist(checklist=. card=..)
if $eq section "badges"
//- At risk and Forecast, Show on Minicard: badges in the dates line,
after the date their row follows in Card Settings.
if $eq field "atRisk"
unless $eq currentBoard.allowsAtRiskOnMinicard false
with atRisk
.badge.badge-state-image-only.minicard-at-risk.js-minicard-at-risk(title=text)
span.badge-icon
i.fa.fa-exclamation-triangle
if $eq field "forecast"
if currentBoard.allowsForecastOnMinicard
with cardForecastOf this
if enough
.badge.minicard-forecast.js-minicard-forecast(title="{{_ 'forecast-done' p50=(forecastDate p50) p85=(forecastDate p85)}}")
span.badge-icon
i.fa.fa-line-chart
span.badge-text {{forecastDate p85}}
else if group.badges
.badges
//- #2012: a Story or a Task.
if cardKindIcon
.badge.card-kind-badge(title="{{cardKindTitle}}")
span.badge-icon
i.fa(class=cardKindIcon)
//- At risk, Show on Minicard (docs/Features/Cards/At-Risk.md): the
reasons are its title.
unless $eq currentBoard.allowsAtRiskOnMinicard false
with atRisk
.badge.badge-state-image-only.minicard-at-risk.js-minicard-at-risk(title=text)
span.badge-icon
i.fa.fa-exclamation-triangle
//- Forecast, Show on Minicard: off until a board turns it on
(docs/Features/Cards/Dates/Forecast.md).
if currentBoard.allowsForecastOnMinicard
with cardForecastOf this
if enough
.badge.minicard-forecast.js-minicard-forecast(title="{{_ 'forecast-done' p50=(forecastDate p50) p85=(forecastDate p85)}}")
each field in group.fields
//- #2012: a Story or a Task - first of the Scrum section.
if $eq field "cardKind"
if cardKindIcon
.badge.card-kind-badge(title="{{cardKindTitle}}")
span.badge-icon
i.fa.fa-line-chart
span.badge-text {{forecastDate p85}}
//- The description BADGE - shown when the description's text is not
on the minicard - belongs to the Description text row, whose own
position is the full text further down; it keeps the first place
of the strip.
if getDescription
unless currentBoard.allowsDescriptionTextOnMinicard
//- Description badge, Show on Minicard.
unless $eq currentBoard.allowsDescriptionBadgeOnMinicard false
.badge.badge-state-image-only(title=getDescription)
span.badge-icon
i.fa.fa-file-text-o
//- #6688: every badge has a Board Settings / Card "Show on Minicard"
toggle (the cheap board flag is read first, so a board that turned a
badge off never counts what it would have shown). Dependencies,
stickers, the comment count, vote and poker rendered unconditionally
before; all default on. client/components/sidebar/sidebar.jade
each field in orderedMinicardBadges
//- Jira issue type (models/lib/issueTypeIcon.js): first by default,
because it says what kind of card this is; its row is "Scrum
settings: Work item type".
i.fa(class=cardKindIcon)
//- #6688: every badge has a Board Settings / Card "Show on Minicard"
toggle (the cheap board flag is read first, so a board that turned a
badge off never counts what it would have shown). Dependencies,
stickers, the comment count, vote and poker rendered unconditionally
before; all default on. client/components/sidebar/sidebar.jade
if $eq field "scrumIssueType"
if showIssueTypeOnMinicard
if issueTypeBadge
@@ -344,16 +252,16 @@ template(name="minicard")
i.fa.fa-paperclip
span.badge-text= attachments.length
if $eq field "subtasks"
if showSubtasks
if allSubtasks.count
.badge
span.badge-icon
i.fa.fa-globe
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
//- #6431: compact checklist item-count badge (finished/total). The outer
guard is a cheap boolean, so when the setting is off the checklist
counting below never runs.
if showSubtasks
if allSubtasks.count
.badge
span.badge-icon
i.fa.fa-globe
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
//- #6431: compact checklist item-count badge (finished/total). The outer
guard is a cheap boolean, so when the setting is off the checklist
counting below never runs.
if $eq field "checklistCount"
if showChecklistCountBadge
if checklists.length
@@ -368,104 +276,199 @@ template(name="minicard")
span.badge-icon
i.fa.fa-sort-numeric-asc
span.badge-text.check-list-sort {{ sort }}
//- Card buttons, Show on Minicard: the board's rule buttons, under the
badges. Off until chosen; the board subscribes to its rules once.
if currentBoard.allowsCardButtonsOnMinicard
+minicardCardButtons(_id=_id boardId=boardId)
if $eq section "descriptionText"
if currentBoard.allowsDescriptionTextOnMinicard
if getDescription
//- Description title, Show on Minicard: the heading over the text.
unless $eq currentBoard.allowsDescriptionTitleOnMinicard false
.minicard-description-title
i.fa.fa-file-text-o
| {{_ 'description'}}
.minicard-description
+viewer
| {{ getDescription }}
//- #4285: compact preview of the most recent comments on the minicard.
Opt-in (allowsCommentsOnMinicard), display-only - no reply/edit here,
just enough to see at a glance, with a "more" link that opens the
full card (the whole minicard is already a link to it, see
listBody.jade's a.minicard-wrapper.js-minicard).
if $eq section "comments"
if showCommentsOnMinicard
with commentsForMinicard
if comments.length
.minicard-comments
each comments
.minicard-comment
span.minicard-comment-icon
i.fa.fa-comment-o
span.minicard-comment-text {{ text }}
if hasMore
a.minicard-comments-more(title="{{_ 'card-comments-on-minicard'}}")
i.fa.fa-caret-down
| {{_ 'card-comments-more'}}
if $eq section "showLists"
if shouldShowListOnMinicard
.minicard-list-name
i.fa.fa-list
| {{ listName }}
if $eq section "swimlaneName"
if shouldShowSwimlaneNameOnMinicard
.minicard-swimlane-name
i.fa.fa-list-alt
| {{ swimlaneName }}
//- What the opened card shows that the minicard did not, each with its
Board Settings / Card "Show on Minicard" row (2026-10-02). Compact and
read-only: the minicard is a link to the card, which edits them.
if $eq section "location"
if currentBoard.allowsLocationOnMinicard
each getLocations
.minicard-location
i.fa.fa-map-marker
| {{#if name}}{{name}}{{else}}{{address}}{{/if}}
if $eq section "requestedBy"
unless $eq currentBoard.allowsRequestedByOnMinicard false
if getRequestedBy
.minicard-requested-by(title="{{_ 'requested-by'}}")
i.fa.fa-shopping-cart
| {{ getRequestedBy }}
if $eq section "assignedBy"
unless $eq currentBoard.allowsAssignedByOnMinicard false
if getAssignedBy
.minicard-assigned-by(title="{{_ 'assigned-by'}}")
i.fa.fa-user-plus
| {{ getAssignedBy }}
if $eq section "flowtime"
if currentBoard.allowsFlowtimeOnMinicard
if isFlowActive
.badge.minicard-flowtime(title="{{_ 'flowtime'}}")
span.badge-icon
i.fa.fa-bolt
span.badge-text {{_ 'flow-interruptions' getFlowInterruptions}}
if $eq section "pomodoro"
if currentBoard.allowsPomodoroOnMinicard
if isPomodoroActive
.badge.minicard-pomodoro(title="{{_ 'pomodoro'}}")
span.badge-icon
i.fa.fa-clock-o
span.badge-text {{_ 'pomodoro-completed' getPomodoroCount}}
if $eq section "attachments"
if currentBoard.allowsAttachmentListOnMinicard
each minicardAttachmentNames
.minicard-attachment-name
i.fa.fa-paperclip
| {{cleanFilename this}}
if $eq section "textNotes"
if currentBoard.allowsTextNotesOnMinicard
if textNotesCount
.badge.minicard-text-notes(title="{{_ 'text-notes'}}")
span.badge-icon
i.fa.fa-file-text-o
span.badge-text {{ textNotesCount }}
if $eq section "activities"
if currentBoard.allowsActivitiesOnMinicard
if dateLastActivity
.minicard-last-activity(title="{{_ 'activities'}}")
i.fa.fa-history
| {{ minicardLastActivity }}
//- The description BADGE, shown when the description's text is not on
the minicard: in Description, after the text's row.
if $eq field "descriptionBadge"
if getDescription
unless currentBoard.allowsDescriptionTextOnMinicard
//- Description badge, Show on Minicard.
unless $eq currentBoard.allowsDescriptionBadgeOnMinicard false
.badge.badge-state-image-only(title=getDescription)
span.badge-icon
i.fa.fa-file-text-o
else
each field in group.fields
if $eq field "scrumSprint"
+scrumMetadata(kind="minicard" record=this field="Sprint")
if $eq field "scrumPastSprints"
+scrumMetadata(kind="minicard" record=this field="PastSprints")
if $eq field "scrumRelease"
+scrumMetadata(kind="minicard" record=this field="Release")
if $eq field "scrumAcceptanceCriteria"
+scrumMetadata(kind="minicard" record=this field="AcceptanceCriteria")
if $eq field "scrumBacklogRank"
+scrumMetadata(kind="minicard" record=this field="BacklogRank")
if $eq field "labels"
unless labelsAboveTitle
+minicardLabels(show=showLabels labels=labels hideText=hiddenMinicardLabelText)
if $eq field "customFields"
if showCustomFieldsOnMinicard
.minicard-custom-fields
each customFieldsWD
if definition.showOnCard
//- #6811: only a field with something in it, as before the
minicard's sections were rewritten - a field a card carries
because it is on every card is empty until filled in.
if minicardCustomFieldHasValue
.minicard-custom-field
// If there is custom field label, show label at left,
// and value at right
if definition.showLabelOnMiniCard
.minicard-custom-field-item
+viewer
= definition.name
.minicard-custom-field-item
if $eq definition.type "currency"
+viewer
= formattedCurrencyCustomFieldValue(definition)
else if $eq definition.type "date"
.date
+minicardCustomFieldDate
else if $eq definition.type "checkbox"
i.fa.minicard-custom-field-checkbox-icon(class="{{#if value}}fa-check-circle minicard-custom-field-checkbox-true{{else}}fa-times-circle minicard-custom-field-checkbox-false{{/if}}" title="{{#if value}}{{_ 'yes'}}{{else}}{{_ 'no'}}{{/if}}")
else if $eq definition.type "stringtemplate"
+viewer
= formattedStringtemplateCustomFieldValue(definition)
else
+viewer
= trueValue
else
// If there is no custom field label,
// show value full width
.minicard-custom-field-item-fullwidth
if $eq definition.type "currency"
+viewer
= formattedCurrencyCustomFieldValue(definition)
else if $eq definition.type "date"
.date
+minicardCustomFieldDate
else if $eq definition.type "checkbox"
i.fa.minicard-custom-field-checkbox-icon(class="{{#if value}}fa-check-circle minicard-custom-field-checkbox-true{{else}}fa-times-circle minicard-custom-field-checkbox-false{{/if}}" title="{{#if value}}{{_ 'yes'}}{{else}}{{_ 'no'}}{{/if}}")
else if $eq definition.type "stringtemplate"
+viewer
= formattedStringtemplateCustomFieldValue(definition)
else
+viewer
= trueValue
if $eq field "assignee"
if showAssignee
if getAssignees
.minicard-assignees.js-minicard-assignees
each getAssignees
+userAvatar(userId=this)
if $eq field "members"
if showMembers
if getMembers
.minicard-members.js-minicard-members
each getMembers
+userAvatar(userId=this)
if $eq field "creator"
if showCreatorOnMinicard
.minicard-creator
+userAvatar(userId=getCreatorId noRemove=true)
if $eq field "checklists"
if shouldShowChecklistAtMinicard
each shouldShowChecklistAtMinicard
+minicardChecklist(checklist=. card=..)
if $eq field "descriptionText"
if currentBoard.allowsDescriptionTextOnMinicard
if getDescription
//- Description title, Show on Minicard: the heading over the text.
unless $eq currentBoard.allowsDescriptionTitleOnMinicard false
.minicard-description-title
i.fa.fa-file-text-o
| {{_ 'description'}}
.minicard-description
+viewer
| {{ getDescription }}
//- #4285: compact preview of the most recent comments on the minicard.
Opt-in (allowsCommentsOnMinicard), display-only - no reply/edit here,
just enough to see at a glance, with a "more" link that opens the
full card (the whole minicard is already a link to it, see
listBody.jade's a.minicard-wrapper.js-minicard).
if $eq field "comments"
if showCommentsOnMinicard
with commentsForMinicard
if comments.length
.minicard-comments
each comments
.minicard-comment
span.minicard-comment-icon
i.fa.fa-comment-o
span.minicard-comment-text {{ text }}
if hasMore
a.minicard-comments-more(title="{{_ 'card-comments-on-minicard'}}")
i.fa.fa-caret-down
| {{_ 'card-comments-more'}}
if $eq field "showLists"
if shouldShowListOnMinicard
.minicard-list-name
i.fa.fa-list
| {{ listName }}
if $eq field "swimlaneName"
if shouldShowSwimlaneNameOnMinicard
.minicard-swimlane-name
i.fa.fa-list-alt
| {{ swimlaneName }}
//- What the opened card shows that the minicard did not, each with its
Board Settings / Card "Show on Minicard" row (2026-10-02). Compact and
read-only: the minicard is a link to the card, which edits them.
if $eq field "location"
if currentBoard.allowsLocationOnMinicard
each getLocations
.minicard-location
i.fa.fa-map-marker
| {{#if name}}{{name}}{{else}}{{address}}{{/if}}
if $eq field "requestedBy"
unless $eq currentBoard.allowsRequestedByOnMinicard false
if getRequestedBy
.minicard-requested-by(title="{{_ 'requested-by'}}")
i.fa.fa-shopping-cart
| {{ getRequestedBy }}
if $eq field "assignedBy"
unless $eq currentBoard.allowsAssignedByOnMinicard false
if getAssignedBy
.minicard-assigned-by(title="{{_ 'assigned-by'}}")
i.fa.fa-user-plus
| {{ getAssignedBy }}
if $eq field "flowtime"
if currentBoard.allowsFlowtimeOnMinicard
if isFlowActive
.badge.minicard-flowtime(title="{{_ 'flowtime'}}")
span.badge-icon
i.fa.fa-bolt
span.badge-text {{_ 'flow-interruptions' getFlowInterruptions}}
if $eq field "pomodoro"
if currentBoard.allowsPomodoroOnMinicard
if isPomodoroActive
.badge.minicard-pomodoro(title="{{_ 'pomodoro'}}")
span.badge-icon
i.fa.fa-clock-o
span.badge-text {{_ 'pomodoro-completed' getPomodoroCount}}
if $eq field "attachments"
if currentBoard.allowsAttachmentListOnMinicard
each minicardAttachmentNames
.minicard-attachment-name
i.fa.fa-paperclip
| {{cleanFilename this}}
if $eq field "textNotes"
if currentBoard.allowsTextNotesOnMinicard
if textNotesCount
.badge.minicard-text-notes(title="{{_ 'text-notes'}}")
span.badge-icon
i.fa.fa-file-text-o
span.badge-text {{ textNotesCount }}
if $eq field "activities"
if currentBoard.allowsActivitiesOnMinicard
if dateLastActivity
.minicard-last-activity(title="{{_ 'activities'}}")
i.fa.fa-history
| {{ minicardLastActivity }}
//- Card buttons, Show on Minicard: the board's rule buttons, after the
sorting number. Off until chosen; the board subscribes to its rules once.
if $eq field "cardButtons"
if currentBoard.allowsCardButtonsOnMinicard
+minicardCardButtons(_id=_id boardId=boardId)
unless $eq currentBoard.allowsParentCardOnMinicard false
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
.parent-subtext
+79 -51
View File
@@ -27,12 +27,37 @@ import {
hiddenMinicardLabelText,
toggleMinicardLabelText,
} from '/client/lib/minicardLabelText';
const {
orderedMinicardSections,
orderedMinicardFieldsOf,
} = require('/models/lib/cardFieldOrder');
const { applyMinicardOrder } = require('/models/lib/cardFieldOrder');
const cardSettingsSections = require('/models/lib/cardSettingsSections');
const { minicardGroups } = require('/models/lib/minicardGroups');
const { isCardFieldHiddenOnBoard } = require('/models/lib/cardFieldVisibility');
// The board as every minicard of it sees it (models/lib/cardShownFields.js
// boardForMinicard): the rows of a Card Settings section hidden whole on Show
// on Minicard read false. Every helper below that decides whether the minicard
// draws a field reads this, not the board itself; Card Settings reads the
// board, so its checkboxes keep showing each row's own flag.
const { boardForMinicard, hidesRow } = require('/models/lib/cardShownFields');
function minicardBoardOf(data) {
const boardId = (data && (data.boardId || (data.card && data.card.boardId))) || null;
const board = boardId ? ReactiveCache.getBoard(boardId) : Utils.getCurrentBoard();
return boardForMinicard(board || Utils.getCurrentBoard());
}
// The minicard and its sub-templates read `currentBoard.allowsXOnMinicard`;
// this template-level helper answers before the global one, as the opened
// card's CARD_FIELD_TEMPLATES do (client/components/cards/cardAddFields.js).
export const MINICARD_FIELD_TEMPLATES = Object.freeze(['minicard', 'minicardChecklist']);
MINICARD_FIELD_TEMPLATES.forEach(name => {
Template[name].helpers({
currentBoard() {
const tpl = Template.instance();
return minicardBoardOf(tpl && tpl.data);
},
});
});
function getMinicardFlag(board, onMinicardField, legacyField, defaultValue) {
if (!board) return false;
if (board[onMinicardField] !== null && board[onMinicardField] !== undefined) {
@@ -61,34 +86,32 @@ Template.minicard.helpers({
return commentBadgeTitle(this.comments(), count => TAPi18n.__('card-comments-title', count));
},
// #1591: the whole-minicard fold, same shape as a list's collapsed() helper.
showCollapseControl() { return ReactiveCache.getBoard(this.boardId)?.allowsMinicardCollapse !== false; },
showCollapseControl() { return minicardBoardOf(this)?.allowsMinicardCollapse !== false; },
showLabelsAboveTitle() {
const board = ReactiveCache.getBoard(this.boardId);
const board = minicardBoardOf(this);
return board?.labelsAboveTitleOnMinicard === true && (board.allowsMinicardCollapse === false || !Utils.getCardCollapseState(this));
},
labelsAboveTitle() { return ReactiveCache.getBoard(this.boardId)?.labelsAboveTitleOnMinicard === true; },
labelsAboveTitle() { return minicardBoardOf(this)?.labelsAboveTitleOnMinicard === true; },
minicardCollapsed() {
return ReactiveCache.getBoard(this.boardId)?.allowsMinicardCollapse !== false && Utils.getCardCollapseState(this);
return minicardBoardOf(this)?.allowsMinicardCollapse !== false && Utils.getCardCollapseState(this);
},
// The minicard's field order from Board Settings / Card's "Show on
// Minicard" column (board.minicardFieldOrder): the sections top to bottom,
// and the fields inside the two grouped ones - the `.dates` line and the
// `.badges` strip. A board that never set it gets the historical order.
// models/lib/cardFieldOrder.js
orderedMinicardSections() {
const board = this.board();
return orderedMinicardSections(board?.minicardFieldOrder);
},
orderedMinicardDates() {
const board = this.board();
return orderedMinicardFieldsOf(board?.minicardFieldOrder, 'dates');
},
orderedMinicardBadges() {
const board = this.board();
return orderedMinicardFieldsOf(board?.minicardFieldOrder, 'badges');
// Card Settings' sections in the board's Show on Minicard section order
// (minicardSettingsSectionOrder), less those hidden whole on Show on
// Minicard, each section's fields in its Show on Minicard row order
// (board.minicardFieldOrder, models/lib/cardFieldOrder.js) - grouped into
// the `.dates` line, the `.badges` strips and blocks
// (models/lib/minicardGroups.js). The raw board for the orders, the
// minicard's view of it for nothing here: the gates are in the template.
minicardGroups() {
const boardId = this && this.boardId;
const board = (boardId && ReactiveCache.getBoard(boardId)) || Utils.getCurrentBoard();
const hidden = cardSettingsSections.hiddenSectionsOf(board, 'minicard');
const flat = applyMinicardOrder(board?.minicardFieldOrder);
const ids = cardSettingsSections.sectionOrderOf(board, 'minicard').filter(id => !hidden.includes(id));
return minicardGroups(ids, id => cardSettingsSections.sectionFieldsOn('minicard', flat, id));
},
showCustomFieldsOnMinicard() {
const board = this.board();
const board = minicardBoardOf(this);
return board?.allowsCustomFieldsOnMinicard === true;
},
// #3078: highlight the minicard when it has comments the current user has
@@ -118,7 +141,7 @@ Template.minicard.helpers({
const card = this;
if (!card || !card._id) return false;
// Unread comments, Show on Minicard: on unless turned off.
if (Utils.getCurrentBoard()?.allowsUnreadCommentsOnMinicard === false) return false;
if (minicardBoardOf(this)?.allowsUnreadCommentsOnMinicard === false) return false;
const comments = ReactiveCache.getCardComments({ cardId: card._id }) || [];
if (!comments.length) return false;
// #6745: only this card's own last-viewed time, so opening ANOTHER card
@@ -228,7 +251,7 @@ Template.minicard.helpers({
showCreatorOnMinicard() {
// cache "board" to reduce the mini-mongodb access
const board = this.board();
const board = minicardBoardOf(this);
let ret = false;
if (board) {
ret = board.allowsCreatorOnMinicard ?? false;
@@ -240,54 +263,54 @@ Template.minicard.helpers({
},
showMembers() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsMembersOnMinicard', 'allowsMembers', true);
},
showAssignee() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsAssigneeOnMinicard', 'allowsAssignee', true);
},
showReceived() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsReceivedDateOnMinicard', 'allowsReceivedDate', true);
},
showStart() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsStartDateOnMinicard', 'allowsStartDate', true);
},
showDue() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsDueDateOnMinicard', 'allowsDueDate', true);
},
showEnd() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsEndDateOnMinicard', 'allowsEndDate', true);
},
// #2530: accumulated spent-time badge on the minicard. Defaults to true
// (models/boards.js) since it was already shown unconditionally; this only
// lets an admin turn it off via Card Settings.
showSpentTime() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsSpentTimeOnMinicard', 'allowsSpentTime', true);
},
showLabels() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsLabelsOnMinicard', 'allowsLabels', true);
},
showCardNumber() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsCardNumberOnMinicard', 'allowsCardNumber', false);
},
// "Mark as complete" toggle on the minicard. No legacy fallback to the
// card-details setting (allowsDueComplete): hidden by default so it only
// appears when explicitly enabled in Card Settings > Show at Minicard.
showDueComplete() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsDueCompleteOnMinicard', null, false);
},
showSubtasks() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsSubtasksOnMinicard', 'allowsSubtasks', true);
},
// #6688: the minicard badges that had no Board Settings / Card toggle.
@@ -296,23 +319,23 @@ Template.minicard.helpers({
// flag is NOT a fallback here - hiding the Dependencies section of the
// opened card is a different decision from hiding its badge.
showDependenciesOnMinicard() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsDependenciesOnMinicard', null, true);
},
showStickersOnMinicard() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsStickersOnMinicard', null, true);
},
showCommentCountOnMinicard() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsCommentCountOnMinicard', null, true);
},
showVoteOnMinicard() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsVoteOnMinicard', null, true);
},
showPokerOnMinicard() {
const board = this.board();
const board = minicardBoardOf(this);
return getMinicardFlag(board, 'allowsPokerOnMinicard', null, true);
},
// #6431: compact checklist item-count badge (finished/total) on the minicard.
@@ -320,7 +343,7 @@ Template.minicard.helpers({
// counting (checklistFinishedCount/checklistItemCount) is gated behind this in
// the template, so a board with the badge off spends no resources counting.
showChecklistCountBadge() {
const board = this.board();
const board = minicardBoardOf(this);
return !!(board && board.allowsChecklistCountBadgeOnMinicard);
},
// #4285: show a card's comments directly on the minicard. Opt-in and OFF by
@@ -329,7 +352,7 @@ Template.minicard.helpers({
// built (and the comment badge's `comments` helper already runs
// unconditionally for the count badge) when this returns true.
showCommentsOnMinicard() {
const board = this.board();
const board = minicardBoardOf(this);
return !!(board && board.allowsCommentsOnMinicard);
},
// Compact, truncated preview of the most recent comments for the minicard.
@@ -384,10 +407,10 @@ Template.minicard.helpers({
// as it was on every board before it had a row.
// Card color, Show on Minicard: on unless turned off.
showCardColorOnMinicard() {
return Utils.getCurrentBoard()?.allowsCardColorOnMinicard !== false;
return minicardBoardOf(this)?.allowsCardColorOnMinicard !== false;
},
showIssueTypeOnMinicard() {
return Utils.getCurrentBoard()?.allowsIssueTypeOnMinicard !== false;
return minicardBoardOf(this)?.allowsIssueTypeOnMinicard !== false;
},
cover() {
// #5666: for a linked card the cover lives on the real card it points at, so
@@ -429,12 +452,14 @@ Template.minicard.helpers({
// Show list name if either:
// 1. Board-wide setting is enabled, OR
// 2. This specific card has the setting enabled
const currentBoard = this.board();
const currentBoard = minicardBoardOf(this);
if (!currentBoard) return false;
if (currentBoard.allowsShowListsOnMinicard) return true;
// The card's own List title, unless unticked in Admin Panel / Settings /
// Visibility / Features (models/lib/cardFieldVisibility.js).
return !isCardFieldHiddenOnBoard(currentBoard, 'listTitle') && Boolean(this.getRealCard().showListOnMinicard);
// ...and unless its section (Card) is hidden whole on Show on Minicard.
return !isCardFieldHiddenOnBoard(currentBoard, 'listTitle') && !hidesRow(currentBoard, 'minicard', 'listTitle')
&& Boolean(this.getRealCard().showListOnMinicard);
},
// #2426: the swimlane a card belongs to isn't otherwise visible on its
@@ -442,7 +467,7 @@ Template.minicard.helpers({
// swimlane), so this board-wide toggle shows the swimlane's name at the
// bottom of the minicard, the same place/style as the list name above.
shouldShowSwimlaneNameOnMinicard() {
const currentBoard = this.board();
const currentBoard = minicardBoardOf(this);
if (!currentBoard) return false;
return currentBoard.allowsSwimlaneNameOnMinicard === true;
},
@@ -455,8 +480,11 @@ Template.minicard.helpers({
shouldShowChecklistAtMinicard() {
// Return checklists that should be shown on minicard
const currentBoard = this.board();
const currentBoard = minicardBoardOf(this);
if (!currentBoard) return [];
// A checklist's own "Show at minicard" overrides the board's default, so
// the Checklists section hidden whole on Show on Minicard is asked first.
if (hidesRow(currentBoard, 'minicard', 'checklists')) return [];
const checklists = this.checklists();
const visibleChecklists = [];
+7 -7
View File
@@ -1,5 +1,5 @@
.js-add-subtask {
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
}
textarea.js-add-subtask-item,
textarea.js-edit-subtask-item {
@@ -12,7 +12,7 @@ textarea.js-edit-subtask-item {
.subtask-title .js-delete-subtask,
.subtask-title .js-view-subtask,
.js-delete-subtask-item {
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
text-decoration: underline;
word-wrap: break-word;
flex-shrink: 0;
@@ -163,7 +163,7 @@ textarea.js-edit-subtask-item {
padding-inline-start: 10px;
}
.subtasks-item .item-title.is-checked {
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
font-style: italic;
}
.subtasks-item .item-title .viewer p {
@@ -186,7 +186,7 @@ textarea.js-edit-subtask-item {
instead of disappearing — the same dimmed/italic treatment a checked
checklist item gets (.subtasks-item .item-title.is-checked above). */
.subtask.is-completed .subtask-title .title {
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
font-style: italic;
text-decoration: line-through;
}
@@ -199,7 +199,7 @@ textarea.js-edit-subtask-item {
.subtasks-hide-completed-toggle {
display: inline-block;
margin: 0.2em 0 0.5em 1.33em;
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
cursor: pointer;
}
.subtasks-hide-completed-toggle:hover {
@@ -224,7 +224,7 @@ textarea.js-edit-subtask-item {
flex-shrink: 0;
margin: 0 0.5em;
padding-top: 6px;
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
font-size: 0.85em;
white-space: nowrap;
overflow: hidden;
@@ -251,5 +251,5 @@ textarea.js-edit-subtask-item {
}
.subtask-title.is-done .title {
text-decoration: line-through;
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
}
+1 -1
View File
@@ -28,4 +28,4 @@ template(name="cardWhiteboard")
svg.card-whiteboard-surface.js-whiteboard-surface(viewBox="0 0 {{width}} {{height}}" preserveAspectRatio="xMidYMid meet" class="{{#if canDraw}}can-draw{{/if}}")
rect(width=width height=height fill="#ffffff")
each strokes
path(d=pathD stroke=color stroke-width=width fill="none" stroke-linecap="round" stroke-linejoin="round")
path(d=pathD stroke=color stroke-width=strokeWidth fill="none" stroke-linecap="round" stroke-linejoin="round")
+5 -1
View File
@@ -116,7 +116,11 @@ Template.cardWhiteboard.helpers({
const current = tpl.current.get();
return [...saved, ...pending, ...(current ? [current] : [])]
.filter(s => s && Array.isArray(s.points) && s.points.length)
.map(s => ({ color: s.color, width: s.width, pathD: pathD(s.points) }));
// strokeWidth, not width: inside `each strokes` Spacebars looks a name up
// among this template's helpers before the stroke's own fields, and the
// width helper is the drawing's (800) - every line was drawn 800 wide and
// covered the whiteboard.
.map(s => ({ color: s.color, strokeWidth: s.width, pathD: pathD(s.points) }));
},
saving() { return Template.instance().saving.get(); },
message() { return Template.instance().message.get(); },
+25 -2
View File
@@ -102,6 +102,29 @@
padding-inline: 4px;
}
.selected-calendar-heading .selected-calendar-date {
margin-inline-start: 4px;
/* #6838: a word, not an arrow - it may be wider than the arrows beside it. */
.selected-calendar-heading .js-calendar-today {
white-space: nowrap;
padding-inline: 6px;
}
/* #6853: year, month and day drop-downs under the calendar. */
.selected-calendar-selects {
display: flex;
gap: 4px;
margin-block-start: 6px;
}
.selected-calendar-selects select,
.selected-calendar-selects input {
flex: 1;
box-sizing: border-box;
height: auto;
margin-bottom: 0;
min-width: 0;
padding-inline: 4px;
}
.selected-calendar-selects .js-calendar-year { flex: 1.4; }
.calendar-date-input .selected-calendar-selects select:focus-visible,
.calendar-date-input .selected-calendar-selects input:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
+15 -3
View File
@@ -20,8 +20,9 @@ template(name="calendarDateInput")
button.primary.js-calendar-previous(type="button" aria-label="{{_ 'previous'}} {{_ 'month'}}") ‹
span(aria-live="polite")
| {{monthLabel}}
if inline
span.selected-calendar-date {{headingDateText}}
//- #6838: back to today's month from wherever the arrows went. It only
//- shows the month; the date stays as it is until a day is clicked.
button.primary.js-calendar-today(type="button" aria-label="{{_ 'today'}}") {{_ 'today'}}
button.primary.js-calendar-next(type="button" aria-label="{{_ 'next'}} {{_ 'month'}}") ›
button.primary.js-calendar-year-next(type="button" aria-label="{{_ 'next'}} {{_ 'predicate-year'}}") »
table
@@ -34,5 +35,16 @@ template(name="calendarDateInput")
tr
each days
td(class="{{#if this.today}}is-today-cell{{/if}}")
if this
unless this.empty
button.primary.js-calendar-day(type="button" class="{{#if today}}is-today{{/if}}" data-date=iso title=title aria-label=title aria-pressed=selected aria-current=ariaCurrent tabindex=tabIndex) {{label}}
//- #6853: any year, month and day by a pick, under the calendar.
.selected-calendar-selects
input.js-calendar-year(type="number" step="1" inputmode="numeric" value=yearValue aria-label="{{_ 'predicate-year'}}")
select.js-calendar-month(aria-label="{{_ 'month'}}")
each monthOptions
option(value=value selected=selected) {{label}}
select.js-calendar-day-select(aria-label="{{_ 'day'}}")
if noDaySelected
option(value="" selected disabled) –
each dayOptions
option(value=value selected=selected) {{label}}
+89 -7
View File
@@ -6,7 +6,10 @@ import { TAPi18n } from '/imports/i18n';
import { formatDate } from '/imports/lib/dateUtils';
import { formatDateForDisplay, dateDisplayPreferences } from '/client/lib/dateDisplay';
const { CALENDAR_SYSTEMS, nativeCalendarParts } = require('/imports/lib/calendarSystems');
const { calendarMonthDays, calendarMonthRange, shiftedDay, shiftCalendarMonth, shiftCalendarYear } = require('/imports/lib/calendarMonth');
const {
calendarMonthDays, calendarMonthRange, shiftedDay, shiftCalendarMonth, shiftCalendarYear,
calendarSignedYear, dateInCalendarYear, calendarYearMonths, sameDayInMonth,
} = require('/imports/lib/calendarMonth');
function fromInput(value) {
if (!value) return null;
@@ -53,9 +56,45 @@ Template.calendarDateInput.helpers({
useNativeInput() { return !this.inline && dateDisplayPreferences().calendarSystem === 'gregorian'; },
inputType() { return this.withTime ? 'datetime-local' : 'date'; },
selectedDateText() { return formatDateForDisplay(Template.instance().selectedDate.get(), false); },
headingDateText() {
// #6853: the year, month and day drop-downs under the calendar. Each option
// carries a date as its value, found in the selected calendar system, so any
// past, present or future year, month and day is one pick away.
// The year as a number any year can be typed or stepped to, also one before
// the era (0 is 1 BC) or thousands of years away.
yearValue() { return calendarSignedYear(Template.instance().shownDate.get(), selectedSystem().intl); },
monthOptions() {
const tpl = Template.instance();
return formatDateForDisplay(tpl.selectedDate.get() || tpl.shownDate.get(), false);
const calendar = selectedSystem().intl;
const language = TAPi18n.getLanguage() || 'en';
return calendarYearMonths(tpl.shownDate.get(), calendar).map(month => {
let label;
try {
label = new Intl.DateTimeFormat(language, { calendar, month: 'long', numberingSystem: 'latn' }).format(month.date);
} catch (error) { label = formatDateForDisplay(month.date, false); }
return { _id: formatDate(month.date), value: formatDate(month.date), label, selected: month.selected };
});
},
dayOptions() {
const tpl = Template.instance();
const calendar = selectedSystem().intl;
const { start, end } = calendarMonthRange(tpl.shownDate.get(), calendar);
const selected = tpl.selectedDate.get();
const selectedIso = selected ? formatDate(selected) : '';
const days = [];
for (let day = shiftedDay(start, 0); day < end; day = shiftedDay(day, 1)) {
days.push({
_id: formatDate(day), value: formatDate(day),
label: nativeCalendarParts(day, calendar).day, selected: formatDate(day) === selectedIso,
});
}
return days;
},
noDaySelected() {
const tpl = Template.instance();
const selected = tpl.selectedDate.get();
if (!selected) return true;
const { start, end } = calendarMonthRange(tpl.shownDate.get(), selectedSystem().intl);
return selected < start || selected >= end;
},
expanded() { return !!Template.instance().data.inline || Template.instance().expanded.get(); },
calendarName() { return TAPi18n.__(selectedSystem().labelKey); },
@@ -63,8 +102,11 @@ Template.calendarDateInput.helpers({
const date = Template.instance().shownDate.get();
try {
return new Intl.DateTimeFormat(TAPi18n.getLanguage() || 'en', {
calendar: selectedSystem().intl, month: 'long',
...(Template.instance().data.inline ? {} : { year: 'numeric' }), numberingSystem: 'latn',
// #6853: the year is always shown, also in the popup: month and year
// above the calendar, never the YYYY-MM-DD of the selected day.
calendar: selectedSystem().intl, month: 'long', year: 'numeric', numberingSystem: 'latn',
// A year before the era needs its era, or 3000 BC reads as 3000.
...(calendarSignedYear(date, selectedSystem().intl) <= 0 ? { era: 'short' } : {}),
}).format(date);
} catch (error) { return formatDateForDisplay(date, false); }
},
@@ -86,9 +128,17 @@ Template.calendarDateInput.helpers({
// the browser's local day, the same local calendar day formatDate() gives
// every other cell, so it is the cell the user means by "today".
const todayIso = formatDate(new Date());
// #6838: every row and every cell - an empty one too - has its own _id.
// Blaze matches list items by _id; without one, going from one month to
// another reused cells by position and the empty cells, all alike, were
// moved around: October came back as "29, empty, 30, empty", without the
// 31st. The month is part of each id, so a new month is new cells.
const month = days.find(Boolean);
const monthId = month ? formatDate(month) : 'empty';
const rows = [];
for (let index = 0; index < days.length; index += 7) {
rows.push({ days: days.slice(index, index + 7).map(date => date && ({
rows.push({ _id: `${monthId}-week-${index / 7}`, days: days.slice(index, index + 7).map((date, cell) => (date ? {
_id: formatDate(date),
iso: formatDate(date),
today: formatDate(date) === todayIso,
// null omits the attribute: only today's button carries aria-current.
@@ -97,7 +147,7 @@ Template.calendarDateInput.helpers({
title: formatDateForDisplay(date, false),
selected: tpl.selectedDate.get() && formatDate(tpl.selectedDate.get()) === formatDate(date),
tabIndex: focusDate && formatDate(focusDate) === formatDate(date) ? 0 : -1,
})) });
} : { _id: `${monthId}-empty-${index + cell}`, empty: true })) });
}
return rows;
},
@@ -121,8 +171,40 @@ Template.calendarDateInput.events({
event.preventDefault();
tpl.shownDate.set(calendarMonthRange(tpl.shownDate.get(), selectedSystem().intl).end);
},
'click .js-calendar-today'(event, tpl) {
event.preventDefault();
tpl.shownDate.set(new Date());
Tracker.afterFlush(() => tpl.find('.js-calendar-day.is-today')?.focus());
},
'click .js-calendar-year-previous'(event, tpl) { event.preventDefault(); tpl.shownDate.set(shiftCalendarYear(tpl.shownDate.get(), -1, selectedSystem().intl)); },
'click .js-calendar-year-next'(event, tpl) { event.preventDefault(); tpl.shownDate.set(shiftCalendarYear(tpl.shownDate.get(), 1, selectedSystem().intl)); },
// #6853: a pick in a drop-down selects that date and shows its month.
'change .js-calendar-year'(event, tpl) {
const input = event.currentTarget;
const target = dateInCalendarYear(tpl.shownDate.get(), Number(input.value), selectedSystem().intl);
if (!target || input.value.trim() === '') {
// A year that is not a whole number, or beyond what a date can hold:
// the field goes back to the year being shown.
input.value = calendarSignedYear(tpl.shownDate.get(), selectedSystem().intl);
return;
}
tpl.shownDate.set(target); tpl.focusedDate.set(target); writeValue(tpl, target);
},
// Enter in the year field applies the year, not the popup's Save.
'keydown .js-calendar-year'(event) {
if (event.key !== 'Enter') return;
event.preventDefault(); event.stopPropagation();
event.currentTarget.dispatchEvent(new Event('change', { bubbles: true }));
},
'change .js-calendar-month'(event, tpl) {
const target = sameDayInMonth(tpl.selectedDate.get() || tpl.shownDate.get(), fromInput(event.currentTarget.value), selectedSystem().intl);
tpl.shownDate.set(target); tpl.focusedDate.set(target); writeValue(tpl, target);
},
'change .js-calendar-day-select'(event, tpl) {
const target = fromInput(event.currentTarget.value);
if (!target) return;
tpl.shownDate.set(target); tpl.focusedDate.set(target); writeValue(tpl, target);
},
'click .js-calendar-day'(event, tpl) {
event.preventDefault();
writeValue(tpl, fromInput(event.currentTarget.dataset.date));
+9 -7
View File
@@ -192,9 +192,11 @@ body.mobile-mode [id^="swimlane-"] .list[style*="--list-width"]:not(.list-collap
min-height: 30px !important;
height: auto !important;
padding: 28px 21px 6px !important;
/* Make sure the background covers the full height */
background-color: #e4e4e4 !important;
border-bottom: 8px solid #e4e4e4 !important;
/* Make sure the background covers the full height. Not !important: it beat
every theme's own list header (moderndark's #6a6a6a, exodark's #222), and
left their light text on this grey at 1.09:1 and 1.27:1 (#6863). */
background-color: #e4e4e4;
border-bottom: 8px solid #e4e4e4;
/* Use original display for consistent button positioning */
display: block !important;
position: relative !important;
@@ -470,7 +472,7 @@ form.inlined-form .list-composer {
.list.list-composer .open-list-composer,
.list .list-composer .open-list-composer {
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
}
.list.list-composer .list-name-input,
.list .list-composer .list-name-input {
@@ -534,7 +536,7 @@ form.inlined-form .list-composer {
padding: 0;
border-radius: 0;
background: transparent;
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
font-weight: bold;
font-size: calc(12px * var(--wekan-ui-font-scale, 1));
vertical-align: middle;
@@ -722,7 +724,7 @@ form.inlined-form .list-composer {
}
.list-header .cardCount {
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
font-size: calc(12px * var(--wekan-ui-font-scale, 1));
font-weight: bold;
}
@@ -781,7 +783,7 @@ form.inlined-form .list-composer {
}
.list-body .open-minicard-composer {
border-radius: 2px;
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
display: block;
/* Smaller than a card title, because it is not a card: it is the affordance
for making one. At the inherited 16px it was the largest text in the list
+1 -1
View File
@@ -74,7 +74,7 @@
selected thing. The fill is the slide where the theme has one. */
.theme-color-picker ul.pop-over-list.theme-scope-row > li.active > a,
.theme-color-picker ul.pop-over-list.theme-scope-row > li.active > a:hover {
background: var(--theme-accent-fill, var(--theme-accent, #2980b9));
background: var(--theme-accent-fill, var(--theme-accent, #2573a7));
color: #fff !important;
}
.theme-color-picker ul.pop-over-list.theme-scope-row > li.active > a i {
+1 -1
View File
@@ -39,7 +39,7 @@ template(name="dueCards")
else
.global-search-results-list-wrapper
.no-results
h3 {{_ 'dueCards-noResults-title'}}
h2 {{_ 'dueCards-noResults-title'}}
p {{_ 'dueCards-noResults-description'}}
template(name="dueCardsViewChangePopup")
+1
View File
@@ -7,6 +7,7 @@ template(name="editor")
dir="auto"
class="{{class}}"
id=id
aria-label=labelText
autofocus=autofocus
placeholder="{{_ 'comment-placeholder'}}")
+Template.contentBlock
+15 -1
View File
@@ -6,7 +6,7 @@ import { Markdown } from 'meteor/wekan-markdown';
import { Utils } from '/client/lib/utils';
import { memberMatchesTerm } from '/models/lib/memberAutocomplete';
import { internalCardPath } from '/models/lib/internalCardLink';
import { parseAllowedUrlSchemes, hrefScheme, BUILT_IN } from '/models/lib/urlSchemeAllowlist';
import { parseAllowedUrlSchemes, hrefScheme, BUILT_IN, NEVER_LINKED } from '/models/lib/urlSchemeAllowlist';
import autosize from 'autosize';
var converter = require('@wekanteam/html-to-markdown');
@@ -45,6 +45,16 @@ function syncMarkdownUrlSchemes(setting) {
return schemes;
}
// WCAG 4.1.2: a field has a name. The including template passes the KEY of its
// name (labelKey="comment"), and the editor translates it, as every included
// template does (tests/jadeInclusionArgsNoMustache.test.cjs).
Template.editor.helpers({
labelText() {
const data = Template.currentData() || {};
return data.labelKey ? TAPi18n.__(data.labelKey) : null;
},
});
const specialHandles = [
{userId: 'board_members', username: 'board_members'},
{userId: 'card_members', username: 'card_members'},
@@ -348,6 +358,10 @@ Template.viewer.events({
const scheme = hrefScheme(href);
if (cardPath) {
FlowRouter.go(cardPath);
} else if (NEVER_LINKED.has(scheme)) {
// The sanitizer never lets one of these be a link; should one reach
// the page some other way, a click still does not launch its
// handler (ms-msdt:, search-ms:, telnet: ...).
} else if (scheme && !BUILT_IN.includes(scheme)) {
// #3218: an allowed custom scheme (thunderlink:, onenote:, ...) is
// handed to the application registered for it by following the
+2
View File
@@ -106,6 +106,8 @@ code {
}
.list-title {
background-color: #a9a9a9;
/* White on this grey was 2.35; black is 8.94 (WCAG 1.4.3). */
color: #000;
}
.global-search-footer {
border: none;
+3 -1
View File
@@ -4,6 +4,7 @@ import { Session } from 'meteor/session';
import { ReactiveCache } from '/imports/reactiveCache';
import { Template } from 'meteor/templating';
import { activeAccent, activeFill } from '/models/lib/themeAccents';
import { readableAccent } from '/models/lib/contrastColor';
// #5778 + docs/Theme/Theme.md: apply the theme color to the whole UI (All Boards,
// Search, Admin Panel, My Cards, etc.) via a `board-color-<name>` class on <body>,
@@ -99,7 +100,8 @@ Meteor.startup(() => {
document.body.classList.remove('has-custom-theme-color');
}
if (c2) {
root.style.setProperty('--theme-accent-2', c2);
// Made readable like the first end: white text is drawn across the slide.
root.style.setProperty('--theme-accent-2', readableAccent(c2));
document.body.classList.add('has-custom-theme-slide');
} else {
root.style.removeProperty('--theme-accent-2');
+16 -4
View File
@@ -17,9 +17,21 @@
#header {
color: #fff;
transition: background-color 0.4s;
background: #2980b9;
background: #2573a7;
z-index: 17;
}
/* An opened card covers the header bars, in every browser. iOS WebKit - iPad Safari and
iPad Chrome, which is WebKit too - makes every SCROLLING element (#content, the board
wrapper) a stacking context of its own. The card is inside them, so its z-index (2001)
is only compared there, while the header bar (#header-quick-access, z-index 1000, a
flex item of body and so a stacking context itself) is painted over all of it: the
card's top row - the collapse caret, the card number, the menu, maximize and X - sat
BEHIND the header bar. While a card is open the header drops to the level of the
content, which comes after it in the page and so paints over it; a browser without that
trap shows no difference. */
body:has(.card-details) #header-quick-access {
z-index: 0;
}
#header #header-main-bar {
/* 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,
@@ -247,7 +259,7 @@
float: inline-start;
display: block;
line-height: calc(28px * var(--wekan-ui-font-scale, 1));
color: #27ae60;
color: #217d49;
margin: 0 10px;
cursor: pointer;
}
@@ -298,7 +310,7 @@ body:not(.board-view) #header #header-main-bar h1 {
of colour now, so the header reads as one header. The per-theme rules do
the same, by naming this bar in the selector list the main bar already has
(boardColors.css) rather than by restating each hex a second time. */
background: #2980b9;
background: #2573a7;
font-size: calc(12px * var(--wekan-ui-font-scale, 1));
display: flex;
z-index: 1000;
@@ -745,7 +757,7 @@ body.mobile-mode #header-quick-access .allBoards {
color: #fff !important;
/* The active theme, like the rest of the bar; the stock blue where no theme
has published an accent. */
background: var(--theme-accent, #2980b9) !important;
background: var(--theme-accent, #2573a7) !important;
}
#header-quick-access .mobile-mode-toggle .board-header-btn.mobile-active i.mobile-icon .fa,
+20 -3
View File
@@ -44,7 +44,9 @@ template(name="header")
that do the same job. Board admins only - everybody else gets the
plain title. The `is-editable` class also gives the element a minimum
width, so a board whose title is EMPTY still has an area to click.
span.header-page-title(title="{{headerTitleFullPath}}"
//- role="heading" aria-level="1": every page has one level-one heading, and this
//- is the page's name (WCAG 1.3.1 / 2.4.6; axe page-has-heading-one).
span.header-page-title(title="{{headerTitleFullPath}}" role="heading" aria-level="1"
class="{{#if canEditBoardTitle}}js-edit-board-title is-editable{{/if}}")
if headerTitleKey
| {{_ headerTitleKey}}
@@ -67,6 +69,19 @@ template(name="header")
div.header-logo
img(src="{{pathFor '/logo-header.png'}}" alt="{{headerLogoAlt}}" title="{{headerLogoAlt}}")
//- Full screen, to the right of the logo (iPad Safari, desktop and Android
browsers). Only drawn where the browser has the Fullscreen API and the page
may use it: not on iPhone, not in an iframe without allowfullscreen. The
label is the existing "Maximize" one. It stays before the first
.js-header-collapsible-icon, so "hide the icons" does not hide it.
models/lib/fullscreen.js
if fullscreenSupported
a.board-header-btn.js-toggle-fullscreen(href="#" role="button" title="{{#if isFullscreen}}{{_ 'minimize-window'}}{{else}}{{_ 'maximize-window'}}{{/if}}" aria-label="{{#if isFullscreen}}{{_ 'minimize-window'}}{{else}}{{_ 'maximize-window'}}{{/if}}" aria-pressed="{{#if isFullscreen}}true{{else}}false{{/if}}")
if isFullscreen
i.fa.fa-compress
else
i.fa.fa-expand
// Mobile/desktop mode toggle (swapped with zoom)
// #6680 follow-up: .js-header-collapsible-icon marks every icon the new
// .js-toggle-header-icons-collapsed button (beside Home) hides as a
@@ -119,7 +134,8 @@ template(name="header")
//- One GROUP: the two adjacent buttons are about the same thing - how
many items you have starred, and whether this one is among them.
.header-star-group.js-header-collapsible-icon
a.board-header-btn.js-open-starred-boards(title="{{_ 'allboards.starred'}}")
//- aria-label: WCAG 2.5.3 Label in Name - the visible text is the star count.
a.board-header-btn.js-open-starred-boards(title="{{_ 'allboards.starred'}}" aria-label="{{starredBoardsCount}} {{_ 'allboards.starred'}}")
i.fa.fa-caret-down
span.board-star-counter {{starredBoardsCount}}
@@ -245,7 +261,8 @@ template(name="header")
if appIsOffline
+offlineWarning
if currentUser.isBoardMember
//- #6854: on Home and All Boards too, not only on a board.
if announcementForUser
if hasAnnouncement
.announcement(role="status" aria-live="polite")
p
+41
View File
@@ -7,6 +7,7 @@ import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import Announcements, {
announcementVersion,
shouldShowAnnouncement,
showAnnouncementToUser,
} from '/models/announcements';
import { Utils } from '/client/lib/utils';
// What this bar calls the page you are on. models/lib/pageTitles.js
@@ -58,6 +59,7 @@ function customPageTitle(routeName) {
// Drag-to-scroll on the two top header bars (they are not scroll containers
// themselves, so the drag is forwarded to the board canvas / page scroller).
import '/client/lib/headerDragscroll';
const fullscreen = require('/models/lib/fullscreen');
Meteor.subscribe('user-admin');
Meteor.subscribe('boards');
@@ -69,6 +71,10 @@ Meteor.subscribe('setting');
Meteor.subscribe('announcements');
Template.header.onCreated(function () {
const templateInstance = this;
// Full screen: whether the browser offers it here, and whether the page is in it.
// Both are set when the header is drawn (onRendered), where there is a document.
templateInstance.fullscreenSupported = new ReactiveVar(false);
templateInstance.isFullscreen = new ReactiveVar(false);
templateInstance.currentSetting = new ReactiveVar();
templateInstance.isLoading = new ReactiveVar(false);
templateInstance.subscribe('setting');
@@ -82,6 +88,14 @@ Template.header.onCreated(function () {
});
Template.header.onRendered(function () {
// The Fullscreen API, where it exists and the page may use it (models/lib/fullscreen.js).
// The state follows the browser's own events, so leaving with Esc, or with the
// browser's own control, changes the icon too.
this.fullscreenSupported.set(fullscreen.isSupported(document));
this.isFullscreen.set(fullscreen.isFullscreen(document));
this.onFullscreenChange = () => this.isFullscreen.set(fullscreen.isFullscreen(document));
fullscreen.CHANGE_EVENTS.forEach(name => document.addEventListener(name, this.onFullscreenChange));
this.autorun(() => {
Meteor.userId();
FlowRouter.watchPathChange();
@@ -91,7 +105,19 @@ Template.header.onRendered(function () {
});
});
Template.header.onDestroyed(function () {
if (this.onFullscreenChange) {
fullscreen.CHANGE_EVENTS.forEach(name => document.removeEventListener(name, this.onFullscreenChange));
}
});
Template.header.helpers({
fullscreenSupported() {
return Template.instance().fullscreenSupported.get();
},
isFullscreen() {
return Template.instance().isFullscreen.get();
},
logoSettingsReady() {
return Template.instance().subscriptionsReady();
},
@@ -316,6 +342,14 @@ Template.header.helpers({
return ['waiting', 'failed', 'offline'].includes(status.status);
},
announcementForUser() {
const user = ReactiveCache.getCurrentUser();
return showAnnouncementToUser({
userId: user && user._id,
board: Utils.getCurrentBoard(),
});
},
hasAnnouncement() {
const announcement = Announcements.findOne();
if (!announcement || !announcement.enabled) {
@@ -356,6 +390,13 @@ Template.header.helpers({
});
Template.header.events({
// Full screen on and off. The request is made inside this click, which is the
// user gesture browsers require; a refusal is not an error (models/lib/fullscreen.js).
'click .js-toggle-fullscreen'(event) {
event.preventDefault();
fullscreen.toggle(document);
},
// The starred boards, by name. They were listed inline in this bar; the bar
// is one row now and the names are in the popup. docs/Features/Header-Bar/Header.md
// Titled "Starred Boards", from the key the app already has for that phrase,
@@ -7,7 +7,7 @@ template(name="historyRecoveryNotice")
.history-recovery-notice.js-history-recovery-notice(role="status" aria-live="polite")
p {{_ pending.messageKey}}
p.quiet {{_ 'history-request-hint'}}
button.primary.js-history-retry(type="button" disabled=busy) {{_ 'history-request-retry'}}
button.primary.js-history-retry(type="button" disabled=retryDisabled) {{_ 'history-request-retry'}}
button.js-history-forget(type="button" disabled=busy) {{_ 'history-request-forget'}}
if checkpoint
.history-recovery-notice.history-checkpoint-notice.js-history-checkpoint-notice(role="status" aria-live="polite" class=checkpointPlacement)
@@ -52,6 +52,13 @@ Template.historyRecoveryNotice.helpers({
busy() {
return Template.instance().busy.get();
},
// undoRedoLast does nothing for someone who may not write on the board, and
// right after a reload that is everyone until the board and its members
// arrive: the notice shows first, and a Retry clicked then was dropped
// without a word. It is enabled once the retry can act.
retryDisabled() {
return Template.instance().busy.get() || !Utils.canModifyBoard();
},
});
async function resolve(tpl, action) {
@@ -4,7 +4,8 @@ template(name="shortcutsModalTitle")
| {{_ 'keyboard-shortcuts'}}
template(name="keyboardShortcuts")
.wrapper.shortcuts-list
//- tabindex: the list scrolls when the window is short, and a scrolling region must be reachable by keyboard (WCAG 2.1.1; axe scrollable-region-focusable).
.wrapper.shortcuts-list(tabindex="0" role="region" aria-label="{{_ 'keyboard-shortcuts'}}")
each mapping
.shortcuts-list-item
.shortcuts-list-item-keys
+87 -2
View File
@@ -49,6 +49,20 @@ summary:focus-visible,
box-shadow: 0 0 0 4px rgba(21, 101, 192, 0.35);
border-radius: 2px;
}
/* #6755: a member who turned off "Show focus outline" (Member Settings / Change
* Settings) sees no ring: the outline and its halo, for keyboard focus and for the
* fallback below. Field borders and the colour a button takes on focus are not the
* ring and stay. The ring is an accessibility feature, so it is on unless hidden. */
body.has-hide-focus-outline :focus-visible,
body.has-hide-focus-outline a:focus,
body.has-hide-focus-outline button:focus,
body.has-hide-focus-outline [tabindex]:focus {
outline: none !important;
outline-offset: 0 !important;
}
body.has-hide-focus-outline :is(a, button, input, select, textarea, summary, [tabindex], [role="button"], [contenteditable="true"]):focus-visible {
box-shadow: none !important;
}
/* Browsers that do not support :focus-visible fall back to :focus so keyboard
* users are never left without an indicator. */
.no-focus-visible-support a:focus,
@@ -378,14 +392,14 @@ h6 {
}
.quiet,
.quiet a {
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
}
.error,
.error a {
color: #eb3800;
}
.no-items-message {
color: #666;
color: var(--wekan-quiet-text, #595959);
margin: 36px 0;
text-align: center;
}
@@ -1133,6 +1147,77 @@ a:not(.disabled).is-active i.fa {
align-items: center;
justify-content: center;
}
/* The administrator's login page background colour (layouts.js sets the two
custom properties and the class while the sign-in layout is shown). The
foreground is white on a dark colour and black on a light one. */
body.has-login-background {
background: var(--login-background);
}
body.has-login-background .textBelowCustomLoginLogo,
body.has-login-background .textBelowCustomLoginLogo a {
color: var(--login-foreground);
}
body.has-login-background hr {
border-color: var(--login-foreground);
opacity: 0.4;
}
/* The text under the login logo: one column, real headings, space between
paragraphs. It has no card or background of its own, so the page colour shows
through. */
.textBelowCustomLoginLogo > * {
width: 100%;
max-width: 860px;
box-sizing: border-box;
padding: 0 16px;
}
.textBelowCustomLoginLogo h1,
.textBelowCustomLoginLogo h2,
.textBelowCustomLoginLogo h3,
.support-page-content h1,
.support-page-content h2,
.support-page-content h3 {
font-weight: 700;
line-height: 1.25;
margin: 1em 0 0.5em;
}
.textBelowCustomLoginLogo h1,
.support-page-content h1 { font-size: 1.8em; }
.textBelowCustomLoginLogo h2,
.support-page-content h2 { font-size: 1.45em; }
.textBelowCustomLoginLogo h3,
.support-page-content h3 { font-size: 1.2em; }
.textBelowCustomLoginLogo p,
.textBelowCustomLoginLogo ul,
.textBelowCustomLoginLogo ol,
.support-page-content p,
.support-page-content ul,
.support-page-content ol {
margin: 0 0 1em;
}
/* The support page: a centred column instead of text flush against the window's
left edge. */
.support-page {
padding: 24px 16px;
}
.support-page-content {
box-sizing: border-box;
max-width: 860px;
margin: 0 auto;
padding: 24px 28px;
background: #fff;
color: #1a1a1a;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
overflow-wrap: anywhere;
}
@media (max-width: 600px) {
.support-page {
padding: 8px;
}
.support-page-content {
padding: 16px;
}
}
.auth-layout .auth-dialog {
margin: 0 !important;
}
+7 -4
View File
@@ -27,11 +27,12 @@ template(name="userFormsLayout")
section.auth-layout
if logoSettingsReady
if currentSetting.hideLogo
h1.at-form-landing-logo
//- A spacer, not a heading: an empty h1 is announced as an empty heading.
div.at-form-landing-logo(aria-hidden="true")
br
br
unless currentSetting.hideLogo
h1.at-form-landing-logo
div.at-form-landing-logo(aria-hidden="true")
if currentSetting.customLoginLogoImageUrl
if currentSetting.customLoginLogoLinkUrl
a(href="{{currentSetting.customLoginLogoLinkUrl}}")
@@ -49,13 +50,15 @@ template(name="userFormsLayout")
+viewer
| {{currentSetting.textBelowCustomLoginLogo}}
hr
section.auth-layout
//- role="main": the sign-in and register form is the page's main landmark
(WCAG 1.3.1 / 2.4.1), so the title, the links and the language picker are in one.
section.auth-layout(role="main")
section.auth-dialog
if isLoading
+loader
else
// ARIA live region for error messages
div#login-error-message(role="alert" aria-live="assertive" style="color: #d32f2f; margin-bottom: 1em;")
div#login-error-message(role="alert" aria-live="assertive" style="color: #d32f2f; margin-bottom: 1em;")= loginErrorText
// Second sign-in step for accounts-2fa (issue #3058). Hidden unless a
// password login comes back with the "no-2fa-code" / "invalid-2fa-code"
// error - see decideTwoFactorLoginStep() in
+60 -5
View File
@@ -2,6 +2,7 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
const { OAUTH_PROVIDERS, providerByKey } = require('/models/lib/oauthProviders');
const { loginOriginMismatch, offersProviderLogin } = require('/models/lib/loginOriginMismatch');
const { loginBackgroundStyle } = require('/models/lib/loginBackground');
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { Accounts } from 'meteor/accounts-base';
import Users from '/models/users';
@@ -303,6 +304,7 @@ Template.userFormsLayout.onDestroyed(function () {
});
Template.userFormsLayout.helpers({
loginErrorText() { return loginErrorText.get(); },
passkeysSupported() { return require('/client/lib/passkeys').passkeysSupported(); },
passkeyMessage() { return Template.instance().passkeyMessage.get(); },
windowsSsoOffered() { return Meteor.settings?.public?.windowsSsoEnabled === true; },
@@ -557,20 +559,23 @@ Tracker.autorun(() => {
}
});
// The message in #login-error-message, rendered by the template as text. Not
// written into the element: it is inside `if isLoading`, and when that flips
// Blaze draws a new, empty element - a SAML popup error shown just before
// then was gone (saml-popup-error.e2e.js, Firefox). Text, never HTML.
const loginErrorText = new ReactiveVar('');
function showLoginError(err) {
const errorDiv = document.getElementById('login-error-message');
if (!errorDiv) return;
if (!err) {
errorDiv.textContent = '';
loginErrorText.set('');
return;
}
const reason = err.reason || err.message || '';
// #2460: an account its member locked to SQRL says so in their language.
const translated = ['oauth-account-conflict', 'sqrl-only', 'sqrl-hardlock']
.find(key => err.error === key || reason === key);
errorDiv.textContent = translated
loginErrorText.set(translated
? TAPi18n.__(translated)
: reason || String(err.error || err);
: reason || String(err.error || err));
}
Template.defaultLayout.events({
@@ -668,6 +673,56 @@ Template.userFormsLayout.onDestroyed(function () {
this.removeAuthenticationSubmit?.();
});
// The administrator's login page background colour (Admin Panel -> Lay-out).
// It is put on <body> only while the sign-in layout is shown, as two custom
// properties and a class, and taken off again when it closes so the pages after
// sign-in keep their own background. loginBackgroundStyle accepts a hex colour
// only, so the value can never carry CSS of its own.
const LOGIN_BACKGROUND_PROPERTIES = ['--login-background', '--login-foreground'];
function clearLoginBackground() {
document.body.classList.remove('has-login-background');
LOGIN_BACKGROUND_PROPERTIES.forEach(name => document.body.style.removeProperty(name));
}
Template.userFormsLayout.onRendered(function () {
this.autorun(() => {
const setting = this.currentSetting.get();
const style = loginBackgroundStyle(setting && setting.loginBackgroundColor);
if (!style) {
clearLoginBackground();
return;
}
document.body.style.setProperty('--login-background', style.background);
document.body.style.setProperty('--login-foreground', style.foreground);
document.body.classList.add('has-login-background');
});
});
Template.userFormsLayout.onDestroyed(clearLoginBackground);
// The form's title ("Sign In", "Create an Account") is an h3 of the useraccounts
// package and the first heading of the page: a level-one heading, whatever the
// package draws it as (WCAG 1.3.1; axe page-has-heading-one and heading-order).
// The package redraws the form when the user moves between sign in and register,
// so the title is marked again whenever the dialog changes.
Template.userFormsLayout.onRendered(function () {
const dialog = this.find('.auth-dialog');
if (!dialog) return;
const mark = () => {
dialog.querySelectorAll('.at-title h1, .at-title h2, .at-title h3').forEach(heading => {
if (heading.getAttribute('aria-level') !== '1') heading.setAttribute('aria-level', '1');
});
};
mark();
this.titleObserver = new MutationObserver(mark);
this.titleObserver.observe(dialog, { childList: true, subtree: true });
});
Template.userFormsLayout.onDestroyed(function () {
if (this.titleObserver) this.titleObserver.disconnect();
});
function getAuthenticationMethod(
settings,
match,
@@ -111,7 +111,7 @@
/* Disabled = the same themed outline, faded. Not a grey block. */
/* A disabled pager still has to look like the THEME. It is filled with the
theme colour, and dimming it is what takes the theme back out: at 0.4 the
belize blue #2980b9 arrived on the page's grey as rgb(122,170,202), a pale
belize blue #2573a7 arrived on the page's grey as rgb(122,170,202), a pale
wash with no family resemblance to the header above it - and on a one-page
list, where BOTH arrows are disabled, that is the whole pager.
+60 -16
View File
@@ -39,6 +39,10 @@
first row of a popup, and it must match the space beside it: `.content` has 18px of
side padding, so 6 + 12 = 18 makes the top gap and the left gutter the same. */
.pop-over .header {
/* A popup is moved by its title bar (popup.js, Pointer Events). Without this a
finger drag on it is taken for a scroll and the pointer is cancelled, so only a
mouse could move it. */
touch-action: none;
height: calc(41px * var(--wekan-ui-font-scale, 1));
position: relative;
margin-bottom: 6px;
@@ -367,19 +371,16 @@
max-width: 90vw !important;
}
/* Board Settings / Card: Draggable, Show on Minicard and Show on Card.
#6796: as wide as the window allows, up to 1800px - width buys COLUMNS
inside each list (sidebar.css), and enough of them means the whole of
both lists is read without scrolling. client/lib/popupOffset.js centres
it at this width (CARD_SETTINGS_MAX_WIDTH) and gives it the window's
height. */
/* Board Settings / Card, and the opened card's "+ Add": the Admin Panel's
left menu and right pane, wide enough for a row's label and both sides
(sidebar.css .card-settings-sections). */
/* Board Settings / Card: Card Settings, TWO panes of checklists - the
Minicard's and the Card's (sidebar.jade boardCardSettingsPopup) - side by
side, each about as wide as the one column was (600px until 2026-10-28,
when the two sides became two panes instead of a Show switch).
client/lib/popupOffset.js centres it at this width
(CARD_SETTINGS_MAX_WIDTH) and gives it the window's height (#6796). */
.pop-over[data-popup='boardCardSettingsPopup'] {
position: fixed;
box-sizing: border-box;
width: min(96vw, 1200px) !important;
width: min(96vw, 1100px) !important;
max-width: 96vw !important;
max-height: calc(100dvh - 24px) !important;
margin-top: 0;
@@ -810,7 +811,7 @@ html[dir='rtl'] .pop-over > button.js-date-popup-resize > span {
above pins every row's text to `#000 !important`; the icon is named separately
for the same reason. */
.pop-over-list li > a:not(.disabled):hover {
background-color: var(--theme-accent, #2980b9);
background-color: var(--theme-accent, #2573a7);
color: #fff !important;
}
.pop-over-list li > a:not(.disabled):hover i,
@@ -825,7 +826,7 @@ html[dir='rtl'] .pop-over > button.js-date-popup-resize > span {
background: #fff;
}
.pop-over-list li > a .sub-name {
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
display: block;
font-size: calc(12px * var(--wekan-ui-font-scale, 1));
font-weight: 400;
@@ -870,7 +871,7 @@ html[dir='rtl'] .pop-over > button.js-date-popup-resize > span {
background-color: #2e85b8;
}
.pop-over-list li > a.disabled {
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
cursor: default;
}
.pop-over-list li > a.disabled .vis-icon {
@@ -881,7 +882,7 @@ html[dir='rtl'] .pop-over > button.js-date-popup-resize > span {
}
.pop-over-list li > a.disabled:hover .sub-name,
.pop-over-list li > a.disabled:hover .quiet {
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
}
.pop-over-list li > a.disabled:active {
background: none;
@@ -975,7 +976,7 @@ html[dir='rtl'] .pop-over > button.js-date-popup-resize > span {
user's own theme, the board's colour or the site theme - whichever is in
force. The fallback is the stock WeKan blue, so an unthemed instance is
unchanged. */
background: var(--theme-accent, #2980b9);
background: var(--theme-accent, #2573a7);
height: calc(48px * var(--wekan-ui-font-scale, 1));
padding: 0px 0px;
border: 0px;
@@ -1444,14 +1445,24 @@ body.mobile-mode .pop-over .content-wrapper {
button (popup.js MOVABLE_POPUPS / MAXIMIZABLE_POPUPS), placed like the opened
card's: the handle before the title, the button beside the close button. */
.pop-over .header .popup-drag-handle {
touch-action: none;
float: inline-start;
padding: 12px 0 12px 12px;
cursor: move;
color: #888;
}
.pop-over .header:has(.popup-drag-handle) {
/* #6839: the title bar moves every popup while drag handles are off; with
them on, the handle does, and the bar is an ordinary title. */
.pop-over .header {
cursor: move;
}
.pop-over .header:has(.popup-drag-handle) {
cursor: auto;
}
.pop-over .header a,
.pop-over .header button {
cursor: pointer;
}
.pop-over .header .popup-maximize-btn {
position: absolute;
top: 0;
@@ -1499,3 +1510,36 @@ body.mobile-mode .pop-over .content-wrapper {
.pop-over.pop-over--maximized .card-drawing {
min-width: 0;
}
/* Card Settings' panes scroll on their own (client/components/cards
/cardAddFields.css .card-settings-panes-container): the popup's content
does not scroll as a whole, it hands its height down to the panes. At every
width - stacked, the panes' container is the one scroller. Also after the
corner grip resized it (popup.js resizeDatePopup, `data-user-resized`,
whose rule above would make the wrapper scroll again). The bottom padding
keeps the grip in a strip of its own, under the panes' scrollbars rather
than over the Card pane's. */
.pop-over[data-popup='boardCardSettingsPopup'] {
display: flex;
flex-direction: column;
padding-bottom: 24px;
}
.pop-over[data-popup='boardCardSettingsPopup'] .content-wrapper,
.pop-over[data-popup='boardCardSettingsPopup'][data-user-resized] > .content-wrapper {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
overflow: hidden !important;
}
.pop-over[data-popup='boardCardSettingsPopup'] .content-container,
.pop-over[data-popup='boardCardSettingsPopup'] .content-container > .content {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
width: 100%;
}
.pop-over[data-popup='boardCardSettingsPopup'] .content-container > .clearfix {
display: none;
}
+34 -8
View File
@@ -29,8 +29,11 @@ function resizeDatePopup(element, width, height) {
Object.entries(styles).forEach(([key, value]) => element.style.setProperty(key, value, 'important'));
}
// Popups dragged by their title bar (or the drag handle beside it), as the
// opened card is. A date popup is movable too, found by its .edit-date.
// #6839: EVERY popup is moved like the opened card: by its title bar when drag
// handles are off, by the drag handle before its title when they are on - the
// handle alone, then, as the card's. These are the popups that may also GROW
// past the window while open (Drawing replacing a menu), so they are kept
// inside it as they grow; so is any popup the user has dragged.
const MOVABLE_POPUPS = new Set(['boardViewSettingsPopup', 'cardDrawingPopup', 'boardCardSettingsPopup']);
// Popups with the opened card's maximize / minimize button.
const MAXIMIZABLE_POPUPS = new Set(['cardDrawingPopup', 'boardCardSettingsPopup']);
@@ -66,7 +69,7 @@ Popup.template.onCreated(function () {
Popup.template.helpers({
popupMovable() {
return MOVABLE_POPUPS.has(this.popupName);
return true;
},
popupMaximizable() {
return MAXIMIZABLE_POPUPS.has(this.popupName);
@@ -87,21 +90,43 @@ Popup.template.events({
'pointerdown .header'(evt, tpl) {
const element = evt.currentTarget.closest('.pop-over');
// A maximized popup has no place to be dragged to.
// With drag handles on, the handle is the only thing that moves it.
const handle = element.querySelector('.js-popup-drag-handle');
if (evt.button !== 0 || element.classList.contains('pop-over--maximized') ||
(!element.querySelector('.edit-date') && !MOVABLE_POPUPS.has(element.dataset.popup)) ||
(handle && !evt.target.closest('.js-popup-drag-handle')) ||
evt.target.closest('a, button, input, select')) return;
evt.preventDefault(); evt.stopPropagation();
const bounds = element.getBoundingClientRect();
// Pin it where it is before it moves. A popup is placed by its margins, by
// inset-inline-end (the member menu, at the right edge) or by left/top, so
// `left: bounds.left` alone put some of them elsewhere - one jumped across
// the window as the drag began. Set it, see where it went, and correct.
const place = (left, top) => {
element.style.setProperty('inset-inline-start', 'auto', 'important');
element.style.setProperty('inset-inline-end', 'auto', 'important');
element.style.setProperty('left', `${left}px`, 'important');
element.style.setProperty('top', `${top}px`, 'important');
};
place(bounds.left, bounds.top);
const placed = element.getBoundingClientRect();
const left = bounds.left + (bounds.left - placed.left);
const top = bounds.top + (bounds.top - placed.top);
place(left, top);
element.dataset.userMoved = 'true';
tpl._dateMove = { element, pointerId: evt.pointerId, x: evt.clientX, y: evt.clientY,
left: bounds.left, top: bounds.top, width: bounds.width, height: bounds.height };
left, top, width: bounds.width, height: bounds.height,
// The clamp keeps the popup's VISIBLE box 12px inside the window.
dx: bounds.left - left, dy: bounds.top - top };
evt.currentTarget.setPointerCapture(evt.pointerId);
},
'pointermove .header'(evt, tpl) {
const drag = tpl._dateMove;
if (!drag || drag.pointerId !== evt.pointerId) return;
evt.preventDefault(); evt.stopPropagation();
const left = Math.max(12, Math.min(window.innerWidth - drag.width - 12, drag.left + evt.clientX - drag.x));
const top = Math.max(12, Math.min(window.innerHeight - drag.height - 12, drag.top + evt.clientY - drag.y));
const boxLeft = Math.max(12, Math.min(window.innerWidth - drag.width - 12, drag.left + drag.dx + evt.clientX - drag.x));
const boxTop = Math.max(12, Math.min(window.innerHeight - drag.height - 12, drag.top + drag.dy + evt.clientY - drag.y));
const left = boxLeft - drag.dx;
const top = boxTop - drag.dy;
// Dragging uses physical pointer coordinates in either text direction.
drag.element.style.setProperty('inset-inline-start', 'auto', 'important');
drag.element.style.setProperty('inset-inline-end', 'auto', 'important');
@@ -221,7 +246,8 @@ Popup.template.onRendered(function () {
if (typeof ResizeObserver !== 'undefined') {
const element = this._popupElement;
this._boundsObserver = new ResizeObserver(() => {
if (!MOVABLE_POPUPS.has(element.dataset.popup) || element.classList.contains('pop-over--maximized')) return;
if (!(MOVABLE_POPUPS.has(element.dataset.popup) || element.dataset.userMoved)
|| element.classList.contains('pop-over--maximized')) return;
const box = element.getBoundingClientRect();
const moved = keptInside(box, window.innerWidth, window.innerHeight);
if (!moved) return;
+4 -4
View File
@@ -14,10 +14,10 @@
.header
a.back-btn.js-back-view(class="{{#unless hasPopupParent}}is-hidden{{/unless}}" aria-label="{{_ 'go-back'}}")
i.fa.fa-caret-left(aria-hidden="true")
//- A movable popup (popup.js MOVABLE_POPUPS) is dragged by its title bar
or this handle, like the opened card - and, like the card's, the handle
is there only while drag handles are on; a maximizable one has the
card's maximize / minimize button beside its close button.
//- #6839: every popup is moved like the opened card - by its title bar
while drag handles are off, by this handle alone while they are on
(popup.js); a maximizable one has the card's maximize / minimize button
beside its close button.
if popupMovable
if isTouchScreenOrShowDesktopDragHandles
span.popup-drag-handle.js-popup-drag-handle(title="{{_ 'drag-to-move'}}")
@@ -43,3 +43,6 @@ template(name="themeColorPicker")
input.js-theme-wheel(type="color" data-index="{{index}}" value="{{value}}" title="{{_ 'custom-color'}}")
.theme-preview
span.background-box(class="{{previewClass}}" style="{{previewStyle}}")
//- WCAG 1.4.3: a colour that white text cannot be read on is darkened; this says so.
if customAdjusted
p.theme-custom-adjusted.js-theme-custom-adjusted {{_ 'custom-color'}}: {{customAdjusted}} (4.5:1)
+13 -3
View File
@@ -2,7 +2,7 @@ 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 { isHexColor, toHex, readableAccent } from '/models/lib/contrastColor';
import { BOARD_COLORS } from '/models/metadata/colors';
import {
THEME_CATEGORY_ORDER,
@@ -33,7 +33,7 @@ import {
// 3. the user's own override
// See docs/Features/General/Theme/Change-Color.md for the whole design.
const DEFAULT_WHEEL = ['#2980b9', '#6dd5fa']; // stock flat accent / second slide stop
const DEFAULT_WHEEL = ['#2573a7', '#6dd5fa']; // stock flat accent / second slide stop
function readCurrent(scope) {
if (scope === 'board') {
@@ -138,7 +138,10 @@ Template.themeColorPicker.helpers({
const tpl = Template.instance();
const cat = customCategory(tpl);
if (!allowsCustomColor(cat)) return '';
const cc = tpl.customColors.get();
// The colours as the theme applies them: made readable under white text
// (models/lib/themeAccents.js activeAccent), so the preview is what the header
// will be, not what the wheel showed.
const cc = tpl.customColors.get().map(c => (c ? readableAccent(c) : c));
if (!cc.some(Boolean)) return '';
if (customColorCount(cat) === 2 && cc[0] && cc[1]) {
return `background: linear-gradient(135deg, ${cc[0]}, ${cc[1]}) !important;`;
@@ -146,6 +149,13 @@ Template.themeColorPicker.helpers({
if (cc[0]) return `background: ${cc[0]} !important;`;
return '';
},
// The colours that had to change to be readable, for the note under the preview.
customAdjusted() {
const tpl = Template.instance();
const chosen = tpl.customColors.get().filter(Boolean).map(c => toHex(c)).filter(Boolean);
const changed = chosen.map(c => readableAccent(c)).filter((c, i) => c !== chosen[i]);
return changed.join(' ');
},
});
// Collect the custom colors to save: [] unless the user actually engaged a wheel, in
+12 -1
View File
@@ -61,9 +61,20 @@ html.has-ui-font-size {
* follow this anyway; the ones with a colour of their own - a red alert, a
* green tick - keep saying what they mean. */
body.has-ui-text-color,
body.has-ui-text-color *:not(.fa) {
body.has-ui-text-color *:not(.fa):not(
#header, #header *,
#header-quick-access, #header-quick-access *,
.card-label, .card-label *,
button.primary, button.primary *
) {
color: var(--wekan-ui-text-color) !important;
}
/* The elements left out of the rule above sit on a COLOUR of their own - the
* theme's header bars, a label's colour, a primary button's accent - with a text
* colour that is chosen to read on it (4.5:1, WCAG 1.4.3). The member's colour is
* made legible on the page's surfaces (models/lib/uiFonts.js readableUiTextColor),
* which is not the same as legible on those: #626262 on the blue header was
* 1.18:1. */
/* There was a "text background color" here (#4759), a colour painted behind the
* text. It is REMOVED. A background needs elements to sit on, and neither choice
+16 -2
View File
@@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { fontFamilyValue, fontSizeValue, fontScaleValue, colorValue } from '/models/lib/uiFonts';
import { fontFamilyValue, fontSizeValue, fontScaleValue, readableUiTextColor } from '/models/lib/uiFonts';
import { activeThemeName } from '/client/lib/activeTheme';
// #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
@@ -37,6 +38,17 @@ Meteor.startup(() => {
}
}
// #6755: a member who hid the focus outline (Member Settings / Change Settings):
// a class on <body>, which layouts.css turns into no ring.
Tracker.autorun(() => {
const user = Meteor.user();
try {
document.body.classList.toggle('has-hide-focus-outline', !!(user && user.profile && user.profile.hideFocusOutline === true));
} catch (_) {
// document.body may not exist yet in exotic embeddings; ignore.
}
});
Tracker.autorun(() => {
const user = Meteor.user();
const profile = (user && user.profile) || {};
@@ -50,7 +62,9 @@ Meteor.startup(() => {
// of it that happened to be written in rem. No class goes with it - a
// variable that is not set falls back to 1 in the calc itself.
toggle('--wekan-ui-font-scale', null, fontScaleValue(profile.uiFontSize));
toggle('--wekan-ui-text-color', 'has-ui-text-color', colorValue(profile.uiTextColor));
// The chosen colour, made legible on the surface of the theme in use (4.5:1,
// WCAG 1.4.3): a colour that already reads is applied as chosen.
toggle('--wekan-ui-text-color', 'has-ui-text-color', readableUiTextColor(profile.uiTextColor, activeThemeName()));
// There was a `--wekan-ui-bg-color` here, for "text background color". It is
// removed: a colour behind the text needs elements to sit on, and there is no
// set of them that looks right - on the boxes it painted the page, on the text
@@ -22,7 +22,7 @@ section#notifications-drawer {
padding-top: 36px;
}
section#notifications-drawer a:hover {
color: #2980b9 !important;
color: #2573a7 !important;
}
section#notifications-drawer .header {
position: fixed;
@@ -47,7 +47,7 @@ section#notifications-drawer .header .notification-menu-toggle {
line-height: calc(24px * var(--wekan-ui-font-scale, 1));
}
section#notifications-drawer .header .notification-menu-toggle:hover {
color: #2980b9;
color: #2573a7;
}
section#notifications-drawer .header .notification-menu {
position: absolute;
@@ -90,7 +90,7 @@ section#notifications-drawer .header .notification-menu .menu-item .check-icon {
min-width: 20px;
margin-inline-end: 8px;
text-align: center;
color: #2980b9;
color: #2573a7;
font-weight: bold;
}
section#notifications-drawer .header .notification-menu .menu-item .menu-icon {
+30 -3
View File
@@ -100,7 +100,7 @@
}
/* Drag-and-drop workflow editor */
.rules-workflow .workflow-help {
color: #727479;
color: var(--wekan-quiet-text, #5a5c61);
font-size: calc(13px * var(--wekan-ui-font-scale, 1));
margin-bottom: 9px;
}
@@ -247,6 +247,33 @@
.rules-back .js-goback .fa-arrow-left {
color: #fff !important;
}
/* #6852: Back to the board, at the top of the Rules page. */
.rules-back-to-board {
margin-block: 8px;
margin-inline: 14px;
}
/* A link, so it opens the board as a link does (also in a new tab), drawn as
the primary buttons around it: forms.css styles only button.primary. */
.rules-back-to-board .js-rules-back-to-board {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 20px;
border-radius: 3px;
background: var(--theme-accent, #005377);
box-shadow: 0 1px 0 #4d4d4d;
color: #fff;
font-weight: 700;
text-decoration: none;
}
.rules-back-to-board .js-rules-back-to-board:hover,
.rules-back-to-board .js-rules-back-to-board:focus {
background: var(--theme-accent, #004766);
color: #fff;
}
.rules-back-to-board .js-rules-back-to-board .fa-arrow-left {
color: #fff !important;
}
.flex,
.triggers-content .triggers-body .triggers-side-menu ul li a {
display: -webkit-box;
@@ -257,7 +284,7 @@
display: flex;
}
.triggers-content {
color: #727479;
color: var(--wekan-quiet-text, #5a5c61);
background: #dedede;
}
.triggers-content .triggers-body {
@@ -410,7 +437,7 @@
}
.rules-import-export .rules-ie-hint {
font-size: calc(12px * var(--wekan-ui-font-scale, 1));
color: #727479;
color: var(--wekan-quiet-text, #5a5c61);
margin-bottom: 0.3em;
}
.rules-import-export .rules-ie-textarea {
+9
View File
@@ -6,6 +6,15 @@
template(name="rulesMain")
.wrapper
.rules-page(class=currentBoard.colorClass)
//- #6852: the way back to the board. The trigger, action and rule
details pages have their own Back, to the rules list, so this one is
shown only where there is none - two Backs going to two places would
be one too many.
if showBackToBoard
.rules-back-to-board
a.js-rules-back-to-board(href="{{pathFor 'board' id=currentBoard._id slug=currentBoard.slug}}" title="{{currentBoard.title}}")
i.fa.fa-arrow-left
| {{_ 'back'}}
//- #2217: who may edit sees every view; a member a board shows its Rules
to reads the rules list and rule details, with no control that
changes a rule; anybody else sees nothing (client/lib/rulesAccess.js).
+4
View File
@@ -44,6 +44,10 @@ Template.rulesMain.helpers({
rulesCurrentTab() {
return Template.instance().rulesCurrentTab;
},
// #6852: every Rules page but the three that already go Back to the list.
showBackToBoard() {
return !['trigger', 'action', 'ruleDetails'].includes(Template.instance().rulesCurrentTab.get());
},
ruleName() {
return Template.instance().ruleName;
},
@@ -0,0 +1,11 @@
.admin-audit-log-search { margin: 0.5rem 0; max-width: 24rem; }
.admin-audit-log-table { width: 100%; border-collapse: collapse; }
.admin-audit-log-table th, .admin-audit-log-table td {
padding: 0.3rem 0.5rem;
border-bottom: 1px solid rgba(128, 128, 128, 0.3);
text-align: start;
vertical-align: top;
}
.admin-audit-args { white-space: pre-wrap; word-break: break-all; font-size: 0.85em; }
.admin-audit-failed .admin-audit-error { color: #c0392b; }
.admin-audit-log-pager { margin-top: 0.5rem; display: flex; gap: 0.5rem; align-items: center; }
@@ -0,0 +1,39 @@
template(name="adminAuditLog")
//- Admin Panel / People / Audit log (models/lib/adminAudit.js): every
//- administrative action of a site administrator, newest first. The
//- arguments are shown as stored - passwords and secrets never are.
.admin-audit-log
p {{_ 'admin-audit-log-info'}}
.admin-audit-log-search
input.wekan-form-control.js-admin-audit-search(type="search" placeholder="{{_ 'search'}}" value=search)
if loading
+spinner
else
table.admin-audit-log-table
thead
tr
th {{_ 'admin-audit-log-when'}}
th {{_ 'admin-audit-log-who'}}
th {{_ 'admin-audit-log-action'}}
th {{_ 'admin-audit-log-details'}}
th IP
tbody
each rows
tr(class="{{#unless ok}}admin-audit-failed{{/unless}}")
td {{when at}}
td {{actorUsername}}
td
code {{method}}
unless ok
|
span.admin-audit-error {{error}}
td
code.admin-audit-args {{args}}
td {{ip}}
else
tr
td(colspan="5") {{_ 'admin-audit-log-empty'}}
.admin-audit-log-pager
button.js-admin-audit-prev(type="button" disabled=noPrev) ‹
span {{page}} / {{pages}} ({{total}})
button.js-admin-audit-next(type="button" disabled=noNext) ›
@@ -0,0 +1,47 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
// Admin Panel / People / Audit log: pages of server/adminAudit.js's trail.
Template.adminAuditLog.onCreated(function () {
this.page = new ReactiveVar(1);
this.search = new ReactiveVar('');
this.result = new ReactiveVar({ rows: [], total: 0, page: 1, perPage: 50 });
this.loading = new ReactiveVar(true);
this.autorun(() => {
const options = { page: this.page.get(), search: this.search.get() };
this.loading.set(true);
Meteor.call('adminAuditLogPage', options, (error, result) => {
this.loading.set(false);
if (!error && result) this.result.set(result);
});
});
});
Template.adminAuditLog.helpers({
loading() { return Template.instance().loading.get(); },
rows() { return Template.instance().result.get().rows; },
total() { return Template.instance().result.get().total; },
page() { return Template.instance().page.get(); },
search() { return Template.instance().search.get(); },
pages() {
const { total, perPage } = Template.instance().result.get();
return Math.max(1, Math.ceil(total / perPage));
},
noPrev() { return Template.instance().page.get() <= 1; },
noNext() {
const { total, perPage } = Template.instance().result.get();
return Template.instance().page.get() >= Math.ceil(total / perPage);
},
when(at) { return at ? new Date(at).toLocaleString() : ''; },
});
let searchTimer = null;
Template.adminAuditLog.events({
'input .js-admin-audit-search'(event, tpl) {
const value = event.currentTarget.value;
clearTimeout(searchTimer);
searchTimer = setTimeout(() => { tpl.page.set(1); tpl.search.set(value); }, 300);
},
'click .js-admin-audit-prev'(event, tpl) { tpl.page.set(Math.max(1, tpl.page.get() - 1)); },
'click .js-admin-audit-next'(event, tpl) { tpl.page.set(tpl.page.get() + 1); },
});
@@ -58,6 +58,8 @@ template(name="adminProblems")
+eventStreamReport(stream="api")
else if isPane 'report-office'
+officeReport
else if isPane 'report-password-length'
+passwordLengthReport
else if tablePageData
+tablePage(tablePageData)
@@ -321,6 +321,9 @@ const PROBLEMS_MENU = [
// Security, and the two sit in the same menu.
{ id: 'report-security', icon: 'fa-shield', labelKey: 'securityReportTitle' },
{ id: 'report-impersonation', icon: 'fa-user-secret', labelKey: 'impersonationReportTitle' },
// Who signs in with a password shorter than PASSWORD_MIN_LENGTH, and the two
// settings that decide it (passwordLengthReport.js).
{ id: 'report-password-length', icon: 'fa-key', labelKey: 'password-length-report-title' },
{ id: 'features-performance', icon: 'fa-tachometer', labelKey: 'features-performance', emoji: true },
{ id: 'report-speed', icon: 'fa-tachometer', labelKey: 'speedReportTitle' },
{ id: 'report-tests', icon: 'fa-flask', labelKey: 'testsReportTitle' },
@@ -538,6 +541,8 @@ const SELF_LOADING_PANES = [
'report-security', 'report-speed', 'report-tests', 'report-cpu', 'report-ram', 'report-disk',
'report-instrumentation',
'report-database', 'report-integrity', 'report-office', 'report-api', 'report-mail',
// passwordLengthReport.js loads its own rows and settings through methods.
'report-password-length',
];
function openReportPane(tmpl, targetID) {
@@ -281,6 +281,17 @@ template(name="attachments")
.materialCheckBox#s3-force-path-style(class="{{#if cloudValue.s3.forcePathStyle}}is-checked{{/if}}")
span {{_ 's3-force-path-style'}}
small.form-text.text-muted {{_ 's3-force-path-style-description'}}
//- Encryption at rest (models/lib/s3Encryption.js).
.form-group
label(for="s3-sse") {{_ 's3-sse'}}
select.wekan-form-control#s3-sse
option(value="" selected="{{s3SseSelected ''}}") {{_ 's3-sse-none'}}
option(value="AES256" selected="{{s3SseSelected 'AES256'}}") SSE-S3 (AES256)
option(value="aws:kms" selected="{{s3SseSelected 'aws:kms'}}") SSE-KMS (aws:kms)
small.form-text.text-muted {{_ 's3-sse-description'}}
.form-group
label(for="s3-sse-kms-key-id") {{_ 's3-sse-kms-key-id'}}
input.wekan-form-control#s3-sse-kms-key-id(type="text" value="{{cloudValue.s3.sseKmsKeyId}}" placeholder="arn:aws:kms:eu-north-1:111122223333:key/…")
.form-group
button.js-test-cloud-connection.btn.btn-secondary(data-provider="s3") {{_ 'test-cloud-connection'}}
@@ -32,6 +32,8 @@ const CLOUD_FIELD_IDS = {
accessKeyId: '#s3-access-key',
secretAccessKey: '#s3-secret-key',
forcePathStyle: '#s3-force-path-style',
serverSideEncryption: '#s3-sse',
sseKmsKeyId: '#s3-sse-kms-key-id',
},
azure: {
enabled: '#azure-enabled',
@@ -858,6 +860,11 @@ Template.attachments.helpers({
CLOUD_PROVIDERS.forEach(p => { out[p] = cloudConfigFromSettings(tpl, p).read !== false; });
return out;
},
// The S3 server-side encryption option that is stored (models/lib/s3Encryption.js).
s3SseSelected(mode) {
const stored = cloudConfigFromSettings(Template.instance(), 's3').serverSideEncryption || '';
return stored === mode ? 'selected' : null;
},
cloudValue() {
const tpl = Template.instance();
const s3 = cloudConfigFromSettings(tpl, 's3');
@@ -872,6 +879,8 @@ Template.attachments.helpers({
bucket: s3.bucket || '',
accessKeyId: s3.accessKeyId || '',
forcePathStyle: s3.forcePathStyle !== false,
serverSideEncryption: s3.serverSideEncryption || '',
sseKmsKeyId: s3.sseKmsKeyId || '',
},
azure: {
accountName: azure.accountName || '',
@@ -36,8 +36,8 @@
}
.migration-progress-header {
/* WeKan blue (matches the main header #2980b9) */
background: linear-gradient(135deg, #2980b9 0%, #1a5276 100%);
/* WeKan blue (matches the main header #2573a7) */
background: linear-gradient(135deg, #2573a7 0%, #1a5276 100%);
color: white;
padding: 20px;
display: flex;
@@ -87,7 +87,7 @@
.migration-progress-overall-fill {
/* WeKan blue */
background: linear-gradient(90deg, #2980b9, #3498db);
background: linear-gradient(90deg, #2573a7, #3498db);
height: 100%;
border-radius: 10px;
transition: width 0.3s ease;
@@ -138,7 +138,7 @@
.migration-progress-step-fill {
/* WeKan blue */
background: linear-gradient(90deg, #3498db, #2980b9);
background: linear-gradient(90deg, #3498db, #2573a7);
height: 100%;
border-radius: 8px;
transition: width 0.3s ease;
@@ -156,7 +156,7 @@
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
border-inline-start: 4px solid #2980b9;
border-inline-start: 4px solid #2573a7;
}
.migration-progress-status-label {
@@ -0,0 +1,23 @@
//- Admin Panel / Problems / Password length (docs/Features/Admin-Panel/Problems/Password-length.md):
//- the minimum, whether it is enforced, and who signs in with a shorter
//- password - through the shared table page (docs/Features/General/Table.md).
template(name="passwordLengthReport")
ul.setting-detail.password-length-settings
li
label(for="password-min-length-input") {{_ 'password-min-length'}}
input#password-min-length-input.js-password-min-length(type="number" min="1" max="256" value="{{minLength}}")
p.description {{_ 'password-min-length-description'}}
li
a.flex.js-toggle-password-min-length-enforce
.materialCheckBox(class="{{#if enforce}}is-checked{{/if}}")
span {{_ 'password-min-length-enforce'}}
p.description {{_ 'password-min-length-enforce-description'}}
li
button.primary.js-save-password-length(type="button") {{_ 'save'}}
if saved
span.password-length-saved {{_ 'password-length-saved'}}
if saveError
p.error.password-length-error= saveError
if fromEnvironment
p.description {{_ 'password-length-from-environment'}}
+tablePage(tablePageData)
@@ -0,0 +1,157 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { formatDateForDisplay } from '/client/lib/dateDisplay';
import { buildFilters, buildHeader, buildRows, pageInfo } from '/models/lib/tablePage';
import { errorMessage } from '/client/lib/errorMessage';
// Admin Panel / Problems / Password length: the two settings and the table of
// accounts that sign in with a password shorter than the minimum
// (server/lib/shortPasswordLogins.js; docs/Features/Admin-Panel/Problems/Password-length.md).
const { addressValue, locationText, locationFlag } = require('/models/lib/addressReportColumns');
const { DEFAULT_FILTER } = require('/models/lib/shortPasswordLogins');
const PER_PAGE = 25;
const formatDate = date => (date ? formatDateForDisplay(date) : '');
// One cell per family: the address, then the city when a CDN in front of
// WeKan said where it is, and the country's flag before both.
const addressColumn = (labelKey, family) => ({
labelKey,
nowrap: true,
value: row => [addressValue(row, family), locationText(row, family)].filter(Boolean).join(' '),
flag: row => locationFlag(row, family),
});
const COLUMNS = [
{ labelKey: 'username', value: row => row.username || row.userId },
{ labelKey: 'password-length-first-login', nowrap: true, value: row => formatDate(row.firstAt) },
addressColumn('event-ipv4', 'ipv4'),
addressColumn('event-ipv6', 'ipv6'),
{ labelKey: 'password-length-last-login', nowrap: true, value: row => formatDate(row.at) },
{ labelKey: 'password-length-login-count', align: 'end', value: row => row.count || 0 },
{ labelKey: 'password-length-fixed', nowrap: true, value: row => formatDate(row.fixedAt) },
];
const FILTER_OPTIONS = [
{ value: 'short', labelKey: 'password-length-filter-short' },
{ value: 'fixed', labelKey: 'password-length-filter-fixed' },
{ value: 'all', labelKey: 'password-length-filter-all' },
];
Template.passwordLengthReport.onCreated(function () {
this.rows = new ReactiveVar([]);
this.total = new ReactiveVar(0);
this.page = new ReactiveVar(1);
this.search = new ReactiveVar('');
this.filter = new ReactiveVar(DEFAULT_FILTER);
this.sources = new ReactiveVar(null);
// The working copy of the two settings, until Save.
this.minLength = new ReactiveVar(8);
this.enforce = new ReactiveVar(false);
this.saved = new ReactiveVar(false);
this.error = new ReactiveVar('');
this.load = () => {
Meteor.call('shortPasswordLoginsPage', {
filter: this.filter.get(),
search: this.search.get() || undefined,
limit: PER_PAGE,
skip: (this.page.get() - 1) * PER_PAGE,
}, (error, result) => {
if (error) return;
this.rows.set((result && result.rows) || []);
this.total.set((result && result.total) || 0);
});
};
this.loadSettings = () => {
Meteor.call('getAuthConfigSources', 'login', (error, result) => {
if (error || !result) return;
this.sources.set(result.sources || {});
this.minLength.set(result.sources?.passwordMinLength?.value || 8);
this.enforce.set(result.sources?.passwordMinLengthEnforce?.value === true);
});
};
this.load();
this.loadSettings();
});
Template.passwordLengthReport.helpers({
minLength() { return Template.instance().minLength.get(); },
enforce() { return Template.instance().enforce.get(); },
saved() { return Template.instance().saved.get(); },
saveError() { return Template.instance().error.get(); },
// An environment variable set on the host is overridden by a save here; say
// so, rather than let the admin wonder which one is in effect.
fromEnvironment() {
const sources = Template.instance().sources.get() || {};
return ['passwordMinLength', 'passwordMinLengthEnforce'].some(key => sources[key]?.source === 'env');
},
tablePageData() {
const t = Template.instance();
const rows = t.rows.get() || [];
const info = pageInfo(t.total.get(), t.page.get(), PER_PAGE);
return {
descKey: 'password-length-report-desc',
emptyKey: 'password-length-no-results',
searchTerm: t.search.get(),
filters: buildFilters([{
id: 'password-length-status', labelKey: 'status', current: t.filter.get(), options: FILTER_OPTIONS,
}]),
header: buildHeader(COLUMNS),
rows: buildRows(rows, COLUMNS),
rowCount: rows.length,
totalCount: t.total.get(),
page: info.page,
totalPages: info.totalPages,
hasPrev: info.hasPrev,
hasNext: info.hasNext,
};
},
});
Template.passwordLengthReport.events({
'input .js-password-min-length'(event, tmpl) {
tmpl.minLength.set(parseInt(event.currentTarget.value, 10) || '');
tmpl.saved.set(false);
},
'click .js-toggle-password-min-length-enforce'(event, tmpl) {
event.preventDefault();
tmpl.enforce.set(!tmpl.enforce.get());
tmpl.saved.set(false);
},
'click .js-save-password-length'(event, tmpl) {
event.preventDefault();
Meteor.call('saveAuthConfigSettings', 'login', {
passwordMinLength: tmpl.minLength.get() || null,
passwordMinLengthEnforce: tmpl.enforce.get(),
}, error => {
tmpl.error.set(error ? errorMessage(error) : '');
if (error) return;
tmpl.saved.set(true);
tmpl.loadSettings();
});
},
'change .js-table-page-filter'(event, tmpl) {
tmpl.filter.set(event.currentTarget.value || DEFAULT_FILTER);
tmpl.page.set(1);
tmpl.load();
},
'click .js-table-page-prev'(event, tmpl) {
event.preventDefault();
if (tmpl.page.get() > 1) { tmpl.page.set(tmpl.page.get() - 1); tmpl.load(); }
},
'click .js-table-page-next'(event, tmpl) {
event.preventDefault();
const info = pageInfo(tmpl.total.get(), tmpl.page.get(), PER_PAGE);
if (info.hasNext) { tmpl.page.set(tmpl.page.get() + 1); tmpl.load(); }
},
'keydown .js-table-page-search'(event, tmpl) {
if (event.keyCode === 13 && !event.shiftKey) {
event.preventDefault();
event.stopPropagation();
tmpl.search.set(event.currentTarget.value || '');
tmpl.page.set(1);
tmpl.load();
}
},
});
@@ -36,6 +36,8 @@ template(name="people")
+authProviderSettings(loginOptionsData)
//- Windows SSO, which works only on a Windows server.
+authProviderSettings(windowsSsoData)
//- Strict-Transport-Security, sent when ROOT_URL is https.
+authProviderSettings(httpSecurityData)
else if samlSetting
+authProviderSettings(samlSettingsData)
else if authConfigSection
@@ -56,6 +58,8 @@ template(name="people")
+peopleGeneral(peopleTablePageData)
else if lockedUsersSetting.get
+lockedUsersGeneral
else if auditLogSetting.get
+adminAuditLog
else if rolesSetting.get
+rolesGeneral
else if templatesSetting.get
+31 -8
View File
@@ -142,6 +142,7 @@ Template.people.onCreated(function () {
this.teamSetting = new ReactiveVar(false);
this.peopleSetting = new ReactiveVar(false);
this.lockedUsersSetting = new ReactiveVar(false);
this.auditLogSetting = new ReactiveVar(false);
this.rolesSetting = new ReactiveVar(false);
this.templatesSetting = new ReactiveVar(false);
// #5850: Admin Panel > People > Domains tab. The domains table itself
@@ -434,6 +435,7 @@ Template.people.onCreated(function () {
this.teamSetting.set('team-setting' === targetID);
this.peopleSetting.set('people-setting' === targetID);
this.lockedUsersSetting.set('locked-users-setting' === targetID);
this.auditLogSetting.set('audit-log-setting' === targetID);
this.rolesSetting.set('roles-setting' === targetID);
this.templatesSetting.set('templates-setting' === targetID);
this.domainSetting.set('domains-setting' === targetID);
@@ -551,6 +553,9 @@ function peopleMenu(user) {
{ id: 'team-setting', icon: 'fa-users', labelKey: 'teams' },
{ id: 'people-setting', icon: 'fa-user', labelKey: 'people' },
{ id: 'locked-users-setting', icon: 'fa-lock', labelKey: 'accounts-lockout-locked-users', iconWrapCls: 'text-red' },
// What site administrators did (models/lib/adminAudit.js). Site admins only:
// tenantAdminPeopleMenu keeps a per-tenant admin's menu to its own panes.
{ id: 'audit-log-setting', icon: 'fa-list-alt', labelKey: 'admin-audit-log' },
{ id: 'roles-setting', icon: 'fa-key', labelKey: 'roles' },
{ id: 'templates-setting', icon: 'fa-clone', labelKey: 'shared-templates' },
{ id: 'registration-setting', icon: 'fa-key', labelKey: 'login', emoji: true },
@@ -581,6 +586,15 @@ function peopleMenu(user) {
return items;
}
// A password the server refused (server/lib/passwordPolicy.js), in the
// reader's language; the popup that sent it has already closed.
function passwordErrorMessage(error) {
const key = error && error.error;
if (key === 'password-too-short') return TAPi18n.__('password-too-short', { min: error.details?.min });
if (['password-pwned', 'password-same-as-username'].includes(key)) return TAPi18n.__(key);
return error.reason || error.message;
}
// People panes drawn from models/lib/authConfigCatalog.js, and the data the
// catalog form (authProviderSettings) needs for one section.
const AUTH_CONFIG_PANES = {
@@ -899,6 +913,8 @@ Template.people.helpers({
// The Login pane's own login settings (PASSWORD_LOGIN_ENABLED, ...).
loginOptionsData() { return authConfigData('login'); },
windowsSsoData() { return authConfigData('windowsSso'); },
// HSTS (HSTS_MAX_AGE, ...), below the login settings.
httpSecurityData() { return authConfigData('httpSecurity'); },
ldapPane() { return Template.instance().activeMenuId.get() === 'ldap-setting'; },
authenticationSection() {
return {
@@ -927,6 +943,9 @@ Template.people.helpers({
lockedUsersSetting() {
return Template.instance().lockedUsersSetting;
},
auditLogSetting() {
return Template.instance().auditLogSetting;
},
rolesSetting() {
return Template.instance().rolesSetting;
},
@@ -2010,7 +2029,7 @@ Template.modifyTeamsUsers.events({
currentUser = ReactiveCache.getUser(selectedUserChkBoxUserIds[i]);
// Copy, so the cached minimongo document is not mutated in place.
userTms = (currentUser.teams || []).slice();
currUserTeamIndex = userTms.findIndex(function(t){ return t.teamId == selectedEltValueId});
currUserTeamIndex = userTms.findIndex(function(t){ return t.teamId == selectedEltValueId;});
if(currUserTeamIndex == -1){
userTms.push({
"teamId": selectedEltValueId,
@@ -2029,7 +2048,7 @@ Template.modifyTeamsUsers.events({
for(let i = 0; i < selectedUserChkBoxUserIds.length; i++){
currentUser = ReactiveCache.getUser(selectedUserChkBoxUserIds[i]);
userTms = (currentUser.teams || []).slice();
currUserTeamIndex = userTms.findIndex(function(t){ return t.teamId == selectedEltValueId});
currUserTeamIndex = userTms.findIndex(function(t){ return t.teamId == selectedEltValueId;});
if(currUserTeamIndex != -1){
userTms.splice(currUserTeamIndex, 1);
}
@@ -2317,7 +2336,7 @@ Template.editUserPopup.events({
userTms.push({
"teamId": userTeamsIdsList[i],
"teamDisplayName": userTeamsList[i],
})
});
}
}
@@ -2330,7 +2349,7 @@ Template.editUserPopup.events({
userOrganizations.push({
"orgId": userOrgsIdsList[i],
"orgDisplayName": userOrgsList[i],
})
});
}
}
@@ -2352,7 +2371,9 @@ Template.editUserPopup.events({
});
if (isChangePassword) {
Meteor.call('setPassword', password, this.userId);
Meteor.call('setPassword', password, this.userId, error => {
if (error) alert(passwordErrorMessage(error));
});
}
if (isChangeEmailVerified) {
@@ -2532,7 +2553,7 @@ const UpdateUserOrgsOrTeamsElement = function(isNewUser = false){
}
selectedElt.value = "-1";
selectedElt.style.display = "none";
}
};
Template.newOrgPopup.events({
submit(event, templateInstance) {
@@ -2612,7 +2633,7 @@ Template.newUserPopup.events({
userTms.push({
"teamId": userTeamsIdsList[i],
"teamDisplayName": userTeamsList[i],
})
});
}
}
@@ -2624,7 +2645,7 @@ Template.newUserPopup.events({
userOrganizations.push({
"orgId": userOrgsIdsList[i],
"orgDisplayName": userOrgsList[i],
})
});
}
}
@@ -2651,6 +2672,8 @@ Template.newUserPopup.events({
} else if (errorElement === 'email-already-taken') {
usernameMessageElement.hide();
emailMessageElement.show();
} else if (String(errorElement).startsWith('password-')) {
alert(passwordErrorMessage(error));
}
} else {
usernameMessageElement.hide();
+7 -7
View File
@@ -10,7 +10,7 @@
height: 100%;
}
.setting-content {
color: #727479;
color: var(--wekan-quiet-text, #5a5c61);
background: #dedede;
width: 100%;
height: 100%;
@@ -99,7 +99,7 @@
selected entry back to white as soon as the pointer crossed it. */
.setting-content .content-body .side-menu ul li.active,
.setting-content .content-body .side-menu ul li.active:hover {
background: var(--theme-accent, #2980b9);
background: var(--theme-accent, #2573a7);
}
/* …and when a THEME is active, the row is painted by that theme's own #header rule
@@ -488,7 +488,7 @@ body.left-menu-resizing-active {
}
.setting-content label:has(> input[type="checkbox"]:focus-visible) {
outline: 2px solid #2980b9;
outline: 2px solid #2573a7;
outline-offset: 2px;
border-radius: 3px;
}
@@ -511,7 +511,7 @@ body.left-menu-resizing-active {
}
.setting-content input[type="checkbox"]:focus-visible {
outline: 2px solid var(--theme-accent, #2980b9);
outline: 2px solid var(--theme-accent, #2573a7);
outline-offset: 2px;
}
@@ -562,13 +562,13 @@ body.left-menu-resizing-active {
}
.setting-content .schedule-day-btn.active {
background: var(--theme-accent, #2980b9);
border-color: var(--theme-accent, #2980b9);
background: var(--theme-accent, #2573a7);
border-color: var(--theme-accent, #2573a7);
color: #fff;
}
.setting-content .schedule-day-btn:focus-visible {
outline: 2px solid var(--theme-accent, #2980b9);
outline: 2px solid var(--theme-accent, #2573a7);
outline-offset: 2px;
}
@@ -630,6 +630,10 @@ template(name='tableVisibilityModeSettings')
.title {{_ 'text-below-custom-login-logo'}}
.form-group
textarea#text-below-custom-login-logo.wekan-form-control(data-editor-submit=".js-visibility-logo-save")= currentSetting.textBelowCustomLoginLogo
li.tableVisibilityMode-form
.title {{_ 'login-background-color'}}
.form-group
input.wekan-form-control#login-background-color(type="text", placeholder="#1b365d" value="{{currentSetting.loginBackgroundColor}}")
li.tableVisibilityMode-form
.title {{_ 'custom-top-left-corner-logo-image-url'}}
.form-group
@@ -811,6 +811,7 @@ Template.tableVisibilityModeSettings.events({
['#custom-login-logo-image-url', 'customLoginLogoImageUrl'],
['#custom-login-logo-link-url', 'customLoginLogoLinkUrl'],
['#text-below-custom-login-logo', 'textBelowCustomLoginLogo'],
['#login-background-color', 'loginBackgroundColor'],
['#custom-top-left-corner-logo-image-url', 'customTopLeftCornerLogoImageUrl'],
['#custom-top-left-corner-logo-link-url', 'customTopLeftCornerLogoLinkUrl'],
['#custom-top-left-corner-logo-height', 'customTopLeftCornerLogoHeight'],
+13 -136
View File
@@ -43,7 +43,7 @@
display: none;
}
.sidebar .sidebar-content h3 {
color: #808080;
color: var(--wekan-quiet-text, #595959);
font-size: 1em;
margin-bottom: 10px;
font-weight: bold;
@@ -147,55 +147,17 @@
cursor: pointer;
}
/* Board Settings / Card - also the opened card's "+ Add" - is the Admin
Panel's left menu and right pane (sidebar.jade boardCardSettingsPopup,
docs/Features/Cards/Card-Fields.md): the sections on the left, the open
section's rows on the right, one line each - the label, then the
"Show on Minicard" cell and the "Show on Card" cell (a checkbox, the
position and the order arrows of that side), and opened from a card the
"On this card" checkbox. */
.board-card-settings {
container-type: inline-size;
}
.card-settings-sections .setting-content {
position: relative;
/* High enough for every entry of the left menu - Card Settings and the
twelve sections - where the window has the room, so the menu does not
scroll; a shorter window caps it. */
height: 52em;
max-height: calc(100dvh - 7em);
background: transparent;
}
.card-settings-sections .setting-content .content-body { height: 100%; }
.card-settings-sections .main-body { overflow: auto; flex: 1 1 auto; min-width: 16em; }
.card-settings-pane-head,
/* Card Settings - on the opened card and in Board Settings / Card, one
template (sidebar.jade cardSettingsInline; its checklist look is
client/components/cards/cardAddFields.css): a row's line, its checkbox and
its name. */
.card-settings-line {
display: grid;
/* Two lists: Show on Card - opened from a card On this card first, then
Show on Card, Show empty (#6806) and the field - and Show on Minicard,
then the field. Logical order: a right-to-left page mirrors it. Each
line is its own grid, so the side columns have FIXED widths - sized by
content, every line and the heading line would size them differently and
nothing would line up. */
grid-template-columns: 9em 8em minmax(10em, 1fr);
column-gap: 14px;
align-items: center;
/* Every heading and every cell starts at its column's left edge (its start,
right-to-left), so a heading and the checkboxes under it line up. */
justify-items: start;
text-align: start;
}
.board-card-settings.has-card .card-settings-list-card > .card-settings-pane-head,
.board-card-settings.has-card .card-settings-list-card > .card-settings-line {
grid-template-columns: 7em 9em 8em minmax(10em, 1fr);
}
.card-settings-list-minicard > .card-settings-pane-head,
.card-settings-list-minicard > .card-settings-line {
grid-template-columns: 9em minmax(10em, 1fr);
}
/* The minicard's list under the card's, a little apart. */
.card-settings-list + .card-settings-list {
margin-block-start: 14px;
padding: calc(4px * min(1, var(--wekan-ui-font-scale, 1))) 0;
border-bottom: 1px solid #eee;
}
/* What a row is dragged by: its handle (drag handles on), its icon or name. */
.card-field-order-drag-label {
@@ -205,30 +167,6 @@
background: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.card-settings-pane-head {
font-weight: bold;
border-bottom: 1px solid #eee;
padding-block-end: 4px;
align-items: start;
}
/* The headings' smaller font is on the headings, not on their line: the
columns are in em, and a smaller font on the line made every column of the
heading narrower than the same column of the rows - each heading drifted
further left of its checkboxes, and the rows' labels past the pane. */
.card-settings-pane-head > span {
font-size: calc(12px * var(--wekan-ui-font-scale, 1));
}
.card-settings-line {
padding: calc(4px * min(1, var(--wekan-ui-font-scale, 1))) 0;
border-bottom: 1px solid #eee;
}
.card-settings-side-head {
/* A heading longer than its column, in another language, wraps in it. */
overflow-wrap: anywhere;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.card-settings-line-label {
display: inline-flex;
align-items: center;
@@ -246,86 +184,19 @@
.card-settings-side > a.flex {
margin: 0;
}
/* Select all / Select none under a column's heading, for that column's rows
of the open section: the Draggable table's buttons
(boardSettingsColumnActions.css), a little closer to their heading. */
.card-settings-select {
margin: 3px 0 0;
font-weight: normal;
}
.card-settings-select button {
white-space: nowrap;
}
.card-settings-inline-action {
/* The forms float their buttons (Save left, Delete right); flow-root keeps
those floats inside the form, or the rows below wrapped around them. */
display: flow-root;
padding: 6px 0 10px;
border-bottom: 1px solid #eee;
}
.card-settings-inline-action h4 { margin: 0 0 6px; }
/* Create Field and Link fields to another card are forms of their own in the
Custom Fields pane: the list's two buttons that open them are not repeated. */
.card-settings-inline-action .card-custom-fields-more { display: none; }
.card-settings-sections .materialCheckBox { transition: none; }
.card-settings-sections .card-add-field.is-animating .materialCheckBox,
.card-settings-sections .card-field-order-toggle.is-animating .materialCheckBox,
.card-settings-sections .card-always-show.is-animating .materialCheckBox,
.card-settings-sections .card-show-empty-fields.is-animating .materialCheckBox,
.card-settings-sections .card-ask-move-reason.is-animating .materialCheckBox { transition: 0.2s; }
.card-field-order-toggle,
.card-field-order-move {
flex-shrink: 0;
}
.card-field-order-move.is-disabled,
.card-field-order-toggle.is-disabled,
.card-always-show.is-disabled,
.card-show-empty-fields.is-disabled,
.card-ask-move-reason.is-disabled {
opacity: 0.3;
cursor: not-allowed;
}
/* Show empty fields: every field is shown on every card, so the Show empty
column is set aside - hidden, and its track taken out of the grid. */
.board-card-settings.show-empty-fields .card-settings-side-always,
.board-card-settings.show-empty-fields .card-settings-side-head-always {
display: none;
}
.board-card-settings.show-empty-fields .card-settings-list-card > .card-settings-pane-head,
.board-card-settings.show-empty-fields .card-settings-list-card > .card-settings-line {
grid-template-columns: 9em minmax(10em, 1fr);
}
.board-card-settings.show-empty-fields.has-card .card-settings-list-card > .card-settings-pane-head,
.board-card-settings.show-empty-fields.has-card .card-settings-list-card > .card-settings-line {
grid-template-columns: 7em 9em minmax(10em, 1fr);
}
.card-settings-board-option { margin-block-end: 6px; }
/* One side of the same popup: a menu that asks for one side hides the other
side's list, so there is still ONE set of lists and one set of handlers. */
.board-card-settings.show-card-only .card-settings-list-minicard,
.board-card-settings.show-minicard-only .card-settings-list-card {
display: none;
}
/* "Labels text" and "List title" are the user's or the card's own, not the
board's: they are not among the board-wide rows of Board Settings / Card,
but they are where they belong - opened from a card, from the minicard's
own view, and for a reader who may change only those. */
.board-card-settings .card-settings-row-personal {
display: none;
}
.board-card-settings.show-minicard-only .card-settings-row-personal,
.board-card-settings.has-card .card-settings-row-personal,
.board-card-settings.show-personal-only .card-settings-row-personal {
display: grid;
}
.board-card-settings.show-personal-only .card-settings-line:not(.card-settings-row-personal) {
display: none;
}
/* Board Settings / Board View: the same table as Card Settings above, with
FIVE columns (Default/Show on Public, Default/Show on Private, the view's
name) - or three when Admin Panel hides public boards and the popup does
@@ -879,6 +750,12 @@ body.sidebar-resizing-active {
margin-inline-end: 6px;
appearance: auto;
}
/* Why a date range was refused, below its buttons (sidebarFilters.js,
rangeRefused): the browser's own bubble covered those buttons in Firefox. */
.filter-date-range-error {
color: #c0392b;
margin: 6px 0 0;
}
.filter-native-group {
border: 0;
padding: 0;
+198 -151
View File
@@ -269,147 +269,39 @@ template(name="boardInfoOnMyBoardsPopup")
i.fa.fa-arrows-h
| {{_ 'personal-list-width'}}
//- Board Settings / Card - what the opened card and the minicard show, and
in what order. Opened from the board's cog menu (boardMenuPopup above); it
used to be split across the CARD's own menu ("Show on Card", side="card")
and the MINICARD's own menu ("Show on Minicard", side="minicard"), and
settingsSideClass() still hides one column when a side is passed.
"Labels text" - the one PERSONAL setting here - stays reachable by a
non-admin, who is passed `personalOnly=true` so every board-wide,
admin-only row is hidden and only that one shows. client/lib/minicardLabelText.js
//- ONE heading, "Card field order", over TWO lists: "Show on Minicard" in
the board's minicard order and "Show on Card" in its card order. The lists
are independent because the orders are: a field can sit third on the
minicard and last on the card. Every row of either list is
`[checkbox] [up] [down] icon label` - the checkbox is whether that side
shows the field, the arrows move it on THAT side only, and the icon and
label are the field's own (models/lib/cardSettingsRows.js). The rows are
not written out here: the two `each` loops draw them from that table in
the order models/lib/cardFieldOrder.js resolves for the board, the SAME
source cardDetails.jade and minicard.jade render from, so what the popup
lists top to bottom is what the card and the minicard draw top to bottom.
A row whose arrows would do nothing (the card's title bar and its
galleries, a minicard row that modifies another element rather than being
one) keeps them, disabled, so every row reads the same.
Up/down buttons rather than a drag library: no sortable-based reorder of a
plain settings LIST exists elsewhere in this codebase to reuse, so buttons
are the simpler, safer mechanism. Keyboard-reachable: `href="#"` and
`role="button"`, titled with the existing move-up/move-down keys.
client/components/sidebar/sidebar.js, tests/cardSettingsCoverage.test.cjs
//- Board Settings / Card - Card Settings: what every opened card and every
minicard of the board show, and in what order. Opened from the board's cog
menu (boardMenuPopup above); the opened card has no Card Settings of its
own. Whoever may not change the board sees the board's rows disabled and
may still set their own ("Labels text").
template(name="boardCardSettingsPopup")
//- Board Settings / Card, and the opened card's "+ Add": ONE popup, the Admin
Panel's left menu and right pane (docs/Features/Cards/Card-Fields.md). The
menu lists the sections (models/lib/cardSettingsSections.js); the pane the
open section's rows, each with its "Show on Minicard" and "Show on Card"
cell - a checkbox and the order arrows of that side - and, opened from a
card, whether THIS card shows the field. A side cell is the
`.js-card-field-order-row` the handlers read data-key and data-side from.
form.board-card-settings.card-settings-sections(class=settingsSideClass)
.setting-content
.content-body
+leftMenu(settingsMenuItems)
.main-body
+paneTitle(settingsPaneTitle)
//- The first entry, Card: what is not a field, then its rows.
if openSettingsSectionIsGeneral
//- Every card shows all its fields, the empty ones too; the Show
empty column is then set aside, its ticks kept.
div.check-div.card-settings-board-option.card-settings-show-empty-fields
a.flex.card-show-empty-fields(class="{{#if canModifyBoard}}js-show-empty-card-fields{{else}}is-disabled{{/if}} {{#if isAnimating 'showEmptyCardFields'}}is-animating{{/if}}" role="checkbox" aria-checked="{{#if showEmptyCardFields}}true{{else}}false{{/if}}")
.materialCheckBox(class="{{#if showEmptyCardFields}}is-checked{{/if}}")
span {{_ 'card-settings-show-empty-fields'}}
//- The same checkbox, in the same place, as Show empty fields.
div.check-div.card-settings-board-option.card-settings-ask-move-reason
a.flex.card-ask-move-reason(class="{{#if canModifyBoard}}js-ask-move-reason{{else}}is-disabled{{/if}} {{#if isAnimating 'askForMoveReason'}}is-animating{{/if}}" role="checkbox" aria-checked="{{#if askForMoveReason}}true{{else}}false{{/if}}")
.materialCheckBox(class="{{#if askForMoveReason}}is-checked{{/if}}")
span {{_ 'ask-move-reason'}}
//- No select-all of Show on Minicard / Show on Card here: their
checkboxes are in the sections, not under it.
+boardDragSettings(section="card")
.card-settings-pane
//- Under the title: everything in the hamburger of this section on
the opened card - Filter: Subtasks, Edit voting, Card kind, the
section's settings ... (docs/Features/Cards/Card-Fields.md). The
card's own actions only when opened from a card.
if sectionCardActions.length
ul.pop-over-list.card-settings-card-actions
each action in sectionCardActions
li
//- An inline action is its form, right here (Edit voting,
Change spent time, Card kind, ...); the others open
their popup.
if action.inline
.card-settings-inline-action
h4
i.fa(class=action.icon aria-hidden="true")
| {{action.title}}
+Template.dynamic(template=action.inline data=action.inlineData)
else
a.js-card-settings-action(data-action="{{action.id}}")
i.fa(class=action.icon aria-hidden="true")
| {{action.title}}
//- Two lists, each in its own order and reordered by drag and
drop (sidebar.js reorderOnDrop): Show on Card - the opened
card's fields of this section, in the card's order, with On
this card (opened from a card) and Show empty beside each - and
Show on Minicard, in the minicard's order. Under each heading,
Select all / Select none for that column. The columns are
mirrored in a right-to-left language by the grid itself.
.card-settings-list.card-settings-list-card.js-card-field-order-sortable(data-side="card")
.card-settings-pane-head
if settingsCardId
span.card-settings-side-head.card-settings-side-head-this
span {{_ 'card-settings-this-card'}}
+cardSettingsSelectButtons(column="this")
span.card-settings-side-head.card-settings-side-head-card
span {{_ 'show-on-card'}}
if canModifyBoard
+cardSettingsSelectButtons(column="card")
span.card-settings-side-head.card-settings-side-head-always
span {{_ 'card-settings-show-empty'}}
if canModifyBoard
+cardSettingsSelectButtons(column="always")
span.card-settings-pane-head-label {{_ 'card-field-order'}}
each row in sectionCardRows
.card-settings-line.js-card-field-order-row(class="{{#if row.personal}}card-settings-row-personal{{/if}} {{#if row.positioned}}is-positioned{{/if}}" data-key="{{row.key}}" data-side="card")
//- Opened from a card: whether this card shows the field -
the field with data is shown, an empty one when ticked
here (models/lib/cardShownFields.js).
if settingsCardId
if row.perCard
span.card-settings-side.card-settings-side-this
a.flex.card-add-field.js-card-add-field(data-key="{{row.key}}" role="checkbox" aria-checked="{{#if row.shownOnThisCard}}true{{else}}false{{/if}}" class="{{#if isAnimating row.key}}is-animating{{/if}}" title="{{_ 'card-settings-this-card'}}")
.materialCheckBox(class="{{#if row.shownOnThisCard}}is-checked{{/if}}")
else
span.card-settings-side.card-settings-side-this.card-settings-side-empty
+cardSettingsSideToggle(row=row)
//- #6806: Show empty - every card of the board shows the
field, empty or not - by default the description,
attachments and comments; "On this card" still hides it
from one card. Hidden while Show empty fields is on.
if row.alwaysShowable
span.card-settings-side.card-settings-side-always
a.flex.card-always-show(class="{{#if canModifyBoard}}js-card-always-show{{else}}is-disabled{{/if}} {{#if row.animatingAlways}}is-animating{{/if}}" data-key="{{row.key}}" role="checkbox" aria-checked="{{#if row.alwaysShown}}true{{else}}false{{/if}}" title="{{_ 'card-settings-show-empty'}}")
.materialCheckBox(class="{{#if row.alwaysShown}}is-checked{{/if}}")
else
span.card-settings-side.card-settings-side-always.card-settings-side-empty
+cardSettingsRowLabel(row=row)
.card-settings-list.card-settings-list-minicard.js-card-field-order-sortable(data-side="minicard")
.card-settings-pane-head
span.card-settings-side-head.card-settings-side-head-minicard
span {{_ 'show-on-minicard'}}
if canModifyBoard
+cardSettingsSelectButtons(column="minicard")
span.card-settings-pane-head-label
each row in sectionMinicardRows
.card-settings-line.js-card-field-order-row(class="{{#if row.personal}}card-settings-row-personal{{/if}} {{#if row.positioned}}is-positioned{{/if}}" data-key="{{row.key}}" data-side="minicard")
+cardSettingsSideToggle(row=row)
+cardSettingsRowLabel(row=row)
//- BOTH sides at once (docs/Features/Cards/Card-Settings.md): the Minicard
pane first - at the start, the left in a left-to-right page - and the Card
pane after it, side by side when the popup is wide enough and stacked
(Minicard above Card) when it is not. Two instances of ONE template, each
with its own fixed side: its sections in that side's order, its rows,
checkboxes, hamburgers and drags all that side's. Each pane collapses by
the caret at its top and scrolls on its own. The popup's own header is its
title and its close button. client/components/sidebar/sidebar.css
.card-settings-panes
//- The splitter between the panes is dragged like the edge between two
spreadsheet columns (or the Admin Panel left menu's): the Minicard pane's
share of the width, kept per user (models/lib/cardSettingsSplit.js). Only
side by side, and only while neither pane is collapsed. `nodragscroll`,
like the left menu's grip, so a drag on it never pans the board.
.card-settings-panes-container.js-card-settings-panes-container
.card-settings-panes.js-card-settings-panes(class="{{#if anyPaneCollapsed}}has-collapsed-pane{{/if}}" style="--card-settings-split: {{splitFraction}}")
+cardSettingsInline(side="minicard")
.card-settings-splitter.js-card-settings-splitter.nodragscroll(
role="separator" aria-orientation="vertical" tabindex="0"
aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{splitPercent}}"
title="{{_ 'card-drawing-resize'}}" aria-label="{{_ 'card-drawing-resize'}}")
+cardSettingsInline(side="card")
//- One side's checkbox of a row of Card Settings: the board's Show on Card or
Show on Minicard, through the handler the row names (cardSettingsRows.js).
The same animated checkbox as On this card; only the one clicked animates.
Only the one clicked animates.
template(name="cardSettingsSideToggle")
span.card-settings-side(class="card-settings-side-{{row.side}}")
a.flex.card-field-order-toggle(class="{{#if row.writable}}{{row.toggle}}{{else}}is-disabled{{/if}} {{#if row.checked}}is-checked{{/if}} {{#if row.animating}}is-animating{{/if}}" title="{{row.title}}" role="checkbox" aria-checked="{{#if row.checked}}true{{else}}false{{/if}}")
@@ -417,12 +309,12 @@ template(name="cardSettingsSideToggle")
//- A row's name in Card Settings, and what it is dragged by: with drag handles
on, its handle, its icon or its name; with them off, its icon or its name.
The Up and Down arrow keys move a focused row one step. A row with no place
of its own (one that only modifies another, or is drawn at a fixed place)
is not dragged.
on, ONLY its handle - the rest of the row then scrolls the page, as a touch
there means to; with them off, its icon and its name. The Up and Down arrow
keys move a focused row one step. A row with no place of its own (one that
only modifies another, or is drawn at a fixed place) is not dragged.
template(name="cardSettingsRowLabel")
span.card-settings-line-label(class="{{#if row.draggable}}js-card-field-order-handle card-field-order-drag-label{{/if}}" tabindex="{{#if row.draggable}}{{#unless row.showDragHandles}}0{{/unless}}{{/if}}" title="{{#if row.draggable}}{{_ 'drag-to-reorder'}}{{/if}}")
span.card-settings-line-label(class="{{#if row.draggable}}{{#unless row.showDragHandles}}js-card-field-order-handle card-field-order-drag-label{{/unless}}{{/if}}" tabindex="{{#if row.draggable}}{{#unless row.showDragHandles}}0{{/unless}}{{/if}}" title="{{#if row.draggable}}{{#unless row.showDragHandles}}{{_ 'drag-to-reorder'}}{{/unless}}{{/if}}")
if row.draggable
if row.showDragHandles
a.flex.card-field-order-move.card-field-order-handle.js-card-field-order-handle(href="#" draggable="false" role="button" tabindex="0" title="{{_ 'drag-to-reorder'}}" aria-label="{{_ 'drag-to-reorder'}}")
@@ -442,14 +334,169 @@ template(name="cardSettingsRowLabel")
| &nbsp;({{_ 'label-text-follows-board'}})
//- Select all / Select none of one column of Card Settings' pane
(boardCardSettingsPopup's 'click .js-card-settings-select').
template(name="cardSettingsSelectButtons")
//- The Draggable table's buttons (boardSettingsColumnActions): white text
on the theme's colour.
span.board-settings-column-actions.card-settings-select
button.js-card-settings-select(type="button" data-column="{{column}}" data-value="all") {{_ 'select-all'}}
button.js-card-settings-select(type="button" data-column="{{column}}" data-value="none") {{_ 'select-none'}}
//- One pane of Card Settings, the body of Board Settings / Card
(docs/Features/Cards/Card-Settings.md), for ONE side - `side` is "minicard"
or "card", fixed by boardCardSettingsPopup: a header line with the caret
that collapses the whole pane at its top start and the side's title, then
the sections as checklists - a title per section with its own checkbox (the
whole section shown on this side or not), its rows as the items, the
checkbox this side's board-wide setting; a section with one row is one
line. Sections and rows are dragged like checklists and their items and
collapse like them; the sections in THIS side's order (the opened card and
the minicard draw them in it); the rest of what a section or a row has is in
its hamburger (cardSettingsSectionPopup, cardSettingsItemPopup) - drawn only
when it has something in it. The body is hidden, not removed, while the pane
is collapsed, so the drags bound to it stay bound.
client/components/sidebar/sidebar.js inlineSectionsOf, cardSettingsEvents.
template(name="cardSettingsInline")
.card-settings-inline.card-settings-pane.js-card-settings-inline.js-card-settings-pane(data-side="{{showSide}}" class="card-settings-pane-{{showSide}} {{#if paneCollapsed}}is-pane-collapsed{{/if}}")
.card-settings-pane-head
a.card-settings-pane-collapse.js-toggle-card-settings-pane(href="#" role="button" aria-expanded="{{#if paneCollapsed}}false{{else}}true{{/if}}" title="{{_ 'collapse'}}" aria-label="{{_ 'collapse'}}")
i.fa(class="{{#if paneCollapsed}}fa-caret-right{{else}}fa-caret-down{{/if}}" aria-hidden="true")
h3.card-settings-pane-title {{paneTitle}}
.card-settings-pane-body.js-card-settings-pane-body(hidden=paneCollapsed)
.card-settings-inline-sections.js-card-settings-sections-sortable(data-side="{{showSide}}")
each section in inlineSections
+cardSettingsInlineSection(section=section side=showSide)
//- One section of Card Settings, drawn like a checklist: a
title that drags and collapses it, left of it a checkbox - whether the WHOLE
section is shown on the side Card Settings shows - a count of the ticked
rows, and its hamburger when it has anything in it; under it the rows, each
a checkbox, its name and - when it has one - its own hamburger. Collapsed,
the rows are hidden, not removed, so the drag bound to the list stays bound.
A section with ONE row on this side is one line (`section.single`): one
checkbox - the field's own, through its real checkbox kept out of sight -
its name, the section's hamburger. Custom Fields' items are the board's
custom fields, with a "+" under them (cardSettingsCustomFields.js).
template(name="cardSettingsInlineSection")
.card-settings-inline-section.js-card-settings-section(data-section="{{section.id}}" class="{{#if section.collapsed}}is-collapsed{{/if}} {{#if section.draggable}}is-draggable{{/if}} {{#if section.single}}is-single{{/if}} {{#unless section.shown}}is-section-hidden{{/unless}}")
.card-settings-inline-section-head(class="{{#if section.flagRow}}js-card-field-order-row{{/if}}" data-key="{{#if section.flagRow}}{{section.flagRow.key}}{{/if}}" data-side="{{#if section.flagRow}}{{section.flagRow.side}}{{/if}}")
unless section.single
a.card-settings-inline-collapse.js-toggle-card-settings-section(href="#" role="button" aria-expanded="{{#if section.collapsed}}false{{else}}true{{/if}}" title="{{_ 'collapse'}}" aria-label="{{_ 'collapse'}}")
i.fa(class="{{#if section.collapsed}}fa-caret-right{{else}}fa-caret-down{{/if}}" aria-hidden="true")
if section.draggable
if section.showDragHandles
a.card-settings-inline-section-move.js-card-settings-section-handle(href="#" draggable="false" role="button" tabindex="0" title="{{_ 'drag-to-reorder'}}" aria-label="{{_ 'drag-to-reorder'}}")
i.fa.fa-arrows(aria-hidden="true")
//- The section's checkbox: for one line and for Custom Fields the
field's own flag (and the section shown); otherwise whether the whole
section is shown on this side. A board admin's, like the rows.
span.card-settings-side.card-settings-section-side
if section.flagRow
a.flex.card-field-order-toggle.card-settings-section-toggle(class="{{#if section.flagWritable}}js-card-settings-single-toggle{{else}}is-disabled{{/if}} {{#if section.flagChecked}}is-checked{{/if}} {{#if section.flagRow.animating}}is-animating{{/if}}" data-side="{{section.side}}" title="{{section.sideTitle}}" aria-label="{{section.sideTitle}}" role="checkbox" aria-checked="{{#if section.flagChecked}}true{{else}}false{{/if}}")
.materialCheckBox(class="{{#if section.flagChecked}}is-checked{{/if}}")
else
a.flex.card-field-order-toggle.card-settings-section-toggle(class="{{#if section.writable}}js-card-settings-section-shown{{else}}is-disabled{{/if}} {{#if section.shown}}is-checked{{/if}} {{#if section.animatingShown}}is-animating{{/if}}" data-side="{{section.side}}" title="{{section.sideTitle}}" aria-label="{{section.sideTitle}}" role="checkbox" aria-checked="{{#if section.shown}}true{{else}}false{{/if}}")
.materialCheckBox(class="{{#if section.shown}}is-checked{{/if}}")
if section.flagRow
//- The field's real checkbox, out of sight: the line's checkbox
clicks it, so the field is set by its own handler.
span.card-settings-single-real.js-card-settings-single-real(hidden aria-hidden="true")
+cardSettingsSideToggle(row=section.flagRow)
//- Like a row: with drag handles on, only the handle drags the section.
h4.card-settings-inline-section-title(class="{{#if section.draggable}}{{#unless section.showDragHandles}}js-card-settings-section-handle{{/unless}}{{/if}}" tabindex="{{#if section.draggable}}{{#unless section.showDragHandles}}0{{/unless}}{{/if}}" title="{{#if section.draggable}}{{#unless section.showDragHandles}}{{_ 'drag-to-reorder'}}{{/unless}}{{/if}}")
i.fa(class=section.icon aria-hidden="true")
| {{section.title}}
unless section.single
span.card-settings-inline-section-count {{section.checkedCount}}/{{section.total}}
if section.hasMenu
a.card-settings-inline-menu.js-open-card-settings-section-menu(href="#" role="button" class="{{#if section.menuOpen}}is-open{{/if}}" aria-expanded="{{#if section.menuOpen}}true{{else}}false{{/if}}" title="{{_ 'actions'}}" aria-label="{{_ 'actions'}}")
i.fa.fa-bars(aria-hidden="true")
//- What the hamburger holds opens HERE, under the section's line, and
pushes the rest down - not in a popup (cardSettingsInlinePanel).
if section.menuOpen
+cardSettingsInlinePanel(kind="section" sectionId=section.id side=side)
unless section.single
if section.customFields
.card-settings-inline-items.js-card-settings-custom-fields-sortable(data-side="{{side}}")
each field in section.customFieldItems
.card-settings-line.card-settings-inline-item.card-settings-custom-field.js-card-settings-custom-field(class="{{#if field.draggable}}is-positioned{{/if}}" data-id="{{field.id}}" data-side="{{field.side}}")
span.card-settings-side
a.flex.card-field-order-toggle(class="{{#if field.writable}}js-card-settings-custom-field-toggle{{else}}is-disabled{{/if}} {{#if field.checked}}is-checked{{/if}} {{#if field.animating}}is-animating{{/if}}" title="{{field.sideTitle}}" role="checkbox" aria-checked="{{#if field.checked}}true{{else}}false{{/if}}")
.materialCheckBox(class="{{#if field.checked}}is-checked{{/if}}")
span.card-settings-line-label(class="{{#if field.draggable}}{{#unless field.showDragHandles}}js-card-settings-custom-field-handle card-field-order-drag-label{{/unless}}{{/if}}" tabindex="{{#if field.draggable}}{{#unless field.showDragHandles}}0{{/unless}}{{/if}}" title="{{#if field.draggable}}{{#unless field.showDragHandles}}{{_ 'drag-to-reorder'}}{{/unless}}{{/if}}")
if field.draggable
if field.showDragHandles
a.flex.card-field-order-move.card-field-order-handle.js-card-settings-custom-field-handle(href="#" draggable="false" role="button" tabindex="0" title="{{_ 'drag-to-reorder'}}" aria-label="{{_ 'drag-to-reorder'}}")
i.fa.fa-arrows
i.fa.fa-list-alt(aria-hidden="true")
| {{field.name}}
if field.hasMenu
a.card-settings-inline-menu.js-card-settings-edit-custom-field(href="#" role="button" class="{{#if field.menuOpen}}is-open{{/if}}" aria-expanded="{{#if field.menuOpen}}true{{else}}false{{/if}}" title="{{_ 'actions'}}" aria-label="{{_ 'actions'}}")
i.fa.fa-bars(aria-hidden="true")
if field.menuOpen
+cardSettingsInlinePanel(kind="field" fieldId=field.id side=side)
//- Under the items, also when there are none: a new custom field of
the board, which then is the last item.
if section.writable
.card-settings-add-custom-field
a.card-settings-add-custom-field-button.js-card-settings-add-custom-field(href="#" role="button" class="{{#if section.createOpen}}is-open{{/if}}" aria-expanded="{{#if section.createOpen}}true{{else}}false{{/if}}" title="{{_ 'createCustomField'}}" aria-label="{{_ 'createCustomField'}}")
i.fa.fa-plus(aria-hidden="true")
if section.createOpen
+cardSettingsInlinePanel(kind="create" side=side)
else
.card-settings-inline-items.js-card-field-order-sortable(data-side="{{side}}")
each row in section.rows
.card-settings-line.card-settings-inline-item.js-card-field-order-row(class="{{#if row.personal}}card-settings-row-personal{{/if}} {{#if row.positioned}}is-positioned{{/if}}" data-key="{{row.key}}" data-side="{{row.side}}")
+cardSettingsSideToggle(row=row)
+cardSettingsRowLabel(row=row)
if row.hasMenu
a.card-settings-inline-menu.js-open-card-settings-item-menu(href="#" role="button" class="{{#if row.menuOpen}}is-open{{/if}}" aria-expanded="{{#if row.menuOpen}}true{{else}}false{{/if}}" title="{{_ 'actions'}}" aria-label="{{_ 'actions'}}")
i.fa.fa-bars(aria-hidden="true")
if row.menuOpen
+cardSettingsInlinePanel(kind="row" rowKey=row.key sectionId=section.id side=side)
//- What a hamburger of Card Settings holds, opened UNDER its line inside the
pane - an accordion, not a popup (docs/Features/Cards/Card-Settings.md
Hamburgers): its hamburger shows it is open (`is-open`, aria-expanded) and
the panel hangs from it. One open at a time per pane; the same hamburger
again, its X or Escape closes it. `kind`:
section - the section's board actions (Subtasks: the subtask settings
themselves); for Card, what is not a field - asking for a move
reason and the Draggable table;
row - Labels text's way back to the board's default;
field - the custom field's edit form; create - the + under Custom
Fields, a new custom field's form.
No event map of its own: its checkboxes are handled by the pane's
(sidebar.js cardSettingsEvents), so a click is handled once. The forms and
the subtask settings keep their own handlers, inside
`.js-card-settings-panel-own-handlers`, which the pane's leave alone.
template(name="cardSettingsInlinePanel")
.card-settings-inline-panel.js-card-settings-panel(class="card-settings-inline-panel-{{kind}}" data-kind="{{kind}}")
a.card-settings-inline-panel-close.js-close-card-settings-panel(href="#" role="button" title="{{_ 'close'}}" aria-label="{{_ 'close'}}")
i.fa.fa-times(aria-hidden="true")
if $eq kind "section"
.card-settings-section-menu
if sectionIsGeneral
//- What is not a field.
div.check-div.card-settings-board-option.card-settings-ask-move-reason
a.flex.card-ask-move-reason(class="{{#if canModifyBoard}}js-ask-move-reason{{else}}is-disabled{{/if}} {{#if isAnimating 'askForMoveReason'}}is-animating{{/if}}" role="checkbox" aria-checked="{{#if askForMoveReason}}true{{else}}false{{/if}}")
.materialCheckBox(class="{{#if askForMoveReason}}is-checked{{/if}}")
span {{_ 'ask-move-reason'}}
each action in sectionActions
.card-settings-section-action.js-card-settings-panel-own-handlers(data-action="{{action.id}}")
h4.card-settings-section-action-title
i.fa(class=action.icon aria-hidden="true")
| {{action.title}}
+Template.dynamic(template=action.template)
if sectionIsGeneral
+boardDragSettings(section="card")
if $eq kind "row"
with itemRow
.card-settings-item-menu.js-card-field-order-row(data-key="{{key}}" data-side="{{side}}")
if menu.labelTextReset
div.check-div.card-settings-board-option
a(class=labelTextResetClass) {{_ 'label-text-use-board-default'}}
if $eq kind "field"
if fieldDefinition
.card-settings-panel-form.js-card-settings-panel-own-handlers
+Template.dynamic(template="createCustomFieldPopup" data=fieldDefinition)
if $eq kind "create"
.card-settings-panel-form.js-card-settings-panel-own-handlers
+Template.dynamic(template="createCustomFieldPopup" data=createData)
//- The board's Subtasks Settings. Its own template, because the same settings
@@ -745,8 +792,8 @@ template(name="boardMenuPopup")
board-wide list of toggles behind two different places depending on
what you were looking at, and which also let anyone open it for the one
PERSONAL row in it ("Labels text" - stored on the user's own profile,
not the board). It is back here as one board setting, and keeps that
same personal-only fallback for a non-admin.
not the board). It is back here as one board setting; a non-admin sees
the board's rows disabled and may still set that personal row.
client/components/cards/cardDetails.jade
ul.pop-over-list
if currentUser.isBoardAdmin
File diff suppressed because it is too large. Load diff
@@ -128,6 +128,16 @@ function getSettings(tpl) {
return settings;
}
// The form drawn inline in a Card Settings panel: tell the pane it is done
// (sidebar.js listens for `cardsettingspaneldone`). Returns whether it was.
function closeCardSettingsPanel(evt) {
const el = evt && (evt.currentTarget || evt.target);
const panel = el && el.closest && el.closest('.js-card-settings-panel');
if (!panel) return false;
panel.dispatchEvent(new CustomEvent('cardsettingspaneldone', { bubbles: true }));
return true;
}
Template.createCustomFieldPopup.onCreated(function () {
const data = Template.currentData();
this.type = new ReactiveVar(
@@ -374,8 +384,12 @@ Template.createCustomFieldPopup.events({
CustomFields.update(currentData._id, { $set: data });
}
// Its own popup steps back; in Card Settings' Custom Fields pane the form
// stays, emptied for the next field (docs/Features/Cards/Card-Fields.md).
// Its own popup steps back. In a Card Settings panel - under the field's
// hamburger or the + (sidebar.jade cardSettingsInlinePanel) - the panel
// closes and Card Settings stays open: Popup.back() there would close
// Card Settings itself.
if (closeCardSettingsPanel(evt)) return;
// Elsewhere inside another popup the form stays, emptied for the next field.
if (!backIfOwnPopup(evt, ['createCustomFieldPopup', 'editCustomFieldPopup']) && !editing) {
const name = tpl.find('.js-field-name');
if (name) name.value = '';
@@ -140,7 +140,10 @@ template(name="filterSidebar")
| {{_ 'filter-dates-label' }}
each dateFilterProviderViews
+Template.dynamic(template=template data=data)
form.js-card-movement-range
//- novalidate: the range is checked in the submit handler (rangeRefused in
sidebarFilters.js). WebKit keeps a half-edited date segment after Clear
empties a field and then refused every submit of the form, with no message.
form.js-card-movement-range(novalidate)
h4 {{_ 'filter-movement-range'}}
label(for="filter-card-movement-from") {{_ 'filter-date-range-from'}}
input.js-card-movement-from(id="filter-card-movement-from" type="date" value=movementFrom)
@@ -148,7 +151,9 @@ template(name="filterSidebar")
input.js-card-movement-to(id="filter-card-movement-to" type="date" value=movementTo)
button.primary(type="submit") {{_ 'apply'}}
button.js-clear-movement-range(type="button") {{_ 'filter-clear'}}
form.js-card-date-range
if movementRangeError
p.js-card-movement-range-error.filter-date-range-error(role="alert") {{_ 'filter-date-range-invalid'}}
form.js-card-date-range(novalidate)
label(for="filter-card-date-field") {{_ 'filter-date-range-field'}}
select.js-card-date-field(id="filter-card-date-field")
each dateRangeFields
@@ -162,6 +167,8 @@ template(name="filterSidebar")
| {{_ 'filter-date-range-missing'}}
button.primary(type="submit") {{_ 'apply'}}
button.js-clear-date-range(type="button") {{_ 'filter-clear'}}
if dateRangeError
p.js-card-date-range-error.filter-date-range-error(role="alert") {{_ 'filter-date-range-invalid'}}
hr
fieldset.filter-native-group
legend {{_ 'due-date'}}
+26 -12
View File
@@ -13,9 +13,23 @@ import { getSidebarInstance } from '/client/features/sidebar/service';
import { CARD_DATE_RANGE_FIELDS, CARD_RECENCY_PRESETS } from '/models/lib/cardDateRange';
import { DEPENDENCY_TYPES } from '/models/metadata/dependencies';
// A refused date range marks its end field invalid - so it is :invalid and
// screen readers announce the reason - and says why in the form itself. Not
// with reportValidity(): Firefox's validation bubble opens over the Apply and
// Clear buttons below the field and takes every click on them while the field
// has focus, so the range could not be cleared at all.
function rangeRefused(end, error, refused) {
end.setCustomValidity(refused ? TAPi18n.__('filter-date-range-invalid') : '');
error.set(refused);
}
Template.filterSidebar.onCreated(function () {
this.presetMessage = new ReactiveVar('');
this.presetBusy = new ReactiveVar(false);
// Why a date range was refused, shown in the form rather than in the
// browser's own validation bubble (see rangeRefused below).
this.movementRangeError = new ReactiveVar(false);
this.dateRangeError = new ReactiveVar(false);
this.autorun(() => {
const boardId = Session.get('currentBoard');
if (Meteor.userId() && boardId) this.subscribe('savedCardFilters', boardId);
@@ -28,6 +42,8 @@ Template.filterSidebar.helpers({
},
presetMessage() { return Template.instance().presetMessage.get(); },
presetBusy() { return Template.instance().presetBusy.get(); },
movementRangeError() { return Template.instance().movementRangeError.get(); },
dateRangeError() { return Template.instance().dateRangeError.get(); },
filterProviderViews() { return Filter.providers.views(); },
dateFilterProviderViews() { return Filter.providers.views('dates'); },
dueUnrestricted() { return Filter.dueAt.state() === null; },
@@ -102,15 +118,15 @@ Template.filterSidebar.events({
const end = tpl.find('.js-card-movement-to');
const valid = Filter.movementDate.set({ field: 'createdAt',
from: tpl.find('.js-card-movement-from').value, to: end.value, includeMissing: false });
end.setCustomValidity(valid ? '' : TAPi18n.__('filter-date-range-invalid'));
if (!valid) end.reportValidity();
else Filter.resetExceptions();
rangeRefused(end, tpl.movementRangeError, !valid);
if (valid) Filter.resetExceptions();
},
'input .js-card-movement-range input'(event, tpl) {
tpl.find('.js-card-movement-to').setCustomValidity('');
rangeRefused(tpl.find('.js-card-movement-to'), tpl.movementRangeError, false);
},
'click .js-clear-movement-range'(event, tpl) {
event.preventDefault(); tpl.find('.js-card-movement-to').setCustomValidity('');
event.preventDefault();
rangeRefused(tpl.find('.js-card-movement-to'), tpl.movementRangeError, false);
Filter.movementDate.reset(); Filter.resetExceptions();
},
'submit .js-card-date-range'(event, tpl) {
@@ -119,22 +135,20 @@ Template.filterSidebar.events({
const valid = Filter.dateRange.set({ field: tpl.find('.js-card-date-field').value,
from: tpl.find('.js-card-date-from').value, to: end.value,
includeMissing: tpl.find('.js-card-date-missing').checked });
end.setCustomValidity(valid ? '' : TAPi18n.__('filter-date-range-invalid'));
if (!valid) end.reportValidity();
else Filter.resetExceptions();
rangeRefused(end, tpl.dateRangeError, !valid);
if (valid) Filter.resetExceptions();
},
'input .js-card-date-range input'(event, tpl) {
tpl.find('.js-card-date-to').setCustomValidity('');
rangeRefused(tpl.find('.js-card-date-to'), tpl.dateRangeError, false);
},
'click .js-clear-date-range'(event, tpl) {
event.preventDefault();
// Reset the native date controls as well as the reactive model. WebKit can
// retain an edited date segment after Blaze changes the value attribute.
for (const selector of ['.js-card-date-from', '.js-card-date-to']) {
const input = tpl.find(selector);
input.value = '';
input.setCustomValidity('');
tpl.find(selector).value = '';
}
rangeRefused(tpl.find('.js-card-date-to'), tpl.dateRangeError, false);
Filter.dateRange.reset(); Filter.resetExceptions();
},
'submit .js-column-age-filter'(event, tpl) {
+3 -3
View File
@@ -417,14 +417,14 @@ body.swimlane-resizing-active * {
`outline`, not `border` or `box-shadow`: it is drawn outside the box and so
moves neither the swimlane's rows nor the list's cards by a pixel. */
.is-revealed {
outline: 3px solid #2980b9;
outline: 3px solid #2573a7;
outline-offset: -3px;
animation: wekan-reveal-fade 2s ease-out forwards;
}
@keyframes wekan-reveal-fade {
0% { outline-color: #2980b9; }
70% { outline-color: #2980b9; }
0% { outline-color: #2573a7; }
70% { outline-color: #2573a7; }
100% { outline-color: transparent; }
}
Loaded 100 of 986 files, more files were not shown because too many files have changed in this diff. Show more