@aiot-nexus/plugin-sdk 2.0.2 → 2.1.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.
- package/README.md +18 -8
- package/dev-host/host.html +13 -12
- package/dev-host/main.js +134 -9
- package/dev-host/plugin-host.js +1 -427
- 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/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/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 +84 -3
- package/templates/minimal/src/main.ts.tmpl +2 -1
- package/templates/sqlite/README.md.tmpl +30 -2
- package/templates/sqlite/src/better-sqlite3.d.ts +12 -1
- package/templates/sqlite/src/main.ts.tmpl +1034 -124
- package/templates/sqlite/src/renderer/app.ts.tmpl +691 -180
- 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,476 @@ 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<boolean> {
|
|
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
|
+
}
|
|
354
|
+
return true;
|
|
194
355
|
} catch (error: unknown) {
|
|
356
|
+
tableData = undefined;
|
|
357
|
+
renderTableData();
|
|
195
358
|
writeStatusMessage("error.refreshFailed", { message: errorMessage(error) });
|
|
359
|
+
return false;
|
|
196
360
|
} finally {
|
|
197
361
|
setBusy(false, "status.idle");
|
|
198
362
|
}
|
|
199
363
|
}
|
|
200
364
|
|
|
201
|
-
async function
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
365
|
+
async function openDatabase(): Promise<void> {
|
|
366
|
+
setBusy(true, "status.opening");
|
|
367
|
+
try {
|
|
368
|
+
const result = await invoke<{ canceled: boolean; database: DatabaseInfo }>("sqlite.chooseDatabase");
|
|
369
|
+
if (result.canceled) {
|
|
370
|
+
writeStatusMessage("status.selectionCanceled");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
closeEditor();
|
|
374
|
+
selectedTable = "";
|
|
375
|
+
offset = 0;
|
|
376
|
+
if (await refreshDatabase(false)) {
|
|
377
|
+
writeStatusMessage("status.databaseOpened", { name: result.database.databaseName });
|
|
378
|
+
}
|
|
379
|
+
} catch (error: unknown) {
|
|
380
|
+
writeStatusMessage("error.operationFailed", { message: errorMessage(error) });
|
|
381
|
+
} finally {
|
|
382
|
+
setBusy(false, "status.idle");
|
|
208
383
|
}
|
|
209
|
-
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function useDefaultDatabase(): Promise<void> {
|
|
387
|
+
setBusy(true, "status.switching");
|
|
210
388
|
try {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
writeStatusMessage("status.
|
|
389
|
+
await invoke<{ database: DatabaseInfo }>("sqlite.useDefaultDatabase");
|
|
390
|
+
closeEditor();
|
|
391
|
+
selectedTable = "plugin_records";
|
|
392
|
+
offset = 0;
|
|
393
|
+
if (await refreshDatabase(true)) {
|
|
394
|
+
writeStatusMessage("status.defaultOpened");
|
|
217
395
|
}
|
|
218
|
-
resetEntryForm();
|
|
219
|
-
await refreshAll();
|
|
220
396
|
} catch (error: unknown) {
|
|
221
|
-
|
|
397
|
+
writeStatusMessage("error.operationFailed", { message: errorMessage(error) });
|
|
222
398
|
} finally {
|
|
223
399
|
setBusy(false, "status.idle");
|
|
224
400
|
}
|
|
225
401
|
}
|
|
226
402
|
|
|
227
|
-
function
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
403
|
+
async function loadSelectedTable(): Promise<void> {
|
|
404
|
+
setBusy(true, "status.loading");
|
|
405
|
+
try {
|
|
406
|
+
await readSelectedTable();
|
|
407
|
+
writeStatusMessage("status.ready");
|
|
408
|
+
} catch (error: unknown) {
|
|
409
|
+
tableData = undefined;
|
|
410
|
+
renderTableData();
|
|
411
|
+
writeStatusMessage("error.refreshFailed", { message: errorMessage(error) });
|
|
412
|
+
} finally {
|
|
413
|
+
setBusy(false, "status.idle");
|
|
414
|
+
}
|
|
236
415
|
}
|
|
237
416
|
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
|
|
417
|
+
async function readSelectedTable(): Promise<void> {
|
|
418
|
+
if (!selectedTable) {
|
|
419
|
+
tableData = undefined;
|
|
420
|
+
renderTableData();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
tableData = await invoke<TableData>("sqlite.readTable", {
|
|
424
|
+
table: selectedTable,
|
|
425
|
+
offset,
|
|
426
|
+
limit: pageSize
|
|
427
|
+
});
|
|
428
|
+
offset = tableData.offset;
|
|
429
|
+
renderTableData();
|
|
241
430
|
}
|
|
242
431
|
|
|
243
|
-
|
|
432
|
+
function openEditor(mode: EditorMode, row?: TableRow): void {
|
|
433
|
+
if (!tableData || !tableData.writable) {
|
|
434
|
+
writeStatusMessage("empty.noWritableColumns");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (mode === "update" && !row?.locator) {
|
|
438
|
+
writeStatusMessage("empty.noLocator");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
editorMode = mode;
|
|
442
|
+
editingRow = row;
|
|
443
|
+
pendingDeleteLocator = "";
|
|
444
|
+
clearFormError();
|
|
445
|
+
renderEditor();
|
|
446
|
+
elements.editorPanel.hidden = false;
|
|
447
|
+
elements.editorPanel.scrollIntoView({ block: "nearest" });
|
|
448
|
+
client.notifyResize();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function closeEditor(): void {
|
|
452
|
+
editorMode = undefined;
|
|
453
|
+
editingRow = undefined;
|
|
454
|
+
clearFormError();
|
|
455
|
+
elements.editorPanel.hidden = true;
|
|
456
|
+
elements.editorFields.replaceChildren();
|
|
457
|
+
client.notifyResize();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function saveRow(): Promise<void> {
|
|
461
|
+
if (!tableData || !editorMode) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
clearFormError();
|
|
465
|
+
const values = readEditorValues();
|
|
466
|
+
setBusy(true, editorMode === "insert" ? "status.inserting" : "status.updating");
|
|
467
|
+
try {
|
|
468
|
+
let successKey: MessageKey;
|
|
469
|
+
try {
|
|
470
|
+
if (editorMode === "insert") {
|
|
471
|
+
await invoke("sqlite.insertRow", { table: selectedTable, values });
|
|
472
|
+
successKey = "status.insertedRow";
|
|
473
|
+
} else {
|
|
474
|
+
if (!editingRow?.locator) {
|
|
475
|
+
throw new Error(text("empty.noLocator"));
|
|
476
|
+
}
|
|
477
|
+
await invoke("sqlite.updateRow", { table: selectedTable, locator: editingRow.locator, values });
|
|
478
|
+
successKey = "status.updatedRow";
|
|
479
|
+
}
|
|
480
|
+
} catch (error: unknown) {
|
|
481
|
+
showFormErrorLiteral(errorMessage(error));
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
closeEditor();
|
|
485
|
+
await refreshAfterMutation(successKey, { table: selectedTable });
|
|
486
|
+
} finally {
|
|
487
|
+
setBusy(false, "status.idle");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function deleteRow(row: TableRow): Promise<void> {
|
|
492
|
+
if (!row.locator) {
|
|
493
|
+
writeStatusMessage("empty.noLocator");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
244
496
|
setBusy(true, "status.deleting");
|
|
245
497
|
try {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
498
|
+
try {
|
|
499
|
+
await invoke("sqlite.deleteRow", { table: selectedTable, locator: row.locator });
|
|
500
|
+
} catch (error: unknown) {
|
|
501
|
+
writeStatusMessage("error.operationFailed", { message: errorMessage(error) });
|
|
502
|
+
return;
|
|
249
503
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
504
|
+
pendingDeleteLocator = "";
|
|
505
|
+
if (editingRow && locatorKey(editingRow.locator) === locatorKey(row.locator)) {
|
|
506
|
+
closeEditor();
|
|
507
|
+
}
|
|
508
|
+
await refreshAfterMutation("status.deletedRow", { table: selectedTable });
|
|
255
509
|
} finally {
|
|
256
510
|
setBusy(false, "status.idle");
|
|
257
511
|
}
|
|
258
512
|
}
|
|
259
513
|
|
|
260
|
-
function
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
514
|
+
async function refreshAfterMutation(successKey: MessageKey, params: MessageParams): Promise<void> {
|
|
515
|
+
const operation = text(successKey, params);
|
|
516
|
+
try {
|
|
517
|
+
await readSelectedTable();
|
|
518
|
+
writeStatusMessage(successKey, params);
|
|
519
|
+
} catch (error: unknown) {
|
|
520
|
+
writeStatusMessage("status.writeSucceededRefreshFailed", {
|
|
521
|
+
operation,
|
|
522
|
+
message: errorMessage(error)
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function renderDatabaseInfo(): void {
|
|
528
|
+
elements.databaseName.textContent = databaseInfo?.databaseName || "-";
|
|
529
|
+
elements.databaseSource.textContent = databaseInfo ? text(databaseInfo.source === "default" ? "source.default" : "source.external") : "-";
|
|
530
|
+
elements.databasePath.textContent = databaseInfo?.databasePath || text("status.connecting");
|
|
531
|
+
elements.databasePath.title = databaseInfo?.databasePath || "";
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function renderTableSelector(): void {
|
|
535
|
+
const options = tables.map((table) => {
|
|
536
|
+
const option = document.createElement("option");
|
|
537
|
+
option.value = table.name;
|
|
538
|
+
option.textContent = table.name;
|
|
539
|
+
option.selected = table.name === selectedTable;
|
|
540
|
+
return option;
|
|
541
|
+
});
|
|
542
|
+
elements.tableSelect.replaceChildren(...options);
|
|
543
|
+
elements.tableSelect.disabled = busy || tables.length === 0;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function renderTableData(): void {
|
|
547
|
+
const data = tableData;
|
|
548
|
+
elements.columnCount.textContent = data ? String(data.columns.filter((column) => !column.hidden).length) : "0";
|
|
549
|
+
elements.recordCount.textContent = data?.totalRows || "0";
|
|
550
|
+
renderTableHeader();
|
|
551
|
+
renderRows();
|
|
552
|
+
renderPager();
|
|
553
|
+
elements.addRowButton.disabled = busy || !data?.writable;
|
|
554
|
+
if (!selectedTable) {
|
|
555
|
+
elements.emptyState.textContent = text("empty.noTables");
|
|
556
|
+
elements.emptyState.hidden = false;
|
|
557
|
+
} else {
|
|
558
|
+
elements.emptyState.textContent = text("empty.noRows");
|
|
559
|
+
elements.emptyState.hidden = Boolean(data && data.rows.length > 0);
|
|
560
|
+
}
|
|
561
|
+
client.notifyResize();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function renderTableHeader(): void {
|
|
565
|
+
if (!tableData) {
|
|
566
|
+
elements.recordsHead.replaceChildren();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const headers = tableData.columns.filter((column) => !column.hidden).map((column) => {
|
|
570
|
+
const header = document.createElement("th");
|
|
571
|
+
const title = document.createElement("div");
|
|
572
|
+
title.className = "column-title";
|
|
573
|
+
const name = document.createElement("strong");
|
|
574
|
+
name.textContent = column.name;
|
|
575
|
+
title.append(name, badge(column.declaredType || text("column.untyped")));
|
|
576
|
+
if (column.primaryKeyOrder > 0) {
|
|
577
|
+
title.append(badge(text("column.primaryKey", { order: column.primaryKeyOrder })));
|
|
578
|
+
}
|
|
579
|
+
if (!column.nullable) {
|
|
580
|
+
title.append(badge(text("column.notNull")));
|
|
581
|
+
}
|
|
582
|
+
if (column.defaultSql !== null) {
|
|
583
|
+
title.append(badge(text("column.default", { value: column.defaultSql })));
|
|
282
584
|
}
|
|
283
|
-
|
|
284
|
-
|
|
585
|
+
if (column.generated) {
|
|
586
|
+
title.append(badge(text("column.generated")));
|
|
587
|
+
}
|
|
588
|
+
header.append(title);
|
|
589
|
+
return header;
|
|
590
|
+
});
|
|
591
|
+
if (tableData.locatorKind) {
|
|
592
|
+
const actions = document.createElement("th");
|
|
593
|
+
actions.className = "actions-column";
|
|
594
|
+
actions.textContent = text("table.actions");
|
|
595
|
+
headers.push(actions);
|
|
596
|
+
}
|
|
597
|
+
elements.recordsHead.replaceChildren(...headers);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function renderRows(): void {
|
|
601
|
+
if (!tableData) {
|
|
602
|
+
elements.recordsBody.replaceChildren();
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const rows = tableData.rows.map((row) => {
|
|
606
|
+
const tableRow = document.createElement("tr");
|
|
607
|
+
for (const cell of row.cells) {
|
|
608
|
+
tableRow.append(renderCell(cell.value));
|
|
609
|
+
}
|
|
610
|
+
if (tableData?.locatorKind) {
|
|
611
|
+
tableRow.append(renderRowActions(row));
|
|
612
|
+
}
|
|
613
|
+
return tableRow;
|
|
285
614
|
});
|
|
286
615
|
elements.recordsBody.replaceChildren(...rows);
|
|
287
|
-
elements.emptyState.hidden = rows.length > 0;
|
|
288
616
|
}
|
|
289
617
|
|
|
290
|
-
function
|
|
618
|
+
function renderCell(value: WireValue): HTMLTableCellElement {
|
|
291
619
|
const cell = document.createElement("td");
|
|
292
|
-
|
|
293
|
-
|
|
620
|
+
const display = displayValue(value);
|
|
621
|
+
cell.textContent = display;
|
|
622
|
+
cell.title = display;
|
|
623
|
+
if (value.kind === "null") {
|
|
624
|
+
cell.className = "cell-null";
|
|
625
|
+
} else if (value.kind === "blob") {
|
|
626
|
+
cell.className = "cell-blob";
|
|
627
|
+
} else if (value.kind === "text" && value.truncated) {
|
|
628
|
+
cell.className = "cell-truncated";
|
|
629
|
+
}
|
|
630
|
+
return cell;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function renderRowActions(row: TableRow): HTMLTableCellElement {
|
|
634
|
+
const cell = document.createElement("td");
|
|
635
|
+
cell.className = "row-actions-cell";
|
|
636
|
+
const actions = document.createElement("div");
|
|
637
|
+
actions.className = "row-actions";
|
|
638
|
+
if (!row.locator) {
|
|
639
|
+
actions.textContent = text("empty.noLocator");
|
|
640
|
+
cell.append(actions);
|
|
641
|
+
return cell;
|
|
642
|
+
}
|
|
643
|
+
const key = locatorKey(row.locator);
|
|
644
|
+
if (pendingDeleteLocator === key) {
|
|
645
|
+
actions.append(
|
|
646
|
+
actionButton(text("action.confirm"), "button-danger compact-button", () => void deleteRow(row)),
|
|
647
|
+
actionButton(text("action.cancel"), "button-secondary compact-button", () => {
|
|
648
|
+
pendingDeleteLocator = "";
|
|
649
|
+
renderRows();
|
|
650
|
+
})
|
|
651
|
+
);
|
|
652
|
+
} else {
|
|
653
|
+
actions.append(
|
|
654
|
+
actionButton(text("action.edit"), "button-secondary compact-button", () => openEditor("update", row)),
|
|
655
|
+
actionButton(text("action.delete"), "button-secondary compact-button", () => {
|
|
656
|
+
pendingDeleteLocator = key;
|
|
657
|
+
renderRows();
|
|
658
|
+
})
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
cell.append(actions);
|
|
662
|
+
return cell;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function renderPager(): void {
|
|
666
|
+
const data = tableData;
|
|
667
|
+
const total = data?.totalRows || "0";
|
|
668
|
+
const start = data && data.rows.length > 0 ? data.offset + 1 : 0;
|
|
669
|
+
const end = data ? data.offset + data.rows.length : 0;
|
|
670
|
+
elements.pageLabel.textContent = text("pager.range", { start, end, total });
|
|
671
|
+
elements.previousPageButton.disabled = busy || !data?.hasPrevious;
|
|
672
|
+
elements.nextPageButton.disabled = busy || !data?.hasNext;
|
|
673
|
+
elements.pageSizeSelect.disabled = busy || !data;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function renderEditor(draft: Map<string, EditorDraftValue> = readEditorDraft()): void {
|
|
677
|
+
if (!editorMode || !tableData) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
elements.editorTitle.textContent = text(editorMode === "insert" ? "editor.insert" : "editor.update");
|
|
681
|
+
elements.saveRowButton.textContent = text(editorMode === "insert" ? "action.insert" : "action.save");
|
|
682
|
+
const currentValues = new Map((editingRow?.cells || []).map((cell) => [cell.column, cell.value]));
|
|
683
|
+
const fields = tableData.columns.filter((column) => column.insertable).map((column) => {
|
|
684
|
+
const field = document.createElement("div");
|
|
685
|
+
field.className = "editor-field";
|
|
686
|
+
field.dataset.column = column.name;
|
|
687
|
+
const heading = document.createElement("div");
|
|
688
|
+
heading.className = "field-heading";
|
|
689
|
+
const name = document.createElement("strong");
|
|
690
|
+
name.textContent = column.name;
|
|
691
|
+
heading.append(name, badge(column.declaredType || text("column.untyped")));
|
|
692
|
+
if (column.requiredOnInsert && editorMode === "insert") {
|
|
693
|
+
heading.append(badge(text("column.required")));
|
|
694
|
+
}
|
|
695
|
+
const modeSelect = document.createElement("select");
|
|
696
|
+
modeSelect.className = "field-mode";
|
|
697
|
+
modeSelect.setAttribute("aria-label", column.name + " mode");
|
|
698
|
+
const availableModes = editorMode === "insert"
|
|
699
|
+
? (["default", ...(column.nullable ? ["null"] : []), "value"] as string[])
|
|
700
|
+
: (["unchanged", ...(column.nullable ? ["null"] : []), "value"] as string[]);
|
|
701
|
+
const defaultMode = editorMode === "insert" && column.requiredOnInsert ? "value" : availableModes[0];
|
|
702
|
+
const savedDraft = draft.get(column.name);
|
|
703
|
+
const selectedMode = savedDraft && availableModes.includes(savedDraft.mode) ? savedDraft.mode : defaultMode;
|
|
704
|
+
for (const mode of availableModes) {
|
|
705
|
+
const option = document.createElement("option");
|
|
706
|
+
option.value = mode;
|
|
707
|
+
option.textContent = text(("mode." + mode) as MessageKey);
|
|
708
|
+
option.selected = mode === selectedMode;
|
|
709
|
+
modeSelect.append(option);
|
|
710
|
+
}
|
|
711
|
+
const input = column.affinity === "text" || column.affinity === "blob"
|
|
712
|
+
? document.createElement("textarea")
|
|
713
|
+
: document.createElement("input");
|
|
714
|
+
input.className = "field-input";
|
|
715
|
+
input.setAttribute("aria-label", column.name + " value");
|
|
716
|
+
input.dataset.affinity = column.affinity;
|
|
717
|
+
input.placeholder = text(column.affinity === "blob" ? "placeholder.hex" : "placeholder.value");
|
|
718
|
+
const currentValue = currentValues.get(column.name);
|
|
719
|
+
input.value = savedDraft?.value ?? editableValue(currentValue);
|
|
720
|
+
input.disabled = selectedMode !== "value" || busy;
|
|
721
|
+
modeSelect.disabled = busy;
|
|
722
|
+
modeSelect.addEventListener("change", () => {
|
|
723
|
+
input.disabled = modeSelect.value !== "value" || busy;
|
|
724
|
+
if (!input.disabled) {
|
|
725
|
+
input.focus();
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
field.append(heading, modeSelect, input);
|
|
729
|
+
if (editorMode === "update" && currentValue) {
|
|
730
|
+
const current = document.createElement("div");
|
|
731
|
+
current.className = "field-current";
|
|
732
|
+
current.textContent = text("editor.current", { value: displayValue(currentValue) });
|
|
733
|
+
current.title = displayValue(currentValue);
|
|
734
|
+
field.append(current);
|
|
735
|
+
}
|
|
736
|
+
return field;
|
|
737
|
+
});
|
|
738
|
+
elements.editorFields.replaceChildren(...fields);
|
|
739
|
+
renderBusyState();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function readEditorValues(): Record<string, { mode: string; value?: string }> {
|
|
743
|
+
const values: Record<string, { mode: string; value?: string }> = Object.create(null) as Record<string, { mode: string; value?: string }>;
|
|
744
|
+
elements.editorFields.querySelectorAll<HTMLElement>(".editor-field").forEach((field) => {
|
|
745
|
+
const column = field.dataset.column;
|
|
746
|
+
const mode = field.querySelector<HTMLSelectElement>(".field-mode");
|
|
747
|
+
const input = field.querySelector<HTMLInputElement | HTMLTextAreaElement>(".field-input");
|
|
748
|
+
if (column && mode && input) {
|
|
749
|
+
values[column] = mode.value === "value" ? { mode: mode.value, value: input.value } : { mode: mode.value };
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
return values;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function readEditorDraft(): Map<string, EditorDraftValue> {
|
|
756
|
+
const result = new Map<string, EditorDraftValue>();
|
|
757
|
+
elements.editorFields.querySelectorAll<HTMLElement>(".editor-field").forEach((field) => {
|
|
758
|
+
const column = field.dataset.column;
|
|
759
|
+
const mode = field.querySelector<HTMLSelectElement>(".field-mode");
|
|
760
|
+
const input = field.querySelector<HTMLInputElement | HTMLTextAreaElement>(".field-input");
|
|
761
|
+
if (column && mode && input) {
|
|
762
|
+
result.set(column, { mode: mode.value, value: input.value });
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
return result;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function editableValue(value: WireValue | undefined): string {
|
|
769
|
+
if (!value || value.kind === "null") {
|
|
770
|
+
return "";
|
|
771
|
+
}
|
|
772
|
+
if (value.kind === "integer" || value.kind === "real") {
|
|
773
|
+
return String(value.value);
|
|
774
|
+
}
|
|
775
|
+
if (value.kind === "text") {
|
|
776
|
+
return value.truncated ? "" : value.value;
|
|
777
|
+
}
|
|
778
|
+
return value.truncated ? "" : value.hex;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function displayValue(value: WireValue): string {
|
|
782
|
+
if (value.kind === "null") {
|
|
783
|
+
return text("value.null");
|
|
784
|
+
}
|
|
785
|
+
if (value.kind === "integer" || value.kind === "real") {
|
|
786
|
+
return String(value.value);
|
|
787
|
+
}
|
|
788
|
+
if (value.kind === "blob") {
|
|
789
|
+
return text("value.blob", { bytes: value.byteLength, preview: value.hex + (value.truncated ? "…" : "") });
|
|
790
|
+
}
|
|
791
|
+
return value.truncated ? text("value.truncated", { value: value.value, length: value.length }) : value.value;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function badge(value: string): HTMLElement {
|
|
795
|
+
const element = document.createElement("span");
|
|
796
|
+
element.className = "column-badge";
|
|
797
|
+
element.textContent = value;
|
|
798
|
+
element.title = value;
|
|
799
|
+
return element;
|
|
294
800
|
}
|
|
295
801
|
|
|
296
802
|
function actionButton(label: string, className: string, action: () => void): HTMLButtonElement {
|
|
@@ -303,18 +809,8 @@ function actionButton(label: string, className: string, action: () => void): HTM
|
|
|
303
809
|
return button;
|
|
304
810
|
}
|
|
305
811
|
|
|
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");
|
|
812
|
+
function locatorKey(locator: RowLocator | undefined): string {
|
|
813
|
+
return locator ? JSON.stringify(locator) : "";
|
|
318
814
|
}
|
|
319
815
|
|
|
320
816
|
function setBusy(nextBusy: boolean, labelKey: MessageKey): void {
|
|
@@ -325,18 +821,29 @@ function setBusy(nextBusy: boolean, labelKey: MessageKey): void {
|
|
|
325
821
|
|
|
326
822
|
function renderBusyState(): void {
|
|
327
823
|
elements.loadingState.textContent = text(busyLabelKey);
|
|
824
|
+
elements.openDatabaseButton.disabled = busy;
|
|
825
|
+
elements.useDefaultDatabaseButton.disabled = busy || databaseInfo?.source === "default";
|
|
328
826
|
elements.refreshButton.disabled = busy;
|
|
329
|
-
elements.
|
|
827
|
+
elements.tableSelect.disabled = busy || tables.length === 0;
|
|
828
|
+
elements.addRowButton.disabled = busy || !tableData?.writable;
|
|
829
|
+
elements.saveRowButton.disabled = busy;
|
|
830
|
+
elements.cancelEditorButton.disabled = busy;
|
|
831
|
+
elements.closeEditorButton.disabled = busy;
|
|
832
|
+
elements.pageSizeSelect.disabled = busy || !tableData;
|
|
833
|
+
elements.previousPageButton.disabled = busy || !tableData?.hasPrevious;
|
|
834
|
+
elements.nextPageButton.disabled = busy || !tableData?.hasNext;
|
|
835
|
+
elements.editorFields.querySelectorAll<HTMLSelectElement>("select").forEach((select) => {
|
|
836
|
+
select.disabled = busy;
|
|
837
|
+
});
|
|
838
|
+
elements.editorFields.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>("input, textarea").forEach((input) => {
|
|
839
|
+
const mode = input.closest(".editor-field")?.querySelector<HTMLSelectElement>(".field-mode")?.value;
|
|
840
|
+
input.disabled = busy || mode !== "value";
|
|
841
|
+
});
|
|
330
842
|
elements.recordsBody.querySelectorAll<HTMLButtonElement>("button").forEach((button) => {
|
|
331
843
|
button.disabled = busy;
|
|
332
844
|
});
|
|
333
845
|
}
|
|
334
846
|
|
|
335
|
-
function showFormErrorMessage(key: MessageKey, params?: MessageParams): void {
|
|
336
|
-
formErrorState = { kind: "message", key, params };
|
|
337
|
-
renderFormError();
|
|
338
|
-
}
|
|
339
|
-
|
|
340
847
|
function showFormErrorLiteral(value: string): void {
|
|
341
848
|
formErrorState = { kind: "literal", value };
|
|
342
849
|
renderFormError();
|
|
@@ -362,16 +869,17 @@ function writeStatusMessage(key: MessageKey, params?: MessageParams): void {
|
|
|
362
869
|
renderStatus();
|
|
363
870
|
}
|
|
364
871
|
|
|
365
|
-
function
|
|
366
|
-
|
|
872
|
+
function writeStatusLiteral(value: string): void {
|
|
873
|
+
statusState = { kind: "literal", value };
|
|
874
|
+
renderStatus();
|
|
367
875
|
}
|
|
368
876
|
|
|
369
|
-
function
|
|
370
|
-
elements.
|
|
371
|
-
elements.databasePath.title = databasePath;
|
|
877
|
+
function renderStatus(): void {
|
|
878
|
+
elements.statusLog.textContent = renderState(statusState);
|
|
372
879
|
}
|
|
373
880
|
|
|
374
881
|
function applyLanguage(nextLanguage: SupportedLanguage): void {
|
|
882
|
+
const editorDraft = readEditorDraft();
|
|
375
883
|
language = nextLanguage;
|
|
376
884
|
document.documentElement.lang = language;
|
|
377
885
|
document.querySelectorAll<HTMLElement>("[data-i18n]").forEach((element) => {
|
|
@@ -386,9 +894,12 @@ function applyLanguage(nextLanguage: SupportedLanguage): void {
|
|
|
386
894
|
element.setAttribute("aria-label", text(key));
|
|
387
895
|
}
|
|
388
896
|
});
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
897
|
+
renderDatabaseInfo();
|
|
898
|
+
renderTableSelector();
|
|
899
|
+
renderTableData();
|
|
900
|
+
if (editorMode) {
|
|
901
|
+
renderEditor(editorDraft);
|
|
902
|
+
}
|
|
392
903
|
renderBusyState();
|
|
393
904
|
renderStatus();
|
|
394
905
|
renderFormError();
|