@aiot-nexus/plugin-sdk 2.0.1 → 2.1.0
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/README.md +19 -7
- package/dev-host/main.js +164 -8
- package/dev-host/plugin-host.js +1 -415
- package/dist/.generated/plugin-framework-version.d.ts +1 -1
- package/dist/.generated/plugin-framework-version.js +1 -1
- package/dist/.generated/plugin-manifest.schema.json +1 -1
- package/dist/browser/frame-client.mjs +1 -1
- package/dist/internal/src-electron/main/core/runtime-result.js +42 -0
- package/dist/internal/src-electron/main/plugins/plugin-electron-facade-broker.js +890 -0
- package/dist/internal/src-electron/plugin-host-bootstrap.js +417 -0
- package/dist/internal/src-electron/shared/contracts.js +2 -0
- package/dist/internal/src-electron/shared/plugin-electron-facade-client.js +181 -0
- package/dist/internal/src-electron/shared/plugin-electron-facade-descriptors.js +70 -0
- package/dist/internal/src-electron/shared/plugin-electron-facade.js +243 -0
- package/dist/internal/src-electron/shared/plugin-rpc.js +245 -0
- package/dist/src/browser/frame-client.js +1 -1
- package/dist/src/electron-facade.d.ts +4 -0
- package/dist/src/electron-facade.js +56 -0
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/dist/src/nexus-dev.js +5 -4
- package/dist/src/scaffold.js +2 -2
- package/dist/src/types.d.ts +86 -2
- package/dist/src/web-dev.js +2 -2
- package/package.json +7 -2
- package/templates/minimal/README.md.tmpl +37 -2
- package/templates/minimal/src/main.ts.tmpl +2 -1
- package/templates/sqlite/README.md.tmpl +32 -2
- package/templates/sqlite/src/better-sqlite3.d.ts +12 -1
- package/templates/sqlite/src/main.ts.tmpl +939 -124
- package/templates/sqlite/src/renderer/app.ts.tmpl +688 -181
- package/templates/sqlite/src/renderer/index.html +40 -22
- package/templates/sqlite/src/renderer/plugin.css +302 -90
|
@@ -1,66 +1,152 @@
|
|
|
1
1
|
import { createPluginFrameClient } from "@aiot-nexus/plugin-sdk/browser";
|
|
2
2
|
|
|
3
|
+
type DatabaseSource = "default" | "external";
|
|
4
|
+
type SqliteAffinity = "integer" | "real" | "text" | "blob" | "numeric";
|
|
5
|
+
type LocatorKind = "primaryKey" | "rowid";
|
|
6
|
+
type EditorMode = "insert" | "update";
|
|
7
|
+
type SupportedLanguage = "en-US" | "zh-CN";
|
|
8
|
+
type ThemeMode = "light" | "dark" | "system";
|
|
9
|
+
type MessageParams = Record<string, string | number>;
|
|
10
|
+
|
|
3
11
|
interface DatabaseInfo {
|
|
4
12
|
databaseName: string;
|
|
5
13
|
databasePath: string;
|
|
6
|
-
|
|
7
|
-
recordCount: number;
|
|
14
|
+
source: DatabaseSource;
|
|
8
15
|
databaseReady: boolean;
|
|
16
|
+
notice?: string;
|
|
9
17
|
}
|
|
10
18
|
|
|
11
|
-
interface
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
interface TableSummary {
|
|
20
|
+
name: string;
|
|
21
|
+
columnCount: number;
|
|
22
|
+
withoutRowid: boolean;
|
|
23
|
+
strict: boolean;
|
|
16
24
|
}
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
interface ColumnInfo {
|
|
27
|
+
name: string;
|
|
28
|
+
declaredType: string;
|
|
29
|
+
affinity: SqliteAffinity;
|
|
30
|
+
nullable: boolean;
|
|
31
|
+
defaultSql: string | null;
|
|
32
|
+
primaryKeyOrder: number;
|
|
33
|
+
generated: boolean;
|
|
34
|
+
hidden: boolean;
|
|
35
|
+
insertable: boolean;
|
|
36
|
+
requiredOnInsert: boolean;
|
|
37
|
+
autoIntegerPrimaryKey: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type WireValue =
|
|
41
|
+
| { kind: "null" }
|
|
42
|
+
| { kind: "integer"; value: string }
|
|
43
|
+
| { kind: "real"; value: number }
|
|
44
|
+
| { kind: "text"; value: string; length: number; truncated: boolean }
|
|
45
|
+
| { kind: "blob"; hex: string; byteLength: number; truncated: boolean };
|
|
46
|
+
|
|
47
|
+
interface RowLocator {
|
|
48
|
+
kind: LocatorKind;
|
|
49
|
+
values: Array<{ column: string; value: WireValue }>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface TableRow {
|
|
53
|
+
cells: Array<{ column: string; value: WireValue }>;
|
|
54
|
+
locator?: RowLocator;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface TableData {
|
|
58
|
+
table: TableSummary;
|
|
59
|
+
columns: ColumnInfo[];
|
|
60
|
+
rows: TableRow[];
|
|
61
|
+
totalRows: string;
|
|
62
|
+
offset: number;
|
|
63
|
+
limit: number;
|
|
64
|
+
hasPrevious: boolean;
|
|
65
|
+
hasNext: boolean;
|
|
66
|
+
writable: boolean;
|
|
67
|
+
locatorKind?: LocatorKind;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface EditorDraftValue {
|
|
71
|
+
mode: string;
|
|
72
|
+
value: string;
|
|
73
|
+
}
|
|
21
74
|
|
|
22
75
|
const englishMessages = {
|
|
23
|
-
"document.title": "
|
|
24
|
-
"heading.
|
|
76
|
+
"document.title": "SQLite Browser",
|
|
77
|
+
"heading.browser": "SQLite Browser",
|
|
78
|
+
"toolbar.databaseActions": "Database actions",
|
|
25
79
|
"summary.aria": "Database summary",
|
|
26
80
|
"summary.database": "Database",
|
|
81
|
+
"summary.source": "Source",
|
|
27
82
|
"summary.table": "Table",
|
|
83
|
+
"summary.columns": "Columns",
|
|
28
84
|
"summary.rows": "Rows",
|
|
29
85
|
"summary.status": "Status",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
86
|
+
"source.default": "Sample",
|
|
87
|
+
"source.external": "External",
|
|
88
|
+
"section.rows": "Table Rows",
|
|
89
|
+
"editor.insert": "Insert Row",
|
|
90
|
+
"editor.update": "Edit Row",
|
|
91
|
+
"editor.help": "Choose how each SQLite column should be written.",
|
|
92
|
+
"editor.current": "Current: {value}",
|
|
93
|
+
"column.untyped": "UNTYPED",
|
|
94
|
+
"column.primaryKey": "PK {order}",
|
|
95
|
+
"column.notNull": "NOT NULL",
|
|
96
|
+
"column.default": "DEFAULT {value}",
|
|
97
|
+
"column.generated": "GENERATED",
|
|
98
|
+
"column.required": "Required",
|
|
99
|
+
"mode.default": "Use default",
|
|
100
|
+
"mode.unchanged": "Do not change",
|
|
101
|
+
"mode.null": "NULL",
|
|
102
|
+
"mode.value": "Value",
|
|
103
|
+
"action.openDatabase": "Open Database",
|
|
104
|
+
"action.useDefault": "Use Sample",
|
|
40
105
|
"action.refresh": "Refresh",
|
|
106
|
+
"action.addRow": "Add Row",
|
|
41
107
|
"action.insert": "Insert",
|
|
42
108
|
"action.save": "Save",
|
|
43
109
|
"action.cancel": "Cancel",
|
|
44
|
-
"action.
|
|
110
|
+
"action.close": "Close",
|
|
45
111
|
"action.edit": "Edit",
|
|
46
112
|
"action.delete": "Delete",
|
|
113
|
+
"action.confirm": "Confirm",
|
|
114
|
+
"action.previous": "Previous",
|
|
115
|
+
"action.next": "Next",
|
|
116
|
+
"table.actions": "Actions",
|
|
117
|
+
"pager.aria": "Pagination",
|
|
118
|
+
"pager.pageSize": "Rows per page",
|
|
119
|
+
"pager.range": "{start}-{end} / {total}",
|
|
47
120
|
"status.connecting": "Connecting",
|
|
48
121
|
"status.waiting": "Waiting",
|
|
49
122
|
"status.idle": "Idle",
|
|
50
123
|
"status.loading": "Loading",
|
|
51
124
|
"status.ready": "Ready",
|
|
52
125
|
"status.unavailable": "Unavailable",
|
|
53
|
-
"status.
|
|
54
|
-
"status.
|
|
55
|
-
"status.
|
|
56
|
-
"status.
|
|
57
|
-
"status.
|
|
58
|
-
"status.
|
|
59
|
-
"
|
|
60
|
-
"
|
|
126
|
+
"status.opening": "Opening database",
|
|
127
|
+
"status.switching": "Switching database",
|
|
128
|
+
"status.inserting": "Inserting row",
|
|
129
|
+
"status.updating": "Updating row",
|
|
130
|
+
"status.deleting": "Deleting row",
|
|
131
|
+
"status.databaseOpened": "Opened {name}.",
|
|
132
|
+
"status.defaultOpened": "Using the sample database.",
|
|
133
|
+
"status.selectionCanceled": "Database selection canceled.",
|
|
134
|
+
"status.insertedRow": "Inserted row into {table}.",
|
|
135
|
+
"status.updatedRow": "Updated row in {table}.",
|
|
136
|
+
"status.deletedRow": "Deleted row from {table}.",
|
|
137
|
+
"status.writeSucceededRefreshFailed": "{operation} Rows could not be refreshed: {message}",
|
|
138
|
+
"empty.noTables": "No user tables were found in this database.",
|
|
139
|
+
"empty.noRows": "No rows in this table.",
|
|
140
|
+
"empty.noWritableColumns": "This table has no writable columns.",
|
|
141
|
+
"empty.noLocator": "This row has no stable primary-key or rowid locator.",
|
|
142
|
+
"value.null": "NULL",
|
|
143
|
+
"value.blob": "BLOB · {bytes} bytes · {preview}",
|
|
144
|
+
"value.truncated": "{value}… ({length} chars)",
|
|
145
|
+
"placeholder.value": "Enter value",
|
|
146
|
+
"placeholder.hex": "Hexadecimal bytes, for example 89504e47",
|
|
61
147
|
"error.initializationFailed": "Initialization failed: {message}",
|
|
62
148
|
"error.refreshFailed": "Refresh failed: {message}",
|
|
63
|
-
"error.
|
|
149
|
+
"error.operationFailed": "Operation failed: {message}"
|
|
64
150
|
} as const;
|
|
65
151
|
|
|
66
152
|
type MessageKey = keyof typeof englishMessages;
|
|
@@ -68,47 +154,80 @@ type MessageKey = keyof typeof englishMessages;
|
|
|
68
154
|
const messages: Record<SupportedLanguage, Record<MessageKey, string>> = {
|
|
69
155
|
"en-US": englishMessages,
|
|
70
156
|
"zh-CN": {
|
|
71
|
-
"document.title": "
|
|
72
|
-
"heading.
|
|
157
|
+
"document.title": "SQLite 数据库浏览器",
|
|
158
|
+
"heading.browser": "SQLite 数据库浏览器",
|
|
159
|
+
"toolbar.databaseActions": "数据库操作",
|
|
73
160
|
"summary.aria": "数据库摘要",
|
|
74
161
|
"summary.database": "数据库",
|
|
162
|
+
"summary.source": "来源",
|
|
75
163
|
"summary.table": "数据表",
|
|
164
|
+
"summary.columns": "数据列",
|
|
76
165
|
"summary.rows": "记录数",
|
|
77
166
|
"summary.status": "状态",
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
167
|
+
"source.default": "示例库",
|
|
168
|
+
"source.external": "外部文件",
|
|
169
|
+
"section.rows": "表数据",
|
|
170
|
+
"editor.insert": "新增记录",
|
|
171
|
+
"editor.update": "编辑记录",
|
|
172
|
+
"editor.help": "为每个 SQLite 数据列选择写入方式。",
|
|
173
|
+
"editor.current": "当前值:{value}",
|
|
174
|
+
"column.untyped": "未声明类型",
|
|
175
|
+
"column.primaryKey": "主键 {order}",
|
|
176
|
+
"column.notNull": "非空",
|
|
177
|
+
"column.default": "默认 {value}",
|
|
178
|
+
"column.generated": "生成列",
|
|
179
|
+
"column.required": "必填",
|
|
180
|
+
"mode.default": "使用默认值",
|
|
181
|
+
"mode.unchanged": "不修改",
|
|
182
|
+
"mode.null": "NULL",
|
|
183
|
+
"mode.value": "输入值",
|
|
184
|
+
"action.openDatabase": "打开数据库",
|
|
185
|
+
"action.useDefault": "使用示例库",
|
|
88
186
|
"action.refresh": "刷新",
|
|
187
|
+
"action.addRow": "新增记录",
|
|
89
188
|
"action.insert": "新增",
|
|
90
189
|
"action.save": "保存",
|
|
91
190
|
"action.cancel": "取消",
|
|
92
|
-
"action.
|
|
191
|
+
"action.close": "关闭",
|
|
93
192
|
"action.edit": "编辑",
|
|
94
193
|
"action.delete": "删除",
|
|
194
|
+
"action.confirm": "确认",
|
|
195
|
+
"action.previous": "上一页",
|
|
196
|
+
"action.next": "下一页",
|
|
197
|
+
"table.actions": "操作",
|
|
198
|
+
"pager.aria": "分页",
|
|
199
|
+
"pager.pageSize": "每页记录",
|
|
200
|
+
"pager.range": "{start}-{end} / {total}",
|
|
95
201
|
"status.connecting": "正在连接",
|
|
96
202
|
"status.waiting": "等待中",
|
|
97
203
|
"status.idle": "空闲",
|
|
98
204
|
"status.loading": "正在加载",
|
|
99
205
|
"status.ready": "已就绪",
|
|
100
206
|
"status.unavailable": "不可用",
|
|
101
|
-
"status.
|
|
102
|
-
"status.
|
|
103
|
-
"status.
|
|
104
|
-
"status.
|
|
105
|
-
"status.
|
|
106
|
-
"status.
|
|
107
|
-
"
|
|
108
|
-
"
|
|
207
|
+
"status.opening": "正在打开数据库",
|
|
208
|
+
"status.switching": "正在切换数据库",
|
|
209
|
+
"status.inserting": "正在新增记录",
|
|
210
|
+
"status.updating": "正在更新记录",
|
|
211
|
+
"status.deleting": "正在删除记录",
|
|
212
|
+
"status.databaseOpened": "已打开 {name}。",
|
|
213
|
+
"status.defaultOpened": "正在使用示例数据库。",
|
|
214
|
+
"status.selectionCanceled": "已取消数据库选择。",
|
|
215
|
+
"status.insertedRow": "已向 {table} 新增记录。",
|
|
216
|
+
"status.updatedRow": "已更新 {table} 中的记录。",
|
|
217
|
+
"status.deletedRow": "已删除 {table} 中的记录。",
|
|
218
|
+
"status.writeSucceededRefreshFailed": "{operation} 但表格刷新失败:{message}",
|
|
219
|
+
"empty.noTables": "数据库中没有可浏览的用户表。",
|
|
220
|
+
"empty.noRows": "当前数据表没有记录。",
|
|
221
|
+
"empty.noWritableColumns": "当前数据表没有可写数据列。",
|
|
222
|
+
"empty.noLocator": "当前记录没有稳定的主键或 rowid 定位信息。",
|
|
223
|
+
"value.null": "NULL",
|
|
224
|
+
"value.blob": "BLOB · {bytes} 字节 · {preview}",
|
|
225
|
+
"value.truncated": "{value}…(共 {length} 个字符)",
|
|
226
|
+
"placeholder.value": "请输入值",
|
|
227
|
+
"placeholder.hex": "十六进制字节,例如 89504e47",
|
|
109
228
|
"error.initializationFailed": "初始化失败:{message}",
|
|
110
229
|
"error.refreshFailed": "刷新失败:{message}",
|
|
111
|
-
"error.
|
|
230
|
+
"error.operationFailed": "操作失败:{message}"
|
|
112
231
|
}
|
|
113
232
|
};
|
|
114
233
|
|
|
@@ -120,45 +239,77 @@ const client = createPluginFrameClient();
|
|
|
120
239
|
const elements = {
|
|
121
240
|
databasePath: requireElement<HTMLElement>("databasePath"),
|
|
122
241
|
databaseName: requireElement<HTMLElement>("databaseName"),
|
|
123
|
-
|
|
242
|
+
databaseSource: requireElement<HTMLElement>("databaseSource"),
|
|
243
|
+
tableSelect: requireElement<HTMLSelectElement>("tableSelect"),
|
|
244
|
+
columnCount: requireElement<HTMLElement>("columnCount"),
|
|
124
245
|
recordCount: requireElement<HTMLElement>("recordCount"),
|
|
125
246
|
loadingState: requireElement<HTMLElement>("loadingState"),
|
|
247
|
+
openDatabaseButton: requireElement<HTMLButtonElement>("openDatabaseButton"),
|
|
248
|
+
useDefaultDatabaseButton: requireElement<HTMLButtonElement>("useDefaultDatabaseButton"),
|
|
126
249
|
refreshButton: requireElement<HTMLButtonElement>("refreshButton"),
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
250
|
+
addRowButton: requireElement<HTMLButtonElement>("addRowButton"),
|
|
251
|
+
editorPanel: requireElement<HTMLElement>("editorPanel"),
|
|
252
|
+
editorTitle: requireElement<HTMLElement>("editorTitle"),
|
|
253
|
+
editorFields: requireElement<HTMLElement>("editorFields"),
|
|
254
|
+
saveRowButton: requireElement<HTMLButtonElement>("saveRowButton"),
|
|
255
|
+
cancelEditorButton: requireElement<HTMLButtonElement>("cancelEditorButton"),
|
|
256
|
+
closeEditorButton: requireElement<HTMLButtonElement>("closeEditorButton"),
|
|
132
257
|
formError: requireElement<HTMLElement>("formError"),
|
|
133
258
|
emptyState: requireElement<HTMLElement>("emptyState"),
|
|
259
|
+
recordsHead: requireElement<HTMLTableRowElement>("recordsHead"),
|
|
134
260
|
recordsBody: requireElement<HTMLTableSectionElement>("recordsBody"),
|
|
135
|
-
statusLog: requireElement<HTMLElement>("statusLog")
|
|
261
|
+
statusLog: requireElement<HTMLElement>("statusLog"),
|
|
262
|
+
pageSizeSelect: requireElement<HTMLSelectElement>("pageSizeSelect"),
|
|
263
|
+
pageLabel: requireElement<HTMLElement>("pageLabel"),
|
|
264
|
+
previousPageButton: requireElement<HTMLButtonElement>("previousPageButton"),
|
|
265
|
+
nextPageButton: requireElement<HTMLButtonElement>("nextPageButton")
|
|
136
266
|
};
|
|
137
267
|
const systemDarkMode = window.matchMedia("(prefers-color-scheme: dark)");
|
|
138
268
|
|
|
139
269
|
let language: SupportedLanguage = "en-US";
|
|
140
270
|
let themeMode: ThemeMode = "system";
|
|
141
|
-
let
|
|
142
|
-
let
|
|
143
|
-
let
|
|
144
|
-
let
|
|
271
|
+
let databaseInfo: DatabaseInfo | undefined;
|
|
272
|
+
let tables: TableSummary[] = [];
|
|
273
|
+
let tableData: TableData | undefined;
|
|
274
|
+
let selectedTable = "";
|
|
275
|
+
let offset = 0;
|
|
276
|
+
let pageSize = 50;
|
|
277
|
+
let editorMode: EditorMode | undefined;
|
|
278
|
+
let editingRow: TableRow | undefined;
|
|
279
|
+
let pendingDeleteLocator = "";
|
|
145
280
|
let busy = false;
|
|
146
281
|
let busyLabelKey: MessageKey = "status.waiting";
|
|
147
282
|
let statusState: LocalizedState = { kind: "message", key: "status.idle" };
|
|
148
283
|
let formErrorState: LocalizedState | undefined;
|
|
149
284
|
|
|
150
|
-
elements.
|
|
151
|
-
elements.
|
|
152
|
-
elements.
|
|
153
|
-
elements.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
285
|
+
elements.openDatabaseButton.addEventListener("click", () => void openDatabase());
|
|
286
|
+
elements.useDefaultDatabaseButton.addEventListener("click", () => void useDefaultDatabase());
|
|
287
|
+
elements.refreshButton.addEventListener("click", () => void refreshDatabase(true));
|
|
288
|
+
elements.addRowButton.addEventListener("click", () => openEditor("insert"));
|
|
289
|
+
elements.saveRowButton.addEventListener("click", () => void saveRow());
|
|
290
|
+
elements.cancelEditorButton.addEventListener("click", closeEditor);
|
|
291
|
+
elements.closeEditorButton.addEventListener("click", closeEditor);
|
|
292
|
+
elements.tableSelect.addEventListener("change", () => {
|
|
293
|
+
selectedTable = elements.tableSelect.value;
|
|
294
|
+
offset = 0;
|
|
295
|
+
closeEditor();
|
|
296
|
+
void loadSelectedTable();
|
|
158
297
|
});
|
|
159
|
-
|
|
160
|
-
|
|
298
|
+
elements.pageSizeSelect.addEventListener("change", () => {
|
|
299
|
+
pageSize = Number(elements.pageSizeSelect.value);
|
|
300
|
+
offset = 0;
|
|
301
|
+
void loadSelectedTable();
|
|
302
|
+
});
|
|
303
|
+
elements.previousPageButton.addEventListener("click", () => {
|
|
304
|
+
offset = Math.max(0, offset - pageSize);
|
|
305
|
+
void loadSelectedTable();
|
|
161
306
|
});
|
|
307
|
+
elements.nextPageButton.addEventListener("click", () => {
|
|
308
|
+
offset += pageSize;
|
|
309
|
+
void loadSelectedTable();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const stopContextUpdates = client.onContextChanged(applyContext);
|
|
162
313
|
systemDarkMode.addEventListener("change", handleSystemThemeChange);
|
|
163
314
|
window.addEventListener("beforeunload", () => {
|
|
164
315
|
stopContextUpdates();
|
|
@@ -168,7 +319,7 @@ window.addEventListener("beforeunload", () => {
|
|
|
168
319
|
|
|
169
320
|
void client.waitForContext().then((context) => {
|
|
170
321
|
applyContext(context);
|
|
171
|
-
return
|
|
322
|
+
return refreshDatabase(false);
|
|
172
323
|
}).catch((error: unknown) => {
|
|
173
324
|
applyLanguage(language);
|
|
174
325
|
applyTheme(themeMode);
|
|
@@ -176,121 +327,472 @@ void client.waitForContext().then((context) => {
|
|
|
176
327
|
writeStatusMessage("error.initializationFailed", { message: errorMessage(error) });
|
|
177
328
|
});
|
|
178
329
|
|
|
179
|
-
async function
|
|
330
|
+
async function refreshDatabase(preserveSelection: boolean): Promise<void> {
|
|
180
331
|
setBusy(true, "status.loading");
|
|
181
332
|
try {
|
|
182
|
-
const [info,
|
|
183
|
-
invoke<DatabaseInfo>("sqlite.
|
|
184
|
-
invoke<{
|
|
333
|
+
const [info, tableList] = await Promise.all([
|
|
334
|
+
invoke<DatabaseInfo>("sqlite.getDatabaseInfo"),
|
|
335
|
+
invoke<{ tables: TableSummary[] }>("sqlite.listTables")
|
|
185
336
|
]);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
337
|
+
databaseInfo = info;
|
|
338
|
+
tables = tableList.tables;
|
|
339
|
+
renderDatabaseInfo();
|
|
340
|
+
const previous = preserveSelection ? selectedTable : "";
|
|
341
|
+
selectedTable = tables.some((table) => table.name === previous)
|
|
342
|
+
? previous
|
|
343
|
+
: tables.some((table) => table.name === "plugin_records")
|
|
344
|
+
? "plugin_records"
|
|
345
|
+
: tables[0]?.name || "";
|
|
346
|
+
offset = preserveSelection && selectedTable === previous ? offset : 0;
|
|
347
|
+
renderTableSelector();
|
|
348
|
+
await readSelectedTable();
|
|
349
|
+
if (info.notice) {
|
|
350
|
+
writeStatusLiteral(info.notice);
|
|
351
|
+
} else {
|
|
352
|
+
writeStatusMessage("status.ready");
|
|
353
|
+
}
|
|
194
354
|
} catch (error: unknown) {
|
|
355
|
+
tableData = undefined;
|
|
356
|
+
renderTableData();
|
|
195
357
|
writeStatusMessage("error.refreshFailed", { message: errorMessage(error) });
|
|
196
358
|
} finally {
|
|
197
359
|
setBusy(false, "status.idle");
|
|
198
360
|
}
|
|
199
361
|
}
|
|
200
362
|
|
|
201
|
-
async function
|
|
202
|
-
|
|
203
|
-
const title = elements.titleInput.value.trim();
|
|
204
|
-
const note = elements.noteInput.value.trim();
|
|
205
|
-
if (!title) {
|
|
206
|
-
showFormErrorMessage("validation.titleRequired");
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
setBusy(true, editingRecord ? "status.updating" : "status.inserting");
|
|
363
|
+
async function openDatabase(): Promise<void> {
|
|
364
|
+
setBusy(true, "status.opening");
|
|
210
365
|
try {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
writeStatusMessage("status.
|
|
214
|
-
|
|
215
|
-
const inserted = await invoke<{ record: SqliteRecord }>("sqlite.insertRecord", { title, note });
|
|
216
|
-
writeStatusMessage("status.insertedRow", { id: inserted.record.id });
|
|
366
|
+
const result = await invoke<{ canceled: boolean; database: DatabaseInfo }>("sqlite.chooseDatabase");
|
|
367
|
+
if (result.canceled) {
|
|
368
|
+
writeStatusMessage("status.selectionCanceled");
|
|
369
|
+
return;
|
|
217
370
|
}
|
|
218
|
-
|
|
219
|
-
|
|
371
|
+
closeEditor();
|
|
372
|
+
selectedTable = "";
|
|
373
|
+
offset = 0;
|
|
374
|
+
await refreshDatabase(false);
|
|
375
|
+
writeStatusMessage("status.databaseOpened", { name: result.database.databaseName });
|
|
376
|
+
} catch (error: unknown) {
|
|
377
|
+
writeStatusMessage("error.operationFailed", { message: errorMessage(error) });
|
|
378
|
+
} finally {
|
|
379
|
+
setBusy(false, "status.idle");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function useDefaultDatabase(): Promise<void> {
|
|
384
|
+
setBusy(true, "status.switching");
|
|
385
|
+
try {
|
|
386
|
+
await invoke<{ database: DatabaseInfo }>("sqlite.useDefaultDatabase");
|
|
387
|
+
closeEditor();
|
|
388
|
+
selectedTable = "plugin_records";
|
|
389
|
+
offset = 0;
|
|
390
|
+
await refreshDatabase(true);
|
|
391
|
+
writeStatusMessage("status.defaultOpened");
|
|
220
392
|
} catch (error: unknown) {
|
|
221
|
-
|
|
393
|
+
writeStatusMessage("error.operationFailed", { message: errorMessage(error) });
|
|
222
394
|
} finally {
|
|
223
395
|
setBusy(false, "status.idle");
|
|
224
396
|
}
|
|
225
397
|
}
|
|
226
398
|
|
|
227
|
-
function
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
399
|
+
async function loadSelectedTable(): Promise<void> {
|
|
400
|
+
setBusy(true, "status.loading");
|
|
401
|
+
try {
|
|
402
|
+
await readSelectedTable();
|
|
403
|
+
writeStatusMessage("status.ready");
|
|
404
|
+
} catch (error: unknown) {
|
|
405
|
+
tableData = undefined;
|
|
406
|
+
renderTableData();
|
|
407
|
+
writeStatusMessage("error.refreshFailed", { message: errorMessage(error) });
|
|
408
|
+
} finally {
|
|
409
|
+
setBusy(false, "status.idle");
|
|
410
|
+
}
|
|
236
411
|
}
|
|
237
412
|
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
|
|
413
|
+
async function readSelectedTable(): Promise<void> {
|
|
414
|
+
if (!selectedTable) {
|
|
415
|
+
tableData = undefined;
|
|
416
|
+
renderTableData();
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
tableData = await invoke<TableData>("sqlite.readTable", {
|
|
420
|
+
table: selectedTable,
|
|
421
|
+
offset,
|
|
422
|
+
limit: pageSize
|
|
423
|
+
});
|
|
424
|
+
offset = tableData.offset;
|
|
425
|
+
renderTableData();
|
|
241
426
|
}
|
|
242
427
|
|
|
243
|
-
|
|
428
|
+
function openEditor(mode: EditorMode, row?: TableRow): void {
|
|
429
|
+
if (!tableData || !tableData.writable) {
|
|
430
|
+
writeStatusMessage("empty.noWritableColumns");
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (mode === "update" && !row?.locator) {
|
|
434
|
+
writeStatusMessage("empty.noLocator");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
editorMode = mode;
|
|
438
|
+
editingRow = row;
|
|
439
|
+
pendingDeleteLocator = "";
|
|
440
|
+
clearFormError();
|
|
441
|
+
renderEditor();
|
|
442
|
+
elements.editorPanel.hidden = false;
|
|
443
|
+
elements.editorPanel.scrollIntoView({ block: "nearest" });
|
|
444
|
+
client.notifyResize();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function closeEditor(): void {
|
|
448
|
+
editorMode = undefined;
|
|
449
|
+
editingRow = undefined;
|
|
450
|
+
clearFormError();
|
|
451
|
+
elements.editorPanel.hidden = true;
|
|
452
|
+
elements.editorFields.replaceChildren();
|
|
453
|
+
client.notifyResize();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function saveRow(): Promise<void> {
|
|
457
|
+
if (!tableData || !editorMode) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
clearFormError();
|
|
461
|
+
const values = readEditorValues();
|
|
462
|
+
setBusy(true, editorMode === "insert" ? "status.inserting" : "status.updating");
|
|
463
|
+
try {
|
|
464
|
+
let successKey: MessageKey;
|
|
465
|
+
try {
|
|
466
|
+
if (editorMode === "insert") {
|
|
467
|
+
await invoke("sqlite.insertRow", { table: selectedTable, values });
|
|
468
|
+
successKey = "status.insertedRow";
|
|
469
|
+
} else {
|
|
470
|
+
if (!editingRow?.locator) {
|
|
471
|
+
throw new Error(text("empty.noLocator"));
|
|
472
|
+
}
|
|
473
|
+
await invoke("sqlite.updateRow", { table: selectedTable, locator: editingRow.locator, values });
|
|
474
|
+
successKey = "status.updatedRow";
|
|
475
|
+
}
|
|
476
|
+
} catch (error: unknown) {
|
|
477
|
+
showFormErrorLiteral(errorMessage(error));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
closeEditor();
|
|
481
|
+
await refreshAfterMutation(successKey, { table: selectedTable });
|
|
482
|
+
} finally {
|
|
483
|
+
setBusy(false, "status.idle");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function deleteRow(row: TableRow): Promise<void> {
|
|
488
|
+
if (!row.locator) {
|
|
489
|
+
writeStatusMessage("empty.noLocator");
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
244
492
|
setBusy(true, "status.deleting");
|
|
245
493
|
try {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
494
|
+
try {
|
|
495
|
+
await invoke("sqlite.deleteRow", { table: selectedTable, locator: row.locator });
|
|
496
|
+
} catch (error: unknown) {
|
|
497
|
+
writeStatusMessage("error.operationFailed", { message: errorMessage(error) });
|
|
498
|
+
return;
|
|
249
499
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
500
|
+
pendingDeleteLocator = "";
|
|
501
|
+
if (editingRow && locatorKey(editingRow.locator) === locatorKey(row.locator)) {
|
|
502
|
+
closeEditor();
|
|
503
|
+
}
|
|
504
|
+
await refreshAfterMutation("status.deletedRow", { table: selectedTable });
|
|
255
505
|
} finally {
|
|
256
506
|
setBusy(false, "status.idle");
|
|
257
507
|
}
|
|
258
508
|
}
|
|
259
509
|
|
|
260
|
-
function
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
510
|
+
async function refreshAfterMutation(successKey: MessageKey, params: MessageParams): Promise<void> {
|
|
511
|
+
const operation = text(successKey, params);
|
|
512
|
+
try {
|
|
513
|
+
await readSelectedTable();
|
|
514
|
+
writeStatusMessage(successKey, params);
|
|
515
|
+
} catch (error: unknown) {
|
|
516
|
+
writeStatusMessage("status.writeSucceededRefreshFailed", {
|
|
517
|
+
operation,
|
|
518
|
+
message: errorMessage(error)
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function renderDatabaseInfo(): void {
|
|
524
|
+
elements.databaseName.textContent = databaseInfo?.databaseName || "-";
|
|
525
|
+
elements.databaseSource.textContent = databaseInfo ? text(databaseInfo.source === "default" ? "source.default" : "source.external") : "-";
|
|
526
|
+
elements.databasePath.textContent = databaseInfo?.databasePath || text("status.connecting");
|
|
527
|
+
elements.databasePath.title = databaseInfo?.databasePath || "";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function renderTableSelector(): void {
|
|
531
|
+
const options = tables.map((table) => {
|
|
532
|
+
const option = document.createElement("option");
|
|
533
|
+
option.value = table.name;
|
|
534
|
+
option.textContent = table.name;
|
|
535
|
+
option.selected = table.name === selectedTable;
|
|
536
|
+
return option;
|
|
537
|
+
});
|
|
538
|
+
elements.tableSelect.replaceChildren(...options);
|
|
539
|
+
elements.tableSelect.disabled = busy || tables.length === 0;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function renderTableData(): void {
|
|
543
|
+
const data = tableData;
|
|
544
|
+
elements.columnCount.textContent = data ? String(data.columns.filter((column) => !column.hidden).length) : "0";
|
|
545
|
+
elements.recordCount.textContent = data?.totalRows || "0";
|
|
546
|
+
renderTableHeader();
|
|
547
|
+
renderRows();
|
|
548
|
+
renderPager();
|
|
549
|
+
elements.addRowButton.disabled = busy || !data?.writable;
|
|
550
|
+
if (!selectedTable) {
|
|
551
|
+
elements.emptyState.textContent = text("empty.noTables");
|
|
552
|
+
elements.emptyState.hidden = false;
|
|
553
|
+
} else {
|
|
554
|
+
elements.emptyState.textContent = text("empty.noRows");
|
|
555
|
+
elements.emptyState.hidden = Boolean(data && data.rows.length > 0);
|
|
556
|
+
}
|
|
557
|
+
client.notifyResize();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function renderTableHeader(): void {
|
|
561
|
+
if (!tableData) {
|
|
562
|
+
elements.recordsHead.replaceChildren();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const headers = tableData.columns.filter((column) => !column.hidden).map((column) => {
|
|
566
|
+
const header = document.createElement("th");
|
|
567
|
+
const title = document.createElement("div");
|
|
568
|
+
title.className = "column-title";
|
|
569
|
+
const name = document.createElement("strong");
|
|
570
|
+
name.textContent = column.name;
|
|
571
|
+
title.append(name, badge(column.declaredType || text("column.untyped")));
|
|
572
|
+
if (column.primaryKeyOrder > 0) {
|
|
573
|
+
title.append(badge(text("column.primaryKey", { order: column.primaryKeyOrder })));
|
|
574
|
+
}
|
|
575
|
+
if (!column.nullable) {
|
|
576
|
+
title.append(badge(text("column.notNull")));
|
|
577
|
+
}
|
|
578
|
+
if (column.defaultSql !== null) {
|
|
579
|
+
title.append(badge(text("column.default", { value: column.defaultSql })));
|
|
282
580
|
}
|
|
283
|
-
|
|
284
|
-
|
|
581
|
+
if (column.generated) {
|
|
582
|
+
title.append(badge(text("column.generated")));
|
|
583
|
+
}
|
|
584
|
+
header.append(title);
|
|
585
|
+
return header;
|
|
586
|
+
});
|
|
587
|
+
if (tableData.locatorKind) {
|
|
588
|
+
const actions = document.createElement("th");
|
|
589
|
+
actions.className = "actions-column";
|
|
590
|
+
actions.textContent = text("table.actions");
|
|
591
|
+
headers.push(actions);
|
|
592
|
+
}
|
|
593
|
+
elements.recordsHead.replaceChildren(...headers);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function renderRows(): void {
|
|
597
|
+
if (!tableData) {
|
|
598
|
+
elements.recordsBody.replaceChildren();
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const rows = tableData.rows.map((row) => {
|
|
602
|
+
const tableRow = document.createElement("tr");
|
|
603
|
+
for (const cell of row.cells) {
|
|
604
|
+
tableRow.append(renderCell(cell.value));
|
|
605
|
+
}
|
|
606
|
+
if (tableData?.locatorKind) {
|
|
607
|
+
tableRow.append(renderRowActions(row));
|
|
608
|
+
}
|
|
609
|
+
return tableRow;
|
|
285
610
|
});
|
|
286
611
|
elements.recordsBody.replaceChildren(...rows);
|
|
287
|
-
elements.emptyState.hidden = rows.length > 0;
|
|
288
612
|
}
|
|
289
613
|
|
|
290
|
-
function
|
|
614
|
+
function renderCell(value: WireValue): HTMLTableCellElement {
|
|
291
615
|
const cell = document.createElement("td");
|
|
292
|
-
|
|
293
|
-
|
|
616
|
+
const display = displayValue(value);
|
|
617
|
+
cell.textContent = display;
|
|
618
|
+
cell.title = display;
|
|
619
|
+
if (value.kind === "null") {
|
|
620
|
+
cell.className = "cell-null";
|
|
621
|
+
} else if (value.kind === "blob") {
|
|
622
|
+
cell.className = "cell-blob";
|
|
623
|
+
} else if (value.kind === "text" && value.truncated) {
|
|
624
|
+
cell.className = "cell-truncated";
|
|
625
|
+
}
|
|
626
|
+
return cell;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function renderRowActions(row: TableRow): HTMLTableCellElement {
|
|
630
|
+
const cell = document.createElement("td");
|
|
631
|
+
cell.className = "row-actions-cell";
|
|
632
|
+
const actions = document.createElement("div");
|
|
633
|
+
actions.className = "row-actions";
|
|
634
|
+
if (!row.locator) {
|
|
635
|
+
actions.textContent = text("empty.noLocator");
|
|
636
|
+
cell.append(actions);
|
|
637
|
+
return cell;
|
|
638
|
+
}
|
|
639
|
+
const key = locatorKey(row.locator);
|
|
640
|
+
if (pendingDeleteLocator === key) {
|
|
641
|
+
actions.append(
|
|
642
|
+
actionButton(text("action.confirm"), "button-danger compact-button", () => void deleteRow(row)),
|
|
643
|
+
actionButton(text("action.cancel"), "button-secondary compact-button", () => {
|
|
644
|
+
pendingDeleteLocator = "";
|
|
645
|
+
renderRows();
|
|
646
|
+
})
|
|
647
|
+
);
|
|
648
|
+
} else {
|
|
649
|
+
actions.append(
|
|
650
|
+
actionButton(text("action.edit"), "button-secondary compact-button", () => openEditor("update", row)),
|
|
651
|
+
actionButton(text("action.delete"), "button-secondary compact-button", () => {
|
|
652
|
+
pendingDeleteLocator = key;
|
|
653
|
+
renderRows();
|
|
654
|
+
})
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
cell.append(actions);
|
|
658
|
+
return cell;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function renderPager(): void {
|
|
662
|
+
const data = tableData;
|
|
663
|
+
const total = data?.totalRows || "0";
|
|
664
|
+
const start = data && data.rows.length > 0 ? data.offset + 1 : 0;
|
|
665
|
+
const end = data ? data.offset + data.rows.length : 0;
|
|
666
|
+
elements.pageLabel.textContent = text("pager.range", { start, end, total });
|
|
667
|
+
elements.previousPageButton.disabled = busy || !data?.hasPrevious;
|
|
668
|
+
elements.nextPageButton.disabled = busy || !data?.hasNext;
|
|
669
|
+
elements.pageSizeSelect.disabled = busy || !data;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function renderEditor(draft: Map<string, EditorDraftValue> = readEditorDraft()): void {
|
|
673
|
+
if (!editorMode || !tableData) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
elements.editorTitle.textContent = text(editorMode === "insert" ? "editor.insert" : "editor.update");
|
|
677
|
+
elements.saveRowButton.textContent = text(editorMode === "insert" ? "action.insert" : "action.save");
|
|
678
|
+
const currentValues = new Map((editingRow?.cells || []).map((cell) => [cell.column, cell.value]));
|
|
679
|
+
const fields = tableData.columns.filter((column) => column.insertable).map((column) => {
|
|
680
|
+
const field = document.createElement("div");
|
|
681
|
+
field.className = "editor-field";
|
|
682
|
+
field.dataset.column = column.name;
|
|
683
|
+
const heading = document.createElement("div");
|
|
684
|
+
heading.className = "field-heading";
|
|
685
|
+
const name = document.createElement("strong");
|
|
686
|
+
name.textContent = column.name;
|
|
687
|
+
heading.append(name, badge(column.declaredType || text("column.untyped")));
|
|
688
|
+
if (column.requiredOnInsert && editorMode === "insert") {
|
|
689
|
+
heading.append(badge(text("column.required")));
|
|
690
|
+
}
|
|
691
|
+
const modeSelect = document.createElement("select");
|
|
692
|
+
modeSelect.className = "field-mode";
|
|
693
|
+
modeSelect.setAttribute("aria-label", column.name + " mode");
|
|
694
|
+
const availableModes = editorMode === "insert"
|
|
695
|
+
? (["default", ...(column.nullable ? ["null"] : []), "value"] as string[])
|
|
696
|
+
: (["unchanged", ...(column.nullable ? ["null"] : []), "value"] as string[]);
|
|
697
|
+
const defaultMode = editorMode === "insert" && column.requiredOnInsert ? "value" : availableModes[0];
|
|
698
|
+
const savedDraft = draft.get(column.name);
|
|
699
|
+
const selectedMode = savedDraft && availableModes.includes(savedDraft.mode) ? savedDraft.mode : defaultMode;
|
|
700
|
+
for (const mode of availableModes) {
|
|
701
|
+
const option = document.createElement("option");
|
|
702
|
+
option.value = mode;
|
|
703
|
+
option.textContent = text(("mode." + mode) as MessageKey);
|
|
704
|
+
option.selected = mode === selectedMode;
|
|
705
|
+
modeSelect.append(option);
|
|
706
|
+
}
|
|
707
|
+
const input = column.affinity === "text" || column.affinity === "blob"
|
|
708
|
+
? document.createElement("textarea")
|
|
709
|
+
: document.createElement("input");
|
|
710
|
+
input.className = "field-input";
|
|
711
|
+
input.setAttribute("aria-label", column.name + " value");
|
|
712
|
+
input.dataset.affinity = column.affinity;
|
|
713
|
+
input.placeholder = text(column.affinity === "blob" ? "placeholder.hex" : "placeholder.value");
|
|
714
|
+
const currentValue = currentValues.get(column.name);
|
|
715
|
+
input.value = savedDraft?.value ?? editableValue(currentValue);
|
|
716
|
+
input.disabled = selectedMode !== "value" || busy;
|
|
717
|
+
modeSelect.disabled = busy;
|
|
718
|
+
modeSelect.addEventListener("change", () => {
|
|
719
|
+
input.disabled = modeSelect.value !== "value" || busy;
|
|
720
|
+
if (!input.disabled) {
|
|
721
|
+
input.focus();
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
field.append(heading, modeSelect, input);
|
|
725
|
+
if (editorMode === "update" && currentValue) {
|
|
726
|
+
const current = document.createElement("div");
|
|
727
|
+
current.className = "field-current";
|
|
728
|
+
current.textContent = text("editor.current", { value: displayValue(currentValue) });
|
|
729
|
+
current.title = displayValue(currentValue);
|
|
730
|
+
field.append(current);
|
|
731
|
+
}
|
|
732
|
+
return field;
|
|
733
|
+
});
|
|
734
|
+
elements.editorFields.replaceChildren(...fields);
|
|
735
|
+
renderBusyState();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function readEditorValues(): Record<string, { mode: string; value?: string }> {
|
|
739
|
+
const values: Record<string, { mode: string; value?: string }> = Object.create(null) as Record<string, { mode: string; value?: string }>;
|
|
740
|
+
elements.editorFields.querySelectorAll<HTMLElement>(".editor-field").forEach((field) => {
|
|
741
|
+
const column = field.dataset.column;
|
|
742
|
+
const mode = field.querySelector<HTMLSelectElement>(".field-mode");
|
|
743
|
+
const input = field.querySelector<HTMLInputElement | HTMLTextAreaElement>(".field-input");
|
|
744
|
+
if (column && mode && input) {
|
|
745
|
+
values[column] = mode.value === "value" ? { mode: mode.value, value: input.value } : { mode: mode.value };
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
return values;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function readEditorDraft(): Map<string, EditorDraftValue> {
|
|
752
|
+
const result = new Map<string, EditorDraftValue>();
|
|
753
|
+
elements.editorFields.querySelectorAll<HTMLElement>(".editor-field").forEach((field) => {
|
|
754
|
+
const column = field.dataset.column;
|
|
755
|
+
const mode = field.querySelector<HTMLSelectElement>(".field-mode");
|
|
756
|
+
const input = field.querySelector<HTMLInputElement | HTMLTextAreaElement>(".field-input");
|
|
757
|
+
if (column && mode && input) {
|
|
758
|
+
result.set(column, { mode: mode.value, value: input.value });
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
return result;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function editableValue(value: WireValue | undefined): string {
|
|
765
|
+
if (!value || value.kind === "null") {
|
|
766
|
+
return "";
|
|
767
|
+
}
|
|
768
|
+
if (value.kind === "integer" || value.kind === "real") {
|
|
769
|
+
return String(value.value);
|
|
770
|
+
}
|
|
771
|
+
if (value.kind === "text") {
|
|
772
|
+
return value.truncated ? "" : value.value;
|
|
773
|
+
}
|
|
774
|
+
return value.truncated ? "" : value.hex;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function displayValue(value: WireValue): string {
|
|
778
|
+
if (value.kind === "null") {
|
|
779
|
+
return text("value.null");
|
|
780
|
+
}
|
|
781
|
+
if (value.kind === "integer" || value.kind === "real") {
|
|
782
|
+
return String(value.value);
|
|
783
|
+
}
|
|
784
|
+
if (value.kind === "blob") {
|
|
785
|
+
return text("value.blob", { bytes: value.byteLength, preview: value.hex + (value.truncated ? "…" : "") });
|
|
786
|
+
}
|
|
787
|
+
return value.truncated ? text("value.truncated", { value: value.value, length: value.length }) : value.value;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function badge(value: string): HTMLElement {
|
|
791
|
+
const element = document.createElement("span");
|
|
792
|
+
element.className = "column-badge";
|
|
793
|
+
element.textContent = value;
|
|
794
|
+
element.title = value;
|
|
795
|
+
return element;
|
|
294
796
|
}
|
|
295
797
|
|
|
296
798
|
function actionButton(label: string, className: string, action: () => void): HTMLButtonElement {
|
|
@@ -303,18 +805,8 @@ function actionButton(label: string, className: string, action: () => void): HTM
|
|
|
303
805
|
return button;
|
|
304
806
|
}
|
|
305
807
|
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
elements.titleInput.value = "";
|
|
309
|
-
elements.noteInput.value = "";
|
|
310
|
-
elements.cancelEditButton.hidden = true;
|
|
311
|
-
clearFormError();
|
|
312
|
-
renderEntryForm();
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function renderEntryForm(): void {
|
|
316
|
-
elements.entryFormTitle.textContent = text(editingRecord ? "form.editRow" : "form.insertRow");
|
|
317
|
-
elements.insertButton.textContent = text(editingRecord ? "action.save" : "action.insert");
|
|
808
|
+
function locatorKey(locator: RowLocator | undefined): string {
|
|
809
|
+
return locator ? JSON.stringify(locator) : "";
|
|
318
810
|
}
|
|
319
811
|
|
|
320
812
|
function setBusy(nextBusy: boolean, labelKey: MessageKey): void {
|
|
@@ -325,18 +817,29 @@ function setBusy(nextBusy: boolean, labelKey: MessageKey): void {
|
|
|
325
817
|
|
|
326
818
|
function renderBusyState(): void {
|
|
327
819
|
elements.loadingState.textContent = text(busyLabelKey);
|
|
820
|
+
elements.openDatabaseButton.disabled = busy;
|
|
821
|
+
elements.useDefaultDatabaseButton.disabled = busy || databaseInfo?.source === "default";
|
|
328
822
|
elements.refreshButton.disabled = busy;
|
|
329
|
-
elements.
|
|
823
|
+
elements.tableSelect.disabled = busy || tables.length === 0;
|
|
824
|
+
elements.addRowButton.disabled = busy || !tableData?.writable;
|
|
825
|
+
elements.saveRowButton.disabled = busy;
|
|
826
|
+
elements.cancelEditorButton.disabled = busy;
|
|
827
|
+
elements.closeEditorButton.disabled = busy;
|
|
828
|
+
elements.pageSizeSelect.disabled = busy || !tableData;
|
|
829
|
+
elements.previousPageButton.disabled = busy || !tableData?.hasPrevious;
|
|
830
|
+
elements.nextPageButton.disabled = busy || !tableData?.hasNext;
|
|
831
|
+
elements.editorFields.querySelectorAll<HTMLSelectElement>("select").forEach((select) => {
|
|
832
|
+
select.disabled = busy;
|
|
833
|
+
});
|
|
834
|
+
elements.editorFields.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>("input, textarea").forEach((input) => {
|
|
835
|
+
const mode = input.closest(".editor-field")?.querySelector<HTMLSelectElement>(".field-mode")?.value;
|
|
836
|
+
input.disabled = busy || mode !== "value";
|
|
837
|
+
});
|
|
330
838
|
elements.recordsBody.querySelectorAll<HTMLButtonElement>("button").forEach((button) => {
|
|
331
839
|
button.disabled = busy;
|
|
332
840
|
});
|
|
333
841
|
}
|
|
334
842
|
|
|
335
|
-
function showFormErrorMessage(key: MessageKey, params?: MessageParams): void {
|
|
336
|
-
formErrorState = { kind: "message", key, params };
|
|
337
|
-
renderFormError();
|
|
338
|
-
}
|
|
339
|
-
|
|
340
843
|
function showFormErrorLiteral(value: string): void {
|
|
341
844
|
formErrorState = { kind: "literal", value };
|
|
342
845
|
renderFormError();
|
|
@@ -362,16 +865,17 @@ function writeStatusMessage(key: MessageKey, params?: MessageParams): void {
|
|
|
362
865
|
renderStatus();
|
|
363
866
|
}
|
|
364
867
|
|
|
365
|
-
function
|
|
366
|
-
|
|
868
|
+
function writeStatusLiteral(value: string): void {
|
|
869
|
+
statusState = { kind: "literal", value };
|
|
870
|
+
renderStatus();
|
|
367
871
|
}
|
|
368
872
|
|
|
369
|
-
function
|
|
370
|
-
elements.
|
|
371
|
-
elements.databasePath.title = databasePath;
|
|
873
|
+
function renderStatus(): void {
|
|
874
|
+
elements.statusLog.textContent = renderState(statusState);
|
|
372
875
|
}
|
|
373
876
|
|
|
374
877
|
function applyLanguage(nextLanguage: SupportedLanguage): void {
|
|
878
|
+
const editorDraft = readEditorDraft();
|
|
375
879
|
language = nextLanguage;
|
|
376
880
|
document.documentElement.lang = language;
|
|
377
881
|
document.querySelectorAll<HTMLElement>("[data-i18n]").forEach((element) => {
|
|
@@ -386,9 +890,12 @@ function applyLanguage(nextLanguage: SupportedLanguage): void {
|
|
|
386
890
|
element.setAttribute("aria-label", text(key));
|
|
387
891
|
}
|
|
388
892
|
});
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
893
|
+
renderDatabaseInfo();
|
|
894
|
+
renderTableSelector();
|
|
895
|
+
renderTableData();
|
|
896
|
+
if (editorMode) {
|
|
897
|
+
renderEditor(editorDraft);
|
|
898
|
+
}
|
|
392
899
|
renderBusyState();
|
|
393
900
|
renderStatus();
|
|
394
901
|
renderFormError();
|