4bnode 4.0.3 → 4.0.5
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 +21 -9
- package/add-crud.js +1 -1
- package/add-mongo-update.js +1 -1
- package/index.js +4 -6
- package/lib/ui.js +0 -1
- package/package.json +2 -2
- package/skeleton/.4bnode/assets/js/App.js +4 -0
- package/skeleton/.4bnode/assets/js/components/Sidebar.js +3 -1
- package/skeleton/.4bnode/assets/js/lib.js +20 -0
- package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +55 -36
- package/skeleton/.4bnode/assets/js/pages/LogsPage.js +205 -0
- package/skeleton/.4bnode/assets/js/pages/ReportsPage.js +667 -0
- package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +176 -147
- package/skeleton/.4bnode/assets/js/pages/SchemasPage.js +131 -4
- package/skeleton/.4bnode/dev-api.js +774 -39
- package/skeleton/.env.example +29 -0
- package/skeleton/README.md +65 -0
- package/skeleton/index.js +7 -1
- package/skeleton/.env.development +0 -1
- package/skeleton/.env.production +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef } from "react";
|
|
2
2
|
import { LuCheck, LuCopy, LuPlug, LuShare2 } from "react-icons/lu";
|
|
3
|
-
import { html, api, Icons } from "../lib.js";
|
|
3
|
+
import { html, api, Icons, copyToClipboard } from "../lib.js";
|
|
4
4
|
import { useApp } from "../context.js";
|
|
5
5
|
|
|
6
6
|
export function RoutesPage() {
|
|
@@ -162,7 +162,7 @@ export function RoutesPage() {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
function copyShareMarkdown() {
|
|
165
|
-
|
|
165
|
+
copyToClipboard(generateShareMarkdown());
|
|
166
166
|
setShareCopied(true);
|
|
167
167
|
toast('Copied to clipboard', 'success');
|
|
168
168
|
setTimeout(() => setShareCopied(false), 2000);
|
|
@@ -986,11 +986,11 @@ function CreateRoutePanel({ onCreated }) {
|
|
|
986
986
|
registerFields:
|
|
987
987
|
routeType === "register" ? registerFields : undefined,
|
|
988
988
|
selectedFields:
|
|
989
|
-
routeType === "methods" && selectedFields.length > 0
|
|
989
|
+
(routeType === "methods" || routeType === "crud") && selectedFields.length > 0
|
|
990
990
|
? selectedFields
|
|
991
991
|
: undefined,
|
|
992
992
|
customFields:
|
|
993
|
-
routeType === "methods" && customFields.length > 0
|
|
993
|
+
(routeType === "methods" || routeType === "crud") && customFields.length > 0
|
|
994
994
|
? customFields
|
|
995
995
|
: undefined,
|
|
996
996
|
},
|
|
@@ -1132,95 +1132,194 @@ function CreateRoutePanel({ onCreated }) {
|
|
|
1132
1132
|
)}
|
|
1133
1133
|
</div>
|
|
1134
1134
|
</div>`}
|
|
1135
|
-
${routeType === "
|
|
1135
|
+
${routeType === "crud" &&
|
|
1136
1136
|
html`<div className="form-group">
|
|
1137
1137
|
<label className="form-label"
|
|
1138
|
-
>
|
|
1138
|
+
>CRUD Methods
|
|
1139
1139
|
<span style=${{ color: "var(--text-muted)", fontWeight: 400 }}
|
|
1140
|
-
>(
|
|
1141
|
-
request)</span
|
|
1140
|
+
>(select which operations to include)</span
|
|
1142
1141
|
></label
|
|
1143
1142
|
>
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1143
|
+
<div className="flex gap-2">
|
|
1144
|
+
${["get", "post", "put", "patch", "delete"].map((m) => {
|
|
1145
|
+
const labels = {
|
|
1146
|
+
get: "READ",
|
|
1147
|
+
post: "CREATE",
|
|
1148
|
+
put: "UPDATE",
|
|
1149
|
+
patch: "PATCH",
|
|
1150
|
+
delete: "DELETE",
|
|
1151
|
+
};
|
|
1152
|
+
return html`<button
|
|
1153
|
+
key=${m}
|
|
1154
|
+
className=${`route-method ${m}`}
|
|
1155
|
+
style=${{
|
|
1156
|
+
cursor: "pointer",
|
|
1157
|
+
border: methods.includes(m)
|
|
1158
|
+
? "2px solid " + methodColors[m]
|
|
1159
|
+
: "2px solid transparent",
|
|
1160
|
+
padding: "6px 12px",
|
|
1161
|
+
fontSize: "11px",
|
|
1162
|
+
opacity: methods.includes(m) ? 1 : 0.35,
|
|
1163
|
+
borderRadius: "6px",
|
|
1164
|
+
transition: "all 0.15s",
|
|
1165
|
+
}}
|
|
1166
|
+
onClick=${() => toggleMethod(m)}
|
|
1167
|
+
>
|
|
1168
|
+
${labels[m]}
|
|
1169
|
+
</button>`;
|
|
1170
|
+
})}
|
|
1171
|
+
</div>
|
|
1172
|
+
</div>`}
|
|
1173
|
+
${isAuthType &&
|
|
1174
|
+
html`<div className="form-group">
|
|
1175
|
+
<label className="form-label">Model</label>
|
|
1176
|
+
<select
|
|
1177
|
+
className="form-select"
|
|
1178
|
+
value=${modelName}
|
|
1179
|
+
onChange=${(e) => {
|
|
1180
|
+
setModelName(e.target.value);
|
|
1181
|
+
setLoginField("");
|
|
1182
|
+
setPasswordField("");
|
|
1183
|
+
setRegisterFields([]);
|
|
1184
|
+
setSelectedFields([]);
|
|
1185
|
+
}}
|
|
1186
|
+
>
|
|
1187
|
+
<option value="">Select a model...</option>
|
|
1188
|
+
${(data.models || []).map(
|
|
1189
|
+
(m) =>
|
|
1190
|
+
html`<option key=${m.name} value=${m.name}>
|
|
1191
|
+
${m.name}
|
|
1192
|
+
</option>`,
|
|
1193
|
+
)}
|
|
1194
|
+
</select>
|
|
1195
|
+
${!data.models?.length &&
|
|
1196
|
+
html`<div
|
|
1197
|
+
style=${{
|
|
1198
|
+
fontSize: "11px",
|
|
1199
|
+
color: "var(--warning)",
|
|
1200
|
+
marginTop: "4px",
|
|
1201
|
+
}}
|
|
1202
|
+
>
|
|
1203
|
+
No models found. Create a schema first.
|
|
1204
|
+
</div>`}
|
|
1205
|
+
</div>`}
|
|
1206
|
+
${(routeType === "methods" || routeType === "crud") &&
|
|
1207
|
+
html`<div className="form-group">
|
|
1208
|
+
<label className="form-label"
|
|
1209
|
+
>Model Fields
|
|
1210
|
+
<span style=${{ color: "var(--text-muted)", fontWeight: 400 }}
|
|
1211
|
+
>(select fields to include in request)</span
|
|
1212
|
+
></label
|
|
1213
|
+
>
|
|
1214
|
+
<select
|
|
1215
|
+
className="form-select"
|
|
1216
|
+
style=${{ marginBottom: "8px" }}
|
|
1217
|
+
value=${modelName}
|
|
1218
|
+
onChange=${(e) => {
|
|
1219
|
+
setModelName(e.target.value);
|
|
1220
|
+
setSelectedFields([]);
|
|
1221
|
+
}}
|
|
1222
|
+
>
|
|
1223
|
+
<option value="">Select a model...</option>
|
|
1224
|
+
${(data.models || []).map(
|
|
1225
|
+
(m) =>
|
|
1226
|
+
html`<option key=${m.name} value=${m.name}>
|
|
1227
|
+
${m.name}
|
|
1228
|
+
</option>`,
|
|
1229
|
+
)}
|
|
1230
|
+
</select>
|
|
1231
|
+
${modelName &&
|
|
1232
|
+
modelFields.length > 0 &&
|
|
1233
|
+
html`<div>
|
|
1234
|
+
<div className="flex gap-2" style=${{ flexWrap: "wrap" }}>
|
|
1235
|
+
${modelFields.map((f) => {
|
|
1236
|
+
const active = selectedFields.includes(f.name);
|
|
1237
|
+
return html`<button
|
|
1238
|
+
key=${f.name}
|
|
1239
|
+
style=${{
|
|
1240
|
+
padding: "5px 12px",
|
|
1241
|
+
borderRadius: "6px",
|
|
1242
|
+
fontSize: "12px",
|
|
1243
|
+
cursor: "pointer",
|
|
1244
|
+
border: active
|
|
1245
|
+
? "1.5px solid var(--accent)"
|
|
1246
|
+
: "1.5px solid var(--border)",
|
|
1247
|
+
background: active
|
|
1248
|
+
? "var(--accent-glow)"
|
|
1249
|
+
: "var(--bg-input)",
|
|
1250
|
+
color: active
|
|
1251
|
+
? "var(--accent-light)"
|
|
1252
|
+
: "var(--text-secondary)",
|
|
1253
|
+
fontWeight: active ? 600 : 400,
|
|
1254
|
+
transition: "all 0.15s",
|
|
1152
1255
|
}}
|
|
1256
|
+
onClick=${() =>
|
|
1257
|
+
setSelectedFields((prev) =>
|
|
1258
|
+
active
|
|
1259
|
+
? prev.filter((x) => x !== f.name)
|
|
1260
|
+
: [...prev, f.name],
|
|
1261
|
+
)}
|
|
1153
1262
|
>
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
${modelFields.map((f) => {
|
|
1169
|
-
const active = selectedFields.includes(f.name);
|
|
1170
|
-
return html`<button
|
|
1171
|
-
key=${f.name}
|
|
1172
|
-
style=${{
|
|
1173
|
-
padding: "5px 12px",
|
|
1174
|
-
borderRadius: "6px",
|
|
1175
|
-
fontSize: "12px",
|
|
1176
|
-
cursor: "pointer",
|
|
1177
|
-
border: active
|
|
1178
|
-
? "1.5px solid var(--accent)"
|
|
1179
|
-
: "1.5px solid var(--border)",
|
|
1180
|
-
background: active
|
|
1181
|
-
? "var(--accent-glow)"
|
|
1182
|
-
: "var(--bg-input)",
|
|
1183
|
-
color: active
|
|
1184
|
-
? "var(--accent-light)"
|
|
1185
|
-
: "var(--text-secondary)",
|
|
1186
|
-
fontWeight: active ? 600 : 400,
|
|
1187
|
-
transition: "all 0.15s",
|
|
1188
|
-
}}
|
|
1189
|
-
onClick=${() =>
|
|
1190
|
-
setSelectedFields((prev) =>
|
|
1191
|
-
active
|
|
1192
|
-
? prev.filter((x) => x !== f.name)
|
|
1193
|
-
: [...prev, f.name],
|
|
1194
|
-
)}
|
|
1195
|
-
>
|
|
1196
|
-
${f.name}
|
|
1197
|
-
<span
|
|
1198
|
-
style=${{
|
|
1199
|
-
fontSize: "10px",
|
|
1200
|
-
color: "var(--text-muted)",
|
|
1201
|
-
marginLeft: "4px",
|
|
1202
|
-
}}
|
|
1203
|
-
>${f.type}</span
|
|
1204
|
-
>
|
|
1205
|
-
</button>`;
|
|
1206
|
-
})}
|
|
1207
|
-
</div>`}`
|
|
1208
|
-
: html`<div
|
|
1263
|
+
${f.name}
|
|
1264
|
+
<span
|
|
1265
|
+
style=${{
|
|
1266
|
+
fontSize: "10px",
|
|
1267
|
+
color: "var(--text-muted)",
|
|
1268
|
+
marginLeft: "4px",
|
|
1269
|
+
}}
|
|
1270
|
+
>${f.type}</span
|
|
1271
|
+
>
|
|
1272
|
+
</button>`;
|
|
1273
|
+
})}
|
|
1274
|
+
</div>
|
|
1275
|
+
<div style=${{ display: "flex", gap: "8px", marginTop: "8px" }}>
|
|
1276
|
+
<button
|
|
1209
1277
|
style=${{
|
|
1210
|
-
|
|
1278
|
+
padding: "3px 10px",
|
|
1279
|
+
fontSize: "10px",
|
|
1280
|
+
borderRadius: "4px",
|
|
1281
|
+
border: "1px solid var(--border)",
|
|
1282
|
+
background: "transparent",
|
|
1283
|
+
color: "var(--accent)",
|
|
1284
|
+
cursor: "pointer",
|
|
1285
|
+
}}
|
|
1286
|
+
onClick=${() => setSelectedFields(modelFields.map((f) => f.name))}
|
|
1287
|
+
>
|
|
1288
|
+
Select All
|
|
1289
|
+
</button>
|
|
1290
|
+
<button
|
|
1291
|
+
style=${{
|
|
1292
|
+
padding: "3px 10px",
|
|
1293
|
+
fontSize: "10px",
|
|
1294
|
+
borderRadius: "4px",
|
|
1295
|
+
border: "1px solid var(--border)",
|
|
1296
|
+
background: "transparent",
|
|
1211
1297
|
color: "var(--text-muted)",
|
|
1212
|
-
|
|
1298
|
+
cursor: "pointer",
|
|
1213
1299
|
}}
|
|
1300
|
+
onClick=${() => setSelectedFields([])}
|
|
1214
1301
|
>
|
|
1215
|
-
|
|
1216
|
-
</
|
|
1302
|
+
Clear
|
|
1303
|
+
</button>
|
|
1304
|
+
</div>
|
|
1305
|
+
</div>`}
|
|
1306
|
+
${!data.models?.length &&
|
|
1307
|
+
html`<div
|
|
1308
|
+
style=${{
|
|
1309
|
+
fontSize: "11px",
|
|
1310
|
+
color: "var(--text-muted)",
|
|
1311
|
+
marginTop: "4px",
|
|
1312
|
+
}}
|
|
1313
|
+
>
|
|
1314
|
+
No models found. You can still add custom fields below.
|
|
1315
|
+
</div>`}
|
|
1217
1316
|
</div>`}
|
|
1218
|
-
${routeType === "methods" &&
|
|
1317
|
+
${(routeType === "methods" || routeType === "crud") &&
|
|
1219
1318
|
html`<div className="form-group">
|
|
1220
1319
|
<label className="form-label"
|
|
1221
1320
|
>Custom Fields
|
|
1222
1321
|
<span style=${{ color: "var(--text-muted)", fontWeight: 400 }}
|
|
1223
|
-
>(add fields received from client)</span
|
|
1322
|
+
>(add extra fields received from client)</span
|
|
1224
1323
|
></label
|
|
1225
1324
|
>
|
|
1226
1325
|
${customFields.length > 0 &&
|
|
@@ -1338,76 +1437,6 @@ function CreateRoutePanel({ onCreated }) {
|
|
|
1338
1437
|
</button>
|
|
1339
1438
|
</div>
|
|
1340
1439
|
</div>`}
|
|
1341
|
-
${routeType === "crud" &&
|
|
1342
|
-
html`<div className="form-group">
|
|
1343
|
-
<label className="form-label"
|
|
1344
|
-
>CRUD Methods
|
|
1345
|
-
<span style=${{ color: "var(--text-muted)", fontWeight: 400 }}
|
|
1346
|
-
>(select which operations to include)</span
|
|
1347
|
-
></label
|
|
1348
|
-
>
|
|
1349
|
-
<div className="flex gap-2">
|
|
1350
|
-
${["get", "post", "put", "patch", "delete"].map((m) => {
|
|
1351
|
-
const labels = {
|
|
1352
|
-
get: "READ",
|
|
1353
|
-
post: "CREATE",
|
|
1354
|
-
put: "UPDATE",
|
|
1355
|
-
patch: "PATCH",
|
|
1356
|
-
delete: "DELETE",
|
|
1357
|
-
};
|
|
1358
|
-
return html`<button
|
|
1359
|
-
key=${m}
|
|
1360
|
-
className=${`route-method ${m}`}
|
|
1361
|
-
style=${{
|
|
1362
|
-
cursor: "pointer",
|
|
1363
|
-
border: methods.includes(m)
|
|
1364
|
-
? "2px solid " + methodColors[m]
|
|
1365
|
-
: "2px solid transparent",
|
|
1366
|
-
padding: "6px 12px",
|
|
1367
|
-
fontSize: "11px",
|
|
1368
|
-
opacity: methods.includes(m) ? 1 : 0.35,
|
|
1369
|
-
borderRadius: "6px",
|
|
1370
|
-
transition: "all 0.15s",
|
|
1371
|
-
}}
|
|
1372
|
-
onClick=${() => toggleMethod(m)}
|
|
1373
|
-
>
|
|
1374
|
-
${labels[m]}
|
|
1375
|
-
</button>`;
|
|
1376
|
-
})}
|
|
1377
|
-
</div>
|
|
1378
|
-
</div>`}
|
|
1379
|
-
${needsModel &&
|
|
1380
|
-
html`<div className="form-group">
|
|
1381
|
-
<label className="form-label">Model</label>
|
|
1382
|
-
<select
|
|
1383
|
-
className="form-select"
|
|
1384
|
-
value=${modelName}
|
|
1385
|
-
onChange=${(e) => {
|
|
1386
|
-
setModelName(e.target.value);
|
|
1387
|
-
setLoginField("");
|
|
1388
|
-
setPasswordField("");
|
|
1389
|
-
setRegisterFields([]);
|
|
1390
|
-
}}
|
|
1391
|
-
>
|
|
1392
|
-
<option value="">Select a model...</option>
|
|
1393
|
-
${(data.models || []).map(
|
|
1394
|
-
(m) =>
|
|
1395
|
-
html`<option key=${m.name} value=${m.name}>
|
|
1396
|
-
${m.name}
|
|
1397
|
-
</option>`,
|
|
1398
|
-
)}
|
|
1399
|
-
</select>
|
|
1400
|
-
${!data.models?.length &&
|
|
1401
|
-
html`<div
|
|
1402
|
-
style=${{
|
|
1403
|
-
fontSize: "11px",
|
|
1404
|
-
color: "var(--warning)",
|
|
1405
|
-
marginTop: "4px",
|
|
1406
|
-
}}
|
|
1407
|
-
>
|
|
1408
|
-
No models found. Create a schema first.
|
|
1409
|
-
</div>`}
|
|
1410
|
-
</div>`}
|
|
1411
1440
|
${routeType === "login" &&
|
|
1412
1441
|
modelName &&
|
|
1413
1442
|
modelFields.length > 0 &&
|
|
@@ -341,7 +341,7 @@ function SchemaDetailPanel({ model, reqCount, uniqCount, onEdit, onDelete }) {
|
|
|
341
341
|
</div>
|
|
342
342
|
</div>
|
|
343
343
|
<div className="flex gap-3">
|
|
344
|
-
${[['Fields', model.fields?.length || 0, null], ['Required', reqCount, reqCount > 0 ? 'var(--success)' : null], ['Unique', uniqCount, uniqCount > 0 ? 'var(--warning)' : null]].map(([label, count, color]) =>
|
|
344
|
+
${[['Fields', model.fields?.length || 0, null], ['Required', reqCount, reqCount > 0 ? 'var(--success)' : null], ['Unique', uniqCount, uniqCount > 0 ? 'var(--warning)' : null], ['Indexes', (model.fields?.filter(f => f.index).length || 0) + (model.indexes?.length || 0), ((model.fields?.filter(f => f.index).length || 0) + (model.indexes?.length || 0)) > 0 ? 'var(--info)' : null]].map(([label, count, color]) =>
|
|
345
345
|
html`<div key=${label} style=${{ padding: '8px 14px', background: 'var(--bg-input)', borderRadius: 'var(--radius)', fontSize: '13px' }}>
|
|
346
346
|
<span style=${{ color: 'var(--text-muted)' }}>${label} </span>
|
|
347
347
|
<span style=${{ fontWeight: 600, color: color || 'var(--text-secondary)' }}>${count}</span>
|
|
@@ -379,7 +379,8 @@ function SchemaDetailPanel({ model, reqCount, uniqCount, onEdit, onDelete }) {
|
|
|
379
379
|
<td style=${tdStyle}><div className="flex gap-1">
|
|
380
380
|
${f.required && html`<span className="badge badge-success" style=${{ fontSize: '10px' }}>required</span>`}
|
|
381
381
|
${f.unique && html`<span className="badge badge-warning" style=${{ fontSize: '10px' }}>unique</span>`}
|
|
382
|
-
${
|
|
382
|
+
${f.index && html`<span className="badge badge-info" style=${{ fontSize: '10px' }}>indexed</span>`}
|
|
383
|
+
${!f.required && !f.unique && !f.index && html`<span style=${{ fontSize: '12px', color: 'var(--text-muted)' }}>—</span>`}
|
|
383
384
|
</div></td>
|
|
384
385
|
</tr>`)}
|
|
385
386
|
</tbody>
|
|
@@ -485,6 +486,11 @@ function SchemaBuilderPanel({ model, onDone }) {
|
|
|
485
486
|
const [freq, setFreq] = useState(false);
|
|
486
487
|
const [funiq, setFuniq] = useState(false);
|
|
487
488
|
const [fref, setFref] = useState("");
|
|
489
|
+
const [findex, setFindex] = useState(false);
|
|
490
|
+
const [indexes, setIndexes] = useState(model?.indexes || []);
|
|
491
|
+
const [idxFields, setIdxFields] = useState([]);
|
|
492
|
+
const [idxType, setIdxType] = useState('compound');
|
|
493
|
+
const [idxUnique, setIdxUnique] = useState(false);
|
|
488
494
|
const nameRef = useRef(null);
|
|
489
495
|
const fieldNameRef = useRef(null);
|
|
490
496
|
|
|
@@ -516,6 +522,7 @@ function SchemaBuilderPanel({ model, onDone }) {
|
|
|
516
522
|
setFreq(false);
|
|
517
523
|
setFuniq(false);
|
|
518
524
|
setFref("");
|
|
525
|
+
setFindex(false);
|
|
519
526
|
setEditIndex(null);
|
|
520
527
|
}
|
|
521
528
|
|
|
@@ -528,6 +535,7 @@ function SchemaBuilderPanel({ model, onDone }) {
|
|
|
528
535
|
unique: funiq,
|
|
529
536
|
};
|
|
530
537
|
if (ftype === "ObjectId" && fref) fieldData.ref = fref;
|
|
538
|
+
if (findex) fieldData.index = true;
|
|
531
539
|
|
|
532
540
|
if (editIndex !== null) {
|
|
533
541
|
setFields((prev) =>
|
|
@@ -549,6 +557,7 @@ function SchemaBuilderPanel({ model, onDone }) {
|
|
|
549
557
|
setFreq(!!f.required);
|
|
550
558
|
setFuniq(!!f.unique);
|
|
551
559
|
setFref(f.ref || "");
|
|
560
|
+
setFindex(!!f.index);
|
|
552
561
|
setEditIndex(index);
|
|
553
562
|
setTimeout(() => fieldNameRef.current?.focus(), 50);
|
|
554
563
|
}
|
|
@@ -573,13 +582,13 @@ function SchemaBuilderPanel({ model, onDone }) {
|
|
|
573
582
|
if (isEdit) {
|
|
574
583
|
await api("/models/" + model.name, {
|
|
575
584
|
method: "PUT",
|
|
576
|
-
body: { fields },
|
|
585
|
+
body: { fields, indexes },
|
|
577
586
|
});
|
|
578
587
|
toast('Model "' + model.name + '" updated', "success");
|
|
579
588
|
} else {
|
|
580
589
|
await api("/models", {
|
|
581
590
|
method: "POST",
|
|
582
|
-
body: { name: name.trim(), fields },
|
|
591
|
+
body: { name: name.trim(), fields, indexes },
|
|
583
592
|
});
|
|
584
593
|
toast('Model "' + name.trim() + '" created', "success");
|
|
585
594
|
}
|
|
@@ -732,6 +741,25 @@ function SchemaBuilderPanel({ model, onDone }) {
|
|
|
732
741
|
</div>
|
|
733
742
|
Unique
|
|
734
743
|
</label>
|
|
744
|
+
<label
|
|
745
|
+
className="flex items-center gap-2"
|
|
746
|
+
style=${{
|
|
747
|
+
cursor: "pointer",
|
|
748
|
+
fontSize: "12px",
|
|
749
|
+
whiteSpace: "nowrap",
|
|
750
|
+
color: findex ? "var(--info)" : "var(--text-muted)",
|
|
751
|
+
}}
|
|
752
|
+
>
|
|
753
|
+
<div className="form-toggle">
|
|
754
|
+
<input
|
|
755
|
+
type="checkbox"
|
|
756
|
+
checked=${findex}
|
|
757
|
+
onChange=${(e) => setFindex(e.target.checked)}
|
|
758
|
+
/>
|
|
759
|
+
<span className="slider" />
|
|
760
|
+
</div>
|
|
761
|
+
Index
|
|
762
|
+
</label>
|
|
735
763
|
</div>
|
|
736
764
|
${editIndex !== null &&
|
|
737
765
|
html`<button
|
|
@@ -858,6 +886,18 @@ function SchemaBuilderPanel({ model, onDone }) {
|
|
|
858
886
|
}}
|
|
859
887
|
>UNQ</span
|
|
860
888
|
>`}
|
|
889
|
+
${f.index &&
|
|
890
|
+
html`<span
|
|
891
|
+
style=${{
|
|
892
|
+
fontSize: "9px",
|
|
893
|
+
padding: "2px 6px",
|
|
894
|
+
borderRadius: "4px",
|
|
895
|
+
background: "rgba(59,130,246,0.15)",
|
|
896
|
+
color: "var(--info)",
|
|
897
|
+
fontWeight: 600,
|
|
898
|
+
}}
|
|
899
|
+
>IDX</span
|
|
900
|
+
>`}
|
|
861
901
|
<button
|
|
862
902
|
className="btn btn-ghost btn-sm btn-icon"
|
|
863
903
|
onClick=${() => startEdit(i)}
|
|
@@ -897,5 +937,92 @@ function SchemaBuilderPanel({ model, onDone }) {
|
|
|
897
937
|
Add fields above to define your schema
|
|
898
938
|
</div>
|
|
899
939
|
</div>`}
|
|
940
|
+
|
|
941
|
+
<div className="card mb-4" style=${{ padding: '16px' }}>
|
|
942
|
+
<div style=${{ fontSize: '11px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px', color: 'var(--text-muted)', marginBottom: '12px' }}>
|
|
943
|
+
Compound Indexes
|
|
944
|
+
</div>
|
|
945
|
+
|
|
946
|
+
<div className="flex items-center gap-3 mb-3" style=${{ flexWrap: 'wrap' }}>
|
|
947
|
+
<select className="form-select" value=${idxType}
|
|
948
|
+
onChange=${(e) => setIdxType(e.target.value)}
|
|
949
|
+
style=${{ fontSize: '13px', width: '120px' }}>
|
|
950
|
+
<option value="compound">Compound</option>
|
|
951
|
+
<option value="text">Text</option>
|
|
952
|
+
</select>
|
|
953
|
+
|
|
954
|
+
${idxType === 'compound' && html`<label className="flex items-center gap-2" style=${{ cursor: 'pointer', fontSize: '12px', color: idxUnique ? 'var(--warning)' : 'var(--text-muted)' }}>
|
|
955
|
+
<div className="form-toggle">
|
|
956
|
+
<input type="checkbox" checked=${idxUnique} onChange=${(e) => setIdxUnique(e.target.checked)} />
|
|
957
|
+
<span className="slider" />
|
|
958
|
+
</div>
|
|
959
|
+
Unique
|
|
960
|
+
</label>`}
|
|
961
|
+
|
|
962
|
+
<button className="btn btn-primary btn-sm"
|
|
963
|
+
disabled=${idxFields.length < (idxType === 'text' ? 1 : 2)}
|
|
964
|
+
onClick=${() => {
|
|
965
|
+
const newIdx = {
|
|
966
|
+
fields: idxFields.map(f => ({ field: f.field, direction: idxType === 'text' ? 'text' : f.direction })),
|
|
967
|
+
unique: idxType === 'compound' ? idxUnique : false,
|
|
968
|
+
type: idxType,
|
|
969
|
+
};
|
|
970
|
+
setIndexes(prev => [...prev, newIdx]);
|
|
971
|
+
setIdxFields([]);
|
|
972
|
+
setIdxUnique(false);
|
|
973
|
+
setSaved(false);
|
|
974
|
+
}}>
|
|
975
|
+
Add Index
|
|
976
|
+
</button>
|
|
977
|
+
</div>
|
|
978
|
+
|
|
979
|
+
<div className="flex gap-2 mb-3" style=${{ flexWrap: 'wrap' }}>
|
|
980
|
+
${fields.map(f => {
|
|
981
|
+
const selected = idxFields.find(i => i.field === f.name);
|
|
982
|
+
return html`<button key=${f.name}
|
|
983
|
+
onClick=${() => {
|
|
984
|
+
if (selected) {
|
|
985
|
+
if (idxType !== 'text') {
|
|
986
|
+
if (selected.direction === 1) {
|
|
987
|
+
setIdxFields(prev => prev.map(i => i.field === f.name ? { ...i, direction: -1 } : i));
|
|
988
|
+
} else {
|
|
989
|
+
setIdxFields(prev => prev.filter(i => i.field !== f.name));
|
|
990
|
+
}
|
|
991
|
+
} else {
|
|
992
|
+
setIdxFields(prev => prev.filter(i => i.field !== f.name));
|
|
993
|
+
}
|
|
994
|
+
} else {
|
|
995
|
+
setIdxFields(prev => [...prev, { field: f.name, direction: 1 }]);
|
|
996
|
+
}
|
|
997
|
+
}}
|
|
998
|
+
style=${{ padding: '4px 10px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--border)', cursor: 'pointer', fontFamily: 'monospace',
|
|
999
|
+
background: selected ? 'var(--accent-glow)' : 'transparent',
|
|
1000
|
+
color: selected ? 'var(--accent-light)' : 'var(--text-muted)' }}>
|
|
1001
|
+
${f.name}${selected && idxType !== 'text' ? (selected.direction === 1 ? ' ASC' : ' DESC') : ''}
|
|
1002
|
+
</button>`;
|
|
1003
|
+
})}
|
|
1004
|
+
</div>
|
|
1005
|
+
${idxFields.length > 0 && html`<div style=${{ fontSize: '11px', color: 'var(--text-muted)', marginBottom: '8px' }}>
|
|
1006
|
+
${idxType === 'text' ? 'Click to select/deselect fields' : 'Click once = ASC, twice = DESC, third = remove'}
|
|
1007
|
+
</div>`}
|
|
1008
|
+
|
|
1009
|
+
${indexes.length > 0 && html`<div style=${{ marginTop: '12px' }}>
|
|
1010
|
+
${indexes.map((idx, i) => html`<div key=${i} className="flex items-center gap-2" style=${{ padding: '6px 12px', background: 'var(--bg-input)', borderRadius: '6px', marginBottom: '4px', fontSize: '12px' }}>
|
|
1011
|
+
<span style=${{ fontSize: '9px', padding: '2px 6px', borderRadius: '4px', fontWeight: 600,
|
|
1012
|
+
background: idx.type === 'text' ? 'rgba(59,130,246,0.15)' : 'rgba(124,58,237,0.15)',
|
|
1013
|
+
color: idx.type === 'text' ? 'var(--info)' : 'var(--accent-light)' }}>
|
|
1014
|
+
${idx.type === 'text' ? 'TEXT' : 'COMPOUND'}
|
|
1015
|
+
</span>
|
|
1016
|
+
<span style=${{ fontFamily: 'monospace', flex: 1 }}>
|
|
1017
|
+
{ ${idx.fields.map(f => f.field + ': ' + (f.direction === 'text' ? "'text'" : f.direction)).join(', ')} }
|
|
1018
|
+
</span>
|
|
1019
|
+
${idx.unique && html`<span style=${{ fontSize: '9px', padding: '2px 6px', borderRadius: '4px', background: 'var(--warning-glow)', color: 'var(--warning)', fontWeight: 600 }}>UNIQUE</span>`}
|
|
1020
|
+
<button className="btn btn-ghost btn-sm btn-icon" onClick=${() => { setIndexes(prev => prev.filter((_, j) => j !== i)); setSaved(false); }}
|
|
1021
|
+
style=${{ color: 'var(--error)', opacity: 0.5, padding: '4px' }}>
|
|
1022
|
+
${Icons.trash}
|
|
1023
|
+
</button>
|
|
1024
|
+
</div>`)}
|
|
1025
|
+
</div>`}
|
|
1026
|
+
</div>
|
|
900
1027
|
</div>`;
|
|
901
1028
|
}
|