6 Commits

Author SHA1 Message Date
chaim 258a5b8136 release: v1.3.2
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 20:01:28 +00:00
chaim 3dc5c4e571 fix: move Data Migration from separate 'customDev' section to 'customization'
Avoid creating separate admin panel sections for each extension.
All custom settings go under the existing "התאמה אישית" section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 20:01:13 +00:00
chaim 8b745bc99f release: v1.3.1 2026-03-29 21:34:25 +00:00
chaim 384d7c6df0 fix: add clientModule to module.json for client-side template resolution
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:33:48 +00:00
chaim 49275d0fc9 release: v1.3.0 2026-03-29 20:38:33 +00:00
chaim ec759b89c1 feat: snapshot management UI — list and delete database snapshots
- ListBackups API now returns snapshots alongside backups
- DeleteBackup API accepts snapshot- prefixed names
- New "Database Snapshots" panel in admin UI with delete buttons
- i18n: English + Hebrew translations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:38:33 +00:00
9 changed files with 126 additions and 9 deletions
@@ -116,6 +116,19 @@
</div>
</div>
{{! === Snapshots === }}
<div class="panel panel-info">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-camera"></i>
{{translate 'Database Snapshots' scope='DataMigration'}}
</h4>
</div>
<div class="panel-body" style="padding: 0;">
<div class="snapshots-list list-group" style="margin-bottom: 0;"></div>
</div>
</div>
{{! === Import === }}
<div class="import-section" style="display: none;">
<div class="panel panel-warning">
@@ -72,6 +72,11 @@ define('modules/data-migration/views/admin/data-migration', ['view'], function (
const name = $(e.currentTarget).data('name');
this.$el.find('.backup-details[data-name="' + name + '"]').toggle();
},
'click [data-action="deleteSnapshot"]': function (e) {
e.stopPropagation();
const name = $(e.currentTarget).data('name');
this.actionDeleteSnapshot(name);
},
'click [data-action="dismissExportLog"]': function () {
this.exportLog = null;
this.$el.find('.export-log-container').hide();
@@ -144,8 +149,10 @@ define('modules/data-migration/views/admin/data-migration', ['view'], function (
Espo.Ajax.getRequest('DataMigration/action/listBackups')
.then(response => {
this.backups = response.backups || [];
this.snapshots = response.snapshots || [];
this.isLoadingBackups = false;
this.renderBackups();
this.renderSnapshots();
})
.catch(() => {
this.isLoadingBackups = false;
@@ -406,6 +413,60 @@ define('modules/data-migration/views/admin/data-migration', ['view'], function (
);
},
renderSnapshots: function () {
const $container = this.$el.find('.snapshots-list');
if (!this.snapshots || this.snapshots.length === 0) {
$container.html(
'<div class="text-muted" style="padding: 15px; text-align: center;">' +
this.translate('No snapshots', 'labels', 'DataMigration') +
'</div>'
);
return;
}
let html = '';
this.snapshots.forEach(snap => {
const date = snap.createdAt ? new Date(snap.createdAt).toLocaleString() : 'Unknown';
html += '<div class="list-group-item">';
html += '<div class="pull-right">';
html += '<button class="btn btn-danger btn-xs" data-action="deleteSnapshot" data-name="' + snap.name + '" title="' + this.translate('Delete') + '">';
html += '<i class="fas fa-trash"></i></button>';
html += '</div>';
html += '<h5 class="list-group-item-heading" style="margin: 0 0 4px;">';
html += '<i class="fas fa-camera text-info"></i> ' + snap.name;
html += '</h5>';
html += '<p class="list-group-item-text text-muted" style="margin: 0; font-size: 0.9em;">';
html += '<i class="fas fa-clock"></i> ' + date;
html += ' &nbsp;|&nbsp; <i class="fas fa-database"></i> ' + snap.database;
html += ' &nbsp;|&nbsp; <i class="fas fa-hdd"></i> ' + snap.sizeFormatted;
html += '</p>';
html += '</div>';
});
$container.html(html);
},
actionDeleteSnapshot: function (name) {
this.confirm(
this.translate('deleteSnapshotConfirmation', 'messages', 'DataMigration'),
() => {
Espo.Ajax.postRequest('DataMigration/action/deleteBackup', {
backupName: name,
})
.then(() => {
Espo.Ui.success(this.translate('Deleted'));
this.loadBackups();
})
.catch(() => {
Espo.Ui.error(this.translate('Failed to delete snapshot', 'labels', 'DataMigration'));
});
}
);
},
renderLog: function (selector, lines, isSuccess) {
const $container = this.$el.find(selector);
@@ -32,7 +32,7 @@ class DeleteBackup implements Action
// Sanitize to prevent directory traversal
$safeName = basename($backupName);
if (!str_starts_with($safeName, 'datamigration-')) {
if (!str_starts_with($safeName, 'datamigration-') && !str_starts_with($safeName, 'snapshot-')) {
throw new Forbidden('Invalid backup name.');
}
@@ -73,7 +73,43 @@ class ListBackups implements Action
];
}
return ResponseComposer::json(['backups' => $backups]);
// Scan for snapshots
$snapshots = [];
foreach ($dirs as $dir) {
if ($dir === '.' || $dir === '..') {
continue;
}
if (!str_starts_with($dir, 'snapshot-')) {
continue;
}
$fullPath = $backupsDir . '/' . $dir;
if (!is_dir($fullPath)) {
continue;
}
$manifest = $this->manifestBuilder->read($fullPath);
$dumpFile = $fullPath . '/database.sql';
$dumpSize = file_exists($dumpFile) ? filesize($dumpFile) : 0;
$snapshots[] = [
'name' => $dir,
'createdAt' => $manifest['createdAt'] ?? null,
'sourceHost' => $manifest['sourceHost'] ?? 'unknown',
'espoVersion' => $manifest['espoVersion'] ?? 'unknown',
'database' => $manifest['database'] ?? 'unknown',
'dumpSizeBytes' => $dumpSize,
'sizeFormatted' => $this->formatBytes($dumpSize),
];
}
return ResponseComposer::json([
'backups' => $backups,
'snapshots' => $snapshots,
]);
}
private function formatBytes(int $bytes): string
@@ -16,6 +16,9 @@
"Import failed": "Import failed",
"Select a backup first": "Please select a backup first",
"Failed to delete backup": "Failed to delete backup",
"Failed to delete snapshot": "Failed to delete snapshot",
"Database Snapshots": "Database Snapshots",
"No snapshots": "No snapshots found",
"Selected backup": "Selected backup",
"Import Options": "Import Options",
"Preserve original IDs": "Preserve original IDs",
@@ -44,6 +47,7 @@
"importConfirmation": "Are you sure you want to import this backup? This will add data to the database. This action cannot be easily undone.",
"dryRunConfirmation": "Run a dry import? This will simulate the import without making any changes to the database.",
"deleteBackupConfirmation": "Are you sure you want to delete this backup? This action cannot be undone.",
"deleteSnapshotConfirmation": "Are you sure you want to delete this database snapshot? This action cannot be undone.",
"noFilesHint": "faster export, without attachment files",
"preserveIdsHint": "keep original record IDs (recommended for same-instance restore)",
"importNoFilesHint": "import only database records, skip attachment files",
@@ -16,6 +16,9 @@
"Import failed": "הייבוא נכשל",
"Select a backup first": "נא לבחור גיבוי קודם",
"Failed to delete backup": "מחיקת הגיבוי נכשלה",
"Failed to delete snapshot": "מחיקת צילום המצב נכשלה",
"Database Snapshots": "צילומי מצב של מסד הנתונים",
"No snapshots": "לא נמצאו צילומי מצב",
"Selected backup": "גיבוי נבחר",
"Import Options": "אפשרויות ייבוא",
"Preserve original IDs": "שמור מזהים מקוריים",
@@ -44,6 +47,7 @@
"importConfirmation": "האם אתה בטוח שברצונך לייבא גיבוי זה? פעולה זו תוסיף נתונים למסד הנתונים ולא ניתן לבטלה בקלות.",
"dryRunConfirmation": "להריץ ייבוא ניסיון? פעולה זו תדמה את הייבוא מבלי לבצע שינויים במסד הנתונים.",
"deleteBackupConfirmation": "האם אתה בטוח שברצונך למחוק גיבוי זה? לא ניתן לבטל פעולה זו.",
"deleteSnapshotConfirmation": "האם אתה בטוח שברצונך למחוק צילום מצב זה? לא ניתן לבטל פעולה זו.",
"noFilesHint": "ייצוא מהיר יותר, ללא קבצים מצורפים",
"preserveIdsHint": "שמור מזהי רשומות מקוריים (מומלץ לשחזור באותה מערכת)",
"importNoFilesHint": "ייבוא רשומות בלבד, ללא קבצים מצורפים",
@@ -1,6 +1,5 @@
{
"customDev": {
"label": "Custom Development",
"customization": {
"itemList": [
"__APPEND__",
{
@@ -11,7 +10,6 @@
"view": "modules/data-migration/views/admin/data-migration",
"order": 50
}
],
"order": 30
]
}
}
@@ -4,5 +4,6 @@
"requires": {
"espocrm": ">=8.0.0"
},
"description": "Data migration tool for case data backup and restore"
"description": "Data migration tool for case data backup and restore",
"clientModule": "data-migration"
}
+2 -2
View File
@@ -1,13 +1,13 @@
{
"name": "DataMigration",
"version": "1.2.4",
"version": "1.3.2",
"acceptableVersions": [
">=8.0.0"
],
"php": [
">=8.1"
],
"releaseDate": "2026-03-29",
"releaseDate": "2026-04-06",
"author": "klear",
"description": "Case data migration: export/import all case-related data between EspoCRM instances"
}