Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
['name' => 'stack#deleted', 'url' => '/{boardId}/stacks/deleted', 'verb' => 'GET'],
['name' => 'stack#archived', 'url' => '/stacks/{boardId}/archived', 'verb' => 'GET'],

// stack automations
['name' => 'stack_automation#index', 'url' => '/stacks/{stackId}/automations', 'verb' => 'GET'],
['name' => 'stack_automation#create', 'url' => '/stacks/{stackId}/automations', 'verb' => 'POST'],
['name' => 'stack_automation#update', 'url' => '/automations/{id}', 'verb' => 'PUT'],
['name' => 'stack_automation#delete', 'url' => '/automations/{id}', 'verb' => 'DELETE'],

// cards
['name' => 'card#read', 'url' => '/cards/{cardId}', 'verb' => 'GET'],
['name' => 'card#create', 'url' => '/cards', 'verb' => 'POST'],
Expand Down
7 changes: 7 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use OCA\Deck\Event\SessionCreatedEvent;
use OCA\Deck\Listeners\AclCreatedRemovedListener;
use OCA\Deck\Listeners\BeforeTemplateRenderedListener;
use OCA\Deck\Listeners\CardAutomationListener;
use OCA\Deck\Listeners\CommentEventListener;
use OCA\Deck\Listeners\FullTextSearchEventListener;
use OCA\Deck\Listeners\LiveUpdateListener;
Expand Down Expand Up @@ -143,6 +144,12 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(CardCreatedEvent::class, FullTextSearchEventListener::class);
$context->registerEventListener(CardUpdatedEvent::class, FullTextSearchEventListener::class);
$context->registerEventListener(CardDeletedEvent::class, FullTextSearchEventListener::class);

// Event listening for stack automations
$context->registerEventListener(CardCreatedEvent::class, CardAutomationListener::class);
$context->registerEventListener(CardUpdatedEvent::class, CardAutomationListener::class);
$context->registerEventListener(CardDeletedEvent::class, CardAutomationListener::class);

$context->registerEventListener(AclCreatedEvent::class, FullTextSearchEventListener::class);
$context->registerEventListener(AclUpdatedEvent::class, FullTextSearchEventListener::class);
$context->registerEventListener(AclDeletedEvent::class, FullTextSearchEventListener::class);
Expand Down
60 changes: 60 additions & 0 deletions lib/Automation/ActionFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Deck\Automation;

use OCA\Deck\Automation\Actions\AddLabelAction;
use OCA\Deck\Automation\Actions\ArchiveAction;
use OCA\Deck\Automation\Actions\RemoveDoneAction;
use OCA\Deck\Automation\Actions\RemoveLabelAction;
use OCA\Deck\Automation\Actions\SetDoneAction;
use OCA\Deck\Automation\Actions\WebhookAction;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\LabelMapper;
use OCA\Deck\Service\CardService;
use OCP\Http\Client\IClientService;
use Psr\Log\LoggerInterface;

class ActionFactory {
public const ACTION_ADD_LABEL = 'add_label';
public const ACTION_REMOVE_LABEL = 'remove_label';
public const ACTION_SET_DONE = 'set_done';
public const ACTION_REMOVE_DONE = 'remove_done';
public const ACTION_WEBHOOK = 'webhook';
public const ACTION_ARCHIVE = 'archive';

public function __construct(
private CardMapper $cardMapper,
private LabelMapper $labelMapper,
private IClientService $clientService,
private LoggerInterface $logger,
) {
}

public function createAction(string $actionType): ?ActionInterface {
return match ($actionType) {
self::ACTION_ADD_LABEL => new AddLabelAction($this->cardMapper, $this->labelMapper, $this->logger),
self::ACTION_REMOVE_LABEL => new RemoveLabelAction($this->cardMapper, $this->labelMapper, $this->logger),
self::ACTION_SET_DONE => new SetDoneAction($this->cardMapper, $this->logger),
self::ACTION_REMOVE_DONE => new RemoveDoneAction($this->cardMapper, $this->logger),
self::ACTION_WEBHOOK => new WebhookAction($this->clientService, $this->logger),
self::ACTION_ARCHIVE => new ArchiveAction($this->cardMapper, $this->logger),
default => null,
};
}

public function getSupportedActions(): array {
return [
self::ACTION_ADD_LABEL,
self::ACTION_REMOVE_LABEL,
self::ACTION_SET_DONE,
self::ACTION_REMOVE_DONE,
self::ACTION_WEBHOOK,
self::ACTION_ARCHIVE,
];
}
}
47 changes: 47 additions & 0 deletions lib/Automation/ActionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Deck\Automation;

use OCA\Deck\Db\Card;

interface ActionInterface {
/**
* Execute the action on the given card
*
* @param Card $card The card to execute the action on
* @param AutomationEvent $event The event that triggered the action
* @param array $config The action configuration
* @throws \Exception If the action fails
*/
public function execute(Card $card, AutomationEvent $event, array $config = []): void;

/**
* Validate the action configuration
*
* @param array $config The configuration to validate
* @return bool True if valid, false otherwise
*/
public function validateConfig(array $config): bool;

/**
* Check if this action is applicable for the given event
* For example, adding labels doesn't make sense on card deletion
*
* @param AutomationEvent $event The event to check
* @return bool True if the action should be executed, false otherwise
*/
public function isApplicableForEvent(AutomationEvent $event): bool;

/**
* Get a human-readable description of the action
*
* @param array $config The action configuration
* @return string Description of the action
*/
public function getDescription(array $config): string;
}
66 changes: 66 additions & 0 deletions lib/Automation/Actions/AddLabelAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Deck\Automation\Actions;

use OCA\Deck\Automation\ActionInterface;
use OCA\Deck\Automation\AutomationEvent;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\LabelMapper;
use Psr\Log\LoggerInterface;

class AddLabelAction implements ActionInterface {
public function __construct(
private CardMapper $cardMapper,
private LabelMapper $labelMapper,
private LoggerInterface $logger,
) {
}

public function execute(Card $card, AutomationEvent $event, array $config = []): void {
$labelIds = $config['labelIds'] ?? [];

if (empty($labelIds) || !is_array($labelIds)) {
$this->logger->warning('AddLabelAction: Missing or invalid labelIds in configuration');
return;
}

try {
// Get currently assigned labels
$assignedLabels = $this->labelMapper->findAssignedLabelsForCard($card->getId());
$assignedLabelIds = array_map(fn($label) => $label->getId(), $assignedLabels);

foreach ($labelIds as $labelId) {
// Only add if not already assigned
if (!in_array((int)$labelId, $assignedLabelIds, true)) {
$this->cardMapper->assignLabel($card->getId(), (int)$labelId);
}
}
} catch (\Exception $e) {
$this->logger->error('AddLabelAction failed: ' . $e->getMessage(), ['exception' => $e]);
throw $e;
}
}

public function validateConfig(array $config): bool {
return isset($config['labelIds'])
&& is_array($config['labelIds'])
&& !empty($config['labelIds'])
&& array_reduce($config['labelIds'], fn($valid, $id) => $valid && is_numeric($id), true);
}

public function isApplicableForEvent(AutomationEvent $event): bool {
return $event->getEventName() !== AutomationEvent::EVENT_DELETE;
}

public function getDescription(array $config): string {
$labelIds = $config['labelIds'] ?? [];
$count = count($labelIds);
return $count > 0 ? "Add {$count} label(s)" : "Add labels";
}
}
45 changes: 45 additions & 0 deletions lib/Automation/Actions/ArchiveAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Deck\Automation\Actions;

use OCA\Deck\Automation\ActionInterface;
use OCA\Deck\Automation\AutomationEvent;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use Psr\Log\LoggerInterface;

class ArchiveAction implements ActionInterface {
public function __construct(
private CardMapper $cardMapper,
private LoggerInterface $logger,
) {
}

public function execute(Card $card, AutomationEvent $event, array $config = []): void {
try {
$card->setArchived(true);
$this->cardMapper->update($card);
} catch (\Exception $e) {
$this->logger->error('ArchiveAction failed: ' . $e->getMessage(), ['exception' => $e]);
throw $e;
}
}

public function validateConfig(array $config): bool {
// No configuration needed
return true;
}

public function isApplicableForEvent(AutomationEvent $event): bool {
return $event->getEventName() !== AutomationEvent::EVENT_DELETE;
}

public function getDescription(array $config): string {
return "Archive card";
}
}
45 changes: 45 additions & 0 deletions lib/Automation/Actions/RemoveDoneAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Deck\Automation\Actions;

use OCA\Deck\Automation\ActionInterface;
use OCA\Deck\Automation\AutomationEvent;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use Psr\Log\LoggerInterface;

class RemoveDoneAction implements ActionInterface {
public function __construct(
private CardMapper $cardMapper,
private LoggerInterface $logger,
) {
}

public function execute(Card $card, AutomationEvent $event, array $config = []): void {
try {
$card->setDone(null);
$this->cardMapper->update($card);
} catch (\Exception $e) {
$this->logger->error('RemoveDoneAction failed: ' . $e->getMessage(), ['exception' => $e]);
throw $e;
}
}

public function validateConfig(array $config): bool {
// No configuration needed
return true;
}

public function isApplicableForEvent(AutomationEvent $event): bool {
return $event->getEventName() !== AutomationEvent::EVENT_DELETE;
}

public function getDescription(array $config): string {
return "Unmark card as done";
}
}
66 changes: 66 additions & 0 deletions lib/Automation/Actions/RemoveLabelAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Deck\Automation\Actions;

use OCA\Deck\Automation\ActionInterface;
use OCA\Deck\Automation\AutomationEvent;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\LabelMapper;
use Psr\Log\LoggerInterface;

class RemoveLabelAction implements ActionInterface {
public function __construct(
private CardMapper $cardMapper,
private LabelMapper $labelMapper,
private LoggerInterface $logger,
) {
}

public function execute(Card $card, AutomationEvent $event, array $config = []): void {
$labelIds = $config['labelIds'] ?? [];

if (empty($labelIds) || !is_array($labelIds)) {
$this->logger->warning('RemoveLabelAction: Missing or invalid labelIds in configuration');
return;
}

try {
// Get currently assigned labels
$assignedLabels = $this->labelMapper->findAssignedLabelsForCard($card->getId());
$assignedLabelIds = array_map(fn($label) => $label->getId(), $assignedLabels);

foreach ($labelIds as $labelId) {
// Only remove if currently assigned
if (in_array((int)$labelId, $assignedLabelIds, true)) {
$this->cardMapper->removeLabel($card->getId(), (int)$labelId);
}
}
} catch (\Exception $e) {
$this->logger->error('RemoveLabelAction failed: ' . $e->getMessage(), ['exception' => $e]);
throw $e;
}
}

public function validateConfig(array $config): bool {
return isset($config['labelIds'])
&& is_array($config['labelIds'])
&& !empty($config['labelIds'])
&& array_reduce($config['labelIds'], fn($valid, $id) => $valid && is_numeric($id), true);
}

public function isApplicableForEvent(AutomationEvent $event): bool {
return $event->getEventName() !== AutomationEvent::EVENT_DELETE;
}

public function getDescription(array $config): string {
$labelIds = $config['labelIds'] ?? [];
$count = count($labelIds);
return $count > 0 ? "Remove {$count} label(s)" : "Remove labels";
}
}
Loading