mysqlsyncr 1.1.10 → 1.2.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.
Files changed (62) hide show
  1. package/README.md +246 -186
  2. package/__tests__/acceptCurrent.test.js +117 -0
  3. package/__tests__/columnAttributes.test.js +101 -0
  4. package/__tests__/options.test.js +259 -0
  5. package/__tests__/parser.test.js +167 -166
  6. package/__tests__/parser2.test.js +155 -152
  7. package/__tests__/paths.test.js +21 -0
  8. package/__tests__/prompt.test.js +106 -0
  9. package/__tests__/sync.test.js +348 -0
  10. package/__tests__/templateDrift.test.js +72 -0
  11. package/__tests__/versions.test.js +138 -0
  12. package/dist/index.cjs +4070 -1730
  13. package/dist/index.js +4106 -1768
  14. package/package.json +66 -59
  15. package/rollup.config.js +25 -24
  16. package/src/DBStructureChecker.js +47 -0
  17. package/src/acceptCurrent.js +225 -0
  18. package/src/apply.js +481 -348
  19. package/src/cli.js +61 -34
  20. package/src/compare.js +106 -92
  21. package/src/differences.js +258 -212
  22. package/src/dump.js +63 -54
  23. package/src/getters.js +151 -139
  24. package/src/index.js +223 -170
  25. package/src/loggers.js +156 -139
  26. package/src/normalizers.js +28 -6
  27. package/src/options.js +133 -0
  28. package/src/parsers.js +197 -145
  29. package/src/paths.js +41 -0
  30. package/src/prompt.js +109 -0
  31. package/src/rebuild.js +106 -0
  32. package/src/rows.js +46 -0
  33. package/src/socketServer.js +311 -229
  34. package/src/templateDrift.js +93 -0
  35. package/src/versions.js +112 -0
  36. package/ui/README.md +55 -40
  37. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-k1h7X_-h.woff +0 -0
  38. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-B7du-70m.woff +0 -0
  39. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-CoAZ_DKt.woff +0 -0
  40. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-D0406B4n.woff +0 -0
  41. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-CnAg2DeQ.woff +0 -0
  42. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-BG9VWE5v.woff +0 -0
  43. package/ui/dist/spa/assets/MainLayout-DLH1Kfg-.js +1 -0
  44. package/ui/dist/spa/assets/axios-2hQ_j_Tz.js +1 -0
  45. package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-OUIwM9U8.woff +0 -0
  46. package/ui/dist/spa/assets/index-Cqrg985P.js +23 -0
  47. package/ui/dist/spa/assets/index-pOWtXlXQ.css +1 -0
  48. package/ui/dist/spa/index.html +2 -2
  49. package/ui/package.json +43 -42
  50. package/ui/src/components/DefinitionDiff.vue +169 -0
  51. package/ui/src/pages/MainComponent.vue +704 -78
  52. package/ui/dist/spa/assets/KFOkCnqEu92Fr1MmgVxIIzQ-Y2mpUUkw.woff +0 -0
  53. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmEU9fBBc--ViM7Ag4Q.woff +0 -0
  54. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmSU5fBBc--CrFzpgl6.woff +0 -0
  55. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmWUlfBBc--CW3_XjG_.woff +0 -0
  56. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmYUtfBBc--Dr8fnyGc.woff +0 -0
  57. package/ui/dist/spa/assets/KFOmCnqEu92Fr1Mu4mxM-DAYh6T4l.woff +0 -0
  58. package/ui/dist/spa/assets/MainLayout-CvQD3xKr.js +0 -1
  59. package/ui/dist/spa/assets/axios-C0XcclIb.js +0 -1
  60. package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
  61. package/ui/dist/spa/assets/index-CwWo8xmP.js +0 -40
  62. package/ui/dist/spa/assets/index-kXpA8n8r.css +0 -5
@@ -54,7 +54,39 @@
54
54
  >
55
55
  {{ totalIssueCount }}
56
56
  </q-badge>
57
+ <q-chip
58
+ v-if="versionInfo"
59
+ dense
60
+ :color="versionInfo.notes.length ? 'warning' : 'white'"
61
+ :text-color="versionInfo.notes.length ? 'black' : 'primary'"
62
+ :icon="versionInfo.notes.length ? 'warning' : 'dns'"
63
+ class="q-ml-md"
64
+ >
65
+ Template MySQL {{ templateVersionText }} · Server MySQL {{ versionInfo.server || 'unknown' }}
66
+ <q-tooltip max-width="420px">
67
+ <div v-if="versionInfo.template?.inferred">
68
+ Template version inferred: it {{ versionInfo.template.reason }}. A new dump records it exactly.
69
+ </div>
70
+ <div v-for="note in versionInfo.notes" :key="note" class="q-mt-xs">{{ note }}</div>
71
+ <div v-if="!versionInfo.notes.length && !versionInfo.template?.inferred">
72
+ The template and server are the same MySQL version.
73
+ </div>
74
+ </q-tooltip>
75
+ </q-chip>
57
76
  </div>
77
+ <q-space />
78
+ <q-btn
79
+ v-if="ignoredCount > 0"
80
+ flat
81
+ dense
82
+ no-caps
83
+ color="white"
84
+ icon="visibility"
85
+ :label="`${ignoredCount} ignored`"
86
+ @click="showAllIgnored"
87
+ >
88
+ <q-tooltip>Show all ignored differences again</q-tooltip>
89
+ </q-btn>
58
90
  <q-btn
59
91
  v-if="totalIssueCount > 0"
60
92
  flat
@@ -75,8 +107,32 @@
75
107
  </div>
76
108
  </q-card-section>
77
109
 
110
+ <q-card-section v-if="templateDrift.length > 0" class="q-pb-none">
111
+ <q-banner dense rounded class="bg-orange-1 text-black">
112
+ <template v-slot:avatar>
113
+ <q-icon name="difference" color="warning" />
114
+ </template>
115
+ {{ templateDrift.length }} table(s) in the template have a tables/&lt;table&gt;.sql file that differs
116
+ from the createSQL in the database's JSON, which is what comparing uses. Pick which version to keep.
117
+ <div class="q-mt-xs">
118
+ <q-chip
119
+ v-for="drift in templateDrift"
120
+ :key="`${drift.database}.${drift.table}`"
121
+ dense
122
+ clickable
123
+ icon="rule"
124
+ @click="reviewDrift(drift)"
125
+ >
126
+ {{ drift.database }}.{{ drift.table }}
127
+ <q-tooltip>Review and pick a version</q-tooltip>
128
+ </q-chip>
129
+ </div>
130
+ </q-banner>
131
+ </q-card-section>
132
+
78
133
  <q-card-section>
79
134
  <q-tree
135
+ ref="tree"
80
136
  :nodes="databaseTree"
81
137
  node-key="id"
82
138
  v-model:expanded="expandedNodes"
@@ -110,8 +166,34 @@
110
166
  >
111
167
  {{ prop.node.diffCount }}
112
168
  </q-badge>
169
+ <q-badge
170
+ v-if="prop.node.hiddenCount"
171
+ color="grey"
172
+ outline
173
+ class="q-ml-sm"
174
+ >
175
+ {{ prop.node.hiddenCount }} ignored
176
+ </q-badge>
113
177
  </div>
114
178
 
179
+ <q-btn
180
+ v-if="
181
+ ['database', 'table'].includes(prop.node.type) &&
182
+ (prop.node.diffCount || isNodeIgnored(prop.node))
183
+ "
184
+ flat
185
+ dense
186
+ color="grey-7"
187
+ :icon="isNodeIgnored(prop.node) ? 'visibility' : 'visibility_off'"
188
+ size="sm"
189
+ class="q-ml-md"
190
+ @click.stop="toggleIgnored(prop.node)"
191
+ >
192
+ <q-tooltip>
193
+ {{ isNodeIgnored(prop.node) ? 'Show these differences again' : 'Ignore these differences for this session' }}
194
+ </q-tooltip>
195
+ </q-btn>
196
+
115
197
  <q-btn
116
198
  v-if="
117
199
  ['issue-group', 'issues', 'category', 'database'].includes(
@@ -281,65 +363,105 @@
281
363
  <!-- Single Issue View -->
282
364
  <template v-if="selectedIssues.length === 1">
283
365
  <q-card-section class="q-pt-none">
366
+ <q-btn
367
+ v-if="issueListBeforeDetails"
368
+ flat
369
+ dense
370
+ icon="arrow_back"
371
+ label="Back to list"
372
+ color="primary"
373
+ class="q-mb-sm"
374
+ @click="backToIssueList"
375
+ />
284
376
  <div class="text-subtitle2 q-mb-sm">
285
377
  Issue Type:
286
378
  <q-badge color="negative">{{
287
379
  selectedIssues[0].type?.replace(/_/g, ' ')
288
380
  }}</q-badge>
289
381
  </div>
290
- <div class="text-subtitle2 q-mb-sm">Name: {{ selectedIssues[0].Name }}</div>
291
-
292
382
  <div v-if="selectedIssues[0].tableName" class="q-mb-sm">
293
383
  <strong>Table:</strong> {{ selectedIssues[0].tableName }}
294
384
  </div>
385
+ <div v-if="issueName(selectedIssues[0])" class="q-mb-sm">
386
+ <strong>{{ issueNameLabel(selectedIssues[0]) }}:</strong> {{ issueName(selectedIssues[0]) }}
387
+ </div>
388
+ <div class="q-mb-sm">{{ issueSummary(selectedIssues[0]) }}</div>
389
+
390
+ <div v-if="selectedIssues[0].type === 'mismatched_index' && selectedIssues[0].index" class="q-mb-sm">
391
+ <strong>Columns:</strong>
392
+ {{ (selectedIssues[0].index.ColumnName || []).join(', ') }}
393
+ <q-badge v-if="selectedIssues[0].index.Primary" color="primary" class="q-ml-sm">
394
+ PRIMARY
395
+ </q-badge>
396
+ <q-badge
397
+ v-else-if="selectedIssues[0].index.Unique"
398
+ color="secondary"
399
+ class="q-ml-sm"
400
+ >
401
+ UNIQUE
402
+ </q-badge>
403
+ </div>
295
404
 
296
- <div v-if="selectedIssues[0].createSQL" class="q-mt-md">
297
- <div class="text-subtitle2 q-mb-sm">SQL Definition:</div>
298
- <q-card class="bg-grey-2">
299
- <q-card-section class="q-pa-md">
300
- <pre class="sql-code">{{ selectedIssues[0].createSQL }}</pre>
301
- </q-card-section>
302
- </q-card>
405
+ <div v-if="definitionComparison(selectedIssues[0])" class="q-mt-md">
406
+ <div class="text-subtitle2 q-mb-sm">Definition differences:</div>
407
+ <DefinitionDiff
408
+ :expected="definitionComparison(selectedIssues[0]).expected"
409
+ :current="definitionComparison(selectedIssues[0]).current"
410
+ />
303
411
  </div>
304
412
 
305
- <div v-if="selectedIssues[0].Definition" class="q-mt-md">
306
- <div class="text-subtitle2 q-mb-sm">Definition:</div>
413
+ <div v-else-if="singleDefinition(selectedIssues[0])" class="q-mt-md">
414
+ <div class="text-subtitle2 q-mb-sm">{{ singleDefinition(selectedIssues[0]).label }}:</div>
307
415
  <q-card class="bg-grey-2">
308
416
  <q-card-section class="q-pa-md">
309
- <pre class="sql-code">{{ selectedIssues[0].Definition }}</pre>
417
+ <pre class="sql-code">{{ singleDefinition(selectedIssues[0]).sql }}</pre>
310
418
  </q-card-section>
311
419
  </q-card>
312
420
  </div>
313
421
 
314
- <div v-if="selectedIssues[0].currentField" class="q-mt-md">
315
- <div class="text-subtitle2 q-mb-sm">Field Comparison:</div>
316
- <q-markup-table flat bordered>
422
+ <div v-if="issueComparison(selectedIssues[0])" class="q-mt-md">
423
+ <div class="text-subtitle2 q-mb-sm">Comparison:</div>
424
+ <q-markup-table flat bordered dense>
317
425
  <thead>
318
426
  <tr>
319
427
  <th class="text-left">Property</th>
320
- <th class="text-left">Current</th>
321
- <th class="text-left">Expected</th>
428
+ <th class="text-left">Database (current)</th>
429
+ <th class="text-left">Template (expected)</th>
322
430
  </tr>
323
431
  </thead>
324
432
  <tbody>
325
- <tr v-for="(value, key) in selectedIssues[0].currentField" :key="key">
326
- <td>{{ key }}</td>
327
- <td>{{ value }}</td>
328
- <td :class="{ 'text-negative': value !== selectedIssues[0].field?.[key] }">
329
- {{ selectedIssues[0].field?.[key] }}
433
+ <tr v-for="row in issueComparison(selectedIssues[0])" :key="row.property">
434
+ <td>{{ row.property }}</td>
435
+ <td :class="{ 'text-grey': row.current === undefined }">
436
+ {{ formatIssueValue(row.current, row.currentPresent) }}
437
+ </td>
438
+ <td :class="{ 'text-negative': row.differs, 'text-grey': row.expected === undefined }">
439
+ {{ formatIssueValue(row.expected, row.expectedPresent) }}
330
440
  </td>
331
441
  </tr>
332
442
  </tbody>
333
443
  </q-markup-table>
334
444
  </div>
335
445
 
336
- <q-btn
337
- @click="fixNodeIssues(this.selectedNode)"
338
- :loading="isNodeFixing['single-issue']"
339
- color="primary"
340
- class="q-mt-md"
341
- label="Fix Issue"
342
- />
446
+ <div class="row q-gutter-sm q-mt-md">
447
+ <!-- Act on the issue shown, not on every issue under the selected tree node -->
448
+ <q-btn
449
+ @click="fixSingleIssue()"
450
+ :loading="isNodeFixing['single-issue']"
451
+ color="primary"
452
+ label="Fix Issue"
453
+ />
454
+ <q-btn
455
+ v-if="canKeepCurrent(selectedIssues[0])"
456
+ @click="keepCurrentInTemplate({ type: 'issue', id: 'single-issue', dbName: selectedIssueDbName, issueData: selectedIssues[0] })"
457
+ :loading="isNodeKeeping['single-issue']"
458
+ color="secondary"
459
+ outline
460
+ label="Keep Current"
461
+ >
462
+ <q-tooltip>Overwrite the template with the database's current value</q-tooltip>
463
+ </q-btn>
464
+ </div>
343
465
  </q-card-section>
344
466
  </template>
345
467
 
@@ -354,7 +476,9 @@
354
476
  label: 'Type',
355
477
  field: (row) => row.type?.replace(/_/g, ' '),
356
478
  },
479
+ { name: 'name', label: 'Name', field: (row) => issueName(row) },
357
480
  { name: 'tableName', label: 'Table', field: 'tableName' },
481
+ { name: 'item', label: 'Item', field: (row) => issueName(row) },
358
482
  { name: 'actions', label: 'Actions', field: 'actions' },
359
483
  ]"
360
484
  row-key="id"
@@ -363,14 +487,16 @@
363
487
  <template v-slot:body="props">
364
488
  <q-tr :props="props">
365
489
  <q-td key="type">{{ props.row.type?.replace(/_/g, ' ') }}</q-td>
490
+ <q-td key="name">{{ issueName(props.row) }}</q-td>
366
491
  <q-td key="tableName">{{ props.row.tableName }}</q-td>
492
+ <q-td key="item">{{ issueName(props.row) }}</q-td>
367
493
  <q-td key="actions">
368
494
  <q-btn
369
495
  flat
370
496
  dense
371
497
  color="primary"
372
498
  icon="visibility"
373
- @click="showSingleIssue(props.row)"
499
+ @click="showSingleIssue(props.row, selectedIssueDbName)"
374
500
  >
375
501
  <q-tooltip>View Details</q-tooltip>
376
502
  </q-btn>
@@ -383,13 +509,32 @@
383
509
  @click="
384
510
  fixNodeIssues({
385
511
  type: 'issue',
386
- id: this.selectedNode.id,
512
+ id: `issue-${props.rowIndex}`,
513
+ dbName: selectedIssueDbName,
387
514
  issueData: props.row,
388
515
  })
389
516
  "
390
517
  >
391
518
  <q-tooltip>Fix Issue</q-tooltip>
392
519
  </q-btn>
520
+ <q-btn
521
+ v-if="canKeepCurrent(props.row)"
522
+ flat
523
+ dense
524
+ color="secondary"
525
+ icon="save_as"
526
+ :loading="isNodeKeeping[`issue-${props.rowIndex}`]"
527
+ @click="
528
+ keepCurrentInTemplate({
529
+ type: 'issue',
530
+ id: `issue-${props.rowIndex}`,
531
+ dbName: selectedIssueDbName,
532
+ issueData: props.row,
533
+ })
534
+ "
535
+ >
536
+ <q-tooltip>Keep Current: overwrite the template with the database's current value</q-tooltip>
537
+ </q-btn>
393
538
  </q-td>
394
539
  </q-tr>
395
540
  </template>
@@ -397,7 +542,7 @@
397
542
 
398
543
  <q-btn
399
544
  @click="fixNodeIssues(this.selectedNode)"
400
- :loading="isNodeFixing['all-issues']"
545
+ :loading="isNodeFixing[selectedNode?.id]"
401
546
  color="primary"
402
547
  class="q-mt-md"
403
548
  label="Fix All Issues"
@@ -411,6 +556,50 @@
411
556
  </q-card>
412
557
  </q-dialog>
413
558
 
559
+ <q-dialog v-model="driftDialog">
560
+ <q-card v-if="selectedDrift" style="min-width: 800px; width: 90vw; max-width: 1200px">
561
+ <q-card-section>
562
+ <div class="text-h6">{{ selectedDrift.database }}.{{ selectedDrift.table }}</div>
563
+ <div class="text-body2 q-mt-sm">
564
+ The table's createSQL in {{ selectedDrift.database }}.json (used for comparing and for
565
+ creating the table) differs from {{ selectedDrift.file }}. Keeping one copies it over the other;
566
+ notes written after the statement in the .sql file are kept either way.
567
+ </div>
568
+ </q-card-section>
569
+ <q-card-section class="q-pt-none">
570
+ <DefinitionDiff
571
+ :expected="selectedDrift.json"
572
+ :current="selectedDrift.sql"
573
+ :expected-label="`${selectedDrift.database}.json (used for comparing)`"
574
+ :current-label="selectedDrift.file"
575
+ />
576
+ </q-card-section>
577
+ <q-card-actions align="right">
578
+ <q-btn flat label="Cancel" color="primary" v-close-popup />
579
+ <q-btn
580
+ outline
581
+ color="primary"
582
+ :label="`Use ${selectedDrift.file}`"
583
+ :loading="resolvingDrift === 'sql'"
584
+ :disable="!!resolvingDrift"
585
+ @click="resolveDrift('sql')"
586
+ >
587
+ <q-tooltip>Copy the .sql file's statement into the JSON, so comparing uses it</q-tooltip>
588
+ </q-btn>
589
+ <q-btn
590
+ unelevated
591
+ color="primary"
592
+ :label="`Use ${selectedDrift.database}.json`"
593
+ :loading="resolvingDrift === 'json'"
594
+ :disable="!!resolvingDrift"
595
+ @click="resolveDrift('json')"
596
+ >
597
+ <q-tooltip>Copy the JSON's createSQL into the .sql file</q-tooltip>
598
+ </q-btn>
599
+ </q-card-actions>
600
+ </q-card>
601
+ </q-dialog>
602
+
414
603
  <FolderBrowser v-if="showFolderBrowser"
415
604
  v-model="showFolderBrowser"
416
605
  :initial-path="dbFolder"
@@ -421,11 +610,30 @@
421
610
 
422
611
  <script>
423
612
  import FolderBrowser from '../components/FolderBrowser.vue'
613
+ import DefinitionDiff from '../components/DefinitionDiff.vue'
614
+
615
+ // Ignored databases/tables last for the browser tab, so they survive a reload but not a new session
616
+ const IGNORED_KEY = 'mysqlsyncr.ignored'
617
+ const loadIgnored = () => {
618
+ try {
619
+ return JSON.parse(sessionStorage.getItem(IGNORED_KEY)) || {}
620
+ } catch {
621
+ return {}
622
+ }
623
+ }
624
+ const saveIgnored = (ignoredByScope) => {
625
+ try {
626
+ sessionStorage.setItem(IGNORED_KEY, JSON.stringify(ignoredByScope))
627
+ } catch {
628
+ // Storage unavailable (private window etc.): ignoring still works until the page reloads
629
+ }
630
+ }
424
631
 
425
632
 
426
633
  export default {
427
634
  components: {
428
- FolderBrowser
635
+ FolderBrowser,
636
+ DefinitionDiff
429
637
  },
430
638
  data() {
431
639
  return {
@@ -452,13 +660,39 @@ export default {
452
660
  issueDialog: false,
453
661
  selectedIssues: [],
454
662
  selectedNode: null,
663
+ selectedIssueDbName: null,
455
664
  isNodeFixing: {}, // Tracks loading state for each node
665
+ isNodeKeeping: {}, // Tracks loading state for Keep Current
666
+ issueListBeforeDetails: null, // The issue list View Details was opened from
667
+ rawDifferences: {}, // differences as the server sent them; this.differences leaves out ignored ones
668
+ // { 'host:port|folder': { databases: [db], tables: ['db.table'] } }, kept for this browser session
669
+ ignoredByScope: loadIgnored(),
670
+ versionInfo: null, // { server, template: { version, inferred, reason }, notes } from /api/versions
671
+ templateDrift: [], // [{ database, table, file, json, sql }]: .sql files that differ from the JSON createSQL
672
+ driftDialog: false,
673
+ selectedDrift: null,
674
+ resolvingDrift: null, // 'json' or 'sql' while saving
456
675
  showFolderBrowser: false,
457
676
  isFixingAll: false,
458
677
  currentFixProgress: 0,
459
678
  }
460
679
  },
461
680
  computed: {
681
+ // What's ignored for the current server and template folder
682
+ ignored() {
683
+ return this.ignoredByScope[this.ignoredScope] || { databases: [], tables: [] }
684
+ },
685
+ ignoredScope() {
686
+ return `${this.host}:${this.port}|${this.dbFolder}`
687
+ },
688
+ templateVersionText() {
689
+ const template = this.versionInfo?.template
690
+ if (!template) return 'unknown'
691
+ return template.inferred ? `${template.version} (inferred)` : template.version
692
+ },
693
+ ignoredCount() {
694
+ return this.ignored.databases.length + this.ignored.tables.length
695
+ },
462
696
  totalIssueCount() {
463
697
  return Object.values(this.differences)
464
698
  .reduce((total, dbIssues) => total + dbIssues.length, 0);
@@ -499,7 +733,7 @@ export default {
499
733
  database: this.database,
500
734
  },
501
735
  })
502
- this.differences = response.data
736
+ this.setDifferences(response.data)
503
737
  } catch (error) {
504
738
  console.error('Error fetching differences:', error)
505
739
  }
@@ -521,6 +755,7 @@ export default {
521
755
  if (response.data && typeof response.data === 'object') {
522
756
  this.existingDatabases = response.data
523
757
  this.buildDatabaseTree()
758
+ this.fetchTemplateDrift()
524
759
  }
525
760
  } catch (error) {
526
761
  console.error('Error fetching databases from existing dumps:', error)
@@ -535,13 +770,15 @@ export default {
535
770
  const dbDiffs = this.differences[dbName] || []
536
771
  const totalIssues = dbDiffs.length
537
772
  console.log(`Database ${dbName} differences ${totalIssues}:`, dbDiffs)
773
+ const ignored = this.isDatabaseIgnored(dbName)
538
774
  return {
539
775
  id: `db-${dbName}`,
540
776
  label: dbName,
541
777
  type: 'database',
542
778
  lazy: true,
543
- icon: totalIssues > 0 ? 'warning' : 'check_circle',
544
- iconColor: totalIssues > 0 ? 'negative' : 'positive',
779
+ icon: ignored ? 'visibility_off' : totalIssues > 0 ? 'warning' : 'check_circle',
780
+ iconColor: ignored ? 'grey' : totalIssues > 0 ? 'negative' : 'positive',
781
+ hiddenCount: this.hiddenCount(dbName),
545
782
  diffCount: totalIssues || null,
546
783
  dbData: db,
547
784
  dbName: dbName,
@@ -560,17 +797,12 @@ export default {
560
797
  const dbDiffs = this.differences[node.dbName] || []
561
798
 
562
799
  // Count issues by main category
563
- const tableIssues = dbDiffs.filter(
564
- (d) =>
565
- d.type.includes('table') ||
566
- d.type.includes('field') ||
567
- d.type.includes('index') ||
568
- d.type.includes('trigger'),
569
- )
800
+ const tableIssues = dbDiffs.filter((d) => this.issueCategory(d) === 'tables')
570
801
  console.log('Table issues:', tableIssues)
571
802
 
572
- const procedureIssues = dbDiffs.filter((d) => d.type.includes('procedure'))
573
- const viewIssues = dbDiffs.filter((d) => d.type.includes('view'))
803
+ const procedureIssues = dbDiffs.filter((d) => this.issueCategory(d) === 'procedures')
804
+ const viewIssues = dbDiffs.filter((d) => this.issueCategory(d) === 'views')
805
+ const otherIssues = dbDiffs.filter((d) => this.issueCategory(d) === 'other')
574
806
 
575
807
  const children = [
576
808
  {
@@ -591,7 +823,8 @@ export default {
591
823
  iconColor: procedureIssues.length > 0 ? 'negative' : 'positive',
592
824
  diffCount: procedureIssues.length || null,
593
825
  },
594
- db.views && {
826
+ // Shown for view differences too, e.g. a view only in the database
827
+ (db.views || viewIssues.length > 0) && {
595
828
  id: `${node.dbName}-views`,
596
829
  label: 'Views',
597
830
  type: 'category',
@@ -600,6 +833,26 @@ export default {
600
833
  iconColor: viewIssues.length > 0 ? 'negative' : 'positive',
601
834
  diffCount: viewIssues.length || null,
602
835
  },
836
+ // Differences that belong to no table, procedure or view, so every counted one is shown
837
+ otherIssues.length > 0 && {
838
+ id: `${node.dbName}-other`,
839
+ label: 'Other',
840
+ type: 'category',
841
+ lazy: false,
842
+ icon: 'warning',
843
+ iconColor: 'negative',
844
+ diffCount: otherIssues.length,
845
+ children: otherIssues.map((issue, i) => ({
846
+ id: `${node.dbName}-other-${i}`,
847
+ label: `${issue.type.replace(/_/g, ' ')}${this.issueName(issue) ? `: ${this.issueName(issue)}` : ''}`,
848
+ type: 'issue',
849
+ icon: 'warning',
850
+ iconColor: 'negative',
851
+ expandable: false,
852
+ diffCount: 1,
853
+ issueData: issue,
854
+ })),
855
+ },
603
856
  ].filter(Boolean)
604
857
 
605
858
  console.log('Database children:', children)
@@ -610,13 +863,7 @@ export default {
610
863
  const dbDiffs = this.differences[dbName] || []
611
864
 
612
865
  if (node.id.includes('-tables')) {
613
- const tableIssues = dbDiffs.filter(
614
- (d) =>
615
- d.type.includes('table') ||
616
- d.type.includes('field') ||
617
- d.type.includes('index') ||
618
- d.type.includes('trigger'),
619
- )
866
+ const tableIssues = dbDiffs.filter((d) => this.issueCategory(d) === 'tables')
620
867
  console.log('Table category issues:', tableIssues)
621
868
 
622
869
  const children = []
@@ -634,8 +881,15 @@ export default {
634
881
  // })
635
882
  // }
636
883
 
637
- // Add actual tables
638
- const tables = this.existingDatabases[dbName].tables
884
+ // Add actual tables, plus any only in the database (extra_table) so their issues show
885
+ const tables = { ...this.existingDatabases[dbName].tables }
886
+ const onlyInDatabase = new Set()
887
+ for (const issue of [...tableIssues, ...(this.rawDifferences[dbName] || [])]) {
888
+ if (issue.tableName && !tables[issue.tableName] && this.issueCategory(issue) === 'tables') {
889
+ tables[issue.tableName] = null
890
+ onlyInDatabase.add(issue.tableName)
891
+ }
892
+ }
639
893
  console.log('Tables:', tables)
640
894
 
641
895
  children.push(
@@ -646,13 +900,18 @@ export default {
646
900
  console.log(`Issues for table ${tableName}:`, specificTableIssues)
647
901
  const hasIssues = specificTableIssues.length > 0
648
902
 
903
+ const ignored = this.isTableIgnored(dbName, tableName) || this.isDatabaseIgnored(dbName)
649
904
  return {
650
905
  id: `${dbName}-table-${tableName}`,
651
- label: tableName,
906
+ label: onlyInDatabase.has(tableName) ? `${tableName} (not in template)` : tableName,
652
907
  type: 'table',
653
- lazy: true,
654
- icon: hasIssues ? 'warning' : 'check_circle',
655
- iconColor: hasIssues ? 'negative' : 'positive',
908
+ dbName,
909
+ tableName,
910
+ // A table only in the database has no template structure to expand
911
+ lazy: table !== null,
912
+ icon: ignored ? 'visibility_off' : hasIssues ? 'warning' : 'check_circle',
913
+ iconColor: ignored ? 'grey' : hasIssues ? 'negative' : 'positive',
914
+ hiddenCount: this.hiddenCount(dbName, tableName),
656
915
  diffCount: hasIssues ? specificTableIssues.length : null,
657
916
  tableData: table,
658
917
  issueData: specificTableIssues,
@@ -693,16 +952,32 @@ export default {
693
952
  done(children)
694
953
  } else if (node.id.includes('-views')) {
695
954
  const children = []
696
- const views = this.existingDatabases[dbName].views
955
+ const views = { ...(this.existingDatabases[dbName].views || {}) }
956
+ const viewIssues = dbDiffs.filter((d) => this.issueCategory(d) === 'views')
957
+ const onlyInDatabase = new Set()
958
+ for (const issue of viewIssues) {
959
+ if (!(issue.viewName in views)) {
960
+ views[issue.viewName] = null
961
+ onlyInDatabase.add(issue.viewName)
962
+ }
963
+ }
697
964
  console.log('Views:', views)
698
965
  children.push(
699
- ...Object.entries(views).map(([name, view]) => ({
700
- id: `${dbName}-view-${name}`,
701
- label: name,
702
- type: 'view',
703
- lazy: false,
704
- view: view,
705
- })),
966
+ ...Object.entries(views).map(([name, view]) => {
967
+ const specificViewIssues = viewIssues.filter((d) => d.viewName === name)
968
+ const hasIssues = specificViewIssues.length > 0
969
+ return {
970
+ id: `${dbName}-view-${name}`,
971
+ label: onlyInDatabase.has(name) ? `${name} (not in template)` : name,
972
+ type: 'view',
973
+ lazy: false,
974
+ view: view,
975
+ icon: hasIssues ? 'warning' : 'check_circle',
976
+ iconColor: hasIssues ? 'negative' : 'positive',
977
+ diffCount: hasIssues ? specificViewIssues.length : null,
978
+ issueData: specificViewIssues,
979
+ }
980
+ }),
706
981
  )
707
982
  done(children)
708
983
  }
@@ -807,8 +1082,9 @@ export default {
807
1082
  output: this.dbFolder,
808
1083
  },
809
1084
  })
810
- this.differences = response.data
1085
+ this.setDifferences(response.data)
811
1086
  console.log('Differences in fetchAllDifferences:', this.differences)
1087
+ this.fetchVersions()
812
1088
  } catch (error) {
813
1089
  console.error('Error fetching all differences:', error)
814
1090
  this.$q.notify({
@@ -833,7 +1109,7 @@ export default {
833
1109
  database: selectedDbName,
834
1110
  },
835
1111
  })
836
- this.differences = response.data
1112
+ this.setDifferences(response.data)
837
1113
  } catch (error) {
838
1114
  console.error('Error fetching differences for selected db:', error)
839
1115
  }
@@ -920,15 +1196,42 @@ export default {
920
1196
  },
921
1197
  showIssueDetails(issue, node) {
922
1198
  console.log('Showing issue details:', issue, "from node:", node)
1199
+ this.issueListBeforeDetails = null
923
1200
  this.selectedIssues = Array.isArray(issue) ? issue : [issue]
924
1201
  this.selectedNode = node;
1202
+ this.selectedIssueDbName = this.getDbNameFromNode(node)
925
1203
  this.issueDialog = true
926
1204
  },
927
1205
  showSingleIssue(issue, dbName) {
1206
+ // Remember the list so the details view can go back to it
1207
+ if (this.selectedIssues.length > 1) this.issueListBeforeDetails = this.selectedIssues
928
1208
  this.selectedIssues = [issue]
929
- this.selectedIssueDbName = dbName;
1209
+ this.selectedIssueDbName = dbName || this.getDbNameFromNode(this.selectedNode)
930
1210
  this.issueDialog = true
931
1211
  },
1212
+ // Fixes only the issue currently shown in the single-issue view, rather
1213
+ // than everything under the node the dialog was opened from.
1214
+ async fixSingleIssue() {
1215
+ const issue = this.selectedIssues[0]
1216
+ if (!issue) return
1217
+ if (!this.selectedIssueDbName) {
1218
+ // Without the database the synthetic node id below would be
1219
+ // parsed as one, so refuse rather than apply to the wrong db.
1220
+ console.error('No database known for issue', issue)
1221
+ this.$q.notify({
1222
+ type: 'negative',
1223
+ message: 'Could not determine which database this issue belongs to',
1224
+ position: 'top',
1225
+ })
1226
+ return
1227
+ }
1228
+ await this.fixNodeIssues({
1229
+ type: 'issue',
1230
+ id: 'single-issue',
1231
+ dbName: this.selectedIssueDbName,
1232
+ issueData: issue,
1233
+ })
1234
+ },
932
1235
  async fixNodeIssues(node) {
933
1236
  if (!node) return
934
1237
  try {
@@ -943,7 +1246,7 @@ export default {
943
1246
  }else{
944
1247
  issues = await this.collectIssuesForNode(node)
945
1248
  }
946
- dbName = this.getDbNameFromNode(node)
1249
+ dbName = node.dbName || this.getDbNameFromNode(node)
947
1250
 
948
1251
  if (!issues.length){
949
1252
  console.error('No issues found to fix', node);
@@ -964,15 +1267,21 @@ export default {
964
1267
  })
965
1268
 
966
1269
  if (response.data.message) {
1270
+ // The server reports differences it could only apply with a change
1271
+ // (an unsupported collation dropped, say). Those are not clean
1272
+ // successes, so say what happened rather than a bare count.
1273
+ const warnings = response.data.warnings || []
967
1274
  this.$q.notify({
968
- type: 'positive',
969
- message: `Fixed ${issues.length} issue(s) successfully`,
1275
+ type: warnings.length ? 'warning' : 'positive',
1276
+ message: warnings.length
1277
+ ? response.data.message
1278
+ : `Fixed ${issues.length} issue(s) successfully`,
970
1279
  position: 'top',
1280
+ timeout: warnings.length ? 10000 : undefined,
971
1281
  })
972
- // Refresh the differences to update the UI
973
-
974
- await this.fetchAllDifferences()
975
- await this.buildDatabaseTree()
1282
+ this.afterIssuesResolved(issues)
1283
+ // Refresh the differences, keeping the tree as it was
1284
+ await this.refreshKeepingExpanded()
976
1285
  }
977
1286
  } catch (error) {
978
1287
  console.error('Error fixing issues:', error)
@@ -981,18 +1290,335 @@ export default {
981
1290
  message: error.response?.data?.message || 'Error fixing issues',
982
1291
  position: 'top',
983
1292
  })
1293
+ // The server tries every difference and saves the ones that work, so refresh
1294
+ // to drop those that were applied; the dialog keeps the rest
1295
+ if (error.response) {
1296
+ await this.refreshKeepingExpanded().catch((refreshError) => console.error('Error refreshing:', refreshError))
1297
+ }
984
1298
  } finally {
985
1299
  this.isNodeFixing[node.id] = false
986
1300
  this.isNodeFixing = { ...this.isNodeFixing } // Trigger reactivity
987
1301
  }
988
1302
  },
1303
+ // Tables whose .sql file disagrees with the JSON createSQL; reads only the template
1304
+ async fetchTemplateDrift() {
1305
+ try {
1306
+ const response = await this.$api.post('/api/templateDrift', { config: { output: this.dbFolder } })
1307
+ this.templateDrift = Array.isArray(response.data) ? response.data : []
1308
+ } catch (error) {
1309
+ console.error('Error checking the template for .sql files that differ:', error)
1310
+ this.templateDrift = []
1311
+ }
1312
+ },
1313
+ reviewDrift(drift) {
1314
+ this.selectedDrift = drift
1315
+ this.driftDialog = true
1316
+ },
1317
+ async resolveDrift(keep) {
1318
+ const { database, table } = this.selectedDrift
1319
+ this.resolvingDrift = keep
1320
+ try {
1321
+ const response = await this.$api.post('/api/resolveTemplateDrift', {
1322
+ config: { output: this.dbFolder },
1323
+ database,
1324
+ table,
1325
+ keep,
1326
+ })
1327
+ this.$q.notify({ type: 'positive', message: response.data.message, position: 'top' })
1328
+ this.driftDialog = false
1329
+ await this.fetchTemplateDrift()
1330
+ // Keeping the .sql file changes the JSON, which is what comparing uses
1331
+ if (keep === 'sql') await this.refreshKeepingExpanded({ reloadTemplate: true })
1332
+ } catch (error) {
1333
+ this.$q.notify({
1334
+ type: 'negative',
1335
+ message: error.response?.data?.message || 'Error updating the template',
1336
+ position: 'top',
1337
+ })
1338
+ } finally {
1339
+ this.resolvingDrift = null
1340
+ }
1341
+ },
1342
+ // Shown next to the differences; a failure just leaves the versions out
1343
+ async fetchVersions() {
1344
+ try {
1345
+ const response = await this.$api.post('/api/versions', {
1346
+ config: {
1347
+ host: this.host,
1348
+ port: this.port,
1349
+ user: this.user,
1350
+ password: this.password,
1351
+ output: this.dbFolder,
1352
+ },
1353
+ })
1354
+ this.versionInfo = response.data
1355
+ } catch (error) {
1356
+ console.error('Error fetching MySQL versions:', error)
1357
+ this.versionInfo = null
1358
+ }
1359
+ },
1360
+ setDifferences(raw) {
1361
+ this.rawDifferences = raw || {}
1362
+ this.differences = this.withoutIgnored(this.rawDifferences)
1363
+ },
1364
+ withoutIgnored(raw) {
1365
+ return Object.fromEntries(
1366
+ Object.entries(raw).map(([dbName, diffs]) => [
1367
+ dbName,
1368
+ this.isDatabaseIgnored(dbName)
1369
+ ? []
1370
+ : (Array.isArray(diffs) ? diffs : []).filter(
1371
+ (diff) => !(diff.tableName && this.isTableIgnored(dbName, diff.tableName)),
1372
+ ),
1373
+ ]),
1374
+ )
1375
+ },
1376
+ isDatabaseIgnored(dbName) {
1377
+ return this.ignored.databases.includes(dbName)
1378
+ },
1379
+ isTableIgnored(dbName, tableName) {
1380
+ return this.ignored.tables.includes(`${dbName}.${tableName}`)
1381
+ },
1382
+ // How many of a database's (or one table's) differences are hidden by ignoring
1383
+ hiddenCount(dbName, tableName) {
1384
+ const diffs = Array.isArray(this.rawDifferences[dbName]) ? this.rawDifferences[dbName] : []
1385
+ if (tableName) {
1386
+ const hidden = this.isTableIgnored(dbName, tableName) || this.isDatabaseIgnored(dbName)
1387
+ return hidden ? diffs.filter((d) => d.tableName === tableName).length : 0
1388
+ }
1389
+ return diffs.length - (this.differences[dbName]?.length ?? 0)
1390
+ },
1391
+ isNodeIgnored(node) {
1392
+ if (node.type === 'database') return this.isDatabaseIgnored(node.dbName)
1393
+ if (node.type === 'table') return this.isTableIgnored(node.dbName, node.tableName)
1394
+ return false
1395
+ },
1396
+ async setIgnored(ignored) {
1397
+ this.ignoredByScope = { ...this.ignoredByScope, [this.ignoredScope]: ignored }
1398
+ saveIgnored(this.ignoredByScope)
1399
+ this.differences = this.withoutIgnored(this.rawDifferences)
1400
+ await this.rebuildTreeKeepingExpanded()
1401
+ },
1402
+ // Hide a database's or table's differences for this session, or show them again
1403
+ async toggleIgnored(node) {
1404
+ const { databases, tables } = this.ignored
1405
+ if (node.type === 'database') {
1406
+ const dbName = node.dbName
1407
+ await this.setIgnored({
1408
+ databases: databases.includes(dbName) ? databases.filter((d) => d !== dbName) : [...databases, dbName],
1409
+ tables,
1410
+ })
1411
+ } else if (node.type === 'table') {
1412
+ const key = `${node.dbName}.${node.tableName}`
1413
+ await this.setIgnored({
1414
+ databases,
1415
+ tables: tables.includes(key) ? tables.filter((t) => t !== key) : [...tables, key],
1416
+ })
1417
+ }
1418
+ },
1419
+ async showAllIgnored() {
1420
+ await this.setIgnored({ databases: [], tables: [] })
1421
+ },
1422
+ async rebuildTreeKeepingExpanded() {
1423
+ const expanded = [...this.expandedNodes]
1424
+ await this.buildDatabaseTree()
1425
+ await this.restoreExpanded(expanded)
1426
+ },
1427
+ // Reload the differences (and the template if it changed) and rebuild the tree,
1428
+ // re-opening the nodes that were expanded
1429
+ async refreshKeepingExpanded({ reloadTemplate = false } = {}) {
1430
+ const expanded = [...this.expandedNodes]
1431
+ await this.fetchAllDifferences()
1432
+ if (reloadTemplate) await this.getDatabasesFromExistingDumps()
1433
+ await this.buildDatabaseTree()
1434
+ await this.restoreExpanded(expanded)
1435
+ },
1436
+ // Expand the given nodes again. Children of lazy nodes only exist once their
1437
+ // parent is expanded, so expand level by level until nothing more is found
1438
+ async restoreExpanded(keys) {
1439
+ const tree = this.$refs.tree
1440
+ if (!tree) return
1441
+ const pending = new Set(keys)
1442
+ while (pending.size > 0) {
1443
+ await this.$nextTick()
1444
+ const found = [...pending].filter((key) => tree.getNodeByKey(key))
1445
+ if (found.length === 0) break
1446
+ for (const key of found) {
1447
+ tree.setExpanded(key, true)
1448
+ pending.delete(key)
1449
+ }
1450
+ }
1451
+ },
1452
+ // Which part of the tree a difference is shown under: tables, procedures, views or other
1453
+ issueCategory(issue) {
1454
+ if (issue.type?.includes('procedure')) return 'procedures'
1455
+ if (issue.type?.includes('view')) return 'views'
1456
+ if (issue.tableName) return 'tables'
1457
+ return 'other'
1458
+ },
1459
+ // The column, index, trigger, procedure, view or row an issue is about
1460
+ issueName(issue) {
1461
+ if (!issue) return ''
1462
+ if (issue.field?.Field) return issue.field.Field
1463
+ if (issue.indexName || issue.index?.Name) return issue.indexName || issue.index.Name
1464
+ if (issue.trigger?.Name) return issue.trigger.Name
1465
+ if (issue.key) return Object.entries(issue.key).map(([k, v]) => `${k} = ${v}`).join(', ')
1466
+ return issue.viewName || issue.Name || issue.database || ''
1467
+ },
1468
+ issueNameLabel(issue) {
1469
+ const kind = issue?.type?.split('_').slice(1).join('_')
1470
+ return { field: 'Column', index: 'Index', trigger: 'Trigger', procedure: 'Procedure', view: 'View', row: 'Row', database: 'Database' }[kind] || 'Name'
1471
+ },
1472
+ issueSummary(issue) {
1473
+ if (!issue?.type) return ''
1474
+ const [state, ...rest] = issue.type.split('_')
1475
+ const kind = this.issueNameLabel(issue).toLowerCase()
1476
+ const thing = rest.join('_') === 'table' ? 'This table' : `The ${kind}`
1477
+ if (state === 'extra') return `${thing} is in the database but not in the template.`
1478
+ if (state === 'missing') return `${thing} is in the template but not in the database.`
1479
+ if (state === 'mismatched') {
1480
+ const what = issue.info || issue.from
1481
+ return `${thing} differs between the database and the template${what ? ` (${what})` : ''}.`
1482
+ }
1483
+ return ''
1484
+ },
1485
+ // [{ property, current, expected, ... }] comparing the database and template side of an issue, or null
1486
+ issueComparison(issue) {
1487
+ const sides = {
1488
+ extra_field: [issue?.field, undefined],
1489
+ missing_field: [undefined, issue?.field],
1490
+ mismatched_field: [issue?.currentField, issue?.field],
1491
+ extra_index: [issue?.index, undefined],
1492
+ missing_index: [undefined, issue?.index],
1493
+ missing_row: [undefined, issue?.row],
1494
+ }[issue?.type]
1495
+ if (!sides) return null
1496
+ const [current, expected] = sides
1497
+ const keys = [...new Set([...Object.keys(current || {}), ...Object.keys(expected || {})])]
1498
+ return keys
1499
+ .filter((key) => (current?.[key] ?? null) !== null || (expected?.[key] ?? null) !== null)
1500
+ .map((key) => ({
1501
+ property: key,
1502
+ current: current?.[key],
1503
+ expected: expected?.[key],
1504
+ currentPresent: !!current,
1505
+ expectedPresent: !!expected,
1506
+ differs: !!current && !!expected && JSON.stringify(current[key] ?? null) !== JSON.stringify(expected[key] ?? null),
1507
+ }))
1508
+ },
1509
+ // { expected, current } definitions of a mismatched procedure, view or trigger, or null
1510
+ definitionComparison(issue) {
1511
+ const pair = {
1512
+ mismatched_procedure: [issue?.Definition, issue?.currentDefinition],
1513
+ mismatched_view: [issue?.definition, issue?.current],
1514
+ mismatched_trigger: [issue?.trigger?.Definition, issue?.currentTrigger?.Definition],
1515
+ }[issue?.type]
1516
+ // Diffs from an older server have no current definition to compare
1517
+ if (!pair || pair[1] === undefined) return null
1518
+ return { expected: pair[0] ?? '', current: pair[1] ?? '' }
1519
+ },
1520
+ // { label, sql } for an issue that has a definition on one side only
1521
+ singleDefinition(issue) {
1522
+ if (!issue) return null
1523
+ const sql = issue.createSQL || issue.Definition || issue.definition || issue.trigger?.Definition
1524
+ if (!sql) return null
1525
+ const fromDatabase = issue.type?.startsWith('extra_')
1526
+ return { label: `Definition in the ${fromDatabase ? 'database (current)' : 'template (expected)'}`, sql }
1527
+ },
1528
+ formatIssueValue(value, present) {
1529
+ if (!present) return '(not present)'
1530
+ if (value === undefined || value === null) return '—'
1531
+ if (Array.isArray(value)) return value.join(', ')
1532
+ if (typeof value === 'boolean') return value ? 'yes' : 'no'
1533
+ return String(value)
1534
+ },
1535
+ // Drop resolved issues from the dialog: back to the list when the details view
1536
+ // is done, and close once nothing is left
1537
+ afterIssuesResolved(resolved) {
1538
+ const remaining = (list) => list?.filter((issue) => !resolved.includes(issue))
1539
+ this.issueListBeforeDetails = remaining(this.issueListBeforeDetails)
1540
+ this.selectedIssues = remaining(this.selectedIssues)
1541
+ if (this.selectedIssues.length === 0 && this.issueListBeforeDetails?.length) {
1542
+ this.backToIssueList()
1543
+ }
1544
+ if (this.selectedIssues.length === 0) this.issueDialog = false
1545
+ },
1546
+ backToIssueList() {
1547
+ this.selectedIssues = this.issueListBeforeDetails
1548
+ this.issueListBeforeDetails = null
1549
+ },
1550
+ // Every difference except these can be kept by changing the template
1551
+ canKeepCurrent(issue) {
1552
+ return !!issue && !['missing_database', 'missing_row'].includes(issue.type)
1553
+ },
1554
+ // The opposite of fixNodeIssues: change the template to match the database
1555
+ async keepCurrentInTemplate(node, loadingKey = node?.id) {
1556
+ if (!node) return
1557
+ const issues = node.issueData
1558
+ ? (Array.isArray(node.issueData) ? node.issueData : [node.issueData])
1559
+ : await this.collectIssuesForNode(node)
1560
+ const keepable = issues.filter((issue) => this.canKeepCurrent(issue))
1561
+ if (!keepable.length) {
1562
+ this.$q.notify({ type: 'warning', message: 'Nothing here can be kept as the current value', position: 'top' })
1563
+ return
1564
+ }
1565
+
1566
+ const confirmed = await new Promise((resolve) => {
1567
+ this.$q
1568
+ .dialog({
1569
+ title: 'Keep Current',
1570
+ message: `Overwrite the template in ${this.dbFolder} with the database's current value for ${keepable.length} difference(s)? The database isn't changed.`,
1571
+ cancel: true,
1572
+ persistent: true,
1573
+ })
1574
+ .onOk(() => resolve(true))
1575
+ .onCancel(() => resolve(false))
1576
+ })
1577
+ if (!confirmed) return
1578
+
1579
+ this.isNodeKeeping = { ...this.isNodeKeeping, [loadingKey]: true }
1580
+ try {
1581
+ await this.$api.post('/api/acceptCurrent', {
1582
+ config: {
1583
+ host: this.host,
1584
+ port: this.port,
1585
+ user: this.user,
1586
+ password: this.password,
1587
+ output: this.dbFolder,
1588
+ },
1589
+ database: node.dbName || this.getDbNameFromNode(node),
1590
+ diffs: keepable,
1591
+ })
1592
+ this.$q.notify({
1593
+ type: 'positive',
1594
+ message: `Template updated for ${keepable.length} difference(s)`,
1595
+ position: 'top',
1596
+ })
1597
+ this.afterIssuesResolved(keepable)
1598
+ } catch (error) {
1599
+ console.error('Error keeping current values:', error)
1600
+ this.$q.notify({
1601
+ type: 'negative',
1602
+ message: error.response?.data?.message || 'Error updating the template',
1603
+ position: 'top',
1604
+ })
1605
+ } finally {
1606
+ this.isNodeKeeping = { ...this.isNodeKeeping, [loadingKey]: false }
1607
+ // Refresh even after a partial failure: the differences that succeeded were saved.
1608
+ // The template changed too, so reload it
1609
+ await this.refreshKeepingExpanded({ reloadTemplate: true })
1610
+ }
1611
+ },
989
1612
  getDbNameFromNode(node) {
990
1613
  // Extract database name from node ID (assumes format: "dbName-something-etc")
991
1614
  if(!node || !node.id) node = this.selectedNode
1615
+ if (!node || !node.id) {
1616
+ console.error('Node ID is required to get db name', node)
1617
+ return null
1618
+ }
992
1619
  if(node.id.startsWith('db-')){
993
1620
  return node.id.split('-')[1]
994
1621
  }
995
- if (!node.id) return null, console.error('Node ID is required to get db name', node);
996
1622
  console.log('Node ID:', node.id)
997
1623
  return node.id.split('-')[0]
998
1624
  },