revert 0e66319a06
Build package / php-lint (8.4) (push) Successful in 43s
Build package / php-lint (8.5) (push) Successful in 40s
Build package / xml-lint (push) Successful in 12s
Build package / unit-tests (push) Successful in 38s
Build package / package (push) Successful in 52s

revert Let each workflow watch the due date or the start date

Deck 1.18 added a start date to cards (Card::$startdate), so a workflow
no longer has to mean "overdue". Each rule now carries a date_field
('due' | 'start'), chosen in the settings form next to board and stacks.

Existing rules keep the due date: the new column's default supplies it
for every stored row, so there is no backfill step, and
Workflow::getDateFieldOrDue() covers entities that were never near the
database as well as hand-edited values. The controller is the one place
that rejects an unknown value instead of normalising it - a client
asking for a rule it would not get should hear about it.

The probe for the field is property_exists(), not method_exists():
Deck's entities declare no getter as real code - getStartdate(),
getDuedate(), even getId() all go through Entity::__call(), which
method_exists() ignores by definition. A method_exists() guard would
have been false on every Deck version and would have turned every
start-date rule into a silent no-op.

assertDeckAvailable() now also refuses Deck older than 1.18.0. That
floor cannot live in appinfo/info.xml - neither the server's nor the app
store's schema has an app-to-app dependency element - but
<nextcloud min-version="34"> already implies it, since 1.18.x is the
only Deck release line published for Nextcloud 34.

isOverdue() becomes hasPassed(): one function for both dates, since the
comparison and the argument against Deck's day-truncating
getDaysUntilDue() are identical for either.

Also carries a pending NcSelect icon-alignment fix that was already in
the working tree.

Bump to 34.1.0.
This commit is contained in:
2026-08-26 07:10:55 +02:00
parent 0e66319a06
commit 28b4edc811
13 changed files with 41 additions and 390 deletions
+1 -90
View File
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace OCA\WorkflowDeckAutomation\Service;
use DateTimeInterface;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
@@ -36,25 +35,6 @@ use Throwable;
class DeckIntegrationService {
private const DECK_APP_ID = 'deck';
/**
* Oldest Deck this app will talk to.
*
* Deck 1.18.0 is the release that added the card start date
* (`Card::$startdate`, PR #7749), which the start-date workflows read.
* The floor cannot live in appinfo/info.xml: neither the server's
* app-info.xsd nor the app store's info.xsd has an app-to-app dependency
* element, only <nextcloud>.
*
* That pin does most of the work anyway — Deck 1.18.x is the only release
* line declaring `>=34.0.0,<35.0.0`, so on the Nextcloud 34 this app
* requires, every installable Deck already carries the field. What is left
* for this check is a Deck put in place by hand or from git.
*
* version_compare() sorts `1.18.0-beta.x` *below* `1.18.0`, and that is
* intended: the betas of that line may predate the field.
*/
private const MIN_DECK_VERSION = '1.18.0';
/**
* Deck's Acl::PERMISSION_TYPE_* values, inlined so this class keeps
* working (degrading to "treat it as a user") if Deck ever moves or
@@ -88,31 +68,11 @@ class DeckIntegrationService {
'OCA\Deck\Activity\ActivityManager',
];
/**
* `Workflow::DATE_FIELD_*` mapped to the property on Deck's Card entity.
* Kept here rather than on the Workflow so this class stays the only one
* that has to know what Deck calls its columns.
*/
private const CARD_DATE_PROPERTIES = [
'due' => 'duedate',
'start' => 'startdate',
];
private const DEFAULT_CARD_DATE = 'due';
/**
* @var list<array{ReflectionProperty, object, ?string}>
*/
private array $identityRestore = [];
/**
* Card properties already reported as missing, so the warning in
* getCardDate() is one line per process instead of one per card.
*
* @var array<string, true>
*/
private array $reportedMissingCardProperties = [];
public function __construct(
private IAppManager $appManager,
private IUserManager $userManager,
@@ -206,16 +166,6 @@ class DeckIntegrationService {
if (!$this->appManager->isEnabledForUser(self::DECK_APP_ID, $user)) {
throw new DeckUnavailableException('Deck is not enabled for user ' . $user->getUID());
}
$version = $this->appManager->getAppVersion(self::DECK_APP_ID);
if ($version === '' || version_compare($version, self::MIN_DECK_VERSION, '<')) {
throw new DeckUnavailableException(sprintf(
'Deck %s is installed, but %s or newer is required',
$version !== '' ? $version : '(unknown version)',
self::MIN_DECK_VERSION,
));
}
if (!class_exists(CardService::class) || !class_exists(BoardService::class) || !class_exists(StackService::class)) {
throw new DeckUnavailableException('Deck internal classes are not available');
}
@@ -537,7 +487,7 @@ class DeckIntegrationService {
// *returns* CardDetails wrappers whose own fields (duedate, stackId, ...) were never
// copied from the wrapped card - only its overridden jsonSerialize() reads through to
// them. Capturing that return value here made every card's getDuedate()/getDaysUntilDue()
// resolve to null, so hasPassed() was always false and no workflow ever matched a card.
// resolve to null, so isOverdue() was always false and no workflow ever matched a card.
// Keep using the original, now-enriched $cards instead.
$cardService->enrichCards($cards);
@@ -550,45 +500,6 @@ class DeckIntegrationService {
}, []);
}
/**
* The card date a workflow is evaluated against: Deck's due date or its
* start date, per `Workflow::DATE_FIELD_*`. Anything unrecognised reads the
* due date, which is what every workflow meant before the choice existed.
*
* The guard is `property_exists()`, deliberately, and this is the one thing
* to get right here: **`method_exists()` cannot see Deck's getters.**
* `Card`, `Board`, `Label`, `Acl` and `Assignment` declare none of them as
* real code — `getStartdate()`, `getDuedate()`, even `getId()` all go
* through `OCP\AppFramework\Db\Entity::__call()`, and `method_exists()`
* ignores `__call()` by definition. A `method_exists($card, 'getStartdate')`
* probe would therefore be false on *every* Deck version and silently turn
* every start-date workflow into a no-op. `property_exists()` sees the
* protected `$startdate` declaration and is exactly what `Entity::getter()`
* checks internally before throwing.
*
* In practice the property is always there — `assertDeckAvailable()`
* enforces Deck >= MIN_DECK_VERSION, which is the release that added it —
* so this branch is a backstop for a hand-installed Deck, loud enough to be
* found in the log rather than silently matching nothing.
*/
public function getCardDate(Card $card, string $dateField): ?DateTimeInterface {
$property = self::CARD_DATE_PROPERTIES[$dateField] ?? self::CARD_DATE_PROPERTIES[self::DEFAULT_CARD_DATE];
if (!property_exists($card, $property)) {
if (!isset($this->reportedMissingCardProperties[$property])) {
$this->reportedMissingCardProperties[$property] = true;
$this->logger->warning(
'This Deck version has no Card::$' . $property . '; workflows using it cannot match any card',
['app' => 'workflow_deck_automation'],
);
}
return null;
}
$value = $property === 'startdate' ? $card->getStartdate() : $card->getDuedate();
return $value instanceof DateTimeInterface ? $value : null;
}
/**
* @return string[]
*/