@ripla/godd-mcp 1.0.3 → 1.0.4-canary.1

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.
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ripla/godd-mcp",
3
- "version": "1.0.3",
3
+ "version": "1.0.4-canary.1",
4
4
  "type": "module",
5
5
  "description": "GoDD MCP Server - AI-powered development workflow tools via Model Context Protocol (slash commands support)",
6
6
  "main": "dist/index.js",