Skip to content

Commit e7aa4c2

Browse files
akoclaude
andcommitted
feat: add MOVE FOLDER command for reorganizing folders
Syntax: MOVE FOLDER Module.FolderName TO FOLDER 'path' [IN Module] Uses the same qualified name pattern as document moves. Double quotes for nested paths: MOVE FOLDER Module."Parent/Child" TO TargetModule. Includes grammar, AST, visitor, executor, writer, docs, skills, help topics, and examples. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 11f8068 commit e7aa4c2

File tree

16 files changed

+6669
-6283
lines changed

16 files changed

+6669
-6283
lines changed

.claude/skills/mendix/organize-project.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,24 @@ MOVE ENTITY CRM.Customer TO CustomerModule;
202202
MOVE ENUMERATION CRM.OrderStatus TO SharedModule;
203203
```
204204

205+
## Moving Folders
206+
207+
Use `MOVE FOLDER` to reorganize folders. Syntax matches document moves: `Module.FolderName`.
208+
209+
```sql
210+
-- Move a folder into another folder
211+
MOVE FOLDER MyModule.Resources TO FOLDER 'Archive';
212+
213+
-- Move a nested folder (use double quotes for paths with /)
214+
MOVE FOLDER MyModule."Orders/Archive" TO MyModule;
215+
216+
-- Move a folder to a different module
217+
MOVE FOLDER MyModule.SharedWidgets TO CommonModule;
218+
219+
-- Move a folder into a folder in another module
220+
MOVE FOLDER MyModule.Templates TO FOLDER 'Shared' IN CommonModule;
221+
```
222+
205223
## Deleting Folders
206224

207225
Use `DROP FOLDER` to remove empty folders. The folder must not contain any documents or sub-folders.

cmd/mxcli/help_topics/move.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ EXAMPLES
5757
SHOW IMPACT OF OldModule.CustomerPage;
5858
MOVE PAGE OldModule.CustomerPage TO NewModule;
5959

60+
MOVE FOLDER
61+
-----------
62+
63+
Move a folder to another location:
64+
MOVE FOLDER Module.FolderName TO FOLDER 'TargetPath';
65+
MOVE FOLDER Module.FolderName TO TargetModule;
66+
MOVE FOLDER Module.FolderName TO FOLDER 'TargetPath' IN TargetModule;
67+
68+
For nested folder paths, use double quotes:
69+
MOVE FOLDER Module."Parent/Child" TO TargetModule;
70+
6071
DROP FOLDER
6172
-----------
6273

docs-site/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@
262262
- [CREATE FOLDER](reference/organization/create-folder.md)
263263
- [DROP FOLDER](reference/organization/drop-folder.md)
264264
- [MOVE](reference/organization/move.md)
265+
- [MOVE FOLDER](reference/organization/move-folder.md)
265266
- [Session Statements](reference/session/README.md)
266267
- [SET](reference/session/set.md)
267268
- [SHOW STATUS](reference/session/show-status.md)

docs-site/src/reference/organization/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ Statements for organizing project structure: creating modules and folders, and m
88
| [CREATE FOLDER](create-folder.md) | Create a folder within a module for organizing documents |
99
| [DROP FOLDER](drop-folder.md) | Drop an empty folder from a module |
1010
| [MOVE](move.md) | Move a document to a different module or folder |
11+
| [MOVE FOLDER](move-folder.md) | Move a folder to another location or module |

docs-site/src/reference/organization/drop-folder.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@ DROP FOLDER 'Processing' IN MyModule;
5353

5454
## See Also
5555

56-
[CREATE FOLDER](create-folder.md), [CREATE MODULE](create-module.md), [MOVE](move.md)
56+
[CREATE FOLDER](create-folder.md), [CREATE MODULE](create-module.md), [MOVE](move.md), [MOVE FOLDER](move-folder.md)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# MOVE FOLDER
2+
3+
## Synopsis
4+
5+
MOVE FOLDER qualified_name TO FOLDER 'folder_path';
6+
MOVE FOLDER qualified_name TO FOLDER 'folder_path' IN module_name;
7+
MOVE FOLDER qualified_name TO module_name;
8+
9+
## Description
10+
11+
Moves a folder (and all its contents) to a different location: another folder within the same module, a folder in a different module, or a module root. The folder's contents (documents and sub-folders) move with it.
12+
13+
The source folder is identified using the standard qualified name syntax: `Module.FolderName`. For nested folder paths containing `/`, use double quotes: `Module."Parent/Child"`.
14+
15+
Target folders are created automatically if they don't exist.
16+
17+
## Parameters
18+
19+
**qualified_name**
20+
: The source folder in `Module.FolderName` format. Use double quotes for paths with `/` or special characters.
21+
22+
**folder_path**
23+
: The target folder path. Use `/` for nested folders.
24+
25+
**module_name**
26+
: The target module name (for cross-module moves).
27+
28+
## Examples
29+
30+
### Move a folder into another folder
31+
32+
```sql
33+
MOVE FOLDER MyModule.Resources TO FOLDER 'Archive';
34+
```
35+
36+
### Move a nested folder to module root
37+
38+
```sql
39+
MOVE FOLDER MyModule."Orders/Archive" TO MyModule;
40+
```
41+
42+
### Move a folder to a different module
43+
44+
```sql
45+
MOVE FOLDER MyModule.SharedWidgets TO CommonModule;
46+
```
47+
48+
### Move a folder into a folder in another module
49+
50+
```sql
51+
MOVE FOLDER MyModule.Templates TO FOLDER 'Shared/Templates' IN CommonModule;
52+
```
53+
54+
## See Also
55+
56+
[CREATE FOLDER](create-folder.md), [DROP FOLDER](drop-folder.md), [MOVE](move.md)

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ AUTHENTICATION Basic, Session
179179
| Microflow folder | `FOLDER 'path'` (before BEGIN) | `CREATE MICROFLOW ... FOLDER 'ACT' BEGIN ... END;` |
180180
| Page folder | `Folder: 'path'` (in properties) | `CREATE PAGE ... (Folder: 'Pages/Detail') { ... }` |
181181
| Drop folder | `DROP FOLDER 'path' IN Module;` | Folder must be empty |
182+
| Move folder | `MOVE FOLDER Module.FolderName TO FOLDER 'path';` | Target folders auto-created |
182183
| Move to folder | `MOVE PAGE\|MICROFLOW\|SNIPPET\|NANOFLOW\|ENUMERATION Module.Name TO FOLDER 'path';` | Folders created automatically |
183184
| Move to module root | `MOVE PAGE Module.Name TO Module;` | Removes from folder |
184185
| Move across modules | `MOVE PAGE Old.Name TO NewModule;` | **Breaks by-name references** — use `SHOW IMPACT OF` first |

mdl-examples/doctype-tests/18-folder-examples.mdl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ MOVE MICROFLOW FolderTest.ACT_Process TO FOLDER 'Resources';
5353
MOVE MICROFLOW FolderTest.ACT_Archive TO FolderTest;
5454
/
5555

56+
-- MARK: - Move Folders
57+
58+
/**
59+
* Level 2.3: Move a folder into another folder
60+
*/
61+
MOVE FOLDER FolderTest.Resources TO FOLDER 'Processing';
62+
/
63+
64+
/**
65+
* Level 2.4: Move a nested folder to module root (use quotes for paths with /)
66+
*/
67+
MOVE FOLDER FolderTest."Processing/Resources" TO FolderTest;
68+
/
69+
5670
-- MARK: - Drop Folders
5771

5872
/**

mdl/ast/ast_enumeration.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ type DropFolderStmt struct {
3535

3636
func (s *DropFolderStmt) isStatement() {}
3737

38+
// MoveFolderStmt represents: MOVE FOLDER Module.FolderName TO ...
39+
type MoveFolderStmt struct {
40+
Name QualifiedName // Source: Module.FolderName (Name may be "Parent/Child" for nested)
41+
TargetFolder string // Target folder path (empty = module root)
42+
TargetModule string // Target module name (empty = same module)
43+
}
44+
45+
func (s *MoveFolderStmt) isStatement() {}
46+
3847
// CreateEnumerationStmt represents: CREATE ENUMERATION Module.Name (values) COMMENT '...'
3948
type CreateEnumerationStmt struct {
4049
Name QualifiedName

mdl/executor/cmd_folders.go

Lines changed: 94 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SPDX-License-Identifier: Apache-2.0
22

3-
// Package executor - DROP FOLDER command
3+
// Package executor - DROP/MOVE FOLDER commands
44
package executor
55

66
import (
@@ -9,30 +9,13 @@ import (
99

1010
"github.com/mendixlabs/mxcli/mdl/ast"
1111
"github.com/mendixlabs/mxcli/model"
12+
"github.com/mendixlabs/mxcli/sdk/mpr"
1213
)
1314

14-
// execDropFolder handles DROP FOLDER 'path' IN Module statements.
15-
// The folder must be empty (no child documents or sub-folders).
16-
func (e *Executor) execDropFolder(s *ast.DropFolderStmt) error {
17-
if e.writer == nil {
18-
return fmt.Errorf("not connected to a project")
19-
}
20-
21-
// Find the module
22-
module, err := e.findModule(s.Module)
23-
if err != nil {
24-
return fmt.Errorf("module not found: %s", s.Module)
25-
}
26-
27-
// List all folders
28-
folders, err := e.reader.ListFolders()
29-
if err != nil {
30-
return fmt.Errorf("failed to list folders: %w", err)
31-
}
32-
33-
// Walk the folder path to find the target folder
34-
parts := strings.Split(s.FolderPath, "/")
35-
currentContainerID := module.ID
15+
// findFolderByPath walks a folder path under a module and returns the folder ID.
16+
func (e *Executor) findFolderByPath(moduleID model.ID, folderPath string, folders []*mpr.FolderInfo) (model.ID, error) {
17+
parts := strings.Split(folderPath, "/")
18+
currentContainerID := moduleID
3619

3720
var targetFolderID model.ID
3821
for i, part := range parts {
@@ -53,20 +36,104 @@ func (e *Executor) execDropFolder(s *ast.DropFolderStmt) error {
5336
}
5437

5538
if !found {
56-
return fmt.Errorf("folder not found: '%s' in %s", s.FolderPath, s.Module)
39+
return "", fmt.Errorf("folder not found: '%s'", folderPath)
5740
}
5841
}
5942

6043
if targetFolderID == "" {
61-
return fmt.Errorf("folder not found: '%s' in %s", s.FolderPath, s.Module)
44+
return "", fmt.Errorf("folder not found: '%s'", folderPath)
45+
}
46+
47+
return targetFolderID, nil
48+
}
49+
50+
// execDropFolder handles DROP FOLDER 'path' IN Module statements.
51+
// The folder must be empty (no child documents or sub-folders).
52+
func (e *Executor) execDropFolder(s *ast.DropFolderStmt) error {
53+
if e.writer == nil {
54+
return fmt.Errorf("not connected to a project")
55+
}
56+
57+
module, err := e.findModule(s.Module)
58+
if err != nil {
59+
return fmt.Errorf("module not found: %s", s.Module)
60+
}
61+
62+
folders, err := e.reader.ListFolders()
63+
if err != nil {
64+
return fmt.Errorf("failed to list folders: %w", err)
65+
}
66+
67+
folderID, err := e.findFolderByPath(module.ID, s.FolderPath, folders)
68+
if err != nil {
69+
return fmt.Errorf("%w in %s", err, s.Module)
6270
}
6371

64-
// Delete the folder (writer checks if empty)
65-
if err := e.writer.DeleteFolder(targetFolderID); err != nil {
72+
if err := e.writer.DeleteFolder(folderID); err != nil {
6673
return fmt.Errorf("failed to delete folder '%s': %w", s.FolderPath, err)
6774
}
6875

6976
e.invalidateHierarchy()
7077
fmt.Fprintf(e.output, "Dropped folder: '%s' in %s\n", s.FolderPath, s.Module)
7178
return nil
7279
}
80+
81+
// execMoveFolder handles MOVE FOLDER Module.FolderName TO ... statements.
82+
func (e *Executor) execMoveFolder(s *ast.MoveFolderStmt) error {
83+
if e.writer == nil {
84+
return fmt.Errorf("not connected to a project")
85+
}
86+
87+
// Find the source module
88+
sourceModule, err := e.findModule(s.Name.Module)
89+
if err != nil {
90+
return fmt.Errorf("source module not found: %s", s.Name.Module)
91+
}
92+
93+
// Find the source folder
94+
folders, err := e.reader.ListFolders()
95+
if err != nil {
96+
return fmt.Errorf("failed to list folders: %w", err)
97+
}
98+
99+
folderID, err := e.findFolderByPath(sourceModule.ID, s.Name.Name, folders)
100+
if err != nil {
101+
return fmt.Errorf("%w in %s", err, s.Name.Module)
102+
}
103+
104+
// Determine target module
105+
var targetModule *model.Module
106+
if s.TargetModule != "" {
107+
targetModule, err = e.findModule(s.TargetModule)
108+
if err != nil {
109+
return fmt.Errorf("target module not found: %s", s.TargetModule)
110+
}
111+
} else {
112+
targetModule = sourceModule
113+
}
114+
115+
// Resolve target container
116+
var targetContainerID model.ID
117+
if s.TargetFolder != "" {
118+
targetContainerID, err = e.resolveFolder(targetModule.ID, s.TargetFolder)
119+
if err != nil {
120+
return fmt.Errorf("failed to resolve target folder: %w", err)
121+
}
122+
} else {
123+
targetContainerID = targetModule.ID
124+
}
125+
126+
// Move the folder
127+
if err := e.writer.MoveFolder(folderID, targetContainerID); err != nil {
128+
return fmt.Errorf("failed to move folder: %w", err)
129+
}
130+
131+
e.invalidateHierarchy()
132+
133+
target := targetModule.Name
134+
if s.TargetFolder != "" {
135+
target += "/" + s.TargetFolder
136+
}
137+
fmt.Fprintf(e.output, "Moved folder %s to %s\n", s.Name.String(), target)
138+
return nil
139+
}

0 commit comments

Comments
 (0)