Files
wekan/server/rulesButton.js
T
Lauri Ojansivu ed19445d30 Fix #6459 #6455 #6458 #6466 #6467 #6468 #6472 #6465 #5547 #5282.
- #6459: card More menu rendered empty (no Delete): the vendored jade
  compiler's htmljs patch made isConstructedObject(Array) false, so
  'label {{_ ...}}:' lines got their content array mis-assigned as tag
  attributes and Blaze threw 'foreign objects in attributes' killing the
  whole cardMorePopup. Also fixed the list More link input (nonexistent
  rootUrl helper -> absoluteUrl).
- #6455: saving a card description never removed a pre-existing unsaved-
  edits record, and the draft/description comparison mismatched on Markdown
  hard-breaks and empty descriptions, re-creating a phantom draft on every
  save. Save now clears the draft; both sides normalized the same way.
- #5547: comment drafts: the draft-saving escape handler had been disarmed
  since 2019 (flag with no setter), so a comment being written was lost
  when the card closed. Drafts are now saved continuously (debounced),
  flushed on teardown, cleared on submit; text no longer visually vanishes
  on outside clicks.
- #5282: attachment delete: guard against the doc being already evicted
  from Minimongo ('Removed nonexistent document'), like the existing
  comment/checklist guards.
- #6472: rules move card to top/bottom: fall back to the card's current
  list when listName does not resolve (was a swallowed TypeError = rule
  silently did nothing); wizard stored listTitle which the engine never
  reads (listName); empty destination wrote sort +-Infinity; rules JSON/CSV
  import now uses the rules.createRule method instead of client inserts
  rejected into minimongo limbo, and defaults missing trigger matching
  fields to '*'; empty boardId no longer overrides the real board; the
  server-side orphaned-cards fallback in List.cards()/cardsUnfiltered()
  now actually resolves its async swimlane lookup.
- #6465: desktop density back to WeKan 6.09: base font 14px and headings
  22/18/16px (vw clamps resolved to 18/24px on desktop), All Boards
  wrapper padding 10px/15px, card details docks to the right edge instead
  of floating on top of its own card (still draggable), 20px canvas
  padding instead of ~48px white borders, removed the force-all-card-text-
  to-title-size rule, admin org/team table Select/Unselect-all header
  links are compact icons and header cells may wrap, zoom pill no longer
  cut off, one-click board settings cog in the board header.
- #6458 #6466: snap: mongodb-control now checks mongod's exit status and
  does an AVX pre-flight (MongoDB 5+ needs AVX; without it mongod SIGILLs)
  instead of pinging a dead port forever behind a 502: it switches to a
  completed FerretDB migration, or (re)runs the migration (AVX-free) with
  a 3-attempt guard. Migration importers no longer fail the whole run on
  >=10 per-item errors ('Avatars Errors' etc.), and a failed 3.x-source
  migration keeps the copied FerretDB data + checkpoint and resumes next
  start instead of deleting it and 'falling back' to a mongod 7 that
  cannot open 3.x data files.
- #6467 #6468: with FerretDB, calm Meteor's no-oplog polling storm
  (METEOR_POLLING_THROTTLE_MS=2000, METEOR_POLLING_INTERVAL_MS=30000
  defaults in the snap and bundle launcher; overridable). The FerretDB
  fork gets filter pushdown, a sane pool size and less locking in its own
  repo.
- Updated ChangeLog.

Thanks to xet7.
2026-07-16 17:08:31 +03:00

130 lines
5.6 KiB
JavaScript

import { Meteor } from 'meteor/meteor';
import { check, Match } from 'meteor/check';
import { ReactiveCache } from '/imports/reactiveCache';
import { RulesHelper } from '/server/rulesHelper';
import Rules from '/models/rules';
import Triggers from '/models/triggers';
import Actions from '/models/actions';
import { canDeleteBoardRule } from '/models/lib/ruleDeletePermission';
// Button rules are manual: a user clicks a card/board button and we run the
// rule's action immediately. This method runs one button rule on demand.
Meteor.methods({
async 'rules.runButton'(ruleId, cardId) {
check(ruleId, String);
check(cardId, Match.Optional(String));
const rule = await ReactiveCache.getRule(ruleId);
if (!rule) throw new Meteor.Error('not-found', 'Rule not found');
const trigger = await ReactiveCache.getTrigger(rule.triggerId);
if (!trigger || trigger.activityType !== 'button') {
throw new Meteor.Error('not-a-button', 'Rule is not a button rule');
}
// The user must be a member of the board the rule belongs to.
const board = await ReactiveCache.getBoard(rule.boardId);
if (!board || !board.hasMember(this.userId)) {
throw new Meteor.Error('not-authorized', 'Not a board member');
}
const action = await ReactiveCache.getAction(rule.actionId);
if (!action) throw new Meteor.Error('not-found', 'Action not found');
await RulesHelper.performAction(
{
activityType: 'button',
cardId,
boardId: rule.boardId,
userId: this.userId,
},
action,
);
return true;
},
// Create a rule (trigger + action + rule) on the server in one call. The rule
// wizard previously did three client-side Collection.insert() calls; those are
// optimistic writes whose documents end up in client-minimongo limbo once the
// wizard's `boardRules` subscription stops on navigation, so a freshly created
// board-button's trigger never re-published to the board view (the button only
// appeared after a full page reload). Inserting on the server avoids the
// optimistic-ghost reconciliation problem entirely.
async 'rules.createRule'(boardId, title, trigger, action) {
check(boardId, String);
check(title, Match.Optional(String));
check(trigger, Object);
check(action, Object);
const board = await ReactiveCache.getBoard(boardId);
if (!board) throw new Meteor.Error('not-found', 'Board not found');
if (!board.hasAdmin(this.userId)) {
throw new Meteor.Error('not-authorized', 'Must be a board admin');
}
const clean = doc => {
const { _id, ...rest } = doc || {};
return rest;
};
const triggerId = await Triggers.insertAsync({ ...clean(trigger), boardId });
// #5536: an action's own boardId is the DESTINATION board (e.g. "move card
// to board X" / "link card to board X"). Let it win over the source boardId
// so cross-board move/link rules created through this method keep their
// target — only fall back to the source board when the action omits one.
// #6472: "omits" must include an EMPTY boardId (a not-yet-loaded board
// select posts ''), which the spread would otherwise let override the
// fallback and make every list lookup in the action fail silently.
const actionDoc = { boardId, ...clean(action) };
if (!actionDoc.boardId) actionDoc.boardId = boardId;
const actionId = await Actions.insertAsync(actionDoc);
const ruleDoc = {
title: title || 'Rule',
triggerId,
actionId,
boardId,
};
// Denormalise manual-button metadata onto the rule. The board view renders
// its board/card buttons from the rule document alone, because in this Meteor
// 3 setup the schemaless `triggers` collection's documents do not reach the
// client over the board subscription (the schema-backed `rules` collection
// does), so reading the label off the published trigger would leave the
// button blank/absent.
if (trigger && trigger.activityType === 'button') {
ruleDoc.buttonType = trigger.buttonType || 'card';
ruleDoc.buttonLabel = trigger.buttonLabel || title || 'Run';
}
const ruleId = await Rules.insertAsync(ruleDoc);
return { _id: ruleId, triggerId, actionId };
},
// Delete a rule (and its trigger + action) on the server in one call. The rule
// wizard/list/workflow views previously ran three client-side
// Collection.remove() calls; each is gated by a per-collection allow() rule
// that resolves the board from that document's own boardId. A Trigger/Action
// whose boardId does not resolve to a board (legacy docs, or docs not
// published to the client) made the allow rule return false and Meteor
// rejected the mutation with 403 "Access denied", failing the delete even for
// a board admin. Authorizing once here and removing all three docs server-side
// (which bypasses allow/deny) fixes that without loosening any permission.
async 'rules.deleteRule'(ruleId) {
check(ruleId, String);
const rule = await ReactiveCache.getRule(ruleId);
if (!rule) throw new Meteor.Error('not-found', 'Rule not found');
const board = await ReactiveCache.getBoard(rule.boardId);
if (!board) throw new Meteor.Error('not-found', 'Board not found');
const user = await ReactiveCache.getUser(this.userId);
if (!canDeleteBoardRule(board, this.userId, { isSiteAdmin: !!(user && user.isAdmin) })) {
throw new Meteor.Error('not-authorized', 'Must be a board admin');
}
await Rules.removeAsync(rule._id);
if (rule.triggerId) await Triggers.removeAsync(rule.triggerId);
if (rule.actionId) await Actions.removeAsync(rule.actionId);
return { _id: rule._id };
},
});