admindb 1.2.0 → 2.0.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 +250 -352
- package/dist/app.d.ts +25 -4
- package/dist/app.js +79 -15
- package/dist/app.js.map +1 -1
- package/dist/auth/config.d.ts +19 -0
- package/dist/auth/config.js +147 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/crypto.d.ts +27 -0
- package/dist/auth/crypto.js +133 -0
- package/dist/auth/crypto.js.map +1 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.js +22 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware.d.ts +14 -0
- package/dist/auth/middleware.js +100 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/routes.d.ts +7 -0
- package/dist/auth/routes.js +78 -0
- package/dist/auth/routes.js.map +1 -0
- package/dist/auth/types.d.ts +35 -0
- package/dist/auth/types.js +11 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli/args.d.ts +45 -0
- package/dist/cli/args.js +328 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/config.d.ts +56 -0
- package/dist/cli/config.js +133 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/runner.d.ts +1 -0
- package/dist/cli/runner.js +222 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli.js +2 -127
- package/dist/cli.js.map +1 -1
- package/dist/data/datasets.d.ts +17 -0
- package/dist/data/datasets.js +133 -0
- package/dist/data/datasets.js.map +1 -0
- package/dist/data/detector.d.ts +30 -0
- package/dist/data/detector.js +472 -0
- package/dist/data/detector.js.map +1 -0
- package/dist/data/engine.d.ts +10 -0
- package/dist/data/engine.js +211 -0
- package/dist/data/engine.js.map +1 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.js +22 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/strategies.d.ts +30 -0
- package/dist/data/strategies.js +343 -0
- package/dist/data/strategies.js.map +1 -0
- package/dist/data/types.d.ts +73 -0
- package/dist/data/types.js +8 -0
- package/dist/data/types.js.map +1 -0
- package/dist/db/database.d.ts +14 -142
- package/dist/db/database.js +209 -435
- package/dist/db/database.js.map +1 -1
- package/dist/db/export.d.ts +2 -2
- package/dist/db/export.js +7 -2
- package/dist/db/export.js.map +1 -1
- package/dist/db/filters.d.ts +22 -0
- package/dist/db/filters.js +158 -0
- package/dist/db/filters.js.map +1 -0
- package/dist/db/index.d.ts +8 -0
- package/dist/db/index.js +25 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/introspection.d.ts +6 -0
- package/dist/db/introspection.js +172 -0
- package/dist/db/introspection.js.map +1 -0
- package/dist/db/manager.d.ts +32 -19
- package/dist/db/manager.js +203 -44
- package/dist/db/manager.js.map +1 -1
- package/dist/db/migrations.d.ts +5 -0
- package/dist/db/migrations.js +110 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/postgres.d.ts +85 -0
- package/dist/db/postgres.js +745 -0
- package/dist/db/postgres.js.map +1 -0
- package/dist/db/types.d.ts +206 -0
- package/dist/db/types.js +7 -0
- package/dist/db/types.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +46 -4
- package/dist/index.js.map +1 -1
- package/dist/public/css/app.css +1 -1
- package/dist/public/css/input.css +83 -13
- package/dist/public/js/app.js +142 -0
- package/dist/public/js/browse.js +591 -104
- package/dist/public/js/databases.js +105 -13
- package/dist/public/js/designer.js +40 -2
- package/dist/public/js/forms.js +610 -28
- package/dist/public/js/icons.js +51 -0
- package/dist/public/js/inspector.js +638 -0
- package/dist/public/js/query.js +93 -3
- package/dist/public/js/schema.js +170 -52
- package/dist/public/js/seed.js +1090 -0
- package/dist/routes/api/helpers.d.ts +42 -0
- package/dist/routes/api/helpers.js +133 -0
- package/dist/routes/api/helpers.js.map +1 -0
- package/dist/routes/api/import-export.d.ts +3 -0
- package/dist/routes/api/import-export.js +85 -0
- package/dist/routes/api/import-export.js.map +1 -0
- package/dist/routes/api/index.d.ts +4 -0
- package/dist/routes/api/index.js +37 -0
- package/dist/routes/api/index.js.map +1 -0
- package/dist/routes/api/query.d.ts +3 -0
- package/dist/routes/api/query.js +76 -0
- package/dist/routes/api/query.js.map +1 -0
- package/dist/routes/api/rows.d.ts +3 -0
- package/dist/routes/api/rows.js +373 -0
- package/dist/routes/api/rows.js.map +1 -0
- package/dist/routes/api/seed.d.ts +3 -0
- package/dist/routes/api/seed.js +82 -0
- package/dist/routes/api/seed.js.map +1 -0
- package/dist/routes/api/tables.d.ts +3 -0
- package/dist/routes/api/tables.js +160 -0
- package/dist/routes/api/tables.js.map +1 -0
- package/dist/routes/databases.d.ts +8 -11
- package/dist/routes/databases.js +100 -66
- package/dist/routes/databases.js.map +1 -1
- package/dist/routes/index.d.ts +3 -0
- package/dist/routes/index.js +20 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/pages.d.ts +3 -3
- package/dist/routes/pages.js +163 -54
- package/dist/routes/pages.js.map +1 -1
- package/dist/serverless.d.ts +40 -0
- package/dist/serverless.js +210 -0
- package/dist/serverless.js.map +1 -0
- package/dist/sql/generator.d.ts +4 -3
- package/dist/sql/generator.js +103 -6
- package/dist/sql/generator.js.map +1 -1
- package/dist/sql/index.d.ts +2 -0
- package/dist/sql/index.js +19 -0
- package/dist/sql/index.js.map +1 -0
- package/dist/types/api.d.ts +412 -0
- package/dist/types/api.js +9 -0
- package/dist/types/api.js.map +1 -0
- package/dist/utils/colors.d.ts +40 -0
- package/dist/utils/colors.js +64 -0
- package/dist/utils/colors.js.map +1 -0
- package/dist/{util.d.ts → utils/common.d.ts} +10 -6
- package/dist/{util.js → utils/common.js} +80 -13
- package/dist/utils/common.js.map +1 -0
- package/dist/utils/csv.js.map +1 -0
- package/dist/utils/datatype.d.ts +49 -0
- package/dist/utils/datatype.js +222 -0
- package/dist/utils/datatype.js.map +1 -0
- package/dist/utils/icons.d.ts +11 -0
- package/dist/utils/icons.js +72 -0
- package/dist/utils/icons.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +21 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/{logger.d.ts → utils/logger.d.ts} +1 -1
- package/dist/utils/logger.js +40 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/views/layouts/main.hbs +33 -11
- package/dist/views/pages/databases.hbs +182 -74
- package/dist/views/pages/designer.hbs +9 -6
- package/dist/views/pages/error.hbs +3 -3
- package/dist/views/pages/form.hbs +5 -3
- package/dist/views/pages/home.hbs +22 -13
- package/dist/views/pages/login.hbs +111 -0
- package/dist/views/pages/query.hbs +10 -8
- package/dist/views/pages/schema.hbs +41 -13
- package/dist/views/pages/seed.hbs +232 -0
- package/dist/views/pages/table.hbs +155 -70
- package/dist/views/partials/icon.hbs +2 -0
- package/dist/views/partials/navbar.hbs +70 -31
- package/dist/views/partials/sidebar.hbs +108 -99
- package/package.json +12 -5
- package/dist/args.d.ts +0 -28
- package/dist/args.js +0 -187
- package/dist/args.js.map +0 -1
- package/dist/config.d.ts +0 -17
- package/dist/config.js +0 -23
- package/dist/config.js.map +0 -1
- package/dist/csv.js.map +0 -1
- package/dist/logger.js +0 -25
- package/dist/logger.js.map +0 -1
- package/dist/routes/api.d.ts +0 -9
- package/dist/routes/api.js +0 -786
- package/dist/routes/api.js.map +0 -1
- package/dist/test/classifier.test.d.ts +0 -1
- package/dist/test/classifier.test.js +0 -44
- package/dist/test/classifier.test.js.map +0 -1
- package/dist/test/csv.test.d.ts +0 -1
- package/dist/test/csv.test.js +0 -64
- package/dist/test/csv.test.js.map +0 -1
- package/dist/test/database.test.d.ts +0 -1
- package/dist/test/database.test.js +0 -402
- package/dist/test/database.test.js.map +0 -1
- package/dist/test/generator.test.d.ts +0 -1
- package/dist/test/generator.test.js +0 -132
- package/dist/test/generator.test.js.map +0 -1
- package/dist/test/manager.test.d.ts +0 -1
- package/dist/test/manager.test.js +0 -146
- package/dist/test/manager.test.js.map +0 -1
- package/dist/util.js.map +0 -1
- /package/dist/{csv.d.ts → utils/csv.d.ts} +0 -0
- /package/dist/{csv.js → utils/csv.js} +0 -0
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
/* "Seed data" page — enhanced interactive data generator.
|
|
2
|
+
*
|
|
3
|
+
* Lets you generate realistic fake rows for a table with 20+ strategies,
|
|
4
|
+
* live preview samples, sparse data simulation, sequential numbering,
|
|
5
|
+
* pattern templates, dual Table/SQL previews, CSV/JSON export, and presets.
|
|
6
|
+
*/
|
|
7
|
+
(function () {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const cfgEl = document.getElementById('seed-config');
|
|
11
|
+
if (!cfgEl) return;
|
|
12
|
+
|
|
13
|
+
const cfg = JSON.parse(cfgEl.textContent);
|
|
14
|
+
const t = encodeURIComponent(cfg.table);
|
|
15
|
+
const columns = cfg.columns || [];
|
|
16
|
+
|
|
17
|
+
// DOM Elements
|
|
18
|
+
const countInput = document.getElementById('seed-count');
|
|
19
|
+
const searchInput = document.getElementById('seed-search');
|
|
20
|
+
const truncateCheckbox = document.getElementById('seed-truncate');
|
|
21
|
+
const columnsBox = document.getElementById('seed-columns');
|
|
22
|
+
const emptySearchBox = document.getElementById('seed-empty-search');
|
|
23
|
+
const colCountBadge = document.getElementById('seed-col-count-badge');
|
|
24
|
+
const feedback = document.getElementById('seed-feedback');
|
|
25
|
+
const previewCard = document.getElementById('seed-preview-card');
|
|
26
|
+
const previewSummaryBadge = document.getElementById('preview-summary-badge');
|
|
27
|
+
const previewThead = document.getElementById('seed-preview-thead');
|
|
28
|
+
const previewTbody = document.getElementById('seed-preview-tbody');
|
|
29
|
+
const tableViewContainer = document.getElementById('seed-table-view-container');
|
|
30
|
+
const sqlViewContainer = document.getElementById('seed-sql-view-container');
|
|
31
|
+
const sqlCode = document.getElementById('seed-sql-code');
|
|
32
|
+
const tabTableView = document.getElementById('tab-table-view');
|
|
33
|
+
const tabSqlView = document.getElementById('tab-sql-view');
|
|
34
|
+
|
|
35
|
+
// Action buttons
|
|
36
|
+
const insertBtn = document.getElementById('seed-insert');
|
|
37
|
+
const previewBtn = document.getElementById('seed-preview');
|
|
38
|
+
const resetBtn = document.getElementById('seed-reset');
|
|
39
|
+
const refreshSamplesBtn = document.getElementById('seed-refresh-samples');
|
|
40
|
+
const applyAllBtn = document.getElementById('seed-apply-all');
|
|
41
|
+
const copySqlBtn = document.getElementById('seed-copy-sql');
|
|
42
|
+
const downloadSqlBtn = document.getElementById('seed-download-sql');
|
|
43
|
+
const exportCsvBtn = document.getElementById('seed-export-csv');
|
|
44
|
+
const exportJsonBtn = document.getElementById('seed-export-json');
|
|
45
|
+
const exportConfigBtn = document.getElementById('seed-export-config');
|
|
46
|
+
const importConfigBtn = document.getElementById('seed-import-config');
|
|
47
|
+
|
|
48
|
+
// Modals
|
|
49
|
+
const configDialog = document.getElementById('seed-config-dialog');
|
|
50
|
+
const configDialogTitle = document.getElementById('seed-config-dialog-title');
|
|
51
|
+
const configTextarea = document.getElementById('seed-config-textarea');
|
|
52
|
+
const configDialogCancel = document.getElementById('seed-config-dialog-cancel');
|
|
53
|
+
const configDialogApply = document.getElementById('seed-config-dialog-apply');
|
|
54
|
+
|
|
55
|
+
const truncateDialog = document.getElementById('seed-truncate-dialog');
|
|
56
|
+
const truncateCancel = document.getElementById('seed-truncate-cancel');
|
|
57
|
+
const truncateConfirm = document.getElementById('seed-truncate-confirm');
|
|
58
|
+
|
|
59
|
+
// State
|
|
60
|
+
const state = {};
|
|
61
|
+
let lastGeneratedResult = null;
|
|
62
|
+
|
|
63
|
+
function escapeHtml(s) {
|
|
64
|
+
return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- Client-side Sample Generator (Fast Live Previews) --------------------
|
|
68
|
+
|
|
69
|
+
const SAMPLE_FIRST = ["Sombat", "Minh", "Hiroshi", "William", "Ravi", "Zara", "Elena", "Liam", "Mei", "Noah"];
|
|
70
|
+
const SAMPLE_LAST = ["Chen", "Smith", "Thompson", "Shah", "Yamamoto", "Patel", "Garcia", "Kim", "Nguyen", "Davis"];
|
|
71
|
+
const SAMPLE_DOMAINS = ["example.com", "acme.io", "tech.asia", "gmail.com", "startup.io"];
|
|
72
|
+
const SAMPLE_CITIES = ["New York", "Tokyo", "London", "Singapore", "Paris", "Berlin", "Sydney", "Toronto"];
|
|
73
|
+
const SAMPLE_COUNTRIES = ["United States", "Japan", "United Kingdom", "Singapore", "Germany", "France", "Canada", "Australia"];
|
|
74
|
+
const SAMPLE_COMPANIES = ["Apex Solutions", "Vortex Digital", "Nexus Systems", "Horizon Labs", "Quantum Dynamics", "Zenith Global"];
|
|
75
|
+
const SAMPLE_JOBS = ["Software Engineer", "Product Manager", "UX Designer", "DevOps Specialist", "Data Scientist", "Marketing Director"];
|
|
76
|
+
const SAMPLE_STATUSES = ["active", "pending", "completed", "archived", "draft", "verified"];
|
|
77
|
+
const SAMPLE_CURRENCIES = ["USD", "EUR", "GBP", "JPY", "CAD", "AUD"];
|
|
78
|
+
const SAMPLE_STATES = ["California", "New York", "Texas", "Washington", "Florida", "Illinois"];
|
|
79
|
+
const SAMPLE_WORDS = ["alpha", "beta", "stream", "cloud", "service", "cluster", "record", "node", "signal"];
|
|
80
|
+
|
|
81
|
+
function samplePick(arr) {
|
|
82
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sampleInt(min, max) {
|
|
86
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function samplePattern(pattern) {
|
|
90
|
+
if (!pattern) return 'INV-2026-' + sampleInt(1000, 9999);
|
|
91
|
+
const now = new Date();
|
|
92
|
+
let s = pattern
|
|
93
|
+
.replace(/\{YYYY\}/g, String(now.getFullYear()))
|
|
94
|
+
.replace(/\{YY\}/g, String(now.getFullYear()).slice(-2))
|
|
95
|
+
.replace(/\{MM\}/g, String(sampleInt(1, 12)).padStart(2, '0'))
|
|
96
|
+
.replace(/\{DD\}/g, String(sampleInt(1, 28)).padStart(2, '0'));
|
|
97
|
+
let res = '';
|
|
98
|
+
const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
99
|
+
const lower = 'abcdefghijklmnopqrstuvwxyz';
|
|
100
|
+
for (let i = 0; i < s.length; i++) {
|
|
101
|
+
const ch = s[i];
|
|
102
|
+
if (ch === '#') res += String(sampleInt(0, 9));
|
|
103
|
+
else if (ch === 'A' || ch === '?') res += upper[sampleInt(0, upper.length - 1)];
|
|
104
|
+
else if (ch === 'a') res += lower[sampleInt(0, lower.length - 1)];
|
|
105
|
+
else res += ch;
|
|
106
|
+
}
|
|
107
|
+
return res;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function computeSampleValue(col, plan) {
|
|
111
|
+
const st = plan.strategy;
|
|
112
|
+
if (st === 'skip') return '(omitted / default)';
|
|
113
|
+
if (st === 'null') return 'NULL';
|
|
114
|
+
if (plan.nullPct && sampleInt(1, 100) <= plan.nullPct && !col.pk && !col.notnull) {
|
|
115
|
+
return 'NULL (simulated)';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let val = '';
|
|
119
|
+
switch (st) {
|
|
120
|
+
case 'fixed':
|
|
121
|
+
val = plan.value || '';
|
|
122
|
+
break;
|
|
123
|
+
case 'list': {
|
|
124
|
+
const vals = (plan.values || []).filter((v) => String(v).trim() !== '');
|
|
125
|
+
val = vals.length ? samplePick(vals) : '(empty list)';
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'pattern':
|
|
129
|
+
val = samplePattern(plan.pattern);
|
|
130
|
+
break;
|
|
131
|
+
case 'first':
|
|
132
|
+
val = samplePick(SAMPLE_FIRST);
|
|
133
|
+
break;
|
|
134
|
+
case 'last':
|
|
135
|
+
val = samplePick(SAMPLE_LAST);
|
|
136
|
+
break;
|
|
137
|
+
case 'fullname':
|
|
138
|
+
val = samplePick(SAMPLE_FIRST) + ' ' + samplePick(SAMPLE_LAST);
|
|
139
|
+
break;
|
|
140
|
+
case 'username':
|
|
141
|
+
val = samplePick(SAMPLE_FIRST).toLowerCase() + sampleInt(10, 999);
|
|
142
|
+
break;
|
|
143
|
+
case 'job':
|
|
144
|
+
val = samplePick(SAMPLE_JOBS);
|
|
145
|
+
break;
|
|
146
|
+
case 'company':
|
|
147
|
+
val = samplePick(SAMPLE_COMPANIES);
|
|
148
|
+
break;
|
|
149
|
+
case 'currency':
|
|
150
|
+
val = samplePick(SAMPLE_CURRENCIES);
|
|
151
|
+
break;
|
|
152
|
+
case 'status':
|
|
153
|
+
val = samplePick(SAMPLE_STATUSES);
|
|
154
|
+
break;
|
|
155
|
+
case 'creditCard':
|
|
156
|
+
val = '4532-****-****-' + sampleInt(1000, 9999);
|
|
157
|
+
break;
|
|
158
|
+
case 'avatar':
|
|
159
|
+
val = 'https://api.dicebear.com/7.x/avataaars/svg?seed=' + samplePick(SAMPLE_FIRST).toLowerCase();
|
|
160
|
+
break;
|
|
161
|
+
case 'color':
|
|
162
|
+
val = plan.format === 'name' ? 'Emerald' : '#3b82f6';
|
|
163
|
+
break;
|
|
164
|
+
case 'email':
|
|
165
|
+
val = samplePick(SAMPLE_FIRST).toLowerCase() + '.' + samplePick(SAMPLE_LAST).toLowerCase() + '@' + samplePick(SAMPLE_DOMAINS);
|
|
166
|
+
break;
|
|
167
|
+
case 'phone':
|
|
168
|
+
val = '+1 (' + sampleInt(200, 999) + ') ' + sampleInt(200, 999) + '-' + sampleInt(1000, 9999);
|
|
169
|
+
break;
|
|
170
|
+
case 'city':
|
|
171
|
+
val = samplePick(SAMPLE_CITIES);
|
|
172
|
+
break;
|
|
173
|
+
case 'state':
|
|
174
|
+
val = samplePick(SAMPLE_STATES);
|
|
175
|
+
break;
|
|
176
|
+
case 'country':
|
|
177
|
+
val = samplePick(SAMPLE_COUNTRIES);
|
|
178
|
+
break;
|
|
179
|
+
case 'countryCode':
|
|
180
|
+
val = samplePick(['US', 'GB', 'CA', 'DE', 'JP', 'FR', 'AU', 'SG']);
|
|
181
|
+
break;
|
|
182
|
+
case 'postal':
|
|
183
|
+
val = String(sampleInt(10000, 99999));
|
|
184
|
+
break;
|
|
185
|
+
case 'address':
|
|
186
|
+
val = sampleInt(100, 9999) + ' ' + samplePick(SAMPLE_WORDS) + ' Ave, ' + samplePick(SAMPLE_CITIES);
|
|
187
|
+
break;
|
|
188
|
+
case 'url':
|
|
189
|
+
val = 'https://www.' + samplePick(SAMPLE_DOMAINS) + '/' + samplePick(SAMPLE_WORDS);
|
|
190
|
+
break;
|
|
191
|
+
case 'domain':
|
|
192
|
+
val = samplePick(SAMPLE_DOMAINS);
|
|
193
|
+
break;
|
|
194
|
+
case 'ip':
|
|
195
|
+
val = sampleInt(1, 223) + '.' + sampleInt(0, 255) + '.' + sampleInt(0, 255) + '.' + sampleInt(1, 254);
|
|
196
|
+
break;
|
|
197
|
+
case 'mac':
|
|
198
|
+
val = '00:1A:2B:' + sampleInt(10, 99) + ':' + sampleInt(10, 99) + ':' + sampleInt(10, 99);
|
|
199
|
+
break;
|
|
200
|
+
case 'uuid':
|
|
201
|
+
val = '550e8400-e29b-41d4-a716-' + sampleInt(100000000000, 999999999999);
|
|
202
|
+
break;
|
|
203
|
+
case 'slug':
|
|
204
|
+
val = samplePick(SAMPLE_WORDS) + '-' + samplePick(SAMPLE_WORDS) + '-' + sampleInt(100, 999);
|
|
205
|
+
break;
|
|
206
|
+
case 'words':
|
|
207
|
+
val = samplePick(SAMPLE_WORDS) + ' ' + samplePick(SAMPLE_WORDS);
|
|
208
|
+
break;
|
|
209
|
+
case 'sentence':
|
|
210
|
+
val = 'Lorem ipsum dolor sit ' + samplePick(SAMPLE_WORDS) + ' amet.';
|
|
211
|
+
break;
|
|
212
|
+
case 'paragraph':
|
|
213
|
+
val = 'Lorem ipsum dolor sit amet. Consectetur adipiscing elit ' + samplePick(SAMPLE_WORDS) + ' dynamicus.';
|
|
214
|
+
break;
|
|
215
|
+
case 'latitude':
|
|
216
|
+
val = (Math.random() * 180 - 90).toFixed(6);
|
|
217
|
+
break;
|
|
218
|
+
case 'longitude':
|
|
219
|
+
val = (Math.random() * 360 - 180).toFixed(6);
|
|
220
|
+
break;
|
|
221
|
+
case 'sequence':
|
|
222
|
+
val = String((plan.start || 1));
|
|
223
|
+
break;
|
|
224
|
+
case 'int':
|
|
225
|
+
val = String(sampleInt(plan.min ?? 1, plan.max ?? 1000));
|
|
226
|
+
break;
|
|
227
|
+
case 'decimal': {
|
|
228
|
+
const min = plan.min ?? 1;
|
|
229
|
+
const max = plan.max ?? 1000;
|
|
230
|
+
const prec = plan.precision ?? 2;
|
|
231
|
+
val = (Math.random() * (max - min) + min).toFixed(prec);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case 'date':
|
|
235
|
+
val = '2026-03-' + String(sampleInt(1, 28)).padStart(2, '0');
|
|
236
|
+
break;
|
|
237
|
+
case 'datetime':
|
|
238
|
+
val = '2026-03-' + String(sampleInt(1, 28)).padStart(2, '0') + 'T14:30:00.000Z';
|
|
239
|
+
break;
|
|
240
|
+
case 'time':
|
|
241
|
+
val = '14:' + String(sampleInt(10, 59)) + ':00';
|
|
242
|
+
break;
|
|
243
|
+
case 'timestampUnix':
|
|
244
|
+
val = plan.format === 'ms' ? String(Date.now()) : String(Math.floor(Date.now() / 1000));
|
|
245
|
+
break;
|
|
246
|
+
case 'bool':
|
|
247
|
+
val = samplePick(['1', '0']);
|
|
248
|
+
break;
|
|
249
|
+
case 'bytes':
|
|
250
|
+
val = '0x' + sampleInt(100000, 999999).toString(16);
|
|
251
|
+
break;
|
|
252
|
+
case 'json':
|
|
253
|
+
val = '{"scope":"read-only","enabled":true}';
|
|
254
|
+
break;
|
|
255
|
+
case 'hash':
|
|
256
|
+
val = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'.slice(0, plan.maxLen || 32);
|
|
257
|
+
break;
|
|
258
|
+
case 'token':
|
|
259
|
+
val = 'tk_' + sampleInt(100000, 999999);
|
|
260
|
+
break;
|
|
261
|
+
case 'fk':
|
|
262
|
+
val = col.fk ? '[Sample from ' + col.fk.table + ']' : 'NULL';
|
|
263
|
+
break;
|
|
264
|
+
default:
|
|
265
|
+
val = '(auto)';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (plan.prefix || plan.suffix) {
|
|
269
|
+
val = (plan.prefix || '') + val + (plan.suffix || '');
|
|
270
|
+
}
|
|
271
|
+
return val;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---- Field Definitions per Strategy -------------------------------------
|
|
275
|
+
|
|
276
|
+
function field(key, label, type, attrs) {
|
|
277
|
+
return Object.assign({ key, label, type }, attrs || {});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const FIELD_DEFS = {
|
|
281
|
+
fixed: [field('value', 'Value', 'text')],
|
|
282
|
+
list: [field('values', 'One value per line', 'textarea')],
|
|
283
|
+
pattern: [field('pattern', 'Pattern (tokens: #=digit, ?=letter, {YYYY})', 'text', { placeholder: 'INV-{YYYY}-####' })],
|
|
284
|
+
int: [field('min', 'Min', 'number'), field('max', 'Max', 'number')],
|
|
285
|
+
decimal: [field('min', 'Min', 'number'), field('max', 'Max', 'number'), field('precision', 'Decimals', 'number', { step: 1, min: 0, max: 10 })],
|
|
286
|
+
sequence: [field('start', 'Start from', 'number', { placeholder: '1' }), field('step', 'Step increment', 'number', { placeholder: '1' })],
|
|
287
|
+
date: [field('from', 'From', 'date'), field('to', 'To', 'date')],
|
|
288
|
+
datetime: [field('from', 'From', 'datetime-local'), field('to', 'To', 'datetime-local')],
|
|
289
|
+
time: [],
|
|
290
|
+
timestampUnix: [field('format', 'Format (sec / ms)', 'select', { options: [{ value: 'sec', label: 'Seconds (10-digit)' }, { value: 'ms', label: 'Milliseconds (13-digit)' }] })],
|
|
291
|
+
color: [field('format', 'Color format', 'select', { options: [{ value: 'hex', label: 'Hex (#3b82f6)' }, { value: 'name', label: 'Color Name (Emerald)' }] })],
|
|
292
|
+
latitude: [field('min', 'Min Lat', 'number', { step: 0.0001, placeholder: '-90' }), field('max', 'Max Lat', 'number', { step: 0.0001, placeholder: '90' })],
|
|
293
|
+
longitude: [field('min', 'Min Lng', 'number', { step: 0.0001, placeholder: '-180' }), field('max', 'Max Lng', 'number', { step: 0.0001, placeholder: '180' })],
|
|
294
|
+
words: [field('minLen', 'Min words', 'number'), field('maxLen', 'Max words', 'number')],
|
|
295
|
+
sentence: [field('minLen', 'Min sentences', 'number'), field('maxLen', 'Max sentences', 'number')],
|
|
296
|
+
paragraph: [],
|
|
297
|
+
bytes: [field('minLen', 'Min bytes', 'number'), field('maxLen', 'Max bytes', 'number')],
|
|
298
|
+
json: [field('jsonKeys', 'JSON keys (comma-separated)', 'text')],
|
|
299
|
+
hash: [field('minLen', 'Min length', 'number'), field('maxLen', 'Max length', 'number')],
|
|
300
|
+
token: [field('minLen', 'Min length', 'number'), field('maxLen', 'Max length', 'number')],
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const CATEGORY_TITLES = {
|
|
304
|
+
identity: '👤 Identity & People',
|
|
305
|
+
contact_web: '🌐 Contact & Web',
|
|
306
|
+
commerce: '🏢 Commerce & Business',
|
|
307
|
+
location: '📍 Location',
|
|
308
|
+
numeric: '🔢 Numbers & Math',
|
|
309
|
+
datetime: '📅 Date & Time',
|
|
310
|
+
system_crypto: '🔒 System & Crypto',
|
|
311
|
+
custom_control: '⚙️ Custom & Control',
|
|
312
|
+
relational: '🔗 Relational (Foreign Key)',
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
function strategyNote(col, strategy) {
|
|
316
|
+
if (strategy === 'fk' && col.fk) return 'Sampled from foreign table ' + escapeHtml(col.fk.table) + (col.fk.to ? '.' + escapeHtml(col.fk.to) : '') + '.';
|
|
317
|
+
if (strategy === 'skip') return 'Omitted from INSERT — DB default or NULL applies.';
|
|
318
|
+
if (strategy === 'null') return 'Explicitly set to NULL for all rows.';
|
|
319
|
+
if (strategy === 'pattern') return 'Replaces # with digits, ? with letters, and {YYYY} with year.';
|
|
320
|
+
if (strategy === 'sequence') return 'Generates sequentially incrementing integers across rows.';
|
|
321
|
+
if (strategy === 'json') return 'Generates valid JSON objects compliant with indexes and defaults.';
|
|
322
|
+
if (strategy === 'hash') return 'Generates random cryptographic hex hashes.';
|
|
323
|
+
if (strategy === 'token') return 'Generates short alphanumeric identifier tokens.';
|
|
324
|
+
return '';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---- Card Building ------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
function updateCardSample(col, card) {
|
|
330
|
+
const sampleEl = card.querySelector('.seed-sample-value');
|
|
331
|
+
if (sampleEl) {
|
|
332
|
+
const sample = computeSampleValue(col, state[col.name] || {});
|
|
333
|
+
sampleEl.textContent = sample;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function buildOptionInputs(container, col, plan, card) {
|
|
338
|
+
container.innerHTML = '';
|
|
339
|
+
const fields = FIELD_DEFS[plan.strategy] || [];
|
|
340
|
+
|
|
341
|
+
const hasOptions = fields.length > 0;
|
|
342
|
+
const isNullable = !col.pk && !col.notnull;
|
|
343
|
+
const isTextish = !['int', 'decimal', 'bool', 'bytes', 'skip', 'null'].includes(plan.strategy);
|
|
344
|
+
|
|
345
|
+
if (!hasOptions && !isNullable) return;
|
|
346
|
+
|
|
347
|
+
const wrap = document.createElement('div');
|
|
348
|
+
wrap.className = 'space-y-2 pt-2 border-t border-base-200/60';
|
|
349
|
+
|
|
350
|
+
if (fields.length) {
|
|
351
|
+
const grid = document.createElement('div');
|
|
352
|
+
grid.className = 'grid grid-cols-2 gap-2';
|
|
353
|
+
fields.forEach((f) => {
|
|
354
|
+
const fieldWrap = document.createElement('label');
|
|
355
|
+
fieldWrap.className = 'block' + (f.type === 'textarea' || f.key === 'pattern' || f.key === 'jsonKeys' ? ' col-span-2' : '');
|
|
356
|
+
const label = document.createElement('span');
|
|
357
|
+
label.className = 'field-label !mb-1 !text-[11px] font-medium text-base-content/70';
|
|
358
|
+
label.textContent = f.label;
|
|
359
|
+
fieldWrap.appendChild(label);
|
|
360
|
+
|
|
361
|
+
let el;
|
|
362
|
+
if (f.type === 'textarea') {
|
|
363
|
+
el = document.createElement('textarea');
|
|
364
|
+
el.rows = 3;
|
|
365
|
+
el.className = 'field-input w-full font-mono !text-[11px] leading-tight';
|
|
366
|
+
el.placeholder = 'one value per line';
|
|
367
|
+
el.spellcheck = false;
|
|
368
|
+
if (Array.isArray(plan.values)) el.value = plan.values.join('\n');
|
|
369
|
+
} else if (f.type === 'select') {
|
|
370
|
+
el = document.createElement('select');
|
|
371
|
+
el.className = 'field-select !py-1 !text-[12px]';
|
|
372
|
+
(f.options || []).forEach((opt) => {
|
|
373
|
+
const o = document.createElement('option');
|
|
374
|
+
o.value = opt.value;
|
|
375
|
+
o.textContent = opt.label;
|
|
376
|
+
if (plan[f.key] === opt.value) o.selected = true;
|
|
377
|
+
el.appendChild(o);
|
|
378
|
+
});
|
|
379
|
+
} else {
|
|
380
|
+
el = document.createElement('input');
|
|
381
|
+
el.type = f.type;
|
|
382
|
+
el.className = 'field-input !py-1 !text-[12px]';
|
|
383
|
+
if (f.step != null) el.step = String(f.step);
|
|
384
|
+
if (f.min != null) el.min = String(f.min);
|
|
385
|
+
if (f.max != null) el.max = String(f.max);
|
|
386
|
+
if (f.placeholder) el.placeholder = f.placeholder;
|
|
387
|
+
const v = plan[f.key];
|
|
388
|
+
if (f.key === 'jsonKeys' && Array.isArray(v)) {
|
|
389
|
+
el.value = v.join(', ');
|
|
390
|
+
} else if (v !== undefined && v !== null) {
|
|
391
|
+
el.value = String(v);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
el.dataset.optionKey = f.key;
|
|
396
|
+
el.addEventListener('input', () => {
|
|
397
|
+
const key = el.dataset.optionKey;
|
|
398
|
+
if (f.type === 'textarea') {
|
|
399
|
+
state[col.name][key] = el.value.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
400
|
+
} else if (key === 'jsonKeys') {
|
|
401
|
+
state[col.name][key] = el.value.split(/[,;]/).map((s) => s.trim()).filter(Boolean);
|
|
402
|
+
} else if (key === 'value' || key === 'from' || key === 'to' || key === 'pattern' || key === 'format') {
|
|
403
|
+
state[col.name][key] = el.value;
|
|
404
|
+
} else {
|
|
405
|
+
const n = Number(el.value);
|
|
406
|
+
state[col.name][key] = el.value === '' || !Number.isFinite(n) ? undefined : n;
|
|
407
|
+
}
|
|
408
|
+
updateCardSample(col, card);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
fieldWrap.appendChild(el);
|
|
412
|
+
grid.appendChild(fieldWrap);
|
|
413
|
+
});
|
|
414
|
+
wrap.appendChild(grid);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Null percentage simulation for nullable columns
|
|
418
|
+
if (isNullable && plan.strategy !== 'skip' && plan.strategy !== 'null') {
|
|
419
|
+
const nullRow = document.createElement('div');
|
|
420
|
+
nullRow.className = 'flex items-center justify-between gap-2 pt-1.5 text-[11px]';
|
|
421
|
+
nullRow.innerHTML =
|
|
422
|
+
'<span class="text-base-content/60">Simulate Nulls:</span>' +
|
|
423
|
+
'<div class="flex items-center gap-1.5">' +
|
|
424
|
+
'<input type="range" min="0" max="100" step="5" value="' + (plan.nullPct || 0) + '" class="range range-xs range-primary w-24" />' +
|
|
425
|
+
'<span class="font-mono text-[11px] w-8 text-right text-base-content/75">' + (plan.nullPct || 0) + '%</span>' +
|
|
426
|
+
'</div>';
|
|
427
|
+
const rangeInput = nullRow.querySelector('input[type="range"]');
|
|
428
|
+
const valSpan = nullRow.querySelector('span:last-child');
|
|
429
|
+
rangeInput.addEventListener('input', () => {
|
|
430
|
+
const val = Number(rangeInput.value);
|
|
431
|
+
valSpan.textContent = val + '%';
|
|
432
|
+
state[col.name].nullPct = val > 0 ? val : undefined;
|
|
433
|
+
updateCardSample(col, card);
|
|
434
|
+
});
|
|
435
|
+
wrap.appendChild(nullRow);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
container.appendChild(wrap);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function buildCard(col) {
|
|
442
|
+
const card = document.createElement('div');
|
|
443
|
+
card.className = 'app-card p-4 flex flex-col justify-between transition-shadow hover:shadow-md';
|
|
444
|
+
card.dataset.column = col.name;
|
|
445
|
+
card.dataset.type = (col.type || '').toLowerCase();
|
|
446
|
+
|
|
447
|
+
// Top Header
|
|
448
|
+
const top = document.createElement('div');
|
|
449
|
+
top.className = 'space-y-3';
|
|
450
|
+
|
|
451
|
+
const head = document.createElement('div');
|
|
452
|
+
head.className = 'flex items-start justify-between gap-2';
|
|
453
|
+
|
|
454
|
+
const titleBox = document.createElement('div');
|
|
455
|
+
titleBox.className = 'min-w-0';
|
|
456
|
+
const nameRow = document.createElement('div');
|
|
457
|
+
nameRow.className = 'flex flex-wrap items-center gap-1.5';
|
|
458
|
+
const name = document.createElement('span');
|
|
459
|
+
name.className = 'font-mono text-sm font-bold tracking-tight text-base-content';
|
|
460
|
+
name.textContent = col.name;
|
|
461
|
+
nameRow.appendChild(name);
|
|
462
|
+
|
|
463
|
+
if (col.pk) {
|
|
464
|
+
const chip = document.createElement('span');
|
|
465
|
+
chip.className = 'badge badge-xs badge-warning font-mono font-bold';
|
|
466
|
+
chip.textContent = 'PK';
|
|
467
|
+
nameRow.appendChild(chip);
|
|
468
|
+
}
|
|
469
|
+
if (col.notnull) {
|
|
470
|
+
const chip = document.createElement('span');
|
|
471
|
+
chip.className = 'badge badge-xs badge-info font-mono';
|
|
472
|
+
chip.textContent = 'NOT NULL';
|
|
473
|
+
nameRow.appendChild(chip);
|
|
474
|
+
} else {
|
|
475
|
+
const chip = document.createElement('span');
|
|
476
|
+
chip.className = 'badge badge-xs badge-ghost text-base-content/40 font-mono';
|
|
477
|
+
chip.textContent = 'NULLABLE';
|
|
478
|
+
nameRow.appendChild(chip);
|
|
479
|
+
}
|
|
480
|
+
if (col.unique) {
|
|
481
|
+
const chip = document.createElement('span');
|
|
482
|
+
chip.className = 'badge badge-xs badge-secondary font-mono';
|
|
483
|
+
chip.textContent = 'UNIQUE';
|
|
484
|
+
nameRow.appendChild(chip);
|
|
485
|
+
}
|
|
486
|
+
if (col.hasDefault) {
|
|
487
|
+
const chip = document.createElement('span');
|
|
488
|
+
chip.className = 'badge badge-xs badge-ghost font-mono';
|
|
489
|
+
chip.textContent = 'DEFAULT';
|
|
490
|
+
nameRow.appendChild(chip);
|
|
491
|
+
}
|
|
492
|
+
if (col.fk) {
|
|
493
|
+
const chip = document.createElement('span');
|
|
494
|
+
chip.className = 'badge badge-xs badge-accent font-mono';
|
|
495
|
+
chip.textContent = 'FK → ' + col.fk.table;
|
|
496
|
+
nameRow.appendChild(chip);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const typeDesc = document.createElement('p');
|
|
500
|
+
typeDesc.className = 'mt-0.5 text-[11px] font-mono text-base-content/45';
|
|
501
|
+
typeDesc.textContent = col.type || 'untyped';
|
|
502
|
+
|
|
503
|
+
titleBox.appendChild(nameRow);
|
|
504
|
+
titleBox.appendChild(typeDesc);
|
|
505
|
+
head.appendChild(titleBox);
|
|
506
|
+
top.appendChild(head);
|
|
507
|
+
|
|
508
|
+
// Strategy Dropdown with Categorized <optgroup>s
|
|
509
|
+
const select = document.createElement('select');
|
|
510
|
+
select.className = 'field-select seed-strategy w-full !py-1.5 !text-xs font-medium';
|
|
511
|
+
select.dataset.column = col.name;
|
|
512
|
+
|
|
513
|
+
const grouped = {};
|
|
514
|
+
(col.strategies || []).forEach((s) => {
|
|
515
|
+
const cat = s.category || 'custom_control';
|
|
516
|
+
if (!grouped[cat]) grouped[cat] = [];
|
|
517
|
+
grouped[cat].push(s);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
Object.keys(grouped).forEach((cat) => {
|
|
521
|
+
const grp = document.createElement('optgroup');
|
|
522
|
+
grp.label = CATEGORY_TITLES[cat] || cat;
|
|
523
|
+
grouped[cat].forEach((s) => {
|
|
524
|
+
const opt = document.createElement('option');
|
|
525
|
+
opt.value = s.id;
|
|
526
|
+
opt.textContent = s.label;
|
|
527
|
+
grp.appendChild(opt);
|
|
528
|
+
});
|
|
529
|
+
select.appendChild(grp);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
top.appendChild(select);
|
|
533
|
+
|
|
534
|
+
// Live Sample Chip
|
|
535
|
+
const sampleBox = document.createElement('div');
|
|
536
|
+
sampleBox.className = 'flex items-center justify-between gap-2 px-2.5 py-1.5 rounded-lg bg-base-200/50 text-[11px]';
|
|
537
|
+
sampleBox.innerHTML =
|
|
538
|
+
'<div class="flex items-center gap-1.5 min-w-0 overflow-hidden text-ellipsis">' +
|
|
539
|
+
'<span class="text-base-content/50 shrink-0 font-medium">Sample:</span>' +
|
|
540
|
+
'<span class="seed-sample-value font-mono text-primary font-medium truncate">...</span>' +
|
|
541
|
+
'</div>' +
|
|
542
|
+
'<button type="button" class="seed-sample-refresh btn btn-ghost btn-xs btn-square h-5 w-5 shrink-0" title="Reroll sample">' +
|
|
543
|
+
'🎲' +
|
|
544
|
+
'</button>';
|
|
545
|
+
|
|
546
|
+
const sampleRefreshBtn = sampleBox.querySelector('.seed-sample-refresh');
|
|
547
|
+
sampleRefreshBtn.addEventListener('click', () => updateCardSample(col, card));
|
|
548
|
+
top.appendChild(sampleBox);
|
|
549
|
+
|
|
550
|
+
// Strategy-specific options area
|
|
551
|
+
const options = document.createElement('div');
|
|
552
|
+
options.className = 'seed-options';
|
|
553
|
+
top.appendChild(options);
|
|
554
|
+
|
|
555
|
+
card.appendChild(top);
|
|
556
|
+
|
|
557
|
+
// Bottom notes & warnings
|
|
558
|
+
const bottom = document.createElement('div');
|
|
559
|
+
bottom.className = 'mt-3 pt-2';
|
|
560
|
+
|
|
561
|
+
const note = document.createElement('p');
|
|
562
|
+
note.className = 'seed-note hidden text-[11px] leading-relaxed text-base-content/45';
|
|
563
|
+
bottom.appendChild(note);
|
|
564
|
+
|
|
565
|
+
const warn = document.createElement('p');
|
|
566
|
+
warn.className = 'seed-warn hidden text-[11px] font-medium text-warning flex items-center gap-1';
|
|
567
|
+
bottom.appendChild(warn);
|
|
568
|
+
|
|
569
|
+
card.appendChild(bottom);
|
|
570
|
+
|
|
571
|
+
// Event listener for strategy selection change
|
|
572
|
+
select.addEventListener('change', () => {
|
|
573
|
+
const st = select.value;
|
|
574
|
+
state[col.name] = Object.assign({}, state[col.name] || {}, { strategy: st });
|
|
575
|
+
buildOptionInputs(options, col, state[col.name], card);
|
|
576
|
+
note.textContent = strategyNote(col, st);
|
|
577
|
+
note.classList.toggle('hidden', !note.textContent);
|
|
578
|
+
updateWarn(col, warn);
|
|
579
|
+
updateCardSample(col, card);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
columnsBox.appendChild(card);
|
|
583
|
+
return { select, options, note, warn };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function updateWarn(col, warnEl) {
|
|
587
|
+
const st = state[col.name] ? state[col.name].strategy : null;
|
|
588
|
+
const risky = (st === 'skip' || st === 'null') && !col.pk && col.notnull && !col.hasDefault;
|
|
589
|
+
warnEl.textContent = risky
|
|
590
|
+
? '⚠️ Column is NOT NULL with no default — "' + st + '" rows may fail.'
|
|
591
|
+
: '';
|
|
592
|
+
warnEl.classList.toggle('hidden', !warnEl.textContent);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function resetCard(col) {
|
|
596
|
+
const plan = Object.assign({}, col.defaultPlan || { strategy: 'skip' });
|
|
597
|
+
state[col.name] = plan;
|
|
598
|
+
const card = columnsBox.querySelector('.app-card[data-column="' + CSS.escape(col.name) + '"]');
|
|
599
|
+
if (card) {
|
|
600
|
+
const select = card.querySelector('.seed-strategy');
|
|
601
|
+
const options = card.querySelector('.seed-options');
|
|
602
|
+
const note = card.querySelector('.seed-note');
|
|
603
|
+
const warn = card.querySelector('.seed-warn');
|
|
604
|
+
if (select) select.value = plan.strategy;
|
|
605
|
+
buildOptionInputs(options, col, plan, card);
|
|
606
|
+
note.textContent = strategyNote(col, plan.strategy);
|
|
607
|
+
note.classList.toggle('hidden', !note.textContent);
|
|
608
|
+
updateWarn(col, warn);
|
|
609
|
+
updateCardSample(col, card);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ---- Filtering / Searching ----------------------------------------------
|
|
614
|
+
|
|
615
|
+
function filterColumns() {
|
|
616
|
+
const q = (searchInput ? searchInput.value : '').toLowerCase().trim();
|
|
617
|
+
let visible = 0;
|
|
618
|
+
columns.forEach((col) => {
|
|
619
|
+
const card = columnsBox.querySelector('.app-card[data-column="' + CSS.escape(col.name) + '"]');
|
|
620
|
+
if (!card) return;
|
|
621
|
+
const match = !q || col.name.toLowerCase().includes(q) || (col.type || '').toLowerCase().includes(q);
|
|
622
|
+
card.classList.toggle('hidden', !match);
|
|
623
|
+
if (match) visible += 1;
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
if (emptySearchBox) emptySearchBox.classList.toggle('hidden', visible > 0);
|
|
627
|
+
if (colCountBadge) colCountBadge.textContent = visible === columns.length ? `${columns.length} columns` : `${visible} of ${columns.length} columns`;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ---- Presets ------------------------------------------------------------
|
|
631
|
+
|
|
632
|
+
function applyPreset(type) {
|
|
633
|
+
columns.forEach((col) => {
|
|
634
|
+
if (type === 'realistic') {
|
|
635
|
+
resetCard(col);
|
|
636
|
+
} else if (type === 'minimal') {
|
|
637
|
+
if (!col.pk && !col.notnull && !col.hasDefault) {
|
|
638
|
+
state[col.name] = { strategy: 'null' };
|
|
639
|
+
} else {
|
|
640
|
+
state[col.name] = Object.assign({}, col.defaultPlan || { strategy: 'skip' });
|
|
641
|
+
}
|
|
642
|
+
} else if (type === 'sparse') {
|
|
643
|
+
state[col.name] = Object.assign({}, col.defaultPlan || { strategy: 'skip' });
|
|
644
|
+
if (!col.pk && !col.notnull) {
|
|
645
|
+
state[col.name].nullPct = 30;
|
|
646
|
+
}
|
|
647
|
+
} else if (type === 'numbered') {
|
|
648
|
+
const t = (col.type || '').toUpperCase();
|
|
649
|
+
if (t.startsWith('INTEGER')) {
|
|
650
|
+
state[col.name] = { strategy: 'sequence', start: 1, step: 1 };
|
|
651
|
+
} else if (t === 'TEXT' || t === '') {
|
|
652
|
+
state[col.name] = { strategy: 'pattern', pattern: 'REC-####' };
|
|
653
|
+
} else {
|
|
654
|
+
state[col.name] = Object.assign({}, col.defaultPlan || { strategy: 'skip' });
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Sync DOM
|
|
659
|
+
const card = columnsBox.querySelector('.app-card[data-column="' + CSS.escape(col.name) + '"]');
|
|
660
|
+
if (card) {
|
|
661
|
+
const select = card.querySelector('.seed-strategy');
|
|
662
|
+
const options = card.querySelector('.seed-options');
|
|
663
|
+
const note = card.querySelector('.seed-note');
|
|
664
|
+
const warn = card.querySelector('.seed-warn');
|
|
665
|
+
if (select) select.value = state[col.name].strategy;
|
|
666
|
+
buildOptionInputs(options, col, state[col.name], card);
|
|
667
|
+
note.textContent = strategyNote(col, state[col.name].strategy);
|
|
668
|
+
note.classList.toggle('hidden', !note.textContent);
|
|
669
|
+
updateWarn(col, warn);
|
|
670
|
+
updateCardSample(col, card);
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
if (window.UI && UI.showToast) UI.showToast('Applied preset: ' + type, 'info');
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ---- Apply strategy to all ---------------------------------------------
|
|
678
|
+
|
|
679
|
+
function unionStrategies() {
|
|
680
|
+
const map = new Map();
|
|
681
|
+
columns.forEach((col) => (col.strategies || []).forEach((s) => map.set(s.id, s.label)));
|
|
682
|
+
return [...map.entries()].map(([id, label]) => ({ id, label }));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function buildApplyAllDialog() {
|
|
686
|
+
const dialog = document.createElement('dialog');
|
|
687
|
+
dialog.className = 'modal';
|
|
688
|
+
dialog.id = 'seed-apply-all-dialog';
|
|
689
|
+
const inner = document.createElement('div');
|
|
690
|
+
inner.className = 'modal-box app-card max-w-md';
|
|
691
|
+
inner.innerHTML =
|
|
692
|
+
'<h3 class="text-base font-bold">Apply strategy to all columns</h3>' +
|
|
693
|
+
'<p class="mt-1 text-xs text-base-content/60">Columns that do not support the chosen strategy are left untouched.</p>' +
|
|
694
|
+
'<label class="field-label mt-4 text-xs font-medium">Strategy</label>' +
|
|
695
|
+
'<select id="seed-apply-all-select" class="field-select w-full text-xs"></select>' +
|
|
696
|
+
'<div class="mt-5 flex justify-end gap-2">' +
|
|
697
|
+
'<button id="seed-apply-all-cancel" type="button" class="btn btn-ghost btn-sm">Cancel</button>' +
|
|
698
|
+
'<button id="seed-apply-all-go" type="button" class="btn btn-primary btn-sm">Apply to All</button>' +
|
|
699
|
+
'</div>';
|
|
700
|
+
dialog.appendChild(inner);
|
|
701
|
+
document.body.appendChild(dialog);
|
|
702
|
+
|
|
703
|
+
const select = inner.querySelector('#seed-apply-all-select');
|
|
704
|
+
unionStrategies().forEach((s) => {
|
|
705
|
+
const opt = document.createElement('option');
|
|
706
|
+
opt.value = s.id;
|
|
707
|
+
opt.textContent = s.label;
|
|
708
|
+
select.appendChild(opt);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const close = () => dialog.close();
|
|
712
|
+
inner.querySelector('#seed-apply-all-cancel').addEventListener('click', close);
|
|
713
|
+
inner.querySelector('#seed-apply-all-go').addEventListener('click', () => {
|
|
714
|
+
const target = select.value;
|
|
715
|
+
columns.forEach((col) => {
|
|
716
|
+
const supported = (col.strategies || []).some((s) => s.id === target);
|
|
717
|
+
if (!supported) return;
|
|
718
|
+
state[col.name] = Object.assign({}, state[col.name] || {}, { strategy: target });
|
|
719
|
+
const card = columnsBox.querySelector('.app-card[data-column="' + CSS.escape(col.name) + '"]');
|
|
720
|
+
if (card) {
|
|
721
|
+
const sel = card.querySelector('.seed-strategy');
|
|
722
|
+
const options = card.querySelector('.seed-options');
|
|
723
|
+
const note = card.querySelector('.seed-note');
|
|
724
|
+
const warn = card.querySelector('.seed-warn');
|
|
725
|
+
if (sel) sel.value = target;
|
|
726
|
+
buildOptionInputs(options, col, state[col.name], card);
|
|
727
|
+
note.textContent = strategyNote(col, target);
|
|
728
|
+
note.classList.toggle('hidden', !note.textContent);
|
|
729
|
+
updateWarn(col, warn);
|
|
730
|
+
updateCardSample(col, card);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
close();
|
|
734
|
+
if (window.UI && UI.showToast) UI.showToast('Strategy applied across columns.', 'success');
|
|
735
|
+
});
|
|
736
|
+
return dialog;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ---- Dual-Mode Preview Rendering ---------------------------------------
|
|
740
|
+
|
|
741
|
+
function showFeedback(warnings) {
|
|
742
|
+
if (!feedback) return;
|
|
743
|
+
if (!warnings || !warnings.length) {
|
|
744
|
+
feedback.innerHTML = '';
|
|
745
|
+
feedback.classList.add('hidden');
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
feedback.innerHTML =
|
|
749
|
+
'<div class="alert alert-warning shadow-soft">' +
|
|
750
|
+
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>' +
|
|
751
|
+
'<div class="text-xs space-y-1">' + warnings.map((w) => '<p>' + escapeHtml(w) + '</p>').join('') + '</div>' +
|
|
752
|
+
'</div>';
|
|
753
|
+
feedback.classList.remove('hidden');
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function renderPreview(data) {
|
|
757
|
+
lastGeneratedResult = data;
|
|
758
|
+
if (!previewCard) return;
|
|
759
|
+
|
|
760
|
+
previewCard.classList.remove('hidden');
|
|
761
|
+
if (previewSummaryBadge) {
|
|
762
|
+
previewSummaryBadge.textContent = `${data.count} row(s) generated`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// 1. SQL View
|
|
766
|
+
if (sqlCode) sqlCode.textContent = data.sql || '';
|
|
767
|
+
|
|
768
|
+
// 2. Table View
|
|
769
|
+
if (previewThead && previewTbody) {
|
|
770
|
+
previewThead.innerHTML = '';
|
|
771
|
+
previewTbody.innerHTML = '';
|
|
772
|
+
|
|
773
|
+
const trHead = document.createElement('tr');
|
|
774
|
+
const thIdx = document.createElement('th');
|
|
775
|
+
thIdx.className = 'w-12 text-center text-base-content/40';
|
|
776
|
+
thIdx.textContent = '#';
|
|
777
|
+
trHead.appendChild(thIdx);
|
|
778
|
+
|
|
779
|
+
columns.forEach((col) => {
|
|
780
|
+
const th = document.createElement('th');
|
|
781
|
+
th.className = 'whitespace-nowrap font-mono';
|
|
782
|
+
th.innerHTML = escapeHtml(col.name) + ' <span class="text-[10px] text-base-content/40 font-normal">(' + escapeHtml(col.type || 'text') + ')</span>';
|
|
783
|
+
trHead.appendChild(th);
|
|
784
|
+
});
|
|
785
|
+
previewThead.appendChild(trHead);
|
|
786
|
+
|
|
787
|
+
const rows = data.previewRows || [];
|
|
788
|
+
rows.forEach((row, i) => {
|
|
789
|
+
const tr = document.createElement('tr');
|
|
790
|
+
const tdIdx = document.createElement('td');
|
|
791
|
+
tdIdx.className = 'text-center font-mono text-[10px] text-base-content/40';
|
|
792
|
+
tdIdx.textContent = String(i + 1);
|
|
793
|
+
tr.appendChild(tdIdx);
|
|
794
|
+
|
|
795
|
+
columns.forEach((col) => {
|
|
796
|
+
const td = document.createElement('td');
|
|
797
|
+
td.className = 'font-mono whitespace-nowrap max-w-xs truncate';
|
|
798
|
+
const val = row[col.name];
|
|
799
|
+
if (val === null) {
|
|
800
|
+
td.innerHTML = '<span class="badge badge-xs badge-ghost text-base-content/40">NULL</span>';
|
|
801
|
+
} else if (val === '(default)') {
|
|
802
|
+
td.innerHTML = '<span class="badge badge-xs badge-outline text-base-content/40">DEFAULT</span>';
|
|
803
|
+
} else if (typeof val === 'boolean') {
|
|
804
|
+
td.innerHTML = val ? '<span class="badge badge-xs badge-success">TRUE</span>' : '<span class="badge badge-xs badge-ghost">FALSE</span>';
|
|
805
|
+
} else {
|
|
806
|
+
td.textContent = String(val);
|
|
807
|
+
}
|
|
808
|
+
tr.appendChild(td);
|
|
809
|
+
});
|
|
810
|
+
previewTbody.appendChild(tr);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
previewCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function setPreviewTab(tab) {
|
|
818
|
+
if (tab === 'table') {
|
|
819
|
+
tabTableView.classList.add('tab-active');
|
|
820
|
+
tabSqlView.classList.remove('tab-active');
|
|
821
|
+
tableViewContainer.classList.remove('hidden');
|
|
822
|
+
sqlViewContainer.classList.add('hidden');
|
|
823
|
+
} else {
|
|
824
|
+
tabSqlView.classList.add('tab-active');
|
|
825
|
+
tabTableView.classList.remove('tab-active');
|
|
826
|
+
sqlViewContainer.classList.remove('hidden');
|
|
827
|
+
tableViewContainer.classList.add('hidden');
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// ---- Export Handlers ----------------------------------------------------
|
|
832
|
+
|
|
833
|
+
function downloadFile(content, filename, mime) {
|
|
834
|
+
const blob = new Blob([content], { type: mime });
|
|
835
|
+
const url = URL.createObjectURL(blob);
|
|
836
|
+
const a = document.createElement('a');
|
|
837
|
+
a.href = url;
|
|
838
|
+
a.download = filename;
|
|
839
|
+
document.body.appendChild(a);
|
|
840
|
+
a.click();
|
|
841
|
+
document.body.removeChild(a);
|
|
842
|
+
URL.revokeObjectURL(url);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function exportCsv() {
|
|
846
|
+
if (!lastGeneratedResult || !lastGeneratedResult.previewRows) {
|
|
847
|
+
if (window.UI && UI.showError) UI.showError('Generate preview data first.');
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const rows = lastGeneratedResult.previewRows;
|
|
851
|
+
const colNames = columns.map((c) => c.name);
|
|
852
|
+
let csv = colNames.map((c) => `"${c.replace(/"/g, '""')}"`).join(',') + '\n';
|
|
853
|
+
rows.forEach((r) => {
|
|
854
|
+
csv += colNames.map((c) => {
|
|
855
|
+
const v = r[c];
|
|
856
|
+
if (v == null) return '';
|
|
857
|
+
return `"${String(v).replace(/"/g, '""')}"`;
|
|
858
|
+
}).join(',') + '\n';
|
|
859
|
+
});
|
|
860
|
+
downloadFile(csv, `${cfg.table}_seed_data.csv`, 'text/csv;charset=utf-8;');
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function exportJson() {
|
|
864
|
+
if (!lastGeneratedResult || !lastGeneratedResult.previewRows) {
|
|
865
|
+
if (window.UI && UI.showError) UI.showError('Generate preview data first.');
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const jsonStr = JSON.stringify(lastGeneratedResult.previewRows, null, 2);
|
|
869
|
+
downloadFile(jsonStr, `${cfg.table}_seed_data.json`, 'application/json;charset=utf-8;');
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function downloadSql() {
|
|
873
|
+
if (!lastGeneratedResult || !lastGeneratedResult.sql) {
|
|
874
|
+
if (window.UI && UI.showError) UI.showError('Generate preview data first.');
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
downloadFile(lastGeneratedResult.sql, `${cfg.table}_seed.sql`, 'text/sql;charset=utf-8;');
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// ---- Collect Plan + Submit ----------------------------------------------
|
|
881
|
+
|
|
882
|
+
function collectPlan() {
|
|
883
|
+
const plan = {};
|
|
884
|
+
columns.forEach((col) => {
|
|
885
|
+
const st = state[col.name];
|
|
886
|
+
if (!st || !st.strategy) return;
|
|
887
|
+
const out = { strategy: st.strategy };
|
|
888
|
+
Object.keys(st).forEach((k) => {
|
|
889
|
+
if (k === 'strategy' || st[k] === undefined) return;
|
|
890
|
+
out[k] = st[k];
|
|
891
|
+
});
|
|
892
|
+
plan[col.name] = out;
|
|
893
|
+
});
|
|
894
|
+
return plan;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function currentCount() {
|
|
898
|
+
const n = Number.parseInt(String(countInput ? countInput.value : '10'), 10);
|
|
899
|
+
return Math.max(1, Math.min(cfg.maxRows || 5000, Number.isFinite(n) ? n : 10));
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async function executeSeed(truncate = false) {
|
|
903
|
+
const count = currentCount();
|
|
904
|
+
const plan = collectPlan();
|
|
905
|
+
const path = '/api/tables/' + t + '/seed';
|
|
906
|
+
try {
|
|
907
|
+
if (insertBtn) {
|
|
908
|
+
insertBtn.disabled = true;
|
|
909
|
+
insertBtn.classList.add('loading');
|
|
910
|
+
}
|
|
911
|
+
const data = await Api.post(path, { count, plan, truncate });
|
|
912
|
+
if (window.UI && UI.showToast) {
|
|
913
|
+
UI.showToast(data.message || `Inserted ${data.inserted} row(s) successfully.`, 'success');
|
|
914
|
+
}
|
|
915
|
+
showFeedback(data.warnings || []);
|
|
916
|
+
} catch (e) {
|
|
917
|
+
if (window.UI && UI.showError) UI.showError(e.message);
|
|
918
|
+
} finally {
|
|
919
|
+
if (insertBtn) {
|
|
920
|
+
insertBtn.disabled = false;
|
|
921
|
+
insertBtn.classList.remove('loading');
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async function submitPreview() {
|
|
927
|
+
const count = currentCount();
|
|
928
|
+
const plan = collectPlan();
|
|
929
|
+
const path = '/api/tables/' + t + '/seed/generate';
|
|
930
|
+
try {
|
|
931
|
+
if (previewBtn) previewBtn.disabled = true;
|
|
932
|
+
const data = await Api.post(path, { count, plan });
|
|
933
|
+
renderPreview(data);
|
|
934
|
+
showFeedback(data.warnings || []);
|
|
935
|
+
} catch (e) {
|
|
936
|
+
if (window.UI && UI.showError) UI.showError(e.message);
|
|
937
|
+
} finally {
|
|
938
|
+
if (previewBtn) previewBtn.disabled = false;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// ---- Init ---------------------------------------------------------------
|
|
943
|
+
|
|
944
|
+
function init() {
|
|
945
|
+
columns.forEach((col) => {
|
|
946
|
+
buildCard(col);
|
|
947
|
+
resetCard(col);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
filterColumns();
|
|
951
|
+
|
|
952
|
+
// Event Listeners
|
|
953
|
+
if (searchInput) searchInput.addEventListener('input', filterColumns);
|
|
954
|
+
if (previewBtn) previewBtn.addEventListener('click', submitPreview);
|
|
955
|
+
|
|
956
|
+
if (insertBtn) {
|
|
957
|
+
insertBtn.addEventListener('click', () => {
|
|
958
|
+
if (truncateCheckbox && truncateCheckbox.checked) {
|
|
959
|
+
if (truncateDialog) truncateDialog.showModal();
|
|
960
|
+
} else {
|
|
961
|
+
executeSeed(false);
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (truncateConfirm) {
|
|
967
|
+
truncateConfirm.addEventListener('click', () => {
|
|
968
|
+
if (truncateDialog) truncateDialog.close();
|
|
969
|
+
executeSeed(true);
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (truncateCancel) {
|
|
974
|
+
truncateCancel.addEventListener('click', () => {
|
|
975
|
+
if (truncateDialog) truncateDialog.close();
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (resetBtn) resetBtn.addEventListener('click', () => columns.forEach(resetCard));
|
|
980
|
+
if (refreshSamplesBtn) refreshSamplesBtn.addEventListener('click', () => {
|
|
981
|
+
columns.forEach((col) => {
|
|
982
|
+
const card = columnsBox.querySelector('.app-card[data-column="' + CSS.escape(col.name) + '"]');
|
|
983
|
+
if (card) updateCardSample(col, card);
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
if (applyAllBtn) {
|
|
988
|
+
applyAllBtn.addEventListener('click', () => {
|
|
989
|
+
const dialog = document.getElementById('seed-apply-all-dialog') || buildApplyAllDialog();
|
|
990
|
+
dialog.showModal();
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Presets
|
|
995
|
+
const pReal = document.getElementById('preset-realistic');
|
|
996
|
+
const pMin = document.getElementById('preset-minimal');
|
|
997
|
+
const pSparse = document.getElementById('preset-sparse');
|
|
998
|
+
const pNum = document.getElementById('preset-numbered');
|
|
999
|
+
if (pReal) pReal.addEventListener('click', () => applyPreset('realistic'));
|
|
1000
|
+
if (pMin) pMin.addEventListener('click', () => applyPreset('minimal'));
|
|
1001
|
+
if (pSparse) pSparse.addEventListener('click', () => applyPreset('sparse'));
|
|
1002
|
+
if (pNum) pNum.addEventListener('click', () => applyPreset('numbered'));
|
|
1003
|
+
|
|
1004
|
+
// Config export / import
|
|
1005
|
+
if (exportConfigBtn) {
|
|
1006
|
+
exportConfigBtn.addEventListener('click', () => {
|
|
1007
|
+
if (!configDialog || !configTextarea) return;
|
|
1008
|
+
configDialogTitle.textContent = 'Export Seed Plan';
|
|
1009
|
+
configTextarea.value = JSON.stringify(collectPlan(), null, 2);
|
|
1010
|
+
configDialogApply.classList.add('hidden');
|
|
1011
|
+
configDialog.showModal();
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (importConfigBtn) {
|
|
1016
|
+
importConfigBtn.addEventListener('click', () => {
|
|
1017
|
+
if (!configDialog || !configTextarea) return;
|
|
1018
|
+
configDialogTitle.textContent = 'Import Seed Plan';
|
|
1019
|
+
configTextarea.value = '';
|
|
1020
|
+
configTextarea.placeholder = 'Paste plan JSON here...';
|
|
1021
|
+
configDialogApply.classList.remove('hidden');
|
|
1022
|
+
configDialog.showModal();
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (configDialogCancel) {
|
|
1027
|
+
configDialogCancel.addEventListener('click', () => {
|
|
1028
|
+
if (configDialog) configDialog.close();
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (configDialogApply) {
|
|
1033
|
+
configDialogApply.addEventListener('click', () => {
|
|
1034
|
+
try {
|
|
1035
|
+
const imported = JSON.parse(configTextarea.value);
|
|
1036
|
+
if (typeof imported !== 'object' || !imported) throw new Error('Invalid JSON format');
|
|
1037
|
+
columns.forEach((col) => {
|
|
1038
|
+
if (imported[col.name]) {
|
|
1039
|
+
state[col.name] = imported[col.name];
|
|
1040
|
+
const card = columnsBox.querySelector('.app-card[data-column="' + CSS.escape(col.name) + '"]');
|
|
1041
|
+
if (card) {
|
|
1042
|
+
const select = card.querySelector('.seed-strategy');
|
|
1043
|
+
const options = card.querySelector('.seed-options');
|
|
1044
|
+
const note = card.querySelector('.seed-note');
|
|
1045
|
+
const warn = card.querySelector('.seed-warn');
|
|
1046
|
+
if (select) select.value = state[col.name].strategy;
|
|
1047
|
+
buildOptionInputs(options, col, state[col.name], card);
|
|
1048
|
+
note.textContent = strategyNote(col, state[col.name].strategy);
|
|
1049
|
+
note.classList.toggle('hidden', !note.textContent);
|
|
1050
|
+
updateWarn(col, warn);
|
|
1051
|
+
updateCardSample(col, card);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
if (configDialog) configDialog.close();
|
|
1056
|
+
if (window.UI && UI.showToast) UI.showToast('Seed plan configuration applied!', 'success');
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
if (window.UI && UI.showError) UI.showError('Failed to parse JSON: ' + err.message);
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Dual-mode tabs
|
|
1064
|
+
if (tabTableView) tabTableView.addEventListener('click', () => setPreviewTab('table'));
|
|
1065
|
+
if (tabSqlView) tabSqlView.addEventListener('click', () => setPreviewTab('sql'));
|
|
1066
|
+
|
|
1067
|
+
// Export buttons
|
|
1068
|
+
if (copySqlBtn) {
|
|
1069
|
+
copySqlBtn.addEventListener('click', () => {
|
|
1070
|
+
if (!sqlCode || !sqlCode.textContent) return;
|
|
1071
|
+
navigator.clipboard.writeText(sqlCode.textContent)
|
|
1072
|
+
.then(() => UI.showToast('Copied SQL to clipboard.', 'success'))
|
|
1073
|
+
.catch(() => UI.showError('Could not copy SQL.'));
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (downloadSqlBtn) downloadSqlBtn.addEventListener('click', downloadSql);
|
|
1078
|
+
if (exportCsvBtn) exportCsvBtn.addEventListener('click', exportCsv);
|
|
1079
|
+
if (exportJsonBtn) exportJsonBtn.addEventListener('click', exportJson);
|
|
1080
|
+
|
|
1081
|
+
// Quick counts
|
|
1082
|
+
document.querySelectorAll('.seed-quick').forEach((btn) => {
|
|
1083
|
+
btn.addEventListener('click', () => {
|
|
1084
|
+
if (countInput) countInput.value = btn.dataset.n;
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
init();
|
|
1090
|
+
})();
|