Skip to content

Commit e7f6987

Browse files
committed
Adding folder:create: Create a folder (with optional parent folder by slug or ID)
1 parent adb441c commit e7f6987

8 files changed

Lines changed: 375 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
All notable changes to `blokctl` will be documented in this file.
44

5-
## 0.2.2 - WIP
5+
## 0.3.0 - 2026-03-17
6+
- Adding `folder:create`: Create a folder (with optional parent folder by slug or ID)
67
- Adding `workflow:stage-show`: Show details of a workflow stage by name or ID
78
- Adding `workflows:list`: List workflows and their stages (lookup stage IDs by name)
89
- Adding `story:workflow-change`: Change the workflow stage of a story

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,35 @@ php bin/blokctl space:preview-add -S 290817118944379 'Staging' 'https://staging.
188188
| Argument | `name` | **(required)** Environment name (e.g. `Staging`, `Local Development`) |
189189
| Argument | `url` | **(required)** Environment URL |
190190

191+
### Folders
192+
193+
#### `folder:create` — Create a folder
194+
195+
```bash
196+
# Create a folder at root
197+
php bin/blokctl folder:create -S 290817118944379 'Archive'
198+
199+
# Create a folder inside a parent folder (by slug)
200+
php bin/blokctl folder:create -S 290817118944379 'Old Posts' --parent-slug=articles
201+
202+
# Create a folder inside a parent folder (by ID)
203+
php bin/blokctl folder:create -S 290817118944379 'Old Posts' --parent-id=123456
204+
205+
# Interactive: prompts for folder name
206+
php bin/blokctl folder:create -S 290817118944379
207+
```
208+
209+
| Type | Name | Description |
210+
|---|---|---|
211+
| Argument | `name` | Folder name (prompted interactively if omitted) |
212+
213+
**Parent folder options** (optional, mutually exclusive — defaults to root):
214+
215+
| Option | Description |
216+
|---|---|
217+
| `--parent-slug` | Parent folder slug (e.g. `articles`, `articles/archive`) |
218+
| `--parent-id` | Parent folder numeric ID (default: `0` for root) |
219+
191220
### Stories
192221

193222
#### `stories:list` — List stories with filters
@@ -668,6 +697,29 @@ $result = $action->preflight($spaceId);
668697
$action->execute($spaceId, $result, 'Staging', 'https://staging.example.com/?path=');
669698
```
670699

700+
### Folders
701+
702+
#### Create a folder
703+
704+
```php
705+
use Blokctl\Action\Folder\FolderCreateAction;
706+
707+
$action = new FolderCreateAction($client);
708+
709+
// Create at root
710+
$result = $action->execute($spaceId, 'Archive');
711+
712+
// Create inside a parent folder (by ID)
713+
$result = $action->execute($spaceId, 'Old Posts', parentId: 123456);
714+
715+
// Resolve parent folder by slug, then create
716+
$parentId = $action->resolveParentBySlug($spaceId, 'articles');
717+
$result = $action->execute($spaceId, 'Old Posts', parentId: $parentId);
718+
719+
$result->folder; // Story object (the created folder)
720+
$result->parentId; // int (0 for root)
721+
```
722+
671723
### Stories
672724

673725
#### List stories with filters

bin/blokctl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use Blokctl\Command\SpacePreviewListCommand;
1010
use Blokctl\Command\SpacePreviewSetCommand;
1111
use Blokctl\Command\SpacePreviewAddCommand;
1212
use Blokctl\Command\SpaceDeleteCommand;
13+
use Blokctl\Command\FolderCreateCommand;
1314
use Blokctl\Command\SpaceDemoRemoveCommand;
1415
use Blokctl\Command\SpacesListCommand;
1516
use Blokctl\Command\StoriesWorkflowAssignCommand;
@@ -34,6 +35,7 @@ $application = new Application('blokctl', '1.0.0');
3435

3536
$application->add(new AppProvisionInstallCommand());
3637
$application->add(new AppProvisionListCommand());
38+
$application->add(new FolderCreateCommand());
3739
$application->add(new SpaceInfoCommand());
3840
$application->add(new SpacePreviewListCommand());
3941
$application->add(new SpacePreviewSetCommand());
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Blokctl\Action\Folder;
6+
7+
use Storyblok\ManagementApi\Data\Story;
8+
use Storyblok\ManagementApi\Data\StoryComponent;
9+
use Storyblok\ManagementApi\Endpoints\StoryApi;
10+
use Storyblok\ManagementApi\ManagementApiClient;
11+
use Storyblok\ManagementApi\QueryParameters\StoriesParams;
12+
13+
final readonly class FolderCreateAction
14+
{
15+
public function __construct(
16+
private ManagementApiClient $client,
17+
) {}
18+
19+
/**
20+
* Resolve a parent folder ID from its slug.
21+
*
22+
* @throws \RuntimeException if the folder is not found
23+
*/
24+
public function resolveParentBySlug(
25+
string $spaceId,
26+
string $folderSlug,
27+
): int {
28+
$params = new StoriesParams(folderOnly: true, withSlug: $folderSlug);
29+
$folders = (new StoryApi($this->client, $spaceId))->page($params)->data();
30+
31+
if (count($folders) !== 1) {
32+
throw new \RuntimeException(
33+
'Parent folder not found with slug: ' . $folderSlug,
34+
);
35+
}
36+
37+
/** @var array{id: int} $folder */
38+
$folder = $folders[0];
39+
40+
return (int) $folder['id'];
41+
}
42+
43+
/**
44+
* Create a folder.
45+
*
46+
* @throws \RuntimeException if the creation fails
47+
*/
48+
public function execute(
49+
string $spaceId,
50+
string $name,
51+
int $parentId = 0,
52+
): FolderCreateResult {
53+
$storyApi = new StoryApi($this->client, $spaceId);
54+
55+
$folder = new Story($name, $this->slugify($name), new StoryComponent('folder'));
56+
$folder->set('is_folder', true);
57+
58+
if ($parentId > 0) {
59+
$folder->setFolderId($parentId);
60+
}
61+
62+
$response = $storyApi->create($folder);
63+
64+
if (!$response->isOk()) {
65+
throw new \RuntimeException(
66+
'Failed to create folder: ' . $response->getErrorMessage(),
67+
);
68+
}
69+
70+
return new FolderCreateResult(
71+
folder: $response->data(),
72+
parentId: $parentId,
73+
);
74+
}
75+
76+
private function slugify(string $name): string
77+
{
78+
$slug = mb_strtolower($name);
79+
$slug = (string) preg_replace('/[^a-z0-9\s-]/', '', $slug);
80+
$slug = (string) preg_replace('/[\s-]+/', '-', $slug);
81+
82+
return trim($slug, '-');
83+
}
84+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Blokctl\Action\Folder;
6+
7+
use Storyblok\ManagementApi\Data\Story;
8+
9+
final readonly class FolderCreateResult
10+
{
11+
public function __construct(
12+
public Story $folder,
13+
public int $parentId,
14+
) {}
15+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Blokctl\Command;
6+
7+
use Blokctl\Action\Folder\FolderCreateAction;
8+
use Blokctl\Render;
9+
use Symfony\Component\Console\Attribute\AsCommand;
10+
use Symfony\Component\Console\Input\InputArgument;
11+
use Symfony\Component\Console\Input\InputInterface;
12+
use Symfony\Component\Console\Input\InputOption;
13+
use Symfony\Component\Console\Output\OutputInterface;
14+
15+
use function Laravel\Prompts\text;
16+
17+
#[AsCommand(
18+
name: 'folder:create',
19+
description: 'Create a folder',
20+
)]
21+
class FolderCreateCommand extends AbstractCommand
22+
{
23+
#[\Override]
24+
protected function configure(): void
25+
{
26+
parent::configure();
27+
$this
28+
->addArgument('name', InputArgument::OPTIONAL, 'Folder name')
29+
->addOption('parent-slug', null, InputOption::VALUE_REQUIRED, 'Parent folder slug (e.g. articles/archive)')
30+
->addOption('parent-id', null, InputOption::VALUE_REQUIRED, 'Parent folder numeric ID (default: 0 for root)');
31+
}
32+
33+
protected function execute(
34+
InputInterface $input,
35+
OutputInterface $output,
36+
): int {
37+
/** @var string|null $name */
38+
$name = $input->getArgument('name');
39+
/** @var string|null $parentSlug */
40+
$parentSlug = $input->getOption('parent-slug');
41+
/** @var string|null $parentIdRaw */
42+
$parentIdRaw = $input->getOption('parent-id');
43+
44+
if ($parentSlug && $parentIdRaw) {
45+
$output->writeln('<error>Provide only one of --parent-slug or --parent-id</error>');
46+
return self::FAILURE;
47+
}
48+
49+
if (empty($name) && !$input->getOption('no-interaction')) {
50+
$name = text(
51+
label: 'Enter the folder name',
52+
placeholder: 'E.g. Articles, Archive',
53+
required: true,
54+
);
55+
}
56+
57+
if (empty($name)) {
58+
$output->writeln('<error>Folder name is required</error>');
59+
return self::FAILURE;
60+
}
61+
62+
$action = new FolderCreateAction($this->client);
63+
64+
try {
65+
// Resolve parent folder
66+
$parentId = 0;
67+
if ($parentSlug !== null) {
68+
$parentId = $action->resolveParentBySlug($this->spaceId, $parentSlug);
69+
} elseif ($parentIdRaw !== null) {
70+
$parentId = (int) $parentIdRaw;
71+
}
72+
73+
$result = $action->execute($this->spaceId, $name, $parentId);
74+
} catch (\RuntimeException $runtimeException) {
75+
Render::error($runtimeException->getMessage());
76+
return self::FAILURE;
77+
}
78+
79+
Render::title('Folder Created');
80+
Render::labelValue('Name', $result->folder->name());
81+
Render::labelValue('Slug', $result->folder->slug());
82+
Render::labelValue('ID', $result->folder->id());
83+
if ($result->parentId > 0) {
84+
Render::labelValue('Parent folder ID', (string) $result->parentId);
85+
} else {
86+
Render::labelValue('Parent', 'Root');
87+
}
88+
89+
return self::SUCCESS;
90+
}
91+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"story": {
3+
"name": "Archive",
4+
"parent_id": 0,
5+
"group_id": null,
6+
"alternates": [],
7+
"created_at": "2026-03-17T10:00:00.000Z",
8+
"deleted_at": null,
9+
"sort_by_date": null,
10+
"tag_list": [],
11+
"updated_at": "2026-03-17T10:00:00.000Z",
12+
"published_at": null,
13+
"id": 550001,
14+
"uuid": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
15+
"is_folder": true,
16+
"content": {
17+
"_uid": "a1b2c3d4-e5f6-7890-abcd-000000000001",
18+
"component": "folder"
19+
},
20+
"published": false,
21+
"slug": "archive",
22+
"path": null,
23+
"full_slug": "archive",
24+
"default_root": null,
25+
"disable_fe_editor": false,
26+
"parent": null,
27+
"is_startpage": false,
28+
"unpublished_changes": false,
29+
"meta_data": null,
30+
"imported_at": null,
31+
"pinned": false,
32+
"breadcrumbs": [],
33+
"position": 0,
34+
"stage": null,
35+
"release_id": null,
36+
"lang": "default"
37+
}
38+
}

0 commit comments

Comments
 (0)