@ripla/godd-mcp 1.0.3 → 1.0.4-canary.2
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.
package/notes-app/README.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
React フロントエンド。docs/ のドキュメント閲覧・編集(Tiptap / Univer / drawio 統合予定)。
|
|
4
4
|
|
|
5
|
+
## エディタ操作
|
|
6
|
+
|
|
7
|
+
### スプレッドシートエディタ(CSV/テーブル)
|
|
8
|
+
|
|
9
|
+
セル内で改行を入力するには、セル編集モード中に以下のショートカットを使用します:
|
|
10
|
+
|
|
11
|
+
| ショートカット | 動作 |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `Enter` | 選択セルを編集モードに開始 |
|
|
14
|
+
| `Alt + Enter` | セル内に改行を挿入 |
|
|
15
|
+
| `Ctrl + Enter` (Windows/Linux) | セル内に改行を挿入 |
|
|
16
|
+
| `Cmd + Enter` (macOS) | セル内に改行を挿入 |
|
|
17
|
+
| `Shift + Enter` | 確定して上のセルへ移動(Univer 標準) |
|
|
18
|
+
|
|
5
19
|
## 前提条件
|
|
6
20
|
|
|
7
21
|
- Node.js 20+
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { render, fireEvent } from "@testing-library/react";
|
|
3
|
+
import SpreadsheetEditor from "./SpreadsheetEditor";
|
|
4
|
+
|
|
5
|
+
const mockSyncExecuteCommand = vi.fn();
|
|
6
|
+
const mockEventCallbacks: Record<string, Array<(p?: unknown) => void>> = {};
|
|
7
|
+
|
|
8
|
+
const mockUniverAPI = {
|
|
9
|
+
createWorkbook: vi.fn(),
|
|
10
|
+
onCommandExecuted: vi.fn(() => ({ dispose: vi.fn() })),
|
|
11
|
+
addEvent: vi.fn((event: string, callback: (p?: unknown) => void) => {
|
|
12
|
+
mockEventCallbacks[event] ??= [];
|
|
13
|
+
mockEventCallbacks[event].push(callback);
|
|
14
|
+
return { dispose: vi.fn() };
|
|
15
|
+
}),
|
|
16
|
+
syncExecuteCommand: mockSyncExecuteCommand,
|
|
17
|
+
Event: {
|
|
18
|
+
BeforeSheetEditStart: "BeforeSheetEditStart",
|
|
19
|
+
SheetEditEnded: "SheetEditEnded",
|
|
20
|
+
BeforeCommandExecute: "BeforeCommandExecute",
|
|
21
|
+
CommandExecuted: "CommandExecuted",
|
|
22
|
+
},
|
|
23
|
+
getActiveWorkbook: vi.fn(() => ({
|
|
24
|
+
startEditing: vi.fn(),
|
|
25
|
+
getActiveSheet: vi.fn(() => ({
|
|
26
|
+
getSelection: vi.fn(() => ({
|
|
27
|
+
getActiveRange: vi.fn(() => ({
|
|
28
|
+
getRow: () => 0,
|
|
29
|
+
getColumn: () => 0,
|
|
30
|
+
getValue: () => "hello",
|
|
31
|
+
})),
|
|
32
|
+
})),
|
|
33
|
+
})),
|
|
34
|
+
})),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const mockUniver = { dispose: vi.fn() };
|
|
38
|
+
|
|
39
|
+
vi.mock("@univerjs/presets", () => ({
|
|
40
|
+
createUniver: vi.fn(() => ({
|
|
41
|
+
univer: mockUniver,
|
|
42
|
+
univerAPI: mockUniverAPI,
|
|
43
|
+
})),
|
|
44
|
+
LocaleType: { JA_JP: "ja-JP" },
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
vi.mock("@univerjs/preset-sheets-core", () => ({
|
|
48
|
+
UniverSheetsCorePreset: vi.fn(() => ({})),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
vi.mock("@univerjs/preset-sheets-core/locales/ja-JP", () => ({
|
|
52
|
+
default: {},
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
vi.mock("@/lib/csv-utils", () => ({
|
|
56
|
+
csvToWorkbookData: vi.fn(() => ({})),
|
|
57
|
+
workbookDataToCsv: vi.fn(() => ""),
|
|
58
|
+
extractWorkbookStyles: vi.fn(() => ({ version: 1, format: "univer", styles: {}, sheets: {} })),
|
|
59
|
+
applyStylesToWorkbook: vi.fn((data: unknown) => data),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
describe("SpreadsheetEditor", () => {
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
vi.clearAllMocks();
|
|
65
|
+
for (const key of Object.keys(mockEventCallbacks)) {
|
|
66
|
+
delete mockEventCallbacks[key];
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("renders without crashing", () => {
|
|
71
|
+
render(
|
|
72
|
+
<SpreadsheetEditor
|
|
73
|
+
content="a,b\n1,2"
|
|
74
|
+
onSave={vi.fn()}
|
|
75
|
+
onCancel={vi.fn()}
|
|
76
|
+
/>,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("cell newline insertion", () => {
|
|
81
|
+
it("inserts newline into textarea on Alt+Enter during editing", () => {
|
|
82
|
+
const { container } = render(
|
|
83
|
+
<SpreadsheetEditor
|
|
84
|
+
content="a,b\n1,2"
|
|
85
|
+
onSave={vi.fn()}
|
|
86
|
+
onCancel={vi.fn()}
|
|
87
|
+
/>,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Activate editing mode via Univer event
|
|
91
|
+
mockEventCallbacks["BeforeSheetEditStart"]?.forEach((cb) => cb());
|
|
92
|
+
|
|
93
|
+
const editorDiv = container.querySelector("div.min-h-0") as HTMLDivElement;
|
|
94
|
+
expect(editorDiv).toBeTruthy();
|
|
95
|
+
|
|
96
|
+
const textarea = document.createElement("textarea");
|
|
97
|
+
textarea.value = "hello world";
|
|
98
|
+
textarea.selectionStart = 5;
|
|
99
|
+
textarea.selectionEnd = 5;
|
|
100
|
+
editorDiv.appendChild(textarea);
|
|
101
|
+
textarea.focus();
|
|
102
|
+
|
|
103
|
+
// Simulate Alt+Enter on the container (capture phase)
|
|
104
|
+
fireEvent.keyDown(textarea, {
|
|
105
|
+
key: "Enter",
|
|
106
|
+
altKey: true,
|
|
107
|
+
shiftKey: false,
|
|
108
|
+
ctrlKey: false,
|
|
109
|
+
metaKey: false,
|
|
110
|
+
bubbles: true,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(textarea.value).toBe("hello\n world");
|
|
114
|
+
expect(textarea.selectionStart).toBe(6);
|
|
115
|
+
expect(textarea.selectionEnd).toBe(6);
|
|
116
|
+
|
|
117
|
+
editorDiv.removeChild(textarea);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("inserts newline into textarea on Ctrl+Enter during editing", () => {
|
|
121
|
+
const { container } = render(
|
|
122
|
+
<SpreadsheetEditor
|
|
123
|
+
content="a,b\n1,2"
|
|
124
|
+
onSave={vi.fn()}
|
|
125
|
+
onCancel={vi.fn()}
|
|
126
|
+
/>,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
mockEventCallbacks["BeforeSheetEditStart"]?.forEach((cb) => cb());
|
|
130
|
+
|
|
131
|
+
const editorDiv = container.querySelector("div.min-h-0") as HTMLDivElement;
|
|
132
|
+
const textarea = document.createElement("textarea");
|
|
133
|
+
textarea.value = "abc";
|
|
134
|
+
textarea.selectionStart = 1;
|
|
135
|
+
textarea.selectionEnd = 1;
|
|
136
|
+
editorDiv.appendChild(textarea);
|
|
137
|
+
textarea.focus();
|
|
138
|
+
|
|
139
|
+
fireEvent.keyDown(textarea, {
|
|
140
|
+
key: "Enter",
|
|
141
|
+
altKey: false,
|
|
142
|
+
shiftKey: false,
|
|
143
|
+
ctrlKey: true,
|
|
144
|
+
metaKey: false,
|
|
145
|
+
bubbles: true,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(textarea.value).toBe("a\nbc");
|
|
149
|
+
|
|
150
|
+
editorDiv.removeChild(textarea);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("inserts newline into textarea on Meta+Enter during editing", () => {
|
|
154
|
+
const { container } = render(
|
|
155
|
+
<SpreadsheetEditor
|
|
156
|
+
content="a,b\n1,2"
|
|
157
|
+
onSave={vi.fn()}
|
|
158
|
+
onCancel={vi.fn()}
|
|
159
|
+
/>,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
mockEventCallbacks["BeforeSheetEditStart"]?.forEach((cb) => cb());
|
|
163
|
+
|
|
164
|
+
const editorDiv = container.querySelector("div.min-h-0") as HTMLDivElement;
|
|
165
|
+
const textarea = document.createElement("textarea");
|
|
166
|
+
textarea.value = "xyz";
|
|
167
|
+
textarea.selectionStart = 2;
|
|
168
|
+
textarea.selectionEnd = 2;
|
|
169
|
+
editorDiv.appendChild(textarea);
|
|
170
|
+
textarea.focus();
|
|
171
|
+
|
|
172
|
+
fireEvent.keyDown(textarea, {
|
|
173
|
+
key: "Enter",
|
|
174
|
+
altKey: false,
|
|
175
|
+
shiftKey: false,
|
|
176
|
+
ctrlKey: false,
|
|
177
|
+
metaKey: true,
|
|
178
|
+
bubbles: true,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(textarea.value).toBe("xy\nz");
|
|
182
|
+
|
|
183
|
+
editorDiv.removeChild(textarea);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("does not insert newline when Shift+Enter without modifier", () => {
|
|
187
|
+
const { container } = render(
|
|
188
|
+
<SpreadsheetEditor
|
|
189
|
+
content="a,b\n1,2"
|
|
190
|
+
onSave={vi.fn()}
|
|
191
|
+
onCancel={vi.fn()}
|
|
192
|
+
/>,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
mockEventCallbacks["BeforeSheetEditStart"]?.forEach((cb) => cb());
|
|
196
|
+
|
|
197
|
+
const editorDiv = container.querySelector("div.min-h-0") as HTMLDivElement;
|
|
198
|
+
const textarea = document.createElement("textarea");
|
|
199
|
+
textarea.value = "test";
|
|
200
|
+
textarea.selectionStart = 2;
|
|
201
|
+
textarea.selectionEnd = 2;
|
|
202
|
+
editorDiv.appendChild(textarea);
|
|
203
|
+
textarea.focus();
|
|
204
|
+
|
|
205
|
+
fireEvent.keyDown(textarea, {
|
|
206
|
+
key: "Enter",
|
|
207
|
+
altKey: false,
|
|
208
|
+
shiftKey: true,
|
|
209
|
+
ctrlKey: false,
|
|
210
|
+
metaKey: false,
|
|
211
|
+
bubbles: true,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(textarea.value).toBe("test");
|
|
215
|
+
|
|
216
|
+
editorDiv.removeChild(textarea);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("does nothing when not in editing mode", () => {
|
|
220
|
+
const { container } = render(
|
|
221
|
+
<SpreadsheetEditor
|
|
222
|
+
content="a,b\n1,2"
|
|
223
|
+
onSave={vi.fn()}
|
|
224
|
+
onCancel={vi.fn()}
|
|
225
|
+
/>,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Do NOT trigger BeforeSheetEditStart — isEditing stays false
|
|
229
|
+
|
|
230
|
+
const editorDiv = container.querySelector("div.min-h-0") as HTMLDivElement;
|
|
231
|
+
const textarea = document.createElement("textarea");
|
|
232
|
+
textarea.value = "no-edit";
|
|
233
|
+
textarea.selectionStart = 3;
|
|
234
|
+
textarea.selectionEnd = 3;
|
|
235
|
+
editorDiv.appendChild(textarea);
|
|
236
|
+
textarea.focus();
|
|
237
|
+
|
|
238
|
+
fireEvent.keyDown(textarea, {
|
|
239
|
+
key: "Enter",
|
|
240
|
+
altKey: true,
|
|
241
|
+
shiftKey: false,
|
|
242
|
+
ctrlKey: false,
|
|
243
|
+
metaKey: false,
|
|
244
|
+
bubbles: true,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(textarea.value).toBe("no-edit");
|
|
248
|
+
|
|
249
|
+
editorDiv.removeChild(textarea);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("does not insert newline when only Enter is pressed during editing", () => {
|
|
253
|
+
const { container } = render(
|
|
254
|
+
<SpreadsheetEditor
|
|
255
|
+
content="a,b\n1,2"
|
|
256
|
+
onSave={vi.fn()}
|
|
257
|
+
onCancel={vi.fn()}
|
|
258
|
+
/>,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
mockEventCallbacks["BeforeSheetEditStart"]?.forEach((cb) => cb());
|
|
262
|
+
|
|
263
|
+
const editorDiv = container.querySelector("div.min-h-0") as HTMLDivElement;
|
|
264
|
+
const textarea = document.createElement("textarea");
|
|
265
|
+
textarea.value = "plain";
|
|
266
|
+
textarea.selectionStart = 2;
|
|
267
|
+
textarea.selectionEnd = 2;
|
|
268
|
+
editorDiv.appendChild(textarea);
|
|
269
|
+
textarea.focus();
|
|
270
|
+
|
|
271
|
+
fireEvent.keyDown(textarea, {
|
|
272
|
+
key: "Enter",
|
|
273
|
+
altKey: false,
|
|
274
|
+
shiftKey: false,
|
|
275
|
+
ctrlKey: false,
|
|
276
|
+
metaKey: false,
|
|
277
|
+
bubbles: true,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(textarea.value).toBe("plain");
|
|
281
|
+
|
|
282
|
+
editorDiv.removeChild(textarea);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
|
@@ -184,6 +184,46 @@ export default function SpreadsheetEditor({
|
|
|
184
184
|
return;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// Alt+Enter / Ctrl+Enter / Cmd+Enter: insert newline in cell editor
|
|
188
|
+
if (e.key === "Enter" && !e.shiftKey && (e.altKey || e.ctrlKey || e.metaKey)) {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
e.stopPropagation();
|
|
191
|
+
|
|
192
|
+
const activeEl = document.activeElement as HTMLElement | null;
|
|
193
|
+
if (
|
|
194
|
+
activeEl &&
|
|
195
|
+
(activeEl.tagName === "TEXTAREA" ||
|
|
196
|
+
activeEl.tagName === "INPUT" ||
|
|
197
|
+
activeEl.isContentEditable)
|
|
198
|
+
) {
|
|
199
|
+
if (activeEl.isContentEditable) {
|
|
200
|
+
const selection = window.getSelection();
|
|
201
|
+
if (selection && selection.rangeCount > 0) {
|
|
202
|
+
const range = selection.getRangeAt(0);
|
|
203
|
+
range.deleteContents();
|
|
204
|
+
const br = document.createElement("br");
|
|
205
|
+
range.insertNode(br);
|
|
206
|
+
range.setStartAfter(br);
|
|
207
|
+
range.setEndAfter(br);
|
|
208
|
+
selection.removeAllRanges();
|
|
209
|
+
selection.addRange(range);
|
|
210
|
+
}
|
|
211
|
+
} else if (
|
|
212
|
+
activeEl.tagName === "TEXTAREA" ||
|
|
213
|
+
activeEl.tagName === "INPUT"
|
|
214
|
+
) {
|
|
215
|
+
const el = activeEl as HTMLTextAreaElement | HTMLInputElement;
|
|
216
|
+
const start = el.selectionStart ?? 0;
|
|
217
|
+
const end = el.selectionEnd ?? 0;
|
|
218
|
+
const value = el.value;
|
|
219
|
+
el.value = value.slice(0, start) + "\n" + value.slice(end);
|
|
220
|
+
el.selectionStart = el.selectionEnd = start + 1;
|
|
221
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
187
227
|
if (!(e.metaKey || e.altKey || e.ctrlKey)) return;
|
|
188
228
|
|
|
189
229
|
// Ctrl+A / Cmd+A: select all within cell editor
|
|
@@ -11,6 +11,7 @@ type TreeNodeProps = {
|
|
|
11
11
|
entry: TreeEntry;
|
|
12
12
|
depth: number;
|
|
13
13
|
selected: string | null;
|
|
14
|
+
activeDir?: string | null;
|
|
14
15
|
onSelect: (path: string) => void;
|
|
15
16
|
onContextMenu?: (e: React.MouseEvent, entry: TreeEntry) => void;
|
|
16
17
|
onMoveFile?: (oldPath: string, newParentDir: string) => void;
|
|
@@ -23,6 +24,7 @@ export default function TreeNode({
|
|
|
23
24
|
entry,
|
|
24
25
|
depth,
|
|
25
26
|
selected,
|
|
27
|
+
activeDir,
|
|
26
28
|
onSelect,
|
|
27
29
|
onContextMenu,
|
|
28
30
|
onMoveFile,
|
|
@@ -78,6 +80,7 @@ export default function TreeNode({
|
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
if (entry.type === "dir") {
|
|
83
|
+
const isActiveDir = activeDir === entry.path;
|
|
81
84
|
return (
|
|
82
85
|
<div>
|
|
83
86
|
<button
|
|
@@ -93,7 +96,11 @@ export default function TreeNode({
|
|
|
93
96
|
onDragLeave={handleDragLeave}
|
|
94
97
|
onDrop={handleDrop}
|
|
95
98
|
className={`w-full text-left py-1 px-2 text-sm rounded flex items-center gap-1 transition-colors ${
|
|
96
|
-
dropOver
|
|
99
|
+
dropOver
|
|
100
|
+
? "bg-blue-700/40 ring-1 ring-blue-500"
|
|
101
|
+
: isActiveDir
|
|
102
|
+
? "bg-blue-600/30 text-blue-200"
|
|
103
|
+
: "hover:bg-gray-700"
|
|
97
104
|
}`}
|
|
98
105
|
style={{ paddingLeft: pl }}
|
|
99
106
|
>
|
|
@@ -112,6 +119,7 @@ export default function TreeNode({
|
|
|
112
119
|
entry={child}
|
|
113
120
|
depth={depth + 1}
|
|
114
121
|
selected={selected}
|
|
122
|
+
activeDir={activeDir}
|
|
115
123
|
onSelect={onSelect}
|
|
116
124
|
onContextMenu={onContextMenu}
|
|
117
125
|
onMoveFile={onMoveFile}
|
|
@@ -795,18 +795,19 @@ export default function MainPage() {
|
|
|
795
795
|
</p>
|
|
796
796
|
</div>
|
|
797
797
|
{filteredTree.map((entry) => (
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
798
|
+
<TreeNode
|
|
799
|
+
key={entry.path}
|
|
800
|
+
entry={entry}
|
|
801
|
+
depth={0}
|
|
802
|
+
selected={selectedFile}
|
|
803
|
+
activeDir={activeDir}
|
|
804
|
+
onSelect={handleFileSelectResponsive}
|
|
805
|
+
onContextMenu={handleTreeContextMenu}
|
|
806
|
+
onMoveFile={handleMoveFile}
|
|
807
|
+
onFolderClick={handleFolderClick}
|
|
808
|
+
canDrag={false}
|
|
809
|
+
collapseKey={collapseKey}
|
|
810
|
+
/>
|
|
810
811
|
))}
|
|
811
812
|
</div>
|
|
812
813
|
)}
|
|
@@ -818,6 +819,7 @@ export default function MainPage() {
|
|
|
818
819
|
entry={entry}
|
|
819
820
|
depth={0}
|
|
820
821
|
selected={selectedFile}
|
|
822
|
+
activeDir={activeDir}
|
|
821
823
|
onSelect={handleFileSelectResponsive}
|
|
822
824
|
onContextMenu={handleTreeContextMenu}
|
|
823
825
|
onMoveFile={handleMoveFile}
|
package/package.json
CHANGED