Add Deck workflow automation Nextcloud app
Lint info.xml / xml-lint (push) Successful in 30s
Lint PHP / php-lint (8.2) (push) Successful in 55s
Lint PHP / php-lint (8.3) (push) Successful in 44s
Lint PHP / php-lint (8.4) (push) Successful in 41s
PHPUnit / unit-tests (push) Failing after 41s

Ports the standalone due-date cron script into a proper Nextcloud 34
app with a personal-settings UI, per-user workflow configuration
(source/target stack, assigned-user and label filters, email
notification), a TimedJob background runner, and Gitea CI pipelines.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Patrick Niebeling
2026-08-13 10:13:05 +02:00
co-authored by Claude Sonnet 5
commit 300acde27e
34 changed files with 1918 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace OCA\WorkflowDeckAutomation\Controller;
use OCA\WorkflowDeckAutomation\Db\Workflow;
use OCA\WorkflowDeckAutomation\Db\WorkflowMapper;
use OCA\WorkflowDeckAutomation\Service\DeckIntegrationService;
use OCA\WorkflowDeckAutomation\Service\DeckUnavailableException;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCS\OCSPreconditionFailedException;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
use OCP\IUserSession;
class WorkflowController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
private WorkflowMapper $workflowMapper,
private DeckIntegrationService $deckService,
private IUserSession $userSession,
) {
parent::__construct($appName, $request);
}
private function currentUserId(): string {
$user = $this->userSession->getUser();
if ($user === null) {
throw new OCSPreconditionFailedException('Not logged in');
}
return $user->getUID();
}
#[NoAdminRequired]
public function index(): DataResponse {
$workflows = $this->workflowMapper->findAllForUser($this->currentUserId());
return new DataResponse(array_map(static fn (Workflow $w) => $w->jsonSerialize(), $workflows));
}
/**
* @param string[] $filterUserIds
* @param int[] $filterLabelIds
*/
#[NoAdminRequired]
public function create(
string $title,
int $boardId,
int $sourceStackId,
int $targetStackId,
array $filterUserIds = [],
array $filterLabelIds = [],
bool $notifyEmail = false,
bool $enabled = true,
): DataResponse {
$this->validate($title, $sourceStackId, $targetStackId);
$workflow = new Workflow();
$workflow->setUserId($this->currentUserId());
$this->applyFields($workflow, $title, $boardId, $sourceStackId, $targetStackId, $filterUserIds, $filterLabelIds, $notifyEmail, $enabled);
$workflow = $this->workflowMapper->insert($workflow);
return new DataResponse($workflow->jsonSerialize(), Http::STATUS_CREATED);
}
/**
* @param string[] $filterUserIds
* @param int[] $filterLabelIds
*/
#[NoAdminRequired]
public function update(
int $id,
string $title,
int $boardId,
int $sourceStackId,
int $targetStackId,
array $filterUserIds = [],
array $filterLabelIds = [],
bool $notifyEmail = false,
bool $enabled = true,
): DataResponse {
$this->validate($title, $sourceStackId, $targetStackId);
try {
$workflow = $this->workflowMapper->findForUser($id, $this->currentUserId());
} catch (DoesNotExistException $e) {
throw new OCSNotFoundException('Workflow not found');
}
$this->applyFields($workflow, $title, $boardId, $sourceStackId, $targetStackId, $filterUserIds, $filterLabelIds, $notifyEmail, $enabled);
$workflow = $this->workflowMapper->update($workflow);
return new DataResponse($workflow->jsonSerialize());
}
#[NoAdminRequired]
public function destroy(int $id): DataResponse {
try {
$workflow = $this->workflowMapper->findForUser($id, $this->currentUserId());
} catch (DoesNotExistException $e) {
throw new OCSNotFoundException('Workflow not found');
}
$this->workflowMapper->delete($workflow);
return new DataResponse([]);
}
#[NoAdminRequired]
public function boards(): DataResponse {
$this->assertDeck();
return new DataResponse($this->deckService->listBoardsForCurrentUser());
}
#[NoAdminRequired]
public function stacks(int $boardId): DataResponse {
$this->assertDeck();
return new DataResponse($this->deckService->listStacks($boardId));
}
#[NoAdminRequired]
public function labels(int $boardId): DataResponse {
$this->assertDeck();
return new DataResponse($this->deckService->listLabels($boardId));
}
#[NoAdminRequired]
public function participants(int $boardId): DataResponse {
$this->assertDeck();
return new DataResponse($this->deckService->listParticipants($boardId));
}
private function assertDeck(): void {
$user = $this->userSession->getUser();
if ($user === null) {
throw new OCSPreconditionFailedException('Not logged in');
}
try {
$this->deckService->assertDeckAvailable($user);
} catch (DeckUnavailableException $e) {
throw new OCSPreconditionFailedException('Deck is not available: ' . $e->getMessage());
}
}
private function validate(string $title, int $sourceStackId, int $targetStackId): void {
if (trim($title) === '') {
throw new OCSBadRequestException('Title must not be empty');
}
if ($sourceStackId === $targetStackId) {
throw new OCSBadRequestException('Source and target stack must differ');
}
}
/**
* @param string[] $filterUserIds
* @param int[] $filterLabelIds
*/
private function applyFields(
Workflow $workflow,
string $title,
int $boardId,
int $sourceStackId,
int $targetStackId,
array $filterUserIds,
array $filterLabelIds,
bool $notifyEmail,
bool $enabled,
): void {
$workflow->setTitle($title);
$workflow->setBoardId($boardId);
$workflow->setSourceStackId($sourceStackId);
$workflow->setTargetStackId($targetStackId);
$workflow->setFilterUserIds($filterUserIds === [] ? null : json_encode(array_values($filterUserIds)));
$workflow->setFilterLabelIds($filterLabelIds === [] ? null : json_encode(array_values($filterLabelIds)));
$workflow->setNotifyEmail($notifyEmail);
$workflow->setEnabled($enabled);
}
}