Skip to content

Commit d8e01f8

Browse files
feat(workflowengine): add name, description and collapsible UI to flow rules
Rules were always rendered expanded, making the list unmanageable when more than a handful of rules were configured. Users had to scroll through all editors just to locate a specific rule. Each rule now shows a collapsible header with its name, falling back to "Unnamed flow". Saved rules collapse by default; new unsaved rules expand automatically. A description field is available inside the expanded rule editor. Signed-off-by: Dominic Blaß <letmefixthis.code@gmail.com>
1 parent 09f1bb2 commit d8e01f8

20 files changed

Lines changed: 735 additions & 109 deletions

apps/workflowengine/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
'OCA\\WorkflowEngine\\Migration\\PopulateNewlyIntroducedDatabaseFields' => $baseDir . '/../lib/Migration/PopulateNewlyIntroducedDatabaseFields.php',
3535
'OCA\\WorkflowEngine\\Migration\\Version2000Date20190808074233' => $baseDir . '/../lib/Migration/Version2000Date20190808074233.php',
3636
'OCA\\WorkflowEngine\\Migration\\Version2200Date20210805101925' => $baseDir . '/../lib/Migration/Version2200Date20210805101925.php',
37+
'OCA\\WorkflowEngine\\Migration\\Version3000Date20260531150003' => $baseDir . '/../lib/Migration/Version3000Date20260531150003.php',
3738
'OCA\\WorkflowEngine\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
3839
'OCA\\WorkflowEngine\\Service\\Logger' => $baseDir . '/../lib/Service/Logger.php',
3940
'OCA\\WorkflowEngine\\Service\\RuleMatcher' => $baseDir . '/../lib/Service/RuleMatcher.php',

apps/workflowengine/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class ComposerStaticInitWorkflowEngine
4949
'OCA\\WorkflowEngine\\Migration\\PopulateNewlyIntroducedDatabaseFields' => __DIR__ . '/..' . '/../lib/Migration/PopulateNewlyIntroducedDatabaseFields.php',
5050
'OCA\\WorkflowEngine\\Migration\\Version2000Date20190808074233' => __DIR__ . '/..' . '/../lib/Migration/Version2000Date20190808074233.php',
5151
'OCA\\WorkflowEngine\\Migration\\Version2200Date20210805101925' => __DIR__ . '/..' . '/../lib/Migration/Version2200Date20210805101925.php',
52+
'OCA\\WorkflowEngine\\Migration\\Version3000Date20260531150003' => __DIR__ . '/..' . '/../lib/Migration/Version3000Date20260531150003.php',
5253
'OCA\\WorkflowEngine\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
5354
'OCA\\WorkflowEngine\\Service\\Logger' => __DIR__ . '/..' . '/../lib/Service/Logger.php',
5455
'OCA\\WorkflowEngine\\Service\\RuleMatcher' => __DIR__ . '/..' . '/../lib/Service/RuleMatcher.php',

apps/workflowengine/lib/Controller/AWorkflowOCSController.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ public function show(string $id): DataResponse {
9595
* @param string $operation Operation class to execute on match
9696
* @param string $entity The matched entity
9797
* @param list<class-string<IEntityEvent>> $events The list of events on which the rule should be validated
98+
* @param string $description Optional free-text description of the workflow rule
9899
* @return DataResponse<Http::STATUS_OK, WorkflowEngineRule, array{}>
99100
*
100101
* 200: Workflow created
@@ -109,10 +110,11 @@ public function create(
109110
string $operation,
110111
string $entity,
111112
array $events,
113+
string $description = '',
112114
): DataResponse {
113115
$context = $this->getScopeContext();
114116
try {
115-
$operation = $this->manager->addOperation($class, $name, $checks, $operation, $context, $entity, $events);
117+
$operation = $this->manager->addOperation($class, $name, $checks, $operation, $context, $entity, $events, $description);
116118
$operation = $this->manager->formatOperation($operation);
117119
return new DataResponse($operation);
118120
} catch (\UnexpectedValueException $e) {
@@ -134,6 +136,7 @@ public function create(
134136
* @param string $operation Operation action to execute on match
135137
* @param string $entity The matched entity
136138
* @param list<class-string<IEntityEvent>> $events The list of events on which the rule should be validated
139+
* @param string $description Optional free-text description of the workflow rule
137140
* @return DataResponse<Http::STATUS_OK, WorkflowEngineRule, array{}>
138141
*
139142
* 200: Workflow updated
@@ -149,10 +152,11 @@ public function update(
149152
string $operation,
150153
string $entity,
151154
array $events,
155+
string $description = '',
152156
): DataResponse {
153157
try {
154158
$context = $this->getScopeContext();
155-
$operation = $this->manager->updateOperation($id, $name, $checks, $operation, $context, $entity, $events);
159+
$operation = $this->manager->updateOperation($id, $name, $checks, $operation, $context, $entity, $events, $description);
156160
$operation = $this->manager->formatOperation($operation);
157161
return new DataResponse($operation);
158162
} catch (\UnexpectedValueException $e) {

apps/workflowengine/lib/Controller/GlobalWorkflowsController.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public function show(string $id): DataResponse {
6565
* @param string $operation Operation class to execute on match
6666
* @param string $entity The matched entity
6767
* @param list<class-string<IEntityEvent>> $events The list of events on which the rule should be validated
68+
* @param string $description Optional free-text description of the workflow rule
6869
* @return DataResponse<Http::STATUS_OK, WorkflowEngineRule, array{}>
6970
*
7071
* 200: Workflow created
@@ -74,8 +75,8 @@ public function show(string $id): DataResponse {
7475
#[\Override]
7576
#[PasswordConfirmationRequired]
7677
#[ApiRoute(verb: 'POST', url: '/api/v1/workflows/global')]
77-
public function create(string $class, string $name, array $checks, string $operation, string $entity, array $events): DataResponse {
78-
return parent::create($class, $name, $checks, $operation, $entity, $events);
78+
public function create(string $class, string $name, array $checks, string $operation, string $entity, array $events, string $description = ''): DataResponse {
79+
return parent::create($class, $name, $checks, $operation, $entity, $events, $description);
7980
}
8081

8182
/**
@@ -87,6 +88,7 @@ public function create(string $class, string $name, array $checks, string $opera
8788
* @param string $operation Operation action to execute on match
8889
* @param string $entity The matched entity
8990
* @param list<class-string<IEntityEvent>> $events The list of events on which the rule should be validated
91+
* @param string $description Optional free-text description of the workflow rule
9092
* @return DataResponse<Http::STATUS_OK, WorkflowEngineRule, array{}>
9193
*
9294
* 200: Workflow updated
@@ -97,8 +99,8 @@ public function create(string $class, string $name, array $checks, string $opera
9799
#[\Override]
98100
#[PasswordConfirmationRequired]
99101
#[ApiRoute(verb: 'PUT', url: '/api/v1/workflows/global/{id}')]
100-
public function update(int $id, string $name, array $checks, string $operation, string $entity, array $events): DataResponse {
101-
return parent::update($id, $name, $checks, $operation, $entity, $events);
102+
public function update(int $id, string $name, array $checks, string $operation, string $entity, array $events, string $description = ''): DataResponse {
103+
return parent::update($id, $name, $checks, $operation, $entity, $events, $description);
102104
}
103105

104106
/**

apps/workflowengine/lib/Controller/UserWorkflowsController.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public function show(string $id): DataResponse {
8383
* @param string $operation Operation class to execute on match
8484
* @param string $entity The matched entity
8585
* @param list<class-string<IEntityEvent>> $events The list of events on which the rule should be validated
86+
* @param string $description Optional free-text description of the workflow rule
8687
* @return DataResponse<Http::STATUS_OK, WorkflowEngineRule, array{}>
8788
*
8889
* 200: Workflow created
@@ -93,8 +94,8 @@ public function show(string $id): DataResponse {
9394
#[NoAdminRequired]
9495
#[PasswordConfirmationRequired]
9596
#[ApiRoute(verb: 'POST', url: '/api/v1/workflows/user')]
96-
public function create(string $class, string $name, array $checks, string $operation, string $entity, array $events): DataResponse {
97-
return parent::create($class, $name, $checks, $operation, $entity, $events);
97+
public function create(string $class, string $name, array $checks, string $operation, string $entity, array $events, string $description = ''): DataResponse {
98+
return parent::create($class, $name, $checks, $operation, $entity, $events, $description);
9899
}
99100

100101
/**
@@ -106,6 +107,7 @@ public function create(string $class, string $name, array $checks, string $opera
106107
* @param string $operation Operation action to execute on match
107108
* @param string $entity The matched entity
108109
* @param list<class-string<IEntityEvent>> $events The list of events on which the rule should be validated
110+
* @param string $description Optional free-text description of the workflow rule
109111
* @return DataResponse<Http::STATUS_OK, WorkflowEngineRule, array{}>
110112
*
111113
* 200: Workflow updated
@@ -117,8 +119,8 @@ public function create(string $class, string $name, array $checks, string $opera
117119
#[NoAdminRequired]
118120
#[PasswordConfirmationRequired]
119121
#[ApiRoute(verb: 'PUT', url: '/api/v1/workflows/user/{id}')]
120-
public function update(int $id, string $name, array $checks, string $operation, string $entity, array $events): DataResponse {
121-
return parent::update($id, $name, $checks, $operation, $entity, $events);
122+
public function update(int $id, string $name, array $checks, string $operation, string $entity, array $events, string $description = ''): DataResponse {
123+
return parent::update($id, $name, $checks, $operation, $entity, $events, $description);
122124
}
123125

124126
/**

apps/workflowengine/lib/Manager.php

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
* @psalm-import-type WorkflowEngineRule from ResponseDefinitions
5353
*/
5454
class Manager implements IManager {
55+
public const MAX_NAME_BYTES = 256;
56+
public const MAX_DESCRIPTION_BYTES = 4000;
57+
5558
/** @var array<string, array<string, array<int, WorkflowEngineCheck>>> */
5659
protected array $operations = [];
5760

@@ -320,12 +323,14 @@ protected function insertOperation(
320323
string $operation,
321324
string $entity,
322325
array $events,
326+
string $description = '',
323327
): int {
324328
$query = $this->connection->getQueryBuilder();
325329
$query->insert('flow_operations')
326330
->values([
327331
'class' => $query->createNamedParameter($class),
328332
'name' => $query->createNamedParameter($name),
333+
'description' => $query->createNamedParameter($description),
329334
'checks' => $query->createNamedParameter(json_encode(array_unique($checkIds))),
330335
'operation' => $query->createNamedParameter($operation),
331336
'entity' => $query->createNamedParameter($entity),
@@ -452,6 +457,7 @@ public function addRuntimeOperation(
452457
* @param string $name
453458
* @param list<WorkflowEngineCheck> $checks
454459
* @param string $operation
460+
* @param string $description optional free-text description shown in the UI
455461
* @return array The added operation
456462
* @throws \UnexpectedValueException
457463
* @throws Exception
@@ -464,8 +470,9 @@ public function addOperation(
464470
ScopeContext $scope,
465471
string $entity,
466472
array $events,
473+
string $description = '',
467474
) {
468-
$this->validateOperation($class, $name, $checks, $operation, $scope, $entity, $events);
475+
$this->validateOperation($class, $name, $checks, $operation, $scope, $entity, $events, $description);
469476

470477
$this->connection->beginTransaction();
471478

@@ -475,7 +482,7 @@ public function addOperation(
475482
$checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
476483
}
477484

478-
$id = $this->insertOperation($class, $name, $checkIds, $operation, $entity, $events);
485+
$id = $this->insertOperation($class, $name, $checkIds, $operation, $entity, $events, $description);
479486
$this->addScope($id, $scope);
480487

481488
$this->connection->commit();
@@ -520,6 +527,7 @@ protected function canModify(int $id, ScopeContext $scopeContext):bool {
520527
* @param string $name
521528
* @param list<WorkflowEngineCheck> $checks
522529
* @param string $operation
530+
* @param string $description optional free-text description shown in the UI
523531
* @return array The updated operation
524532
* @throws \UnexpectedValueException
525533
* @throws \DomainException
@@ -533,12 +541,13 @@ public function updateOperation(
533541
ScopeContext $scopeContext,
534542
string $entity,
535543
array $events,
544+
string $description = '',
536545
): array {
537546
if (!$this->canModify($id, $scopeContext)) {
538547
throw new \DomainException('Target operation not within scope');
539548
};
540549
$row = $this->getOperation($id);
541-
$this->validateOperation($row['class'], $name, $checks, $operation, $scopeContext, $entity, $events);
550+
$this->validateOperation($row['class'], $name, $checks, $operation, $scopeContext, $entity, $events, $description);
542551

543552
$checkIds = [];
544553
try {
@@ -550,6 +559,7 @@ public function updateOperation(
550559
$query = $this->connection->getQueryBuilder();
551560
$query->update('flow_operations')
552561
->set('name', $query->createNamedParameter($name))
562+
->set('description', $query->createNamedParameter($description))
553563
->set('checks', $query->createNamedParameter(json_encode(array_unique($checkIds))))
554564
->set('operation', $query->createNamedParameter($operation))
555565
->set('entity', $query->createNamedParameter($entity))
@@ -645,11 +655,19 @@ protected function validateEvents(string $entity, array $events, IOperation $ope
645655
* @param array $events
646656
* @throws \UnexpectedValueException
647657
*/
648-
public function validateOperation(string $class, string $name, array $checks, string $operation, ScopeContext $scope, string $entity, array $events): void {
658+
public function validateOperation(string $class, string $name, array $checks, string $operation, ScopeContext $scope, string $entity, array $events, string $description = ''): void {
649659
if (strlen($operation) > IManager::MAX_OPERATION_VALUE_BYTES) {
650660
throw new \UnexpectedValueException($this->l->t('The provided operation data is too long'));
651661
}
652662

663+
if (strlen($name) > self::MAX_NAME_BYTES) {
664+
throw new \UnexpectedValueException($this->l->t('The provided name is too long'));
665+
}
666+
667+
if (strlen($description) > self::MAX_DESCRIPTION_BYTES) {
668+
throw new \UnexpectedValueException($this->l->t('The provided description is too long'));
669+
}
670+
653671
/** @psalm-suppress TaintedCallable newInstance is not called */
654672
$reflection = new \ReflectionClass($class);
655673
if ($class !== IOperation::class && !in_array(IOperation::class, $reflection->getInterfaceNames())) {
@@ -810,7 +828,7 @@ protected function addScope(int $operationId, ScopeContext $scope): void {
810828
}
811829

812830
/**
813-
* @param array{class: class-string<\OCP\WorkflowEngine\IOperation>, entity: class-string<\OCP\WorkflowEngine\IEntity>, checks: string, events: string, id: int, name: string, operation: string} $operation
831+
* @param array{class: class-string<\OCP\WorkflowEngine\IOperation>, entity: class-string<\OCP\WorkflowEngine\IEntity>, checks: string, events: string, id: int, name: string, description: ?string, operation: string} $operation
814832
* @return WorkflowEngineRule
815833
*/
816834
public function formatOperation(array $operation): array {
@@ -823,6 +841,9 @@ public function formatOperation(array $operation): array {
823841
$events = json_decode($operation['events'], true) ?? [];
824842
$operation['events'] = $events;
825843

844+
$operation['name'] = (string)($operation['name'] ?? '');
845+
$operation['description'] = (string)($operation['description'] ?? '');
846+
826847
return $operation;
827848
}
828849

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\WorkflowEngine\Migration;
11+
12+
use Closure;
13+
use OCP\DB\ISchemaWrapper;
14+
use OCP\DB\Types;
15+
use OCP\Migration\IOutput;
16+
use OCP\Migration\SimpleMigrationStep;
17+
use Override;
18+
19+
/**
20+
* Adds an optional description column to flow_operations
21+
*/
22+
class Version3000Date20260531150003 extends SimpleMigrationStep {
23+
/**
24+
* @param IOutput $output
25+
* @param Closure(): ISchemaWrapper $schemaClosure
26+
* @param array $options
27+
* @return null|ISchemaWrapper
28+
*/
29+
#[Override]
30+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
31+
$schema = $schemaClosure();
32+
33+
if (!$schema->hasTable('flow_operations')) {
34+
return null;
35+
}
36+
37+
$table = $schema->getTable('flow_operations');
38+
if (!$table->hasColumn('description')) {
39+
$table->addColumn('description', Types::TEXT, [
40+
'notnull' => false,
41+
'default' => null,
42+
]);
43+
}
44+
45+
return $schema;
46+
}
47+
}

apps/workflowengine/lib/ResponseDefinitions.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
* id: int,
2828
* class: class-string<IOperation>,
2929
* name: string,
30+
* description: string,
3031
* checks: list<WorkflowEngineCheck>,
3132
* operation: string,
3233
* entity: class-string<IEntity>,

apps/workflowengine/openapi-administration.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"id",
9090
"class",
9191
"name",
92+
"description",
9293
"checks",
9394
"operation",
9495
"entity",
@@ -106,6 +107,9 @@
106107
"name": {
107108
"type": "string"
108109
},
110+
"description": {
111+
"type": "string"
112+
},
109113
"checks": {
110114
"type": "array",
111115
"items": {
@@ -315,6 +319,11 @@
315319
"type": "string",
316320
"minLength": 1
317321
}
322+
},
323+
"description": {
324+
"type": "string",
325+
"default": "",
326+
"description": "Optional free-text description of the workflow rule"
318327
}
319328
}
320329
}
@@ -640,6 +649,11 @@
640649
"type": "string",
641650
"minLength": 1
642651
}
652+
},
653+
"description": {
654+
"type": "string",
655+
"default": "",
656+
"description": "Optional free-text description of the workflow rule"
643657
}
644658
}
645659
}

0 commit comments

Comments
 (0)