Skip to content

Commit 40106c6

Browse files
committed
Adding workflows:list: List workflows and their stages (lookup stage IDs by name)
1 parent a604ccb commit 40106c6

7 files changed

Lines changed: 227 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
All notable changes to `blokctl` will be documented in this file.
44

55
## 0.2.2 - WIP
6+
- Adding `workflows:list`: List workflows and their stages (lookup stage IDs by name)
67
- Adding `story:workflow-change`: Change the workflow stage of a story
7-
-
8+
89
## 0.2.1 - 2026-03-14
910
- Adding`story:move` — Move a story to a different folder
1011

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,16 @@ php bin/blokctl stories:workflow-assign -S 290817118944379 --workflow-stage-id=1
363363

364364
Finds all stories without a workflow stage and assigns the selected stage to them.
365365

366+
### Workflows
367+
368+
#### `workflows:list` — List workflows and their stages
369+
370+
```bash
371+
php bin/blokctl workflows:list -S 290817118944379
372+
```
373+
374+
Lists all workflows configured in the space, along with their stages and IDs. Useful for looking up stage IDs by name (e.g. before using `story:workflow-change --stage-id=...`).
375+
366376
### Components
367377

368378
#### `components:list` — List components with filters
@@ -755,6 +765,25 @@ if ($result->countWithoutStage > 0) {
755765
}
756766
```
757767

768+
### Workflows
769+
770+
#### List workflows and stages
771+
772+
```php
773+
use Blokctl\Action\Workflow\WorkflowsListAction;
774+
775+
$result = (new WorkflowsListAction($client))->execute($spaceId);
776+
777+
$result->workflows; // array of ['id' => int, 'name' => string, 'isDefault' => bool, 'stages' => [...]]
778+
$result->count(); // int
779+
780+
foreach ($result->workflows as $workflow) {
781+
foreach ($workflow['stages'] as $stage) {
782+
// $stage['id'], $stage['name'], $stage['position']
783+
}
784+
}
785+
```
786+
758787
### Components
759788

760789
#### List components

bin/blokctl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use Blokctl\Command\StoryMoveCommand;
2323
use Blokctl\Command\StoryShowCommand;
2424
use Blokctl\Command\StoryWorkflowChangeCommand;
2525
use Blokctl\Command\UserMeCommand;
26+
use Blokctl\Command\WorkflowsListCommand;
2627
use Symfony\Component\Console\Application;
2728

2829
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
@@ -50,5 +51,6 @@ $application->add(new StoryMoveCommand());
5051
$application->add(new StoryShowCommand());
5152
$application->add(new StoryWorkflowChangeCommand());
5253
$application->add(new UserMeCommand());
54+
$application->add(new WorkflowsListCommand());
5355

5456
$application->run();
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Blokctl\Action\Workflow;
6+
7+
use Storyblok\ManagementApi\Endpoints\WorkflowApi;
8+
use Storyblok\ManagementApi\Endpoints\WorkflowStageApi;
9+
use Storyblok\ManagementApi\ManagementApiClient;
10+
11+
final readonly class WorkflowsListAction
12+
{
13+
public function __construct(
14+
private ManagementApiClient $client,
15+
) {}
16+
17+
public function execute(string $spaceId): WorkflowsListResult
18+
{
19+
$workflowApi = new WorkflowApi($this->client, $spaceId);
20+
$workflows = $workflowApi->list()->data();
21+
22+
$stageApi = new WorkflowStageApi($this->client, $spaceId);
23+
24+
/** @var array<int, array{id: int, name: string, isDefault: bool, stages: array<int, array{id: int, name: string, position: int}>}> $items */
25+
$items = [];
26+
27+
/** @phpstan-ignore foreach.nonIterable */
28+
foreach ($workflows as $workflow) {
29+
/** @var int $workflowId */
30+
$workflowId = $workflow->get('id');
31+
/** @var string $workflowName */
32+
$workflowName = $workflow->get('name');
33+
$isDefault = $workflow->getBoolean('is_default');
34+
35+
$stages = $stageApi->list((string) $workflowId)->data();
36+
37+
/** @var array<int, array{id: int, name: string, position: int}> $stageItems */
38+
$stageItems = [];
39+
40+
/** @phpstan-ignore foreach.nonIterable */
41+
foreach ($stages as $stage) {
42+
/** @var int $stageId */
43+
$stageId = $stage->get('id');
44+
/** @var string $stageName */
45+
$stageName = $stage->get('name');
46+
/** @var int $position */
47+
$position = $stage->get('position');
48+
$stageItems[] = [
49+
'id' => $stageId,
50+
'name' => $stageName,
51+
'position' => $position,
52+
];
53+
}
54+
55+
$items[] = [
56+
'id' => $workflowId,
57+
'name' => $workflowName,
58+
'isDefault' => $isDefault,
59+
'stages' => $stageItems,
60+
];
61+
}
62+
63+
return new WorkflowsListResult(workflows: $items);
64+
}
65+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Blokctl\Action\Workflow;
6+
7+
final readonly class WorkflowsListResult
8+
{
9+
/**
10+
* @param array<int, array{id: int, name: string, isDefault: bool, stages: array<int, array{id: int, name: string, position: int}>}> $workflows
11+
*/
12+
public function __construct(
13+
public array $workflows,
14+
) {}
15+
16+
public function count(): int
17+
{
18+
return count($this->workflows);
19+
}
20+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Blokctl\Command;
6+
7+
use Blokctl\Action\Workflow\WorkflowsListAction;
8+
use Blokctl\Render;
9+
use Symfony\Component\Console\Attribute\AsCommand;
10+
use Symfony\Component\Console\Input\InputInterface;
11+
use Symfony\Component\Console\Output\OutputInterface;
12+
13+
#[AsCommand(
14+
name: 'workflows:list',
15+
description: 'List workflows and their stages',
16+
)]
17+
class WorkflowsListCommand extends AbstractCommand
18+
{
19+
protected function execute(
20+
InputInterface $input,
21+
OutputInterface $output,
22+
): int {
23+
$result = (new WorkflowsListAction($this->client))->execute($this->spaceId);
24+
25+
if ($result->count() === 0) {
26+
Render::log('No workflows found');
27+
return self::SUCCESS;
28+
}
29+
30+
foreach ($result->workflows as $workflow) {
31+
$label = $workflow['name'];
32+
if ($workflow['isDefault']) {
33+
$label .= ' (default)';
34+
}
35+
36+
Render::titleSection($label . ' — ID: ' . $workflow['id']);
37+
38+
foreach ($workflow['stages'] as $stage) {
39+
Render::labelValue(
40+
$stage['name'],
41+
'ID: ' . $stage['id'],
42+
);
43+
}
44+
}
45+
46+
return self::SUCCESS;
47+
}
48+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Unit\Action\Workflow;
6+
7+
use Blokctl\Action\Workflow\WorkflowsListAction;
8+
use PHPUnit\Framework\Attributes\Test;
9+
use Tests\TestCase;
10+
11+
final class WorkflowsListActionTest extends TestCase
12+
{
13+
#[Test]
14+
public function execute_returns_workflows_with_stages(): void
15+
{
16+
$client = $this->createMockClient(
17+
$this->mockResponse('list-workflows'), // WorkflowApi->list
18+
$this->mockResponse('list-workflow-stages'), // WorkflowStageApi->list (1st workflow)
19+
$this->mockResponse('list-workflow-stages'), // WorkflowStageApi->list (2nd workflow)
20+
);
21+
22+
$action = new WorkflowsListAction($client);
23+
$result = $action->execute('680');
24+
25+
$this->assertSame(2, $result->count());
26+
27+
// First workflow
28+
$this->assertSame(12345, $result->workflows[0]['id']);
29+
$this->assertSame('Article ', $result->workflows[0]['name']);
30+
$this->assertFalse($result->workflows[0]['isDefault']);
31+
$this->assertCount(2, $result->workflows[0]['stages']);
32+
$this->assertSame(653554, $result->workflows[0]['stages'][0]['id']);
33+
$this->assertSame('Drafting', $result->workflows[0]['stages'][0]['name']);
34+
$this->assertSame(653555, $result->workflows[0]['stages'][1]['id']);
35+
$this->assertSame('Review', $result->workflows[0]['stages'][1]['name']);
36+
37+
// Second workflow (default)
38+
$this->assertSame(12346, $result->workflows[1]['id']);
39+
$this->assertSame('Default one', $result->workflows[1]['name']);
40+
$this->assertTrue($result->workflows[1]['isDefault']);
41+
}
42+
43+
#[Test]
44+
public function execute_returns_empty_when_no_workflows(): void
45+
{
46+
$emptyJson = json_encode(['workflows' => []], JSON_THROW_ON_ERROR);
47+
48+
$client = $this->createMockClient(
49+
new \Symfony\Component\HttpClient\Response\MockResponse(
50+
$emptyJson,
51+
['http_code' => 200],
52+
),
53+
);
54+
55+
$action = new WorkflowsListAction($client);
56+
$result = $action->execute('680');
57+
58+
$this->assertSame(0, $result->count());
59+
$this->assertSame([], $result->workflows);
60+
}
61+
}

0 commit comments

Comments
 (0)