@ripla/godd-mcp 1.0.2-canary.7 → 1.0.2-canary.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createdFileToContent, createdFileToTreeEntry } from "./created-file";
3
+
4
+ describe("created-file helpers", () => {
5
+ it("作成APIの戻り値からファイルツリー項目を作る", () => {
6
+ const entry = createdFileToTreeEntry({
7
+ path: "docs/project/new-note.md",
8
+ sha: "abc123",
9
+ });
10
+
11
+ expect(entry).toEqual({
12
+ name: "new-note.md",
13
+ path: "docs/project/new-note.md",
14
+ type: "file",
15
+ });
16
+ });
17
+
18
+ it(".md 作成直後に表示できる空のファイル内容を作る", () => {
19
+ const content = createdFileToContent({
20
+ path: "docs/project/new-note.md",
21
+ sha: "abc123",
22
+ });
23
+
24
+ expect(content).toEqual({
25
+ path: "docs/project/new-note.md",
26
+ sha: "abc123",
27
+ content: "",
28
+ });
29
+ });
30
+ });
@@ -0,0 +1,22 @@
1
+ import type { FileContent, TreeEntry } from "@/types";
2
+
3
+ export type CreatedFile = {
4
+ path: string;
5
+ sha: string;
6
+ };
7
+
8
+ export function createdFileToTreeEntry(file: CreatedFile): TreeEntry {
9
+ return {
10
+ name: file.path.split("/").pop() ?? file.path,
11
+ path: file.path,
12
+ type: "file",
13
+ };
14
+ }
15
+
16
+ export function createdFileToContent(file: CreatedFile): FileContent {
17
+ return {
18
+ path: file.path,
19
+ sha: file.sha,
20
+ content: "",
21
+ };
22
+ }
@@ -35,3 +35,42 @@ export function removeEntry(entries: TreeEntry[], targetPath: string): TreeEntry
35
35
  };
36
36
  });
37
37
  }
38
+
39
+ export function moveEntry(
40
+ entries: TreeEntry[],
41
+ oldPath: string,
42
+ newParentDir: string,
43
+ ): TreeEntry[] {
44
+ if (newParentDir === oldPath || newParentDir.startsWith(oldPath + "/")) {
45
+ return entries;
46
+ }
47
+
48
+ const entry = findEntry(entries, oldPath);
49
+ const targetParent = findEntry(entries, newParentDir);
50
+ if (!entry || targetParent?.type !== "dir") return entries;
51
+
52
+ const newPath = `${newParentDir}/${entry.name}`;
53
+ const moved = rebaseEntryPath(entry, oldPath, newPath);
54
+ return insertChildEntry(removeEntry(entries, oldPath), newParentDir, moved);
55
+ }
56
+
57
+ function findEntry(entries: TreeEntry[], targetPath: string): TreeEntry | null {
58
+ for (const entry of entries) {
59
+ if (entry.path === targetPath) return entry;
60
+ if (entry.children) {
61
+ const found = findEntry(entry.children, targetPath);
62
+ if (found) return found;
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+
68
+ function rebaseEntryPath(entry: TreeEntry, oldPath: string, newPath: string): TreeEntry {
69
+ return {
70
+ ...entry,
71
+ path: entry.path === oldPath
72
+ ? newPath
73
+ : entry.path.replace(oldPath + "/", newPath + "/"),
74
+ children: entry.children?.map((child) => rebaseEntryPath(child, oldPath, newPath)),
75
+ };
76
+ }
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import type { TreeEntry } from "@/types";
3
- import { insertChildEntry, removeEntry } from "@/lib/tree-utils";
3
+ import { insertChildEntry, moveEntry, removeEntry } from "@/lib/tree-utils";
4
4
 
5
5
  const baseTree: TreeEntry[] = [
6
6
  {
@@ -110,3 +110,32 @@ describe("removeEntry", () => {
110
110
  expect(result).toEqual(baseTree);
111
111
  });
112
112
  });
113
+
114
+ describe("moveEntry", () => {
115
+ it("moves a file to another folder and updates its path", () => {
116
+ const result = moveEntry(baseTree, "docs/index.md", "docs/project");
117
+ const docsChildren = result[0].children ?? [];
118
+ const project = docsChildren.find((entry) => entry.path === "docs/project");
119
+
120
+ expect(docsChildren.some((entry) => entry.path === "docs/index.md")).toBe(false);
121
+ expect(project?.children?.some((entry) => entry.path === "docs/project/index.md")).toBe(true);
122
+ });
123
+
124
+ it("moves a folder and rebases child paths", () => {
125
+ const tree: TreeEntry[] = [
126
+ ...baseTree,
127
+ { name: "archive", path: "archive", type: "dir", children: [] },
128
+ ];
129
+ const result = moveEntry(tree, "docs/project", "archive");
130
+ const archive = result.find((entry) => entry.path === "archive");
131
+ const moved = archive?.children?.find((entry) => entry.path === "archive/project");
132
+
133
+ expect(JSON.stringify(result)).not.toContain("docs/project/readme.md");
134
+ expect(moved?.children?.[0].path).toBe("archive/project/readme.md");
135
+ });
136
+
137
+ it("does not move into itself", () => {
138
+ const result = moveEntry(baseTree, "docs/project", "docs/project");
139
+ expect(result).toEqual(baseTree);
140
+ });
141
+ });
@@ -34,7 +34,8 @@ import BranchSelectorModal from "@/components/BranchSelectorModal";
34
34
  import IssueList from "@/components/IssueList";
35
35
  import type { TreeEntry, FileContent, ApiState, FileStyles } from "@/types";
36
36
  import { getFileExt } from "@/types";
37
- import { insertChildEntry, removeEntry } from "@/lib/tree-utils";
37
+ import { insertChildEntry, moveEntry, removeEntry } from "@/lib/tree-utils";
38
+ import { createdFileToContent, createdFileToTreeEntry } from "@/lib/created-file";
38
39
 
39
40
  const MarkdownEditor = lazy(() => import("@/components/MarkdownEditor"));
40
41
  const SpreadsheetEditor = lazy(() => import("@/components/SpreadsheetEditor"));
@@ -394,19 +395,36 @@ export default function MainPage() {
394
395
  const handleCreateFile = useCallback(
395
396
  async (fileName: string) => {
396
397
  if (!dialog || dialog.type !== "create") return;
398
+ const unsaved = autoSave.status === "unsaved" || autoSave.status === "saving";
399
+ if (unsaved && !window.confirm("未保存の変更があります。ファイルを切り替えてもよろしいですか?")) {
400
+ return;
401
+ }
397
402
  const parentPath = dialog.entry.path;
398
403
  const fullPath = `${parentPath}/${fileName}`;
399
404
  setDialog(null);
400
405
  try {
401
- await createFileApi(fullPath);
406
+ const created = await createFileApi(fullPath);
402
407
  showToast(`${fileName} を作成しました`, "success");
403
- loadTree();
404
- handleFileSelect(fullPath);
408
+ fetchAbortRef.current?.abort();
409
+ autoSave.reset();
410
+ pendingStylesRef.current = null;
411
+ setSelectedIssue(null);
412
+ setSelectedFile(created.path);
413
+ setActiveDir(parentPath);
414
+ setSearchParams({ file: created.path }, { replace: true });
415
+ setFileLoading(false);
416
+ setEditing(false);
417
+ setShowOriginal(false);
418
+ setEditedContent(null);
419
+ setFileContent(createdFileToContent(created));
420
+ setTree((prev) => insertChildEntry(prev, parentPath, createdFileToTreeEntry(created)));
421
+ if (treeSyncTimerRef.current) clearTimeout(treeSyncTimerRef.current);
422
+ treeSyncTimerRef.current = setTimeout(() => loadTree(), 1500);
405
423
  } catch (e) {
406
424
  showToast(`作成に失敗しました: ${getErrorMessage(e)}`, "error");
407
425
  }
408
426
  },
409
- [dialog, showToast, loadTree, handleFileSelect],
427
+ [dialog, showToast, autoSave, setSearchParams, loadTree],
410
428
  );
411
429
 
412
430
  const handleCreateFolder = useCallback(
@@ -529,7 +547,9 @@ export default function MainPage() {
529
547
  handleFileSelect(newPath);
530
548
  }
531
549
  }
532
- loadTree();
550
+ setTree((prev) => moveEntry(prev, oldPath, newParentDir));
551
+ if (treeSyncTimerRef.current) clearTimeout(treeSyncTimerRef.current);
552
+ treeSyncTimerRef.current = setTimeout(() => loadTree(), 1500);
533
553
  } catch (e) {
534
554
  showToast(`移動に失敗しました: ${getErrorMessage(e)}`, "error");
535
555
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ripla/godd-mcp",
3
- "version": "1.0.2-canary.7",
3
+ "version": "1.0.2-canary.9",
4
4
  "type": "module",
5
5
  "description": "GoDD (Governance-orchestrated Driven Development) MCP Server - Encrypted prompt distribution via Model Context Protocol",
6
6
  "main": "dist/index.js",