Compare commits

...
648 Commits
Author SHA1 Message Date
Lauri Ojansivu 74f2352387 v9.35 2026-06-06 10:54:38 +03:00
Lauri Ojansivu f299ca1285 Updated translations. 2026-06-06 10:29:24 +03:00
Lauri Ojansivu f47e67b5b4 Fixed Move Attachments and Avatars.
Thanks to xet7 and Claude !

Fixes #6364
2026-06-06 10:19:03 +03:00
Lauri Ojansivu fe16b94c1e Updated ChangeLog. 2026-06-06 09:59:16 +03:00
Lauri Ojansivu b77b62bfa2 Ask to install npm dependencies before running tests.
Thanks to xet7 and Claude !
2026-06-06 09:57:06 +03:00
Lauri Ojansivu 43d8bf1b92 Updated ChangeLog. 2026-06-06 09:31:39 +03:00
Lauri Ojansivu dca9427bfb New features:
- **Admin Panel / Attachments: the MongoDB GridFS page notes how to make legacy
  files visible.** A translatable line under the "MongoDB GridFS Storage" title
  reminds admins: "To have attachments and avatars visible, move them from
  CollectionFS to any other Storage." Thanks to Claude.
- **Admin Panel / Attachments: the Google Cloud Storage page now documents the
  required bucket permission.** A note under the "GCS Storage" title explains that
  the service account needs the **Storage Object Admin** role
  (`roles/storage.objectAdmin`) on the bucket (Cloud Console → Cloud Storage →
  Buckets → your bucket → Permissions → Grant access → add the service account →
  role "Storage Object Admin"), and that the bucket must exist in the same
  project — otherwise "Test connection" fails with `storage.objects.list denied`.
  Thanks to Claude.
- **Admin Panel / Attachments: a "Save" button next to the Enabled / Read toggles
  at the top of each cloud storage.** S3, Azure and Google Cloud Storage now have
  a Save button directly below the "[ ] Enabled  [ ] Read" row, so those toggles
  can be saved without scrolling to the bottom of the (now quite long) storage
  page. It saves the same way as the existing bottom Save button — the server
  merges that provider's config and preserves secrets left blank — and the bottom
  Save button is kept. (Filesystem and GridFS have only a Read toggle, which
  already saves the instant it is toggled, so they need no button.) Thanks to Claude.

Fixes:

- **Admin Panel / Attachments: "Run MongoDB compact" now actually compacts the
  database.** MongoDB refuses `compact` on an active replica-set primary unless
  `force: true` is given, so every collection failed with "will not run compact
  on an active replica set primary … use force:true to force" — and because the
  usual Meteor setup is a single-node replica set (only a primary, no
  secondaries), nothing was compacted at all and no disk space was reclaimed. The
  primary is now compacted with `force: true` (secondaries are still compacted
  without it, so the primary stays available while they run), so the GridFS
  collections are actually rewritten and freed space is returned to the
  filesystem. Thanks to Claude.
- **Admin Panel / Attachments: clearer cloud "Test connection" errors, and cloud
  config inputs are trimmed.** Testing an Azure/GCS/S3 connection used to report a
  single generic `Incomplete configuration or adapter not installed` even when the
  real problem was specific — e.g. Azure rejecting the config with `Invalid URL`
  (a stray space/newline in the account name, or a bad endpoint / connection
  string). `testCloudConnection` now reports the actual cause: adapter not
  installed, required fields missing, the adapter's own configuration error (such
  as `Invalid URL`), or the real `listFiles` error (auth failure, container not
  found, …). It also pre-validates the config and gives an actionable message
  before the adapter turns it into a cryptic error — e.g. for Azure it explains
  that the **Storage account name** must be just the account name (3–24 lowercase
  letters/numbers, e.g. `wekanstorage`, not a full `https://…` URL and with no
  spaces), or that the **Connection string** is malformed; and when Azure still
  returns `Invalid URL` the message appends which field to check. In addition, all
  cloud-storage text fields are now trimmed when read from the form, so
  leading/trailing whitespace from copy-paste no longer produces an invalid URL.

Thanks to xet7 and Claude.
2026-06-06 09:28:05 +03:00
Lauri Ojansivu 7f17616b75 Updated translations. 2026-06-06 09:23:15 +03:00
Lauri Ojansivu 4a9dcdd0ef Updated translations. 2026-06-06 08:41:23 +03:00
Lauri Ojansivu a1a4176ffb Updated ChangeLog. 2026-06-06 07:30:45 +03:00
Lauri Ojansivu a86a087915 New features:
- **Admin Panel / Attachments: "Calculate file counts" on every storage backend,
  shown right below the "Read" toggle.** Azure Blob Storage and Google Cloud
  Storage now have a "Calculate file counts" button (new `getAzureStorageStats` /
  `getGcsStorageStats` methods) that reports how many attachments and avatars are
  stored on that backend, matching what Filesystem, MongoDB GridFS and S3 already
  offer. The count is read from the stored file metadata (fast, no cloud API
  calls). The S3 button was also moved up to sit directly under its "Read"
  checkbox, so all backends are consistent. Thanks to Claude.
- **Admin Panel / Attachments: bilingual, self-documenting cloud-storage fields
  (S3, Azure and Google Cloud Storage).** Each cloud-storage setting now guides
  the admin from top to bottom with both English and translated text, so it is
  easy to follow along while clicking through the provider's Cloud Console:
  - the field **label** uses the provider's own console wording in English
    (e.g. *Access key ID*, *Secret access key*, *Storage account key*,
    *Service account key (JSON)*), with the translated label shown below it;
  - a literal **example** of what the value should look like
    (e.g. `eu-west-1`, an example bucket name / endpoint / connection string),
    which is intentionally not translated;
  - a short **description** of the value, in English and then translated;
  - the **Cloud Console menu path** showing exactly where to find or create the
    value (e.g. *AWS Console → IAM → Users → your user → Security credentials →
    Access keys → Create access key*; *Azure Portal → Storage accounts → your
    account → Access keys → key1*; *Google Cloud Console → IAM & Admin →
    Service accounts → … → Keys → Add key → Create new key → JSON*), again in
    English and then translated.
  The provider field names, examples and menu paths are kept in English so they
  match what the Cloud Console actually shows, while the descriptions and menu
  paths are also translatable; section titles, the enable/read/force-path-style
  checkboxes and the Test/Save buttons remain fully translated. Thanks to Claude.
- **Admin Panel / Attachments / Move Attachment: "Repair file locations" button
  and a persistent "last move" message.** The new **Repair file locations** button
  scans all attachments and avatars and finds any whose recorded storage
  (`versions.<v>.storage` / `path` / `meta.gridFsFileId`) no longer matches where
  the binary actually is — left inconsistent by an interrupted or failed move —
  and fixes the database to point at the real location: it detects the binary in
  GridFS (by the `metadata.fileId` stamped on upload, recovering files whose
  GridFS id reference was lost) or on the filesystem (using the storage
  strategy's own thorough path resolution, so the repair agrees with what a real
  download/move would find even when `versions.<v>.path` is stale), then corrects
  `storage`, `path` and `meta.gridFsFileId` accordingly. Cloud-stored files are
  left untouched, and files found nowhere are reported as "Not found". The scan is
  streamed (memory-safe) and shows a per-scope searched / repaired / not-found
  summary. Separately, after a move finishes the page now keeps showing
  the last move operation — source → destination (scope) and the date/time as
  `YYYY-MM-DD HH:MM:SS` — so it is clear what was last done. Thanks to Claude.

 New fixes:

- **Fixed moving attachments to S3 (and other cloud storage) failing and
  crashing the server.** Uploading to S3 failed two ways, each of which crashed
  the whole server because the rejection was unhandled and SyncedCron treats
  those as fatal. The `@tweedegolf/sab-adapter-amazon-s3` adapter uploaded with a
  `PutObjectCommand` whose `Body` was a live stream: (1) with no `ContentLength`
  the AWS SDK fell back to a chunked signed upload that needs a decoded content
  length, which was undefined for a stream of unknown size — `Invalid value
  "undefined" for header "x-amz-decoded-content-length"`; and (2) if the socket
  dropped mid-upload (`socket hang up`), the SDK's body-stream promise rejected
  unhandled. Fixed by uploading the file as a complete in-memory **buffer**
  (`addFileFromBuffer`) instead of a live stream: a buffer has a known length (no
  content-length error) and no socket-bound stream to re-reject. The cloud upload
  promise now never rejects — any failure is captured and exposed via
  `waitUntilStored()`, which `moveToStorage` checks inside a try/catch so a failed
  upload leaves the source file intact and is logged cleanly. (Files are moved one
  at a time, so peak memory is one file.) Thanks to Claude.
- **Fixed `TypeError: Cannot read properties of undefined (reading 'on')` when
  moving attachments between Meteor-Files/GridFS and Filesystem.** If a file's
  source binary was missing at its recorded location (e.g. a file left
  half-moved by an earlier interrupted run — storage says "gridfs" but the
  GridFS id reference is gone, or a filesystem path that no longer exists),
  `getReadStream()`/`getWriteStream()` returned `undefined` and `moveToStorage`
  (and `copyFile`) called `.on()` on it, throwing. Both now detect a missing
  read/write stream, log a clear message (which file, version, from→to storage,
  and why), **skip that file and leave the source intact** so no data is lost,
  and continue with the rest. Use the new "Repair file locations" button to fix
  the underlying inconsistent records. Thanks to Claude.
- **Fixed moving attachments/avatars from CollectionFS to Meteor-Files crashing
  the server, hanging the Admin Panel move at "File 1 / N", and leaving broken
  avatars.** Three problems in Admin Panel / Attachments / Move Attachment:
  - **Server crash on the first file** (`uncaughtException: TypeError: Cannot read
    properties of undefined (reading '_id')` at
    `AttachmentStoreStrategyGridFs.writeStreamFinished`). The current mongodb
    driver's GridFS `GridFSBucketWriteStream` `'finish'` event no longer passes
    the stored file document, so `finishedData._id` threw and killed the process
    (which is also why the move appeared stuck at "File 1 / 4" — the background
    job died mid-file and its persisted status was frozen). The GridFS strategy
    now reads the uploaded file id from the write stream itself (`gridFSFile._id`
    / `id`), the `'finish'` handler is wrapped so it can never crash the process,
    and a startup reconciliation clears a stale "running" move status left by a
    crashed run so the UI un-sticks and a new move can be started.
  - **Broken avatar after migrating avatars.** The bulk move only repointed card
    cover references (`cards.coverId`); a user's `profile.avatarUrl` still pointed
    at the deleted legacy `/cfs/files/avatars/<oldId>` URL, so the avatar rendered
    broken. The move now repoints `profile.avatarUrl` to the migrated avatar
    (`remapReferences` handles avatars), and migrated files stamp
    `meta.migratedFromId` so references can be repaired to the **exact** new file.
    The startup repair in `server/models/users.js` (previously a no-op: it
    string-replaced the URL prefix while keeping the now-deleted id and never
    saved the document) now points each affected user's avatar at their migrated
    Meteor-Files avatar — matched precisely by `meta.migratedFromId`, otherwise
    strictly by `userId` (newest migrated avatar first), so a user can only ever
    be given their own avatar, never another user's.
  - **`ObjectID` deprecation warning** from `models/lib/grid/createObjectId.js`
    (`MongoInternals.NpmModule.ObjectID` → `ObjectId`).

Thanks to Claude and xet7 !
2026-06-06 07:21:59 +03:00
Lauri Ojansivu f9ace4cde9 Updated translations. 2026-06-06 06:47:36 +03:00
Lauri Ojansivu 4677324425 Updated translations. 2026-06-06 05:23:18 +03:00
Lauri Ojansivu 7e5852c62c Updated ChangeLog. 2026-06-06 04:23:25 +03:00
Lauri Ojansivu e01f99eb52 Fix Dropdown list cannot be created with values.
**Fixed Dropdown custom field options not being addable / saving as empty.**
Creating a "Dropdown" custom field showed the "List Options" box, but pressing
Enter (or typing and saving) never added any option, and on the card the only
selectable value was `(none)`. The custom-fields sidebar component was migrated
from `BlazeComponent` to a plain `Template`, but the template still iterates the
options with `{{#each dropdownItems.get}}` — under BlazeComponent that resolved
to the instance's `dropdownItems` ReactiveVar, whereas a plain Template does not
expose instance variables to the template, so the list rendered nothing. Because
the options were never rendered as inputs, `getDropdownItems()` then overwrote
the ReactiveVar with the empty DOM on save and every entered value was dropped.
Fixed by re-adding the missing `dropdownItems` helper (returning the ReactiveVar),
matching the pattern the other migrated templates already use. Thanks to Claude.

**Fixed `Exception in global helper _` when opening the Create Custom Field
popup (and any translation containing a literal `%`).** i18n is configured with
a global sprintf post-processor (`postProcess: ["sprintf"]`), so every
translation is run through `i18next-sprintf-postprocessor`. The help text
`custom-field-stringtemplate-format` (`"Format (use %{value} as placeholder)"`)
contains a literal `%{value}` that sprintf cannot parse, so `vsprintf` threw and
crashed the global Blaze `_` translation helper — breaking that popup and any
string (in any language, including user translation overrides) that contains a
stray `%`. `TAPi18n.__` now retries without the sprintf post-processor when it
throws, returning the raw string (so `%{value}` is shown literally) instead of
crashing.

Thanks to rouceto1 and Claude.

Fixes #6357
2026-06-06 04:19:03 +03:00
Lauri Ojansivu 8ff9a41f2d Updated ChangeLog. 2026-06-06 04:14:25 +03:00
Lauri Ojansivu 075e86b00e **Fixed new checklists (and checklist items) on a newly added card not being
visible until logout/login.** The `board` publication batched checklists,
checklist items, comments and attachments into board-level cursors filtered by
`cardId: { $in: cardIds }`, where `cardIds` was a one-time snapshot. In
`reywood:publish-composite` a child cursor only re-runs when its parent (the
board) document changes, so the snapshot never refreshed when a card was added
— a checklist on a card created after subscribing matched no published card and
only appeared on the next subscribe (logout/login). This was a regression from
the "Optimized board loading" change, which had replaced the original reactive
per-card child cursors with these batched snapshots. Fixed by **denormalizing a
`boardId` field onto `Checklists` and `ChecklistItems`** so they can be
published with a single board-level cursor filtered by `boardId` — one cursor
per collection (keeping the load optimization) that still reacts to checklists
on newly added cards, because a new checklist is created already carrying the
board's id. `boardId` is set on insert (server `before.insert` hooks, plus
explicitly in the Trello/WeKan board importers, which use `direct.insert` and
bypass hooks), re-derived when a checklist/item or its card moves to another
card (`before.update` on `cardId`) or the card moves to another board
(`Cards.after.update` cascade), and backfilled for existing data by an
idempotent startup migration. New `{ boardId: 1 }` indexes were added on both
collections. Comments and attachments instead remain reactive as per-card
children of the cards cursor. Assigned-only board members still only receive
checklists for cards assigned to them (the board-level cursor falls back to the
assigned cards' ids for those roles).

Thanks to ahlgrimma and Claude !

Fixes #6361
2026-06-06 04:02:48 +03:00
Lauri Ojansivu f20080a603 Updated translations. 2026-06-06 03:26:10 +03:00
Lauri Ojansivu 3594d75b5d Updated ChangeLog. 2026-06-06 03:13:31 +03:00
Lauri Ojansivu 72767ad9a7 - Fixed SyncedCron crash.
**Fixed deleting archived lists (or many cards) crashing the server with
  `SyncedCron: Fatal error encountered (unhandledRejection): TypeError: Cannot
  read properties of undefined (reading 'boardId')` at
  `server/models/checklistItems.js`.** Deleting a list cascades into removing its
  cards, and each card's checklists, checklist items, comments and attachments
  (`cardRemover` in `models/cards.js`). `Cards.before.remove` called `cardRemover`
  **without `await`** (and was not `async`), so the card document was deleted
  first and the cascade then ran with the parent card already gone; the
  `ChecklistItems.before.remove` / `Checklists.before.remove` hooks dereferenced
  the now-undefined card (`card.boardId`) and threw, and because the promise was
  unhandled, SyncedCron caught the rejection and tore down all running cron jobs.
  Fixed by making `Cards.before.remove` `async` and awaiting `cardRemover` (so
  sub-items are removed while the card still exists), and the REST card-delete
  handler now runs `cardRemover` before removing the card. As defense in depth,
  every card-activity hook and helper that looked up a card and used its
  `boardId` / `listId` / `swimlaneId` now skips (with a warning) when the parent
  card — or, for checklist-completion activities, the parent checklist — is
  missing, instead of throwing: `before.remove` on checklist items and
  checklists, `Checklists.after.insert`, the `Cards.before.update` timing
  activity, and the shared `itemCreation` / `publishCheckActivity` /
  `publishChekListCompleted` / `publishChekListUncompleted` / `commentCreation`
  helpers (matching the guards already present in
  `server/models/cardComments.js`).

Thanks to titver968 and Claude !

Fixes #6366
2026-06-06 03:10:13 +03:00
Lauri Ojansivu 7db386fda3 Updated ChangeLog. 2026-06-06 02:55:55 +03:00
Lauri Ojansivu 39f3c89b0a Fixed upgrade crash.
Thanks to poc-sm and Claude !

Fixes #6367
2026-06-06 02:53:58 +03:00
Lauri Ojansivu 15a598525f Updated ChangeLog. 2026-06-06 02:10:21 +03:00
Lauri Ojansivu d30803276d - **Fixed Admin Panel / Attachments / Move Attachment doing nothing / crashing.**
Several issues in the bulk move:
- The GridFS source matcher required `meta.gridFsFileId` to be *absent*, but
  Meteor-Files always sets it, so selecting "MongoDB Meteor-Files" matched zero
  files and "nothing happened". The matcher now recognizes real GridFS files
  (`versions.*.storage === 'gridfs'` or a `meta.gridFsFileId` reference), and a
  "nothing to move" message is shown when a source is empty instead of silently
  doing nothing.
- Moving files crashed the whole server with
  `FilesCollection#findOne() not available in server` — `ReactiveCache.getAttachment`
  used a synchronous `findOne()`; it now uses `findOneAsync()`. The background
  job is also hardened so a single failing file is skipped instead of crashing
  the server via an unhandled rejection.
- The attachment **copy** API now honours the admin API upload limits (it
  previously skipped them).
- **Fixed the "MongoDB Meteor-Files" file-count statistic** in Admin Panel /
  Attachments, which counted *every* attachment metadata document (so files on
  the Filesystem were wrongly reported as being in GridFS). It now counts only
  attachments actually stored in GridFS, consistent with the move tool. Also
  renamed the mislabeled "Mongo-Files" column to "Meteor-Files". Thanks to Claude.
- **Unified attachment/avatar storage migration: move any → any.** Admin Panel /
  Attachments / Move Attachment can now move **Attachments, Avatars, or both**,
  from **any source to any destination** across Filesystem, Meteor-Files GridFS,
  Cloud (S3/Azure/GCS) and legacy CollectionFS GridFS. The default source is
  "All Read-enabled storages" (every backend whose Read flag is enabled and whose
  settings work), so everything can be consolidated into one destination in a
  single run. All metadata is preserved (board / swimlane / list / card / user /
  uploaded date / name / type / size), attachment cover references
  (`cards.coverId`) are remapped when an id changes, and the legacy source is
  deleted only after the new copy is verified. Thanks to Claude.
- **Legacy CollectionFS GridFS is now a first-class storage backend** (read,
  migrate-from, and export-to) for both attachments and avatars, via the new
  `models/lib/collectionFsStore.js` (binary keyed by `copies.<coll>.key` in the
  `cfs_gridfs.<coll>` bucket, metadata in `cfs.<coll>.filerecord`). Thanks to Claude.
- **The storage strategy layer is now collection-aware.** `moveToStorage` and the
  GridFS/Cloud strategies previously hard-coded the `Attachments` collection, so
  moving avatars to GridFS/cloud would have updated the wrong collection. The
  factory now carries its collection (`Attachments` or `Avatars`), so **avatars
  can be stored in Meteor-Files GridFS and cloud**, not only on the filesystem.
  Attachment behavior is unchanged. Thanks to Claude.
- **Read legacy CollectionFS attachments and avatars in place** (without
  migrating). The backward-compatibility layer
  (`models/lib/attachmentBackwardCompatibility.js`) was broken — it looked up the
  GridFS binary by the filerecord `_id` and by filename instead of by
  `ObjectId(copies.<coll>.key)`, so legacy files were never found. It is fixed and
  generalized for attachments and avatars. Legacy attachments now appear in the
  card attachment gallery (new `legacyBoardAttachments` publication) and stream
  from the `cfs_gridfs.attachments` bucket, and legacy avatars
  (`/cfs/files/avatars/<id>` URLs) are served from the `cfs_gridfs.avatars`
  bucket instead of redirecting to a 404.

Thanks to Claude.
2026-06-06 02:06:16 +03:00
Lauri Ojansivu be63a357dd Updated ChangeLog. 2026-06-05 23:47:19 +03:00
Lauri Ojansivu c71a97cba2 **Fixed OpenAPI REST API documentation generation**, which had been broken
since after WeKan v7.93 and only generated docs for the `login`/`register`
endpoints (2 operations) instead of the full API. The Meteor 3 migration moved
the REST routes from `models/*.js` (`JsonRoutes.add(...)`) into
`server/models/*.js` (`WebApp.handlers.get/post/put/delete(...)`) and
introduced optional chaining (`?.`) that the `esprima` Python parser cannot
read, so `openapi/generate_openapi.py` silently skipped every route file.
The generator now understands both routing styles, scans both `models/` and
`server/models/`, downlevels modern JS syntax so files parse, handles the
`type: Array` SimpleSchema idiom, and `releases/rebuild-docs.sh` works directly
with Python 3.12.x (PEP 668). The generated `public/api/wekan.yml` /
`wekan.html` now cover the full API again (89 operations / 61 paths).
Thanks to Claude.

**Documented the attachment / file REST API in the OpenAPI docs** — file
upload, download, info, listing a board's files, copy, move and delete (the
`/api/attachment/*` endpoints registered via `WebApp.handlers.use()`, which the
generator cannot auto-discover) are now documented in `openapi/extra_paths.yml`
and injected into the generated docs, including their authentication and
permission requirements. Thanks to Claude.

**Security: attachment write operations in the REST/DDP API now require board
write access, not just membership (CWE-862, CWE-639).** Both attachment API
implementations (`server/routes/attachmentApi.js` HTTP routes and
`server/attachmentApi.js` Meteor methods) gated upload / copy / move / delete
on `board.hasMember()`, which is true for any active member regardless of role.
This let read-only, comment-only, no-comments, worker and assigned-only board
members add, copy, move and delete attachments through the API — actions the UI
forbids for those roles. They now require board write access (global site
admins still allowed); read operations (download / list / info) keep
membership-level access. Added security tests in
`server/lib/tests/attachmentApi.tests.js`. Thanks to Claude.

**The attachment copy API now honours the admin "Admin Panel / Attachments"
API transfer limits.** Copying an attachment creates a new attachment but
skipped the `apiUploadBlocked` / `apiUploadMaxBytes` checks that upload
enforces; copy now respects them in both API implementations. Thanks to Claude.

**SVG image uploads are now sanitized instead of rejected.** Uploaded SVGs
(attachments and avatars) are cleaned in place in `onAfterUpload` via the new
`models/lib/sanitizeSvg.js`, which removes JavaScript (`<script>`, inline
`on*=` event handlers, `javascript:`/`vbscript:` URIs, `<foreignObject>` /
`<iframe>` / `<object>` / `<embed>` and similar active content) and XML loops
(`<!DOCTYPE>` / `<!ENTITY>` entity-expansion / XXE constructs and
`<?xml-stylesheet?>`), so SVG images can be uploaded safely.

Thanks to Claude.
2026-06-05 23:44:04 +03:00
Lauri Ojansivu 357de728c0 Fix [CloneBleed](https://wekan.fi/hall-of-fame/clonebleed/) and other security issues. Added more security tests to test suite.
Thanks to dizconnectz for the coordinated disclosure, and Claude.
2026-06-05 21:40:24 +03:00
Lauri Ojansivu db3c0c18f9 Updated ChangeLog. 2026-06-05 19:09:11 +03:00
Lauri Ojansivu 45706292e7 Updated translations. 2026-06-05 19:08:57 +03:00
Lauri Ojansivu 29b3f52e50 Updated to Meteor 3.5-rc.1 .
Thanks to Meteor developers !
2026-06-05 19:04:12 +03:00
Lauri Ojansivu f81a0d1537 Merge pull request #6365 from italojs/main
Update DDP transport and reactivity order for multi-tenant deployment
2026-06-05 18:57:44 +03:00
Lauri Ojansivu e613e8763b Updated Docker Meteor 3 settings. Part 2.
Thanks to italojs and xet7 !

Related https://github.com/meteor/meteor/pull/14112#issuecomment-4595290396
2026-06-02 00:03:27 +03:00
Lauri Ojansivu 3d17810293 Updated Docker Meteor 3 settings.
Thanks to italojs and xet7 !

Related https://github.com/meteor/meteor/pull/14112#issuecomment-4595290396
2026-06-02 00:00:22 +03:00
Lauri Ojansivu 1871873da0 Caddyfile: Separate websocket port not required.
Thanks to italojs !

Related https://github.com/meteor/meteor/pull/14112#issuecomment-4596252210
2026-06-01 23:40:52 +03:00
Lauri Ojansivu 15b3213a4d Fix typos 2026-06-01 22:51:20 +03:00
Lauri Ojansivu 4265288417 Updates 2026-06-01 22:49:15 +03:00
Lauri Ojansivu a36d6f9326 Merge branch 'main' of github.com:wekan/wekan 2026-06-01 22:45:07 +03:00
Lauri Ojansivu 3c50f90b51 Add separate websocket port 2026-06-01 22:44:27 +03:00
italo jose 24a4b8e5ee Refactor MongoDB service entrypoint and command for improved readability 2026-06-01 16:22:46 -03:00
italo jose e676f96905 Update DDP transport and reactivity order for multi-tenant deployment; add docker-compose example 2026-06-01 15:54:54 -03:00
Lauri Ojansivu a903154ab2 Updated ChangeLog. 2026-05-31 14:38:24 +03:00
Lauri Ojansivu beedf2fefa Update Windows MongoDB.
Thanks to xet7 !
2026-05-31 14:36:32 +03:00
Lauri Ojansivu dee5711000 v9.34 2026-05-31 13:42:43 +03:00
Lauri Ojansivu e93c049e8c Updated translations. 2026-05-31 13:38:57 +03:00
Lauri Ojansivu fc10ce4259 Fix to not list all boards etc at Admin Panel / Attachments / Move Attachment.
Thanks to xet7 !
2026-05-31 13:35:34 +03:00
Lauri Ojansivu 2573034149 v9.33 2026-05-31 12:46:36 +03:00
Lauri Ojansivu a1fc2a91dc 9.33 2026-05-31 12:40:33 +03:00
Lauri Ojansivu e86c37e636 Updated translations. 2026-05-31 12:18:56 +03:00
Lauri Ojansivu 8b71386b90 Updated translations. 2026-05-31 11:51:31 +03:00
Lauri Ojansivu 2edef5b4ab Updated translations. 2026-05-31 11:50:10 +03:00
Lauri Ojansivu d2a25ae9a6 Updated ChangeLog. 2026-05-31 11:15:12 +03:00
Lauri Ojansivu 6f021aa387 Mobile / touch fixes (Fairphone 4 postmarketOS Firefox and Fairphone 4 Ubuntu Touch Morph browser; iPhone 12 Mini was already correct):
- Fixed one-finger drag-to-scroll not working in the All Boards view and the
  Calendar view on Fairphone 4 Firefox and Ubuntu Touch Morph. These browsers
  do not emit the synthetic mouse events the `@wekanteam/dragscroll` library
  relies on, so scrolling only worked in the Swimlanes/Lists views. Added a new
  native touch-scrolling helper `client/lib/dragscrollTouch.js` that scrolls the
  nearest `.dragscroll` container with a one-finger drag (iOS is skipped, since
  native momentum scrolling already works there), and added the `dragscroll`
  class to the All Boards list and the Calendar view.
- Fixed all text, buttons, the All Boards tables and the top bars (header and
  the second bar) rendering about 2x too large on Fairphone 4. `getMobileMode()`
  defaulted to mobile mode only for iPhone, so every other phone fell back to
  desktop mode, whose large desktop sizing looks oversized on a small phone
  screen. The default is now mobile mode for any phone-sized touch device
  (iPhone, Android/Mobile browsers, Ubuntu Touch, or a coarse-pointer touch
  screen with viewport width ≤ 800px); desktop browsers stay in desktop mode and
  the user's own Mobile/Desktop toggle still takes priority.
- Fixed not being able to reorder swimlanes and lists by dragging their drag
  handle on touch devices. The new touch-scroll helper was hijacking touches
  that started on a drag handle; `.handle` / `.ui-sortable-handle` elements are
  now excluded from touch-scrolling so dragging the handle reorders as expected.
- Fixed a card duplicating into two cards when it was dragged by its title-row
  drag handle while its Card Details panel was open. The card sortable in
  `client/components/lists/list.js` was re-initialized on every list re-render
  without first destroying the previous instance, so two `stop` handlers fired
  per drag. It now destroys any existing sortable before re-initializing, the
  same guard already used for swimlanes.

Attachment storage:

- Added configurable attachment storage backends in the admin panel: choose the
  default save storage and store attachments on the local filesystem, GridFS, or
  cloud object storage — S3-compatible (AWS S3, MinIO, Cloudflare R2, Backblaze
  B2, Wasabi, DigitalOcean Spaces), Azure Blob Storage, or Google Cloud Storage
  — via the `@tweedegolf/storage-abstraction` adapters (new
  `models/lib/cloudStorage.js`). Includes a "Test connection" action and
  English translations for the new settings.
- Added a server-side bulk attachment move (new `server/attachmentBulkMove.js`
  and `models/attachmentBulkMoveStatus.js`) that moves all attachments from one
  storage backend to another as a background job whose progress survives the
  admin navigating away from or closing the page.

Thanks to xet7.
2026-05-31 11:12:41 +03:00
Lauri Ojansivu d80afc7834 v9.32 2026-05-31 05:00:13 +03:00
Lauri Ojansivu d8975d21be Updated ChangeLog. 2026-05-31 04:22:36 +03:00
Lauri Ojansivu b0918686a2 Added "Test Playwright Chromium", "Test Playwright Firefox" and "Test
Playwright Webkit" menu options to `rebuild-wekan.sh` for running the
Playwright end-to-end test suite per browser.

Thanks to xet7.
2026-05-31 04:20:30 +03:00
Lauri Ojansivu 71c9e1b3ee Updated translations. 2026-05-31 02:41:32 +03:00
Lauri Ojansivu 10759d777b Updated ChangeLog. 2026-05-31 02:24:33 +03:00
Lauri Ojansivu c956ab5a4e Add User board access roles to Admin Panel / People / Roles.
A new global "Roles" tab in Admin Panel / People lets a site admin choose which
board roles are allowed to invite users to a board ("Allow Invite to Board").
Each board role has its own toggle — Board Admin, Normal, Worker, Comment only,
No comments, Only Assigned Normal, Only Assigned Comment, Read Only and Only
Assigned Read — plus an "All Board Members" master toggle that selects them all.
The policy is enforced server-side in the `inviteUserToBoard` and `searchUsers`
methods, and the add-member button in the board sidebar is shown only to roles
the policy allows. Global Admin Panel users (site admins) always have all rights
and cannot be restricted here; this is kept clearly distinct in code from the
per-board "Board Admin" role. Secure default: only Board Admin and Normal may invite.

Thanks to xet7.
2026-05-31 02:22:31 +03:00
Lauri Ojansivu f9b790668d Updated ChangeLog. 2026-05-31 00:39:21 +03:00
Lauri Ojansivu 0e5fef6f31 Fix GHSA-hc3x-hq3m-663q: Server-Side Request Forgery (SSRF) via webhook
integration URLs (CWE-918). Wekan's outgoing-webhook integrations let a board
admin store an arbitrary URL that is later fetched server-side, so a
caller-controlled URL pointing at an internal address (cloud metadata at
`http://169.254.169.254/latest/meta-data/`, loopback, RFC 1918 ranges, etc.)
could be used to reach internal services or exfiltrate data.

**Originally fixed at the delivery layer in v8.35 and v8.36** (the
[IntegrationBleed](https://wekan.fi/hall-of-fame/integrationbleed/) fixes).
[Fix IntegrationBleed (v8.35)](https://github.com/wekan/wekan/commit/2cd702f48df2b8aef0e7381685f8e089986a18a4)
routed webhook delivery in `server/notifications/outgoing.js` through
`fetchSafe` (`server/lib/ssrfGuard.js`), which validates the URL and blocks
private/loopback IPs, and
[Fix RebindBleed of IntegrationBleed (v8.36)](https://github.com/wekan/wekan/commit/92beaa313706bc26fbea7e1cc8cfaf836609e038)
hardened it further to resolve DNS once, pin the connection to the validated
IP, and block redirects — closing the actual request-time SSRF including DNS
rebinding.

**New in this release:** the missing input-side validation the advisory
requested, added at the REST write paths in `server/models/integrations.js`.
Those endpoints accepted webhook URLs without a robust check:
`POST /api/boards/:boardId/integrations` relied only on the schema's regex
`custom()` validator (no DNS resolution, blind to a hostname that resolves to
a private IP and to decimal/octal/IPv4-mapped-IPv6 encodings), and
`PUT /api/boards/:boardId/integrations/:intId` wrote the URL via
`Integrations.direct.updateAsync`, which bypasses schema validation entirely,
so updated URLs were never validated at the data layer. Both endpoints now run
the DNS-aware `validateAttachmentUrl()`
(`models/lib/attachmentUrlValidation.js`, the same validator already used for
attachment imports) before storing the URL, rejecting
private/loopback/link-local/reserved targets with HTTP 400. Together with the
v8.35/v8.36 `fetchSafe` delivery guard and the schema validator on
client-side inserts/updates, webhook URLs are now validated on every write
path and again at delivery.

Thanks to Claude.
2026-05-31 00:35:49 +03:00
Lauri Ojansivu 832265e091 Updated ChangeLog. 2026-05-31 00:27:54 +03:00
Lauri Ojansivu 8940a10397 Fix GHSA-7w2h-g83c-jqrp: Authorization bypass in copyBoard DDP method allows
any user to copy private boards (CWE-862). The `copyBoard` Meteor method in
`server/publications/boards.js` had no authorization check: any logged-in user
could copy any board by ID — including private boards they are not a member of —
cloning all cards, checklists, custom fields, labels and rules, while the
equivalent REST endpoint `POST /api/boards/:boardId/copy` correctly required
board admin. The method also looped over caller-supplied `properties`
(`for (const key in properties) board[key] = properties[key]`) and copied them
onto the new board, letting an attacker inject arbitrary fields such as
`members` (to add themselves as admin of the copy) or `permission: 'public'`
(to expose the copy to everyone). The member-level fix shipped in v9.09 as part
of the AuthBleed fixes (caller must be authenticated, board must exist, caller
must be a board member, and `members`/`permission` are stripped from
`properties` before the copy). This release tightens it further to full parity
with the REST endpoint: `copyBoard` now requires the caller to be a board admin
(`board.hasAdmin(this.userId)`, the same `{isActive:true, isAdmin:true}`
condition the REST API enforces via `checkAdminOrCondition`) rather than merely
a member.

Thanks to Claude.
2026-05-31 00:24:13 +03:00
Lauri Ojansivu 1e1d9586d7 Updated ChangeLog. 2026-05-31 00:14:42 +03:00
Lauri Ojansivu 305864f0c7 Fix GHSA-cv95-8h7c-2ffq: Missing authorization on OIDC Meteor methods allows privilege escalation to admin (CWE-269, CWE-862).
Six Meteor methods used internally by the OIDC login flow were registered as globally DDP-callable
with no authorization, while their non-OIDC counterparts require admin. `setCreateOrgFromOidc` and
`setOrgAllFieldsFromOidc` (`server/models/org.js`), `setCreateTeamFromOidc` and
`setTeamAllFieldsFromOidc` (`server/models/team.js`) let any authenticated user create/rename/
deactivate/modify arbitrary organizations and teams — including `orgAutoAddUsersWithDomainName` —
bypassing the admin-only restriction. Most critically, `groupRoutineOnLogin`
(`packages/wekan-oidc/oidc_server.js`) sets `isAdmin` from caller-supplied group data, so with
`PROPAGATE_OIDC_DATA` enabled any authenticated user could call it over DDP with
`{groups:[{isAdmin:true,forceCreate:true}]}` and promote themselves to global admin;
`boardRoutineOnLogin` could likewise add the caller to the default board. These six methods are only
ever invoked server-side (via `Meteor.callAsync`) during the OIDC handshake, where a fix that checks
`isAdmin`/`this.userId` would break legitimate group/admin propagation (the user is not yet logged in
or admin at that point). Fixed by rejecting any direct client/DDP invocation: a server-to-server
`Meteor.callAsync` runs with `this.connection === null`, whereas a client call has a non-null
connection, so each of the six methods now throws `not-authorized` when `this.connection !== null`.
The legitimate OIDC login flow is unaffected.

Thanks to alexwaira for the coordinated disclosure, and Claude.
2026-05-31 00:11:18 +03:00
Lauri Ojansivu d475214224 Updated ChangeLog. 2026-05-31 00:01:06 +03:00
Lauri Ojansivu 73204d4e0a Fix GHSA-mp7g-hj5q-gxhq: OIDC Account Takeover via Unconditional Email-Based Account Merge in
`Accounts.onCreateUser` hook (CWE-287).
The `onCreateUser` hook in `server/models/users.js` unconditionally merged an incoming OIDC login
into any existing Wekan account whose email **or** username matched the (attacker-controlled) OIDC
claims — no ownership check, no email-verification check, no notification. An attacker who could
present a matching `email`/`username` claim (trivial on self-hosted Keycloak/Authentik, where the
`email`/`email_verified` claims are attacker-settable) inherited the victim's `_id`, boards, cards,
attachments, API tokens and admin status, all during the attacker's own first OIDC login with no
victim interaction. Fixed to fail closed, mirroring `LDAP_MERGE_EXISTING_USERS`: matching is now by
email only (never username); auto-linking is opt-in via the new `OAUTH2_MERGE_EXISTING_USERS`
setting (OFF by default, so the default deployment never merges); even when enabled the OIDC
provider must assert `email_verified=true`; otherwise the OIDC login is rejected with
`oidc-email-already-in-use` instead of merging. The provider's `email_verified` claim is now
captured into the OIDC service data in `packages/wekan-oidc/oidc_server.js`.

Thanks to alexwaira for the coordinated disclosure, and Claude.
2026-05-30 23:59:03 +03:00
Lauri Ojansivu 533187d328 Updated ChangeLog. 2026-05-30 23:45:45 +03:00
Lauri Ojansivu 70db04a93f Fix GHSA-6733-4wgq-8xvr: Read-only board members could create/modify/delete Custom Fields
(privilege escalation via read-level authz on write operations, CWE-862).
All six mutating REST handlers in `server/models/customFields.js` (POST/PUT custom-fields,
POST/PUT/DELETE dropdown-items, DELETE custom-fields) called the read-level
`Authentication.checkBoardAccess` instead of the write-level `checkBoardWriteAccess`, letting a
board member with the read-only role (`isReadOnly` / `isReadAssignedOnly`) write Custom Field
data via the REST API when `WITH_API=true`. Replaced the check with `checkBoardWriteAccess` in
all six mutating handlers (the two GET handlers correctly stay on `checkBoardAccess`), mirroring
`lists.js`/`swimlanes.js`/`cards.js`.

Thanks to Wernerina for the coordinated disclosure, and Claude.
2026-05-30 23:44:00 +03:00
Lauri Ojansivu e8eae6a1fc Updated ChangeLog. 2026-05-30 23:40:13 +03:00
Lauri Ojansivu 8ea5a6a097 Fix regression from the avatar RCE fix GHSA-35j7-h385-2q9g: external antivirus scanner broken (asyncExec undefined)
The avatar RCE fix renamed `asyncExec` to `asyncExecFile` in `models/fileValidation.js`, but the
admin-configured external scanner command line still called the now-undefined `asyncExec`, throwing
`ReferenceError` (swallowed by the catch) and making every upload silently fail validation whenever
an external scanner was configured. Restored a shell-based `asyncExec` used only for that
admin-configured command line; MIME detection still uses the shell-free `asyncExecFile`.

Thanks to Claude !
2026-05-30 23:38:58 +03:00
Lauri Ojansivu e6a2314a1e Updated ChangeLog. 2026-05-30 23:27:52 +03:00
Lauri OjansivuandCopilot Autofix powered by AI 7836ce27e7 Potential fix for code scanning alert no. 417: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-30 23:25:23 +03:00
Lauri Ojansivu a5fc4a4893 Updated ChangeLog. 2026-05-30 23:24:25 +03:00
Lauri OjansivuandCopilot Autofix powered by AI 358d893c33 Potential fix for code scanning alert no. 35: Insecure randomness
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-30 23:22:44 +03:00
Lauri Ojansivu ae211b5adf Updated ChangeLog. 2026-05-30 23:21:15 +03:00
Lauri Ojansivu d0b2b5201e Delete calendar demos, so that CodeQL stops complaining.
Thanks to xet7 !
2026-05-30 23:19:27 +03:00
Lauri Ojansivu 0130858584 Updated ChangeLog. 2026-05-30 23:16:09 +03:00
Lauri OjansivuandCopilot Autofix powered by AI 6b923eb4c2 Potential fix for code scanning alert no. 419: Clear-text logging of sensitive information
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-30 23:10:33 +03:00
Lauri Ojansivu d033dad20f Updated ChangeLog. 2026-05-30 23:09:11 +03:00
Lauri OjansivuandCopilot Autofix powered by AI f882a14ee0 Potential fix for code scanning alert no. 418: Clear-text logging of sensitive information
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-30 23:07:26 +03:00
Lauri Ojansivu 990d06b8e1 Updated ChangeLog. 2026-05-30 23:06:32 +03:00
Lauri OjansivuandCopilot Autofix powered by AI b6564c81f1 Potential fix for code scanning alert no. 48: Clear-text logging of sensitive information
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-30 23:03:37 +03:00
Lauri Ojansivu 8df46d8512 Updated ChangeLog. 2026-05-30 23:02:15 +03:00
Lauri OjansivuandCopilot Autofix powered by AI 5072861687 Potential fix for code scanning alert no. 55: Incomplete string escaping or encoding
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-30 22:59:59 +03:00
Lauri Ojansivu 919bd9e29b Updated ChangeLog. 2026-05-30 22:56:34 +03:00
Lauri OjansivuandCopilot Autofix powered by AI c5e42607af Potential fix for code scanning alert no. 56: Incomplete string escaping or encoding
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-30 22:55:01 +03:00
Lauri Ojansivu e64e2c6d53 Updated ChangeLog. 2026-05-30 22:53:56 +03:00
Lauri OjansivuandCopilot Autofix powered by AI 36274f35f4 Potential fix for code scanning alert no. 57: Incomplete multi-character sanitization
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-30 22:51:07 +03:00
Lauri Ojansivu d5e717e9ce Updated ChangeLog. 2026-05-30 22:50:26 +03:00
Lauri OjansivuandCopilot Autofix powered by AI 13f78f6207 Potential fix for code scanning alert no. 58: Incomplete multi-character sanitization
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-30 22:48:37 +03:00
Lauri Ojansivu e7aedd9f6b Updated ChangeLog. 2026-05-30 22:48:02 +03:00
Lauri OjansivuandCopilot Autofix powered by AI fc76d0e576 Potential fix for code scanning alert no. 60: Useless regular-expression character escape
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-30 22:44:58 +03:00
Lauri Ojansivu a1b7194261 Updated ChangeLog. 2026-05-30 22:43:45 +03:00
Lauri OjansivuandCopilot Autofix powered by AI 66d15f6ab0 Potential fix for code scanning alert no. 67: Incomplete multi-character sanitization
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-30 22:38:16 +03:00
Lauri Ojansivu 561e62a05c Updated ChangeLog. 2026-05-30 22:31:04 +03:00
Lauri Ojansivu b4b81684e0 Fix Incomplete string escaping in jade.js.
Thanks to CodeQL !
2026-05-30 22:28:20 +03:00
Lauri Ojansivu 898f625f1f Updated ChangeLog. 2026-05-30 22:19:42 +03:00
Lauri Ojansivu 105199991f Fix Polynomial regex DoS in Jade parser, part 2: remove the redundant $ anchor
from the interpolation regex in `compiler.js` and bundled `jade.js`. The greedy
`[\s\S]*` already matches to end of string, so dropping `$` keeps the same match
while eliminating the backtracking that CodeQL alert `js/polynomial-redos` flagged.
Thanks to CodeQL.
2026-05-30 22:16:42 +03:00
Lauri Ojansivu 89be5c1f6e Updated ChangeLog. 2026-05-30 22:12:01 +03:00
Lauri Ojansivu ae671bc70a Fix Polynomial regex DoS in Jade parser.
Thanks to CodeQL.
2026-05-30 22:07:32 +03:00
Lauri Ojansivu 78e34b5522 Updated ChangeLog. 2026-05-30 22:03:49 +03:00
Lauri Ojansivu 42d79b3dcf Updated dependencies. Part 8.
Thanks to developers of dependencies !
2026-05-30 22:02:34 +03:00
Lauri Ojansivu 2d4178a281 Updated ChangeLog. 2026-05-30 21:58:58 +03:00
Lauri Ojansivu ac0e11e548 Updated dependencies. Part 7.
Thanks to developers of dependencies !
2026-05-30 21:56:54 +03:00
Lauri Ojansivu 0543cc42de Updated ChangeLog. 2026-05-30 20:52:24 +03:00
Lauri Ojansivu 06c803388b Updated dependencies. Part 6. Fork and update meteor-node-stubs.
Thanks to developers of dependencies !
2026-05-30 20:48:43 +03:00
Lauri Ojansivu 58b00162d9 Updated ChangeLog. 2026-05-30 20:34:03 +03:00
Lauri Ojansivu 08084d96e8 Updated dependencies. Part 5.
Thanks to developers of dependencies !
2026-05-30 20:32:58 +03:00
Lauri Ojansivu 8b63e84cba Updated ChangeLog. 2026-05-30 20:30:40 +03:00
Lauri Ojansivu a2e76f2cf0 Updated dependencies. Part 4.
Thanks to developers of dependencies !
2026-05-30 20:29:34 +03:00
Lauri Ojansivu 72f22d1235 Updated ChangeLog. 2026-05-30 20:26:47 +03:00
Lauri Ojansivu 76945ee5b5 Updated dependencies. Part 3.
Thanks to developers of dependencies !
2026-05-30 20:25:46 +03:00
Lauri Ojansivu d8b7d6e5f4 Updated ChangeLog. 2026-05-30 20:23:02 +03:00
Lauri Ojansivu 1369316401 Updated dependencies. Part 2.
Thanks to developers of dependencies !
2026-05-30 20:19:37 +03:00
Lauri Ojansivu 29b0cc1f86 Updated dependencies.
Thanks to developers of dependencies !
2026-05-30 20:07:17 +03:00
Lauri Ojansivu 74f422b523 Updated ChangeLog. 2026-05-30 20:00:13 +03:00
Lauri Ojansivu 0fefdeec4c Fix iOS Chrome jQuery event handler runtime error.
Thanks to xet7 !
2026-05-30 19:58:18 +03:00
Lauri Ojansivu b99e7979db Updated ChangeLog. 2026-05-30 13:50:30 +03:00
Lauri Ojansivu 64b43a42dd Fix typos.
Thanks to xet7 !
2026-05-30 13:49:23 +03:00
Lauri Ojansivu 9d35870863 Copy wepica style of hide/show password to WeKan login and register pages.
Thanks to Chostakovitch and xet7 !
2026-05-30 13:43:52 +03:00
Lauri Ojansivu 526580cbb6 Updated translations. 2026-05-30 09:05:04 +03:00
Lauri Ojansivu d3366ca7cd v9.31 2026-05-27 22:51:13 +03:00
Lauri Ojansivu 048a90f876 Updated ChangeLog. 2026-05-27 22:06:37 +03:00
Lauri Ojansivu abc24cf381 Fix typo.
Thanks to xet7 !
2026-05-27 22:05:13 +03:00
Lauri Ojansivu 9d21f19ab8 Updated translations. 2026-05-27 21:57:04 +03:00
Lauri Ojansivu 07a0072b87 Updated translations. 2026-05-27 21:52:56 +03:00
Lauri Ojansivu dbe5705b47 Updated translations. 2026-05-27 21:49:57 +03:00
Lauri Ojansivu 0ba3255382 Updated ChangeLog. 2026-05-27 20:56:32 +03:00
Lauri Ojansivu 2e34b2d004 Updated ChangeLog. 2026-05-27 20:54:15 +03:00
Lauri Ojansivu fc92b342ce Fix GHSA-g6vm-7757-pr88. Moved Attachment Storage options from board to Admin Panel. Changed API to use Expose Meteor user context to Express endpoints.
Thanks to Jan Kahmen of turingpoint GmbH for reporting GHSA-g6vm-7757-pr88 and xet7 for fixes and attachment improvements.

Thanks to nachocodoner about https://forums.meteor.com/t/expose-meteor-user-context-to-express-endpoints/64384.
2026-05-27 20:54:05 +03:00
Lauri Ojansivu 45c4afd22b Add Header Login. Part 2.
Fixes #4311,
fixes #2019
2026-05-27 20:51:44 +03:00
Lauri Ojansivu d9f1ecb834 Add Header Login.
Fixes #4311,
fixes #2019
2026-05-27 20:51:44 +03:00
Lauri Ojansivu a37190e0fc Add more tests. Fix tests.
Thanks to xet7 !
2026-05-27 20:51:44 +03:00
Lauri Ojansivu cd37c6c980 Merge pull request #6362 from harryadel/upgrade-meteor-3.5-beta.12
Upgrade Meteor to 3.5-beta.12
2026-05-27 20:51:34 +03:00
Harry Adel 316567f219 Upgrade Meteor to 3.5-beta.12 2026-05-27 18:51:15 +03:00
Lauri Ojansivu e5c61b32e3 Fix typos.
Thanks to xet7 !
2026-05-27 03:59:32 +03:00
Lauri Ojansivu a41d5ff2c0 Updated ChangeLog. 2026-05-26 23:26:15 +03:00
Lauri Ojansivu 769a139e70 Updated dependencies.
Thanks to developers of dependencies !
2026-05-26 23:25:21 +03:00
Lauri Ojansivu 7729d9f33e Updated ChangeLog. 2026-05-26 17:34:50 +03:00
Lauri Ojansivu 8e13f2bce9 Update dependencies and maintenance scripts.
Thanks to developers of dependencies !
2026-05-26 17:30:29 +03:00
Lauri Ojansivu 4e28ffef9c Merge pull request #6360 from wekan/dependabot/github_actions/docker/build-push-action-7.2.0
Bump docker/build-push-action from 7.1.0 to 7.2.0
2026-05-26 17:20:04 +03:00
Lauri Ojansivu d06a7a2c76 Merge pull request #6359 from wekan/dependabot/github_actions/docker/metadata-action-6.1.0
Bump docker/metadata-action from 6.0.0 to 6.1.0
2026-05-26 17:19:25 +03:00
Lauri Ojansivu 886f3bb840 Merge pull request #6358 from wekan/dependabot/github_actions/docker/login-action-4.2.0
Bump docker/login-action from 4.1.0 to 4.2.0
2026-05-26 17:18:42 +03:00
dependabot[bot] b501d7af33 Bump docker/build-push-action from 7.1.0 to 7.2.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.1.0 to 7.2.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/bcafcacb16a39f128d818304e6c9c0c18556b85f...f9f3042f7e2789586610d6e8b85c8f03e5195baf)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 02:04:21 +00:00
dependabot[bot] c3ea57986f Bump docker/metadata-action from 6.0.0 to 6.1.0
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/030e881283bb7a6894de51c315a6bfe6a94e05cf...80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 02:04:18 +00:00
dependabot[bot] 048947ea7e Bump docker/login-action from 4.1.0 to 4.2.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/4907a6ddec9925e35a0a9e82d7399ccc52663121...650006c6eb7dba73a995cc03b0b2d7f5ca915bee)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 02:04:16 +00:00
Lauri Ojansivu 7834a660a5 Updated translations. 2026-05-25 07:57:20 +03:00
Lauri Ojansivu eee7c49439 Updated ChangeLog. 2026-05-23 00:29:02 +03:00
Lauri Ojansivu dbab502be4 Update Windows docs.
Thanks to xet7 !
2026-05-23 00:27:49 +03:00
Lauri Ojansivu f4d5279a09 Updated ChangeLog. 2026-05-22 22:10:46 +03:00
Lauri Ojansivu c8430a8c14 Updated platforms.
Thanks to xet7 !
2026-05-22 22:09:46 +03:00
Lauri Ojansivu 63efa5d0f0 Updated ChangeLog. 2026-05-22 21:05:35 +03:00
Lauri Ojansivu 8cda80752f Ubuntu 26.04 based core26 beta can be at devel like beta channel, not yet candidate.
Thanks to xet7 !
2026-05-22 21:02:51 +03:00
Lauri Ojansivu abeb2d2477 v9.30 2026-05-22 20:34:06 +03:00
Lauri Ojansivu a7c74e8d8b v9.30 2026-05-22 20:29:27 +03:00
Lauri Ojansivu 4b7ed35300 Updated ChangeLog. 2026-05-22 19:54:03 +03:00
Lauri Ojansivu 6463a1ef87 Release scripts: Use locally cached files.
Thanks to xet7 !
2026-05-22 19:52:23 +03:00
Lauri Ojansivu f85dc3fa76 Updated ChangeLog. 2026-05-22 19:36:48 +03:00
Lauri Ojansivu fb05b1a847 Fix Snap LXD container to work when network already exists.
Thanks to xet7 !
2026-05-22 19:34:35 +03:00
Lauri Ojansivu 5b751217d5 Updated ChangeLog. 2026-05-22 19:32:43 +03:00
Lauri Ojansivu 07e6f663db Try to fix Snap build LXD container network Internet access.
Thanks to xet7 !
2026-05-22 19:30:30 +03:00
Lauri Ojansivu 23ed41bddf v9.29 2026-05-22 19:12:17 +03:00
Lauri Ojansivu 3e33c766d6 Updated dependencies.
Thanks to developers of dependencies !
2026-05-22 18:51:02 +03:00
Lauri Ojansivu 83078736f5 Try to fix Snap.
Thanks to xet7 !
2026-05-22 17:50:13 +03:00
Lauri Ojansivu 7ccb50e4f5 v9.28 2026-05-22 16:37:57 +03:00
Lauri Ojansivu 1032c9d9cf Updated ChangeLog. 2026-05-22 16:34:10 +03:00
Lauri Ojansivu ecbecc9d86 Try to fix Snap by using correct versions of dependencies.
Thanks to xet7 !
2026-05-22 16:32:35 +03:00
Lauri Ojansivu 797426335a Updated ChangeLog. 2026-05-22 16:26:11 +03:00
Lauri Ojansivu e7f2068105 Build scripts: Update arm64 build script.
Thanks to xet7 !
2026-05-22 16:24:41 +03:00
Lauri Ojansivu afc51165f4 v9.27 2026-05-22 16:15:00 +03:00
Lauri Ojansivu 87a6354e42 Docker: Do not include pebble to final Docker container, only use it when building.
Thanks to xet7 !
2026-05-22 15:53:28 +03:00
Lauri Ojansivu edc8c17e35 Updated ChangeLog. 2026-05-22 15:16:03 +03:00
Lauri Ojansivu 2a700f5690 Try to fix Snap.
Thanks to xet7 !
2026-05-22 15:15:00 +03:00
Lauri Ojansivu a6ce798862 Updated ChangeLog. 2026-05-22 14:21:40 +03:00
Lauri Ojansivu 30fd008310 Fix Snap local build.
Thanks to xet7 !
2026-05-22 14:19:33 +03:00
Lauri Ojansivu 3608d4b657 v9.26 2026-05-22 13:54:02 +03:00
Lauri Ojansivu 7c1ee7f161 Updated ChangeLog. 2026-05-22 13:44:46 +03:00
Lauri Ojansivu d978d626ae Use Ubuntu 26.04 base for amd64 and arm64 at Docker and Snap.
Thanks to xet7 !
2026-05-22 13:41:56 +03:00
Lauri Ojansivu 1059164255 Updated ChangeLog. 2026-05-22 13:09:31 +03:00
Lauri Ojansivu 741668a831 Try to fix Snap: Use local npm package cache to prevent local build timeout.
Thanks to xet7 !
2026-05-22 13:07:11 +03:00
Lauri Ojansivu 5f06ebfaec v9.25 2026-05-22 12:18:13 +03:00
Lauri Ojansivu b5e79b37ed Try to fix Snap.
Thanks to xet7 !
2026-05-22 12:11:07 +03:00
Lauri Ojansivu 7076ce5df3 v9.24 2026-05-22 11:35:23 +03:00
Lauri Ojansivu 3e00b2ccc2 Updated ChangeLog. 2026-05-22 11:26:06 +03:00
Lauri Ojansivu 5681592572 Fix Snap pidFilePath.
Thanks to csonkaoszimt and xet7 !

Fixes #6355
2026-05-22 11:22:53 +03:00
Lauri Ojansivu 87dcfdbff6 Updates ip address 2026-05-21 16:24:43 +03:00
Lauri Ojansivu b035bed3d8 v9.23 2026-05-21 15:58:29 +03:00
Lauri Ojansivu 94d032c847 Updated translations. 2026-05-21 15:54:37 +03:00
Lauri Ojansivu 347d30c3e0 Merge pull request #6350 from wekan/dependabot/npm_and_yarn/brace-expansion-5.0.6
Bump brace-expansion from 5.0.5 to 5.0.6
2026-05-21 15:49:17 +03:00
Lauri Ojansivu 720ef251ae Updated ChangeLog. 2026-05-21 15:48:20 +03:00
Lauri Ojansivu 24577ad2a9 Fix tests. Added Filter test. Part 4.
Thanks to xet7 !
2026-05-21 15:46:32 +03:00
Lauri Ojansivu 06cdd459db Fix Filter.
Thanks to hmeunier95 and xet7 !

Fixes #6351
2026-05-21 15:43:08 +03:00
Lauri Ojansivu fad217db8a Added script to revert git add.
Thanks to xet7 !
2026-05-21 10:45:34 +03:00
Lauri Ojansivu 4aeb3aa3d2 Fix No label and dates on any minicard.
Thanks to hmeunier95 and xet7 !

Fixes #6347
2026-05-21 10:08:41 +03:00
Lauri Ojansivu 8baa9124a6 Fix CRITICAL SECURITY ISSUE BFLABleed https://wekan.fi/hall-of-fame/bflableed/
Thanks to Fredrik Dietrichson and xet7 !
2026-05-21 09:33:25 +03:00
Lauri Ojansivu 1b1b04d083 Fix tests.
Thanks to xet7 !
2026-05-21 09:24:05 +03:00
dependabot[bot] 982f0b1cf5 Bump brace-expansion from 5.0.5 to 5.0.6
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.5 to 5.0.6.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.5...v5.0.6)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 17:34:27 +00:00
Lauri Ojansivu b5f3cbc38e Updated ChangeLog. 2026-05-18 05:09:25 +03:00
Lauri Ojansivu 28464685a7 Fix WeKan version update script.
Thanks to xet7 !
2026-05-18 05:08:04 +03:00
Lauri Ojansivu 3a6aa7dccd Updated ChangeLog. 2026-05-18 04:55:18 +03:00
Lauri Ojansivu 2cc30a85f2 Add local build cache, for npm packages and http downloads.
Thanks to xet7 !
2026-05-18 04:52:45 +03:00
Lauri Ojansivu c5ac0cf8ce v9.22 2026-05-18 04:10:17 +03:00
Lauri Ojansivu 95ad27e0df Fix minicard drag handle to be below minicard menu. Not at top of minicard menu anymore.
Thanks to xet7 !
2026-05-18 04:02:08 +03:00
Lauri Ojansivu b0e2674b11 Updated ChangeLog. 2026-05-18 03:44:07 +03:00
Lauri Ojansivu 46e67512ac Backup also attachments and avatars.
Thanks to xet7 !
2026-05-18 03:42:40 +03:00
Lauri Ojansivu 994ecf8ec0 Updated ChangeLog. 2026-05-18 03:24:06 +03:00
Lauri Ojansivu d486eb2a8f Added backup-all.sh script.
Thanks to xet7 !
2026-05-18 03:20:42 +03:00
Lauri Ojansivu 8f574799e2 Fixed path 2026-05-18 03:08:45 +03:00
Lauri Ojansivu 7496d994e1 Script to executeable. 2026-05-18 03:02:57 +03:00
Lauri Ojansivu ec728dabf6 Updated ChangeLog. 2026-05-18 02:46:57 +03:00
Lauri Ojansivu 2474ff15d6 Docs: Added link to Meteor Security.
Thanks to xet7 !
2026-05-18 02:44:40 +03:00
Lauri Ojansivu 53c205b4ce Updated ChangeLog. 2026-05-18 02:40:38 +03:00
Lauri Ojansivu 7b88f4c21a Use only oplog sockjs so that multitenancy works.
Thanks to xet7 !
2026-05-18 02:38:20 +03:00
Lauri Ojansivu 20368533a0 Added nocache header to Caddy, trying to fix showing correct page.
Thanks to xet7 !
2026-05-17 22:07:07 +03:00
Lauri Ojansivu ac7712f141 Updated ChangeLog. 2026-05-17 21:05:58 +03:00
Lauri Ojansivu 0b9d33cac4 Script for upgrading Docker WeKan from old version to new version.
Thanks to xet7 !
2026-05-17 21:04:08 +03:00
Lauri Ojansivu d4aca1c009 v9.21 2026-05-17 19:48:16 +03:00
Lauri Ojansivu a5ca6b7a79 Updated ChangeLog. 2026-05-17 19:43:01 +03:00
Lauri Ojansivu 964caabaaf Set default to changeStreams and uws.
Thanks to xet7 !
2026-05-17 19:41:55 +03:00
Lauri Ojansivu 9455646d66 Updated ChangeLog. 2026-05-17 19:35:53 +03:00
Lauri Ojansivu c2f67001d6 Fix sudden logouts.
Thanks to xet7 !
2026-05-17 19:33:54 +03:00
Lauri Ojansivu b57362c5e8 Updated ChangeLog. 2026-05-17 19:20:08 +03:00
Lauri Ojansivu d5d04728fb Added Docs for Meteor 3 Docker WeKan with ChangeStreams etc.
Thanks to xet7 !
2026-05-17 19:16:58 +03:00
Lauri Ojansivu b99e1ccc05 Updated ChangeLog. 2026-05-17 02:38:53 +03:00
Lauri Ojansivu 3390324c2a Updated Kin 2.0 Meta OS to Friend page.
Thanks to xet7 !
2026-05-17 02:34:55 +03:00
Lauri Ojansivu 5748475e8d Updated version scripts.
Thanks to xet7 !
2026-05-16 15:05:54 +03:00
Lauri Ojansivu accb51cc67 v9.20 2026-05-16 14:21:25 +03:00
Lauri Ojansivu f81fe11dae Added remaining MongoDB indexes and MongoDB settings.
Thanks to xet7 !
2026-05-16 14:16:37 +03:00
Lauri Ojansivu 5309e4863d Use oplog and sockjs as default. Not changeStreams and uws.
Thanks to xet7 !
2026-05-16 14:01:27 +03:00
Lauri Ojansivu f26a2ac9ad Added fallback so attachments and avatars can be at WRITABLE_PATH or WRITABLE_PATH/files.
Thanks to xet7 !
2026-05-16 13:46:56 +03:00
Lauri Ojansivu c37047d8ee Update build scripts.
Thanks to xet7 !
2026-05-16 13:45:47 +03:00
Lauri Ojansivu d5ca31a38b v9.19 2026-05-16 07:14:57 +03:00
Lauri Ojansivu 0958182306 Updated ChangeLog. 2026-05-16 07:06:46 +03:00
Lauri Ojansivu 9f979054de After register or login, redirect to All Boards page.
Thanks to bcook-konza and xet7 !

Fixes #6328
2026-05-16 07:04:30 +03:00
Lauri Ojansivu 86e671ea49 Updated ChangeLog. 2026-05-16 06:55:03 +03:00
Lauri Ojansivu 47794f799b Removed empty line from star board description at top bar of board, and added some space between icons and text.
Thanks to xet7 !
2026-05-16 06:52:42 +03:00
Lauri Ojansivu b6200c14fc Updated translations. 2026-05-16 06:30:10 +03:00
Lauri Ojansivu 53b0224cce Deleted old file. 2026-05-16 06:22:40 +03:00
Lauri Ojansivu f02e02b401 Updated ChangeLog. 2026-05-14 05:10:21 +03:00
Lauri Ojansivu 52ce4f6128 Fix Esc not work in an opened card when Keyboard Shortcuts enabled.
Thanks to mimZD and xet7 !

This is temporary fix, using click.
If sometime later browser protections prevents this
(like happened previously with click code),
fix this properly by extracting js-close-card-details() to function
and change to run that function directly, without using click.
2026-05-14 05:04:26 +03:00
Lauri Ojansivu a12e075780 Updated ChangeLog. 2026-05-14 04:41:40 +03:00
Lauri Ojansivu 597eb6b2bb Updated @wekanteam/html-to-markdown with Fix Some HTML tags should not get newlines when converted to markdown.
Fixes #6154

Add space after hashes, so that headings views correctly:
https://github.com/wekan/html-to-markdown/commit/3b254e1fa32b6067dd22789fbaab8d4742349bd6
2026-05-14 04:37:39 +03:00
Lauri Ojansivu ba953b57e5 Updated ChangeLog. 2026-05-14 04:17:41 +03:00
Lauri Ojansivu 3adf69bd4c Fix Change list inside of card should show only lists of current swimlane.
Thanks to mimZD and xet7 !

Fixes #6161

The two changes made:

- **[cardDetails.js](client/components/cards/cardDetails.js)**:
Added `currentSwimlaneListsSorted` helper that queries lists
filtered by the card's `swimlaneId`
(mirroring the logic in `dialogWithBoardSwimlaneList.js`),
sorted by `sort` field (left-to-right column order → top-to-bottom in dropdown).

- **[cardDetails.jade](client/components/cards/cardDetails.jade:300)**:
Changed `each currentBoard.lists` to `each currentSwimlaneListsSorted`
so the dropdown only shows lists belonging to the card's current swimlane.
The default swimlane edge case is handled: if the card is in the default swimlane, lists with `swimlaneId` of `null`, `''`, or the default swimlane ID are all included.
2026-05-14 04:14:04 +03:00
Lauri Ojansivu cb1c7da78c Updated ChangeLog. 2026-05-14 04:10:59 +03:00
Lauri Ojansivu 64bc774b1e Fix Labels are disappearing when you are filtering cards by label.
Thanks to hmeunier95 and xet7 !

Fixes #6265

**Fix 1 — [`models/cards.js:797`](models/cards.js)**
`labels()` called `this.board().labels` without a null guard.
If `board()` ever returns `undefined` (board not yet in minimongo,
or a card with a bad `boardId`), this throws silently in Blaze
and all label chips disappear. Now returns `[]` safely.

**Fix 2 — [`client/components/swimlanes/swimlanes.js:660`](client/components/swimlanes/swimlanes.js)**
In swimlane view, `Template.swimlane`'s `visible()` helper
called `list.cards()` **without** the current swimlane ID.
This meant a list in swimlane S2 was considered "non-empty"
(and therefore kept visible) if swimlane S1 had
a card matching the label filter — even though S2 itself
had zero matching cards. The list would appear in S2
but be completely empty, explaining
"some lists are empty even when they contain cards with the label."
Now passes `this._id` (the current swimlane) so
only that swimlane's cards are counted.
2026-05-14 04:06:20 +03:00
Lauri Ojansivu 172f3bb31b Updated ChangeLog. 2026-05-14 03:52:00 +03:00
Lauri Ojansivu 8183a645a1 Fix Remaining view: Drag board starts drag preview but drop is no-op.
Thanks to YosserDerbali and xet7 !

Fixes #6258

**Root cause:** When dragging a board in the Remaining view,
there were no visual cues indicating where to drop — users tried
dropping on other boards (which does nothing), not knowing
they need to drop on a workspace in the left sidebar.

**Changes made:**

1. **[boardsList.js](client/components/boards/boardsList.js)** —
   `dragstart .js-board`: On drag start, adds `.board-drag-hint`
   class to all workspace nodes and the Remaining menu item
   in the sidebar, visually signalling valid drop targets.
   New `dragend .js-board` cleans up the class.

2. **[boardsList.js](client/components/boards/boardsList.js)** —
   `boardWorkspaceDragHint()`: Updated tooltip
   from "Drag board: Remaining <-> Workspaces"
   to "Drag board to assign to Workspaces
   (drop on workspace in left sidebar)" —
   removes the ambiguous bidirectional arrow that implied reordering.

3. **[boardsList.css](client/components/boards/boardsList.css)**:
   Added green dashed border highlight for
   `.workspace-node.board-drag-hint` and
   `.js-select-menu.board-drag-hint` so valid drop targets
   light up during a board drag.

4. **[en.i18n.json](imports/i18n/data/en.i18n.json)**:
   Added `drag-board-to-workspace` key and
   updated `board-open-and-move-between-remaining-and-workspaces`
   to reflect the correct purpose (workspace assignment, not reordering).

This resolves the UX mismatch without implementing board reorder
(which carries regression risk).
The drag handle stays visible since it's genuinely functional
for workspace assignment — users just now get
a clear visual guide showing where to drop.
2026-05-14 03:46:06 +03:00
Lauri Ojansivu f69e543376 Updated ChangeLog. 2026-05-14 03:42:05 +03:00
Lauri Ojansivu fbce6e5e9b Fix Card pop-up menu content overflows viewport.
Thanks to aminsaidane and xet7 !

Fixes #6219

**Root cause:** [popup.css](client/components/main/popup.css)
had duplicate and conflicting rules at the bottom of the file.
Two rules were overriding the correct `cardDetailsActionsPopup`-specific fix:

1. `.pop-over:has(.pop-over-list) { max-height: 400px !important }` — had
   the same CSS specificity as `.pop-over[data-popup="cardDetailsActionsPopup"]`
   but appeared later, so it won, forcing the popup to 400px instead of 80vh.
2. `.pop-over .pop-over-list { max-height: 300px !important }` — forced each
   individual list element to independently scroll at 300px, which is wrong
   (the container should scroll, not each list).
3. There was an entire duplicate of the same block, adding the rules twice.

**What was removed:** The "STRONGER FIX" section and its duplicate (~55 lines),
which contained the conflicting `max-height: 400px` and `max-height: 300px` rules.

**What remains (and works correctly):**
- The `cardDetailsActionsPopup`-specific rules at lines 343–361
  (max-height: 80vh, scrollable content-wrapper)
- The general flex-column layout at the end that constrains all popups to
  `max-height: 90vh` with a fixed header and scrollable content area
- The `listActionsPopup`-specific rules
- The JS `_getOffset` in [popup.js](client/lib/popup.js:267) which
  already correctly calculates available space above/below the opener
  and passes `maxHeight` to the template
2026-05-14 03:37:08 +03:00
Lauri Ojansivu 5efb90640c Updated ChangeLog. 2026-05-14 03:34:42 +03:00
Lauri Ojansivu 93f90873e8 Fix Cards disappear from lists that has been minimized.
Thanks to robertjensen, harryadel and xet7 !

Fixes #6266,
fixes https://github.com/wekan/wekan/pull/6278

**Bug:** Cards disappear from lists that were previously minimized.

**Root cause** — [`client/components/lists/listBody.jade:2`](client/components/lists/listBody.jade):

```jade
template(name="listBody")
  unless collapsed    ← BUG: reads raw list.collapsed from MongoDB
```

`Template.listBody` has no `collapsed` helper defined in `listBody.js`.
In Blaze, when a helper isn't found, it falls through to
the raw data context — the list document from MongoDB.
So it reads `list.collapsed` directly from the database.

The parent template `list.jade` wraps `+listBody` with
its own `unless collapsed` that correctly uses
`Utils.getListCollapseState()`, which reads from the user's profile.
These two checks were reading from **different sources**.

**Failure sequence:**
1. Old WeKan versions (or a DB migration) stored `collapsed: true`
   directly on the list document in MongoDB
2. User clicks the expand arrow → `Utils.setListCollapseState(list, false)
   updates `profile.collapsedLists` only — it does **not** clear `list.collapsed`
   in MongoDB
3. `list.jade`'s helper sees `false` (from profile) → renders `+listBody`
4. `listBody.jade`'s `unless collapsed` reads the raw
   MongoDB field → still `true` → hides all cards, the add-card compose
   and the `.js-minicards` container (breaking drag-and-drop too)

**Fix:** Remove the redundant `unless collapsed` from `listBody.jade`.
The parent template already guards it with the correct
helper — if `listBody` is being rendered at all, the list is expanded.
The inner check was both redundant and broken.
2026-05-14 03:27:51 +03:00
Lauri Ojansivu 8b8a0c8458 Updated ChangeLog. 2026-05-14 03:10:06 +03:00
Lauri Ojansivu fc37c08687 Fix Not possible to change the tracking mode.
Thanks to hmeunier95 and xet7 !

Fixes #6267

In [boardHeader.js:429](client/components/boards/boardHeader.js),
the event handler uses `Template.currentData()` which returns
the popup template's data context — not the string from
the inner `with "watching"` / `with "tracking"` / `with "muted"` blocks.
It should use `this` instead, since `this` in a Blaze event handler
refers to the data context of the element that was clicked.
2026-05-14 03:06:34 +03:00
Lauri Ojansivu 933bd97393 Updated ChangeLog. 2026-05-14 02:57:27 +03:00
Lauri Ojansivu ed5304cd30 Fix Email notifications show "undefined" instead of username.
Thanks to apiccolim, Raawen-dev and xet7 !

Fixes #6280

**Fix 1 — [models/cards.js:2170](models/cards.js)**: Added `await` to `Cards.updateAsync()` in the `move()` method.

Without `await`, the card update was dispatched as a fire-and-forget Promise. In Meteor 3's AsyncLocalStorage-based DDP context, this can cause the collection hooks (`after.update`) to fire outside the original DDP method context, making `Meteor.userId()` return `null`. With `await`, the async chain is intact and the DDP userId context is preserved through to the hooks.

Also tightened the `UserPositionHistory` guard to `typeof Meteor.userId === 'function'` — the error log showed `TypeError: o.Meteor.userId is not a function` in certain bundle contexts, and this guard prevents that crash.

**Fix 2 — [server/rulesHelper.js](server/rulesHelper.js)**: Added `withUserId()` helper that sets `DDP._CurrentMethodInvocation` before calling `card.move()` from rule execution.

When automation rules trigger a card move, the code runs in a server-side callback with no active DDP method invocation. `CollectionHooks.getUserId()` → `Meteor.userId()` returns `null`, so the resulting activity gets `userId: null`, and the email shows `undefined`. Wrapping `card.move()` in `DDP._CurrentMethodInvocation.withValue({ userId: activity.userId }, ...)` restores the user context so hooks pick up the correct userId.

**Fix 3 — [server/notifications/email.js:37](server/notifications/email.js)**: Defensive fallback `const actorName = params.user || ''` instead of interpolating `params.user` directly.

If `params.user` is ever `undefined` (e.g., activity inserted without a userId from a migration or cron job), the template previously produced the literal string `"undefined moved card X to list Y"`. The fallback produces a blank actor name instead, which is less confusing and avoids the regression.
2026-05-14 02:53:31 +03:00
Lauri Ojansivu e6a3ce6772 Updated ChangeLog. 2026-05-14 02:36:11 +03:00
Lauri Ojansivu acc49aeccb Fix Broken card report.
Thanks to titver968 and xet7 !

Fixes #6293

This fix matches the pattern already used in [server/publications/boards.js:329-330](server/publications/boards.js:329-330).

**Root cause:** `ReactiveCache.getAttachments(..., true)` can return either:
- A FilesCollection cursor (`FileCursor`) which has a `.cursor` property holding the real Mongo cursor, OR
- A plain Mongo cursor with no `.cursor` property

The old code always accessed `.cursor`, which would be `undefined` when the result is already a plain cursor. `undefined` in the cursors array causes Meteor DDP to throw "Publish function returned an array of non-Cursors".

**Fix:** Extract the result first, then use `attachmentsResult.cursor || attachmentsResult` — exactly like `boards.js` already does — so it works regardless of which type is returned.
2026-05-14 02:32:09 +03:00
Lauri Ojansivu f19938b6bb Updated ChangeLog. 2026-05-14 02:10:07 +03:00
Lauri Ojansivu 997931e462 Fix Show Checklist on Minicard and make it editable at Minicard. Fix spurious minicard cover element.
Thanks to brlin-tw and xet7 !

Fixes #6301,
fixes #6315
2026-05-14 02:06:18 +03:00
Lauri Ojansivu e837c0948a Fix Boards Place Sorting.
Thanks to titver968 and xet7 !

Fixes #6294

**Bug:** When the jQuery sortable board-reordering code was replaced with HTML5 drag-and-drop (for workspace management), the board-to-board sorting was accidentally dropped. The `dragstart .js-board` handler only set `dataTransfer` data for workspace assignment — there were no `dragover`/`drop` handlers on `.js-board` elements to actually reorder boards.

**Fix in [boardsList.js](client/components/boards/boardsList.js):** Added four new event handlers after `dragstart .js-board`:

- `dragover .js-board` — allows drop (calls `preventDefault`) and shows a blue top/bottom border indicator based on cursor position relative to the target board's midpoint. Skips workspace drags by checking `dataTransfer.types`.
- `dragleave .js-board` — removes the visual indicator.
- `dragend .js-board` — cleans up all drag indicators on all boards.
- `drop .js-board` — determines drop position (before/after target), walks siblings to find the adjacent `.js-board` DOM elements, calls `Utils.calculateIndex()` to get a sort value, then saves it via `currentUser.setBoardSortIndex()` — matching exactly what the old jQuery sortable did.

**Fix in [boardsList.css](client/components/boards/boardsList.css):** Added `board-drag-over-top` and `board-drag-over-bottom` CSS classes with a 3px blue border to give visual feedback while dragging.
2026-05-14 01:29:59 +03:00
Lauri Ojansivu 361099b2cc Updated ChangeLog. 2026-05-13 23:35:27 +03:00
Lauri Ojansivu 5105cf6489 Fix Search cards should return more results.
Thanks to snowsky and xet7 !

Fixes #6332

Summary of 3 fixes:

**`server/publications/cards.js`** — three publications were returning the raw `{ cursors, sessionData }` object from `findCards()` instead of publishing correctly:
- `brokenCards` (line 907–909): now destructures and pushes `sessionData` via `this.added()`, returns `cursors`
- `nextPage` (line 919–921): same fix
- `previousPage` (line 931–933): same fix

**`client/lib/cardSearch.js`** — `getResults()` had two bugs:
- A `directCards` undefined variable reference (would throw a `ReferenceError` at runtime)
- A fallback when `sessionData` is null that queried `Cards` filtered by `dueAt` — completely unrelated to search, and likely the direct cause of "1 card shown" (whichever card had a due date in MiniMongo)

The replacement uses `Cards.find({ _id: { $in: this.sessionData.cards } })` to fetch all matching cards in a single query, which is both correct and avoids the per-ID `ReactiveCache` staleness issues. When `sessionData` is null (subscription not ready yet), it now returns an empty array rather than random due-date cards.
2026-05-13 23:32:21 +03:00
Lauri Ojansivu 01043096f3 Updated ChangeLog. 2026-05-13 23:28:26 +03:00
Lauri Ojansivu af86ea8785 Fix Sub-URL at ROOT_URL does not work.
Thanks to Meridiana and xet7 !

Fixes #6322

**[`client/00-startup.js`](client/00-startup.js)**

1. **Build chunks / language popup** — Sets `__webpack_public_path__` at module evaluation time (before any `import()` expressions run, which only happen inside `Meteor.startup` callbacks). Reads `window.__meteor_runtime_config__.ROOT_URL`, extracts the pathname (e.g. `/wekan`), and sets the prefix so rspack loads chunks from `/wekan/build-chunks/...` instead of `/build-chunks/...`.

2. **Service worker** — Replaces the hardcoded `/pwa-service-worker.js` path with `new URL('pwa-service-worker.js', Meteor.absoluteUrl()).pathname`, which automatically includes the context path.

**[`models/lib/universalUrlGenerator.js`](models/lib/universalUrlGenerator.js)**

3. **Attachment download links** — Added `getRootUrlPath()` which reads the pathname from ROOT_URL on the client (returns `''` on the server, where `WebApp.handlers` already strips the prefix before routing). `generateUniversalFileUrl` and `generateFallbackUrl` now prepend this prefix, so generated URLs are `/wekan/cdn/storage/attachments/...` instead of `/cdn/storage/attachments/...`.

**[`server/routes/universalFileServer.js`](server/routes/universalFileServer.js)**

4. **Redirect Location headers** — The `/attachments/` and `/avatars/` fallback routes issued relative-path redirects (`/cdn/storage/...`) that ignored the context path. They now use `Meteor.absoluteUrl(...)` which produces a correct absolute URL including the path prefix.
2026-05-13 23:25:19 +03:00
Lauri Ojansivu b32a14fbd5 Updated ChangeLog. 2026-05-13 23:19:15 +03:00
Lauri Ojansivu 14de1ac072 Fix Add Board / Import is not visible.
Thanks to titver968 and xet7 !

Fixes #6325
2026-05-13 23:18:45 +03:00
Lauri Ojansivu ddc3335854 Updated ChangeLog. 2026-05-13 23:10:12 +03:00
Lauri Ojansivu c01743b53a Fix Template "Board not Found" after being archieved, by always showing Template boards.
Thanks to WassimDhib and xet7 !

Fixes #4632
2026-05-13 23:07:45 +03:00
Lauri Ojansivu 508d7a80ef Updated ChangeLog. 2026-05-13 23:03:24 +03:00
Lauri Ojansivu 49ee49d2b8 Fix no due date notification email.
Thanks to JaumeFigueras and xet7 !

Fixes #6338

**Three bugs fixed:**

**1. `NOTIFY_DUE_DAYS_BEFORE_AND_AFTER=0` was silently ignored** ([cards.js:2842](models/cards.js))
- Old: `!(iValue > 0 && iValue < 15)` — `0 > 0` is false, so `0` was always rejected
- Also: `.filter(Boolean)` would strip the number `0` even if the condition was fixed (`Boolean(0) === false`)
- Fix: changed condition to `isNaN(iValue) || iValue < -14 || iValue > 14`, and filter to `.filter(v => v !== false)`

**2. `day=1` produced an empty date range** ([cards.js:2825](models/cards.js))
- Old: `args = [then(1), then(day), ...]` → for `day=1`, both bounds were the same timestamp → `$gte: tomorrow, $lt: tomorrow` → zero results
- Fix: `args = [then(1), then(day + 1), ...]` so `day=1` correctly queries `[tomorrow, day_after_tomorrow)`

**3. Crash on cards with deleted/missing users** ([cards.js:2799](models/cards.js))
- Old: `user.username` was accessed without checking if `user` was null — a single bad card would crash the entire nightly scan silently
- Fix: null-guard with a `continue` and a warning log

**Practical impact for the reporter:**
- `NOTIFY_DUE_DAYS_BEFORE_AND_AFTER=0` now works (today's due cards)
- `NOTIFY_DUE_DAYS_BEFORE_AND_AFTER=1` now works (cards due tomorrow)
- `NOTIFY_DUE_DAYS_BEFORE_AND_AFTER=2,0` now works for both values
- Negative values (e.g. `-1`) now also work for past-due notifications
2026-05-13 22:59:03 +03:00
Lauri Ojansivu 89999ab10f Updated ChangeLog. 2026-05-13 22:53:30 +03:00
Lauri Ojansivu 39effe9a0b Fix Clicking on a card does not open the card detail panel.
Thanks to gaetanquentin !

Fixes #6331
2026-05-13 22:50:21 +03:00
Lauri Ojansivu ad3a38e900 Updated ChangeLog. 2026-05-13 22:33:24 +03:00
Lauri Ojansivu b4742b84ab Try fix tests. 2026-05-13 22:32:38 +03:00
Lauri Ojansivu 9ddad24e2a Updated ChangeLog. 2026-05-13 22:19:49 +03:00
Lauri Ojansivu 9f3c216576 Fix Board visibility ist only private and cannot change it. Fix Public Board, Archive, and menus visibility.
Thanks to titver968 and xet7 !

Fixes #6333
2026-05-13 22:16:45 +03:00
Lauri Ojansivu 664c285cbb Updated ChangeLog. 2026-05-13 19:43:01 +03:00
Lauri Ojansivu 7fb2c3de7b Fix editing comments.
Thanks to dehnamaki and xet7 !

Fixes #6336
2026-05-13 19:41:21 +03:00
Lauri Ojansivu 24f4bfd5ad Try to fix tests. 2026-05-13 19:40:22 +03:00
Lauri Ojansivu 45595d95ab Updated ChangeLog. 2026-05-13 19:35:04 +03:00
Lauri Ojansivu 568ea35ed7 Try to fix tests. 2026-05-13 19:34:19 +03:00
Lauri Ojansivu 6243550c84 Updated ChangeLog. 2026-05-13 19:24:57 +03:00
Lauri Ojansivu 6650618682 Try to fix tests. 2026-05-13 19:23:06 +03:00
Lauri Ojansivu b26d9fa163 Updated ChangeLog. 2026-05-13 18:58:02 +03:00
Lauri Ojansivu 0c6acbfaa2 Try to fix Playwright tests at GitHub Actions. 2026-05-13 18:56:36 +03:00
Lauri Ojansivu af7323a0ab Updated ChangeLog. 2026-05-13 18:53:16 +03:00
Lauri Ojansivu 1950aff865 Fix LDAP Admin Sync.
Thanks to titver968 and xet7 !

Fixes #6343
2026-05-13 18:51:27 +03:00
Lauri Ojansivu 9013ffc5ad Updated ChangeLog. 2026-05-13 18:46:55 +03:00
Lauri Ojansivu 6d56d69ae3 Try to fix tests at GitHub Actions.
Thanks to xet7 !

Related #6334
2026-05-13 18:45:21 +03:00
Lauri Ojansivu 80f4b37337 Updated ChangeLog. 2026-05-13 18:29:22 +03:00
Lauri Ojansivu 0e287c9117 Try to fix Playwright tests at GitHub Actions.
Thanks to xet7 !

Related #6334
2026-05-13 18:27:35 +03:00
Lauri Ojansivu a3ed18294f Updated ChangeLog. 2026-05-13 18:17:44 +03:00
Lauri Ojansivu 8e4404ab49 Updated tests dependencies. 2026-05-13 18:16:38 +03:00
Lauri Ojansivu 8b6c459453 Revert not working PR 6337 Fix attachments API 500 error and upload crash.
Thanks to xet7 !
2026-05-13 18:14:42 +03:00
Lauri Ojansivu 8d6d409cc2 Updated ChangeLog. 2026-05-13 18:04:29 +03:00
Lauri Ojansivu 9962f2bd9b Fix Playwright tests.
Thanks to snowsky and xet7 !

Fixes #6334
2026-05-13 18:01:25 +03:00
Lauri Ojansivu 6c816e573d Updated ChangeLog. 2026-05-13 17:08:41 +03:00
Lauri Ojansivu 7f1dd16dfc rebuild-wekan.sh: At macOS, add paths to node@24.
Thanks to xet7 !
2026-05-13 17:06:35 +03:00
Lauri Ojansivu 09a5737e21 Updated dependencies. Part 2. 2026-05-13 17:05:14 +03:00
Lauri Ojansivu 7d94e575b7 Updated ChangeLog. 2026-05-13 16:58:53 +03:00
Lauri Ojansivu bfd554e65c Merge pull request #6344 from wekan/copilot/update-playwright-dependency-setup
Isolate Playwright from root node_modules to fix E2E Rspack bundling errors
2026-05-13 16:57:37 +03:00
Lauri Ojansivu a692f270da Merge pull request #6337 from KhaoulaMaleh/fix/error
Fix attachments API 500 error and upload crash
2026-05-13 16:53:52 +03:00
copilot-swe-agent[bot]andxet7 6999faced9 Isolate Playwright deps from root app to fix E2E Rspack errors
Agent-Logs-Url: https://github.com/wekan/wekan/sessions/16011cb0-2e0b-4045-9969-074a1dc40cbe

Co-authored-by: xet7 <15545+xet7@users.noreply.github.com>
2026-05-13 13:52:47 +00:00
copilot-swe-agent[bot] ea70df6805 Initial plan 2026-05-13 13:49:59 +00:00
Lauri Ojansivu 736fa07dca Revert removing GitHub Workflows, added them back, bugs were unrelated updates that are now done.
Thanks to xet7 !
2026-05-13 16:36:58 +03:00
Lauri Ojansivu 83a683435a Updates 2026-05-13 16:35:06 +03:00
Lauri Ojansivu 7efba64c39 Updated dependencies.
Thanks to developers of dependencies !
2026-05-13 16:31:18 +03:00
Lauri Ojansivu 79521baad8 Updated ChangeLog. 2026-05-13 15:48:16 +03:00
Lauri Ojansivu c1f0da1217 Fix Docker Compose missing dashes.
Thanks to C0rn3j, kichkasch and xet7 !

Fixes #6327
2026-05-13 15:38:11 +03:00
Lauri Ojansivu 8da4a2ad04 Deleted buggy GitHub Workflows.
Thanks to xet7 !
2026-05-12 16:27:22 +03:00
Lauri Ojansivu 6f8a502cf2 Merge pull request #6342 from wekan/dependabot/github_actions/actions/checkout-6 2026-05-12 09:32:24 +03:00
Lauri Ojansivu 98fc5d6ffc Merge pull request #6341 from wekan/dependabot/github_actions/actions/upload-artifact-7 2026-05-12 09:32:04 +03:00
Lauri Ojansivu 8a8469ac2a Merge pull request #6340 from wekan/dependabot/github_actions/actions/setup-node-6 2026-05-12 09:31:47 +03:00
Lauri Ojansivu 059fc979dc Merge pull request #6339 from wekan/dependabot/github_actions/actions/dependency-review-action-5 2026-05-12 09:31:25 +03:00
dependabot[bot] 21f2e2e6ef Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 03:02:18 +00:00
dependabot[bot] 82e5ce1d76 Bump actions/upload-artifact from 4 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 03:02:12 +00:00
dependabot[bot] b92292fe8f Bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 03:02:07 +00:00
dependabot[bot] 6b7cb473aa Bump actions/dependency-review-action from 4 to 5
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4 to 5.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](https://github.com/actions/dependency-review-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 03:02:02 +00:00
KhaoulaMaleh ac2bacf42c attachement fix 2026-05-10 01:41:03 -11:00
Lauri Ojansivu 21c62c6b82 v9.18 2026-05-10 05:00:45 +03:00
Lauri Ojansivu 1a998f9ece Try to fix Parallel Snap paths.
Thanks to xet7 !
2026-05-10 04:54:05 +03:00
Lauri Ojansivu fd10c4c9ad Updated ChangeLog. 2026-05-10 01:28:25 +03:00
Lauri Ojansivu 8d73d2f575 Release Scripts: Include updating version at same script.
Thanks to xet7 !
2026-05-10 01:25:38 +03:00
Lauri Ojansivu 22b9930168 Build scripts: Not change firewall settings anymore. 2026-05-10 01:15:43 +03:00
Lauri Ojansivu 7337854bb7 v9.17 2026-05-10 00:39:14 +03:00
Lauri Ojansivu f6303840c5 Updated ChangeLog. 2026-05-10 00:27:47 +03:00
Lauri Ojansivu 31bcf947cb Try to fix Parallel Snap MongoDB ports.
Thanks to xet7 !
2026-05-10 00:25:02 +03:00
Lauri Ojansivu 0f8ff1f21c Updates 2026-05-09 23:02:26 +03:00
Lauri Ojansivu 61554fa53a v9.16 2026-05-09 22:52:25 +03:00
Lauri Ojansivu da4b40115d Updated ChangeLog. 2026-05-09 22:42:17 +03:00
Lauri Ojansivu c7f9c95596 Fix Parallel Snap MongoDB port and paths, that worked at WeKan 9.07 but later broke.
Thanks to xet7 !
2026-05-09 22:38:53 +03:00
Lauri Ojansivu 5c31adb314 Updated ChangeLog. 2026-05-09 22:06:23 +03:00
Lauri Ojansivu cea6aee038 Fixed uploading attachment to card.
Thanks to xet7 !
2026-05-09 22:05:01 +03:00
Lauri Ojansivu cb32b11586 Temporary fix to https://github.com/jamauro/offline/issues/27, waiting for new jam:offline version.
Thanks to xet7 !
2026-05-09 21:58:37 +03:00
Lauri Ojansivu d0de79cba2 Updated ChangeLog. 2026-05-09 20:56:26 +03:00
Lauri Ojansivu c52f2481d3 Fix login and register to not require page reload by adding router fallback.
Thanks to xet7 !

Fixes #6328
2026-05-09 20:52:33 +03:00
Lauri Ojansivu 690f49e60c Updated ChangeLog. 2026-05-09 13:49:45 +03:00
Lauri Ojansivu fa78267068 Merge pull request #6335 from wekan/dependabot/npm_and_yarn/fast-uri-3.1.2
Bump fast-uri from 3.1.1 to 3.1.2
2026-05-09 13:46:41 +03:00
dependabot[bot] e0391f7dd4 Bump fast-uri from 3.1.1 to 3.1.2
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.1 to 3.1.2.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.1...v3.1.2)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-09 05:42:13 +00:00
Lauri Ojansivu 0f6883bdf3 Updates 2026-05-08 04:40:19 +03:00
Lauri Ojansivu b5a406f8eb v9.15 2026-05-08 00:17:53 +03:00
Lauri Ojansivu 80ffbcd562 Updated ChangeLog. 2026-05-08 00:15:25 +03:00
Lauri Ojansivu 8715133868 Update release script to force include required file.
Thanks to xet7 !
2026-05-08 00:13:01 +03:00
Lauri Ojansivu 75cecab73f Updated ChangeLog. 2026-05-08 00:09:15 +03:00
Lauri Ojansivu f778707236 Fix to run code only if there is userId and user.
Thanks to xet7 !
2026-05-08 00:07:00 +03:00
Lauri Ojansivu b6b9d5b787 Updates 2026-05-07 22:06:27 +03:00
Lauri Ojansivu d19506d32f v9.14 2026-05-07 21:59:31 +03:00
Lauri Ojansivu 8349167a32 Snap Candidate: Reinitialize MongoDB ReplicaSet if it needs reconfig. When restoring, don't restore indexes.
Thanks to xet7 !
2026-05-07 21:54:17 +03:00
Lauri Ojansivu cf2bae0bfb Updates 2026-05-07 20:06:09 +03:00
Lauri Ojansivu eb75d60df1 v9.12 2026-05-07 20:00:39 +03:00
Lauri Ojansivu 57f1f9da5b Fix Snap Candidate: Wait for MongoDB to start before starting Node.js.
Thanks to xet7 !
2026-05-07 19:56:36 +03:00
Lauri Ojansivu c8c7230331 Updates 2026-05-07 19:56:06 +03:00
Lauri Ojansivu 7e193ef603 v9.11 2026-05-07 18:36:29 +03:00
Lauri Ojansivu 5b1cd3dc2f Fix Snap common directory path for Parallel Snap.
Thanks to xet7 !
2026-05-07 18:33:40 +03:00
Lauri Ojansivu b895a66e24 v9.10 2026-05-07 17:59:58 +03:00
Lauri Ojansivu 25dd4be3f8 v9.10 2026-05-07 17:56:53 +03:00
Lauri Ojansivu 6d70826b40 Updated ChangeLog. 2026-05-07 17:55:20 +03:00
Lauri Ojansivu 2dbe335b30 Merge pull request #6330 from KhaoulaMaleh/fix/attachement
Fix attachment cursor handling and await board access check
2026-05-07 17:42:42 +03:00
Lauri Ojansivu 9e94321c90 Updates 2026-05-07 17:20:56 +03:00
Lauri Ojansivu 6e07efdfa8 v9.09 2026-05-07 17:03:15 +03:00
Lauri Ojansivu 5c6aa15564 Updated ChangeLog. 2026-05-07 16:59:06 +03:00
Lauri Ojansivu 62c01725f9 Fixed CRITICAL SECURITY ISSUES of [AuthBleed](https://wekan.fi/hall-of-fame/authbleed/).
Thanks to Qiulin Deng and xet7 !
2026-05-07 16:54:51 +03:00
Lauri Ojansivu b404ac6068 Updated ChangeLog. 2026-05-07 16:10:02 +03:00
Lauri Ojansivu 4520a432c4 Updated GitHub Actions to build release bundles.
Thanks to xet7 !
2026-05-07 16:08:35 +03:00
Lauri Ojansivu aecdac5aca Updated ChangeLog. 2026-05-07 15:59:55 +03:00
Lauri Ojansivu f2d6695fdc Added Playwright Tests 2026-05-07: 16 specs. 306/306 passing.
Thanks to xet7 !
2026-05-07 15:57:42 +03:00
Lauri Ojansivu 5435753573 Updated ChangeLog. 2026-05-07 10:00:37 +03:00
Lauri Ojansivu 90cfc3f696 Docs: Clarified SECURITY.md . Part 2.
Thanks to xet7 !
2026-05-07 09:59:16 +03:00
Lauri Ojansivu 7b7e376ba2 Updated ChangeLog. 2026-05-07 09:57:34 +03:00
Lauri Ojansivu f347dc7d4d Docs: Clarified SECURITY.md .
Thanks to xet7 !
2026-05-07 09:55:43 +03:00
KhaoulaMaleh d174a4832e fix Accessing attachments 2026-05-06 18:03:20 -11:00
Lauri Ojansivu bc1cd58491 Updates 2026-05-04 22:08:56 +03:00
Lauri Ojansivu 86ade7f1f6 v9.08 2026-05-04 21:59:00 +03:00
Lauri Ojansivu f39f66192f Updated ChangeLog. 2026-05-04 21:54:28 +03:00
Lauri Ojansivu 330e0e3570 Upgrade to Meteor 3.5-beta.10, changeStreams, DDP_TRANSPORT=uws.
Thanks to Meteor developers !

Fixes #6307
2026-05-04 21:51:50 +03:00
Lauri Ojansivu a54f23e2ec Updated ChangeLog. 2026-05-03 19:52:49 +03:00
Lauri Ojansivu f7db51b457 Release scripts: Add Snap Store scripts to list and publish revisions to channels, because snapcraft.io website currently does not work.
Thanks to xet7 !
2026-05-03 19:49:42 +03:00
Lauri Ojansivu ebaa31c1e0 v9.07 2026-05-03 19:01:03 +03:00
Lauri Ojansivu 123417d931 Merge pull request #6324 from stegoh/main
Fix CleanDark theme: transparent list header, white popup text, no co…
2026-05-03 18:55:09 +03:00
Lauri Ojansivu d7b0441376 Updated ChangeLog. 2026-05-03 18:54:38 +03:00
Lauri Ojansivu a4c74a5980 Fix avatars rce.
Thanks to Trung Nguyen from CyStack Security and xet7 !
2026-05-03 18:36:15 +03:00
Stefan GohmannandClaude Sonnet 4.6 5e09f6b599 Fix CleanDark theme: transparent list header, white popup text, no column border
- Add !important to list header background/border overrides so they are
  not overridden by list.css rules that use !important
- Add !important to list background and border-left overrides to prevent
  the default #dedede background and 1px border from showing
- Add color overrides for pop-over-list links: white for CleanDark,
  dark for CleanLight, to counter the global color:#000 !important in
  popup.css (regression introduced with the popup overflow fixes)

Fixes #6168

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 08:56:24 +02:00
Lauri Ojansivu f2968f0235 Updates 2026-05-02 03:58:03 +03:00
Lauri Ojansivu fa8036aa3c v9.06 2026-05-02 03:55:32 +03:00
Lauri Ojansivu 7556b3ed8e Fix Snap Candidate attachments directory permissions at parallel snap file path /var/snap/wekan_SOMENAME/common/files/
Thanks to xet7 !
2026-05-02 03:46:05 +03:00
Lauri Ojansivu 786c9295a9 Docs: Updated manual parallel snap command.
Thanks to xet7 !
2026-05-02 03:15:24 +03:00
Lauri Ojansivu c1cc1e3e3d Updated ChangeLog. 2026-05-02 02:43:19 +03:00
Lauri Ojansivu 158f31ad4f Updates 2026-05-02 02:40:53 +03:00
Lauri Ojansivu 5e4319473e Updated ChangeLog. 2026-05-02 01:08:27 +03:00
Lauri Ojansivu 82c78ebd1e Docs: Added firewall config for Internet access from LXD Ubuntu container at Fedora Asahi Linux Remix, because building Linux arm64 version of WeKan.
Thanks to xet7 !
2026-05-02 01:03:35 +03:00
Lauri Ojansivu 628a49d84e Updated ChangeLog. 2026-05-02 00:43:36 +03:00
Lauri Ojansivu 3c8db21d2f Update docs lxc commands.
Thanks to xet7 !
2026-05-02 00:42:11 +03:00
Lauri Ojansivu 550f4a19cc v9.05 2026-05-02 00:21:46 +03:00
Lauri Ojansivu cd2c69f5f0 Fix Snap Candidate attachments WRITABLE_PATH for Parallel Snap installs. Part 2.
Thanks to xet7 !
2026-05-02 00:17:58 +03:00
Lauri Ojansivu cadd015d20 Fix Snap Candidate attachments WRITABLE_PATH for Parallel Snap installs.
Thanks to xet7 !
2026-05-02 00:04:26 +03:00
Lauri Ojansivu 5aa2219541 Updated ChangeLog. 2026-05-01 16:33:44 +03:00
Lauri Ojansivu 4e77df9ff5 Update version script and dependencies.
Thanks to xet7 !
2026-05-01 16:32:11 +03:00
Lauri Ojansivu b40e6caa4d v9.04 2026-05-01 14:17:01 +03:00
Lauri Ojansivu 2a3aa981d2 Upgrade to MongoDB 7.0.32 at Windows Bundle docs.
Thanks to MongoDB developers !
2026-05-01 14:14:06 +03:00
Lauri Ojansivu 653c61081a Update version script.
Thanks to xet7 !
2026-05-01 14:06:11 +03:00
Lauri Ojansivu cc3b5cb8c6 Updated ChangeLog. 2026-05-01 13:57:30 +03:00
Lauri Ojansivu df153dcc56 Revert Incomplete Fix "Show on Minicard" board card preference lost after upgrading from 8.74 to 8.98
Thanks to and xet7 !

Opens #6301
2026-05-01 13:56:16 +03:00
Lauri Ojansivu 5878c80fa6 Updated ChangeLog. 2026-05-01 13:47:46 +03:00
Lauri Ojansivu d04beea6f8 Fix: Added space between Card Number and Card Title at Minicard and Opened Card.
Thanks to xet7 !
2026-05-01 13:44:59 +03:00
Lauri Ojansivu d160d985c9 Updated ChangeLog. 2026-05-01 13:37:04 +03:00
Lauri Ojansivu f0a25358b4 Fix "Show on Minicard" board card preference lost after upgrading from 8.74 to 8.98 by changing back to original database field names without suffix OnMinicard.
Thanks to brlin-tw and xet7 !

Fixes #6301
2026-05-01 13:31:01 +03:00
Lauri Ojansivu 36e1753f89 Updated ChangeLog. 2026-05-01 12:53:45 +03:00
Lauri Ojansivu 9defe9681b Fix Cannot delete card checklist title, by removing "Delete" and "Convert to Card" that belong to Checklist Items only. Checklist Menu already has Delete Checklist.
Thanks to xet7 !

Fixes #5827
2026-05-01 12:49:08 +03:00
Lauri Ojansivu 2bb6999656 Updated ChangeLog. 2026-05-01 12:35:10 +03:00
Lauri Ojansivu 309b488861 Fix can not edit checklist title.
Thanks to xet7 !
2026-05-01 12:33:04 +03:00
Lauri Ojansivu 67192cf5c4 Updates 2026-04-30 02:36:05 +03:00
Lauri Ojansivu c84473ae43 v9.03 2026-04-29 00:57:29 +03:00
Lauri Ojansivu f6328937c9 Merge pull request #6321 from KhaoulaMaleh/fix/access
feat: add missing board‑attachments API endpoint (fix #6319)
2026-04-28 23:50:45 +03:00
KhaoulaMaleh 80db2517fd fixing access 2026-04-28 21:39:47 -11:00
Lauri Ojansivu 918bd0b6b5 Updated ChangeLog. 2026-04-28 20:41:20 +03:00
Lauri Ojansivu e22f422615 Update version script. Part 2.
Thanks to xet7 !
2026-04-28 20:40:19 +03:00
Lauri Ojansivu 1826f51664 Updated ChangeLog. 2026-04-28 20:37:12 +03:00
Lauri Ojansivu e547be9ab8 Update version script.
Thanks to xet7 !
2026-04-28 20:35:32 +03:00
Lauri Ojansivu 89140f1fee Updates 2026-04-28 20:08:08 +03:00
Lauri Ojansivu 1b57d09d0d v9.02 2026-04-28 20:01:39 +03:00
Lauri Ojansivu e7d67e3ddb Updated ChangeLog. 2026-04-28 19:42:34 +03:00
Lauri Ojansivu a4b5ac6c4e Fix mongodb in snap 9.01 on Ubuntu 24.04 crashes because of missing libcrypto.so.1.1 .
Thanks to sbaecker !

Fixes #6320
2026-04-28 19:39:35 +03:00
Lauri Ojansivu 9b22f9e157 Updates 2026-04-28 15:09:04 +03:00
Lauri Ojansivu 2ab0f59f13 v9.01 2026-04-28 13:48:23 +03:00
Lauri Ojansivu 611368abc6 Removed old file. 2026-04-28 13:06:15 +03:00
Lauri Ojansivu d3b3be2a52 Updated ChangeLog. 2026-04-28 12:59:50 +03:00
Lauri Ojansivu 11a2c95ea7 Fix Snap mongodb port wrong.
Thanks to xet7 !

Fixes #6316
2026-04-28 12:58:09 +03:00
Lauri Ojansivu 3e612c39b7 Updated ChangeLog. 2026-04-28 12:09:51 +03:00
Lauri Ojansivu 58de6bb737 Disable not working GitHub workflows.
Thanks to xet7 !
2026-04-28 12:03:16 +03:00
Lauri Ojansivu f6d8befb56 Moved website to separate repo. Moved docs2 back to docs.
Thanks to xet7 !
2026-04-28 11:59:16 +03:00
Lauri Ojansivu b49aa050e9 Removed charts, that are moved back to github.com/wekan/charts.
Thanks to xet7 !
2026-04-28 11:33:40 +03:00
Lauri Ojansivu 2ef8465e8d 9.00.0 2026-04-28 11:26:02 +03:00
Lauri Ojansivu 7878596608 9.00.0 2026-04-28 11:02:54 +03:00
Lauri Ojansivu 1f2f9f571e Updated ChangeLog. 2026-04-27 19:48:27 +03:00
Lauri Ojansivu 855cb86088 Docs: Updated s390x page.
Thanks to xet7 !
2026-04-27 19:45:56 +03:00
Lauri Ojansivu ed3cf96c6f Updated ChangeLog. 2026-04-27 08:32:23 +03:00
Lauri Ojansivu fdb477f351 Updated Docker platforms for what currently builds successfully.
Thanks to xet7 !
2026-04-27 08:30:08 +03:00
Lauri Ojansivu ac6cc1041a 9.00 2026-04-27 03:57:36 +03:00
Lauri Ojansivu 7d09a649ac Prepare v9.00 release 2026-04-27 03:38:04 +03:00
Lauri Ojansivu 040ed6e605 v9.00 2026-04-27 03:17:40 +03:00
Lauri Ojansivu d7e36f5157 Updated ChangeLog. 2026-04-27 03:03:47 +03:00
Lauri Ojansivu 1a08003834 Merge pull request #6314 from aminsaidane/fix/mycards-subscription
Fix/mycards subscription
2026-04-26 22:38:43 +03:00
Lauri Ojansivu 64bc6ea71c Updated ChangeLog. 2026-04-26 22:01:14 +03:00
Lauri Ojansivu ef4161889a Updated website.
Thanks to xet7 !
2026-04-26 21:59:13 +03:00
aminsaidane dd9d6083c0 fix:remove leftover code 2026-04-26 14:38:00 +01:00
aminsaidane 027b6a45a1 fix(myCards): fix subscription crash caused by non-cursor in publish return* 2026-04-26 14:32:30 +01:00
Lauri Ojansivu b717e5d7ad Updated ChangeLog. 2026-04-24 22:33:24 +03:00
Lauri Ojansivu 74965f5f95 Updated dependencies.
Thanks to developers of dependencies !
2026-04-24 22:32:25 +03:00
Lauri Ojansivu fa812faf6d Updated ChangeLog. 2026-04-24 22:06:50 +03:00
Lauri Ojansivu b459151798 Updated dependencies.
Thanks to developers of dependencies !
2026-04-24 22:05:36 +03:00
Lauri Ojansivu 73679919fb Updated ChangeLog. 2026-04-24 21:51:08 +03:00
Lauri Ojansivu 58b0ae29e1 Revert Fix MongoDB duplicate key error in notification_cleanup cron job.
Thanks to xet7 !
2026-04-24 21:50:18 +03:00
Lauri Ojansivu 773931a538 Updated ChangeLog. 2026-04-24 16:41:08 +03:00
Lauri Ojansivu 1ef814fe6b Merge pull request #6311 from Raawen-dev/fix-cron-5110-only
Fix MongoDB duplicate key error in notification_cleanup cron job (Issue #5110)
2026-04-24 16:33:34 +03:00
Lauri Ojansivu b010c687f3 Merge branch 'main' into fix-cron-5110-only 2026-04-24 16:33:18 +03:00
Lauri Ojansivu 05950fb287 Merge pull request #6310 from Raawen-dev/fix-popup-6221-only
Fix list actions popup overflow (Issue #6221)
2026-04-24 16:19:56 +03:00
RAWEN KHEFIFI 56a685983b Merge upstream/main into fix-cron-5110-only 2026-04-23 17:54:47 +01:00
RAWEN KHEFIFI 339c0a2bb7 Fix MongoDB duplicate key error in notification_cleanup cron job (#5110)
- Add check to remove existing cron job before adding
- Wrap job logic in try-catch for duplicate key errors
- Prevents E11000 error in cronHistory collection
2026-04-23 17:51:13 +01:00
RAWEN KHEFIFIandRaawen-dev a790453c9a Fix list actions popup overflow (#6221)
- Append CSS fix to existing popup.css
- Add max-height: 300px and overflow-y: auto to .pop-over-list
- Prevents list actions menu from overflowing viewport
- Tested working on Mac (Chrome/Firefox)

Co-authored-by: Raawen-dev <rawenkhefifi19@gmail.com>
2026-04-23 17:50:45 +01:00
RAWEN KHEFIFI 04eb8a8fbb Fix list actions popup overflow (Issue #6221)
- Add max-height: 300px to .pop-over-list
- Enable scrolling with overflow-y: auto
- Prevent popup from overflowing viewport
2026-04-23 15:42:05 +01:00
Lauri Ojansivu 4ab1bc9638 Updated ChangeLog. 2026-04-23 05:00:38 +03:00
Lauri Ojansivu 699b9a9c71 Update release and version scripts. 2026-04-23 04:59:20 +03:00
Lauri Ojansivu 3a203d274d Release scripts: Fix version detection.
Thanks to xet7 !
2026-04-23 03:46:25 +03:00
Lauri Ojansivu 887511fdda Updated ChangeLog. 2026-04-23 03:43:17 +03:00
Lauri Ojansivu 64f58455ff Update releases scripts.
Thanks to xet7 !
2026-04-23 03:41:59 +03:00
Lauri Ojansivu 5163ec0da2 Updated ChangeLog. 2026-04-23 01:59:52 +03:00
Lauri Ojansivu c47eb013b4 Fix UI hangs and data loss by disabling changeStreams, until there is new Meteor release that included bugfix to changeStreams.
Thanks to bcook-konza and italojs !

Fixes #6307
2026-04-23 01:55:13 +03:00
Lauri Ojansivu d4cbe12316 Updated translations. 2026-04-23 01:34:57 +03:00
Lauri Ojansivu 794222c85e Updated ChangeLog. 2026-04-23 01:26:56 +03:00
Lauri Ojansivu 675c661114 Updated HoF.
Thanks to xet7 !
2026-04-23 01:07:46 +03:00
Lauri Ojansivu 7700efde1f Update security.md etc.
Thanks to xet7 !
2026-04-23 00:44:47 +03:00
Lauri Ojansivu 16832124ce Updated dependencies.
Thanks to developers of dependencies !
2026-04-22 03:53:46 +03:00
Lauri Ojansivu bf5ca3d3eb Added back jam packages. Updated dependencies.
Thanks to xet7 !
2026-04-22 03:47:13 +03:00
Lauri Ojansivu 189b6cae84 Merge pull request #6306 from wekan/dependabot/github_actions/actions/deploy-pages-5
Bump actions/deploy-pages from 4 to 5
2026-04-21 09:08:55 +03:00
Lauri Ojansivu 9bf0114148 Merge pull request #6305 from wekan/dependabot/github_actions/docker/setup-buildx-action-4
Bump docker/setup-buildx-action from 3 to 4
2026-04-21 09:08:37 +03:00
Lauri Ojansivu 9f3bbb5ed2 Merge pull request #6304 from wekan/dependabot/github_actions/actions/upload-pages-artifact-5
Bump actions/upload-pages-artifact from 4 to 5
2026-04-21 09:08:15 +03:00
dependabot[bot] 85b981e1c7 Bump actions/deploy-pages from 4 to 5
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4 to 5.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/deploy-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 22:22:25 +00:00
dependabot[bot] eb5ac8b7dd Bump docker/setup-buildx-action from 3 to 4
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 22:22:22 +00:00
dependabot[bot] 5ef32af248 Bump actions/upload-pages-artifact from 4 to 5
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 22:22:18 +00:00
Lauri Ojansivu 9cb5837e3e Updated ChangeLog. 2026-04-18 19:14:46 +03:00
Lauri Ojansivu 5ae9c2b7ac Merge pull request #6303 from Raawen-dev/first-fix
Fix Issue #6007: Add permission checks to list width method
2026-04-18 19:12:06 +03:00
RAWEN KHEFIFI 7d16fed34e Fix Issue #6007: Add permission checks to applyListWidth method
- Add user authentication check
- Add board membership verification
- Change update to updateAsync to fix server error
- List widths now persist after page refresh
2026-04-18 17:04:58 +01:00
Lauri Ojansivu db2701f1d7 Update release scripts. In Progress.
Thanks to xet7 !
2026-04-17 18:34:36 +03:00
Lauri Ojansivu 76818e2c98 Updated ChangeLog. 2026-04-17 15:02:27 +03:00
Lauri Ojansivu c4e084ffbb Fix MongoDB replicaset setup at docker-compose.yml.
Thanks to C0rn3j.

Fixes #6297
2026-04-17 14:59:24 +03:00
Lauri Ojansivu a10b28bf6a Updated ChangeLog. 2026-04-17 14:50:30 +03:00
Lauri Ojansivu 90c0110615 Merge pull request #6299 from snowsky/main
Update cards.js
2026-04-17 14:46:00 +03:00
Hao 73c594bb36 Update cards.js
Fix the error:

```
Exception from sub myCards id xxx TypeError: Promise.await is not a function
```
2026-04-16 18:40:58 -05:00
Lauri Ojansivu 903544c680 Prepare v8.99 release 2026-04-16 22:15:49 +03:00
Lauri Ojansivu 32e1ec9d22 Release scripts: Update versions.
Thanks to xet7 !
2026-04-16 22:11:57 +03:00
Lauri Ojansivu 3516a21329 Prepare v8.98 release 2026-04-16 21:00:29 +03:00
Lauri Ojansivu f6198dee30 Release scripts: Updated creating Helm Charts to docs/wekan and docs/charts.
Thanks to xet7 !
2026-04-16 20:57:46 +03:00
Lauri Ojansivu 13f14df880 Updated Helm Charts. 2026-04-16 20:36:37 +03:00
Lauri Ojansivu f3ae64cf21 Prepare v8.98 release 2026-04-16 19:55:20 +03:00
Lauri Ojansivu 1820804b9f Fix Helm Chart parts to docs/wekan and docs/charts.
Thanks to xet7 !
2026-04-16 19:53:24 +03:00
Lauri Ojansivu d3d76b12f7 Prepare v8.98 release 2026-04-16 17:12:56 +03:00
Lauri Ojansivu 51ad6ed99d Release scripts: use node:24-slim directly.
Thanks to xet7 !
2026-04-16 17:11:24 +03:00
Lauri Ojansivu 1c8cea9ada Prepare v8.98 release 2026-04-16 16:47:23 +03:00
Lauri Ojansivu a0c190137d Release scripts: Use 24-slim fallback.
Thanks to xet7 !
2026-04-16 16:45:37 +03:00
Lauri Ojansivu 83e1845290 Prepare v8.98 release 2026-04-16 16:19:55 +03:00
Lauri Ojansivu 052d8c1584 Release scripts: Require only newest version number when releasing.
Thanks to xet7 !
2026-04-16 16:18:11 +03:00
Lauri Ojansivu 7b0e2213cf Release scripts: Fix updating version numbers.
Thanks to xet7 !
2026-04-16 16:06:19 +03:00
Lauri Ojansivu 05e6d15478 Prepare v8.98 release 2026-04-16 15:47:05 +03:00
Lauri Ojansivu be0283381b Release scripts: Use latest available Node.js slim docker images when building.
Thanks to xet7 !
2026-04-16 15:47:05 +03:00
github-actions[bot] 379a92846e v8.98 2026-04-16 12:33:19 +00:00
Lauri Ojansivu bb6163a70f Prepare v8.98 release 2026-04-16 15:32:31 +03:00
Lauri Ojansivu 10f009f5b1 Release scripts: Fix indentation at release-all.yml.
Thanks to xet7 !
2026-04-16 15:30:13 +03:00
Lauri Ojansivu 19c882ed2f Prepare v8.98 release 2026-04-16 15:23:37 +03:00
Lauri Ojansivu e7cb6ac6f0 Updated ChangeLog. 2026-04-16 15:20:40 +03:00
Lauri Ojansivu 38e488ee4a Moved Helm Charts from helm directory to docs/wekan and docs/charts to be at wekan.fi/wekan and wekan.fi/charts.
Thanks to xet7 !
2026-04-16 15:18:14 +03:00
Lauri Ojansivu 589fa30fd1 Updated ChangeLog. 2026-04-16 15:05:44 +03:00
Lauri Ojansivu 64d351d835 Commented out requirement to update website manually.
Thanks to xet7 !
2026-04-16 15:04:19 +03:00
Lauri Ojansivu 4151ecb7e7 Release scripts: Update versions at wekan.fi/install/ as part of GitHub Actions builds.
Thanks to xet7 !
2026-04-16 15:01:10 +03:00
Lauri Ojansivu f4b5e5f07d Updated ChangeLog. 2026-04-16 14:51:42 +03:00
Lauri Ojansivu c1639ea589 Update markdown docs links from docs to docs2.
Thanks to xet7 !
2026-04-16 14:50:12 +03:00
Lauri Ojansivu 21b0ab064c Updated ChangeLog. 2026-04-16 14:38:32 +03:00
Lauri Ojansivu 1c0d8e9964 Moved docs to docs2, and wekan.fi to docs, to be able publish with GitHub Pages.
Thanks to xet7 !
2026-04-16 14:36:10 +03:00
Lauri Ojansivu 8a7949a8ab Updated ChangeLog. 2026-04-16 14:30:01 +03:00
Lauri Ojansivu 3c36db33e9 Moved website from repo wekan/wekan.fi to wekan/wekan/docs/wekan.fi .
Thanks to xet7 !
2026-04-16 14:28:15 +03:00
Lauri Ojansivu 59d0cfc457 Updated ChangeLog. 2026-04-16 14:06:21 +03:00
Lauri Ojansivu 72f6d96dbc Release scripts: Detect newest MongoDB 7.x stable version and update to use it.
Thanks to xet7 !
2026-04-16 14:03:51 +03:00
Lauri Ojansivu 49c81f5426 Updated ChangeLog. 2026-04-16 13:44:42 +03:00
Lauri Ojansivu 08ed1705b1 Release scripts: Update versions at website with release-website.sh script.
Thanks to xet7 !
2026-04-16 13:44:42 +03:00
Lauri Ojansivu 47088c30c0 Release scripts: Detect newest Node.js 24.x version.
Thanks to xet7 !
2026-04-16 13:44:42 +03:00
github-actions[bot] 539dbdc034 Helm chart 8.97.0 2026-04-16 10:43:18 +00:00
github-actions[bot] 129af0bad9 v8.97 2026-04-16 10:02:21 +00:00
Lauri Ojansivu a9d95dd6b7 Prepare v8.97 release 2026-04-16 13:01:42 +03:00
Lauri Ojansivu 6d0978d0ed Release Scripts: Clean GitHub Cache. Part 3.
Thanks to xet7 !
2026-04-16 13:00:39 +03:00
Lauri Ojansivu 13cdf08e22 Release Scripts: Clean GitHub Cache. Part 2.
Thanks to xet7 !
2026-04-16 12:56:52 +03:00
Lauri Ojansivu e59bc9cd86 Release Scripts: Clean GitHub Cache.
Thanks to xet7 !
2026-04-16 12:53:42 +03:00
Lauri Ojansivu 6478d27d1c Fix GitHub Actions builds.
Thanks to xet7 !
2026-04-16 12:49:37 +03:00
Lauri Ojansivu 76d1800ec7 Prepare v8.96 release 2026-04-16 12:46:15 +03:00
Lauri Ojansivu 03ba747bc0 Fix GitHub Actions builds.
Thanks to xet7 !
2026-04-16 12:43:35 +03:00
Lauri Ojansivu 9eb3889d51 Prepare v8.95 release 2026-04-16 12:33:53 +03:00
Lauri Ojansivu 5d4bc7a886 Updated ChangeLog. 2026-04-16 12:31:58 +03:00
Lauri Ojansivu 135af977af Helm Chart: Use current Finland time for created field at index.yaml for newest built Helm Chart release.
Thanks to xet7 !
2026-04-16 12:26:44 +03:00
Lauri Ojansivu 6dfe81cbcb Fix Cannot find module /build/main.js error in latest Docker image.
Thanks to s8321414 and xet7 !

Fixes #6291
2026-04-16 12:16:58 +03:00
Lauri Ojansivu 3ec2821058 Updated ChangeLog. 2026-04-16 12:04:27 +03:00
Lauri Ojansivu 1c5ac8efba Merge pull request #6292 from titver968/helm-improvements
helm: add single-node ReplicaSet support with auto MONGO_OPLOG_URL
2026-04-16 12:00:01 +03:00
titver968 ce7fa0e314 helm: add single-node ReplicaSet support with auto MONGO_OPLOG_URL
- mongodb.url template now appends ?replicaSet=<name> for single-replica
  setups when mongodb.replicaSet.name is set, eliminating the need for
  a secretEnv workaround
- New mongodb.oplog.url helper generates MONGO_OPLOG_URL automatically
  when a ReplicaSet is configured (single-node or multi-node)
- deployment.yaml injects MONGO_OPLOG_URL env var when the oplog URL
  helper produces a non-empty value
- values.yaml documents the replicaSet.name option with example config
2026-04-16 09:58:44 +02:00
github-actions[bot] 0f464b21a3 Helm chart 8.94.0 2026-04-16 02:34:48 +00:00
github-actions[bot] 7169da8dae v8.94 2026-04-16 02:15:31 +00:00
Lauri Ojansivu 0f56e2dfc3 Prepare v8.94 release 2026-04-16 05:14:48 +03:00
Lauri Ojansivu 1b086c80a7 Updated ChangeLog. 2026-04-16 05:12:42 +03:00
Lauri Ojansivu c27d5d0412 Updated dompurify.
Thanks to developers of dompurify !
2026-04-16 05:10:59 +03:00
Lauri Ojansivu 7b3043bf46 Updated ChangeLog. 2026-04-16 05:09:46 +03:00
Lauri Ojansivu da19160e87 Fix Can't start latest docker image because of MongoDB Index errors.
Thanks to s8321414 and xet7 !

Fixes #6290
2026-04-16 05:09:46 +03:00
github-actions[bot] 790a801f30 Helm chart 8.93.0 2026-04-16 02:07:36 +00:00
github-actions[bot] b08c66a483 v8.93 2026-04-16 01:48:41 +00:00
Lauri Ojansivu 524aa9124d Prepare v8.93 release 2026-04-16 04:48:01 +03:00
Lauri Ojansivu df85b5ac45 Updated ChangeLog. 2026-04-16 04:44:04 +03:00
Lauri Ojansivu c009ddc915 Fix SSO Login broken. Part 2.
Thanks to xet7 !

Fixes #6276
2026-04-16 04:44:04 +03:00
github-actions[bot] 5407e4533c Helm chart 8.92.0 2026-04-16 01:08:11 +00:00
github-actions[bot] 98088c7734 v8.92 2026-04-16 00:49:02 +00:00
Lauri Ojansivu 0d6fb33043 Prepare v8.92 release 2026-04-16 03:48:15 +03:00
Lauri Ojansivu 1dbad1b454 Updated ChangeLog. 2026-04-16 03:44:46 +03:00
Lauri Ojansivu 2c8a6c8ec6 GitHub Actions: Added missing sudo to snap build.
Thanks to xet7 !
2026-04-16 03:42:56 +03:00
Lauri Ojansivu 06b9946dd8 Updated ChangeLog. 2026-04-16 03:36:32 +03:00
Lauri Ojansivu dbcdf15244 Updated Helm Charts versions.
Thanks to xet7 !
2026-04-16 03:36:32 +03:00
github-actions[bot] 7e2067d0b1 v8.91 2026-04-16 00:21:29 +00:00
Lauri Ojansivu 7489f720ea Prepare v8.91 release 2026-04-16 03:02:50 +03:00
Lauri Ojansivu 140e844238 Updated Helm Chart URL at readme.
Thanks to xet7 !
2026-04-16 03:02:50 +03:00
Lauri Ojansivu de55d5afbe Fix building Snap at GitHub Actions.
Thanks to xet7 !
2026-04-16 03:02:50 +03:00
github-actions[bot] c5fccb52cc Helm chart 8.90.0 2026-04-15 23:50:03 +00:00
github-actions[bot] 507ca57a50 v8.90 2026-04-15 23:28:22 +00:00
Lauri Ojansivu b2e072e02c Prepare v8.90 release 2026-04-16 02:27:44 +03:00
Lauri Ojansivu 357f63d01f Release scripts: Moved more steps to GitHub Actions.
Thanks to xet7 !
2026-04-16 02:24:17 +03:00
Lauri Ojansivu e6422d842a Updated ChangeLog. 2026-04-16 02:02:33 +03:00
Lauri Ojansivu 99519af83a Changed ArtifactHub from wekan/charts repo to wekan/wekan repo.
Thanks to xet7 !
2026-04-16 02:00:47 +03:00
Lauri Ojansivu 514ce6ad24 Updated ChangeLog. 2026-04-16 01:52:33 +03:00
Lauri Ojansivu f57ce52198 Added GitHub Actions for Docker and Snap. Moved Helm Charts from charts repo to wekan repo.
Thanks to xet7 !
2026-04-16 01:49:42 +03:00
Lauri Ojansivu 2a9c921ba6 Updated ChangeLog. 2026-04-16 00:54:13 +03:00
Lauri Ojansivu 0dd924f148 Removed jam:offline jam:method jam:pub-sub, because they are not used, and they cause too much database usage.
Thanks to xet7 !
2026-04-16 00:50:58 +03:00
Lauri Ojansivu 678eb0896d v8.89 2026-04-15 23:01:39 +03:00
Lauri Ojansivu 233c1c8e81 Updated ChangeLog. 2026-04-15 22:57:58 +03:00
Lauri Ojansivu f6a851a3d8 Disable jam:offline keepAll BEFORE any collections are created. Part 2.
Thanks to xet7 !
2026-04-15 22:56:01 +03:00
Lauri Ojansivu 4fba9d7802 v8.88 2026-04-15 21:45:44 +03:00
Lauri Ojansivu d41ecb054c Updated ChangeLog. 2026-04-15 21:37:09 +03:00
Lauri Ojansivu ec0c1bc9af Disable jam:offline keepAll to prevent full-collection COLLSCAN queries and Ensure standard GridFS index on attachments.chunks for efficient chunk lookups.
Disable jam:offline keepAll to prevent full-collection COLLSCAN queries.
WeKan uses modifiedAt (not updatedAt) and archived (not deleted),
so the default offline sync generates queries on non-existent fields
causing massive collection scans on every sync cycle.

Ensure standard GridFS index on attachments.chunks for efficient chunk lookups.
Without this, queries like find({files_id: ObjectId}) do full collection scans.
2026-04-15 21:33:18 +03:00
Lauri Ojansivu 05b77d24fa v8.87 2026-04-15 17:39:22 +03:00
Lauri Ojansivu 4535303807 Updated ChangeLog. 2026-04-15 17:37:33 +03:00
Lauri Ojansivu f232fdc30b Merge pull request #6287 from FlorentDotMe/fix_quick-access-scroll
ui: enable the horizontal scroll in the quick access menu boards list
2026-04-15 17:36:24 +03:00
Lauri Ojansivu dab72d80dc Updated ChangeLog. 2026-04-15 17:35:55 +03:00
Lauri Ojansivu 6f83464b24 Merge pull request #6286 from FlorentDotMe/fix_rooturl-as-env-variable
improve handling of ROOT_URL as an environement variable
2026-04-15 17:34:48 +03:00
Lauri Ojansivu 41cd0a8925 Updated ChangeLog. 2026-04-15 17:34:35 +03:00
Lauri Ojansivu fc02a15753 Try to fix Snap.
Thanks to xet7 !
2026-04-15 17:31:09 +03:00
Florent Bervas bd9c5a058a improve handling of ROOT_URL as an environement variable 2026-04-15 13:47:28 +00:00
Lauri Ojansivu c1692cc6b9 v8.86 2026-04-15 16:29:44 +03:00
Florent Bervas 5259c09a5c ui: enable the horizontal scroll in the quick access menu boards list 2026-04-15 13:28:08 +00:00
Lauri Ojansivu 10f801d31b Try to fix Snap.
Thanks to xet7 !
2026-04-15 16:25:06 +03:00
Lauri Ojansivu 28d19db328 v8.85 2026-04-15 16:02:59 +03:00
Lauri Ojansivu 0e13bfc50e Try to fix Snap.
Thanks to xet7 !
2026-04-15 16:00:33 +03:00
Lauri Ojansivu 4f4aa22ca1 v8.84 2026-04-15 15:29:15 +03:00
Lauri Ojansivu 6bec01198d Updated ChangeLog. 2026-04-15 15:27:02 +03:00
Lauri Ojansivu 5dbceb43b4 Updated ChangeLog. 2026-04-15 15:17:14 +03:00
Lauri Ojansivu bc92978fa9 Try to fix Snap Candidate.
Thanks to xet7 !
2026-04-15 15:16:02 +03:00
Lauri Ojansivu 893f83b79e Updated ChangeLog. 2026-04-15 15:09:53 +03:00
Lauri Ojansivu bde23f6c4d Merge pull request #6284 from cloudron-io/oidc-login-fix
Use local content variable in error case
2026-04-15 15:05:53 +03:00
Johannes Zellner e6d4b38c64 Use local content variable in error case 2026-04-15 10:27:53 +02:00
Lauri Ojansivu 23264044e6 v8.83 2026-04-14 21:39:23 +03:00
Lauri Ojansivu d51a381a79 Updated ChangeLog. 2026-04-14 21:32:31 +03:00
Lauri Ojansivu da4459304f Changed start-wekan.sh and start-wekan.bat to not use Change Streams.
Thanks to xet7 !
2026-04-14 21:31:08 +03:00
Lauri Ojansivu 085699f202 Updated ChangeLog. 2026-04-14 01:12:42 +03:00
Lauri Ojansivu 2c43aa463a Release scripts: Fix Node.js 24.x download URL for Snap builds.
Thanks to xet7 !
2026-04-14 01:09:26 +03:00
Lauri Ojansivu 41db7b4ad1 Release scripts: Docker not for armhf, no official Node.js binary for it.
Thanks to xet7 !
2026-04-14 00:58:35 +03:00
Lauri Ojansivu d0a9fa9a71 Release scripts: Removed armhf, there is no official Node.js binary for it.
Thanks to xet7 !
2026-04-14 00:38:55 +03:00
Lauri Ojansivu ed0675aaff v8.82 2026-04-14 00:37:28 +03:00
Lauri Ojansivu cad8f58468 Merge pull request #6282 from wekan/dependabot/github_actions/docker/build-push-action-7.1.0
Bump docker/build-push-action from 7.0.0 to 7.1.0
2026-04-14 00:31:12 +03:00
Lauri Ojansivu 819d58cf31 Merge pull request #6281 from wekan/dependabot/github_actions/softprops/action-gh-release-3
Bump softprops/action-gh-release from 2 to 3
2026-04-14 00:30:43 +03:00
dependabot[bot] b951515e3e Bump docker/build-push-action from 7.0.0 to 7.1.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.0.0 to 7.1.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/d08e5c354a6adb9ed34480a06d141179aa583294...bcafcacb16a39f128d818304e6c9c0c18556b85f)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 21:24:10 +00:00
dependabot[bot] 4c3265bca5 Bump softprops/action-gh-release from 2 to 3
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 21:24:05 +00:00
Lauri Ojansivu 11672a13f7 Release scripts: Added script for deleting repo git tag.
Thanks to xet7 !
2026-04-14 00:19:15 +03:00
Lauri Ojansivu 9a8141a0eb v8.81 2026-04-14 00:16:28 +03:00
Lauri Ojansivu 5462fb0647 Release scripts: Comment out build for armhf because no official Node.js 24.x binaries exist for armhf (arm/v7).
Thanks to xet7 !
2026-04-14 00:10:51 +03:00
Lauri Ojansivu 6b74b8f770 v8.81 2026-04-13 22:57:32 +03:00
Lauri Ojansivu 99fff1a1a1 Updated ChangeLog. 2026-04-13 20:25:00 +03:00
Lauri Ojansivu 645f233b83 Merge pull request #6279 from kolaente/patch-1
fix(exporter): attachments always empty in board JSON export
2026-04-13 20:22:38 +03:00
kolaente 643e2738bd fix(exporter): attachments always empty in board JSON export 2026-04-13 18:15:48 +02:00
Lauri Ojansivu cd356a29ce Updated translations. 2026-04-13 17:16:10 +03:00
Lauri Ojansivu 341be982f9 Updated ChangeLog. 2026-04-13 17:14:21 +03:00
Lauri Ojansivu 2e7565f9eb Docs: Added History of Schema Migration Systems.
Thanks to xet7 !
2026-04-13 17:13:00 +03:00
Lauri Ojansivu 1637a9c938 Updated dependencies.
Thanks to developers of dependencies !
2026-04-13 17:11:04 +03:00
Lauri Ojansivu 67c2be1858 Updated ChangeLog. 2026-04-13 16:25:14 +03:00
Lauri Ojansivu b8d4c01e60 Updated ChangeLog. 2026-04-13 16:23:41 +03:00
Lauri Ojansivu e259ab86f7 Updated dependencies.
Thanks to developers of dependencies !
2026-04-13 16:23:41 +03:00
Lauri Ojansivu 029b3523cb Merge pull request #6275 from KhaoulaMaleh/fix/export
Fix: include historical comments and activities in board exports
2026-04-13 16:21:02 +03:00
KhaoulaMaleh f4657ad817 Fix: include historical comments and activities in board exports 2026-04-13 00:12:57 -11:00
Lauri Ojansivu 5efddff154 Updated translations. 2026-04-12 17:35:51 +03:00
Lauri Ojansivu 25efe93507 Updated ChangeLog. 2026-04-12 17:31:27 +03:00
Lauri Ojansivu a393e67ef8 Merge pull request #6262 from Raawen-dev/first-fix
Fix Issue #6069: Add migration for v7.95 due dates
2026-04-12 17:29:21 +03:00
Lauri Ojansivu 7f6758cbe9 Merge pull request #6268 from YosserDerbali/fix/board-drag-tooltip-clarification
Clarify board drag tooltip text in Remaining and Workspaces
2026-04-12 17:28:48 +03:00
Lauri Ojansivu c5644a3321 Updated ChangeLog. 2026-04-12 17:28:38 +03:00
Lauri Ojansivu 4fec03101f Updated ChangeLog. 2026-04-12 17:27:32 +03:00
Lauri Ojansivu 10fed0a85c Merge pull request #6271 from YosserDerbali/fix/readme-node-version-and-changes-made
Fix/readme node version and changes made
2026-04-12 17:25:51 +03:00
Lauri Ojansivu fff6316192 Updated ChangeLog. 2026-04-12 17:23:19 +03:00
Lauri Ojansivu 61623f50b1 Merge pull request #6273 from AymenHassini19/solve-white-container-“Link-Card”-dialog
Fix Link Card modal overflow in Docker environment
2026-04-12 17:21:59 +03:00
Lauri Ojansivu 9f626407cf Updated ChangeLog. 2026-04-12 17:20:26 +03:00
Lauri Ojansivu 3a51678495 Merge pull request #6272 from snowsky/fix-activities-not-showing
Fix activities are not showing
2026-04-12 17:14:02 +03:00
Aymen Hassini c730a0e733 Add missing Meteor test dependencies 2026-04-12 10:17:18 +01:00
Aymen Hassini 80a9971508 solving white container "Link Card" 2026-04-12 09:31:49 +01:00
Hao Wang 85788e1d7d Fix activities are not showing 2026-04-11 19:39:27 +00:00
Yosser Derbali 23680547ea docs: update README to Node.js 24.x and remove Rosetta mention 2026-04-11 14:30:04 +01:00
Yosser Derbali 9767ead9f3 docs: update README Node.js version and setup guidance 2026-04-10 22:26:16 +01:00
Yosser Derbali 910c9a7f1f docs: improve README with first-time setup guide and fix typos 2026-04-10 22:24:40 +01:00
Yosser Derbali 0bc6c6abb6 docs: fix typos and grammar in SECURITY.md
- Fix capitalization: 'About Money', 'QA About PubSub', etc.
- Add missing articles ('the', 'a')
- Fix spelling: 'Wordwide' -> 'Worldwide', 'logics' -> 'logic'
- Remove duplicate 'by' and extra spaces
- Fix grammar and sentence structure for clarity
- Improve consistency with WeKan capitalization
2026-04-10 22:24:40 +01:00
Yosser Derbali 6361e16dba Clarify board drag tooltip text 2026-04-10 18:21:04 +01:00
RAWEN KHEFIFI 74966a0bc7 Fix Issue #6069: Add migration for v7.95 due dates
- Add migration script to convert dueDate to dueAt
- Convert string dates to Date objects
- Add missing userId and members fields
- Fix invalid board/list/swimlane references

This fixes cards not appearing in Due Cards view after migration from v7.95 to v8.20
2026-04-08 16:44:19 +01:00
523 changed files with 201600 additions and 145635 deletions

No files matched your search

+39 -10
View File
@@ -20,7 +20,10 @@
"strict": 0,
"no-undef": 0,
"accessor-pairs": 2,
"comma-dangle": [2, "always-multiline"],
"comma-dangle": [
2,
"always-multiline"
],
"consistent-return": 2,
"dot-notation": 2,
"eqeqeq": 2,
@@ -28,15 +31,28 @@
"no-cond-assign": 2,
"no-constant-condition": 2,
"no-eval": 2,
"no-inner-declarations": [0],
"no-inner-declarations": [
0
],
"no-unneeded-ternary": 2,
"radix": 2,
"semi": [2, "always"],
"camelcase": [2, { "properties": "never" }],
"semi": [
2,
"always"
],
"camelcase": [
2,
{
"properties": "never"
}
],
"comma-spacing": 2,
"comma-style": 2,
"eol-last": 2,
"linebreak-style": [2, "unix"],
"linebreak-style": [
2,
"unix"
],
"meteor/audit-argument-checks": 0,
"new-parens": 2,
"no-lonely-if": 2,
@@ -45,7 +61,13 @@
"no-spaced-func": 2,
"no-trailing-spaces": 2,
"operator-linebreak": 2,
"quotes": [2, "single", { "avoidEscape": true }],
"quotes": [
2,
"single",
{
"avoidEscape": true
}
],
"semi-spacing": 2,
"space-unary-ops": 2,
"arrow-spacing": 2,
@@ -57,6 +79,7 @@
"prefer-spread": 2,
"prefer-template": 2,
"no-unused-vars": "warn",
"@typescript-eslint/no-floating-promises": "error",
"prettier/prettier": [
"error",
{
@@ -72,11 +95,18 @@
"settings": {
"import/resolver": {
"meteor": {
"extensions": [".js", ".jsx"]
"extensions": [
".js",
".jsx"
]
}
}
},
"plugins": ["prettier", "meteor"],
"plugins": [
"prettier",
"meteor",
"@typescript-eslint"
],
"globals": {
"Meteor": false,
"Session": false,
@@ -95,7 +125,6 @@
"_": false,
"Avatar": true,
"Avatars": true,
"CollectionHooks": false,
"ESSearchResults": false,
"FastRender": false,
@@ -110,7 +139,7 @@
"Ps": true,
"Restivus": false,
"SimpleSchema": false,
"T9n": false,
"T9n": false,
"TAPi18n": false,
"AccountsTemplates": true,
"Activities": true,
+1 -1
View File
@@ -11,4 +11,4 @@ jobs:
- name: 'Checkout Repository'
uses: actions/checkout@v6
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4
uses: actions/dependency-review-action@v5
+3 -3
View File
@@ -38,7 +38,7 @@ jobs:
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -48,14 +48,14 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
+176
View File
@@ -0,0 +1,176 @@
name: Playwright E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: playwright-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
unit-tests:
name: Meteor unit tests
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Setup Node 24
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- name: Install Meteor
run: curl https://install.meteor.com/ | sh
- name: Install npm dependencies
run: npm ci
- name: Run Meteor unit tests
run: meteor test --once --driver-package meteortesting:mocha
env:
TEST_WATCH: '0'
e2e-tests:
name: Playwright E2E (${{ matrix.project }})
runs-on: ubuntu-24.04
needs: unit-tests
strategy:
fail-fast: false
matrix:
project: [chromium, firefox]
# webkit excluded from CI (requires macOS runner) — run locally with npm run test:e2e:all
services:
mongodb:
image: mongo:7
ports: ['27017:27017']
options: >-
--health-cmd "mongosh --eval 'db.adminCommand(\"ping\")'"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v6
- name: Setup Node 24
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- name: Install Meteor
run: curl https://install.meteor.com/ | sh
- name: Install app npm dependencies
run: npm ci
- name: Install Playwright test dependencies
run: npm ci
working-directory: tests/playwright
- name: Install Playwright browsers
run: npx playwright install --with-deps ${{ matrix.project }}
working-directory: tests/playwright
- name: Start WeKan in background
run: |
MONGO_URL=mongodb://localhost:27017/wekan_test \
ROOT_URL=http://localhost:3000 \
PORT=3000 \
meteor run --settings settings.json &
# Wait up to 120 s for WeKan to become available
for i in $(seq 1 24); do
curl -sf http://localhost:3000 > /dev/null && break
echo "Waiting for WeKan... attempt $i"
sleep 5
done
env:
DISABLE_CAPTCHA: 'true'
- name: Run Playwright tests (${{ matrix.project }})
run: |
npx playwright test \
--project=${{ matrix.project }} \
--reporter=github
working-directory: tests/playwright
env:
WEKAN_BASE_URL: http://localhost:3000
WEKAN_MONGO_URL: mongodb://localhost:27017/wekan_test
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v7
with:
name: playwright-report-${{ matrix.project }}
path: tests/playwright/playwright-report/
retention-days: 14
- name: Upload test results (screenshots / videos)
if: failure()
uses: actions/upload-artifact@v7
with:
name: test-results-${{ matrix.project }}
path: tests/playwright/test-results/
retention-days: 7
e2e-regression:
name: Puppeteer regression suite
runs-on: ubuntu-24.04
needs: unit-tests
services:
mongodb:
image: mongo:7
ports: ['27017:27017']
options: >-
--health-cmd "mongosh --eval 'db.adminCommand(\"ping\")'"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v6
- name: Setup Node 24
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- name: Install Meteor
run: curl https://install.meteor.com/ | sh
- name: Install npm dependencies
run: npm ci
- name: Install Chromium
run: sudo apt-get install -y chromium-browser
- name: Start WeKan in background
run: |
MONGO_URL=mongodb://localhost:27017/wekan_regression \
ROOT_URL=http://localhost:3000 \
PORT=3000 \
meteor run &
for i in $(seq 1 24); do
curl -sf http://localhost:3000 > /dev/null && break
sleep 5
done
env:
DISABLE_CAPTCHA: 'true'
- name: Run list-regressions e2e
run: node tests/e2e/list-regressions.js
env:
WEKAN_BASE_URL: http://localhost:3000
WEKAN_MONGO_URL: mongodb://localhost:27017/wekan_regression
CHROMIUM_PATH: /usr/bin/chromium-browser
HEADLESS: 'true'
+132 -195
View File
@@ -1,24 +1,38 @@
name: Release All Platforms
# Triggered when maintainer pushes a version tag, e.g.: git tag v8.43 && git push --tags
# Does not work:
# Required GitHub Secrets:
# QUAY_USERNAME - Quay.io username
# QUAY_PASSWORD - Quay.io password or token
# DOCKERHUB_USERNAME - Docker Hub username
# DOCKERHUB_TOKEN - Docker Hub access token
# SNAP_STORE_LOGIN - Snapcraft store credentials: run 'snapcraft export-login --snaps wekan --channels stable -' and base64-encode the output
# Triggered manually via GitHub CLI or Actions UI.
# Usage:
# gh workflow run release-all.yml -f old_version=8.42 -f new_version=8.43
#
# Or use the convenience script (commits pending changes, pushes, and triggers):
# ./releases/release-all.sh 8.42 8.43
#
# Required GitHub Secrets:
# GHCR_AUTH - base64(username:token) for ghcr.io
# DOCKERHUB_AUTH - base64(username:token) for Docker Hub
# QUAY_AUTH - base64(username:token) for Quay.io
# SNAP_AUTH - Snapcraft store credentials
on:
push:
tags:
- 'v*.*'
workflow_dispatch:
inputs:
old_version:
description: 'Previous version number (e.g. 8.42, without v prefix)'
required: true
type: string
new_version:
description: 'New version number (e.g. 8.43, without v prefix)'
required: true
type: string
permissions:
contents: write
packages: write
pages: write
id-token: write
jobs:
# ─── 1. Extract version and release notes ───────────────────────────────────
prepare:
runs-on: ubuntu-latest
@@ -27,10 +41,15 @@ jobs:
changelog: ${{ steps.changelog.outputs.content }}
steps:
- uses: actions/checkout@v6
with:
ref: refs/tags/v${{ needs.prepare.outputs.version }}
- name: Extract version from tag
- name: Set version from input
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
run: |
VERSION="${{ needs.prepare.outputs.version }}"
VERSION="${VERSION#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Extract release notes from CHANGELOG.md
id: changelog
@@ -65,15 +84,18 @@ jobs:
# Therefore the full Meteor build always runs here on x86_64.
# All other Linux architectures then rebuild only the native npm modules
# from this bundle (much faster than a full Meteor build).
# Node.js version must match what Meteor 3.5-beta.7 bundles: 24.14.1 (ABI 143).
# Node.js version must match what Meteor 3.5-beta.7 bundles (detected automatically).
build-amd64:
needs: prepare
runs-on: ubuntu-24.04
timeout-minutes: 180
env:
VERSION: ${{ needs.prepare.outputs.version }}
NODE_VERSION: ${{ needs.prepare.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
ref: refs/tags/v${{ inputs.new_version }}
- name: Cache Meteor installation
uses: actions/cache@v5
@@ -93,11 +115,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential g++ make python3 curl wget zip unzip
# Pin to Node 24.14.1 — the exact version bundled by Meteor 3.5-beta.7.
# Pin to detected latest Node.js 24.x — must match Meteor's bundled version.
# Using the same version for build and npm rebuild ensures consistent native module ABI.
- uses: actions/setup-node@v6
with:
node-version: '24.14.1'
node-version: '${{ needs.prepare.outputs.version }}'
- name: Install Meteor
run: |
@@ -134,22 +156,26 @@ jobs:
timeout-minutes: 60
env:
VERSION: ${{ needs.prepare.outputs.version }}
NODE_VERSION: ${{ needs.prepare.outputs.version }}
NODE_VERSION_ARM64: ''
steps:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential g++ make python3 curl unzip zip
- uses: actions/setup-node@v6
with:
node-version: '24.14.1'
- name: Set Node.js version for arm64
id: node_arm64
run: |
echo "NODE_VERSION_ARM64=24" >> $GITHUB_ENV
- name: Download amd64 bundle
uses: actions/download-artifact@v8
with:
name: bundle-amd64
- name: Rebuild native modules for arm64
- name: Rebuild native modules for arm64 in container
run: |
unzip wekan-${VERSION}-amd64.zip
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
@@ -166,10 +192,12 @@ jobs:
popd > /dev/null
rm -rf "$TMP"
fi
cd bundle/programs/server
rm -rf node_modules
npm install
cd ../..
# Use the latest available Node.js 24.x slim image for arm64
docker run --rm --platform linux/arm64 \
-v "$(pwd)/bundle:/bundle" \
-w /bundle/programs/server \
node:24-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
- name: Clean up and create arm64 zip
run: |
@@ -184,14 +212,15 @@ jobs:
# ─── 4. Build s390x bundle (rebuild native modules via QEMU container) ───────
# Meteor 3.5-beta.7 does NOT support s390x (installer exits with error on that arch).
# GitHub Actions has no native s390x runners.
# Solution: QEMU emulation + node:24.14.1-slim (s390x variant) for npm rebuild.
# node:24.14.1-slim is pinned to match Meteor 3.5-beta.7's bundled Node.js version exactly.
# Solution: QEMU emulation + node:<version>-slim (s390x variant) for npm rebuild.
build-s390x:
needs: [prepare, build-amd64]
runs-on: ubuntu-latest
timeout-minutes: 90
env:
VERSION: ${{ needs.prepare.outputs.version }}
NODE_VERSION: ${{ needs.prepare.outputs.version }}
NODE_VERSION_S390X: ''
steps:
- name: Set up QEMU for s390x
uses: docker/setup-qemu-action@v4
@@ -206,6 +235,11 @@ jobs:
with:
name: bundle-amd64
- name: Set Node.js version for s390x
id: node_s390x
run: |
echo "NODE_VERSION_S390X=24" >> $GITHUB_ENV
- name: Rebuild native modules for s390x in container
run: |
unzip wekan-${VERSION}-amd64.zip
@@ -224,11 +258,11 @@ jobs:
popd > /dev/null
rm -rf "$TMP"
fi
# node:24.14.1-slim s390x — same Node.js version as Meteor 3.5-beta.7 bundled
# Use the latest available Node.js 24.x slim image for s390x
docker run --rm --platform linux/s390x \
-v "$(pwd)/bundle:/bundle" \
-w /bundle/programs/server \
node:24.14.1-slim \
node:24-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
- name: Clean up and create s390x zip
@@ -244,13 +278,15 @@ jobs:
# ─── 4b. Build ppc64le bundle (rebuild native modules via QEMU container) ────
# Meteor 3.5-beta.7 does NOT support ppc64le (installer exits with error on that arch).
# GitHub Actions has no native ppc64le runners.
# Solution: QEMU emulation + node:24.14.1-slim (ppc64le variant) for npm rebuild.
# Solution: QEMU emulation + node:<version>-slim (ppc64le variant) for npm rebuild.
build-ppc64le:
needs: [prepare, build-amd64]
runs-on: ubuntu-latest
timeout-minutes: 90
env:
VERSION: ${{ needs.prepare.outputs.version }}
NODE_VERSION: ${{ needs.prepare.outputs.version }}
NODE_VERSION_PPC64LE: ''
steps:
- name: Set up QEMU for ppc64le
uses: docker/setup-qemu-action@v4
@@ -265,6 +301,11 @@ jobs:
with:
name: bundle-amd64
- name: Set Node.js version for ppc64le
id: node_ppc64le
run: |
echo "NODE_VERSION_PPC64LE=24" >> $GITHUB_ENV
- name: Rebuild native modules for ppc64le in container
run: |
unzip wekan-${VERSION}-amd64.zip
@@ -283,11 +324,11 @@ jobs:
popd > /dev/null
rm -rf "$TMP"
fi
# node:24.14.1-slim ppc64le — same Node.js version as Meteor 3.5-beta.7 bundled
# Use the latest available Node.js 24.x slim image for ppc64le
docker run --rm --platform linux/ppc64le \
-v "$(pwd)/bundle:/bundle" \
-w /bundle/programs/server \
node:24.14.1-slim \
node:24-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
- name: Clean up and create ppc64le zip
@@ -300,65 +341,62 @@ jobs:
path: wekan-${{ needs.prepare.outputs.version }}-ppc64le.zip
retention-days: 1
# ─── 4c. Build armhf bundle (rebuild native modules via QEMU container) ──────
# Meteor 3.5-beta.7 does NOT support arm/v7 (armhf). Installer explicitly rejects it.
# GitHub Actions has no native arm/v7 runners.
# Solution: QEMU emulation + node:24.14.1-slim (arm/v7 variant) for npm rebuild.
# arm/v7 under QEMU is slowest — timeout set to 120 min.
build-armhf:
needs: [prepare, build-amd64]
runs-on: ubuntu-latest
timeout-minutes: 120
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- name: Set up QEMU for arm/v7
uses: docker/setup-qemu-action@v4
with:
platforms: arm
- name: Install unzip/zip
run: sudo apt-get update && sudo apt-get install -y unzip zip
- name: Download amd64 bundle
uses: actions/download-artifact@v8
with:
name: bundle-amd64
- name: Rebuild native modules for armhf in container
run: |
unzip wekan-${VERSION}-amd64.zip
rm -rf bundle/programs/server/node_modules
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
# which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
if [ -d "$BCRYPT_DIR" ]; then
rm -rf "$BCRYPT_DIR"
TMP=$(mktemp -d)
pushd "$TMP" > /dev/null
npm pack bcrypt@6.0.0
mkdir bcrypt && tar xzf bcrypt-*.tgz --strip-components=1 -C bcrypt
cd bcrypt && npm install --ignore-scripts && cd ..
cp -a bcrypt "$BCRYPT_DIR"
popd > /dev/null
rm -rf "$TMP"
fi
# node:24.14.1-slim arm/v7 — same Node.js version as Meteor 3.5-beta.7 bundled
docker run --rm --platform linux/arm/v7 \
-v "$(pwd)/bundle:/bundle" \
-w /bundle/programs/server \
node:24.14.1-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
- name: Clean up and create armhf zip
run: |
zip -r wekan-${VERSION}-armhf.zip bundle
- uses: actions/upload-artifact@v7
with:
name: bundle-armhf
path: wekan-${{ needs.prepare.outputs.version }}-armhf.zip
retention-days: 1
# ─── 4c. Build armhf bundle — COMMENTED OUT ─────────────────────────────────
# No official Node.js binaries exist for armhf (arm/v7).
#build-armhf:
# needs: [prepare, build-amd64]
# runs-on: ubuntu-latest
# timeout-minutes: 120
# env:
# VERSION: ${{ needs.prepare.outputs.version }}
# steps:
# - name: Set up QEMU for arm/v7
# uses: docker/setup-qemu-action@v4
# with:
# platforms: arm
#
# - name: Install unzip/zip
# run: sudo apt-get update && sudo apt-get install -y unzip zip
#
# - name: Download amd64 bundle
# uses: actions/download-artifact@v8
# with:
# name: bundle-amd64
#
# - name: Rebuild native modules for armhf in container
# run: |
# unzip wekan-${VERSION}-amd64.zip
# rm -rf bundle/programs/server/node_modules
# # Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
# # which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
# BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
# if [ -d "$BCRYPT_DIR" ]; then
# rm -rf "$BCRYPT_DIR"
# TMP=$(mktemp -d)
# pushd "$TMP" > /dev/null
# npm pack bcrypt@6.0.0
# mkdir bcrypt && tar xzf bcrypt-*.tgz --strip-components=1 -C bcrypt
# cd bcrypt && npm install --ignore-scripts && cd ..
# cp -a bcrypt "$BCRYPT_DIR"
# popd > /dev/null
# rm -rf "$TMP"
# fi
# # node:<version>-slim arm/v7 — same Node.js version as Meteor bundled
# docker run --rm --platform linux/arm/v7 \
# -v "$(pwd)/bundle:/bundle" \
# -w /bundle/programs/server \
# node:${NODE_VERSION}-slim \
# sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
#
# - name: Clean up and create armhf zip
# run: |
# zip -r wekan-${VERSION}-armhf.zip bundle
#
# - uses: actions/upload-artifact@v7
# with:
# name: bundle-armhf
# path: wekan-${{ needs.prepare.outputs.version }}-armhf.zip
# retention-days: 1
# ─── 5. Build win64 bundle (rebuild native modules on Windows) ───────────────
build-win64:
@@ -370,7 +408,7 @@ jobs:
steps:
- uses: actions/setup-node@v6
with:
node-version: '24.14.1'
node-version: '${{ needs.prepare.outputs.version }}'
## With Node 24 these are already preinstalled at GitHub Windows runners
#- name: Install node-gyp and build tools
@@ -424,7 +462,7 @@ jobs:
# ─── 6. Create GitHub Release and upload all bundles ────────────────────────
release:
needs: [prepare, build-amd64, build-arm64, build-s390x, build-ppc64le, build-armhf, build-win64]
needs: [prepare, build-amd64, build-arm64, build-s390x, build-ppc64le, build-win64]
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
@@ -436,8 +474,9 @@ jobs:
merge-multiple: true
- name: Create GitHub Release with all bundles
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: v${{ needs.prepare.outputs.version }}
name: v${{ needs.prepare.outputs.version }}
body: ${{ needs.prepare.outputs.changelog }}
files: |
@@ -445,106 +484,4 @@ jobs:
wekan-${{ needs.prepare.outputs.version }}-arm64.zip
wekan-${{ needs.prepare.outputs.version }}-s390x.zip
wekan-${{ needs.prepare.outputs.version }}-ppc64le.zip
wekan-${{ needs.prepare.outputs.version }}-armhf.zip
wekan-${{ needs.prepare.outputs.version }}-win64.zip
# ─── 7. Build and push multi-platform Docker image ──────────────────────────
#docker:
# needs: [prepare, release]
# runs-on: ubuntu-latest
# env:
# VERSION: ${{ needs.prepare.outputs.version }}
# steps:
# - uses: actions/checkout@v6
#
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v4
#
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v3
#
# - name: Log in to Docker Hub
# uses: docker/login-action@v4
# with:
# username: ${{ secrets.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_TOKEN }}
#
# - name: Log in to Quay.io
# uses: docker/login-action@v4
# with:
# registry: quay.io
# username: ${{ secrets.QUAY_USERNAME }}
# password: ${{ secrets.QUAY_PASSWORD }}
#
# - name: Log in to GitHub Container Registry
# uses: docker/login-action@v4
# with:
# registry: ghcr.io
# username: ${{ github.actor }}
# password: ${{ secrets.GITHUB_TOKEN }}
#
# - name: Build and push Docker image to all registries
# uses: docker/build-push-action@v7
# with:
# context: .
# platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/ppc64le,linux/s390x
# push: true
# build-args: VERSION=${{ needs.prepare.outputs.version }}
# tags: |
# #wekanteam/wekan:v${{ needs.prepare.outputs.version }}
# #wekanteam/wekan:latest
# quay.io/wekan/wekan:v${{ needs.prepare.outputs.version }}
# quay.io/wekan/wekan:latest
# ghcr.io/wekan/wekan:v${{ needs.prepare.outputs.version }}
# ghcr.io/wekan/wekan:latest
# cache-from: type=gha
# cache-to: type=gha,mode=max
# ─── 8. Build and publish snap packages (all architectures via Launchpad) ────
# Uses snapcraft remote-build on Launchpad's native builders (more reliable
# than QEMU for snap builds). Each architecture bundles its own database:
# amd64, arm64 — MongoDB 7.x prebuilt native binary
# armhf, s390x, ppc64el — MongoDB 7.x amd64 binary run via qemu-x86_64-static
# (no prebuilt MongoDB binaries exist for these arches)
#snap:
# needs: [prepare, release]
# runs-on: ubuntu-latest
# env:
# VERSION: ${{ needs.prepare.outputs.version }}
# steps:
# - uses: actions/checkout@v6
#
# - name: Install snapcraft
# run: sudo snap install snapcraft --classic
#
# - name: Remote-build snap for all architectures via Launchpad
# env:
# SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }}
# run: |
# snapcraft remote-build \
# --build-for=amd64,arm64,armhf,s390x,ppc64el \
# --launchpad-accept-public-upload
#
# - name: Upload all snap files as artifacts
# uses: actions/upload-artifact@v7
# with:
# name: snaps-all-arches
# path: "*.snap"
# retention-days: 30
#
# # Publish to edge — NOT stable.
# # Stable channel holds the old WeKan version until database migrations are ready.
# # Promote to stable manually via the Snap Store dashboard when migrations exist.
# - name: Publish all snaps to Snap Store (candidate, beta, edge)
# env:
# SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }}
# run: |
# for snap_file in *.snap; do
# echo "Publishing $snap_file ..."
# snapcraft upload --release=edge "$snap_file"
# done
#
# - name: Upload all snaps to GitHub Release
# uses: softprops/action-gh-release@v2
# with:
# files: "*.snap"
-168
View File
@@ -1,168 +0,0 @@
name: Test suite
on:
push:
branches:
- main
pull_request:
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
# the following are optional jobs and need to be configured according
# to this project's settings:
#
# lintcode:
# name: Javascript lint
# runs-on: ubuntu-latest
# steps:
# - name: checkout
# uses: actions/checkout@v6
#
# - name: setup node
# uses: actions/setup-node@v1
# with:
# node-version: '12.x'
#
# - name: cache dependencies
# uses: actions/cache@v1
# with:
# path: ~/.npm
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-node-
#
# - run: npm install
# - run: npm run lint:code
#
# lintstyle:
# name: SCSS lint
# runs-on: ubuntu-latest
# needs: [lintcode]
# steps:
# - name: checkout
# uses: actions/checkout@v6
#
# - name: setup node
# uses: actions/setup-node@v1
# with:
# node-version: '12.x'
#
# - name: cache dependencies
# uses: actions/cache@v1
# with:
# path: ~/.npm
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-node-
# - run: npm install
# - run: npm run lint:style
#
# lintdocs:
# name: documentation lint
# runs-on: ubuntu-latest
# needs: [lintcode,lintstyle]
# steps:
# - name: checkout
# uses: actions/checkout@v6
#
# - name: setup node
# uses: actions/setup-node@v1
# with:
# node-version: '12.x'
#
# - name: cache dependencies
# uses: actions/cache@v1
# with:
# path: ~/.npm
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-node-
#
# - run: npm install
# - run: npm run lint:markdown
tests:
name: Meteor ${{ matrix.meteor }} tests
runs-on: ubuntu-latest
steps:
# CHECKOUTS
- name: Checkout
uses: actions/checkout@v6
# CACHING
- name: Install Meteor
id: cache-meteor-install
uses: actions/cache@v5
with:
path: ~/.meteor
key: v1-meteor-${{ hashFiles('.meteor/versions') }}
restore-keys: |
v1-meteor-
- name: Cache NPM dependencies
id: cache-meteor-npm
uses: actions/cache@v5
with:
path: ~/.npm
key: v1-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
v1-npm-
- name: Cache Meteor build
id: cache-meteor-build
uses: actions/cache@v5
with:
path: |
.meteor/local/resolver-result-cache.json
.meteor/local/plugin-cache
.meteor/local/isopacks
.meteor/local/bundler-cache/scanner
key: v1-meteor_build_cache-${{ github.ref }}-${{ github.sha }}
restore-keys: |
v1-meteor_build_cache-
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24.14.1'
- name: Setup meteor
uses: meteorengineer/setup-meteor@v3
with:
meteor-release: '3.5-beta.7'
- name: Install NPM Dependencies
run: meteor npm install
- name: Run Tests
run: sh ./test-wekan.sh -cv
- name: Upload coverage
uses: actions/upload-artifact@v7
with:
name: coverage-folder
path: .coverage/
coverage:
name: Coverage report
runs-on: ubuntu-latest
needs: [tests]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download coverage
uses: actions/download-artifact@v8
with:
name: coverage-folder
path: .coverage/
- name: Coverage Report
uses: VeryGoodOpenSource/very_good_coverage@v3.0.0
with:
path: ".coverage/lcov.info"
min_coverage: 1 # TODO add tests and increase to 95!
+11
View File
@@ -1,4 +1,5 @@
*~
/*.log
*.sw*
.meteor-spk
*.sublime-workspace
@@ -53,3 +54,13 @@ _build
*/build-assets
*/build-chunks
.rsdoctor
# Tests
playwright-report/
test-results/
# Claude AI
.claude/
# Tools like mongosh etc so that they work inside Flatpak sandbox
.tools/
+13 -17
View File
@@ -6,9 +6,9 @@
meteor-base@1.5.2
# Build system
ecmascript@0.17.0
ecmascript@0.18.0
standard-minifier-js@3.2.0
rspack@1.0.0
rspack@1.1.0
blaze@3.0.2
@@ -16,18 +16,11 @@ blaze@3.0.2
aldeed:collection2
aldeed:schema-index@4.0.0-beta.5
reywood:publish-composite
mongo@2.3.0-beta350.7
# The holy trifecta - this is a replacement for subs-manager
# for IndexedDB persistence and auto-sync
jam:offline
# for optimistic methods with offline queuing
jam:method
# global subscription caching
jam:pub-sub
mongo@2.4.0-rc350.1
# Account system
accounts-password@3.2.3-beta350.7
accounts-password@3.3.0-rc350.1
accounts-express@1.0.0-rc350.1
communitypackages:core
communitypackages:unstyled
communitypackages:flow-routing
@@ -44,7 +37,7 @@ wekan-accounts-oidc
check@1.5.0
random@1.2.2
reactive-dict@1.3.2
session@1.2.2
session@1.2.3-rc350.1
tracker@1.3.4
audit-argument-checks@1.0.8
@@ -52,18 +45,21 @@ audit-argument-checks@1.0.8
ostrio:i18n
reactive-var@1.0.13
shell-server@0.7.0
email@3.2.0-beta350.7
email@3.2.0-rc350.1
dynamic-import@0.7.4
wekan-markdown
quave:synced-cron
ostrio:cookies
ostrio:files
# lmieulet:meteor-coverage
meteortesting:mocha
meteortesting:mocha@3.0.0
matb33:collection-hooks
service-configuration@1.3.5
minifier-css@2.0.1
ejson@1.2.0-beta350.7
minifier-css@2.0.2-rc350.1
ejson@1.2.0-rc350.1
logging@1.3.6
wekan-fullcalendar
wekan-fontawesome
jam:offline
jam:method
jam:pub-sub
+1 -1
View File
@@ -1 +1 @@
METEOR@3.5-beta.7
METEOR@3.5-rc.1
+48 -48
View File
@@ -1,48 +1,48 @@
accounts-base@3.3.0-beta350.7
accounts-oauth@1.4.6
accounts-password@3.2.3-beta350.7
accounts-base@3.3.0-rc350.1
accounts-express@1.0.0-rc350.1
accounts-oauth@1.4.7-rc350.1
accounts-password@3.3.0-rc350.1
aldeed:collection2@4.1.5
aldeed:schema-index@4.0.0
aldeed:simple-schema@1.13.1
allow-deny@2.1.0
audit-argument-checks@1.0.8
autoupdate@2.0.1
babel-compiler@7.13.0
babel-compiler@7.14.0
babel-runtime@1.5.2
base64@1.0.13
binary-heap@1.0.12
blaze@3.0.2
blaze-tools@2.0.0
base64@1.0.14-rc350.1
binary-heap@1.0.13-rc350.1
blaze@3.0.3
blaze-tools@2.0.1
boilerplate-generator@2.1.0
caching-compiler@2.0.1
caching-html-compiler@2.0.0
callback-hook@1.6.1
caching-html-compiler@2.0.1
callback-hook@1.8.0-rc350.1
check@1.5.0
communitypackages:core@2.0.3
communitypackages:flow-routing@2.1.0
communitypackages:unstyled@2.0.1
core-runtime@1.0.0
ddp@1.4.2
ddp-client@3.2.0-beta350.7
ddp-common@1.4.4
ddp-rate-limiter@1.3.0-beta350.7
ddp-server@3.2.0-beta350.7
ddp-client@3.3.0-rc350.1
ddp-common@1.4.5-rc350.1
ddp-rate-limiter@1.3.0-rc350.1
ddp-server@3.3.0-rc350.1
diff-sequence@1.1.3
dynamic-import@0.7.4
ecmascript@0.17.0
ecmascript@0.18.0
ecmascript-runtime@0.8.3
ecmascript-runtime-client@0.12.3
ecmascript-runtime-client@0.13.0
ecmascript-runtime-server@0.11.1
ejson@1.2.0-beta350.7
email@3.2.0-beta350.7
ejson@1.2.0-rc350.1
email@3.2.0-rc350.1
es5-shim@4.8.1
facts-base@1.0.2
fetch@0.1.6
fetch@0.2.0-rc350.1
geojson-utils@1.0.12
hot-code-push@1.0.5
html-tools@2.0.0
htmljs@2.0.1
http@1.4.4
html-tools@2.0.1
htmljs@2.0.2
id-map@1.2.0
inter-process-messaging@0.1.2
jam:method@1.9.1
@@ -53,36 +53,36 @@ lai:collection-extensions@1.0.0
localstorage@1.2.1
logging@1.3.6
matb33:collection-hooks@2.1.0
meteor@2.2.0
meteor@2.3.1-rc350.1
meteor-base@1.5.2
meteortesting:browser-tests@1.4.2
meteortesting:mocha@2.1.0
meteortesting:mocha-core@8.0.1
minifier-css@2.0.1
minifier-js@3.1.0
minimongo@2.1.0-beta350.7
meteortesting:browser-tests@1.8.0
meteortesting:mocha@3.3.0
meteortesting:mocha-core@8.2.0
minifier-css@2.0.2-rc350.1
minifier-js@3.2.0
minimongo@2.2.0-rc350.1
modern-browsers@0.2.3
modules@0.20.3
modules-runtime@0.13.2
mongo@2.3.0-beta350.7
mongo@2.4.0-rc350.1
mongo-decimal@0.2.0
mongo-dev-server@1.1.1
mongo-id@1.0.9
npm-mongo@6.16.1
oauth@3.0.2
oauth@3.0.4-rc350.1
oauth2@1.3.3
observe-sequence@2.0.0
observe-sequence@2.0.1
ordered-dict@1.2.0
ostrio:cookies@2.9.1
ostrio:cookies@2.9.2
ostrio:cstorage@4.0.1
ostrio:files@3.0.1
ostrio:flow-router-extra@3.12.1
ostrio:flow-router-extra@3.15.3
ostrio:i18n@3.2.1
promise@1.0.0
quave:synced-cron@2.4.0
raix:eventemitter@1.0.0
random@1.2.2
rate-limit@1.2.0-beta350.7
rate-limit@1.2.0-rc350.1
react-fast-refresh@0.3.0
reactive-dict@1.3.2
reactive-var@1.0.13
@@ -90,24 +90,24 @@ reload@1.3.2
retry@1.1.1
reywood:publish-composite@1.9.0
routepolicy@1.1.2
rspack@1.0.0
rspack@1.1.0
service-configuration@1.3.5
session@1.2.2
session@1.2.3-rc350.1
sha@1.0.10
shell-server@0.7.0
socket-stream-client@0.7.0-beta350.7
spacebars@2.0.0
spacebars-compiler@2.0.0
socket-stream-client@0.7.0-rc350.1
spacebars@2.0.1
spacebars-compiler@2.0.1
standard-minifier-js@3.2.0
templating@1.4.4
templating-compiler@2.0.0
templating-runtime@2.0.1
templating-tools@2.0.0
tools-core@1.0.0
templating@1.4.5
templating-compiler@2.0.1
templating-runtime@2.0.2
templating-tools@2.0.1
tools-core@1.1.0
tracker@1.3.4
typescript@5.9.3
url@1.3.5
webapp@2.2.0-beta350.7
typescript@5.10.0
url@1.3.6-rc350.1
webapp@2.2.0-rc350.1
webapp-hashing@1.1.2
wekan-accounts-cas@0.2.0
wekan-accounts-lockout@1.1.0
+1
View File
@@ -1 +1,2 @@
npm-packages/
tests/
+1624 -10
View File
File diff suppressed because it is too large. Load diff
+10 -7
View File
@@ -1,21 +1,21 @@
FROM ubuntu:24.04
FROM ubuntu:26.04
LABEL maintainer="wekan"
LABEL org.opencontainers.image.ref.name="ubuntu"
LABEL org.opencontainers.image.version="24.04"
LABEL org.opencontainers.image.version="26.04"
LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
# TARGETARCH and TARGETVARIANT are automatically provided by Docker Buildx
ARG TARGETARCH
ARG TARGETVARIANT
ARG VERSION=8.80
ARG VERSION=9.35
ARG DEBIAN_FRONTEND=noninteractive
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
ENV \
DEBUG=false \
NODE_VERSION=v24.14.1 \
METEOR_RELEASE=METEOR@3.5-beta.7 \
NODE_VERSION=v24.16.0 \
METEOR_RELEASE=METEOR@3.5-beta.12 \
USE_EDGE=false \
NPM_VERSION=11.11.0 \
SRC_PATH=./ \
@@ -190,7 +190,7 @@ rm -f "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt.asc
ln -s "/usr/local/bin/node" "/usr/local/bin/nodejs"
# NPM configuration
npm install -g npm@${NPM_VERSION} --production
npm install -g npm@${NPM_VERSION}
chown --recursive wekan:wekan /home/wekan/
# Temporary Tar swap for Meteor bundle
@@ -203,13 +203,16 @@ cd /home/wekan/app
wget "https://github.com/wekan/wekan/releases/download/v${VERSION}/wekan-${VERSION}-${WEKAN_ARCH}.zip"
unzip "wekan-${VERSION}-${WEKAN_ARCH}.zip"
rm "wekan-${VERSION}-${WEKAN_ARCH}.zip"
npm install --production --no-audit --prefix ./bundle/programs/server
npm install --prefix ./bundle/programs/server
mv /home/wekan/app/bundle /build
# Restore original tar
mv $(which tar)~ $(which tar)
# Cleanup
# Remove unused Go-based pebble binary shipped by base image to reduce CVE surface.
apt-get remove --purge --assume-yes pebble || true
rm -f /usr/bin/pebble
apt-get remove --purge --assume-yes ${BUILD_DEPS}
apt-get autoremove --assume-yes
apt-get clean --assume-yes
+70 -6
View File
@@ -47,12 +47,13 @@ See bottom of https://github.com/wekan/wekan/issues/3874
## FAQ
**NOTE**:
- Please read the [FAQ](https://github.com/wekan/wekan/blob/main/docs/FAQ/FAQ.md) first
- Please don't feed the [trolls](https://github.com/wekan/wekan/blob/main/docs/FAQ/FAQ.md#why-am-i-called-a-troll) and [spammers](https://github.com/wekan/wekan/blob/main/docs/FAQ/FAQ.md#why-am-i-called-a-spammer) that are mentioned in the FAQ :)
## About WeKan ®
WeKan ® is an completely [Open Source][open_source] and [Free software][free_software]
WeKan ® is a completely [Open Source][open_source] and [Free software][free_software]
collaborative kanban board application with MIT license.
Whether you’re maintaining a personal todo list, planning your holidays with some friends,
@@ -76,15 +77,15 @@ that by providing one-click installation on various platforms.
- 1 GB RAM minimum free for WeKan ®. Production server should have minimum total 4 GB RAM.
For thousands of users, for example with [Docker](https://github.com/wekan/wekan/blob/main/docker-compose.yml): 3 frontend servers,
each having 2 CPU and 2 wekan-app containers. One backend wekan-db server with many CPUs.
- Enough disk space and alerts about low disk space. If you run out disk space, MongoDB database gets corrupted.
- Enough disk space and alerts about low disk space. If you run out of disk space, MongoDB database gets corrupted.
- SECURITY: Updating to newest WeKan ® version very often. Please check you do not have automatic updates of Sandstorm or Snap turned off.
Old versions have security issues because of old versions Node.js etc. Only newest WeKan ® is supported.
WeKan ® on Sandstorm is not usually affected by any Standalone WeKan ® (Snap/Docker/Source) security issues.
- [Reporting all new bugs immediately](https://github.com/wekan/wekan/issues).
New features and fixes are added to WeKan ® [many times a day](https://github.com/wekan/wekan/blob/main/CHANGELOG.md).
- [Backups](https://github.com/wekan/wekan/blob/main/docs/Backup/Backup.md) of WeKan ® database once a day miminum.
- [Backups](https://github.com/wekan/wekan/blob/main/docs/Backup/Backup.md) of WeKan ® database once a day minimum.
Bugs, updates, users deleting list or card, harddrive full, harddrive crash etc can eat your data. There is no undo yet.
Some bug can cause WeKan ® board to not load at all, requiring manual fixing of database content.
Some bugs can cause WeKan ® board to not load at all, requiring manual fixing of database content.
## Roadmap and Demo
@@ -92,7 +93,7 @@ that by providing one-click installation on various platforms.
[Developer Documentation][dev_docs]
- There is many companies and individuals contributing code to WeKan ®, to add features and bugfixes
- There are many companies and individuals contributing code to WeKan ®, to add features and bugfixes
[many times a day](https://github.com/wekan/wekan/blob/main/CHANGELOG.md).
- [Please add Add new Feature Requests and Bug Reports immediately](https://github.com/wekan/wekan/issues).
- [Commercial Support](https://wekan.fi/commercial-support/).
@@ -102,12 +103,75 @@ By working directly with WeKan ® you get the benefit of active maintenance and
## Getting Started with Development
The default branch uses [Meteor 2 with Node.js 14](https://wekan.fi/install/).
The default branch uses Meteor 3.5-beta.7 with Node.js 24.x.
See [CHANGELOG.md](https://github.com/wekan/wekan/blob/main/CHANGELOG.md) for the latest runtime updates.
To contribute, [create a fork](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#2-create-fork-of-httpsgithubcomwekanwekan-at-github-web-page) and run `./rebuild-wekan.sh` (or `./rebuild-wekan.bat` on Windows) as detailed [here](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#3-select-option-1-to-install-dependencies-and-then-enter). Once you're ready, please test your code and [submit a pull request (PR)](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#7-test).
Please refer to the [developer documentation](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Developer-Documentation.md) for more information.
## First-Time Setup for Development
### Prerequisites
Before building WeKan from source, ensure you have:
- **Git** - for cloning the repository
- **Node.js 24.x** - WeKan requires Node.js 24.x
- **Meteor** - the JavaScript framework WeKan is built with
### Installing Node.js 24.x
```bash
# Using nvm (recommended)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.zshrc # or ~/.bashrc
nvm install 24
nvm use 24
```
### Installing Meteor
```bash
curl https://install.meteor.com/ | sh
```
### Building WeKan
The `rebuild-wekan.sh` script guides you through a three-stage process:
1. **Install dependencies (Option 1)** - Downloads all required Meteor packages and npm modules
2. **Build WeKan (Option 2)** - Compiles the application
3. **Run WeKan (Option 3)** - Starts the development server at http://localhost:4000
```bash
# Clone your fork
git clone git@github.com:YOUR_USERNAME/wekan.git
cd wekan
# Make the script executable
chmod +x rebuild-wekan.sh
# Step 1: Install dependencies
./rebuild-wekan.sh
# Press 1 and Enter
# Step 2: Build WeKan (after dependencies complete)
./rebuild-wekan.sh
# Press 2 and Enter
# Step 3: Run WeKan in development mode
./rebuild-wekan.sh
# Press 3 and Enter
```
### WSL Users
WSL users can use Snap Candidate. See [install docs](https://wekan.fi/install/).
The development server will start at http://localhost:4000. Any changes you make to the source code will automatically trigger a rebuild and refresh your browser.
## Screenshot
[More screenshots at Features page](https://github.com/wekan/wekan/tree/main/docs/Features)
+77 -77
View File
@@ -1,38 +1,35 @@
## About money
## About Money: No bounty
See [CONTRIBUTING](CONTRIBUTING.md)
There is no bounty, like described at [CONTRIBUTING.md](CONTRIBUTING.md),
because WeKan is NOT Big Tech. WeKan is FLOSS.
## Responsible Security Disclosure
## Steps for Coordinated Vulnerability Disclosure
1. To send email, if possible, use PGP key [security-at-wekan.fi.asc](security-at-wekan.fi.asc)
2. Send info about security issue ONLY to security@wekan.fi . NOT TO ANYWHERE ELSE. NO CC, NO BCC.
3. Wait for new WeKan release that fixes security issue to appear to top of
https://github.com/wekan/wekan/blob/main/CHANGELOG.md
4. We will thank you by adding you to Hall of Fame: https://wekan.fi/hall-of-fame/
5. All vulnerability details will be private to security@wekan.fi ,
unless you help all WeKan platforms to have a way to upgrade, like sending
database migrations code to security@wekan.fi or PRs to https://github.com/wekan/wekan/pulls .
There is no benefit to Wordwide Security Community to have more details about vulnerabilities,
if Worldwide Security Community does not help to make upgrades possible.
6. If there some day becomes available a way to upgrade all WeKan platforms,
this page will be updated to add permission for security researchers
to request new GHSA or CVE ID and publish your vulnerability details at your blog, talks, etc,
and send that info also to security@wekan.fi to be added to
Hall of Fame: https://wekan.fi/hall-of-fame/ to get Upgrade Bonus Point Stars.
In that case, it will become possible for security@wekan.fi to publish all
remaining private security details, and publicly thank Worldwide Security Community.
### 1. Security Researcher:
## Bonus Points
- **Report**: Please email details regarding security vulnerabilities to **security@wekan.fi**.
- **Proof of Concept (PoC)**: If possible, include a fix or a reproduction script/code.
- **Encryption**: An optional PGP key is available at [security-at-wekan.fi.asc](security-at-wekan.fi.asc).
- If you include code for fixing security issue
### 2. WeKan Security Team:
## Losing Points
- **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/).
- **CVE Policy**: WeKan Security does not facilitate **CVE Coordination** due to
our rapid release cycle (new versions are released almost daily).
- If you ask about [bounty](CONTRIBUTING.md). There is no bounty. WeKan is NOT Big Tech. WeKan is FLOSS.
- If you forget to include vulnerability details.
- If you send info about security issue to somewhere else than security@wekan.fi
### 3. Post-Release and Public Disclosure:
## How should reports be formatted?
- **Independent CVE Assignment**: After the fix is public, you are welcome to coordinate
**CVE assignment** independently. Please send the assigned CVE IDs to **security@wekan.fi**,
and we will update your entry in the [Hall of Fame](https://wekan.fi/hall-of-fame/).
- Publicity: If you publish blog posts, give talks, or write articles about the
reported vulnerabilities, please send the links to us. We will link them
to your entry in the [Hall of Fame](https://wekan.fi/hall-of-fame/) for additional context.
## How Should Reports Be Formatted?
```
Name: %name
@@ -57,15 +54,19 @@ added to the WeKan Hall of Fame https://wekan.fi/hall-of-fame/
No public domains, because all those are donated to Wekan Open Source project,
and we don't have any permissions to do security scans on those donated servers.
Please don't perform research that could impact other users. Secondly, please keep
the reports short and succinct. If we fail to understand the logics of your bug, we will tell you.
Please don't perform research that could impact other users. Second, please keep
the reports short and succinct. If we fail to understand the logic of your bug, we will tell you.
You can [Install Wekan](https://github.com/wekan/wekan/releases) to your own computer
You can [Install Wekan](https://wekan.fi/install/) on your own computer
and scan it's vulnerabilities there.
## About Wekan versions
## About Wekan Versions
There are only 2 versions of Wekan: Standalone Wekan, and Sandstorm Wekan.
There are 2 versions of Wekan: Standalone Wekan, and Sandstorm Wekan.
## Meteor Security
https://guide.meteor.com/security
### Standalone Wekan Security
@@ -74,11 +75,12 @@ like Snap and Docker have their own specific sandboxing etc features.
Standalone Wekan by default does not load any files from Internet, like fonts, CSS, etc.
This also means all Standalone Wekan functionality works in offline local networks.
WeKan is used at most countries of the world https://snapcraft.io/wekan
and by by companies that have 30k users.
WeKan is used in most countries of the world https://snapcraft.io/wekan
and by companies that have 30k users.
- Wekan private board attachments are not accessible without logging in.
- There is feature to set board public, so that board is visible without logging in in readonly mode, with realtime updates.
- There is feature to set board public, so that board is visible without logging
in readonly mode, with realtime updates.
- Admin Panel has feature to disable all public boards, so all boards are private.
## SSL/TLS
@@ -90,24 +92,25 @@ and by by companies that have 30k users.
## XSS
- Dompurify https://www.npmjs.com/package/dompurify
- WeKan uses dompurify npm package to filter for XSS at fields like cards, as you can see from
- DOMPurify https://www.npmjs.com/package/dompurify
- WeKan uses DOMPurify npm package to filter for XSS at fields like cards, as you can see from
[package.json](https://github.com/wekan/wekan/blob/main/package.json). Other used versions can be seen from
[Meteor versions file](https://github.com/wekan/wekan/blob/main/.meteor/versions).
- Forms can include markdown links, html, image tags etc like you see at https://wekan.github.io .
- It's possible to add attachments to cards, and markdown/html links to files.
- Dompurify cleans up viewed code, so Javascript in input fields does not execute
- DOMPurify cleans up viewed code, so JavaScript in input fields does not execute
- https://wekan.github.io/hall-of-fame/fieldbleed/
- Reaction in comment is now checked, that it does not have extra added code
- https://wekan.github.io/hall-of-fame/reactionbleed/
- https://github.com/wekan/wekan/blob/main/packages/markdown/src/template-integration.js#L76
## QA about PubSub
## QA About PubSub
Q:
Hello,
I have just seen the Meteor DevTools Evolved extension and was wondering if anyone had asked themselves the question of security.
I have just seen the Meteor DevTools Evolved extension and was wondering if anyone
had asked themselves the question of security.
Insofar as all data is shown in the minimongo tab in plain text.
How can data be hidden from this extension?
@@ -115,32 +118,36 @@ A:
## PubSub
- It is not security issue to show some text or image, that user has permission to see. It is a security issue, if browserside is some text or image that user should not see.
- Meteor has browserside minimongo database, made with Javascript, updated with Publish/Subscribe, PubSub.
- Publish/Subscribe means, that realtime web framework reads database changes stream, and then immediately updates webpage,
like like dashboards, chat, kanban. That is the point in any realtime web framework in any programming language.
- Yes, you should check with Meteor DevTools Evolved Chromium/Firefox extension that at minimongo is only text that user has permission to see.
- Do checking as logged in user, and logged out user.
- It is not a security issue to show text or images that the user has permission to see.
It is a security issue, if browserside is some text or image that user should not see.
- Meteor has browserside minimongo database, made with JavaScript, updated with Publish/Subscribe, PubSub.
- Publish/Subscribe means, that realtime web framework reads database changes stream,
and then immediately updates webpage, like dashboards, chat, kanban.
That is the point of any realtime web framework in any programming language.
- Yes, you should check with Meteor DevTools Evolved Chromium/Firefox extension that
at minimongo is only text that user has permission to see.
- Check as a logged-in user and as a logged-out user.
- Check permissions and sanitize before allowing some change, because someone could modify content of input field,
PubSub/websocket data (for example with Burp Suite Community Edition), etc.
- If you have REST API, also check that only those that have login token, and have permission, can view or edit text
- If you have a REST API, also check that only those who have a login token and permission can view or edit text.
- You should not include any data user is not allowed to see. Not to webpage text, not to websockets/PubSub, etc.
- Minimongo should not have password hashes PubSub https://wekan.github.io/hall-of-fame/userbleed/
- PubSub uses Websockets, so you need those to be enabled at webserver like Caddy/Nginx/Apache etc, examples of settings
- PubSub uses WebSockets, so you need those to be enabled on the webserver like Caddy/Nginx/Apache etc, examples of settings
at right menu of https://github.com/wekan/wekan/wiki
- Clientside https://github.com/wekan/wekan/tree/main/client/components subscribes to
PubSub https://github.com/wekan/wekan/tree/main/server/publications or calls meteor methods at https://github.com/wekan/wekan/tree/main/models
- Clientside https://github.com/wekan/wekan/tree/main/client/components subscribes to
PubSub https://github.com/wekan/wekan/tree/main/server/publications or calls meteor methods
at https://github.com/wekan/wekan/tree/main/models
- For Admin:
- You can have input field for password https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
- You can save password to database https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
- Check that only current user or Admin can change password https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
- Note that currentUser uses code like Meteor.user() in .js file
- Do not have password hashes in PubSub https://github.com/wekan/wekan/blob/main/server/publications/users.js
- Do not include password hashes in PubSub https://github.com/wekan/wekan/blob/main/server/publications/users.js
- Only show Admin Panel to Admin https://github.com/wekan/wekan/blob/main/client/components/settings/settingBody.jade#L3
- If there is a lot of data, use pagination https://github.com/wekan/wekan/blob/main/client/components/settings/peopleBody.js
- Only have limited amount of data published in PubSub. Limit in MongoDB query in publications how much is published. Too much could make browser too slow.
- Use Environment variables for any email etc passwords.
- But what if you would like to remove minimongo? And only use Meteor methods for saving? In that case, you don't have realtime updates,
- If there is a large amount of data, use pagination https://github.com/wekan/wekan/blob/main/client/components/settings/peopleBody.js
- Only publish a limited amount of data in PubSub. Limit in MongoDB query in publications how much is published. Too much could make browser too slow.
- Environment variables for email and other passwords.
- But what if you would like to remove Minimongo and only use Meteor methods for saving? In that case, you don't have realtime updates,
and you need to write much more code to load and save data yourself, handle any multi user data saving conflicts yourself,
and many Meteor Atmospherejs.com PubSub using packages would not work anymore https://github.com/wekan/we
@@ -152,10 +159,10 @@ A:
## Permissions and Roles
- For any user permissions, it's best to use Meteor package package https://github.com/Meteor-Community-Packages/meteor-roles .
- Currently WeKan has custom hardcoded permissions, WeKan does not yet use that meteor-roles package.
- For any user permissions, it's best to use Meteor package https://github.com/Meteor-Community-Packages/meteor-roles .
- Currently, WeKan has custom hardcoded permissions and does not yet use the meteor-roles package.
- Using permissions at WeKan sidebar https://github.com/wekan/wekan/blob/main/client/components/sidebar/sidebar.js#L1854-L1875
- List of roles https://github.com/wekan/wekan/wiki/REST-API-Role . Change at board or Admin Panel. Also Organizations/Teams.
- List of roles: https://github.com/wekan/wekan/wiki/REST-API-Role . Change at board or Admin Panel. Also Organizations/Teams.
- Worker role: https://github.com/wekan/wekan/issues/2788
- Not implemented yet: Granular Roles https://github.com/wekan/wekan/issues/3022
- Check is user logged in, with `if (Meteor.user()) {`
@@ -164,8 +171,8 @@ A:
## Environment variables
- For any passwords, use environment variables, those are serverside
- Do not copy environment variable to public variable that is visible browserside https://github.com/wekan/wekan/blob/main/server/max-size.js
- For any passwords, use environment variables; those are serverside
- Do not copy environment variables to public variables that are visible on the browserside https://github.com/wekan/wekan/blob/main/server/max-size.js
```
Meteor.startup(() => {
@@ -176,13 +183,13 @@ Meteor.startup(() => {
```
- For serverside, you can set Meteor.settings.variablename, without text public
- For WeKan kanban, there is feature for setting board public, it can be viewed by anyone, there is realtime updates. But
- For WeKan kanban, there is a feature for setting a board to public; it can be viewed by anyone, with realtime updates. But
- Some of those permissions are checked at users.js models at https://github.com/wekan/wekan/tree/main/models
- Environment variables are used for email server passwords, etc, at all platforms https://github.com/wekan/wekan/commit/a781c0e7dcfdbe34c1483ee83cec12455b7026f7
## Escape HTML comment tags so that HTML comments are visible
- Someone reported, that it is problem that content of HTML comments in edit mode, are not visible at at view mode, so this makes HTML comments visible.
- Someone reported that it was a problem that the content of HTML comments in edit mode was not visible in view mode, so this change makes HTML comments visible.
- https://github.com/wekan/wekan/commit/167863d95711249e69bb3511175d73b34acbbdb3
- https://wekan.github.io/hall-of-fame/invisiblebleed/
@@ -249,18 +256,16 @@ Meteor.startup(() => {
### Sandstorm Wekan Security
On Sandstorm platform using environment variable Standalone Wekan features like Admin Panel etc are
turned off, because Sandstorm platform provides SSO for all apps running on Sandstorm.
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 separate Open Source platform that has been
[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 also has passwordless login, LDAP, SAML, Google etc auth options already.
At Sandstorm code is read-only and signed by app maintainers, only grain content can be modified.
Wekan at Sandstorm runs in sandboxed grain, it does not have access elsewhere without user-visible
PowerBox request or opening randomly-generated API key URL.
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/).
For Sandstorm specific security issues you can contact [kentonv](https://github.com/kentonv) by email.
For Sandstorm specific security issues you can contact [kentonv](https://github.com/kentonv) by email.
## What Wekan bugs are eligible?
@@ -276,16 +281,11 @@ a security issue, we'd like to know about it, and also how to fix it:
## What Wekan bugs are NOT eligible?
Typical already known or "no impact" bugs such as:
Typical already-known or 'no impact' bugs such as:
- [Wekan API old tokens not replaced correctly](https://github.com/wekan/wekan/issues/1437)
- Missing Cookie flags on non-session cookies or 3rd party cookies
- Logout CSRF
- Social engineering
- Denial of service
- SSL BEAST/CRIME/etc. Wekan does not have SSL built-in, it uses Caddy/Nginx/Apache etc at front.
Integrated Caddy support is updated often.
- Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server.
- SSL BEAST/CRIME/etc. WeKan does not have SSL built-in; it uses Caddy/Nginx/Apache on the front end.
Wekan is Open Source with MIT license, and free to use also for commercial use.
We welcome all fixes to improve security by email to security@wekan.fi
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v8.80.0"
appVersion: "v9.35.0"
files:
userUploads:
- README.md
+31 -22
View File
@@ -25,19 +25,44 @@ syntax = """=== Wekan API Python CLI: Shows IDs for addcard ===
# AUTHORID is USERID that writes card or custom field.
If *nix: chmod +x api.py => ./api.py users
Syntax:
User API:
User API:
python3 api.py user # Current user and list of current user boards
python3 api.py users # All users
python3 api.py boards # All Public Boards
python3 api.py boards USERID # Boards of USERID
python3 api.py board BOARDID # Info of BOARDID
Board/List/Card API:
python3 api.py swimlanes BOARDID # Swimlanes of BOARDID
python3 api.py lists BOARDID # Lists of BOARDID
python3 api.py list BOARDID LISTID # Info of LISTID
python3 api.py createlist BOARDID LISTTITLE # Create list
python3 api.py addcard AUTHORID BOARDID SWIMLANEID LISTID CARDTITLE CARDDESCRIPTION
python3 api.py editcard BOARDID LISTID CARDID NEWCARDTITLE NEWCARDDESCRIPTION
python3 api.py getcard BOARDID LISTID CARDID # Get card info
python3 api.py cardsbyswimlane BOARDID SWIMLANEID # Retrieve cards list on a swimlane
python3 api.py deleteallcards BOARDID SWIMLANEID ( * Be careful will delete ALL CARDS INSIDE the swimlanes automatically in every list * ) # Delete all cards on a swimlane
python3 api.py get_list_cards_count BOARDID LISTID # Retrieve how many cards in a list
python3 api.py get_board_cards_count BOARDID # Retrieve how many cards in a board
python3 api.py editboardtitle BOARDID NEWBOARDTITLE # Edit board title
python3 api.py copyboard BOARDID NEWBOARDTITLE # Copy a board
Custom Fields / Labels API:
python3 api.py customfields BOARDID # Custom Fields of BOARDID
python3 api.py customfield BOARDID CUSTOMFIELDID # Info of CUSTOMFIELDID
python3 api.py addcustomfieldtoboard AUTHORID BOARDID NAME TYPE SETTINGS SHOWONCARD AUTOMATICALLYONCARD SHOWLABELONMINICARD SHOWSUMATTOPOFLIST # Add Custom Field to Board
python3 api.py editcustomfield BOARDID LISTID CARDID CUSTOMFIELDID NEWCUSTOMFIELDVALUE # Edit Custom Field
python3 api.py createlabel BOARDID LABELCOLOR LABELNAME (Color available: `white`, `green`, `yellow`, `orange`, `red`, `purple`, `blue`, `sky`, `lime`, `pink`, `black`, `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`, `slateblue`, `magenta`, `gold`, `navy`, `gray`, `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`) # Create a new label
python3 api.py addlabel BOARDID LISTID CARDID LABELID # Add label to a card
python3 api.py addcardwithlabel AUTHORID BOARDID SWIMLANEID LISTID CARDTITLE CARDDESCRIPTION LABELIDS # Add a card and a label
python3 api.py editcardcolor BOARDID LISTID CARDID COLOR (Color available: `white`, `green`, `yellow`, `orange`, `red`, `purple`, `blue`, `sky`, `lime`, `pink`, `black`, `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`, `slateblue`, `magenta`, `gold`, `navy`, `gray`, `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`) # Edit card color
Checklist API:
python3 api.py addchecklist BOARDID CARDID TITLE ITEM1 ITEM2 ITEM3 ITEM4 (You can add multiple items or just one, or also without any item, just TITLE works as well. * If items or Title contains spaces, you should add ' between them.) # Add checklist + item on a card
python3 api.py checklistid BOARDID CARDID # Retrieve Checklist ID attached to a card
python3 api.py checklistinfo BOARDID CARDID CHECKLISTID # Get checklist info
Attachment API:
python3 api.py listattachments BOARDID # List attachments
python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND] # Upload attachment to card
python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH # Download attachment to local file
@@ -45,25 +70,9 @@ If *nix: chmod +x api.py => ./api.py users
python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID # List attachments for specific card
python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move] # Copy or move attachment
python3 api.py deleteattachment ATTACHMENTID # Delete attachment
python3 api.py cardsbyswimlane SWIMLANEID LISTID # Retrieve cards list on a swimlane
python3 api.py getcard BOARDID LISTID CARDID # Get card info
python3 api.py addlabel BOARDID LISTID CARDID LABELID # Add label to a card
python3 api.py addcardwithlabel AUTHORID BOARDID SWIMLANEID LISTID CARDTITLE CARDDESCRIPTION LABELIDS # Add a card and a label
python3 api.py editboardtitle BOARDID NEWBOARDTITLE # Edit board title
python3 api.py copyboard BOARDID NEWBOARDTITLE # Copy a board
python3 api.py createlabel BOARDID LABELCOLOR LABELNAME (Color available: `white`, `green`, `yellow`, `orange`, `red`, `purple`, `blue`, `sky`, `lime`, `pink`, `black`, `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`, `slateblue`, `magenta`, `gold`, `navy`, `gray`, `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`) # Create a new label
python3 api.py editcardcolor BOARDID LISTID CARDID COLOR (Color available: `white`, `green`, `yellow`, `orange`, `red`, `purple`, `blue`, `sky`, `lime`, `pink`, `black`, `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`, `slateblue`, `magenta`, `gold`, `navy`, `gray`, `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`) # Edit card color
python3 api.py addchecklist BOARDID CARDID TITLE ITEM1 ITEM2 ITEM3 ITEM4 (You can add multiple items or just one, or also without any item, just TITLE works as well. * If items or Title contains spaces, you should add ' between them.) # Add checklist + item on a card
python3 api.py deleteallcards BOARDID SWIMLANEID ( * Be careful will delete ALL CARDS INSIDE the swimlanes automatically in every list * ) # Delete all cards on a swimlane
python3 api.py checklistid BOARDID CARDID # Retrieve Checklist ID attached to a card
python3 api.py checklistinfo BOARDID CARDID CHECKLISTID # Get checklist info
python3 api.py get_list_cards_count BOARDID LISTID # Retrieve how many cards in a list
python3 api.py get_board_cards_count BOARDID # Retrieve how many cards in a board
Admin API:
python3 api.py users # All users
python3 api.py boards # All Public Boards
python3 api.py newuser USERNAME EMAIL PASSWORD
"""
@@ -723,9 +732,9 @@ if arguments == 2:
print(f"Error: {e}")
# ------- GET BOARD CARDS COUNT END -----------
if arguments == 1:
if arguments >= 1:
if sys.argv[1] == 'users':
if arguments == 1 and sys.argv[1] == 'users':
# ------- LIST OF USERS START -----------
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
@@ -736,7 +745,7 @@ if arguments == 1:
print(data2)
# ------- LIST OF USERS END -----------
if sys.argv[1] == 'user':
if arguments == 1 and sys.argv[1] == 'user':
# ------- LIST OF ALL USERS START -----------
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
print(user)
@@ -746,7 +755,7 @@ if arguments == 1:
print(data2)
# ------- LIST OF ALL USERS END -----------
if sys.argv[1] == 'boards':
if arguments == 1 and sys.argv[1] == 'boards':
# ------- LIST OF PUBLIC BOARDS START -----------
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
@@ -761,7 +770,7 @@ if arguments == 1:
if sys.argv[1] == 'uploadattachment':
# ------- UPLOAD ATTACHMENT START -----------
if arguments < 5:
if arguments < 6:
print("Usage: python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND]")
print("Storage backends: fs, gridfs, s3")
exit(1)
+83 -2
View File
@@ -1,7 +1,41 @@
// PWA
// Fix dynamic-import chunk loading when Wekan runs under a sub-path (ROOT_URL includes a pathname,
// e.g. https://example.com/wekan). Rspack uses __webpack_public_path__ to prefix chunk URLs; by
// default it is set to /build-chunks/ (no context path). We update it here, at module evaluation
// time, before any import() expression runs (those are deferred to Meteor.startup callbacks).
/* global __webpack_public_path__:writable */
try {
const _cfg = typeof window !== 'undefined' && window.__meteor_runtime_config__;
const _rootUrl = _cfg && _cfg.ROOT_URL;
if (_rootUrl) {
const _rootPath = new URL(_rootUrl).pathname.replace(/\/+$/, '');
if (_rootPath && _rootPath !== '/') {
__webpack_public_path__ = _rootPath + '/build-chunks/';
}
}
} catch (_) {}
// Fix bug in jam:offline 0.4.1: s?.message.includes() crashes when s.message is
// undefined (e.g. during WebSocket reconnect errors that don't carry a .message).
// Wrap _debug so the optional chain is complete: s?.message?.includes().
(function patchJamOfflineDebug() {
const patched = Meteor._debug;
Meteor._debug = function (m, s) {
try {
return patched.call(this, m, s);
} catch (e) {
// jam:offline's override threw — fall back to the bare console log.
if (typeof console !== 'undefined') {
console.log(m, s);
}
}
};
})();
// PWA — use Meteor.absoluteUrl so the path is correct under sub-URL deployments
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/pwa-service-worker.js');
const swPath = new URL('pwa-service-worker.js', Meteor.absoluteUrl()).pathname;
navigator.serviceWorker.register(swPath);
});
}
@@ -26,6 +60,22 @@ import { Utils } from '/client/lib/utils';
// Token already lives in localStorage; cookie adds same-origin send-on-request semantics
Meteor.startup(() => {
const COOKIE_NAME = 'meteor_login_token';
const USER_ID_COOKIE = 'meteor_user_id';
const TOKEN_EXPIRES_COOKIE = 'meteor_login_token_expires';
const getCookie = (name) => {
try {
const parts = document.cookie ? document.cookie.split(';') : [];
for (const part of parts) {
const [k, ...rest] = part.trim().split('=');
if (decodeURIComponent(k) === name) {
return decodeURIComponent(rest.join('='));
}
}
} catch (_) {}
return '';
};
const cookieAttrs = () => {
const attrs = ['Path=/', 'SameSite=Lax'];
try {
@@ -53,6 +103,37 @@ Meteor.startup(() => {
}
};
const bootstrapTokenFromCookie = () => {
try {
const currentToken = Accounts && typeof Accounts._storedLoginToken === 'function'
? Accounts._storedLoginToken()
: null;
if (currentToken) {
return;
}
const token = getCookie(COOKIE_NAME);
const userId = getCookie(USER_ID_COOKIE);
const tokenExpiresRaw = getCookie(TOKEN_EXPIRES_COOKIE);
const tokenExpires = tokenExpiresRaw ? new Date(tokenExpiresRaw) : null;
if (!token || !userId || !tokenExpires || Number.isNaN(tokenExpires.getTime())) {
return;
}
if (Accounts && typeof Accounts._storeLoginToken === 'function') {
Accounts._storeLoginToken(userId, token, tokenExpires);
if (Meteor.status && !Meteor.status().connected && typeof Meteor.reconnect === 'function') {
Meteor.reconnect();
}
}
} catch (_) {
// ignore
}
};
bootstrapTokenFromCookie();
// Initial sync on startup
syncCookie();
+9 -3
View File
@@ -258,14 +258,18 @@ Template.activity.helpers({
attachmentLink() {
const attachment = this.activity.attachment();
const attachmentUrl = attachment && typeof attachment.link === 'function'
? attachment.link()
: '';
// trying to display url before file is stored generates js errors
return (
(attachment &&
attachment.path &&
attachmentUrl &&
Blaze.toHTML(
HTML.A(
{
href: `${attachment.link()}?download=true`,
href: `${attachmentUrl}?download=true`,
target: '_blank',
},
sanitizeText(attachment.name),
@@ -290,7 +294,8 @@ Template.activity.helpers({
Template.commentReactions.events({
'click .reaction'(event) {
if (ReactiveCache.getCurrentUser().isBoardMember()) {
const user = ReactiveCache.getCurrentUser();
if (user && user.isBoardMember()) {
const codepoint = event.currentTarget.dataset['codepoint'];
const commentId = Template.instance().data.commentId;
const cardComment = ReactiveCache.getCardComment(commentId);
@@ -302,7 +307,8 @@ Template.commentReactions.events({
Template.addReactionPopup.events({
'click .add-comment-reaction'(event) {
if (ReactiveCache.getCurrentUser().isBoardMember()) {
const user = ReactiveCache.getCurrentUser();
if (user && user.isBoardMember()) {
const codepoint = event.currentTarget.dataset['codepoint'];
const commentId = Template.instance().data.commentId;
const cardComment = ReactiveCache.getCardComment(commentId);
+1 -1
View File
@@ -70,7 +70,7 @@ Template.comment.events({
evt.preventDefault();
const textarea = tpl.find('.js-edit-comment textarea,input[type=text]');
const commentText = textarea && textarea.value ? textarea.value.trim() : '';
const commentId = this._id;
const commentId = tpl.data._id;
if (commentText) {
CardComments.update(commentId, {
$set: {
+1 -1
View File
@@ -64,7 +64,7 @@ template(name="boardBody")
template(name="calendarView")
if isViewCalendar
.calendar-view.swimlane
.calendar-view.swimlane.dragscroll
if currentCard
+cardDetails(currentCard)
+fullcalendar(calendarOptions)
+47 -57
View File
@@ -2,6 +2,7 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { TAPi18n } from '/imports/i18n';
import dragscroll from '@wekanteam/dragscroll';
import '/client/lib/dragscrollTouch';
import { boardConverter } from '/client/lib/boardConverter';
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
import Swimlanes from '/models/swimlanes';
@@ -19,23 +20,26 @@ let BoardBody = null;
Template.board.onCreated(function () {
this.isBoardReady = new ReactiveVar(false);
// Kept for the template/jade which still references it; conversion is a no-op.
this.isConverting = new ReactiveVar(false);
this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes
this._boardProcessed = false; // Track if board has been processed
this._lastProcessedBoardId = null; // Track last processed board ID
this._swimlaneCreated = new Set(); // boards where a default swimlane was ensured
// Ensure a board has at least one swimlane. Guarded to run once per board and
// fire-and-forget so it never blocks rendering. The board is marked
// immediately to avoid duplicate concurrent calls, and the mark is rolled
// back on failure so a later attempt can retry.
this.ensureDefaultSwimlane = async (boardId) => {
// Only create swimlane once per board
if (this._swimlaneCreated.has(boardId)) {
return;
}
this._swimlaneCreated.add(boardId);
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) return;
const swimlanes = board.swimlanes();
if (swimlanes.length === 0) {
if (!board) {
this._swimlaneCreated.delete(boardId);
return;
}
if (board.swimlanes().length === 0) {
const swimlaneId = await Meteor.callAsync('ensureDefaultSwimlane', boardId);
if (process.env.DEBUG === 'true' && swimlaneId) {
console.log(
@@ -43,67 +47,41 @@ Template.board.onCreated(function () {
);
}
}
this._swimlaneCreated.add(boardId);
} catch (error) {
this._swimlaneCreated.delete(boardId); // allow a later retry
console.error('Error creating default swimlane:', error);
}
};
this.checkAndConvertBoard = async (boardId) => {
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) {
this.isBoardReady.set(true);
return;
}
this.isBoardReady.set(true);
} catch (error) {
console.error('Error during board conversion check:', error);
this.isConverting.set(false);
this.isBoardReady.set(true); // Show board even if conversion check failed
}
};
// The pattern we use to manually handle data loading is described here:
// https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
// XXX The boardId should be readed from some sort the component "props",
// unfortunatly, Blaze doesn't have this notion.
// Single reactive computation that subscribes to the current board and tracks
// its ready state in one place. Previously a Tracker.autorun was created
// *inside* this autorun on every re-run without being stopped, leaking
// computations that raced on shared ready-state flags. When navigating
// between boards — e.g. opening a freshly created board — that race could
// leave swimlanes/lists unrendered until the board was re-selected. A single
// self-managed autorun (auto-stopped by Blaze on destroy) avoids both the
// leak and the race.
// Pattern: https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
this.autorun(() => {
const currentBoardId = Session.get('currentBoard');
if (!currentBoardId) return;
if (!currentBoardId) {
this.isBoardReady.set(false);
return;
}
const handle = Meteor.subscribe('board', currentBoardId, false);
const ready = handle.ready();
this.isBoardReady.set(ready);
// Use a separate autorun for subscription ready state to avoid reactive loops
this.subscriptionReadyAutorun = Tracker.autorun(() => {
if (handle.ready()) {
if (
!this._boardProcessed ||
this._lastProcessedBoardId !== currentBoardId
) {
this._boardProcessed = true;
this._lastProcessedBoardId = currentBoardId;
// Ensure default swimlane exists (only once per board)
this.ensureDefaultSwimlane(currentBoardId);
// Check if board needs conversion
this.checkAndConvertBoard(currentBoardId);
}
} else {
this.isBoardReady.set(false);
}
});
if (ready) {
// Run outside the computation so this autorun depends only on the board
// id and the subscription's ready state — not on the swimlane data that
// ensureDefaultSwimlane reads — which would otherwise cause extra re-runs.
Tracker.nonreactive(() => this.ensureDefaultSwimlane(currentBoardId));
}
});
});
Template.board.onDestroyed(function () {
// Clean up the subscription ready autorun to prevent memory leaks
if (this.subscriptionReadyAutorun) {
this.subscriptionReadyAutorun.stop();
}
});
Template.board.helpers({
onlyShowCurrentCard() {
const isMiniScreen = Utils.isMiniScreen();
@@ -142,6 +120,7 @@ Template.board.helpers({
Template.boardBody.onCreated(function () {
Meteor.subscribe('tableVisibilityModeSettings');
Meteor.subscribe('inviteToBoardRolesSettings');
this.showOverlay = new ReactiveVar(false);
this.draggingActive = new ReactiveVar(false);
this._isDragging = false;
@@ -606,6 +585,17 @@ Template.boardBody.onDestroyed(function () {
});
Template.boardBody.helpers({
openCards() {
const isMobile = Utils.getMobileMode();
if (!isMobile) {
const openCardIds = Session.get('openCards') || [];
return openCardIds
.map((id) => ReactiveCache.getCard(id))
.filter((card) => card);
}
return [];
},
draggingActive() {
return Template.instance().draggingActive.get();
},
+14 -5
View File
@@ -3370,18 +3370,19 @@ THEME - Clean Light
.board-color-cleanlight .list,
.board-color-cleandark .list {
background: none;
border-left: none;
background: none !important;
border-left: none !important;
}
.board-color-cleanlight .list .list-header,
.board-color-cleandark .list .list-header {
border-bottom: none;
display: flex;
border-bottom: none !important;
display: flex !important;
justify-content: space-between;
align-items: center;
font-size: 16px;
background: none;
background: none !important;
background-color: transparent !important;
}
.board-color-cleanlight .list .list-header div:has(.list-header-name),
@@ -4279,6 +4280,14 @@ THEME - Clean Light
font-weight: 500;
}
.board-color-cleandark .pop-over-list li > a {
color: rgba(255, 255, 255, 1) !important;
}
.board-color-cleanlight .pop-over-list li > a {
color: rgba(10, 10, 20, 0.85) !important;
}
.board-color-cleanlight .pop-over-list li > a i,
.board-color-cleandark .pop-over-list li > a i {
margin-right: 6px !important;
+136 -135
View File
@@ -1,151 +1,152 @@
template(name="boardHeaderBar")
h1.header-board-menu
with currentBoard
if $eq title 'Templates'
| {{_ 'templates'}}
else
+viewer
= title
unless notDisplayThisBoard
h1.header-board-menu
with currentBoard
if $eq title 'Templates'
| {{_ 'templates'}}
else
+viewer
= title
.board-header-btns.left
unless isMiniScreen
.board-header-btns.left
unless isMiniScreen
if currentBoard
if currentUser
with currentBoard
if currentUser.isBoardAdmin
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
span {{_ currentBoard.permission}}
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
i.fa.fa-eye
if $eq watchLevel "tracking"
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
span {{_ watchLevel}}
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
if showStarCounter
span.board-star-counter {{currentBoard.stars}}
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
span {{_ 'log-in'}}
.board-header-btns.center
.board-header-btns.right
if currentBoard
if currentUser
with currentBoard
if currentUser.isBoardAdmin
if isMiniScreen
if currentUser
with currentBoard
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
span {{_ currentBoard.permission}}
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
i.fa.fa-eye
if $eq watchLevel "tracking"
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
span {{_ watchLevel}}
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
if showStarCounter
span.board-star-counter {{currentBoard.stars}}
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
i.fa.fa-eye
if $eq watchLevel "tracking"
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
span {{_ 'log-in'}}
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
.board-header-btns.center
if isSandstorm
if currentUser
a.board-header-btn.js-open-archived-board
i.fa.fa-archive
.board-header-btns.right
if currentBoard
if isMiniScreen
if currentUser
with currentBoard
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
//if showSort
//
a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
//
i.fa(class="{{directionClass}}")
//
span {{_ 'sort'}}{{_ listSortShortDesc}}
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
i.fa.fa-eye
if $eq watchLevel "tracking"
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
if isSandstorm
if currentUser
a.board-header-btn.js-open-archived-board
i.fa.fa-archive
//if showSort
//
a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
//
i.fa(class="{{directionClass}}")
//
span {{_ 'sort'}}{{_ listSortShortDesc}}
a.board-header-btn.js-open-filter-view(
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
class="{{#if Filter.isActive}}js-filter-active{{/if}}")
i.fa.fa-filter
span {{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}
if Filter.isActive
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
i.fa.fa-search
span {{_ 'search'}}
unless currentBoard.isTemplatesBoard
a.board-header-btn.js-toggle-board-view
i.fa.fa-caret-down
if $eq boardView 'board-view-swimlanes'
i.fa.fa-th-large
if $eq boardView 'board-view-lists'
i.fa.fa-trello
if $eq boardView 'board-view-cal'
i.fa.fa-calendar
if $eq boardView 'board-view-gantt'
i.fa.fa-bar-chart
span
if $eq boardView 'board-view-swimlanes'
| {{_ 'swimlanes'}}
if $eq boardView 'board-view-lists'
| {{_ 'lists'}}
if $eq boardView 'board-view-cal'
| {{_ 'calendar'}}
if $eq boardView 'board-view-gantt'
| {{_ 'gantt'}}
if canModifyBoard
a.board-header-btn.js-multiselection-activate(
title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
i.fa.fa-check-square-o
span {{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}
if MultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
a.board-header-btn.js-open-filter-view(
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
class="{{#if Filter.isActive}}js-filter-active{{/if}}")
i.fa.fa-filter
span {{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}
if Filter.isActive
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
.separator
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
i.fa.fa-bars
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
i.fa.fa-search
span {{_ 'search'}}
unless currentBoard.isTemplatesBoard
a.board-header-btn.js-toggle-board-view
i.fa.fa-caret-down
if $eq boardView 'board-view-swimlanes'
i.fa.fa-th-large
if $eq boardView 'board-view-lists'
i.fa.fa-trello
if $eq boardView 'board-view-cal'
i.fa.fa-calendar
if $eq boardView 'board-view-gantt'
i.fa.fa-bar-chart
span
if $eq boardView 'board-view-swimlanes'
| {{_ 'swimlanes'}}
if $eq boardView 'board-view-lists'
| {{_ 'lists'}}
if $eq boardView 'board-view-cal'
| {{_ 'calendar'}}
if $eq boardView 'board-view-gantt'
| {{_ 'gantt'}}
if canModifyBoard
a.board-header-btn.js-multiselection-activate(
title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
i.fa.fa-check-square-o
span {{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}
if MultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
.separator
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
i.fa.fa-bars
template(name="boardVisibilityList")
ul.pop-over-list
+27 -6
View File
@@ -39,7 +39,22 @@ Template.boardChangeTitlePopup.events({
},
});
Template.boardHeaderBar.onCreated(function () {
Meteor.subscribe('tableVisibilityModeSettings');
});
Template.boardHeaderBar.helpers({
notDisplayThisBoard() {
const allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
const currentBoard = Utils.getCurrentBoard();
return (
allowPrivateVisibilityOnly !== undefined &&
allowPrivateVisibilityOnly.booleanValue &&
currentBoard &&
currentBoard.permission === 'public'
);
},
watchLevel() {
const currentBoard = Utils.getCurrentBoard();
return currentBoard && currentBoard.getWatchLevel(Meteor.userId());
@@ -218,7 +233,7 @@ function createBoardHelpers() {
return Template.instance().visibility.get();
},
notAllowPrivateVisibilityOnly() {
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly')?.booleanValue;
},
visibilityCheck() {
return Template.currentData() === Template.instance().visibility.get();
@@ -288,7 +303,7 @@ async function createBoardSubmit(tpl, event) {
function createBoardEvents() {
return {
'click .js-select-visibility'(event, tpl) {
tpl.visibility.set(Template.currentData());
tpl.visibility.set(this);
tpl.visibilityMenuIsOpen.set(false);
},
'click .js-change-visibility'(event, tpl) {
@@ -347,7 +362,7 @@ Template.headerBarCreateBoardPopup.helpers(createBoardHelpers());
Template.headerBarCreateBoardPopup.events({
'click .js-select-visibility'(event, tpl) {
tpl.visibility.set(Template.currentData());
tpl.visibility.set(this);
tpl.visibilityMenuIsOpen.set(false);
},
'click .js-change-visibility'(event, tpl) {
@@ -366,13 +381,19 @@ Template.headerBarCreateBoardPopup.events({
},
});
Template.boardVisibilityList.helpers({
notAllowPrivateVisibilityOnly() {
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly')?.booleanValue;
},
});
Template.boardChangeVisibilityPopup.onCreated(function () {
Meteor.subscribe('tableVisibilityModeSettings');
});
Template.boardChangeVisibilityPopup.helpers({
notAllowPrivateVisibilityOnly(){
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly')?.booleanValue;
},
visibilityCheck() {
const currentBoard = Utils.getCurrentBoard();
@@ -383,7 +404,7 @@ Template.boardChangeVisibilityPopup.helpers({
Template.boardChangeVisibilityPopup.events({
'click .js-select-visibility'() {
const currentBoard = Utils.getCurrentBoard();
const visibility = Template.currentData();
const visibility = this;
if (typeof visibility === 'string') {
currentBoard.setVisibility(visibility);
Popup.back();
@@ -405,7 +426,7 @@ Template.boardChangeWatchPopup.helpers({
Template.boardChangeWatchPopup.events({
'click .js-select-watch'() {
const level = Template.currentData();
const level = this;
if (typeof level === 'string') {
Meteor.call(
'watch',
+27
View File
@@ -62,6 +62,17 @@
border: 2px dashed #2196F3;
}
/* Highlight valid drop targets while a board is being dragged */
.workspace-node.board-drag-hint > .workspace-node-content {
background: #e8f5e9;
border: 1px dashed #4CAF50;
border-radius: 4px;
}
.boards-left-menu .menu-item a.board-drag-hint {
background: #e8f5e9;
border: 1px dashed #4CAF50;
}
.workspaces-header {
display: flex;
align-items: center;
@@ -950,6 +961,22 @@ body.grey-icons-enabled .checkmark-no-grey {
font-size: inherit !important; /* Inherit the 2x scaling */
}
/* Stack the two-column All Boards layout on small screens. The fixed
"260px 1fr" grid had no mobile collapse, so on narrow non-iPhone browsers
(Fairphone Firefox ESR / Ubuntu Touch Morph, narrow desktop) it overflowed
and the left menu (Starred / Templates / Remaining / Workspaces / + Add
board) was clipped. Width-based, so it applies to every browser. */
.boards-layout {
grid-template-columns: 1fr !important;
}
.boards-left-menu {
border-right: none !important;
border-bottom: 1px solid #e0e0e0;
padding-right: 0 !important;
padding-bottom: 12px;
}
.wrapper .fa, .wrapper .icon {
font-size: 2em !important; /* 2x bigger icons */
}
+4 -4
View File
@@ -95,7 +95,7 @@ template(name="boardList")
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
span.emoji-icon
i.fa.fa-times
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
ul.board-list.clearfix.js-boards.dragscroll(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
li.js-add-board
if isSelectedMenu 'templates'
a.board-list-item.label(title="{{_ 'add-template-container'}}")
@@ -130,7 +130,7 @@ template(name="boardList")
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.board-handle(title="{{_ 'drag-board'}}")
span.board-handle.nodragscroll(title="{{boardWorkspaceDragHint}}")
span.emoji-icon
i.fa.fa-arrows
@@ -158,13 +158,13 @@ template(name="boardList")
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.board-handle(title="{{_ 'drag-board'}}")
span.board-handle.nodragscroll(title="{{boardWorkspaceDragHint}}")
span.emoji-icon
i.fa.fa-arrows
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
span.details
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
span.board-list-item-name(title="{{boardOpenAndMoveHint}}")
+viewer
= title
unless currentSetting.hideBoardMemberList
+51 -8
View File
@@ -2,6 +2,8 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import getSlug from 'limax';
import dragscroll from '@wekanteam/dragscroll';
import '/client/lib/dragscrollTouch';
import TableVisibilityModeSettings from '/models/tableVisibilityModeSettings';
import { BoardMultiSelection } from '/client/lib/boardMultiSelection';
import { EscapeActions } from '/client/lib/escapeActions';
@@ -197,6 +199,13 @@ Template.boardList.onCreated(function () {
});
Template.boardList.onRendered(function () {
// Enable drag-to-scroll on the All Boards list (.board-list.dragscroll), the
// same way the board canvas/swimlanes do. This makes mouse drag-scrolling
// work on desktop; finger drag-scrolling is handled by dragscrollTouch.js.
// The .board-list element is stable across menu/workspace switches (only its
// li children are re-rendered), so a single reset() binding is enough.
dragscroll.reset();
// jQuery sortable is disabled in favor of HTML5 drag-and-drop for space management
// The old sortable code has been removed to prevent conflicts
/* OLD SORTABLE CODE - DISABLED
@@ -354,12 +363,6 @@ Template.boardList.helpers({
if (FlowRouter.getRouteName() === 'home') {
membershipOrs.push({ 'members.userId': Meteor.userId() });
if (
allowPrivateVisibilityOnly !== undefined &&
allowPrivateVisibilityOnly.booleanValue
) {
query.$and.push({ permission: 'private' });
}
const currUser = ReactiveCache.getCurrentUser();
let orgIdsUserBelongs = currUser?.orgIdsUserBelongs() || '';
@@ -539,6 +542,28 @@ Template.boardList.helpers({
hasBoardsSelected() {
return BoardMultiSelection.count() > 0;
},
boardWorkspaceDragHint() {
const remaining = TAPi18n.__('allboards.remaining') || 'Remaining';
const workspaces = TAPi18n.__('allboards.workspaces') || 'Workspaces';
return (
TAPi18n.__('drag-board-to-workspace', { remaining, workspaces }) ||
`Drag board to assign to ${workspaces} (drop on workspace in left sidebar)`
);
},
boardOpenAndMoveHint() {
const remaining = TAPi18n.__('allboards.remaining') || 'Remaining';
const workspaces = TAPi18n.__('allboards.workspaces') || 'Workspaces';
return (
TAPi18n.__(
'board-open-and-move-between-remaining-and-workspaces',
{
remaining,
workspaces,
},
) ||
`Click board icon to open board. Drag board between ${remaining} and ${workspaces}.`
);
},
});
Template.workspaceTree.helpers({
@@ -642,6 +667,20 @@ Template.boardList.events({
evt.originalEvent.dataTransfer.setData('text/plain', boardId);
} catch (e) {}
}
// Highlight valid drop targets in the sidebar so users know where to drop
document.querySelectorAll('.workspace-node').forEach((el) => {
el.classList.add('board-drag-hint');
});
document.querySelectorAll('.js-select-menu').forEach((el) => {
if (el.getAttribute('data-type') === 'remaining') {
el.classList.add('board-drag-hint');
}
});
},
'dragend .js-board'() {
document.querySelectorAll('.workspace-node.board-drag-hint, .js-select-menu.board-drag-hint').forEach((el) => {
el.classList.remove('board-drag-hint');
});
},
'click .js-clone-board'(evt) {
if (confirm(TAPi18n.__('duplicate-board-confirm'))) {
@@ -675,7 +714,9 @@ Template.boardList.events({
'click .js-archive-board'(evt) {
if (confirm(TAPi18n.__('archive-board-confirm'))) {
const boardId = this._id;
Meteor.call('archiveBoard', boardId);
Meteor.call('archiveBoard', boardId, (err) => {
if (err) alert(err?.reason || err?.message || 'Failed to archive board');
});
evt.preventDefault();
}
},
@@ -718,7 +759,9 @@ Template.boardList.events({
confirm(TAPi18n.__('archive-board-confirm'))
) {
selectedBoards.forEach((boardId) => {
Meteor.call('archiveBoard', boardId);
Meteor.call('archiveBoard', boardId, (err) => {
if (err) alert(err?.reason || err?.message || 'Failed to archive board');
});
});
BoardMultiSelection.reset();
}
-17
View File
@@ -125,23 +125,6 @@ template(name="attachmentActionsPopup")
else
| {{_ 'add-background-image'}}
if $neq versions.original.storage "fs"
a.js-move-storage-fs
i.fa.fa-arrow-right
| {{_ 'attachment-move-storage-fs'}}
if $neq versions.original.storage "gridfs"
if versions.original.storage
a.js-move-storage-gridfs
i.fa.fa-arrow-right
| {{_ 'attachment-move-storage-gridfs'}}
if $neq versions.original.storage "s3"
if versions.original.storage
a.js-move-storage-s3
i.fa.fa-arrow-right
| {{_ 'attachment-move-storage-s3'}}
template(name="attachmentRenamePopup")
input.js-edit-attachment-name(type='text' autofocus value="{{getNameWithoutExtension}}" dir="auto")
.edit-controls.clearfix
+44 -18
View File
@@ -7,6 +7,7 @@ import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManag
import Attachments from '/models/attachments';
import { Utils } from '/client/lib/utils';
import { formatDateTime } from '/imports/lib/dateUtils';
import { EscapeActions } from '/client/lib/escapeActions';
const { filesize } = require('filesize');
import prettyMilliseconds from 'pretty-ms';
@@ -44,6 +45,10 @@ Template.attachmentGallery.events({
},
'click .js-rename': Popup.open('attachmentRename'),
'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete', async function() {
const card = this.meta && this.meta.cardId ? ReactiveCache.getCard(this.meta.cardId) : null;
if (card && card.coverId === this._id) {
await card.unsetCover();
}
await Attachments.removeAsync(this._id);
Popup.back();
}),
@@ -84,6 +89,10 @@ function attachmentCanBeOpened(attachment) {
);
}
function getAttachmentUrl(attachment) {
return attachment && typeof attachment.link === 'function' ? attachment.link() : '';
}
function openAttachmentViewer(attachmentId) {
const attachment = ReactiveCache.getAttachment(attachmentId);
@@ -100,18 +109,18 @@ function openAttachmentViewer(attachmentId) {
*/
switch(true){
case (attachment.isImage):
$("#image-viewer").attr("src", attachment.link());
$("#image-viewer").attr("src", getAttachmentUrl(attachment));
$("#image-viewer").removeClass("hidden");
break;
case (attachment.isPDF):
$("#pdf-viewer").attr("data", attachment.link());
$("#pdf-viewer").attr("data", getAttachmentUrl(attachment));
$("#pdf-viewer").removeClass("hidden");
break;
case (attachment.isVideo):
// We have to create a new <source> DOM element and append it to the video
// element, otherwise the video won't load
let videoSource = document.createElement('source');
videoSource.setAttribute('src', attachment.link());
videoSource.setAttribute('src', getAttachmentUrl(attachment));
$("#video-viewer").append(videoSource);
$("#video-viewer").removeClass("hidden");
@@ -120,14 +129,14 @@ function openAttachmentViewer(attachmentId) {
// We have to create a new <source> DOM element and append it to the audio
// element, otherwise the audio won't load
let audioSource = document.createElement('source');
audioSource.setAttribute('src', attachment.link());
audioSource.setAttribute('src', getAttachmentUrl(attachment));
$("#audio-viewer").append(audioSource);
$("#audio-viewer").removeClass("hidden");
break;
case (attachment.isText):
case (attachment.isJSON):
$("#txt-viewer").attr("data", attachment.link());
$("#txt-viewer").attr("data", getAttachmentUrl(attachment));
$("#txt-viewer").removeClass("hidden");
break;
}
@@ -262,6 +271,24 @@ Template.attachmentViewer.events({
});
Template.attachmentGallery.helpers({
attachments() {
const card = Template.currentData();
if (!card) return [];
const cardId = typeof card.getRealId === 'function' ? card.getRealId() : card._id;
if (!cardId) return [];
const filesCursor = Attachments.find(
{ 'meta.cardId': cardId },
{ sort: { uploadedAt: -1 } },
);
// Call fetch() on the underlying Mongo cursor to establish a reactive
// dependency directly inside this Blaze computation. Without this,
// .each() (which calls cursor.map()) would not register reactivity and
// newly-uploaded attachments would not appear until a page reload.
if (filesCursor && filesCursor.cursor) {
filesCursor.cursor.fetch();
}
return filesCursor.each();
},
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
@@ -314,6 +341,7 @@ Template.cardAttachmentsPopup.helpers({
Template.cardAttachmentsPopup.events({
async 'change .js-attach-file'(event, templateInstance) {
event.stopPropagation();
const card = this;
const files = event.currentTarget.files;
if (files) {
@@ -336,6 +364,10 @@ Template.cardAttachmentsPopup.events({
}
},
'click .js-computer-upload'(event, templateInstance) {
// Prevent the click fired when the OS file-picker dialog closes from
// triggering EscapeActions and closing the popup before the upload starts.
// Same pattern as swimlanes.js after drag operations.
EscapeActions.preventNextClick();
templateInstance.find('.js-attach-file').click();
event.preventDefault();
},
@@ -400,6 +432,11 @@ export async function handleFileUpload(card, files) {
fileName: fileName,
meta: Utils.getCommonAttachmentMetaFrom(card),
chunkSize: 'dynamic',
// Use HTTP transport instead of DDP so file chunks go over a dedicated
// fetch POST rather than flooding the WebSocket/DDP channel. DDP
// upload causes repeated DDP reconnects in Safari, showing the
// "Loading, please wait" offline banner and stalling progress at ~95%.
transport: 'http',
};
config.meta.fileId = fileId;
@@ -491,6 +528,7 @@ Template.previewClipboardImagePopup.events({
meta: Utils.getCommonAttachmentMetaFrom(card),
fileName: file.name || file.type.replace('image/', 'clipboard.'),
chunkSize: 'dynamic',
transport: 'http',
};
config.meta.fileId = fileId;
const uploader = await Attachments.insertAsync(
@@ -550,23 +588,11 @@ Template.attachmentActionsPopup.events({
Utils.reload();
event.preventDefault();
},
'click .js-move-storage-fs'() {
Meteor.call('moveAttachmentToStorage', this._id, "fs");
Popup.back();
},
'click .js-move-storage-gridfs'() {
Meteor.call('moveAttachmentToStorage', this._id, "gridfs");
Popup.back();
},
'click .js-move-storage-s3'() {
Meteor.call('moveAttachmentToStorage', this._id, "s3");
Popup.back();
},
});
Template.attachmentRenamePopup.helpers({
getNameWithoutExtension() {
const ret = this.name.replace(new RegExp("\." + this.extension + "$"), "");
const ret = this.name.replace(new RegExp("\\." + this.extension + "$"), "");
return ret;
},
});
+1 -1
View File
@@ -170,7 +170,7 @@ Template['cardCustomField-date'].helpers({
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
showISODate() {
+17 -19
View File
@@ -146,14 +146,12 @@ function cardDateHelpers(extraHelpers) {
},
showWeekOfYear() {
const user = ReactiveCache.getCurrentUser();
if (!user) {
return false;
}
if (!user) return window.localStorage.getItem('showWeekOfYear') === 'true';
return user.isShowWeekOfYear();
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
showISODate() {
@@ -196,7 +194,7 @@ Template.cardReceivedDate.helpers(cardDateHelpers({
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
},
@@ -237,7 +235,7 @@ Template.cardStartDate.helpers(cardDateHelpers({
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
},
@@ -286,7 +284,7 @@ Template.cardDueDate.helpers(cardDateHelpers({
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
},
@@ -356,7 +354,7 @@ Template.cardCustomFieldDate.helpers(cardDateHelpers({
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${formattedDate}`;
},
@@ -400,13 +398,13 @@ Template.minicardReceivedDate.helpers(cardDateHelpers({
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
}));
@@ -446,13 +444,13 @@ Template.minicardStartDate.helpers(cardDateHelpers({
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
}));
@@ -500,13 +498,13 @@ Template.minicardDueDate.helpers(cardDateHelpers({
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
}));
@@ -549,7 +547,7 @@ Template.minicardEndDate.helpers(cardDateHelpers({
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
}));
@@ -570,13 +568,13 @@ Template.minicardCustomFieldDate.onCreated(function () {
Template.minicardCustomFieldDate.helpers(cardDateHelpers({
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${formattedDate}`;
},
@@ -602,7 +600,7 @@ Template.voteEndDate.helpers(cardDateHelpers({
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
showTitle() {
@@ -630,7 +628,7 @@ Template.pokerEndDate.helpers(cardDateHelpers({
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
showTitle() {
+147 -146
View File
@@ -66,7 +66,7 @@ template(name="cardDetails")
+viewer
if currentBoard.allowsCardNumber
span.card-number
| ##{getCardNumber}
| ##{getCardNumber} &nbsp;
= getTitle
if isWatching
i.card-details-watch
@@ -297,7 +297,7 @@ template(name="cardDetails")
i.fa.fa-list
| {{_ 'list'}}
select.js-select-card-details-lists(disabled="{{#unless canModifyCard}}disabled{{/unless}}")
each currentBoard.lists
each currentSwimlaneListsSorted
option(value="{{_id}}" selected="{{#if isCurrentListId _id}}selected{{/if}}") {{title}}
//.card-details-items
@@ -624,19 +624,18 @@ template(name="cardDetails")
+attachmentGallery
hr
unless currentUser.isNoComments
if currentBoard.allowsComments
.comment-title
h3.card-details-item-title
i.fa.fa-comment-o
| {{_ 'comments'}}
if currentBoard.allowsComments
if currentUser.isBoardMember
unless currentUser.isNoComments
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
+commentForm
+comments
if currentUser.isBoardMember
unless currentUser.isNoComments
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
+commentForm
+comments
hr
.card-details-right
@@ -691,154 +690,156 @@ template(name="editCardSortOrderForm")
a.js-close-inlined-form
template(name="cardDetailsActionsPopup")
ul.pop-over-list
li
a.js-toggle-watch-card
if isWatching
i.fa.fa-eye
| {{_ 'unwatch'}}
else
i.fa.fa-eye
| {{_ 'watch'}}
hr
if canModifyCard
unless currentUser.isWorker
ul.pop-over-list
//li: a.js-members {{_ 'card-edit-members'}}
//li: a.js-labels {{_ 'card-edit-labels'}}
//li: a.js-attachments {{_ 'card-edit-attachments'}}
li
a.js-start-voting
i.fa.fa-thumbs-up
| {{_ 'card-edit-voting'}}
li
a.js-start-planning-poker
i.fa.fa-thumbs-up
| {{_ 'card-edit-planning-poker'}}
if currentUser.isBoardAdmin
li
a.js-custom-fields
i.fa.fa-list
| {{_ 'card-edit-custom-fields'}}
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
//li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
//li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
li
a.js-spent-time
i.fa.fa-clock-o
| {{_ 'editCardSpentTimePopup-title'}}
li
a.js-set-card-color
i.fa.fa-paint-brush
| {{_ 'setCardColorPopup-title'}}
li
a.js-toggle-show-list-on-minicard
if showListOnMinicard
i.fa.fa-eye
| {{_ 'hide-list-on-minicard'}}
else
i.fa.fa-eye
| {{_ 'show-list-on-minicard'}}
if canModifyCard
if currentUser
ul.pop-over-list
li
a.js-toggle-watch-card
if isWatching
i.fa.fa-eye
| {{_ 'unwatch'}}
else
i.fa.fa-eye
| {{_ 'watch'}}
hr
else
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
hr
if canModifyCard
unless currentUser.isWorker
ul.pop-over-list
//li: a.js-members {{_ 'card-edit-members'}}
//li: a.js-labels {{_ 'card-edit-labels'}}
//li: a.js-attachments {{_ 'card-edit-attachments'}}
li
a.js-start-voting
i.fa.fa-thumbs-up
| {{_ 'card-edit-voting'}}
li
a.js-start-planning-poker
i.fa.fa-thumbs-up
| {{_ 'card-edit-planning-poker'}}
if currentUser.isBoardAdmin
li
a.js-custom-fields
i.fa.fa-list
| {{_ 'card-edit-custom-fields'}}
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
//li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
//li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
li
a.js-spent-time
i.fa.fa-clock-o
| {{_ 'editCardSpentTimePopup-title'}}
li
a.js-set-card-color
i.fa.fa-paint-brush
| {{_ 'setCardColorPopup-title'}}
li
a.js-toggle-show-list-on-minicard
if showListOnMinicard
i.fa.fa-eye
| {{_ 'hide-list-on-minicard'}}
else
i.fa.fa-eye
| {{_ 'show-list-on-minicard'}}
if canModifyCard
hr
else
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
hr
ul.pop-over-list
li
a.js-export-card
i.fa.fa-upload
| {{_ 'export-card'}}
unless canModifyCard
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
hr
ul.pop-over-list
li
a.js-move-card-to-top
i.fa.fa-arrow-up
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
i.fa.fa-arrow-down
| {{_ 'moveCardToBottom-title'}}
hr
ul.pop-over-list
if currentUser.isBoardAdmin
li
a.js-move-card
i.fa.fa-arrow-right
| {{_ 'moveCardPopup-title'}}
unless currentUser.isWorker
li
a.js-copy-card
i.fa.fa-clipboard
| {{_ 'copyCardPopup-title'}}
unless currentUser.isWorker
ul.pop-over-list
li
a.js-copy-checklist-cards
i.fa.fa-copy
| {{_ 'copyManyCardsPopup-title'}}
unless archived
hr
ul.pop-over-list
li
a.js-archive
i.fa.fa-archive
| {{_ 'archive-card'}}
if currentUser
unless canModifyCard
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
hr
ul.pop-over-list
li
a.js-more
span.emoji-icon
i.fa.fa-link
| {{_ 'cardMorePopup-title'}}
if canModifyCard
hr
ul.pop-over-list
li
a.js-move-card-to-top
i.fa.fa-arrow-up
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
i.fa.fa-arrow-down
| {{_ 'moveCardToBottom-title'}}
hr
ul.pop-over-list
if currentUser.isBoardAdmin
li
a.js-move-card
i.fa.fa-arrow-right
| {{_ 'moveCardPopup-title'}}
unless currentUser.isWorker
li
a.js-copy-card
i.fa.fa-clipboard
| {{_ 'copyCardPopup-title'}}
unless currentUser.isWorker
ul.pop-over-list
li
a.js-copy-checklist-cards
i.fa.fa-copy
| {{_ 'copyManyCardsPopup-title'}}
unless archived
hr
ul.pop-over-list
li
a.js-archive
i.fa.fa-archive
| {{_ 'archive-card'}}
a.js-move-card-to-top
i.fa.fa-arrow-up
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
i.fa.fa-arrow-down
| {{_ 'moveCardToBottom-title'}}
hr
ul.pop-over-list
if currentUser.isBoardAdmin
li
a.js-move-card
i.fa.fa-arrow-right
| {{_ 'moveCardPopup-title'}}
unless currentUser.isWorker
li
a.js-copy-card
i.fa.fa-clipboard
| {{_ 'copyCardPopup-title'}}
unless currentUser.isWorker
ul.pop-over-list
li
a.js-copy-checklist-cards
i.fa.fa-copy
| {{_ 'copyManyCardsPopup-title'}}
unless archived
hr
ul.pop-over-list
li
a.js-archive
i.fa.fa-archive
| {{_ 'archive-card'}}
hr
ul.pop-over-list
li
a.js-more
span.emoji-icon
i.fa.fa-link
| {{_ 'cardMorePopup-title'}}
if canModifyCard
hr
ul.pop-over-list
li
a.js-more
span.emoji-icon
i.fa.fa-link
| {{_ 'cardMorePopup-title'}}
a.js-move-card-to-top
i.fa.fa-arrow-up
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
i.fa.fa-arrow-down
| {{_ 'moveCardToBottom-title'}}
hr
ul.pop-over-list
if currentUser.isBoardAdmin
li
a.js-move-card
i.fa.fa-arrow-right
| {{_ 'moveCardPopup-title'}}
unless currentUser.isWorker
li
a.js-copy-card
i.fa.fa-clipboard
| {{_ 'copyCardPopup-title'}}
unless currentUser.isWorker
ul.pop-over-list
li
a.js-copy-checklist-cards
i.fa.fa-copy
| {{_ 'copyManyCardsPopup-title'}}
unless archived
hr
ul.pop-over-list
li
a.js-archive
i.fa.fa-archive
| {{_ 'archive-card'}}
hr
ul.pop-over-list
li
a.js-more
span.emoji-icon
i.fa.fa-link
| {{_ 'cardMorePopup-title'}}
template(name="exportCardPopup")
ul.pop-over-list
+54 -9
View File
@@ -121,6 +121,16 @@ Template.cardDetails.onCreated(function () {
};
Meteor.subscribe('unsaved-edits');
// Surface legacy CollectionFS attachments for the current board so they show
// up in the card's attachment gallery and can be read in place (without
// migrating them to Meteor-Files first).
this.autorun(() => {
const board = Utils.getCurrentBoard();
if (board && board._id) {
Meteor.subscribe('legacyBoardAttachments', board._id);
}
});
});
Template.cardDetails.onRendered(function () {
@@ -268,7 +278,11 @@ Template.cardDetails.helpers({
},
cardMaximized() {
return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized();
const currentUser = ReactiveCache.getCurrentUser();
const maximized = currentUser
? currentUser.hasCardMaximized()
: window.localStorage.getItem('cardMaximized') === 'true';
return !Utils.getPopupCardId() && maximized;
},
showActivities() {
@@ -336,11 +350,27 @@ Template.cardDetails.helpers({
return user && user.isVerticalScrollbars();
},
currentSwimlaneListsSorted() {
const card = Template.currentData();
if (!card || !card.boardId) return [];
const board = ReactiveCache.getBoard(card.boardId);
if (!board) return [];
const swimlaneId = card.swimlaneId;
const selector = { boardId: card.boardId, archived: false };
if (swimlaneId) {
const defaultSwimlane = board.getDefaultSwimline && board.getDefaultSwimline();
if (defaultSwimlane && defaultSwimlane._id === swimlaneId) {
selector.swimlaneId = { $in: [swimlaneId, null, ''] };
} else {
selector.swimlaneId = swimlaneId;
}
}
return ReactiveCache.getLists(selector, { sort: { sort: 1 } });
},
isCurrentListId(listId) {
// When called from within 'each currentBoard.lists' block, we need the card from parent context
let data = Template.currentData();
if (!data || typeof data.listId === 'undefined') {
// Try parent context (for use within each loops)
data = Template.parentData(1);
}
if (!data || typeof data.listId === 'undefined') return false;
@@ -480,7 +510,11 @@ Template.cardDetails.events({
},
'change .js-date-format-selector'(event) {
const dateFormat = event.target.value;
Meteor.call('changeDateFormat', dateFormat);
if (Meteor.userId()) {
Meteor.call('changeDateFormat', dateFormat);
} else {
window.localStorage.setItem('dateFormat', dateFormat);
}
},
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
// Mobile: switch to desktop popup view (maximize)
@@ -614,11 +648,19 @@ Template.cardDetails.events({
Meteor.call('toggleCustomFieldsGrid');
},
'click .js-maximize-card-details'() {
Meteor.call('toggleCardMaximized');
if (Meteor.userId()) {
Meteor.call('toggleCardMaximized');
} else {
window.localStorage.setItem('cardMaximized', 'true');
}
autosize($('.card-details'));
},
'click .js-minimize-card-details'() {
Meteor.call('toggleCardMaximized');
if (Meteor.userId()) {
Meteor.call('toggleCardMaximized');
} else {
window.localStorage.setItem('cardMaximized', 'false');
}
autosize($('.card-details'));
},
'click .js-vote'(e) {
@@ -775,7 +817,10 @@ Template.cardDetails.helpers({
},
isDateFormat(format) {
const currentUser = ReactiveCache.getCurrentUser();
if (!currentUser) return format === 'YYYY-MM-DD';
if (!currentUser) {
const stored = window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD';
return format === stored;
}
return currentUser.getDateFormat() === format;
},
// Upload progress helpers
@@ -1858,8 +1903,8 @@ EscapeActions.register(
Session.set('cardDetailsIsMouseDown', false);
} else {
// Prevent close card when the user is selecting text and moves the mouse cursor outside the card detail area
Utils.goBoardId(Session.get('currentBoard'));
// Trigger the close button so all close logic (openCards, routing, mini-screen) runs consistently
$('.js-close-card-details').first().trigger('click');
}
},
() => {
+23 -1
View File
@@ -34,7 +34,7 @@ template(name="checklists")
template(name="checklistDetail")
.js-checklist.checklist.nodragscroll
+inlinedForm(classNames="js-edit-checklist-title" checklist = checklist)
+editChecklistItemForm(checklist = checklist)
+editChecklistForm(checklist = checklist)
else
.checklist-title
span
@@ -85,6 +85,19 @@ template(name="addChecklistItemForm")
label.toggle-label(for="toggleNewlineBecomesNewChecklistItemOriginOrder")
| {{_ 'originOrder'}}
template(name="editChecklistForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
textarea.js-edit-checklist-item(rows='1' autofocus dir="auto")
if $eq type 'item'
= item.title
else
= checklist.title
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
span(title=createdAt) {{ displayDate createdAt }}
template(name="editChecklistItemForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
@@ -171,6 +184,15 @@ template(name="checklistActionsPopup")
else
input.toggle-switch(type="checkbox" id="toggleHideAllChecklistItems_{{checklist._id}}")
label.toggle-label(for="toggleHideAllChecklistItems_{{checklist._id}}")
a.js-show-checklist-at-minicard
i.fa.fa-th-list
| {{_ "show-on-minicard"}} ...
.material-toggle-switch(title="{{_ 'show-on-minicard'}}")
if checklist.showChecklistAtMinicard
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}" checked="checked")
else
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}")
label.toggle-label(for="toggleShowChecklistAtMinicard_{{checklist._id}}")
template(name="copyChecklistPopup")
+copyAndMoveChecklist
+10 -2
View File
@@ -135,8 +135,11 @@ Template.checklists.events({
event.preventDefault();
const textarea = tpl.find('textarea.js-edit-checklist-item');
const title = textarea.value.trim();
const checklist = Template.currentData().checklist;
checklist.setTitle(title);
const formData = Blaze.getData(event.currentTarget) || Blaze.getData(event.target);
const checklist = formData?.checklist;
if (checklist) {
checklist.setTitle(title);
}
},
'submit .js-add-checklist-item'(event, tpl) {
event.preventDefault();
@@ -271,6 +274,11 @@ Template.checklistActionsPopup.events({
Template.currentData().checklist.toggleHideAllChecklistItems();
Popup.back();
},
'click .js-show-checklist-at-minicard'(event) {
event.preventDefault();
Template.currentData().checklist.toggleShowChecklistAtMinicard();
Popup.back();
},
});
Template.editChecklistItemForm.onRendered(function () {
+33 -30
View File
@@ -3,7 +3,7 @@
position: relative;
display: flex;
align-items: center;
margin-bottom: 1.2vh;
margin-bottom: 8px;
}
.minicard-wrapper.placeholder {
background: #ccc;
@@ -16,8 +16,8 @@
}
.minicard-wrapper.ui-sortable-helper .and-n-other {
width: 100%;
height: 2.5vh;
padding: 0.5vh;
height: 25px;
padding: 5px;
background-color: #f2f2f2;
text-align: center;
border-radius: 0.4vw;
@@ -30,13 +30,13 @@
}
@media only screen {
.minicard {
padding: 0.8vh 1vw 0.3vh;
padding: 8px 1vw 3px;
position: relative;
flex: 1;
flex-wrap: wrap;
background-color: #fff;
min-height: 2.5vh;
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.15);
min-height: 25px;
box-shadow: 0 2px 3px rgba(0,0,0,0.15);
border-radius: 0.3vw;
color: #4d4d4d;
overflow: hidden;
@@ -77,7 +77,7 @@
border-bottom-right-radius: 0;
border-top-right-radius: 0;
z-index: 25;
box-shadow: -0.3vw 0.2vh 0.3vh rgba(0,0,0,0.2);
box-shadow: -0.3vw 2px 3px rgba(0,0,0,0.2);
}
.minicard:hover:not(.minicard-composer),
.is-selected .minicard,
@@ -91,9 +91,9 @@
background-position: center;
background-repeat: no-repeat;
background-size: contain;
height: 20vh;
height: 150px;
user-select: none;
margin: 0.8vh -1vw 0.8vh -1vw;
margin: 8px -1vw 8px -1vw;
border-radius: top 0.3vw;
}
.minicard .minicard-labels {
@@ -105,7 +105,7 @@
height: clamp(12px, 1.5vw, 16px);
border-radius: 0.3vw;
margin-right: 0.4vw;
margin-bottom: 0.4vh;
margin-bottom: 4px;
}
.minicard .minicard-labels-no-text {
display: flex;
@@ -137,7 +137,7 @@
height: clamp(20px, 2.5vw, 28px);
position: absolute;
right: 0vw;
top: 4vh;
top: 28px;
display: none;
z-index: 1;
}
@@ -313,7 +313,7 @@
.minicard .badges {
float: left;
margin-top: 1vh;
margin-top: 8px;
color: #808080;
}
.minicard .badges:empty {
@@ -322,7 +322,7 @@
.minicard .badges .badge {
float: left;
margin-right: 1.5vw;
margin-bottom: 0.4vh;
margin-bottom: 4px;
font-size: 0.9em;
}
.minicard .badges .badge.is-finished {
@@ -356,7 +356,7 @@
.minicard .minicard-creator {
float: right;
margin-left: 0.7vw;
margin-bottom: 0.5vh;
margin-bottom: 4px;
}
.minicard .minicard-members .member,
.minicard .minicard-assignees .member,
@@ -365,7 +365,7 @@
border-radius: 50%;
height: clamp(24px, 3.5vw, 32px);
width: clamp(24px, 3.5vw, 32px);
margin-bottom: 0.5vh;
margin-bottom: 4px;
}
.minicard .minicard-members .assignee,
.minicard .minicard-assignees .assignee,
@@ -378,7 +378,7 @@
.minicard .minicard-members + .badges,
.minicard .minicard-assignees + .badges,
.minicard .minicard-creator + .badges {
margin-top: 0.7vh;
margin-top: 6px;
}
.minicard .minicard-assignees {
border-bottom: 1px solid #f00;
@@ -391,11 +391,11 @@
display: none;
}
.minicard .minicard-description {
padding: 0.8vh 0 0 1vw;
padding: 8px 0 0 1vw;
color: #000;
background-color: #eee;
width: 100%;
margin-bottom: 0.3vh;
margin-bottom: 3px;
margin-left: -0.5vw;
border-radius: 0.4vw;
overflow: hidden;
@@ -405,7 +405,7 @@
-webkit-box-orient: vertical;
}
.minicard.minicard-composer {
margin-bottom: 1.3vh;
margin-bottom: 12px;
}
.minicard.minicard-composer textarea.minicard-composer-textarea,
.minicard.minicard-composer textarea.minicard-composer-textarea:focus {
@@ -416,9 +416,9 @@
height: auto;
margin: 0;
padding: 0;
max-height: 22vh;
min-height: 5vh;
margin-bottom: 2.5vh;
max-height: 200px;
min-height: 50px;
margin-bottom: 20px;
overflow-y: auto;
}
.parent-prefix {
@@ -736,7 +736,7 @@
.minicard-list-name {
font-size: 0.75em;
color: #8c8c8c;
margin-top: 0.2vh;
margin-top: 2px;
display: flex;
align-items: center;
gap: 0.3vw;
@@ -745,9 +745,11 @@
/* Checklist display on minicard */
.minicard-checklist {
width: 100%;
margin-top: 0.5vh;
margin-bottom: 0.5vh;
padding: 0.3vh 0.5vw;
box-sizing: border-box;
clear: both;
margin-top: 2px;
margin-bottom: 5px;
padding: 3px 0.5vw;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 0.3vw;
border: 1px solid #e0e0e0;
@@ -757,7 +759,7 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.3vh;
margin-bottom: 3px;
}
.minicard-checklist .checklist-title {
@@ -765,6 +767,7 @@
font-weight: bold;
color: #4d4d4d;
flex: 1;
margin: 0;
}
.minicard-checklist .checklist-menu {
@@ -783,13 +786,13 @@
.minicard-checklist .checklist-item {
font-size: 0.75em;
color: #666;
margin-bottom: 0.2vh;
margin-bottom: 2px;
display: flex;
align-items: flex-start;
gap: 0.3vw;
line-height: 1.2;
cursor: pointer;
padding: 0.2vh 0;
padding: 2px 0;
border-radius: 0.2vw;
transition: background-color 0.2s;
}
@@ -806,7 +809,7 @@
.minicard-checklist .checklist-item .check-box-unicode {
flex-shrink: 0;
font-size: 0.8em;
margin-top: 0.1vh;
margin-top: 1px;
transition: transform 0.2s;
}
+29 -14
View File
@@ -74,7 +74,7 @@ template(name="minicard")
+viewer
if showCardNumber
span.card-number
| ##{getCardNumber}
| ##{getCardNumber} &nbsp;
= getTitle
if showLabels
if labels
@@ -148,14 +148,16 @@ template(name="minicard")
.minicard-creator
+userAvatar(userId=this.userId noRemove=true)
if shouldShowChecklistAtMinicard
each shouldShowChecklistAtMinicard
+minicardChecklist(checklist=. card=..)
.badges
if canModifyCard
if comments.length
.badge(title="{{_ 'card-comments-title' comments.length }}")
span.badge-icon.badge-comment.badge-text
i.fa.fa-comment-o
= ' '
= comments.length
if comments.length
.badge(title="{{_ 'card-comments-title' comments.length }}")
span.badge-icon.badge-comment.badge-text
i.fa.fa-comment-o
= ' '
= comments.length
//span.badge-comment.badge-text
//|
{{_ 'comment'}}
@@ -197,9 +199,6 @@ template(name="minicard")
span.badge-icon
i.fa.fa-sort-numeric-asc
span.badge-text.check-list-sort {{ sort }}
if shouldShowChecklistAtMinicard
each shouldShowChecklistAtMinicard
+minicardChecklist(checklist=. card=..)
if currentBoard.allowsDescriptionTextOnMinicard
if getDescription
.minicard-description
@@ -224,10 +223,26 @@ template(name="editCardSortOrderPopup")
template(name="minicardChecklist")
.minicard-checklist
.checklist-header
.checklist-title= checklist.title
if canModifyCard
+inlinedForm(classNames="js-edit-checklist-title" checklist=checklist)
+editChecklistForm(checklist=checklist)
else
h4.checklist-title.js-open-inlined-form.is-editable= checklist.title
else
h4.checklist-title= checklist.title
if canModifyCard
a.checklist-menu.js-open-checklist-menu(title="{{_ 'checklistActionsPopup-title'}}")
i.fa.fa-bars
each visibleItems
+checklistItemDetail(item = . checklist = checklist card = card)
.checklist-items.js-checklist-items
each visibleItems
+inlinedForm(classNames="js-edit-checklist-item" item=. checklist=checklist)
+editChecklistItemForm(type='item' item=. checklist=checklist)
else
+checklistItemDetail(item=. checklist=checklist card=card)
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist=checklist)
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=false)
else
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
i.fa.fa-plus
+91 -19
View File
@@ -4,6 +4,18 @@ import { CustomFieldStringTemplate } from '/client/lib/customFields';
import { handleFileUpload } from './attachments';
import uploadProgressManager from '../../lib/uploadProgressManager';
import { Utils } from '/client/lib/utils';
import ChecklistItems from '/models/checklistItems';
function getMinicardFlag(board, onMinicardField, legacyField, defaultValue) {
if (!board) return false;
if (board[onMinicardField] !== null && board[onMinicardField] !== undefined) {
return board[onMinicardField];
}
if (legacyField && board[legacyField] !== null && board[legacyField] !== undefined) {
return board[legacyField];
}
return defaultValue;
}
// Template.cards.events({
// 'click .member': Popup.open('cardMember')
@@ -51,48 +63,40 @@ Template.minicard.helpers({
showMembers() {
const board = this.board();
let ret = false;
if (board) {
ret = board.allowsMembersOnMinicard ?? false;
}
return ret;
return getMinicardFlag(board, 'allowsMembersOnMinicard', 'allowsMembers', true);
},
showAssignee() {
const board = this.board();
let ret = false;
if (board) {
ret = board.allowsAssigneeOnMinicard ?? false;
}
return ret;
return getMinicardFlag(board, 'allowsAssigneeOnMinicard', 'allowsAssignee', true);
},
showReceived() {
const board = this.board();
return board ? board.allowsReceivedDateOnMinicard : false;
return getMinicardFlag(board, 'allowsReceivedDateOnMinicard', 'allowsReceivedDate', true);
},
showStart() {
const board = this.board();
return board ? board.allowsStartDateOnMinicard : false;
return getMinicardFlag(board, 'allowsStartDateOnMinicard', 'allowsStartDate', true);
},
showDue() {
const board = this.board();
return board ? board.allowsDueDateOnMinicard : false;
return getMinicardFlag(board, 'allowsDueDateOnMinicard', 'allowsDueDate', true);
},
showEnd() {
const board = this.board();
return board ? board.allowsEndDateOnMinicard : false;
return getMinicardFlag(board, 'allowsEndDateOnMinicard', 'allowsEndDate', true);
},
showLabels() {
const board = this.board();
return board ? board.allowsLabelsOnMinicard : false;
return getMinicardFlag(board, 'allowsLabelsOnMinicard', 'allowsLabels', true);
},
showCardNumber() {
const board = this.board();
return board ? board.allowsCardNumberOnMinicard : false;
return getMinicardFlag(board, 'allowsCardNumberOnMinicard', 'allowsCardNumber', false);
},
showSubtasks() {
const board = this.board();
return board ? board.allowsSubtasksOnMinicard : false;
return getMinicardFlag(board, 'allowsSubtasksOnMinicard', 'allowsSubtasks', true);
},
hiddenMinicardLabelText() {
@@ -109,10 +113,11 @@ Template.minicard.helpers({
if (!this.coverId) return null;
const attachment = ReactiveCache.getAttachment(this.coverId);
if (!attachment) return null;
const coverId = this.coverId;
const coverLink = typeof attachment.link === 'function' ? attachment.link() : '';
if (!coverLink) return null;
return {
link() {
return `/cdn/storage/attachments/${coverId}`;
return coverLink;
},
};
},
@@ -270,6 +275,13 @@ Template.minicardChecklist.helpers({
});
Template.minicardChecklist.events({
'click .js-convert-checklist-item-to-card'(event) {
event.stopPropagation();
const formData = Blaze.getData(event.currentTarget);
if (!formData) return;
const context = { currentData: () => formData };
Popup.open('convertChecklistItemToCard').call(context, event);
},
'click .js-open-checklist-menu'(event) {
const data = Template.currentData();
const checklist = data.checklist || data;
@@ -277,6 +289,66 @@ Template.minicardChecklist.events({
const context = { currentData: () => ({ checklist, card }) };
Popup.open('checklistActions').call(context, event);
},
'click .js-checklist-item .check-box-container'(event) {
event.stopPropagation();
event.preventDefault();
const data = Blaze.getData(event.target) || Blaze.getData(event.currentTarget);
const item = data && data.item;
if (item && item._id) {
item.toggleItem();
}
},
'click .js-submit-edit-checklist-item-form'(event) {
event.preventDefault();
event.stopPropagation();
const $btn = $(event.currentTarget);
const $form = $btn.closest('form');
const textarea = $form.find('textarea.js-edit-checklist-item')[0];
if (!textarea) return;
const title = textarea.value.trim();
if (!title) return;
const formData = Blaze.getData($form[0]);
if (formData && formData.item && formData.item._id) {
formData.item.setTitle(title);
} else if (formData && formData.checklist && formData.checklist._id) {
formData.checklist.setTitle(title);
}
$form.find('.js-close-inlined-form').trigger('click');
},
'submit .js-add-checklist-item'(event, tpl) {
event.preventDefault();
const textarea = tpl.find('textarea.js-add-checklist-item');
if (!textarea) return;
const title = textarea.value.trim();
const data = Template.currentData() || {};
const checklist = data.checklist;
if (!checklist || !title) return;
const items = checklist.items();
const lastItem = items[items.length - 1] || null;
const sortIndex = Utils.calculateIndexData(lastItem, null).base;
ChecklistItems.insert({
title,
checklistId: checklist._id,
cardId: checklist.cardId,
sort: sortIndex,
});
textarea.value = '';
textarea.focus();
},
'click .js-delete-checklist-item': Popup.afterConfirm('checklistItemDelete', function () {
Popup.back();
const item = this && this.item ? this.item : this;
if (item && item._id) {
ChecklistItems.remove(item._id);
}
}),
'keydown textarea.js-add-checklist-item'(event) {
if (event.keyCode === 13 && !event.shiftKey) {
event.preventDefault();
const $form = $(event.currentTarget).closest('form');
$form.find('button[type=submit]').click();
}
},
});
Template.editCardSortOrderPopup.events({
+12 -12
View File
@@ -152,12 +152,12 @@ body.list-resizing-active * {
/* Ensure consistent header height for all lists */
.list-header {
/* Maintain consistent height and padding for all lists */
min-height: 2.5vh !important;
min-height: 30px !important;
height: auto !important;
padding: 2.5vh 1.5vw 0.5vh !important;
padding: 28px 1.5vw 6px !important;
/* Make sure the background covers the full height */
background-color: #e4e4e4 !important;
border-bottom: 0.8vh solid #e4e4e4 !important;
border-bottom: 8px solid #e4e4e4 !important;
/* Use original display for consistent button positioning */
display: block !important;
position: relative !important;
@@ -401,24 +401,24 @@ form.inlined-form .list-composer {
.list.list-composer .list-name-input,
.list .list-composer .list-name-input {
background: #fff;
margin: -0.4vh 0 1vh;
margin: -4px 0 8px;
}
.list-header-add {
flex: 0 0 auto;
padding: 1.5vh 1.5vw;
padding: 14px 1.5vw;
position: relative;
min-height: 2.5vh;
min-height: 30px;
}
.list-header {
flex: 0 0 auto;
padding: 2.5vh 1.5vw 0.5vh;
padding: 28px 1.5vw 6px;
position: relative;
min-height: 2.5vh;
min-height: 30px;
background-color: #e4e4e4;
border-bottom: 0.8vh solid #e4e4e4;
border-bottom: 8px solid #e4e4e4;
}
.list-header.list-header-card-count {
min-height: 4.5vh;
min-height: 45px;
height: auto;
}
.list-header.ui-sortable-handle {
@@ -433,7 +433,7 @@ form.inlined-form .list-composer {
line-height: 1.2;
margin: 0;
font-weight: bold;
min-height: 1.2vh;
min-height: 14px;
min-width: 4vw;
overflow-wrap: break-word;
word-wrap: break-word;
@@ -1196,7 +1196,7 @@ form.inlined-form .list-composer {
/* Allow long list titles to expand on desktop (non-mobile, non-collapsed) */
.list:not(.mobile-view):not(.list-collapsed) .list-header {
padding-top: calc(2.5vh - 6px) !important;
padding-top: 22px !important;
overflow: visible !important;
}
+33 -9
View File
@@ -38,6 +38,15 @@ Template.list.onRendered(function () {
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards');
// Destroy any existing sortable before re-initializing. Without this, a
// re-render of the list (e.g. when a Card Details panel opens) binds a SECOND
// sortable to the same .js-minicards element, so dragging a card from its
// handle fires the `stop` handler twice and the card appears to duplicate.
// Mirrors the guard in swimlanes.js initSortable().
if ($cards.data('uiSortable') || $cards.data('sortable')) {
$cards.sortable('destroy');
}
$cards.sortable({
connectWith: '.js-minicards:not(.js-list-full)',
tolerance: 'pointer',
@@ -345,9 +354,19 @@ Template.list.onCreated(function () {
return 550;
};
// Read the horizontal page coordinate from either a jQuery mouse event
// or a native touch event (touchstart/move expose `touches`, touchend
// exposes `changedTouches`).
const getEventPageX = (e) => {
const oe = e.originalEvent || e;
if (oe.touches && oe.touches.length) return oe.touches[0].pageX;
if (oe.changedTouches && oe.changedTouches.length) return oe.changedTouches[0].pageX;
return e.pageX;
};
const startResize = (e) => {
isResizing = true;
startX = e.pageX || e.originalEvent.touches[0].pageX;
startX = getEventPageX(e);
startWidth = $list.outerWidth();
// Add visual feedback
@@ -366,7 +385,7 @@ Template.list.onCreated(function () {
return;
}
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
const currentX = getEventPageX(e);
const deltaX = currentX - startX;
const newWidth = Math.max(minWidth, startWidth + deltaX);
@@ -390,7 +409,7 @@ Template.list.onCreated(function () {
isResizing = false;
// Calculate final width
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
const currentX = getEventPageX(e);
const deltaX = currentX - startX;
const finalWidth = Math.max(minWidth, startWidth + deltaX);
const listConstraint = getListConstraint();
@@ -468,10 +487,15 @@ Template.list.onCreated(function () {
$(document).on('mousemove', doResize);
$(document).on('mouseup', stopResize);
// Touch events for mobile
$resizeHandle.on('touchstart', startResize, { passive: false });
$(document).on('touchmove', doResize, { passive: false });
$(document).on('touchend', stopResize, { passive: false });
// Touch events for mobile.
// NOTE: jQuery's .on() does not accept an addEventListener-style options
// object — passing { passive: false } made jQuery bind the object itself
// as the handler, throwing "handler.apply is not a function" on every
// touch. Use the native API so { passive: false } actually applies and
// preventDefault() works during the resize.
$resizeHandle[0].addEventListener('touchstart', startResize, { passive: false });
document.addEventListener('touchmove', doResize, { passive: false });
document.addEventListener('touchend', stopResize, { passive: false });
// Prevent dragscroll interference
$resizeHandle.on('mousedown', (e) => {
@@ -492,8 +516,8 @@ Template.list.onCreated(function () {
tpl.view.onViewDestroyed(() => {
$(document).off('mousemove', doResize);
$(document).off('mouseup', stopResize);
$(document).off('touchmove', doResize);
$(document).off('touchend', stopResize);
document.removeEventListener('touchmove', doResize);
document.removeEventListener('touchend', stopResize);
});
};
});
+31 -32
View File
@@ -1,39 +1,38 @@
template(name="listBody")
unless collapsed
.list-body(class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}")
.minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
+inlinedForm(autoclose=false position="top")
+addCardForm(listId=_id position="top")
ul.sidebar-list
each customFieldsSum
li
.list-body(class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}")
.minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
+inlinedForm(autoclose=false position="top")
+addCardForm(listId=_id position="top")
ul.sidebar-list
each customFieldsSum
li
+viewer
= name
if $eq customFieldsSum.type "number"
+viewer
= name
if $eq customFieldsSum.type "number"
+viewer
= value
if $eq customFieldsSum.type "currency"
+viewer
= formattedCurrencyCustomFieldValue(value)
each (cardsWithLimit (idOrNull ../../_id))
a.minicard-wrapper.js-minicard(href=originRelativeUrl
class="{{#if cardIsSelected}}is-selected{{/if}}"
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
if MultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-multi-selection(
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
+minicard(this)
if (showSpinner (idOrNull ../../_id))
+spinnerList
if canSeeAddCard
+inlinedForm(autoclose=false position="bottom")
+addCardForm(listId=_id position="bottom")
else
a.open-minicard-composer.js-card-composer.js-open-inlined-form(title="{{_ 'add-card-to-bottom-of-list'}}")
i.fa.fa-plus
| {{_ 'add-card'}}
= value
if $eq customFieldsSum.type "currency"
+viewer
= formattedCurrencyCustomFieldValue(value)
each (cardsWithLimit (idOrNull ../../_id))
a.minicard-wrapper.js-minicard(href=originRelativeUrl
class="{{#if cardIsSelected}}is-selected{{/if}}"
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
if MultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-multi-selection(
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
+minicard(this)
if (showSpinner (idOrNull ../../_id))
+spinnerList
if canSeeAddCard
+inlinedForm(autoclose=false position="bottom")
+addCardForm(listId=_id position="bottom")
else
a.open-minicard-composer.js-card-composer.js-open-inlined-form(title="{{_ 'add-card-to-bottom-of-list'}}")
i.fa.fa-plus
| {{_ 'add-card'}}
+inlinedForm(autoclose=false position="bottom")
+addCardForm(listId=_id position="bottom")
template(name="spinnerList")
.sk-spinner.sk-spinner-list(
+7
View File
@@ -192,6 +192,13 @@ Template.listBody.onCreated(function () {
}
const clickedTitle = $target.closest('.minicard-title').length > 0;
const clickedLinkedReference = $target.closest('.js-linked-link').length > 0;
const clickedChecklist = $target.closest('.minicard-checklist').length > 0;
if (clickedChecklist) {
evt.preventDefault();
evt.stopPropagation();
return;
}
// Title clicks should open the regular board card details view.
if (clickedTitle && !clickedLinkedReference) {
+99 -7
View File
@@ -104,6 +104,26 @@
height: 24px;
float: left;
}
/* On non-board pages (All Boards / home, public, my cards, …) the second bar
holds only a title (e.g. "My Boards"). The fixed 40px bar height plus the
floated h1 with a 1.7em line-height clipped the bottom of the text on iOS
Safari. Vertically center the title in a bar that can grow so it is never
cut off. Scoped to non-board-view so the board header (floated buttons) is
untouched. */
body:not(.board-view) #header #header-main-bar {
display: flex;
align-items: center;
height: auto;
min-height: 40px;
padding-top: 0;
padding-bottom: 0;
}
body:not(.board-view) #header #header-main-bar h1 {
float: none;
line-height: 1.3;
}
#header-quick-access {
color: #fff;
transition: background-color 0.4s;
@@ -157,6 +177,10 @@
#header-quick-access i.fa:hover {
color: #ccc;
}
/* Drag-handle toggle icons 30% smaller than the other quick-access icons. */
#header-quick-access .js-toggle-desktop-drag-handles .fa {
font-size: 0.7em;
}
#header-quick-access #header-user-bar,
#header-quick-access #header-new-board-icon,
#header-quick-access ul li {
@@ -177,24 +201,25 @@
}
#header-quick-access ul.header-quick-access-list {
transition: opacity 0.2s;
overflow: hidden;
white-space: nowrap;
padding: 10px;
margin: -10px;
margin: 10px;
flex: 1; /* Take up available space */
min-width: 0; /* Allow shrinking below content size */
display: flex; /* Use flexbox for better control */
align-items: center;
overflow-x: auto;
scrollbar-color: white #2573a7;
}
/* Hide scrollbar completely */
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar {
display: none;
display: flex;
}
#header-quick-access ul.header-quick-access-list {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: thin; /* IE and Edge */
scrollbar-width: thin; /* Firefox */
}
#header-quick-access ul.header-quick-access-list li {
display: inline-block; /* Keep inline-block for proper spacing */
@@ -1154,9 +1179,76 @@
display: none;
}
#header-quick-access #header-user-bar {
right: 0px;
right: auto;
padding: 10px;
margin: -8px 0 -10px -10px;
margin: 0 0.5rem;
}
/* ---- Small-screen quick-access bar (ALL browsers, width-based) ----
These rules use width-based selectors (not the .iphone-device class) so
they apply to every browser at small width — Fairphone/Android, narrow
desktop and iPhone alike. Keep every item visible: let the bar wrap and
grow instead of pinning its height and clipping the overflow. */
#header-quick-access {
height: auto !important;
min-height: 28px !important;
overflow: visible !important;
flex-wrap: wrap !important;
align-items: center !important;
row-gap: 4px !important;
}
/* The All Boards page scales this bar up to 2em-3em for every browser, which
is too big. Tame it to a moderate size so ~7 items fit in two wrapped rows.
(.iphone-device variants also cover the board view on detected iPhones.) */
body:not(.board-view) #header-quick-access,
.wrapper ~ #header-quick-access,
.iphone-device #header-quick-access,
.iphone-device .wrapper ~ #header-quick-access,
.iphone-device body:not(.board-view) #header-quick-access {
font-size: 1.2em !important;
}
body:not(.board-view) #header-quick-access .fa,
body:not(.board-view) #header-quick-access .icon,
.wrapper ~ #header-quick-access .fa,
.wrapper ~ #header-quick-access .icon,
.iphone-device #header-quick-access .fa,
.iphone-device #header-quick-access .icon,
.iphone-device .wrapper ~ #header-quick-access .fa,
.iphone-device .wrapper ~ #header-quick-access .icon,
.iphone-device body:not(.board-view) #header-quick-access .fa,
.iphone-device body:not(.board-view) #header-quick-access .icon {
font-size: 1.5em !important;
}
/* Zoom (100%), drag-handle toggle and mobile/desktop toggle 30% smaller than
the other quick-access controls at small width — for all browsers. */
body:not(.board-view) #header-quick-access .js-toggle-desktop-drag-handles .fa,
body:not(.board-view) #header-quick-access .mobile-mode-toggle .fa,
.wrapper ~ #header-quick-access .js-toggle-desktop-drag-handles .fa,
.wrapper ~ #header-quick-access .mobile-mode-toggle .fa,
.iphone-device #header-quick-access .js-toggle-desktop-drag-handles .fa,
.iphone-device #header-quick-access .mobile-mode-toggle .fa,
.iphone-device .wrapper ~ #header-quick-access .js-toggle-desktop-drag-handles .fa,
.iphone-device .wrapper ~ #header-quick-access .mobile-mode-toggle .fa,
.iphone-device body:not(.board-view) #header-quick-access .js-toggle-desktop-drag-handles .fa,
.iphone-device body:not(.board-view) #header-quick-access .mobile-mode-toggle .fa {
font-size: 1.05em !important; /* ~30% smaller than the 1.5em icons */
}
body:not(.board-view) #header-quick-access .zoom-controls,
body:not(.board-view) #header-quick-access .zoom-controls .zoom-level,
body:not(.board-view) #header-quick-access .zoom-controls .zoom-input,
.wrapper ~ #header-quick-access .zoom-controls,
.wrapper ~ #header-quick-access .zoom-controls .zoom-level,
.wrapper ~ #header-quick-access .zoom-controls .zoom-input,
.iphone-device #header-quick-access .zoom-controls,
.iphone-device #header-quick-access .zoom-controls .zoom-level,
.iphone-device #header-quick-access .zoom-controls .zoom-input,
.iphone-device .wrapper ~ #header-quick-access .zoom-controls,
.iphone-device .wrapper ~ #header-quick-access .zoom-controls .zoom-level,
.iphone-device .wrapper ~ #header-quick-access .zoom-controls .zoom-input,
.iphone-device body:not(.board-view) #header-quick-access .zoom-controls,
.iphone-device body:not(.board-view) #header-quick-access .zoom-controls .zoom-level,
.iphone-device body:not(.board-view) #header-quick-access .zoom-controls .zoom-input {
font-size: 0.7em !important; /* ~30% smaller than surrounding text */
}
}
@media print {
+1 -5
View File
@@ -68,12 +68,8 @@ template(name="header")
+viewer
= title
else
li.current.empty(title="{{_ 'quick-access-description'}}")
li.current(title="{{_ 'quick-access-description'}}")
| {{_ 'quick-access-description'}}
#header-new-board-icon
// Next line is used only for spacing at header,
// there is no visible clickable icon.
#header-new-board-icon
//
Hide duplicate create board button,
//
+22 -20
View File
@@ -593,12 +593,20 @@ a:not(.disabled).is-active i.fa {
}
#header-quick-access {
/* Keep quick-access items in one row */
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
width: 100%;
/* On narrow screens (e.g. iPhone 12 mini, 375px) the icon buttons cannot
shrink below their min-width, so a single nowrap row overflows and the
base rule's overflow:hidden clips the right-side items (mobile/desktop
toggle, notifications, member/user menu). Allow wrapping to a second row
and let the bar grow so nothing is ever hidden. */
flex-wrap: wrap;
overflow: visible;
height: auto;
min-height: 28px;
}
/* Hide elements that should move to the hamburger menu on mobile */
@@ -607,20 +615,18 @@ a:not(.disabled).is-active i.fa {
display: none !important;
}
/* Show only the home icon (hide the trailing text) on mobile */
/* Show the home link with its full "All Boards" text on mobile. The label is
a bare text node (not a span), so the old max-width:28px + overflow:hidden
clip revealed only "A"; removing it shows the whole label. The bar wraps,
so the extra width is fine. */
#header-quick-access .home-icon a {
display: inline-flex;
align-items: center;
max-width: 28px; /* enough to display the icon */
overflow: hidden;
max-width: none;
overflow: visible;
white-space: nowrap;
}
/* Hide text in home icon on mobile, show only icon */
#header-quick-access .home-icon a span:not(.fa) {
display: none !important;
}
/* Ensure proper spacing for mobile header elements */
#header-quick-access .zoom-controls {
margin-left: auto;
@@ -673,22 +679,18 @@ a:not(.disabled).is-active i.fa {
-webkit-overflow-scrolling: touch;
}
/* Admin panel mobile optimization */
/* Admin panel mobile optimization.
Keep the side-by-side (row) layout from settingBody.css even on narrow
screens — do NOT switch to a stacked column layout, which left the menu
only 32vw wide with empty space beside it and pushed the content off
screen. The side menu keeps a fixed basis and the main body fills the
rest, so both stay visible. */
.setting-content .content-body {
flex-direction: column;
gap: 16px;
gap: 8px;
padding: 8px;
}
.setting-content .content-body .side-menu {
width: 100%;
order: 2;
}
.setting-content .content-body .main-body {
order: 1;
min-height: 60vh;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
+69 -2
View File
@@ -78,23 +78,32 @@
overflow-x: hidden;
}
/* Allow dynamic max-height to override default constraint */
/* Bounded height: flex column + clip so inline max-height cannot spill past the shell.
Class pop-over--bounded matches popup.tpl.jade when max-height is set (more reliable
than attribute substring matching on the style attribute alone). */
.pop-over.pop-over--bounded,
.pop-over[style*='max-height'] {
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
box-sizing: border-box;
}
.pop-over.pop-over--bounded .header,
.pop-over[style*='max-height'] .header {
flex: 0 0 auto;
}
.pop-over.pop-over--bounded .content-wrapper,
.pop-over[style*='max-height'] .content-wrapper {
flex: 1 1 auto;
/* 0% basis: use remaining space under header, scroll inside (not past white bg) */
flex: 1 1 0%;
min-height: 0;
max-height: none;
overflow-y: auto;
overflow-x: hidden;
background: inherit;
}
.pop-over .content-container {
width: 100%;
@@ -103,6 +112,7 @@
}
/* Allow dynamic max-height to override default constraint for content-container */
.pop-over.pop-over--bounded .content-container,
.pop-over[style*='max-height'] .content-container {
max-height: none;
}
@@ -757,3 +767,60 @@ body.mobile-mode .pop-over .content-wrapper {
overflow-y: auto !important;
overflow-x: hidden !important;
}
/* ============================================
FIX: Popup overflow (Issues #6200, #6221, #6223)
Prevents menus from overflowing outside viewport
============================================ */
/* Specific fix for list actions menu */
.pop-over[data-popup="listActionsPopup"] {
max-height: 85vh !important;
}
.pop-over[data-popup="listActionsPopup"] .content-wrapper {
max-height: calc(85vh - 50px) !important;
overflow-y: auto !important;
}
/* Prevent any popup from overflowing viewport */
.pop-over {
max-height: 90vh !important;
display: flex !important;
flex-direction: column !important;
}
.pop-over .content-wrapper {
flex: 1 1 auto !important;
overflow-y: auto !important;
min-height: 0 !important;
}
.pop-over .content-container {
flex: 1 1 auto !important;
display: flex !important;
flex-direction: column !important;
min-height: 0 !important;
}
.pop-over .content {
flex: 1 1 auto !important;
overflow-y: auto !important;
min-height: 0 !important;
}
/* Keep header fixed while content scrolls */
.pop-over .header {
flex-shrink: 0 !important;
}
/* Mobile-specific adjustments */
@media screen and (max-width: 800px) {
.pop-over[data-popup="listActionsPopup"] {
max-height: 100vh !important;
}
.pop-over[data-popup="listActionsPopup"] .content-wrapper {
max-height: calc(100vh - 48px) !important;
}
}
+1
View File
@@ -2,6 +2,7 @@
class="{{#unless title}}miniprofile{{/unless}}"
class=currentBoard.colorClass
class="{{#unless title}}no-title{{/unless}}"
class="{{#if offset.maxHeight}}pop-over--bounded{{/if}}"
data-popup="{{popupName}}"
style="left:{{offset.left}}px; top:{{offset.top}}px;{{#if offset.maxHeight}} max-height:{{offset.maxHeight}}px;{{/if}}")
.header
+234
View File
@@ -2,7 +2,241 @@
display: flex;
gap: 10px;
}
.attachments-content .move-compact-section,
.setting-content .content-body .main-body .form-group .compact-block {
margin: 16px 0;
padding: 12px;
border: 1px solid #d0d7de;
border-radius: 6px;
background: #fff8e1;
max-width: 650px;
box-sizing: border-box;
}
.attachments-content .move-compact-section p,
.setting-content .content-body .main-body .form-group .compact-block p {
max-width: 100%;
white-space: normal;
word-wrap: break-word;
}
.attachments-content .move-compact-section h4,
.setting-content .content-body .main-body h4 {
margin: 0 0 6px;
font-size: 1em;
font-weight: 600;
}
.attachments-content .compact-result-list,
.setting-content .content-body .main-body .compact-result-list {
margin: 6px 0 0;
padding-left: 20px;
font-size: 0.85em;
font-family: monospace;
}
.attachments-content .move-progress-section {
margin: 12px 0 16px;
padding: 12px;
border: 1px solid #d0d7de;
border-radius: 6px;
background: #f6f8fa;
}
.attachments-content .move-progress-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.attachments-content .move-progress-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.attachments-content .move-progress-counter {
font-weight: 600;
font-size: 0.9em;
}
.attachments-content .move-progress-filename {
font-size: 0.85em;
color: #57606a;
word-break: break-all;
}
.attachments-content .move-progress-bar-track {
background: #dde1e4;
border-radius: 4px;
height: 8px;
overflow: hidden;
margin: 6px 0;
}
.attachments-content .move-progress-bar-fill {
background: #1976d2;
height: 100%;
border-radius: 4px;
transition: width 0.4s ease;
min-width: 2px;
}
.attachments-content .move-progress-bar-indeterminate {
height: 100%;
border-radius: 4px;
background: linear-gradient(90deg, transparent 0%, #1976d2 40%, #42a5f5 60%, transparent 100%);
background-size: 200% 100%;
animation: move-progress-slide 1.4s ease-in-out infinite;
}
@keyframes move-progress-slide {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.attachments-content .move-progress-controls {
display: flex;
gap: 8px;
margin-top: 10px;
}
.attachments-content .move-storage-form {
display: flex;
align-items: flex-end;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.attachments-content .move-storage-selects {
display: flex;
align-items: flex-end;
gap: 12px;
flex-wrap: wrap;
}
.attachments-content .move-storage-form .form-group {
margin: 0;
}
.attachments-content .move-storage-form .form-group label {
display: block;
margin-bottom: 4px;
font-weight: 600;
}
.attachments-content .move-attachment-buttons button,
.attachments-content .board-attachments td button {
background-color: #1976d2;
border: 1px solid #1565c0;
color: #fff;
}
.attachments-content .move-attachment-buttons button:hover,
.attachments-content .board-attachments td button:hover {
background-color: #1565c0;
color: #fff;
}
.attachments-content hr {
height: 0px;
border: 1px solid #000;
}
.setting-content .content-body .main-body .storage-rw-row {
display: flex;
align-items: center;
gap: 24px;
margin-bottom: 12px;
padding: 6px 0;
}
.setting-content .content-body .main-body .storage-rw-item {
display: flex;
align-items: center;
gap: 6px;
}
.setting-content .content-body .main-body .storage-rw-row input[type="checkbox"] {
position: static;
left: auto;
visibility: visible;
display: inline-block;
width: 16px;
height: 16px;
margin: 0;
flex-shrink: 0;
cursor: pointer;
}
.setting-content .content-body .main-body .storage-rw-row input[type="checkbox"]:disabled {
cursor: default;
opacity: 0.6;
}
.setting-content .content-body .main-body .storage-rw-row label {
margin: 0;
font-weight: 600;
cursor: pointer;
}
.setting-content .content-body .main-body .storage-rw-row input[type="checkbox"]:disabled + label {
cursor: default;
opacity: 0.6;
}
.setting-content .content-body .main-body .gridfs-stats-table {
border-collapse: collapse;
margin-top: 12px;
width: auto !important;
min-width: 0 !important;
}
.setting-content .content-body .main-body .gridfs-stats-table th,
.setting-content .content-body .main-body .gridfs-stats-table td {
border: 1px solid #d0d7de;
padding: 6px 8px;
text-align: left;
white-space: nowrap;
min-width: 0 !important;
}
/* Cloud storage (S3 / Azure / GCS): example value shown below each input. */
.attachments-content .cloud-input-example {
display: block;
color: #888;
font-size: 0.82em;
font-family: monospace;
word-break: break-all;
line-height: 1.35;
}
/* Cloud storage: where-to-find-this-value path shown below the example. */
.attachments-content .cloud-input-path {
display: block;
color: #6a737d;
font-size: 0.82em;
line-height: 1.4;
margin-top: 2px;
}
/* Cloud storage: translated label shown under the (English) label. */
.attachments-content .cloud-input-label-tr {
display: block;
color: #57606a;
font-size: 0.9em;
margin-bottom: 4px;
}
/* Cloud storage: description text (English literal and translated). */
.attachments-content .cloud-input-desc {
display: block;
color: #6a737d;
font-size: 0.82em;
line-height: 1.4;
margin-top: 2px;
}
+530 -77
View File
@@ -3,94 +3,547 @@ template(name="attachments")
unless currentUser.isAdmin
| {{_ 'error-notAuthorized'}}
else
.content-title.ext-box
span
span.emoji-icon
i.fa.fa-paperclip
| {{_ 'attachments'}}
.content-body
.side-menu
ul
li
a.js-move-attachments(data-id="move-attachments")
li(class="{{#if isMoveActive}}active{{/if}}")
a.js-attachments-menu(data-id="move")
span.emoji-icon
i.fa.fa-arrow-right
| {{_ 'attachment-move'}}
li(class="{{#if isDefaultStorageActive}}active{{/if}}")
a.js-attachments-menu(data-id="default-save-storage")
span.emoji-icon
i.fa.fa-save
| {{_ 'default-save-storage'}}
li(class="{{#if isLimitsActive}}active{{/if}}")
a.js-attachments-menu(data-id="limits")
span.emoji-icon
i.fa.fa-sliders
| {{_ 'attachment-limits'}}
li(class="{{#if isGridFsActive}}active{{/if}}")
a.js-attachments-menu(data-id="gridfs")
span.emoji-icon
i.fa.fa-database
| {{_ 'mongodb-gridfs-storage'}}
li(class="{{#if isFilesystemActive}}active{{/if}}")
a.js-attachments-menu(data-id="filesystem")
span.emoji-icon
i.fa.fa-folder-open
| {{_ 'filesystem-storage'}}
li(class="{{#if isS3Active}}active{{/if}}")
a.js-attachments-menu(data-id="s3")
span.emoji-icon
i.fa.fa-cloud
| {{_ 's3-minio-storage'}}
li(class="{{#if isAzureActive}}active{{/if}}")
a.js-attachments-menu(data-id="azure")
span.emoji-icon
i.fa.fa-cloud
| {{_ 'azure-blob-storage'}}
li(class="{{#if isGcsActive}}active{{/if}}")
a.js-attachments-menu(data-id="gcs")
span.emoji-icon
i.fa.fa-cloud
| {{_ 'gcs-storage'}}
.main-body
if loading.get
+spinner
else if showMoveAttachments.get
+moveAttachments
else if isMoveActive
ul#attachment-move-setting.setting-detail
li
h3 {{_ 'attachment-move'}}
+moveAttachments
else if isDefaultStorageActive
ul#attachment-default-storage-setting.setting-detail
li
h3 {{_ 'default-save-storage'}}
.description {{_ 'default-save-storage-description'}}
.form-group
select.wekan-form-control.js-default-save-storage
each defaultStorageOptions
option(value=value selected=selected) {{_ labelKey}}
.form-group
button.js-save-default-storage.primary {{_ 'save'}}
else if isLimitsActive
ul#attachment-limits-setting.setting-detail
li
h3 {{_ 'attachment-transfer-limits-title'}}
.description {{_ 'attachment-transfer-limits-description'}}
li
.title {{_ 'attachment-upload-limit-label'}}
.form-group
select.wekan-form-control.js-attachment-limit-mode(data-field="attachmentsUploadMaxBytes")
each attachmentTransferLimitModeOptions 'attachmentsUploadMaxBytes'
option(value=value selected=selected) {{_ labelKey}}
.form-group(class="{{#if isAttachmentLimitMode 'attachmentsUploadMaxBytes' 'max-size'}}{{else}}hide{{/if}}")
input.wekan-form-control#attachments-upload-limit-value(type="number" min="1" step="any" value="{{attachmentTransferLimitValue 'attachmentsUploadMaxBytes'}}")
select.wekan-form-control.js-attachment-limit-unit(data-field="attachmentsUploadMaxBytes")
each attachmentTransferLimitUnitOptions 'attachmentsUploadMaxBytes'
option(value=value selected=selected) {{_ labelKey}}
li
.title {{_ 'attachment-download-limit-label'}}
.form-group
select.wekan-form-control.js-attachment-limit-mode(data-field="attachmentsDownloadMaxBytes")
each attachmentTransferLimitModeOptions 'attachmentsDownloadMaxBytes'
option(value=value selected=selected) {{_ labelKey}}
.form-group(class="{{#if isAttachmentLimitMode 'attachmentsDownloadMaxBytes' 'max-size'}}{{else}}hide{{/if}}")
input.wekan-form-control#attachments-download-limit-value(type="number" min="1" step="any" value="{{attachmentTransferLimitValue 'attachmentsDownloadMaxBytes'}}")
select.wekan-form-control.js-attachment-limit-unit(data-field="attachmentsDownloadMaxBytes")
each attachmentTransferLimitUnitOptions 'attachmentsDownloadMaxBytes'
option(value=value selected=selected) {{_ labelKey}}
li
.title {{_ 'api-upload-limit-label'}}
.form-group
select.wekan-form-control.js-attachment-limit-mode(data-field="apiUploadMaxBytes")
each attachmentTransferLimitModeOptions 'apiUploadMaxBytes'
option(value=value selected=selected) {{_ labelKey}}
.form-group(class="{{#if isAttachmentLimitMode 'apiUploadMaxBytes' 'max-size'}}{{else}}hide{{/if}}")
input.wekan-form-control#api-upload-limit-value(type="number" min="1" step="any" value="{{attachmentTransferLimitValue 'apiUploadMaxBytes'}}")
select.wekan-form-control.js-attachment-limit-unit(data-field="apiUploadMaxBytes")
each attachmentTransferLimitUnitOptions 'apiUploadMaxBytes'
option(value=value selected=selected) {{_ labelKey}}
li
.title {{_ 'api-download-limit-label'}}
.form-group
select.wekan-form-control.js-attachment-limit-mode(data-field="apiDownloadMaxBytes")
each attachmentTransferLimitModeOptions 'apiDownloadMaxBytes'
option(value=value selected=selected) {{_ labelKey}}
.form-group(class="{{#if isAttachmentLimitMode 'apiDownloadMaxBytes' 'max-size'}}{{else}}hide{{/if}}")
input.wekan-form-control#api-download-limit-value(type="number" min="1" step="any" value="{{attachmentTransferLimitValue 'apiDownloadMaxBytes'}}")
select.wekan-form-control.js-attachment-limit-unit(data-field="apiDownloadMaxBytes")
each attachmentTransferLimitUnitOptions 'apiDownloadMaxBytes'
option(value=value selected=selected) {{_ labelKey}}
li
button.js-save-attachment-transfer-limits.primary {{_ 'save'}}
else if isFilesystemActive
ul#attachment-filesystem-setting.setting-detail
li
h3 {{_ 'filesystem-storage'}}
.storage-rw-row
.storage-rw-item
input.js-toggle-filesystem-read#filesystem-read(type="checkbox" checked="{{filesystemRead}}")
label(for="filesystem-read") {{_ 'storage-read'}}
.form-group
button.js-calculate-filesystem-stats.btn.btn-primary(type="button") {{_ 'calculate-file-counts'}}
if filesystemStatsLoading
p.form-text.text-muted {{_ 'calculating-counts'}}
if filesystemStatsError
p.form-text.text-danger {{filesystemStatsError}}
if hasFilesystemStats
table.gridfs-stats-table
tr
th {{_ 'stats-scope'}}
th {{_ 'stats-count'}}
tr
td {{_ 'attachments'}}
td {{filesystemStats.attachments}}
tr
td {{_ 'avatars'}}
td {{filesystemStats.avatars}}
.form-group
label {{_ 'writable-path'}}
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
small.form-text.text-muted {{_ 'filesystem-path-description'}}
.form-group
label {{_ 'attachments-path'}}
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
small.form-text.text-muted {{_ 'attachments-path-description'}}
.form-group
label {{_ 'avatars-path'}}
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
small.form-text.text-muted {{_ 'avatars-path-description'}}
else if isGridFsActive
ul#attachment-gridfs-setting.setting-detail
li
h3 {{_ 'mongodb-gridfs-storage'}}
.description {{_ 'gridfs-move-collectionfs-note'}}
.storage-rw-row
.storage-rw-item
input.js-toggle-gridfs-read#gridfs-read(type="checkbox" checked="{{gridfsRead}}")
label(for="gridfs-read") {{_ 'storage-read'}}
.form-group
button.js-calculate-gridfs-stats.btn.btn-primary(type="button") {{_ 'calculate-file-counts'}}
if gridFsStatsLoading
p.form-text.text-muted {{_ 'calculating-counts'}}
if gridFsStatsError
p.form-text.text-danger {{gridFsStatsError}}
if hasGridFsStats
table.gridfs-stats-table
tr
th {{_ 'stats-scope'}}
th {{_ 'stats-collectionfs'}}
th {{_ 'stats-mongo-files'}}
tr
td {{_ 'attachments'}}
td {{gridFsStats.attachments.collectionFs}}
td {{gridFsStats.attachments.mongoFiles}}
tr
td {{_ 'avatars'}}
td {{gridFsStats.avatars.collectionFs}}
td {{gridFsStats.avatars.mongoFiles}}
.form-group
.compact-block
h4 {{_ 'mongodb-compact'}}
p.form-text {{_ 'mongodb-compact-description'}}
p.form-text.text-muted {{_ 'mongodb-compact-warning'}}
button.js-compact-mongodb-gridfs.btn.btn-warning(type="button") {{_ 'mongodb-compact-run'}}
if compactLoading
p.form-text.text-muted {{_ 'mongodb-compact-running'}}
if compactError
p.form-text.text-danger {{_ 'mongodb-compact-error'}} {{compactError}}
if compactResult
p.form-text.text-success {{_ 'mongodb-compact-success'}}
ul.compact-result-list
each item in compactResultItems
li
strong {{item.name}}
| : {{item.status}}
else if isS3Active
ul#attachment-s3-setting.setting-detail
li
h3 {{_ 's3-minio-storage'}}
.description {{_ 's3-minio-storage-description'}}
.storage-rw-row
.storage-rw-item
input#s3-enabled(type="checkbox" checked=cloudEnabled.s3)
label(for="s3-enabled") {{_ 'storage-enabled'}}
.storage-rw-item
input#s3-read(type="checkbox" checked=cloudRead.s3)
label(for="s3-read") {{_ 'storage-read'}}
.form-group
button.js-save-cloud-settings.btn.btn-primary(data-provider="s3") {{_ 'save'}}
.form-group
button.js-calculate-s3-stats.btn.btn-primary(type="button") {{_ 'calculate-file-counts'}}
if s3StatsLoading
p.form-text.text-muted {{_ 'calculating-counts'}}
if s3StatsError
p.form-text.text-danger {{s3StatsError}}
if hasS3Stats
table.gridfs-stats-table
tr
th {{_ 'stats-scope'}}
th {{_ 'stats-count'}}
tr
td {{_ 'attachments'}}
td {{s3Stats.attachments}}
tr
td {{_ 'avatars'}}
td {{s3Stats.avatars}}
.form-group
label Endpoint
small.form-text.cloud-input-label-tr {{_ 's3-endpoint'}}
input.wekan-form-control#s3-endpoint(type="text" value=cloudValue.s3.endpoint)
small.form-text.cloud-input-example https://s3.eu-west-1.amazonaws.com
small.form-text.cloud-input-desc S3 endpoint URL (e.g., s3.amazonaws.com or minio.example.com)
small.form-text.cloud-input-desc {{_ 's3-endpoint-description'}}
small.form-text.cloud-input-path AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.
small.form-text.cloud-input-path {{_ 's3-endpoint-menu-path'}}
.form-group
label AWS Region
small.form-text.cloud-input-label-tr {{_ 's3-region'}}
input.wekan-form-control#s3-region(type="text" value=cloudValue.s3.region)
small.form-text.cloud-input-example eu-west-1
small.form-text.cloud-input-desc AWS S3 region (e.g., us-east-1)
small.form-text.cloud-input-desc {{_ 's3-region-description'}}
small.form-text.cloud-input-path AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.
small.form-text.cloud-input-path {{_ 's3-region-menu-path'}}
.form-group
label Bucket name
small.form-text.cloud-input-label-tr {{_ 's3-bucket'}}
input.wekan-form-control#s3-bucket(type="text" value=cloudValue.s3.bucket)
small.form-text.cloud-input-example wekan-s3-test-369341155299-eu-west-1-an
small.form-text.cloud-input-desc S3 bucket name for storing files
small.form-text.cloud-input-desc {{_ 's3-bucket-description'}}
small.form-text.cloud-input-path AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).
small.form-text.cloud-input-path {{_ 's3-bucket-menu-path'}}
.form-group
label Access key ID
small.form-text.cloud-input-label-tr {{_ 's3-access-key'}}
input.wekan-form-control#s3-access-key(type="text" value=cloudValue.s3.accessKeyId)
small.form-text.cloud-input-example AKIAIOSFODNN7EXAMPLE
small.form-text.cloud-input-desc AWS S3 access key for authentication
small.form-text.cloud-input-desc {{_ 's3-access-key-description'}}
small.form-text.cloud-input-path AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.
small.form-text.cloud-input-path {{_ 's3-access-key-menu-path'}}
.form-group
label Secret access key
small.form-text.cloud-input-label-tr {{_ 's3-secret-key'}}
input.wekan-form-control#s3-secret-key(type="password" placeholder=cloudSecretPlaceholder.s3.secretAccessKey)
small.form-text.cloud-input-example wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
small.form-text.cloud-input-desc Leave blank to keep the current value.
small.form-text.cloud-input-desc {{_ 'cloud-secret-keep-blank'}}
small.form-text.cloud-input-path Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).
small.form-text.cloud-input-path {{_ 's3-secret-key-menu-path'}}
.form-group
.storage-rw-item
input#s3-force-path-style(type="checkbox" checked=cloudValue.s3.forcePathStyle)
label(for="s3-force-path-style") {{_ 's3-force-path-style'}}
small.form-text.text-muted {{_ 's3-force-path-style-description'}}
.form-group
button.js-test-cloud-connection.btn.btn-secondary(data-provider="s3") {{_ 'test-cloud-connection'}}
button.js-save-cloud-settings.btn.btn-primary(data-provider="s3") {{_ 'save'}}
if cloudTestResult.s3
p.form-text.text-success {{_ 'cloud-connection-success'}}
if cloudTestError.s3
p.form-text.text-danger {{_ 'cloud-connection-failed'}}: {{cloudTestError.s3}}
else if isAzureActive
ul#attachment-azure-setting.setting-detail
li
h3 {{_ 'azure-blob-storage'}}
.storage-rw-row
.storage-rw-item
input#azure-enabled(type="checkbox" checked=cloudEnabled.azure)
label(for="azure-enabled") {{_ 'storage-enabled'}}
.storage-rw-item
input#azure-read(type="checkbox" checked=cloudRead.azure)
label(for="azure-read") {{_ 'storage-read'}}
.form-group
button.js-save-cloud-settings.btn.btn-primary(data-provider="azure") {{_ 'save'}}
.form-group
button.js-calculate-azure-stats.btn.btn-primary(type="button") {{_ 'calculate-file-counts'}}
if azureStatsLoading
p.form-text.text-muted {{_ 'calculating-counts'}}
if azureStatsError
p.form-text.text-danger {{azureStatsError}}
if hasAzureStats
table.gridfs-stats-table
tr
th {{_ 'stats-scope'}}
th {{_ 'stats-count'}}
tr
td {{_ 'attachments'}}
td {{azureStats.attachments}}
tr
td {{_ 'avatars'}}
td {{azureStats.avatars}}
.form-group
label Storage account name
small.form-text.cloud-input-label-tr {{_ 'azure-account-name'}}
input.wekan-form-control#azure-account-name(type="text" value=cloudValue.azure.accountName)
small.form-text.cloud-input-example wekanstorage
small.form-text.cloud-input-desc The name of your Azure Storage account.
small.form-text.cloud-input-desc {{_ 'azure-account-name-description'}}
small.form-text.cloud-input-path Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).
small.form-text.cloud-input-path {{_ 'azure-account-name-menu-path'}}
.form-group
label Storage account key
small.form-text.cloud-input-label-tr {{_ 'azure-account-key'}}
input.wekan-form-control#azure-account-key(type="password" placeholder=cloudSecretPlaceholder.azure.accountKey)
small.form-text.cloud-input-example base64AccountKey==
small.form-text.cloud-input-desc Leave blank to keep the current value.
small.form-text.cloud-input-desc {{_ 'cloud-secret-keep-blank'}}
small.form-text.cloud-input-path Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.
small.form-text.cloud-input-path {{_ 'azure-account-key-menu-path'}}
.form-group
label Connection string
small.form-text.cloud-input-label-tr {{_ 'azure-connection-string'}}
input.wekan-form-control#azure-connection-string(type="password" placeholder=cloudSecretPlaceholder.azure.connectionString)
small.form-text.cloud-input-example DefaultEndpointsProtocol=https;AccountName=wekanstorage;AccountKey=...;EndpointSuffix=core.windows.net
small.form-text.cloud-input-desc Optional: use a full connection string instead of account name and key.
small.form-text.cloud-input-desc {{_ 'azure-connection-string-description'}}
small.form-text.cloud-input-path Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.
small.form-text.cloud-input-path {{_ 'azure-connection-string-menu-path'}}
.form-group
label Container name
small.form-text.cloud-input-label-tr {{_ 'azure-container'}}
input.wekan-form-control#azure-bucket(type="text" value=cloudValue.azure.bucket)
small.form-text.cloud-input-example wekan-attachments
small.form-text.cloud-input-desc The blob container to store files in.
small.form-text.cloud-input-desc {{_ 'azure-container-description'}}
small.form-text.cloud-input-path Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).
small.form-text.cloud-input-path {{_ 'azure-container-menu-path'}}
.form-group
button.js-test-cloud-connection.btn.btn-secondary(data-provider="azure") {{_ 'test-cloud-connection'}}
button.js-save-cloud-settings.btn.btn-primary(data-provider="azure") {{_ 'save'}}
if cloudTestResult.azure
p.form-text.text-success {{_ 'cloud-connection-success'}}
if cloudTestError.azure
p.form-text.text-danger {{_ 'cloud-connection-failed'}}: {{cloudTestError.azure}}
else if isGcsActive
ul#attachment-gcs-setting.setting-detail
li
h3 {{_ 'gcs-storage'}}
.description Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In "New principals", paste the service account email — the "client_email" value from your JSON key — then choose the role "Storage Object Admin" and Save.
.description {{_ 'gcs-permissions-note'}}
.storage-rw-row
.storage-rw-item
input#gcs-enabled(type="checkbox" checked=cloudEnabled.gcs)
label(for="gcs-enabled") {{_ 'storage-enabled'}}
.storage-rw-item
input#gcs-read(type="checkbox" checked=cloudRead.gcs)
label(for="gcs-read") {{_ 'storage-read'}}
.form-group
button.js-save-cloud-settings.btn.btn-primary(data-provider="gcs") {{_ 'save'}}
.form-group
button.js-calculate-gcs-stats.btn.btn-primary(type="button") {{_ 'calculate-file-counts'}}
if gcsStatsLoading
p.form-text.text-muted {{_ 'calculating-counts'}}
if gcsStatsError
p.form-text.text-danger {{gcsStatsError}}
if hasGcsStats
table.gridfs-stats-table
tr
th {{_ 'stats-scope'}}
th {{_ 'stats-count'}}
tr
td {{_ 'attachments'}}
td {{gcsStats.attachments}}
tr
td {{_ 'avatars'}}
td {{gcsStats.avatars}}
.form-group
label Project ID
small.form-text.cloud-input-label-tr {{_ 'gcs-project-id'}}
input.wekan-form-control#gcs-project-id(type="text" value=cloudValue.gcs.projectId)
small.form-text.cloud-input-example my-gcp-project-123456
small.form-text.cloud-input-desc Your Google Cloud project id.
small.form-text.cloud-input-desc {{_ 'gcs-project-id-description'}}
small.form-text.cloud-input-path Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).
small.form-text.cloud-input-path {{_ 'gcs-project-id-menu-path'}}
.form-group
label Bucket name
small.form-text.cloud-input-label-tr {{_ 'gcs-bucket'}}
input.wekan-form-control#gcs-bucket(type="text" value=cloudValue.gcs.bucket)
small.form-text.cloud-input-example wekan-attachments
small.form-text.cloud-input-desc The name of the Cloud Storage bucket to store files in.
small.form-text.cloud-input-desc {{_ 'gcs-bucket-description'}}
small.form-text.cloud-input-path Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).
small.form-text.cloud-input-path {{_ 'gcs-bucket-menu-path'}}
.form-group
label Service account key file path
small.form-text.cloud-input-label-tr {{_ 'gcs-key-filename'}}
input.wekan-form-control#gcs-key-filename(type="text" value=cloudValue.gcs.keyFilename)
small.form-text.cloud-input-example /data/gcs-service-account.json
small.form-text.cloud-input-desc Optional: path to a service account JSON key file on the server.
small.form-text.cloud-input-desc {{_ 'gcs-key-filename-description'}}
small.form-text.cloud-input-path Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.
small.form-text.cloud-input-path {{_ 'gcs-key-filename-menu-path'}}
.form-group
label Service account key (JSON)
small.form-text.cloud-input-label-tr {{_ 'gcs-credentials'}}
textarea.wekan-form-control#gcs-credentials(rows="4" placeholder=cloudSecretPlaceholder.gcs.credentials)
small.form-text.cloud-input-example { "type": "service_account", "project_id": "my-gcp-project", "client_email": "wekan@my-gcp-project.iam.gserviceaccount.com", "private_key": "-----BEGIN PRIVATE KEY-----..." }
small.form-text.cloud-input-desc Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.
small.form-text.cloud-input-desc {{_ 'gcs-credentials-description'}}
small.form-text.cloud-input-path Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.
small.form-text.cloud-input-path {{_ 'gcs-credentials-menu-path'}}
small.form-text.text-muted {{_ 'gcs-credentials-description'}}
.form-group
button.js-test-cloud-connection.btn.btn-secondary(data-provider="gcs") {{_ 'test-cloud-connection'}}
button.js-save-cloud-settings.btn.btn-primary(data-provider="gcs") {{_ 'save'}}
if cloudTestResult.gcs
p.form-text.text-success {{_ 'cloud-connection-success'}}
if cloudTestError.gcs
p.form-text.text-danger {{_ 'cloud-connection-failed'}}: {{cloudTestError.gcs}}
template(name="moveAttachments")
.move-attachment-buttons
.js-move-attachment
button.js-move-all-attachments-to-fs {{_ 'move-all-attachments-to-fs'}}
.js-move-attachment
button.js-move-all-attachments-to-gridfs {{_ 'move-all-attachments-to-gridfs'}}
.js-move-attachment
button.js-move-all-attachments-to-s3 {{_ 'move-all-attachments-to-s3'}}
.move-storage-form
.move-storage-selects
.form-group
label {{_ 'move-scope'}}
select.wekan-form-control.js-move-scope
option(value="attachments") {{_ 'move-scope-attachments'}}
option(value="avatars") {{_ 'move-scope-avatars'}}
option(value="both") {{_ 'move-scope-both'}}
.form-group
label {{_ 'move-source'}}
select.wekan-form-control.js-move-source-storage
option(value="all") {{_ 'move-storage-all'}}
option(value="collectionfs") {{_ 'move-storage-collectionfs'}}
option(value="gridfs") {{_ 'move-storage-gridfs'}}
option(value="fs") {{_ 'move-storage-fs'}}
option(value="s3") {{_ 'move-storage-s3'}}
option(value="azure") {{_ 'move-storage-azure'}}
option(value="gcs") {{_ 'move-storage-gcs'}}
.form-group
label {{_ 'move-destination'}}
select.wekan-form-control.js-move-dest-storage
option(value="gridfs") {{_ 'move-storage-gridfs'}}
option(value="fs") {{_ 'move-storage-fs'}}
option(value="s3") {{_ 'move-storage-s3'}}
option(value="azure") {{_ 'move-storage-azure'}}
option(value="gcs") {{_ 'move-storage-gcs'}}
option(value="collectionfs") {{_ 'move-storage-collectionfs'}}
button.js-move-all-attachments.btn.btn-primary {{_ 'move-all-attachments'}}
each board in getBoardsWithAttachments
+moveBoardAttachments board
if lastMove
.move-last-operation
span.emoji-icon
i.fa.fa-info-circle
| {{_ 'attachment-last-move'}}: {{lastMoveText}}
template(name="moveBoardAttachments")
hr
.board-description
table
tr
th {{_ 'board'}} ID
th {{_ 'board-title'}}
tr
td {{ _id }}
td {{ title }}
.move-repair-section
h4 {{_ 'attachment-repair-locations'}}
.description {{_ 'attachment-repair-locations-description'}}
button.js-repair-attachment-locations.btn.btn-secondary(type="button") {{_ 'attachment-repair-locations'}}
if repairLoading
p.form-text.text-muted {{_ 'attachment-repair-running'}}
if repairError
p.form-text.text-danger {{repairError}}
if repairResult
p.form-text.text-success {{_ 'attachment-repair-done'}}
table.gridfs-stats-table
tr
th {{_ 'stats-scope'}}
th {{_ 'attachment-repair-scanned'}}
th {{_ 'attachment-repair-repaired'}}
th {{_ 'attachment-repair-broken'}}
tr
td {{_ 'attachments'}}
td {{repairResult.attachments.scanned}}
td {{repairResult.attachments.repaired}}
td {{repairResult.attachments.broken}}
tr
td {{_ 'avatars'}}
td {{repairResult.avatars.scanned}}
td {{repairResult.avatars.repaired}}
td {{repairResult.avatars.broken}}
.move-attachment-buttons
.js-move-attachment
button.js-move-all-attachments-of-board-to-fs {{_ 'move-all-attachments-of-board-to-fs'}}
.js-move-attachment
button.js-move-all-attachments-of-board-to-gridfs {{_ 'move-all-attachments-of-board-to-gridfs'}}
.js-move-attachment
button.js-move-all-attachments-of-board-to-s3 {{_ 'move-all-attachments-of-board-to-s3'}}
.board-attachments
table
tr
th {{_ 'card'}}-Id
th {{_ 'attachment'}}-Id
th {{_ 'name'}}
th {{_ 'path'}}
th {{_ 'version-name'}}
th {{_ 'size'}}
th GridFsFileId
th S3FileId
th {{_ 'storage'}}
th {{_ 'action'}}
each attachment in attachments
+moveAttachment attachment
template(name="moveAttachment")
each version in flatVersion
tr
td {{ meta.cardId }}
td {{ _id }}
td {{ name }}
td {{ version.path }}
td {{ version.versionName }}
td {{ fileSize version.size }}
td {{ version.meta.gridFsFileId }}
td {{ version.meta.s3FileId }}
td {{ version.storageName }}
td
if $neq version.storageName "fs"
button.js-move-storage-fs
i.fa.fa-arrow-right
| {{_ 'attachment-move-storage-fs'}}
if $neq version.storageName "gridfs"
if version.storageName
button.js-move-storage-gridfs
i.fa.fa-arrow-right
| {{_ 'attachment-move-storage-gridfs'}}
if $neq version.storageName "s3"
if version.storageName
button.js-move-storage-s3
i.fa.fa-arrow-right
| {{_ 'attachment-move-storage-s3'}}
if moveProgress
.move-progress-section
.move-progress-header
.move-progress-info
.move-progress-counter {{_ 'move-progress-file'}} {{moveProgress.current}} / {{moveProgress.total}}
.move-progress-filename {{moveProgress.name}} ({{moveProgressSize}})
.move-progress-bar-track
.move-progress-bar-fill(style="{{moveProgressBarStyle}}")
.move-progress-bar-track
.move-progress-bar-indeterminate
.move-progress-controls
if moveProgressPaused
button.js-resume-move.btn.btn-secondary {{_ 'move-progress-resume'}}
else
button.js-pause-move.btn.btn-secondary {{_ 'move-progress-pause'}}
button.js-cancel-move.btn.btn-danger {{_ 'move-progress-cancel'}}
File diff suppressed because it is too large. Load diff
+24
View File
@@ -354,3 +354,27 @@ table td:first-child {
height: 100%;
width: 100%;
}
/* Roles tab. The .main-body scrolls horizontally and is wider than the visible
area (the side-menu takes ~250px), so by default a long description would run
off the right edge and only be fully readable at the smallest (mobile) width.
Pin the heading/description to the visible left edge and wrap them within the
viewport so they stay visible at every width. */
.roles-settings {
padding: 0 10px;
}
.roles-settings h3,
.roles-settings h4,
.roles-settings .roles-description {
position: sticky;
left: 0;
/* Cap to ~60 characters, and never wider than the viewport, so the text wraps
into the visible left area at every width (desktop, mid and mobile) instead
of running off the horizontally-scrolling main-body. */
max-width: min(60ch, 88vw);
white-space: normal;
overflow-wrap: break-word;
}
.roles-settings .roles-description {
margin-bottom: 1rem;
}
@@ -63,6 +63,11 @@ template(name="people")
i.fa.fa-lock
unless isMiniScreen
| {{_ 'accounts-lockout-locked-users'}}
else if rolesSetting.get
span
i.fa.fa-key
unless isMiniScreen
| {{_ 'roles'}}
.content-body
.side-menu
@@ -84,6 +89,10 @@ template(name="people")
span.emoji-icon.text-red
i.fa.fa-lock
| {{_ 'accounts-lockout-locked-users'}}
li
a.js-roles-menu(data-id="roles-setting")
i.fa.fa-key
| {{_ 'roles'}}
.main-body
if loading.get
+spinner
@@ -95,6 +104,8 @@ template(name="people")
+peopleGeneral(peopleList=peopleList peopleCurrentPage=peopleCurrentPage peopleTotalPages=peopleTotalPages hasPeoplePrevPage=hasPeoplePrevPage hasPeopleNextPage=hasPeopleNextPage)
else if lockedUsersSetting.get
+lockedUsersGeneral
else if rolesSetting.get
+rolesGeneral
template(name="orgGeneral")
@@ -719,3 +730,22 @@ template(name="lockedUsersGeneral")
input.wekan-form-control#unknown-failure-window(type="number", min="1", max="60", placeholder="15" value="{{unknownFailureWindow}}")
button.js-lockout-save.primary {{_ 'save'}}
template(name="rolesGeneral")
.roles-settings
h3 {{_ 'roles'}}
p.roles-description {{_ 'roles-info'}}
h4 {{_ 'allow-invite-to-board'}}
ul#roles-setting.setting-detail
li
a.flex.js-toggle-all-roles
.materialCheckBox(class="{{#if allRolesAllowed}}is-checked{{/if}}")
span {{_ 'all-board-members'}}
each roleOptions
li
a.flex.js-toggle-role(data-role="{{key}}")
.materialCheckBox(class="{{#if allowed}}is-checked{{/if}}")
span {{_ label}}
button.js-roles-save.primary {{_ 'save'}}
+70
View File
@@ -6,6 +6,10 @@ import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import Org from '/models/org';
import Team from '/models/team';
import Users from '/models/users';
import InviteToBoardRolesSettings, {
INVITE_TO_BOARD_ROLES,
INVITE_TO_BOARD_ROLES_ID,
} from '/models/inviteToBoardRolesSettings';
const orgsPerPage = 25;
const teamsPerPage = 25;
@@ -22,6 +26,8 @@ Template.people.onCreated(function () {
this.teamSetting = new ReactiveVar(false);
this.peopleSetting = new ReactiveVar(false);
this.lockedUsersSetting = new ReactiveVar(false);
this.rolesSetting = new ReactiveVar(false);
this.subscribe('inviteToBoardRolesSettings');
this.findOrgsOptions = new ReactiveVar({});
this.findTeamsOptions = new ReactiveVar({});
this.findUsersOptions = new ReactiveVar({});
@@ -191,6 +197,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.rolesSetting.set('roles-setting' === targetID);
// When switching to locked users tab, refresh the locked users list
if ('locked-users-setting' === targetID) {
@@ -263,6 +270,9 @@ Template.people.helpers({
lockedUsersSetting() {
return Template.instance().lockedUsersSetting;
},
rolesSetting() {
return Template.instance().rolesSetting;
},
orgList() {
const tpl = Template.instance();
const page = tpl.orgPage.get();
@@ -521,6 +531,66 @@ Template.people.events({
'click a.js-locked-users-menu'(event, tpl) {
tpl.switchMenu(event);
},
'click a.js-roles-menu'(event, tpl) {
tpl.switchMenu(event);
},
});
Template.rolesGeneral.onCreated(function () {
// Working copy of the allowed-roles set; null until the published doc loads.
this.workingRoles = new ReactiveVar(null);
this.autorun(() => {
if (this.workingRoles.get() === null) {
const doc = InviteToBoardRolesSettings.findOne(INVITE_TO_BOARD_ROLES_ID);
if (doc) {
this.workingRoles.set((doc.allowedRoles || []).slice());
}
}
});
});
Template.rolesGeneral.helpers({
roleOptions() {
const working = Template.instance().workingRoles.get() || [];
// The role key doubles as the i18n key. 'board-admin' renders as
// "Board Admin", deliberately distinct from the global Admin Panel admin.
return INVITE_TO_BOARD_ROLES.map((key) => ({
key,
label: key,
allowed: working.includes(key),
}));
},
allRolesAllowed() {
const working = Template.instance().workingRoles.get() || [];
return INVITE_TO_BOARD_ROLES.every((key) => working.includes(key));
},
});
Template.rolesGeneral.events({
'click a.js-toggle-role'(event, tpl) {
event.preventDefault();
const role = $(event.currentTarget).data('role');
const working = (tpl.workingRoles.get() || []).slice();
const idx = working.indexOf(role);
if (idx >= 0) {
working.splice(idx, 1);
} else {
working.push(role);
}
tpl.workingRoles.set(working);
},
'click a.js-toggle-all-roles'(event, tpl) {
event.preventDefault();
const working = tpl.workingRoles.get() || [];
const allOn = INVITE_TO_BOARD_ROLES.every((key) => working.includes(key));
tpl.workingRoles.set(allOn ? [] : INVITE_TO_BOARD_ROLES.slice());
},
'click .js-roles-save'(event, tpl) {
event.preventDefault();
InviteToBoardRolesSettings.update(INVITE_TO_BOARD_ROLES_ID, {
$set: { allowedRoles: tpl.workingRoles.get() || [] },
});
},
});
Template.orgRow.helpers({
+32 -3
View File
@@ -21,6 +21,11 @@
}
.setting-content .content-body {
display: flex;
/* Always keep the menu (left) and content (right) side by side, even on
narrow windows. The side menu has a fixed basis and the main body fills
the rest, so both stay visible without switching to a stacked layout. */
flex-direction: row;
flex-wrap: nowrap;
padding-top: 2vh;
height: 100%;
gap: 1.3vw;
@@ -30,6 +35,8 @@
border: 1px solid #f0f0f0;
border-radius: 0.5vw;
width: min(250px, 32vw);
/* Never let the menu be squeezed away on narrow windows; keep it visible. */
flex: 0 0 auto;
box-shadow: inset -0.2vh -0.2vh 0.4vh rgba(0,0,0,0.05);
}
.setting-content .content-body .side-menu ul li {
@@ -62,9 +69,14 @@
overflow-x: scroll !important;
overflow-y: scroll !important;
scrollbar-gutter: stable;
/* Force horizontal scrollbar to always be visible */
min-width: 100%;
width: 100%;
/* Grow into the space left next to the side menu. flex-basis MUST be 0 here:
this element holds a min-width:1200px table and a 100vw ::after, so a
content-based (auto) basis is huge and, as an overflow:scroll flex item,
resolves unpredictably (collapsing the panel). Basis 0 + grow makes the
width = container - side menu, deterministically. min-width:0 lets it
shrink so the side menu is never pushed out on narrow windows. */
flex: 1 1 0;
min-width: 0;
}
/* Ensure scrollbars are always visible with proper styling for all admin pages */
@@ -249,3 +261,20 @@ li.has-error .form-group .wekan-form-control {
max-width: calc(100vw - 240px);
box-sizing: border-box;
}
.setting-content .content-body .main-body .attachment-limit-row {
display: flex;
align-items: center;
gap: 10px;
max-width: 560px;
}
.setting-content .content-body .main-body .attachment-limit-row input.wekan-form-control {
flex: 1;
min-width: 200px;
}
.setting-content .content-body .main-body .attachment-limit-row select.wekan-form-control {
width: 120px;
min-width: 120px;
}
@@ -44,11 +44,6 @@ template(name="setting")
span.emoji-icon
i.fa.fa-globe
| {{_ 'global-webhook'}}
else if isAttachmentSettings
span
span.emoji-icon
i.fa.fa-paperclip
| {{_ 'attachments'}}
//- COMMENTED OUT: Migration settings title section
.content-body
.side-menu
@@ -94,86 +89,10 @@ template(name="setting")
span.emoji-icon
i.fa.fa-globe
| {{_ 'global-webhook'}}
li(class="{{#if isAttachmentSettings}}active{{/if}}")
a.js-setting-menu(data-id="attachment-settings")
span.emoji-icon
i.fa.fa-paperclip
| {{_ 'attachments'}}
//- COMMENTED OUT: Migration menu item
.main-body
if isLoading
+spinner
else if isAttachmentSettings
ul#attachment-setting.setting-detail
li
h3 {{_ 'attachment-storage-configuration'}}
.form-group
label {{_ 'writable-path'}}
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
small.form-text.text-muted {{_ 'filesystem-path-description'}}
.form-group
label {{_ 'attachments-path'}}
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
small.form-text.text-muted {{_ 'attachments-path-description'}}
.form-group
label {{_ 'avatars-path'}}
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
small.form-text.text-muted {{_ 'avatars-path-description'}}
li
h3 {{_ 'mongodb-gridfs-storage'}}
.form-group
label {{_ 'gridfs-enabled'}}
input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
li
h3 {{_ 's3-minio-storage'}}
.form-group
label {{_ 's3-enabled'}}
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
small.form-text.text-muted {{_ 's3-enabled-description'}}
.form-group
label {{_ 's3-endpoint'}}
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
small.form-text.text-muted {{_ 's3-endpoint-description'}}
.form-group
label {{_ 's3-bucket'}}
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
small.form-text.text-muted {{_ 's3-bucket-description'}}
.form-group
label {{_ 's3-region'}}
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
small.form-text.text-muted {{_ 's3-region-description'}}
.form-group
label {{_ 's3-access-key'}}
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
small.form-text.text-muted {{_ 's3-access-key-description'}}
.form-group
label {{_ 's3-secret-key'}}
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
small.form-text.text-muted {{_ 's3-secret-key-description'}}
.form-group
label {{_ 's3-ssl-enabled'}}
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
.form-group
label {{_ 's3-port'}}
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
small.form-text.text-muted {{_ 's3-port-description'}}
.form-group
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
else if isGeneralSetting
+general
else if isEmailSetting
+292
View File
@@ -118,6 +118,128 @@ function cleanAndValidateJSON(content) {
}
}
const LIMIT_UNIT_FACTORS = {
bytes: 1,
mb: 1024 * 1024,
gb: 1024 * 1024 * 1024,
};
const DEFAULT_LIMIT_SETTINGS = {
attachmentsUploadMaxBytes: 0,
attachmentsDownloadMaxBytes: 0,
apiUploadMaxBytes: 50 * 1024 * 1024,
apiDownloadMaxBytes: 20 * 1024 * 1024,
};
function toNonNegativeInteger(value, fallback = 0) {
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed < 0) {
return fallback;
}
return parsed;
}
function normalizeLimitSettings(settingsDoc) {
const fromDoc = settingsDoc?.limitSettings || {};
const legacyUpload = settingsDoc?.uploadSettings?.maxFileSize;
const attachmentsUploadMaxBytes = Number.isFinite(fromDoc.attachmentsUploadMaxBytes)
? toNonNegativeInteger(fromDoc.attachmentsUploadMaxBytes, DEFAULT_LIMIT_SETTINGS.attachmentsUploadMaxBytes)
: (Number.isFinite(legacyUpload)
? toNonNegativeInteger(legacyUpload, DEFAULT_LIMIT_SETTINGS.attachmentsUploadMaxBytes)
: DEFAULT_LIMIT_SETTINGS.attachmentsUploadMaxBytes);
return {
attachmentsUploadMaxBytes,
attachmentsDownloadMaxBytes: Number.isFinite(fromDoc.attachmentsDownloadMaxBytes)
? toNonNegativeInteger(fromDoc.attachmentsDownloadMaxBytes, DEFAULT_LIMIT_SETTINGS.attachmentsDownloadMaxBytes)
: DEFAULT_LIMIT_SETTINGS.attachmentsDownloadMaxBytes,
apiUploadMaxBytes: Number.isFinite(fromDoc.apiUploadMaxBytes)
? toNonNegativeInteger(fromDoc.apiUploadMaxBytes, DEFAULT_LIMIT_SETTINGS.apiUploadMaxBytes)
: DEFAULT_LIMIT_SETTINGS.apiUploadMaxBytes,
apiDownloadMaxBytes: Number.isFinite(fromDoc.apiDownloadMaxBytes)
? toNonNegativeInteger(fromDoc.apiDownloadMaxBytes, DEFAULT_LIMIT_SETTINGS.apiDownloadMaxBytes)
: DEFAULT_LIMIT_SETTINGS.apiDownloadMaxBytes,
};
}
function pickUnitForBytes(bytes) {
const safeBytes = toNonNegativeInteger(bytes, 0);
if (safeBytes > 0 && safeBytes % LIMIT_UNIT_FACTORS.gb === 0) {
return 'gb';
}
if (safeBytes > 0 && safeBytes % LIMIT_UNIT_FACTORS.mb === 0) {
return 'mb';
}
return 'bytes';
}
function toDisplayValue(bytes, unit) {
const safeBytes = toNonNegativeInteger(bytes, 0);
const factor = LIMIT_UNIT_FACTORS[unit] || LIMIT_UNIT_FACTORS.bytes;
return safeBytes / factor;
}
function toBytes(value, unit) {
const numericValue = Number.parseFloat(value);
if (!Number.isFinite(numericValue) || numericValue < 0) {
return null;
}
const factor = LIMIT_UNIT_FACTORS[unit] || LIMIT_UNIT_FACTORS.bytes;
return Math.round(numericValue * factor);
}
function refreshAttachmentStorageSettings(tpl, showLoading = false) {
if (!tpl) {
return;
}
if (showLoading) {
tpl.loading.set(true);
}
Meteor.call('getAttachmentStorageSettings', (error, settings) => {
if (showLoading) {
tpl.loading.set(false);
}
if (error || !settings) {
if (process.env.DEBUG === 'true') {
console.warn('Failed to load attachment storage settings:', error);
}
return;
}
const normalizedLimits = normalizeLimitSettings(settings);
tpl.attachmentStorageSettings.set({
...settings,
limitSettings: normalizedLimits,
});
tpl.attachmentLimitUnits.set({
attachmentsUploadMaxBytes: pickUnitForBytes(normalizedLimits.attachmentsUploadMaxBytes),
attachmentsDownloadMaxBytes: pickUnitForBytes(normalizedLimits.attachmentsDownloadMaxBytes),
apiUploadMaxBytes: pickUnitForBytes(normalizedLimits.apiUploadMaxBytes),
apiDownloadMaxBytes: pickUnitForBytes(normalizedLimits.apiDownloadMaxBytes),
});
tpl.attachmentLimitEnabled.set({
attachmentsUploadMaxBytes: normalizedLimits.attachmentsUploadMaxBytes > 0,
attachmentsDownloadMaxBytes: normalizedLimits.attachmentsDownloadMaxBytes > 0,
apiUploadMaxBytes: normalizedLimits.apiUploadMaxBytes > 0,
apiDownloadMaxBytes: normalizedLimits.apiDownloadMaxBytes > 0,
});
});
}
function getLimitUnitOptions(selectedUnit) {
return [
{ value: 'gb', labelKey: 'attachment-limit-unit-gb', selected: selectedUnit === 'gb' },
{ value: 'mb', labelKey: 'attachment-limit-unit-mb', selected: selectedUnit === 'mb' },
{ value: 'bytes', labelKey: 'attachment-limit-unit-bytes', selected: selectedUnit === 'bytes' },
];
}
Template.setting.onCreated(function () {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
@@ -131,6 +253,19 @@ Template.setting.onCreated(function () {
this.layoutSetting = new ReactiveVar(false);
this.webhookSetting = new ReactiveVar(false);
this.attachmentSettings = new ReactiveVar(false);
this.attachmentStorageSettings = new ReactiveVar(null);
this.attachmentLimitUnits = new ReactiveVar({
attachmentsUploadMaxBytes: 'mb',
attachmentsDownloadMaxBytes: 'mb',
apiUploadMaxBytes: 'mb',
apiDownloadMaxBytes: 'mb',
});
this.attachmentLimitEnabled = new ReactiveVar({
attachmentsUploadMaxBytes: false,
attachmentsDownloadMaxBytes: false,
apiUploadMaxBytes: false,
apiDownloadMaxBytes: false,
});
// this.cronSettings = new ReactiveVar(false);
// this.migrationErrorsList = new ReactiveVar([]);
@@ -142,6 +277,7 @@ Template.setting.onCreated(function () {
Meteor.subscribe('accessibilitySettings');
Meteor.subscribe('globalwebhooks');
Meteor.subscribe('lockoutSettings');
Meteor.subscribe('attachmentStorageSettings');
// Poll for migration errors
// this.errorPollInterval = Meteor.setInterval(() => {
@@ -161,6 +297,52 @@ Template.setting.onDestroyed(function () {
// }
});
Template.setting.onRendered(function () {
this.previousAttachmentLimitEnabled = null;
this.autorun(() => {
const enabledMap = this.attachmentLimitEnabled?.get() || {};
const previousMap = this.previousAttachmentLimitEnabled || {};
const fields = [
'attachmentsUploadMaxBytes',
'attachmentsDownloadMaxBytes',
'apiUploadMaxBytes',
'apiDownloadMaxBytes',
];
fields.forEach((fieldName) => {
const row = this.$(`.attachment-limit-row[data-field="${fieldName}"]`);
if (!row.length) {
return;
}
const isEnabled = enabledMap[fieldName] === true;
const wasEnabled = previousMap[fieldName];
if (typeof wasEnabled === 'undefined') {
if (isEnabled) {
row.show();
} else {
row.hide();
}
return;
}
if (isEnabled === wasEnabled) {
return;
}
if (isEnabled) {
row.stop(true, true).hide().slideDown();
} else {
row.stop(true, true).slideUp();
}
});
this.previousAttachmentLimitEnabled = { ...enabledMap };
});
});
Template.setting.helpers({
isGeneralSetting() {
const inst = Template.instance();
@@ -252,6 +434,28 @@ Template.setting.helpers({
return process.env.S3_PORT || 443;
},
attachmentTransferLimitValue(fieldName) {
const tpl = Template.instance();
const settingsDoc = tpl.attachmentStorageSettings.get();
const units = tpl.attachmentLimitUnits.get() || {};
const limits = normalizeLimitSettings(settingsDoc);
const unit = units[fieldName] || 'bytes';
return toDisplayValue(limits[fieldName], unit);
},
attachmentTransferLimitUnitOptions(fieldName) {
const tpl = Template.instance();
const units = tpl.attachmentLimitUnits.get() || {};
const selectedUnit = units[fieldName] || 'bytes';
return getLimitUnitOptions(selectedUnit);
},
isAttachmentLimitEnabled(fieldName) {
const tpl = Template.instance();
const enabledMap = tpl.attachmentLimitEnabled.get() || {};
return enabledMap[fieldName] === true;
},
// Cron settings helpers
// migrationStatus() {
// return cronMigrationStatus.get() || TAPi18n.__('idle');
@@ -466,6 +670,7 @@ Template.setting.events({
tpl.webhookSetting.set(true);
} else if (targetID === 'attachment-settings') {
tpl.attachmentSettings.set(true);
refreshAttachmentStorageSettings(tpl, true);
// Set default sub-menu state for attachment settings
console.log('Initializing attachment sub-menu');
} // else if (targetID === 'cron-settings') {
@@ -800,6 +1005,93 @@ Template.setting.events({
});
},
'change select.js-attachment-limit-unit'(event, tpl) {
const fieldName = event.currentTarget.dataset.field;
const selectedUnit = event.currentTarget.value;
if (!fieldName || !selectedUnit) {
return;
}
const current = tpl.attachmentLimitUnits.get() || {};
tpl.attachmentLimitUnits.set({
...current,
[fieldName]: selectedUnit,
});
},
'click a.js-toggle-attachment-limit'(event, tpl) {
event.preventDefault();
const fieldName = event.currentTarget.dataset.field;
if (!fieldName) {
return;
}
const current = tpl.attachmentLimitEnabled.get() || {};
tpl.attachmentLimitEnabled.set({
...current,
[fieldName]: !current[fieldName],
});
},
'click button.js-save-attachment-transfer-limits'(event, tpl) {
event.preventDefault();
const currentSettings = tpl.attachmentStorageSettings.get();
if (!currentSettings) {
alert(TAPi18n.__('attachment-transfer-limits-save-failed'));
return;
}
const currentUnits = tpl.attachmentLimitUnits.get() || {};
const enabledMap = tpl.attachmentLimitEnabled.get() || {};
const fieldConfig = [
{ fieldName: 'attachmentsUploadMaxBytes', inputId: '#attachments-upload-limit-value' },
{ fieldName: 'attachmentsDownloadMaxBytes', inputId: '#attachments-download-limit-value' },
{ fieldName: 'apiUploadMaxBytes', inputId: '#api-upload-limit-value' },
{ fieldName: 'apiDownloadMaxBytes', inputId: '#api-download-limit-value' },
];
const nextLimitSettings = {};
for (const field of fieldConfig) {
if (!enabledMap[field.fieldName]) {
nextLimitSettings[field.fieldName] = 0;
continue;
}
const unit = currentUnits[field.fieldName] || 'bytes';
const value = $(field.inputId).val();
const bytesValue = toBytes(value, unit);
if (bytesValue === null || bytesValue <= 0) {
alert(TAPi18n.__('attachment-transfer-limits-invalid-value'));
return;
}
nextLimitSettings[field.fieldName] = bytesValue;
}
const nextSettings = {
...currentSettings,
uploadSettings: {
...(currentSettings.uploadSettings || {}),
// Keep legacy field in sync for backward compatibility.
maxFileSize: nextLimitSettings.attachmentsUploadMaxBytes,
},
limitSettings: {
...(currentSettings.limitSettings || {}),
...nextLimitSettings,
},
};
Meteor.call('updateAttachmentStorageSettings', nextSettings, (error) => {
if (error) {
alert(`${TAPi18n.__('attachment-transfer-limits-save-failed')}: ${error.reason || error.message}`);
return;
}
alert(TAPi18n.__('attachment-transfer-limits-saved'));
refreshAttachmentStorageSettings(tpl, false);
});
},
// Event handlers for cron settings
// 'click button.js-start-migration'(event, tpl) {
// event.preventDefault();
@@ -26,3 +26,26 @@
margin-top: 1px;
margin-right: 10px;
}
/* Keep all admin tabs (Settings, People, Reports, Attachments, Translation,
Version) visible on narrow screens. The bar is a fixed 40px tall and the
tabs float, so on narrow widths they wrapped below the bar and were clipped
(disappeared). Let the bar grow and wrap the tabs to as many rows as needed
so none are ever hidden. Scoped via :has() so the board header is untouched. */
#header #header-main-bar:has(.setting-header-btns) {
height: auto;
min-height: 40px;
padding-bottom: 6px;
overflow: visible;
}
#header #header-main-bar .setting-header-btns.left {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
float: none;
}
#header #header-main-bar .setting-header-btn {
float: none;
margin: 2px 0;
}
+2 -12
View File
@@ -82,7 +82,7 @@ template(name="membersWidget")
if currentUser.isBoardMember
a.member.add-member.sandstorm-powerbox-request-identity(title="{{_ 'add-members'}}")
i.fa.fa-plus
else if currentUser.isBoardAdmin
else if currentUser.canInviteToBoard
a.member.add-member.js-manage-board-members(title="{{_ 'add-members'}}")
i.fa.fa-plus
.clearfix
@@ -368,16 +368,6 @@ template(name="boardCardSettingsPopup")
i.fa.fa-file-text-o
| {{_ 'description'}}
| {{_ 'custom-field-text'}}
.card-settings-row
.card-settings-column
span
.card-settings-column
a.flex.js-field-has-description-text-on-minicard(title="{{_ 'description-on-minicard'}}" class="{{#if allowsDescriptionTextOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsDescriptionTextOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-file-text-o
| {{_ 'description-on-minicard'}}
.card-settings-row
.card-settings-column
a.flex.js-field-has-checklists(title="{{_ 'checklists'}}" class="{{#if allowsChecklists}}is-checked{{/if}}")
@@ -504,7 +494,7 @@ template(name="boardSubtaskSettingsPopup")
else
option(value=_id) {{title}}
template(name="chooseBoardSource")
template(name="chooseBoardSourcePopup")
ul.pop-over-list
li
a(href="{{pathFor '/import/trello'}}") {{_ 'from-trello'}}
+71 -179
View File
@@ -24,6 +24,17 @@ const defaultView = 'home';
const MCB = '.materialCheckBox';
const CKCLS = 'is-checked';
function getMinicardSetting(board, onMinicardField, cardField, defaultValue) {
if (!board) return false;
if (board[onMinicardField] !== null && board[onMinicardField] !== undefined) {
return board[onMinicardField];
}
if (cardField && board[cardField] !== null && board[cardField] !== undefined) {
return board[cardField];
}
return defaultValue;
}
const viewTitles = {
filter: 'filter-cards',
search: 'search-cards',
@@ -185,13 +196,20 @@ Template.sidebar.events({
FlowRouter.go('shortcuts');
},
'click .js-keyboard-shortcuts-toggle'() {
ReactiveCache.getCurrentUser().toggleKeyboardShortcuts();
const user = ReactiveCache.getCurrentUser();
if (user) user.toggleKeyboardShortcuts();
},
'click .js-vertical-scrollbars-toggle'() {
ReactiveCache.getCurrentUser().toggleVerticalScrollbars();
},
'click .js-show-week-of-year-toggle'() {
ReactiveCache.getCurrentUser().toggleShowWeekOfYear();
const user = ReactiveCache.getCurrentUser();
if (user) {
user.toggleShowWeekOfYear();
} else {
const current = window.localStorage.getItem('showWeekOfYear') === 'true';
window.localStorage.setItem('showWeekOfYear', String(!current));
}
},
'click .sidebar-accessibility'(event, tpl) {
FlowRouter.go('accessibility');
@@ -226,7 +244,8 @@ Template.homeSidebar.helpers({
},
isShowWeekOfYear() {
const user = ReactiveCache.getCurrentUser();
return user && user.isShowWeekOfYear();
if (!user) return window.localStorage.getItem('showWeekOfYear') === 'true';
return user.isShowWeekOfYear();
},
showActivities() {
let ret = Utils.getCurrentBoard().showActivities ?? false;
@@ -371,10 +390,12 @@ Template.boardMenuPopup.events({
}),
'click .js-archive-board ': Popup.afterConfirm('archiveBoard', async function() {
const currentBoard = Utils.getCurrentBoard();
await currentBoard.archive();
// XXX We should have some kind of notification on top of the page to
// confirm that the board was successfully archived.
FlowRouter.go('home');
try {
await Meteor.callAsync('archiveBoard', currentBoard._id);
FlowRouter.go('home');
} catch (err) {
alert(err?.reason || err?.message || 'Failed to archive board');
}
}),
'click .js-delete-board': Popup.afterConfirm('deleteBoard', function() {
const currentBoard = Utils.getCurrentBoard();
@@ -1032,7 +1053,7 @@ Template.boardCardSettingsPopup.helpers({
allowsReceivedDateOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsReceivedDateOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsReceivedDateOnMinicard', 'allowsReceivedDate', true);
},
allowsStartDate() {
const boardId = Session.get('currentBoard');
@@ -1042,7 +1063,7 @@ Template.boardCardSettingsPopup.helpers({
allowsStartDateOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsStartDateOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsStartDateOnMinicard', 'allowsStartDate', true);
},
allowsDueDate() {
const boardId = Session.get('currentBoard');
@@ -1052,7 +1073,7 @@ Template.boardCardSettingsPopup.helpers({
allowsDueDateOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDueDateOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsDueDateOnMinicard', 'allowsDueDate', true);
},
allowsEndDate() {
const boardId = Session.get('currentBoard');
@@ -1062,7 +1083,7 @@ Template.boardCardSettingsPopup.helpers({
allowsEndDateOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsEndDateOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsEndDateOnMinicard', 'allowsEndDate', true);
},
allowsSubtasks() {
const boardId = Session.get('currentBoard');
@@ -1072,7 +1093,7 @@ Template.boardCardSettingsPopup.helpers({
allowsSubtasksOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsSubtasksOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsSubtasksOnMinicard', 'allowsSubtasks', true);
},
allowsCreator() {
const boardId = Session.get('currentBoard');
@@ -1082,7 +1103,7 @@ Template.boardCardSettingsPopup.helpers({
allowsCreatorOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? (currentBoard.allowsCreatorOnMinicard ?? false) : false;
return getMinicardSetting(currentBoard, 'allowsCreatorOnMinicard', 'allowsCreator', false);
},
allowsMembers() {
const boardId = Session.get('currentBoard');
@@ -1092,7 +1113,7 @@ Template.boardCardSettingsPopup.helpers({
allowsMembersOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsMembersOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsMembersOnMinicard', 'allowsMembers', true);
},
allowsAssignee() {
const boardId = Session.get('currentBoard');
@@ -1102,7 +1123,7 @@ Template.boardCardSettingsPopup.helpers({
allowsAssigneeOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAssigneeOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsAssigneeOnMinicard', 'allowsAssignee', true);
},
allowsAssignedBy() {
const boardId = Session.get('currentBoard');
@@ -1112,7 +1133,7 @@ Template.boardCardSettingsPopup.helpers({
allowsAssignedByOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAssignedByOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsAssignedByOnMinicard', 'allowsAssignedBy', true);
},
allowsRequestedBy() {
const boardId = Session.get('currentBoard');
@@ -1122,7 +1143,7 @@ Template.boardCardSettingsPopup.helpers({
allowsRequestedByOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsRequestedByOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsRequestedByOnMinicard', 'allowsRequestedBy', true);
},
allowsCardSortingByNumber() {
const boardId = Session.get('currentBoard');
@@ -1142,12 +1163,12 @@ Template.boardCardSettingsPopup.helpers({
allowsLabelsOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsLabelsOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsLabelsOnMinicard', 'allowsLabels', true);
},
allowsShowListsOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsShowListsOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsShowListsOnMinicard', 'allowsShowLists', false);
},
allowsChecklists() {
const boardId = Session.get('currentBoard');
@@ -1157,7 +1178,7 @@ Template.boardCardSettingsPopup.helpers({
allowsChecklistsOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsChecklistsOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsChecklistsOnMinicard', 'allowsChecklists', true);
},
allowsAttachments() {
const boardId = Session.get('currentBoard');
@@ -1167,7 +1188,7 @@ Template.boardCardSettingsPopup.helpers({
allowsAttachmentsOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAttachmentsOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsAttachmentsOnMinicard', 'allowsAttachments', true);
},
allowsComments() {
const boardId = Session.get('currentBoard');
@@ -1182,7 +1203,7 @@ Template.boardCardSettingsPopup.helpers({
allowsCardNumberOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsCardNumberOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsCardNumberOnMinicard', 'allowsCardNumber', false);
},
allowsDescriptionTitle() {
const boardId = Session.get('currentBoard');
@@ -1192,7 +1213,7 @@ Template.boardCardSettingsPopup.helpers({
allowsDescriptionTitleOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDescriptionTitleOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsDescriptionTitleOnMinicard', 'allowsDescriptionTitle', true);
},
allowsDescriptionText() {
const boardId = Session.get('currentBoard');
@@ -1202,7 +1223,7 @@ Template.boardCardSettingsPopup.helpers({
isBoardSelected() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.dateSettingsDefaultBoardID : false;
return currentBoard ? currentBoard.dateSettingsDefaultBoardId : false;
},
isNullBoardSelected() {
const boardId = Session.get('currentBoard');
@@ -1215,7 +1236,7 @@ Template.boardCardSettingsPopup.helpers({
allowsDescriptionTextOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDescriptionTextOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsDescriptionTextOnMinicard', 'allowsDescriptionText', false);
},
allowsCoverAttachmentOnMinicard() {
const boardId = Session.get('currentBoard');
@@ -1230,7 +1251,7 @@ Template.boardCardSettingsPopup.helpers({
allowsCardSortingByNumberOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsCardSortingByNumberOnMinicard : false;
return getMinicardSetting(currentBoard, 'allowsCardSortingByNumberOnMinicard', 'allowsCardSortingByNumber', false);
},
boards() {
const ret = ReactiveCache.getBoards(
@@ -1400,35 +1421,13 @@ Template.boardCardSettingsPopup.events({
},
'click .js-field-has-card-show-lists-on-minicard'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsShowListsOnMinicard = !tpl.currentBoard
.allowsShowListsOnMinicard;
tpl.currentBoard.setAllowsShowListsOnMinicard(
tpl.currentBoard.allowsShowListsOnMinicard,
);
$(`.js-field-has-card-show-lists-on-minicard ${MCB}`).toggleClass(
CKCLS,
tpl.currentBoard.allowsShowListsOnMinicard,
);
$('.js-field-has-card-show-lists-on-minicard').toggleClass(
CKCLS,
tpl.currentBoard.allowsShowListsOnMinicard,
);
const newValue = !tpl.currentBoard.allowsShowListsOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsShowListsOnMinicard: newValue } });
},
'click .js-field-has-description-title'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsDescriptionTitle = !tpl.currentBoard
.allowsDescriptionTitle;
tpl.currentBoard.setAllowsDescriptionTitle(
tpl.currentBoard.allowsDescriptionTitle,
);
$(`.js-field-has-description-title ${MCB}`).toggleClass(
CKCLS,
tpl.currentBoard.allowsDescriptionTitle,
);
$('.js-field-has-description-title').toggleClass(
CKCLS,
tpl.currentBoard.allowsDescriptionTitle,
);
const newValue = !tpl.currentBoard.allowsDescriptionTitle;
Boards.update(tpl.currentBoard._id, { $set: { allowsDescriptionTitle: newValue } });
},
'click .js-field-has-description-title-on-minicard'(evt, tpl) {
evt.preventDefault();
@@ -1437,19 +1436,8 @@ Template.boardCardSettingsPopup.events({
},
'click .js-field-has-card-number'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsCardNumber = !tpl.currentBoard
.allowsCardNumber;
tpl.currentBoard.setAllowsCardNumber(
tpl.currentBoard.allowsCardNumber,
);
$(`.js-field-has-card-number ${MCB}`).toggleClass(
CKCLS,
tpl.currentBoard.allowsCardNumber,
);
$('.js-field-has-card-number').toggleClass(
CKCLS,
tpl.currentBoard.allowsCardNumber,
);
const newValue = !tpl.currentBoard.allowsCardNumber;
Boards.update(tpl.currentBoard._id, { $set: { allowsCardNumber: newValue } });
},
'click .js-field-has-card-number-on-minicard'(evt, tpl) {
evt.preventDefault();
@@ -1458,51 +1446,18 @@ Template.boardCardSettingsPopup.events({
},
'click .js-field-has-description-text-on-minicard'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsDescriptionTextOnMinicard = !tpl.currentBoard
.allowsDescriptionTextOnMinicard;
tpl.currentBoard.setallowsDescriptionTextOnMinicard(
tpl.currentBoard.allowsDescriptionTextOnMinicard,
);
$(`.js-field-has-description-text-on-minicard ${MCB}`).toggleClass(
CKCLS,
tpl.currentBoard.allowsDescriptionTextOnMinicard,
);
$('.js-field-has-description-text-on-minicard').toggleClass(
CKCLS,
tpl.currentBoard.allowsDescriptionTextOnMinicard,
);
const newValue = !tpl.currentBoard.allowsDescriptionTextOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsDescriptionTextOnMinicard: newValue } });
},
'click .js-field-has-description-text'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsDescriptionText = !tpl.currentBoard
.allowsDescriptionText;
tpl.currentBoard.setAllowsDescriptionText(
tpl.currentBoard.allowsDescriptionText,
);
$(`.js-field-has-description-text ${MCB}`).toggleClass(
CKCLS,
tpl.currentBoard.allowsDescriptionText,
);
$('.js-field-has-description-text').toggleClass(
CKCLS,
tpl.currentBoard.allowsDescriptionText,
);
const newValue = !tpl.currentBoard.allowsDescriptionText;
Boards.update(tpl.currentBoard._id, { $set: { allowsDescriptionText: newValue } });
},
'click .js-field-has-checklists'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsChecklists = !tpl.currentBoard
.allowsChecklists;
tpl.currentBoard.setAllowsChecklists(
tpl.currentBoard.allowsChecklists,
);
$(`.js-field-has-checklists ${MCB}`).toggleClass(
CKCLS,
tpl.currentBoard.allowsChecklists,
);
$('.js-field-has-checklists').toggleClass(
CKCLS,
tpl.currentBoard.allowsChecklists,
);
const newValue = !tpl.currentBoard.allowsChecklists;
Boards.update(tpl.currentBoard._id, { $set: { allowsChecklists: newValue } });
},
'click .js-field-has-checklists-on-minicard'(evt, tpl) {
evt.preventDefault();
@@ -1511,19 +1466,8 @@ Template.boardCardSettingsPopup.events({
},
'click .js-field-has-attachments'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsAttachments = !tpl.currentBoard
.allowsAttachments;
tpl.currentBoard.setAllowsAttachments(
tpl.currentBoard.allowsAttachments,
);
$(`.js-field-has-attachments ${MCB}`).toggleClass(
CKCLS,
tpl.currentBoard.allowsAttachments,
);
$('.js-field-has-attachments').toggleClass(
CKCLS,
tpl.currentBoard.allowsAttachments,
);
const newValue = !tpl.currentBoard.allowsAttachments;
Boards.update(tpl.currentBoard._id, { $set: { allowsAttachments: newValue } });
},
'click .js-field-has-attachments-on-minicard'(evt, tpl) {
evt.preventDefault();
@@ -1532,80 +1476,28 @@ Template.boardCardSettingsPopup.events({
},
'click .js-field-has-comments'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsComments = !tpl.currentBoard.allowsComments;
tpl.currentBoard.setAllowsComments(tpl.currentBoard.allowsComments);
$(`.js-field-has-comments ${MCB}`).toggleClass(
CKCLS,
tpl.currentBoard.allowsComments,
);
$('.js-field-has-comments').toggleClass(
CKCLS,
tpl.currentBoard.allowsComments,
);
const newValue = !tpl.currentBoard.allowsComments;
Boards.update(tpl.currentBoard._id, { $set: { allowsComments: newValue } });
},
'click .js-field-has-activities'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsActivities = !tpl.currentBoard
.allowsActivities;
tpl.currentBoard.setAllowsActivities(
tpl.currentBoard.allowsActivities,
);
$(`.js-field-has-activities ${MCB}`).toggleClass(
CKCLS,
tpl.currentBoard.allowsActivities,
);
$('.js-field-has-activities').toggleClass(
CKCLS,
tpl.currentBoard.allowsActivities,
);
const newValue = !tpl.currentBoard.allowsActivities;
Boards.update(tpl.currentBoard._id, { $set: { allowsActivities: newValue } });
},
'click .js-field-has-cover-attachment-on-minicard'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsCoverAttachmentOnMinicard = !tpl.currentBoard
.allowsCoverAttachmentOnMinicard;
tpl.currentBoard.setallowsCoverAttachmentOnMinicard(
tpl.currentBoard.allowsCoverAttachmentOnMinicard,
);
$(`.js-field-has-cover-attachment-on-minicard ${MCB}`).toggleClass(
CKCLS,
tpl.currentBoard.allowsCoverAttachmentOnMinicard,
);
$('.js-field-has-cover-attachment-on-minicard').toggleClass(
CKCLS,
tpl.currentBoard.allowsCoverAttachmentOnMinicard,
);
const newValue = !tpl.currentBoard.allowsCoverAttachmentOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsCoverAttachmentOnMinicard: newValue } });
},
'click .js-field-has-badge-attachment-on-minicard'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsBadgeAttachmentOnMinicard = !tpl.currentBoard
.allowsBadgeAttachmentOnMinicard;
tpl.currentBoard.setallowsBadgeAttachmentOnMinicard(
tpl.currentBoard.allowsBadgeAttachmentOnMinicard,
);
$(`.js-field-has-badge-attachment-on-minicard ${MCB}`).toggleClass(
CKCLS,
tpl.currentBoard.allowsBadgeAttachmentOnMinicard,
);
$('.js-field-has-badge-attachment-on-minicard').toggleClass(
CKCLS,
tpl.currentBoard.allowsBadgeAttachmentOnMinicard,
);
const newValue = !tpl.currentBoard.allowsBadgeAttachmentOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsBadgeAttachmentOnMinicard: newValue } });
},
'click .js-field-has-card-sorting-by-number-on-minicard'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsCardSortingByNumberOnMinicard = !tpl.currentBoard
.allowsCardSortingByNumberOnMinicard;
tpl.currentBoard.setallowsCardSortingByNumberOnMinicard(
tpl.currentBoard.allowsCardSortingByNumberOnMinicard,
);
$(`.js-field-has-card-sorting-by-number-on-minicard ${MCB}`).toggleClass(
CKCLS,
tpl.currentBoard.allowsCardSortingByNumberOnMinicard,
);
$('.js-field-has-card-sorting-by-number-on-minicard').toggleClass(
CKCLS,
tpl.currentBoard.allowsCardSortingByNumberOnMinicard,
);
const newValue = !tpl.currentBoard.allowsCardSortingByNumberOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsCardSortingByNumberOnMinicard: newValue } });
},
});
+29 -26
View File
@@ -6,14 +6,15 @@ template(name="archivesSidebar")
li.tab-item(class="{{isArchiveTabActive slug}} {{class}}" data-tab="{{slug}}") {{name}}
.tabs-content-container
section.tabs-content(class="{{isArchiveTabActive 'cards'}}" data-tab="cards")
unless isWorker
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
p.quiet
a.js-restore-all-cards {{_ 'restore-all'}}
if currentUser.isBoardAdmin
| -
a.js-delete-all-cards {{_ 'delete-all'}}
if currentUser
unless isWorker
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
p.quiet
a.js-restore-all-cards {{_ 'restore-all'}}
if currentUser.isBoardAdmin
| -
a.js-delete-all-cards {{_ 'delete-all'}}
each archivedCards
.minicard-wrapper.js-minicard
+minicard(this)
@@ -36,18 +37,19 @@ template(name="archivesSidebar")
p.no-items-message {{_ 'no-archived-cards'}}
section.tabs-content(class="{{isArchiveTabActive 'lists'}}" data-tab="lists")
unless isWorker
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
p.quiet
a.js-restore-all-lists {{_ 'restore-all'}}
if currentUser.isBoardAdmin
| -
a.js-delete-all-lists {{_ 'delete-all'}}
if currentUser
unless isWorker
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
p.quiet
a.js-restore-all-lists {{_ 'restore-all'}}
if currentUser.isBoardAdmin
| -
a.js-delete-all-lists {{_ 'delete-all'}}
ul.archived-lists
each archivedLists
li.archived-lists-item
= title
span.archived-item-title {{title}}
if currentUser.isBoardMember
unless isWorker
unless currentUser.isReadOnly
@@ -65,18 +67,19 @@ template(name="archivesSidebar")
li.no-items-message {{_ 'no-archived-lists'}}
section.tabs-content(class="{{isArchiveTabActive 'swimlanes'}}" data-tab="swimlanes")
unless isWorker
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
p.quiet
a.js-restore-all-swimlanes {{_ 'restore-all'}}
if currentUser.isBoardAdmin
| -
a.js-delete-all-swimlanes {{_ 'delete-all'}}
if currentUser
unless isWorker
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
p.quiet
a.js-restore-all-swimlanes {{_ 'restore-all'}}
if currentUser.isBoardAdmin
| -
a.js-delete-all-swimlanes {{_ 'delete-all'}}
ul.archived-lists
each archivedSwimlanes
li.archived-lists-item
= title
span.archived-item-title {{title}}
if currentUser.isBoardMember
unless isWorker
unless currentUser.isReadOnly
+5 -4
View File
@@ -151,13 +151,14 @@ Template.archivesSidebar.helpers({
return Template.instance().isArchiveReady;
},
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
const user = ReactiveCache.getCurrentUser();
return user && user.isBoardAdmin();
},
isWorker() {
const currentBoard = Utils.getCurrentBoard();
return (
!currentBoard.hasAdmin(this.userId) && currentBoard.hasWorker(this.userId)
);
if (!currentBoard) return false;
const userId = Meteor.userId();
return !currentBoard.hasAdmin(userId) && currentBoard.hasWorker(userId);
},
tabs() {
@@ -139,6 +139,15 @@ Template.createCustomFieldPopup.helpers({
});
},
// The template iterates `{{#each dropdownItems.get}}`. This component used to
// be a BlazeComponent, where `dropdownItems` resolved to the instance's
// ReactiveVar; after the migration to a plain Template that binding rendered
// nothing (so dropdown options never appeared and were lost on save). Expose
// the ReactiveVar as a helper again so `dropdownItems.get` works.
dropdownItems() {
return Template.instance().dropdownItems;
},
getDropdownItems() {
return getDropdownItems(Template.instance());
},
@@ -21,14 +21,14 @@ template(name="filterSidebar")
| {{_ 'filter-labels-label'}}
ul.sidebar-list
li(class="{{#if Filter.labelIds.isSelected undefined}}active{{/if}}")
a.name.js-toggle-label-filter
a.name.js-toggle-label-filter(data-filter-id="__none__")
span.sidebar-list-item-description
| {{_ 'filter-no-label'}}
if Filter.labelIds.isSelected undefined
i.fa.fa-check
each currentBoard.labels
li
a.name.js-toggle-label-filter
a.name.js-toggle-label-filter(data-filter-id="{{_id}}")
span.card-label.square(class="card-label-{{color}}")
span.sidebar-list-item-description
if name
@@ -43,7 +43,7 @@ template(name="filterSidebar")
| {{_ 'filter-member-label'}}
ul.sidebar-list
li(class="{{#if Filter.members.isSelected undefined}}active{{/if}}")
a.name.js-toggle-member-filter
a.name.js-toggle-member-filter(data-filter-id="__none__")
span.sidebar-list-item-description
| {{_ 'filter-no-member'}}
if Filter.members.isSelected undefined
@@ -51,7 +51,7 @@ template(name="filterSidebar")
each currentBoard.activeMembers
with getUser userId
li(class="{{#if Filter.members.isSelected _id}}active{{/if}}")
a.name.js-toggle-member-filter
a.name.js-toggle-member-filter(data-filter-id="{{_id}}")
+userAvatar(userId=this._id)
span.sidebar-list-item-description
= profile.fullname
@@ -64,7 +64,7 @@ template(name="filterSidebar")
| {{_ 'filter-assignee-label'}}
ul.sidebar-list
li(class="{{#if Filter.assignees.isSelected undefined}}active{{/if}}")
a.name.js-toggle-assignee-filter
a.name.js-toggle-assignee-filter(data-filter-id="__none__")
span.sidebar-list-item-description
| {{_ 'filter-no-assignee'}}
if Filter.assignees.isSelected undefined
@@ -72,7 +72,7 @@ template(name="filterSidebar")
each currentBoard.activeMembers
with getUser userId
li(class="{{#if Filter.assignees.isSelected _id}}active{{/if}}")
a.name.js-toggle-assignee-filter
a.name.js-toggle-assignee-filter(data-filter-id="{{_id}}")
+userAvatar(userId=this._id)
span.sidebar-list-item-description
= profile.fullname
@@ -126,14 +126,14 @@ template(name="filterSidebar")
| {{_ 'filter-custom-fields-label'}}
ul.sidebar-list
li(class="{{#if Filter.customFields.isSelected undefined}}active{{/if}}")
a.name.js-toggle-custom-fields-filter
a.name.js-toggle-custom-fields-filter(data-filter-id="__none__")
span.sidebar-list-item-description
| {{_ 'filter-no-custom-fields'}}
if Filter.customFields.isSelected undefined
i.fa.fa-check
each currentBoard.customFields
li(class="{{#if Filter.customFields.isSelected _id}}active{{/if}}")
a.name.js-toggle-custom-fields-filter
a.name.js-toggle-custom-fields-filter(data-filter-id="{{_id}}")
span.sidebar-list-item-description
| {{ name }}
if Filter.customFields.isSelected _id
+15 -4
View File
@@ -7,6 +7,17 @@ import { Utils } from '/client/lib/utils';
// SubsManager removed for Meteor 3 migration
function getFilterIdFromEvent(evt, fallbackId) {
const filterId = evt.currentTarget?.getAttribute('data-filter-id');
if (filterId === '__none__') {
return undefined;
}
if (filterId !== null) {
return filterId;
}
return fallbackId;
}
Template.filterSidebar.events({
'submit .js-list-filter'(evt, tpl) {
evt.preventDefault();
@@ -19,17 +30,17 @@ Template.filterSidebar.events({
},
'click .js-toggle-label-filter'(evt) {
evt.preventDefault();
Filter.labelIds.toggle(Template.currentData()._id);
Filter.labelIds.toggle(getFilterIdFromEvent(evt, this?._id));
Filter.resetExceptions();
},
'click .js-toggle-member-filter'(evt) {
evt.preventDefault();
Filter.members.toggle(Template.currentData()._id);
Filter.members.toggle(getFilterIdFromEvent(evt, this?._id));
Filter.resetExceptions();
},
'click .js-toggle-assignee-filter'(evt) {
evt.preventDefault();
Filter.assignees.toggle(Template.currentData()._id);
Filter.assignees.toggle(getFilterIdFromEvent(evt, this?._id));
Filter.resetExceptions();
},
'click .js-toggle-no-due-date-filter'(evt) {
@@ -81,7 +92,7 @@ Template.filterSidebar.events({
},
'click .js-toggle-custom-fields-filter'(evt) {
evt.preventDefault();
Filter.customFields.toggle(Template.currentData()._id);
Filter.customFields.toggle(getFilterIdFromEvent(evt, this?._id));
Filter.resetExceptions();
},
'change .js-field-advanced-filter'(evt, tpl) {
+25 -8
View File
@@ -475,9 +475,19 @@ function initializeSwimlaneResize(tpl, retryCount = 0) {
const minHeight = 100;
const maxHeight = 2000;
// Read the vertical page coordinate from either a jQuery mouse event or a
// native touch event (touchstart/move expose `touches`, touchend exposes
// `changedTouches`).
const getEventPageY = (e) => {
const oe = e.originalEvent || e;
if (oe.touches && oe.touches.length) return oe.touches[0].pageY;
if (oe.changedTouches && oe.changedTouches.length) return oe.changedTouches[0].pageY;
return e.pageY;
};
const startResize = (e) => {
isResizing = true;
startY = e.pageY || e.originalEvent.touches[0].pageY;
startY = getEventPageY(e);
startHeight = parseInt($swimlane.css('height')) || 300;
$swimlane.addClass('swimlane-resizing');
@@ -493,7 +503,7 @@ function initializeSwimlaneResize(tpl, retryCount = 0) {
return;
}
const currentY = e.pageY || e.originalEvent.touches[0].pageY;
const currentY = getEventPageY(e);
const deltaY = currentY - startY;
const newHeight = Math.max(minHeight, Math.min(maxHeight, startHeight + deltaY));
@@ -517,7 +527,7 @@ function initializeSwimlaneResize(tpl, retryCount = 0) {
isResizing = false;
// Calculate final height
const currentY = e.pageY || e.originalEvent.touches[0].pageY;
const currentY = getEventPageY(e);
const deltaY = currentY - startY;
const finalHeight = Math.max(minHeight, Math.min(maxHeight, startHeight + deltaY));
@@ -582,10 +592,15 @@ function initializeSwimlaneResize(tpl, retryCount = 0) {
$(document).on('mousemove', doResize);
$(document).on('mouseup', stopResize);
// Touch events for mobile
$resizeHandle.on('touchstart', startResize, { passive: false });
$(document).on('touchmove', doResize, { passive: false });
$(document).on('touchend', stopResize, { passive: false });
// Touch events for mobile.
// NOTE: jQuery's .on() does not accept an addEventListener-style options
// object — passing { passive: false } made jQuery bind the object itself as
// the handler, throwing "handler.apply is not a function" on every touch.
// Use the native API so { passive: false } actually applies and
// preventDefault() works during the resize.
$resizeHandle[0].addEventListener('touchstart', startResize, { passive: false });
document.addEventListener('touchmove', doResize, { passive: false });
document.addEventListener('touchend', stopResize, { passive: false });
// Prevent dragscroll interference
$resizeHandle.on('mousedown', (e) => {
@@ -655,7 +670,9 @@ Template.swimlane.helpers({
}
}
if (Filter.hideEmpty.isSelected()) {
const cards = list.cards();
// Pass the current swimlane ID so we only count cards belonging to
// this swimlane (not cards in other swimlanes that happen to match).
const cards = list.cards(this._id);
if (cards.length === 0) {
return false;
}
+1 -1
View File
@@ -3,7 +3,7 @@ template(name='passwordInput')
label(for='at-field-{{_id}}') {{displayName}}
.password-input-container
input.password-field(type="{{type}}" placeholder="{{displayName}}" autocomplete="{{autocomplete}}" required="{{required}}")
button.password-toggle-btn(type="button" tabindex="-1" aria-label="Toggle password visibility" title="Toggle password visibility")
button.password-toggle-btn.primary(type="button" tabindex="-1" aria-label="Toggle password visibility" title="Toggle password visibility")
i.fa.fa-eye.eye-icon
i.fa.fa-eye-slash.eye-slash-icon
if errs
+41 -49
View File
@@ -25,65 +25,57 @@
width: 100%;
}
.password-input-container {
position: relative;
display: flex;
align-items: center;
align-self: stretch;
align-items: stretch;
column-gap: 0.5rem;
}
.password-input-container input {
flex: 1;
padding-right: 55px; /* More room for the bigger button */
width: 100%;
box-sizing: border-box;
}
.password-toggle-btn {
position: absolute;
right: 5px; /* Adjusted for larger button */
top: calc(50% - 26px); /* Moved up by 20px + 6px = 26px total */
transform: translateY(-50%);
background: #f8f8f8 !important;
border: 1px solid #ddd !important;
border-radius: 3px !important;
color: #000 !important; /* Black color for the icon */
cursor: pointer;
padding: 8px 6px 8px 12px; /* 2x bigger padding, 6px less on right */
font-size: 16px; /* 2x bigger font size */
width: auto !important;
height: auto !important;
line-height: 1;
display: flex !important;
/* The show/hide password button is a square: its width and height match the
password input field height, and the field flex-grows to fill the space the
narrower button frees up on the right. */
.password-input-container .password-field {
flex: 1 1 auto;
min-width: 0;
}
.auth-layout .auth-dialog .at-form .password-input-container .password-toggle-btn {
position: static;
transform: none;
flex: 0 0 auto;
align-self: stretch;
aspect-ratio: 1 / 1;
width: auto;
height: auto;
min-width: 0;
padding: 0;
border: 0;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
min-width: 40px; /* 2x bigger minimum width */
min-height: 32px; /* 2x bigger minimum height */
}
/* Adjust position for login and register pages */
.auth-layout .password-toggle-btn {
top: calc(50% - 11px); /* Move 15px down for login/register */
}
.password-toggle-btn .eye-text {
color: #000 !important;
font-size: 16px !important;
text-align: center;
line-height: 1;
filter: grayscale(100%);
-webkit-filter: grayscale(100%);
opacity: 0.8;
}
.eye-slash-line {
position: absolute;
top: 10px;
left: 10px;
width: 20px;
height: 20px;
pointer-events: none;
stroke: #000;
stroke-width: 2;
fill: none;
.auth-layout .auth-dialog .at-form .password-input-container .password-toggle-btn .fa {
display: block;
margin: 0;
padding: 0;
line-height: 1;
vertical-align: middle;
}
.password-toggle-btn:hover .eye-text {
color: #000 !important;
filter: grayscale(100%);
-webkit-filter: grayscale(100%);
opacity: 0.8;
.auth-layout .auth-dialog .at-form .password-input-container .password-field {
padding-right: 0;
}
body.mobile-mode .password-input-container {
flex-direction: column;
align-items: stretch;
}
body.mobile-mode .auth-layout .auth-dialog .at-form .password-input-container .password-toggle-btn {
aspect-ratio: auto;
width: 100%;
}
.auth-layout .auth-dialog .at-form button {
width: 100%;
+1
View File
@@ -16,6 +16,7 @@ import '/imports/lib/customHeadDefaults';
import '/imports/lib/dateUtils';
import '/imports/lib/secureDOMPurify';
import '/models/attachmentMigrationStatus';
import '/models/attachmentBulkMoveStatus';
import '/imports/cronMigrationClient';
// ----------------------------------------------------------------------------
+5 -38
View File
@@ -123,47 +123,14 @@ export class CardSearchPaged {
this.sessionData = this.getSessionData();
const cards = [];
if (this.sessionData && this.sessionData.cards) {
this.sessionData.cards.forEach(cardId => {
const card = ReactiveCache.getCard(cardId);
if (card && card._id) {
cards.push(card);
}
if (this.sessionData && this.sessionData.cards && this.sessionData.cards.length > 0) {
Cards.find({ _id: { $in: this.sessionData.cards } }).forEach(card => {
if (card && card._id) cards.push(card);
});
// Fallback: if no cards found, try fetching directly from Cards collection
if (cards.length === 0 && this.sessionData.cards && this.sessionData.cards.length > 0) {
if (directCards && directCards.length > 0) {
directCards.forEach(card => {
if (card && card._id) {
cards.push(card);
}
});
}
}
this.queryErrors = this.sessionData.errors || [];
} else if (this.sessionData) {
this.queryErrors = this.sessionData.errors || [];
} else {
// Fallback: try to get cards directly from the client-side collection
// Use a more efficient query with limit and sort
const selector = {
type: 'cardType-card',
dueAt: { $exists: true, $nin: [null, ''] }
};
const options = {
sort: { dueAt: 1 }, // Sort by due date ascending (oldest first)
limit: 100 // Limit to 100 cards for performance
};
const allCards = Cards.find(selector, options).fetch();
if (allCards && allCards.length > 0) {
allCards.forEach(card => {
if (card && card._id) {
cards.push(card);
}
});
}
this.queryErrors = [];
}
if (this.queryErrors.length) {
+85
View File
@@ -0,0 +1,85 @@
// Native one-finger touch scrolling for `.dragscroll` containers.
//
// The @wekanteam/dragscroll library only listens for mouse events
// (mousedown / mousemove / mouseup). On iPad / iPhone Safari finger
// scrolling of those containers works anyway, because they are also native
// overflow scrollers with momentum scrolling. But on Fairphone 4 postmarketOS
// Firefox and on the newer Ubuntu Touch Morph Browser, dragging a finger over
// a `.dragscroll` container (All Boards list, board canvas, swimlanes, lists,
// calendar) did not scroll it, because those browsers do not emit the
// synthetic mouse events dragscroll relies on.
//
// This module adds real touch handling so a one-finger drag scrolls the
// nearest `.dragscroll` ancestor on every touch browser. It mirrors the
// behaviour of dragscroll's mouse handler (same `.nodragscroll` / form-field
// exclusions) and is delegated on `document`, so it also covers `.dragscroll`
// elements that are added or replaced reactively without needing a reset.
//
// iOS is intentionally skipped: there finger scrolling already works natively
// (with momentum), and taking it over with preventDefault would only remove
// that momentum.
const isIOS = /iP(hone|ad|od)/.test(navigator.userAgent);
let active = null;
function isExcluded(target) {
return !!(
target.closest &&
target.closest(
// Form fields and editors must keep their own touch behaviour, and
// `.nodragscroll` opts an element out explicitly. `.handle` (the
// swimlane/list drag handles, .js-swimlane-header-handle /
// .js-list-handle) must be excluded too: those start a jQuery UI
// sortable reorder via touch-punch, and hijacking the touch to scroll
// here would block dragging the handle to reorder swimlanes/lists.
'input, select, textarea, .note-editable, .nodragscroll, .handle, .ui-sortable-handle',
)
);
}
function canScroll(el) {
return el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth;
}
function onTouchStart(e) {
active = null;
if (e.touches.length !== 1) return;
const target = e.target;
if (isExcluded(target)) return;
const el = target.closest && target.closest('.dragscroll');
if (!el || !canScroll(el)) return;
const touch = e.touches[0];
active = {
el,
startX: touch.clientX,
startY: touch.clientY,
scrollLeft: el.scrollLeft,
scrollTop: el.scrollTop,
};
}
function onTouchMove(e) {
if (!active || e.touches.length !== 1) return;
const touch = e.touches[0];
const el = active.el;
el.scrollLeft = active.scrollLeft - (touch.clientX - active.startX);
el.scrollTop = active.scrollTop - (touch.clientY - active.startY);
// We own the gesture: suppress page rubber-banding and the synthetic mouse
// events that would otherwise double-scroll through dragscroll.
if (e.cancelable) e.preventDefault();
}
function onTouchEnd() {
active = null;
}
// Guard so importing this module from more than one place (boardBody,
// boardsList, ...) still only attaches the listeners once.
if (!isIOS && typeof document !== 'undefined' && !window.__wekanDragscrollTouch) {
window.__wekanDragscrollTouch = true;
document.addEventListener('touchstart', onTouchStart, { passive: true });
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchend', onTouchEnd, { passive: true });
document.addEventListener('touchcancel', onTouchEnd, { passive: true });
}
-10
View File
@@ -1,10 +1,4 @@
const hotkeys = require('hotkeys-js').default;
// Late-bind Sidebar to avoid circular dependency (sidebar.js needs its template first)
let _Sidebar;
function getSidebar() {
if (!_Sidebar) _Sidebar = require('/client/features/sidebar/service').getSidebarInstance;
return _Sidebar();
}
// Pressing `Escape` should close the last opened "element" and only the last
// one. Components can register themselves using a label a condition, and an
@@ -128,10 +122,6 @@ export const EscapeActions = {
// in the hotkeys filter (keyboard.js) so it works in textarea and inputs.
hotkeys('escape', () => {
EscapeActions.executeLowest();
const sidebar = getSidebar();
if (sidebar) {
sidebar.hide();
}
});
// On a left click on the document, we try to exectute one escape action (eg,
+21 -3
View File
@@ -99,9 +99,27 @@ export const Utils = {
return user.profile.mobileMode;
}
// Default to mobile mode for iPhone/iPod
const isIPhone = /iPhone|iPod/i.test(navigator.userAgent);
return isIPhone;
// No explicit preference: pick a sensible default from the device.
// Previously this returned true ONLY for iPhone/iPod, so every other phone
// (Fairphone 4 postmarketOS Firefox, Ubuntu Touch Morph, Android Firefox,
// ...) defaulted to desktop-mode. desktop-mode applies large desktop sizing
// (see .desktop-mode rules in boardHeader.css) which on a small phone screen
// looks roughly 2x too big. Default to mobile-mode for any phone-sized touch
// device, not just iPhone, so non-iPhone phones render at the correct size.
const ua = navigator.userAgent;
const isIPhone = /iPhone|iPod/i.test(ua);
const isPhoneUA =
/Mobile|Android|BlackBerry|IEMobile|Opera Mini/i.test(ua) &&
!/iPad/i.test(ua);
const isUbuntuTouch = /Ubuntu/i.test(ua);
const hasTouch =
('maxTouchPoints' in navigator && navigator.maxTouchPoints > 0) ||
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches);
// A small touch screen (phone-sized) — covers touch browsers whose UA does
// not contain a phone token. Desktop browsers (no touch, or wide) stay in
// desktop-mode.
const isSmallTouchScreen = hasTouch && window.innerWidth <= 800;
return isIPhone || isPhoneUA || isUbuntuTouch || isSmallTouchScreen;
},
setMobileMode(enabled) {
+5
View File
@@ -92,7 +92,12 @@ AccountsTemplates.configure({
sendVerificationEmail: true,
showForgotPasswordLink: !disableForgotPassword,
forbidClientAccountCreation: disableRegistration,
homeRoutePath: '/',
onSubmitHook(error, state) {
if (!error && (state === 'signUp' || state === 'signIn')) {
FlowRouter.go('/');
return;
}
if (error) {
// Display error to user
const errorDiv = document.getElementById('login-error-message');
+155
View File
@@ -0,0 +1,155 @@
# WeKan multi-tenant deployment on one Linux host
#
# Use this compose file when you want to run several WeKan instances
# (one per customer / domain) on the SAME Linux host, all sharing the
# host's kernel network namespace (network_mode: "host") and one
# MongoDB replica set, with separate logical databases per tenant.
#
# This file shows the minimal extra configuration needed for the
# `uws` DDP transport plus `changeStreams` reactivity to work
# correctly in this topology, namely:
#
# - DDP_TRANSPORT=uws on every service
# - METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling on every service
# - METEOR_SETTINGS with a DISTINCT uws.port per service (5001, 5002, …)
#
# Why the distinct uws.port: the uws DDP transport runs an internal
# WebSocket proxy server on its own TCP port (default 127.0.0.1:5001)
# and forwards WS upgrades from the public PORT into it. When two
# services share a kernel netns and both default to 127.0.0.1:5001,
# the second one to start fails to bind because the listen socket is
# opened exclusively. Give each service its own internal port via
# METEOR_SETTINGS and they coexist cleanly.
#
# Reference: see docs/Platforms/FOSS/Docker/Meteor3/multitenancy.md
# and https://docs.meteor.com/performance/ddp-transport#multitenancy
#
# Setup:
# docker compose -f docker-compose-multitenancy.yml up -d
#
# Tear down:
# docker compose -f docker-compose-multitenancy.yml down -v
#
# Adding more tenants:
# Copy a tenant block, change the service name, PORT, ROOT_URL,
# MONGO_URL database name, and the uws.port inside METEOR_SETTINGS
# to a fresh value (5003, 5004, …). The MongoDB service and the
# reverse proxy in front of WeKan don't need any change.
version: '3.8'
services:
# ---------------------------------------------------------------------
# Shared MongoDB 7 with replica set (required for changeStreams).
# All tenants connect to this instance using DIFFERENT logical
# databases (?authSource if you enable auth).
# ---------------------------------------------------------------------
wekandb:
image: mongo:7
container_name: wekan-db
restart: always
network_mode: "host"
entrypoint: ["sh", "-c"]
command:
- |
mongod --storageEngine wiredTiger --wiredTigerCacheSizeGB 1 --timeZoneInfo /usr/share/zoneinfo --oplogSize 2048 --replSet rs0 --bind_ip 127.0.0.1 --port 27017 --quiet &
until mongosh --host 127.0.0.1 --port 27017 --quiet --eval 'try { const s = rs.status(); quit(s.ok === 1 ? 0 : 1); } catch (e) { if (e.codeName === "NotYetInitialized" || /no replset config has been received|not yet initialized/.test(e.message)) { rs.initiate({_id:"rs0", members:[{_id:0, host:"127.0.0.1:27017"}]}); quit(1); } quit(1); }' >/dev/null 2>&1; do
sleep 2
done
wait
# Report healthy only once the replica set has an elected primary, so the
# tenant apps (depends_on: condition: service_healthy) do not start before
# MongoDB is ready and crash with "Topology is closed". start_period gives a
# large database time to replay its journal after an upgrade.
healthcheck:
test: ["CMD", "mongosh", "--host", "127.0.0.1", "--port", "27017", "--quiet", "--eval", "try { quit(rs.status().ok === 1 && db.hello().isWritablePrimary ? 0 : 1) } catch (e) { quit(1) }"]
interval: 10s
timeout: 10s
retries: 30
start_period: 60s
volumes:
- wekan-db:/data/db
- wekan-db-dump:/dump
# ---------------------------------------------------------------------
# Tenant 1
#
# Each tenant needs three things distinct from every other tenant:
# 1. PORT — public HTTP port (here 8081)
# 2. MONGO_URL database name — here `wekan_tenant1`
# 3. uws.port in METEOR_SETTINGS — here 5001
# ---------------------------------------------------------------------
wekan-tenant1:
image: ghcr.io/wekan/wekan:latest
container_name: wekan-tenant1
restart: always
network_mode: "host"
depends_on:
wekandb:
condition: service_healthy
environment:
- PORT=8081
- ROOT_URL=https://tenant1.example.com
- MONGO_URL=mongodb://127.0.0.1:27017/wekan_tenant1?replicaSet=rs0
- MONGO_OPLOG_URL=mongodb://127.0.0.1:27017/local?replicaSet=rs0
- DDP_TRANSPORT=uws
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
# Internal uws proxy port — MUST be distinct per service.
- METEOR_SETTINGS={"packages":{"ddp-server":{"uws":{"port":5001,"host":"127.0.0.1"}}}}
- WRITABLE_PATH=/data
- WITH_API=true
volumes:
- wekan-tenant1-data:/data
# ---------------------------------------------------------------------
# Tenant 2 — same template, distinct PORT / database / uws.port
# ---------------------------------------------------------------------
wekan-tenant2:
image: ghcr.io/wekan/wekan:latest
container_name: wekan-tenant2
restart: always
network_mode: "host"
depends_on:
wekandb:
condition: service_healthy
environment:
- PORT=8082
- ROOT_URL=https://tenant2.example.com
- MONGO_URL=mongodb://127.0.0.1:27017/wekan_tenant2?replicaSet=rs0
- MONGO_OPLOG_URL=mongodb://127.0.0.1:27017/local?replicaSet=rs0
- DDP_TRANSPORT=uws
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
- METEOR_SETTINGS={"packages":{"ddp-server":{"uws":{"port":5002,"host":"127.0.0.1"}}}}
- WRITABLE_PATH=/data
- WITH_API=true
volumes:
- wekan-tenant2-data:/data
# ---------------------------------------------------------------------
# Add more tenants by copying a block above and bumping:
# PORT (e.g. 8083, 8084, …)
# ROOT_URL (the tenant's public domain)
# MONGO_URL (a new database name)
# uws.port (5003, 5004, …)
# ---------------------------------------------------------------------
volumes:
wekan-db:
wekan-db-dump:
wekan-tenant1-data:
wekan-tenant2-data:
# ---------------------------------------------------------------------
# Verifying the uws internal listen sockets are isolated.
#
# From the host (or any container sharing the netns):
#
# cat /proc/net/tcp | awk '$4 == "0A" {print $2}' | sort | uniq -c
#
# You want to see ONE listener per uws port — the hex addresses
# `0100007F:1389` (5001) and `0100007F:138A` (5002) should each
# appear with count 1, no duplicates. If you see `2 0100007F:1389`,
# two services are trying to share the default port; reconfigure
# their METEOR_SETTINGS.uws.port to distinct values and restart.
# ---------------------------------------------------------------------
+59 -9
View File
@@ -171,7 +171,7 @@ services:
# Leave it unset or empty to skip migration.
#-------------------------------------------------------------------------------------
#wekan-migrate:
# image: node:24.14.1-slim
# image: node:24.15.0-slim
# container_name: wekan-migrate
# restart: "no"
# networks:
@@ -226,19 +226,49 @@ services:
image: mongo:7
container_name: wekan-db
restart: always
#command: mongod --logpath /dev/null --oplogSize 128 --replSet rs0 --quiet
#command: mongod --oplogSize 128 --replSet rs0 --quiet
#command: mongod --logpath /dev/null --storageEngine wiredTiger --wiredTigerCacheSizeGB 32 --timeZoneInfo /usr/share/zoneinfo --setParameter logicalSessionRefreshMillis=900000 --setParameter localLogicalSessionTimeoutMinutes=45 --oplogSize 20480 --replSet rs0 --quiet
#command: mongod --storageEngine wiredTiger --wiredTigerCacheSizeGB 32 --timeZoneInfo /usr/share/zoneinfo --setParameter logicalSessionRefreshMillis=900000 --setParameter localLogicalSessionTimeoutMinutes=45 --oplogSize 20480 --replSet rs0 --quiet
command: >
sh -c "
mongod --oplogSize 128 --replSet rs0 --quiet --bind_ip_all &
until mongosh --eval 'rs.initiate()' > /dev/null 2>&1; do
sh -c '
mongod --storageEngine wiredTiger --wiredTigerCacheSizeGB 32 --timeZoneInfo /usr/share/zoneinfo --setParameter logicalSessionRefreshMillis=900000 --setParameter localLogicalSessionTimeoutMinutes=45 --oplogSize 20480 --replSet rs0 --bind_ip_all --quiet &
until mongosh --host 127.0.0.1 --quiet --eval "
try {
const status = rs.status();
quit(status.ok === 1 ? 0 : 1);
} catch (e) {
if (
e.codeName === \"NotYetInitialized\" ||
e.message.includes(\"no replset config has been received\") ||
e.message.includes(\"not yet initialized\")
) {
rs.initiate({
_id: \"rs0\",
members: [{ _id: 0, host: \"wekandb:27017\" }]
});
quit(1);
}
quit(1);
}
" >/dev/null 2>&1; do
sleep 2
done;
wait"
done
wait
'
networks:
- wekan-tier
expose:
- 27017
# Healthcheck: report healthy only once the replica set has an elected
# primary, so the wekan app (depends_on: condition: service_healthy) does
# not start before MongoDB is ready and crash with "Topology is closed".
# start_period gives a large database time to replay its journal after an
# upgrade before failures are counted.
healthcheck:
test: ["CMD", "mongosh", "--host", "127.0.0.1", "--port", "27017", "--quiet", "--eval", "try { quit(rs.status().ok === 1 && db.hello().isWritablePrimary ? 0 : 1) } catch (e) { quit(1) }"]
interval: 10s
timeout: 10s
retries: 30
start_period: 60s
volumes:
- /etc/localtime:/etc/localtime:ro
- wekan-db:/data/db
@@ -294,7 +324,11 @@ services:
#- METEOR_REACTIVITY_ORDER=polling
# https://forums.meteor.com/t/meteor-3-5-beta-change-streams-performance-improvements/64461#change-streams-setup-3
# https://github.com/meteor/meteor/blob/release-3.5/v3-docs/docs/performance/change-streams-observer-driver.md#choosing-the-reactivity-driver-order
# Use oplog,polling to fix performance until changeStreams is fixed at next release of Meteor 3.5 Beta:
# https://github.com/wekan/wekan/issues/6307#issuecomment-4299349231
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
- DDP_TRANSPORT=uws
#- DDP_TRANSPORT=sockjs
#-----------------------------------------------------------------
# ==== WRITEABLE PATH FOR FILE UPLOADS ====
- WRITABLE_PATH=/data
@@ -535,6 +569,13 @@ services:
#- OAUTH2_ADFS_ENABLED=false
# Azure AD B2C. https://github.com/wekan/wekan/issues/5242
#- OAUTH2_B2C_ENABLED=false
# SECURITY (GHSA-mp7g-hj5q-gxhq): Link an OIDC login to a pre-existing
# Wekan account (password/LDAP) with the same email. OFF by default; when
# false an OIDC login whose email already exists is rejected instead of
# merged, preventing account takeover via spoofed email claims. Only
# enable if you fully trust your OIDC provider's email claims, and even
# then the provider must send email_verified=true for the merge to happen.
#- OAUTH2_MERGE_EXISTING_USERS=false
# OAuth2 login style: popup or redirect.
#- OAUTH2_LOGIN_STYLE=redirect
# Application GUID captured during app registration:
@@ -828,6 +869,9 @@ services:
#- HEADER_LOGIN_FIRSTNAME=HEADERFIRSTNAME
#- HEADER_LOGIN_LASTNAME=HEADERLASTNAME
#- HEADER_LOGIN_EMAIL=HEADEREMAILADDRESS
# Optional comma-separated trusted proxy source IPs for header login.
# If empty, all source IPs are accepted (not recommended).
#- HEADER_LOGIN_TRUSTED_IPS=127.0.0.1,10.0.0.2
#---------------------------------------------------------------------
# ==== LOGOUT TIMER, probably does not work yet ====
# LOGOUT_WITH_TIMER : Enables or not the option logout with timer
@@ -869,7 +913,13 @@ services:
# Wait spinner to use
# - WAIT_SPINNER=Bounce
#---------------------------------------------------------------------
#depends_on:
# Wait for MongoDB to be healthy (replica-set primary elected) before
# starting WeKan, so index creation does not crash with "Topology is closed".
# If you use FerretDB instead, comment out the wekandb block below and
# uncomment the wekan-ferretdb block.
depends_on:
wekandb:
condition: service_healthy
# wekan-migrate:
# condition: service_completed_successfully
# wekan-ferretdb:
@@ -0,0 +1,33 @@
# History of Schema Migration Systems
1. At `server/migrations.js` was changes to database schema, but making modifications to thousands
of cards did take many hours, it is too much time.
2. There was Admin Panel options to migrate attachments from MongoDB GridFS to filesystem, but that could
cause WeKan to become unresponsive, it did take too much resources.
3. There was separate migration script to change database schema, but it was incomplete, some data was not migrated.
4. Migrations were moved to opening of board, but that caused opening board to take too much time
5. Migrations were moved to Right Sidebar/Board Settings/Migrations, but there was complains that
it is too much try to get users to learn using those migrations, and it could take
many hours for migrations to run at big board. Also, there was Responsible Security Disclosure
that Board Admins have too much rights to change board structure so much, so migrations were moved
to Admin Panel to be used only by Admins.
6. At Admin Panel/Cron, migrations did not show progress of migrations well enough, it looked like no
migrations were happening, migrations did take too much time, and WeKan did slow down while migrations
were in progress, as was seen at increased CPU and RAM usage.
7. Migrations were commented out, so that they would not slow down WeKan anymore.
8. WeKan was migrated to Meteor 3.x.
9. Someone added datetime migrations, to convert previous date strings to Date Objects, that PR was merged,
but questions still is, does it cause too much slowdown. It should have been changed to detect and show
any current date, detecting is it a string or Date Object.
10. There was added code, that could show all Swimlanes/Lists/Cards without migrations, showing both
previous Shared Lists and newest Per-Swimlane Lists, and also orphaned Swimlanes/Lists/Cards.
But there was bug that something still disappeared when moving. It remains to be seen,
can there continue to be showing all data without migrations, can that code be made to work,
or is it really necessary to reintroduce migrations that take too much time.
How should migrations work?
a) Separate migrations script, that would be run before starting WeKan?
b) Reading any database structure and showing data, without needing to migrate any data?
c) Migrating while WeKan is running, that could cause slowdown?
d) Some other way?
+178
View File
@@ -0,0 +1,178 @@
#!/bin/bash
# Make sure to run as root
if [ "$EUID" -ne 0 ]; then
echo "Error: This script needs to be run as root."
echo "Use for example 'sudo su' before running this script."
exit 1
fi
# Check that script has been given only one parameter
if [ "$#" -ne 1 ]; then
echo "Error: Give a short lowecase ascii database name a parameter."
echo "Example: $0 <databasename>"
exit 1
fi
DB_NAME=$1
MONGO_PORT=27017
MONGO_HOST="localhost"
CONFIG_FILE="/etc/mongod.conf"
ADMIN_FILE="admin.txt"
TARGET_FILE="${DB_NAME}.txt"
# Function for generating random password
generate_password() {
openssl rand -base64 12 | tr -dc 'a-zA-Z0-9' | head -c 16
}
# --- STEP 1: CHECK ADMIN-USER AND PASSWORDLESS MONGODB STATE ---
# Check is there already authorization enabled at mongod.conf
IS_AUTH_ENABLED=$(grep -A 5 "^security:" "$CONFIG_FILE" 2>/dev/null | grep "authorization:" | grep -q "enabled"; echo $?)
if [ ! -f "$ADMIN_FILE" ]; then
# If admin.txt is missing, but database already requires auth, passwordless usage is not possible
if [ "$IS_AUTH_ENABLED" -eq 0 ]; then
echo "Error: MongoDB is already protected (authorization: enabled), but file $ADMIN_FILE is missing."
echo "Script can not create admin-user without access to database. Exiting."
exit 1
fi
echo "--- There is no Admin yet. Creating Admin to passwordless database... ---"
ADMIN_USER="admin_root"
ADMIN_PASS=$(generate_password)
# Creating admin-user localhost by using exception
mongosh --port $MONGO_PORT --eval "
db = db.getSiblingDB('admin');
db.createUser({
user: '${ADMIN_USER}',
pwd: '${ADMIN_PASS}',
roles: [ { role: 'root', db: 'admin' } ]
});
" > /dev/null
if [ $? -ne 0 ]; then
echo "Error: Creating Admin-user failed. Check is MongoDB running?"
exit 1
fi
# Save Admin-user details
ADMIN_URL="mongodb://${ADMIN_USER}:${ADMIN_PASS}@${MONGO_HOST}:${MONGO_PORT}/admin"
echo "MONGO_URL=${ADMIN_URL}" > "$ADMIN_FILE"
echo "MONGOSH_CMD=mongosh \"${ADMIN_URL}\"" >> "$ADMIN_FILE"
echo "Admin-user created successfully."
echo "--- Changing MongoDB to require login (/etc/mongod.conf) ---"
# Editing mongod.conf settings
if grep -q "^security:" "$CONFIG_FILE"; then
if grep -A 5 "^security:" "$CONFIG_FILE" | grep -q "authorization:"; then
sed -i '/authorization:/c\ authorization: enabled' "$CONFIG_FILE"
else
sed -i '/^security:/a\ authorization: enabled' "$CONFIG_FILE"
fi
else
echo -e "\nsecurity:\n authorization: enabled" >> "$CONFIG_FILE"
fi
echo "--- Restarting MongoDB, so that using password is always required... ---"
systemctl restart mongod
sleep 2
echo "MongoDB is password protected and restarted."
else
echo "--- Found existing $ADMIN_FILE. Reading admin-password... ---"
SAVED_URL=$(grep "MONGO_URL=" "$ADMIN_FILE" | cut -d'=' -f2)
ADMIN_USER=$(echo "$SAVED_URL" | sed -e 's|mongodb://||' -e 's|@.*||' | cut -d':' -f1)
ADMIN_PASS=$(echo "$SAVED_URL" | sed -e 's|mongodb://||' -e 's|@.*||' | cut -d':' -f2)
fi
# --- STEP 2: CHECK DOES TARGET DATABASE AND USER EXIST ---
echo "--- Check does database or user already exist... ---"
# Ask MongoDB is there does user already exist
USER_EXISTS=$(mongosh --port $MONGO_PORT -u "$ADMIN_USER" -p "$ADMIN_PASS" --authenticationDatabase "admin" --quiet --eval "
db = db.getSiblingDB('${DB_NAME}');
print(db.getUser('${DB_NAME}_user') !== null);
")
if [ "$USER_EXISTS" == "true" ] || [ -f "$TARGET_FILE" ]; then
echo "--------------------------------------------------"
echo "Database user '${DB_NAME}_user' or file '$TARGET_FILE' already exists."
echo "Script did not make any changes. Exiting."
echo "--------------------------------------------------"
exit 0
fi
# --- STEP 3: CREATE READOPLOGONLY-ROLE (IF NOT EXIST YET) ---
echo "--- Checking does readOplogOnly-role exist at admin-database... ---"
mongosh --port $MONGO_PORT \
-u "$ADMIN_USER" \
-p "$ADMIN_PASS" \
--authenticationDatabase "admin" \
--eval "
db = db.getSiblingDB('admin');
if (db.getRole('readOplogOnly') === null) {
db.runCommand({
createRole: 'readOplogOnly',
privileges: [
{
resource: { db: 'local', collection: 'oplog.rs' },
actions: [ 'find' ]
}
],
roles: []
});
print('Role created.');
} else {
print('Role already exists.');
}
" > /dev/null
# --- STEP 4: CREATE DATABASE ACCORGIND TO PARAMETER ---
echo "--- Creating database '$DB_NAME' and user... ---"
TARGET_PASS=$(generate_password)
mongosh --port $MONGO_PORT \
-u "$ADMIN_USER" \
-p "$ADMIN_PASS" \
--authenticationDatabase "admin" \
--eval "
db = db.getSiblingDB('${DB_NAME}');
db.createUser({
user: '${DB_NAME}_user',
pwd: '${TARGET_PASS}',
roles: [
{ role: 'readWrite', db: '${DB_NAME}' },
{ role: 'dbAdmin', db: '${DB_NAME}' },
{ role: 'readOplogOnly', db: 'admin' }
]
});
" > /dev/null
if [ $? -eq 0 ]; then
# Creating also ready MONGO_OPLOG_URL to output / file for ease of use
TARGET_URL="mongodb://${DB_NAME}_user:${TARGET_PASS}@${MONGO_HOST}:${MONGO_PORT}/${DB_NAME}?authSource=${DB_NAME}"
OPLOG_URL="mongodb://${DB_NAME}_user:${TARGET_PASS}@${MONGO_HOST}:${MONGO_PORT}/local?replicaSet=rs0&authSource=${DB_NAME}"
echo "MONGO_URL=${TARGET_URL}" > "$TARGET_FILE"
echo "MONGO_OPLOG_URL=${OPLOG_URL}" >> "$TARGET_FILE"
echo "--------------------------------------------------"
echo "Ready! Database and user created successfully."
echo "New database details saved to $TARGET_FILE"
echo "--------------------------------------------------"
else
echo "Error: Creating database failed."
exit 1
fi
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# Make sure to run as root
if [ "$EUID" -ne 0 ]; then
echo "Error: This script needs to be run as root."
echo "Use for example 'sudo su' before running this script."
exit 1
fi
# Check that script has been given only one parameter
if [ "$#" -ne 1 ]; then
echo "Error: Give a short lowecase ascii database name a parameter."
echo "Example: $0 <databasename>"
exit 1
fi
mkdir -p restore/$1
cat restore/docker-start.yml > restore/$1/docker-compose.yml
sed -i "s|PARAMETER|${1}|g" restore/$1/docker-compose.yml
cat $1.txt >> restore/$1/docker-compose.yml
cat snap-settings/wekan_$1.txt >> restore/$1/docker-compose.yml
cat restore/docker-end.yml >> restore/$1/docker-compose.yml
+55
View File
@@ -0,0 +1,55 @@
#!/bin/bash
if [ "$#" -ne 1 ]; then
echo "Error: Enter the name of the database to restore as a parameter."
echo "Usage: $0 <database_name>"
exit 1
fi
DB_PARAM=$1
RESTORE_DIR=restore
TARGET_FILE="${DB_PARAM}.txt"
# Checking the identity file
if [ ! -f "$TARGET_FILE" ]; then
echo "Error: File '$TARGET_FILE' not found."
exit 1
fi
# IMPORTANT CHANGE: Pointing to the dump folder as the root, not the wekan folder!
if [ -d "./${RESTORE_DIR}/${DB_PARAM}/dump" ]; then
BACKUP_DIR="./${RESTORE_DIR}/${DB_PARAM}/dump"
else
BACKUP_DIR="./${RESTORE_DIR}/${DB_PARAM}"
fi
# Retrieving MONGO_URL
MONGO_URL=$(grep "^MONGO_URL=" "$TARGET_FILE" | cut -d'=' -f2-)
if [[ -z "$MONGO_URL" || ! "$MONGO_URL" =~ ^mongodb:// ]]; then
echo "Error: No valid MONGO_URL line found in file $TARGET_FILE."
exit 1
fi
CONNECTION_URI=$(echo "$MONGO_URL" | sed 's/\/[^\/]*$/\//')
echo "--- Starting the restore of database '$DB_PARAM' ---"
echo "Using backup from: $BACKUP_DIR"
echo "---------------------------------------------------"
# Run mongorestore by giving it a dump folder.
# The tool reads the "wekan" subfolder from there and changes it to "PARAMETER".
mongorestore --uri="$CONNECTION_URI" \
--authenticationDatabase="$DB_PARAM" \
--drop \
--nsFrom="wekan.*" \
--nsTo="${DB_PARAM}.*" \
--dir="$BACKUP_DIR"
if [ $? -eq 0 ]; then
echo "--------------------------------------------------"
echo "Done! Database '$DB_PARAM' restored successfully."
echo "------------------------------------------------"
else
echo "Error: mongorestore failed."
exit 1
fi
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# Transfer attachments and avatars from server c1 Snap to server c2 Docker
if [ "$#" -ne 1 ]; then
echo "Error: Enter the name of what snap files to transfer to Docker files."
echo "Usage: $0 <database_name>"
exit 1
fi
rsync -aur --progress c1:/var/snap/wekan_$1/common/files /var/lib/docker/volumes/$1_wekan-files/_data/
chown -R caddy:systemd-journal /var/lib/docker/volumes/$1_wekan-files/_data/files
# At c2:
# ssh-keygen
# At c2 ~/.ssh/config:
# Host c1
# Hostname 123.123.123.123
# User root
# IdentityFile ~/.ssh/id_rsa
# At c1 ~/.ssh/authorized_keys has content of id_rsa at c1:
# ssh-rsa ... root@c2
# At c1 file permissions:
# chmod og-rwx ~/.ssh/authorized_keys
@@ -0,0 +1,94 @@
# Meteor 3 WeKan: Hosting many WeKan kanban, websites, etc
Note: Only `Oplog sockjs` works with multitenancy, see [multitenancy.md](multitenancy).
Migrating from Parallel Snap setup, because `snap set wekan root-url=...`
etc commands are slow to run, and did not get Parallel Snap working properly yet.
```
caddy proxy: Let's Encrypt and CloudFlare TLS to localhost http ports
|
|==> Many Parallel WeKan Snap Containers at different ports,
| each Container with it's own Node.js/MongoDB
|==> Sandstorm, many websites with WordPress and Hackers CMS
|==> PHP/MySQL WordPress (example)
|==> PHP/MySQL Hesk Helpdesk (example)
|==> PHP/MySQL Kanboard (example)
|==> Static websites (example)
```
To this Docker setup:
```
caddy proxy: Let's Encrypt and CloudFlare TLS to localhost http ports
|
|==> Many Docker Meteor 3 WeKan Node.js Containers,
| Uses changeStreams and DDP_TRANSPORT=uws
| |
| |=> One MongoDB without container,
| always requires password to login,
| for admin and each other users,
| each user has it's own database, username and password
|
|==> Sandstorm, many websites with WordPress and Hackers CMS
|==> PHP/MySQL WordPress (example)
|==> PHP/MySQL Hesk Helpdesk (example)
|==> PHP/MySQL Kanboard (example)
|==> Static websites (example)
```
Save all Parallel Snap container settings with:
```
snap-save-settings.sh
```
For setting up MongoDB etc, use examples at `apt` directory.
MongoDB config `/etc/mongod.conf` is at `mongo` directory.
Create MongoDB key:
```
./keys.sh
```
1) Adding new user, it saves MongoDB connection string to username.txt:
```
./1createdb.sh username
```
Same username.txt is used when login to database, backup, restore, files transfer, etc.
You can connect to database:
```
./sh.sh username
```
Or sometime make Backup to `backup/username/YYYY-MM-DD_HH-MM-SS/`
```
./backup.sh username
```
2) Create docker-compose.yml to `restore/username/`
```
./2docker.sh username
```
Edit docker-compose.yml and start Docker container:
```
cd restore/username/
nano docker-compose.yml
docker compose up -d
```
3) Restore database
```
./3restoredb.sh username
```
4) Transfer attachments and avatars from Snap server to Docker server:
```
./4files.sh username
```
5) Restart Docker container:
```
cd restore/username/
docker compose stop
docker compose start
```
6) Upgrade WeKan from old version to new version, for example:
```
./update-wekan-version.sh 9.10 9.11"
```
+7
View File
@@ -0,0 +1,7 @@
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
+3
View File
@@ -0,0 +1,3 @@
apt clean
apt -y autoclean
apt -y autoremove
+4
View File
@@ -0,0 +1,4 @@
apt update
apt -y dist-upgrade
snap refresh
+11
View File
@@ -0,0 +1,11 @@
# Loop through all subdirectories in the current directory
for d in restore/*/ ; do
# Get only directory name, without restore/ part
FOLDER_NAME=$(basename "$d")
echo "--------------------------------------------------"
echo "Creating backup: $FOLDER_NAME"
echo "--------------------------------------------------"
./backup.sh "$FOLDER_NAME" || continue
done
echo "All backups done!"
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
# Check that the script was given exactly one parameter
if [ "$#" -ne 1 ]; then
echo "Error: Enter the name of the database to be backed up as a parameter."
echo "Usage: $0 <database_name>"
exit 1
fi
DB_PARAM=$1
TARGET_FILE="${DB_PARAM}.txt"
# 1. Check if the credentials file exists
if [ ! -f "$TARGET_FILE" ]; then
echo "Error: File '$TARGET_FILE' not found. Unable to retrieve login information."
exit 1
fi
# 2. Retrieve MONGO_URL from file
MONGO_URL=$(grep "^MONGO_URL=" "$TARGET_FILE" | cut -d'=' -f2-)
if [[ -z "$MONGO_URL" || ! "$MONGO_URL" =~ ^mongodb:// ]]; then
echo "Error: No valid MONGO_URL row found in file $TARGET_FILE."
exit 1
fi
# 3. Remove the database name from the end of the URL for mongodump,
# so that the connection is made to the server root (e.g. mongodb://user:pass@host:port/)
CONNECTION_URI=$(echo "$MONGO_URL" | sed 's/\/[^\/]*$/\//')
# 4. Create a timestamp and destination folder path (Format: YYYY-MM-DD_HH-MM-SS)
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
BACKUP_PATH="backup/${DB_PARAM}/${TIMESTAMP}"
echo "--- Starting backup of database '$DB_PARAM' ---"
echo "Destination folder: $BACKUP_PATH"
echo "--------------------------------------------------"
# Run mongodump
# --uri = connection address with ID
# --db = from which database the dump is taken
# --out = destination folder where the data is saved
mongodump --uri="$CONNECTION_URI" --db="$DB_PARAM" --out="$BACKUP_PATH"
# Backup attachments and avatars
cp -pR "/var/lib/docker/volumes/${DB_PARAM}_wekan-files/_data/files" "${BACKUP_PATH}/"
# Check if the backup was successful
if [ $? -eq 0 ]; then
echo "------------------------------------------------"
echo "Done! Backup saved successfully to:"
echo "$BACKUP_PATH"
echo "------------------------------------------------"
else
echo "Error: mongodump failed."
exit 1
fi
@@ -0,0 +1,441 @@
# REQUIRED:
# - CloudFlare/WeKan Domain
# - Speed/Settings: Disable only "HTTP/2 to Origin"
# - Caching/Overview: Development Mode, Temporarily bypass cache
# - See screenshots at cloudflare directory
{
# Using below "servers {protocols h1}" to force Caddy to use only
# HTTP/1.1 -protocol (h1) at background connections,
# to prevent multitenancy round robin bug, see multitenancy.md
servers {
protocols h1
}
}
# Redirecting http to https
(redirect) {
@http {
protocol http
}
redir @http https://{host}{uri}
}
## Hosting examples
# 1) WeKan hosting: Custom Domain with default Let's Encrypt TLS certificate
# - at CloudFlare DNS there is grey cloud, no CloudFlare proxy
# - Customer DNS: wekan.company.com CNAME customer.wekan.team
# - WeKan hosting CloudFlare DNS: customer.wekan.team A Server-IPv4-Address
wekan.customer.com {
# b) No custom domain, using customer.wekan.team:
# - Using CloudFlare TLS Origin Certificate with Strict Enforcement
# - At CloudFlare DNS change to orange cloud, with CloudFlare proxy,
# - https://github.com/wekan/wekan/blob/main/docs/Webserver/Caddy.md
# tls {
# load /etc/caddy/certs
# }
header {
-Content-Security-Policy-Report-Only
-Content-Security-Policy
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: https://static.cloudflareinsights.com https://accounts.google.com https://apis.google.com; worker-src 'self' blob:; connect-src 'self' wss://wekan.customer.com https://wekan.customer.com https://cloudflareinsights.com https://accounts.google.com; img-src 'self' data: https://*.googleusercontent.com; frame-src 'self' https://accounts.google.com; style-src 'self' 'unsafe-inline'; font-src 'self' data:;"
Cross-Origin-Opener-Policy "same-origin-allow-popups"
X-Content-Type-Options nosniff
Strict-Transport-Security "max-age=31536000;"
-X-Frame-Options
Cache-Control "no-cache, no-store, must-revalidate"
}
handle /.well-known/* {
header Content-Type application/json
header Access-Control-Allow-Origin *
}
handle {
reverse_proxy 127.0.0.1:3001
}
}
## 2) WeKan hosting: No custom domain, using customer.wekan.team:
# - Using CloudFlare TLS Origin Certificate with Strict Enforcement
# - At CloudFlare DNS change to orange cloud, with CloudFlare proxy,
# - https://github.com/wekan/wekan/blob/main/docs/Webserver/Caddy.md
# - Difference is additional tls section: tls { load /etc/caddy/certs }
customer.wekan.team {
tls {
load /etc/caddy/certs
}
header {
-Content-Security-Policy-Report-Only
-Content-Security-Policy
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: https://static.cloudflareinsights.com https://accounts.google.com https://apis.google.com; worker-src 'self' blob:; connect-src 'self' wss://customer.wekan.team https://customer.wekan.team https://cloudflareinsights.com https://accounts.google.com; img-src 'self' data: https://*.googleusercontent.com; frame-src 'self' https://accounts.google.com; style-src 'self' 'unsafe-inline'; font-src 'self' data:;"
Cross-Origin-Opener-Policy "same-origin-allow-popups"
X-Content-Type-Options nosniff
Strict-Transport-Security "max-age=31536000;"
-X-Frame-Options
Cache-Control "no-cache, no-store, must-revalidate"
}
handle /.well-known/* {
header Content-Type application/json
header Access-Control-Allow-Origin *
}
handle {
reverse_proxy 127.0.0.1:3002
}
}
# 3) Static website
# 3.1) Static Website: Let's Encrypt TLS
customer.com {
root * /data/websites/customer.com
file_server
}
# 3.2) Static Website: CloudFlare TLS
customer.com {
tls {
load /etc/caddy/certs
}
root * /data/websites/customer.com
file_server
}
# 4) Example of Sandstorm https://sandstorm.org web hosting
# - with WordPress, Hacker CMS, etc
# - https://github.com/wekan/wekan/blob/main/docs/Platforms/FOSS/Sandstorm/Sandstorm.md
# 4.1) Using Let's Encrypt: WordPress, Hacker CMS, etc
customer.fi {
reverse_proxy 127.0.0.1:81
}
# 4.2) Using CloudFlare TLS: WordPress, Hacker CMS, etc
blog.wekan.fi {
tls {
load /etc/caddy/certs
}
reverse_proxy 127.0.0.1:81
}
# 4.3) Sandstorm backup: /opt/backup.sh
#
# #!/bin/bash
# sandstorm stop
# tar -cf sandstorm-backup-$(date -I).tar /opt/sandstorm
# sandstorm start
# 4.4) Sandstorm config: /opt/sanstorm/sandstorm.conf
#
# SERVER_USER=sandstorm
# PORT=81
# MONGO_PORT=6081
# BIND_IP=127.0.0.1
# BASE_URL=https://wekan.dev
# WILDCARD_HOST=*.wekan.dev
# UPDATE_CHANNEL=dev
# ALLOW_DEV_ACCOUNTS=false
# SMTP_LISTEN_PORT=25
###### OLD, NOT IN USE:
### SANDCATS_BASE_DOMAIN=sandcats.io
### HTTPS_PORT=443
# 5) PHP/MySQL Wordpress
customer.com {
tls {
load /etc/caddy/certs
}
root * /data/websites/customer.com
encode gzip
file_server
php_fastcgi unix//run/php/php-fpm.sock
@disallowed {
path /xmlrpc.php
path *.sql
path /wp-content/uploads/*.php
}
rewrite @disallowed '/index.php'
}
# 6) Kanboard
# 6.1) Let's Encrypt TLS
kanboard.customer.com {
root * /data/websites/kanboard.customer.com
encode gzip
php_fastcgi unix//run/php/php-fpm.sock
try_files {path} {path}/ /index.php?{query}
file_server
log {
output file /var/log/caddy/kanboard_access.log
}
}
# 6.2) CloudFlare TLS
kanboard.customer.com {
tls {
load /etc/caddy/certs
}
root * /data/websites/kanboard.customer.com
encode gzip
php_fastcgi unix//run/php/php-fpm.sock
try_files {path} {path}/ /index.php?{query}
file_server
log {
output file /var/log/caddy/kanboard_access.log
}
}
# 7) Hesk Helpdesk
# 7.1) Let's Encrypt
hesk.customer.com {
root * /data/websites/hesk.customer.com
encode zstd gzip
php_fastcgi unix//run/php/php-fpm.sock
file_server
log {
output file /var/log/caddy/hesk_access.log
}
}
# 7.1) CloudFlare TLS
hesk.customer.com {
tls {
load /etc/caddy/certs
}
root * /data/websites/hesk.customer.com
encode zstd gzip
php_fastcgi unix//run/php/php-fpm.sock
file_server
log {
output file /var/log/caddy/hesk_access.log
}
}
# 8) (Note to myself: Some Apple developer example, not in use)
# (Currently website at GitHub Pages, this is not in use)
#wekan.fi www.wekan.fi {
# tls {
# load /etc/caddy/certs
# }
# header /.well-known/apple-developer-merchantid-domain-association/* Content-Type text/plain
# header /.well-known/apple-developer-merchantid-domain-association/* Access-Control-Allow-Origin *
# respond /.well-known/apple-developer-merchantid-domain-association `7B227073704964223A2246354246304143324336314131413238313043373531453439333444414433384346393037313041303935303844314133453241383436314141363232414145222C2276657273696F6E223A312C22637265617465644F6E223A313731353230343037313232362C227369676E6174757265223A223330383030363039326138363438383666373064303130373032613038303330383030323031303133313064333030623036303936303836343830313635303330343032303133303830303630393261383634383836663730643031303730313030303061303830333038323033653333303832303338386130303330323031303230323038313636333463386230653330353731373330306130363038326138363438636533643034303330323330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333031653137306433323334333033343332333933313337333433373332333735613137306433323339333033343332333833313337333433373332333635613330356633313235333032333036303335353034303330633163363536333633326437333664373032643632373236663662363537323264373336393637366535663535343333343264353035323466343433313134333031323036303335353034306230633062363934663533323035333739373337343635366437333331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333035393330313330363037326138363438636533643032303130363038326138363438636533643033303130373033343230303034633231353737656465626436633762323231386636386464373039306131323138646337623062643666326332383364383436303935643934616634613534313162383334323065643831316633343037653833333331663163353463336637656233323230643662616435643465666634393238393839336537633066313361333832303231313330383230323064333030633036303335353164313330313031666630343032333030303330316630363033353531643233303431383330313638303134323366323439633434663933653465663237653663346636323836633366613262626664326534623330343530363038326230363031303530353037303130313034333933303337333033353036303832623036303130353035303733303031383632393638373437343730336132663266366636333733373032653631373037303663363532653633366636643266366636333733373033303334326436313730373036633635363136393633363133333330333233303832303131643036303335353164323030343832303131343330383230313130333038323031306330363039326138363438383666373633363430353031333038316665333038316333303630383262303630313035303530373032303233303831623630633831623335323635366336393631366536333635323036663665323037343638363937333230363336353732373436393636363936333631373436353230363237393230363136653739323037303631373237343739323036313733373337353664363537333230363136333633363537303734363136653633363532303666363632303734363836353230373436383635366532303631373037303663363936333631363236633635323037333734363136653634363137323634323037343635373236643733323036313665363432303633366636653634363937343639366636653733323036663636323037353733363532633230363336353732373436393636363936333631373436353230373036663663363936333739323036313665363432303633363537323734363936363639363336313734363936663665323037303732363136333734363936333635323037333734363137343635366436353665373437333265333033363036303832623036303130353035303730323031313632613638373437343730336132663266373737373737326536313730373036633635326536333666366432663633363537323734363936363639363336313734363536313735373436383666373236393734373932663330333430363033353531643166303432643330326233303239613032376130323538363233363837343734373033613266326636333732366332653631373037303663363532653633366636643266363137303730366336353631363936333631333332653633373236633330316430363033353531643065303431363034313439343537646236666435373438313836383938393736326637653537383530376537396235383234333030653036303335353164306630313031666630343034303330323037383033303066303630393261383634383836663736333634303631643034303230353030333030613036303832613836343863653364303430333032303334393030333034363032323130306336663032336362323631346262333033383838613136323938336531613933663130353666353066613738636462396261346361323431636331346532356530323231303062653363643064666431363234376636343934343735333830653964343463323238613130383930613361316463373234623862346362383838393831386263333038323032656533303832303237356130303330323031303230323038343936643266626633613938646139373330306130363038326138363438636533643034303330323330363733313162333031393036303335353034303330633132343137303730366336353230353236663666373432303433343132303264323034373333333132363330323430363033353530343062306331643431373037303663363532303433363537323734363936363639363336313734363936663665323034313735373436383666373236393734373933313133333031313036303335353034306130633061343Line truncated
# root * /data/websites/wekan.fi
# file_server
#}
## 9) WeKan app https://wekan.fi/app/
# Note: It is not necessary to define all icons etc here at Caddyfile,
# because PWA related icons etc settings are also at
# newest WeKan Admin Panel / Settings.
boards.wekan.team {
tls {
load /etc/caddy/certs
}
header {
-Content-Security-Policy-Report-Only
-Content-Security-Policy
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: https://static.cloudflareinsights.com https://accounts.google.com https://apis.google.com; worker-src 'self' blob:; connect-src 'self' wss://boards.wekan.team https://boards.wekan.team https://cloudflareinsights.com https://accounts.google.com; img-src 'self' data: https://*.googleusercontent.com; frame-src 'self' https://accounts.google.com; style-src 'self' 'unsafe-inline'; font-src 'self' data:;"
Cross-Origin-Opener-Policy "same-origin-allow-popups"
X-Content-Type-Options nosniff
Strict-Transport-Security "max-age=31536000;"
-X-Frame-Options
Cache-Control "no-cache, no-store, must-revalidate"
}
handle /.well-known/assetlinks.json {
header Content-Type application/json
header Access-Control-Allow-Origin *
respond `[ {
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "team.wekan.boards.twa",
"sha256_cert_fingerprints": [
"61:41:86:5B:05:13:9B:64:5F:39:75:5A:16:C3:F2:22:25:6C:DA:74:B9:B0:8C:5F:93:B0:D2:26:65:16:1B:E6",
"AA:AA:ED:7D:4C:9C:5A:A3:B5:DA:10:66:14:34:07:5D:EB:BE:96:CD:82:7B:09:46:47:13:65:29:5B:EA:96:30"
]
}
} ]`
}
handle /.well-known/* {
header Content-Type application/json
header Access-Control-Allow-Origin *
}
handle /site.webmanifest {
header Content-Type application/manifest+json
header Access-Control-Allow-Origin *
respond ` {
"name": "Wekan",
"short_name": "Wekan",
"icons": [
{ "src": "svg-etc/wekan-logo-512.svg", "sizes": "512x512", "type": "image/svg+xml", "purpose": "any" },
{ "src": "android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "Square150x150Logo.scale-100.png", "sizes": "150x150", "type": "image/png" },
{ "src": "Square44x44Logo.scale-100.png", "sizes": "44x44", "type": "image/png" },
{ "src": "StoreLogo.scale-100.png", "sizes": "50x50", "type": "image/png" },
{ "src": "maskable_icon.png", "sizes": "474x474", "type": "image/png", "purpose": "maskable" },
{ "src": "monochrome-icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "monochrome" },
{ "src": "windows11/SmallTile.scale-100.png", "sizes": "71x71" },
{ "src": "windows11/SmallTile.scale-125.png", "sizes": "89x89" },
{ "src": "windows11/SmallTile.scale-150.png", "sizes": "107x107" },
{ "src": "windows11/SmallTile.scale-200.png", "sizes": "142x142" },
{ "src": "windows11/SmallTile.scale-400.png", "sizes": "284x284" },
{ "src": "windows11/Square150x150Logo.scale-100.png", "sizes": "150x150" },
{ "src": "windows11/Square150x150Logo.scale-125.png", "sizes": "188x188" },
{ "src": "windows11/Square150x150Logo.scale-150.png", "sizes": "225x225" },
{ "src": "windows11/Square150x150Logo.scale-200.png", "sizes": "300x300" },
{ "src": "windows11/Square150x150Logo.scale-400.png", "sizes": "600x600" },
{ "src": "windows11/Wide310x150Logo.scale-100.png", "sizes": "310x150" },
{ "src": "windows11/Wide310x150Logo.scale-125.png", "sizes": "388x188" },
{ "src": "windows11/Wide310x150Logo.scale-150.png", "sizes": "465x225" },
{ "src": "windows11/Wide310x150Logo.scale-200.png", "sizes": "620x300" },
{ "src": "windows11/Wide310x150Logo.scale-400.png", "sizes": "1240x600" },
{ "src": "windows11/LargeTile.scale-100.png", "sizes": "310x310" },
{ "src": "windows11/LargeTile.scale-125.png", "sizes": "388x388" },
{ "src": "windows11/LargeTile.scale-150.png", "sizes": "465x465" },
{ "src": "windows11/LargeTile.scale-200.png", "sizes": "620x620" },
{ "src": "windows11/LargeTile.scale-400.png", "sizes": "1240x1240" },
{ "src": "windows11/Square44x44Logo.scale-100.png", "sizes": "44x44" },
{ "src": "windows11/Square44x44Logo.scale-125.png", "sizes": "55x55" },
{ "src": "windows11/Square44x44Logo.scale-150.png", "sizes": "66x66" },
{ "src": "windows11/Square44x44Logo.scale-200.png", "sizes": "88x88" },
{ "src": "windows11/Square44x44Logo.scale-400.png", "sizes": "176x176" },
{ "src": "windows11/StoreLogo.scale-100.png", "sizes": "50x50" },
{ "src": "windows11/StoreLogo.scale-125.png", "sizes": "63x63" },
{ "src": "windows11/StoreLogo.scale-150.png", "sizes": "75x75" },
{ "src": "windows11/StoreLogo.scale-200.png", "sizes": "100x100" },
{ "src": "windows11/StoreLogo.scale-400.png", "sizes": "200x200" },
{ "src": "windows11/SplashScreen.scale-100.png", "sizes": "620x300" },
{ "src": "windows11/SplashScreen.scale-125.png", "sizes": "775x375" },
{ "src": "windows11/SplashScreen.scale-150.png", "sizes": "930x450" },
{ "src": "windows11/SplashScreen.scale-200.png", "sizes": "1240x600" },
{ "src": "windows11/SplashScreen.scale-400.png", "sizes": "2480x1200" },
{ "src": "windows11/Square44x44Logo.targetsize-16.png", "sizes": "16x16" },
{ "src": "windows11/Square44x44Logo.targetsize-20.png", "sizes": "20x20" },
{ "src": "windows11/Square44x44Logo.targetsize-24.png", "sizes": "24x24" },
{ "src": "windows11/Square44x44Logo.targetsize-30.png", "sizes": "30x30" },
{ "src": "windows11/Square44x44Logo.targetsize-32.png", "sizes": "32x32" },
{ "src": "windows11/Square44x44Logo.targetsize-36.png", "sizes": "36x36" },
{ "src": "windows11/Square44x44Logo.targetsize-40.png", "sizes": "40x40" },
{ "src": "windows11/Square44x44Logo.targetsize-44.png", "sizes": "44x44" },
{ "src": "windows11/Square44x44Logo.targetsize-48.png", "sizes": "48x48" },
{ "src": "windows11/Square44x44Logo.targetsize-60.png", "sizes": "60x60" },
{ "src": "windows11/Square44x44Logo.targetsize-64.png", "sizes": "64x64" },
{ "src": "windows11/Square44x44Logo.targetsize-72.png", "sizes": "72x72" },
{ "src": "windows11/Square44x44Logo.targetsize-80.png", "sizes": "80x80" },
{ "src": "windows11/Square44x44Logo.targetsize-96.png", "sizes": "96x96" },
{ "src": "windows11/Square44x44Logo.targetsize-256.png", "sizes": "256x256" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-16.png", "sizes": "16x16" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-20.png", "sizes": "20x20" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-24.png", "sizes": "24x24" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-30.png", "sizes": "30x30" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-32.png", "sizes": "32x32" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-36.png", "sizes": "36x36" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-40.png", "sizes": "40x40" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-44.png", "sizes": "44x44" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-48.png", "sizes": "48x48" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-60.png", "sizes": "60x60" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-64.png", "sizes": "64x64" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-72.png", "sizes": "72x72" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-80.png", "sizes": "80x80" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-96.png", "sizes": "96x96" },
{ "src": "windows11/Square44x44Logo.altform-unplated_targetsize-256.png", "sizes": "256x256" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png", "sizes": "16x16" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png", "sizes": "20x20" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png", "sizes": "24x24" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png", "sizes": "30x30" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png", "sizes": "32x32" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png", "sizes": "36x36" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png", "sizes": "40x40" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png", "sizes": "44x44" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png", "sizes": "48x48" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png", "sizes": "60x60" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png", "sizes": "64x64" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png", "sizes": "72x72" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png", "sizes": "80x80" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png", "sizes": "96x96" },
{ "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png", "sizes": "256x256" },
{ "src": "android/android-launchericon-512-512.png", "sizes": "512x512" },
{ "src": "android/android-launchericon-192-192.png", "sizes": "192x192" },
{ "src": "android/android-launchericon-144-144.png", "sizes": "144x144" },
{ "src": "android/android-launchericon-96-96.png", "sizes": "96x96" },
{ "src": "android/android-launchericon-72-72.png", "sizes": "72x72" },
{ "src": "android/android-launchericon-48-48.png", "sizes": "48x48" },
{ "src": "ios/16.png", "sizes": "16x16" },
{ "src": "ios/20.png", "sizes": "20x20" },
{ "src": "ios/29.png", "sizes": "29x29" },
{ "src": "ios/32.png", "sizes": "32x32" },
{ "src": "ios/40.png", "sizes": "40x40" },
{ "src": "ios/50.png", "sizes": "50x50" },
{ "src": "ios/57.png", "sizes": "57x57" },
{ "src": "ios/58.png", "sizes": "58x58" },
{ "src": "ios/60.png", "sizes": "60x60" },
{ "src": "ios/64.png", "sizes": "64x64" },
{ "src": "ios/72.png", "sizes": "72x72" },
{ "src": "ios/76.png", "sizes": "76x76" },
{ "src": "ios/80.png", "sizes": "80x80" },
{ "src": "ios/87.png", "sizes": "87x87" },
{ "src": "ios/100.png", "sizes": "100x100" },
{ "src": "ios/114.png", "sizes": "114x114" },
{ "src": "ios/120.png", "sizes": "120x120" },
{ "src": "ios/128.png", "sizes": "128x128" },
{ "src": "ios/144.png", "sizes": "144x144" },
{ "src": "ios/152.png", "sizes": "152x152" },
{ "src": "ios/167.png", "sizes": "167x167" },
{ "src": "ios/180.png", "sizes": "180x180" },
{ "src": "ios/192.png", "sizes": "192x192" },
{ "src": "ios/256.png", "sizes": "256x256" },
{ "src": "ios/512.png", "sizes": "512x512" },
{ "src": "ios/1024.png", "sizes": "1024x1024" }
],
"screenshots": [
{ "src": "screenshot1.webp", "sizes": "1280x720", "type": "image/webp" },
{ "src": "screenshot2.webp", "sizes": "1280x720", "type": "image/webp" }
],
"theme_color": "#000000",
"background_color": "#000000",
"start_url": "sign-in",
"id": "/",
"launch_handler": { "client_mode": "navigate-existing" },
"handle_links": "preferred",
"related_applications": [
{ "platform": "play", "url": "https://play.google.com/store/apps/details?id=team.wekan.boards.twa", "id": "team.wekan.boards.twa" },
{ "platform": "windows", "url": "https://apps.microsoft.com/detail/9P2MRXVD087R", "id": "61178WekanTeam.Wekan" },
{ "platform": "webapp", "url": "https://boards.wekan.team/sign-in", "id": "wekan.wekanteam" }
],
"display": "fullscreen",
"orientation": "any",
"categories": [ "productivity" ],
"iarc_rating_id": "70d7c4a4-3e5a-4714-a7dc-fa006613ba96",
"description": "Open Source kanban with MIT license",
"dir": "auto",
"lang": "en",
"scope": "https://boards.wekan.team",
"prefer_related_applications": false
}`
}
handle {
reverse_proxy 127.0.0.1:3025
}
}
+1
View File
@@ -0,0 +1 @@
curl -s http://127.0.0.1:3025 | grep -o 'sockjs'
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

@@ -0,0 +1,13 @@
# /etc/default/grub at Hetzner Ubuntu 26.04: mem_encrypt=on for
# RAM Memory Encryption. Also using XFS for MongoDB with RAID.
# ----
# If you change this file or any /etc/default/grub.d/*.cfg file,
# run 'update-grub' afterwards to update /boot/grub/grub.cfg.
# For full documentation of the options in these files, see:
# info -f grub -n 'Simple configuration'
GRUB_DEFAULT=0
GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR=`( . /etc/os-release && echo ${NAME} )`
GRUB_CMDLINE_LINUX_DEFAULT="consoleblank=0 mem_encrypt=on"
GRUB_CMDLINE_LINUX="consoleblank=0 systemd.show_status=true"
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# Create MongoDB keyfile
# Make sure to run as root
if [ "$EUID" -ne 0 ]; then
echo "Error: This script needs to be run as root."
echo "Use for example 'sudo su' before running this script."
exit 1
fi
# Create a folder for the key if it doesn't exist
mkdir -p /var/lib/mongodb/keys
# Generate a random 756 byte key in base64 format
openssl rand -base64 756 > /var/lib/mongodb/keys/mongo-keyfile
# Change the file owner to the mongodb user
chown -R mongodb:mongodb /var/lib/mongodb/keys
# Set strict permissions: only the owner can read
# (very important, otherwise Mongo won't start!)
chmod 400 /var/lib/mongodb/keys/mongo-keyfile
@@ -0,0 +1,3 @@
## `/etc/mongod.conf`
You also need to make key, with `../keys.sh`
@@ -0,0 +1,49 @@
# mongod.conf
# for documentation of all options, see:
# http://docs.mongodb.org/manual/reference/configuration-options/
# Where and how to store data.
storage:
dbPath: /var/lib/mongodb
engine: wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 32
# where to write logging data.
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod.log
# network interfaces
net:
port: 27017
bindIp: 127.0.0.1,172.17.0.1
# how the process runs
processManagement:
timeZoneInfo: /usr/share/zoneinfo
security:
authorization: enabled
keyFile: /var/lib/mongodb/keys/mongo-keyfile
setParameter:
logicalSessionRefreshMillis: 900000
localLogicalSessionTimeoutMinutes: 45
#operationProfiling:
#replication:
replication:
oplogSizeMB: 20480
replSetName: rs0
#sharding:
## Enterprise-Only Options:
#auditLog:
@@ -0,0 +1,96 @@
# Multitenancy
Run several WeKan instances (one per customer / domain) on the same
Linux host. Two supported topologies, pick whichever fits your
deployment.
## a) Recommended — one host, many WeKan domains, `uws` + `changeStreams`
Highest DDP throughput and lowest reactive latency.
Configure each instance with:
- `DDP_TRANSPORT=uws`
- `METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling`
- A **distinct** `Meteor.settings.packages['ddp-server'].uws.port`
per instance (`5001`, `5002`, `5003`, … in the example below)
- `network_mode: "host"` (or any setup that shares one kernel
network namespace)
- The same MongoDB replica set (separate logical databases per
tenant)
Why the distinct `uws.port`: the `uws` DDP transport runs its own
internal WebSocket proxy server on a dedicated TCP port (default
`127.0.0.1:5001`) and forwards public WebSocket upgrades into it.
The internal listen socket is opened exclusively, so each instance
in the same kernel netns must declare its own internal port.
Working example: see [`docker-compose-multitenancy.yml`](../../../../../docker-compose-multitenancy.yml)
in the repository root.
Sketch of the per-tenant environment block:
```yaml
environment:
- PORT=8081 # public HTTP port
- ROOT_URL=https://tenant1.example.com
- MONGO_URL=mongodb://127.0.0.1:27017/wekan_tenant1?replicaSet=rs0
- DDP_TRANSPORT=uws
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
- METEOR_SETTINGS={"packages":{"ddp-server":{"uws":{"port":5001,"host":"127.0.0.1"}}}}
```
For tenant 2, bump `PORT`, `ROOT_URL`, the MongoDB database name, and
the `uws.port` (e.g. `5002`). The reverse proxy in front of WeKan
keeps routing customer domains to each tenant's public `PORT`
exactly as before — the internal `uws.port` is never exposed.
### Verifying the configuration
From the host (or from inside any container sharing the netns):
```sh
cat /proc/net/tcp | awk '$4 == "0A" {print $2}' | sort | uniq -c
```
Each `uws.port` should appear exactly once. `0x1389` is `5001`,
`0x138A` is `5002`, etc. If you see `2 0100007F:1389`, two tenants
are trying to share the default port; reconfigure their
`METEOR_SETTINGS.uws.port` to distinct values and restart.
### If you forget to set a distinct `uws.port`
The second tenant to start refuses the bind and Meteor throws at
startup with an actionable error:
```text
Error: uWebSockets.js: failed to listen on 127.0.0.1:5001 (address already in use).
Another Meteor instance in this network namespace is already bound to this port.
Set a distinct Meteor.settings.packages["ddp-server"].uws.port (or .host) for each instance.
```
Add a `METEOR_SETTINGS` line with a free port to the failing
service, restart.
## b) Alternative — `sockjs` + `oplog` (maximum proxy compatibility)
Use this if your reverse proxy / load balancer cannot guarantee
WebSocket upgrade pass-through, or if some clients sit on networks
that block raw WebSocket. SockJS will fall back to HTTP long-polling
on those clients.
```yaml
- DDP_TRANSPORT=sockjs
- METEOR_REACTIVITY_ORDER=oplog,polling
```
No per-tenant `uws.port` configuration is needed because `sockjs`
does not run a separate internal listen socket. The trade-off is
lower DDP throughput compared to `uws`.
## Reference
- Meteor 3.5 DDP transport docs, including the multi-tenant section:
<https://docs.meteor.com/performance/ddp-transport#multitenancy>
- Meteor changeStreams reactivity driver:
<https://docs.meteor.com/performance/change-streams-observer-driver>
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# Loop through all subdirectories in the current directory
for d in */ ; do
# Remove the trailing slash to get just the folder name
FOLDER_NAME="${d%/}"
echo "--------------------------------------------------"
echo "Processing folder: $FOLDER_NAME"
echo "--------------------------------------------------"
# Change into the subdirectory, skip if it fails
cd "$FOLDER_NAME" || continue
# Run the Docker commands using the folder name
docker compose down
docker compose up -d --force-recreate
#docker compose stop
#docker compose start
#docker rm "${FOLDER_NAME}-tenant"
#docker compose up -d
# Return to the parent directory
cd ..
done
echo "All folders processed!"
@@ -0,0 +1,21 @@
# ========== CUSTOM END =====
## See ../multitenancy.md about commented out settings:
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
- DDP_TRANSPORT=uws
#- METEOR_REACTIVITY_ORDER=oplog,polling
#- DDP_TRANSPORT=sockjs
- WRITABLE_PATH=/data
- WITH_API=true
- RICHER_CARD_COMMENT_EDITOR=false
- CARD_OPENED_WEBHOOK_ENABLED=false
- BIGEVENTS_PATTERN=NONE
- NODE_FLAGS=--max-old-space-size=16384
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- /etc/localtime:/etc/localtime:ro
- wekan-files:/data:rw
volumes:
wekan-files:
driver: local
@@ -0,0 +1,9 @@
- PORT=3001
- ROOT_URL=https://wekan.customer.com
- MONGO_URL=mongodb://PARAMETER_user:PASSWORD@localhost:27017/PARAMETER?replicaSet=rs0&appName=PARAMETER-tenant&authSource=PARAMETER
- MONGO_OPLOG_URL=mongodb://PARAMETER_user:PASSWORD@localhost:27017/local?replicaSet=rs0&appName=PARAMETER-tenant-oplog&authSource=PARAMETER
# Distinct uws internal port per tenant — required since the
# 3.5-beta.12 fix (PR #14425) binds with LIBUS_LISTEN_EXCLUSIVE_PORT.
- METEOR_SETTINGS={"packages":{"ddp-server":{"uws":{"port":5001,"host":"127.0.0.1"}}}}
- MAIL_FROM=Customer WeKan Boards <boards@wekan.team>
- MAIL_URL=smtp://AKIA-USERNAME:PASSWORD@email-smtp.eu-west-1.amazonaws.com:587?tls={ciphers:"SSLv3"}&secureConnection=false
@@ -0,0 +1,9 @@
services:
PARAMETER:
image: ghcr.io/wekan/wekan:v9.20
container_name: PARAMETER-tenant
restart: unless-stopped
network_mode: "host"
environment:
# ========== CUSTOM START =====
Loaded 100 of 523 files, more files were not shown because too many files have changed in this diff. Show more