8 Commits

Author SHA1 Message Date
chaim a57e23dec3 release: v1.1.4 2026-03-29 20:06:18 +00:00
chaim a8c4a2ebe1 feat: add skip attachment files option for import
Allows importing only database records without restoring attachment
files. Useful when migrating data between instances.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:06:12 +00:00
chaim 412b1e643b release: v1.1.3 2026-03-29 20:01:24 +00:00
chaim eed25a87b2 fix: skip entity types whose module is not installed during import
Prevents 500 error when importing a backup that contains entities
(e.g. SignatureRequest) from modules not installed on the target instance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:01:20 +00:00
chaim 838ef5c2c6 release: v1.1.2 2026-03-29 19:51:08 +00:00
chaim 319e2df45e fix: move admin panel item to customDev section
Move Data Migration from system section to unified "Custom Development"
section shared by all custom extensions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:50:59 +00:00
chaim dc2d6c57fe release: v1.1.1 2026-03-29 19:48:11 +00:00
chaim 4c3ec67a7d fix: use __APPEND__ in adminPanel to not overwrite system items
Without __APPEND__, the itemList replaced all existing system admin
panel entries instead of adding to them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:48:07 +00:00
9 changed files with 43 additions and 6 deletions
@@ -150,6 +150,14 @@
<span class="text-muted"> — {{translate 'skipDuplicatesHint' category='messages' scope='DataMigration'}}</span>
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="importNoFiles">
{{translate 'Skip attachment files' scope='DataMigration'}}
<span class="text-muted"> — {{translate 'importNoFilesHint' category='messages' scope='DataMigration'}}</span>
</label>
</div>
</div>
<div class="btn-group">
@@ -301,6 +301,7 @@ define('modules/data-migration/views/admin/data-migration', ['view'], function (
const preserveIds = this.$el.find('input[name="preserveIds"]').is(':checked');
const skipDuplicates = this.$el.find('input[name="skipDuplicates"]').is(':checked');
const noFiles = this.$el.find('input[name="importNoFiles"]').is(':checked');
const confirmMsg = dryRun
? this.translate('dryRunConfirmation', 'messages', 'DataMigration')
@@ -315,6 +316,7 @@ define('modules/data-migration/views/admin/data-migration', ['view'], function (
Espo.Ajax.postRequest('DataMigration/action/import', {
backupName: this.selectedBackup,
preserveIds: preserveIds,
noFiles: noFiles,
skipDuplicates: skipDuplicates,
dryRun: dryRun,
}, {timeout: 600000})
@@ -46,6 +46,7 @@ class Import implements Action
$preserveIds = $body->preserveIds ?? true;
$skipDuplicates = $body->skipDuplicates ?? true;
$dryRun = $body->dryRun ?? false;
$noFiles = $body->noFiles ?? false;
$io = new BufferedIO();
@@ -54,6 +55,7 @@ class Import implements Action
'preserveIds' => $preserveIds,
'skipDuplicates' => $skipDuplicates,
'dryRun' => $dryRun,
'noFiles' => $noFiles,
'verbose' => true,
], $io);
@@ -42,6 +42,7 @@
"deleteBackupConfirmation": "Are you sure you want to delete this backup? 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",
"skipDuplicatesHint": "skip records that already exist in the database"
}
}
@@ -1,6 +1,7 @@
{
"labels": {
"Data Migration": "העברת נתונים"
"Data Migration": "העברת נתונים",
"Custom Development": "פיתוח עצמאי"
},
"descriptions": {
"dataMigration": "ייצוא וייבוא נתוני תיקים בין מערכות EspoCRM"
@@ -42,6 +42,7 @@
"deleteBackupConfirmation": "האם אתה בטוח שברצונך למחוק גיבוי זה? לא ניתן לבטל פעולה זו.",
"noFilesHint": "ייצוא מהיר יותר, ללא קבצים מצורפים",
"preserveIdsHint": "שמור מזהי רשומות מקוריים (מומלץ לשחזור באותה מערכת)",
"importNoFilesHint": "ייבוא רשומות בלבד, ללא קבצים מצורפים",
"skipDuplicatesHint": "דלג על רשומות שכבר קיימות במסד הנתונים"
}
}
@@ -1,14 +1,17 @@
{
"system": {
"customDev": {
"label": "Custom Development",
"itemList": [
"__APPEND__",
{
"url": "#Admin/dataMigration",
"label": "Data Migration",
"iconClass": "fas fa-exchange-alt",
"description": "dataMigration",
"view": "modules/data-migration/views/admin/data-migration",
"order": 85
"order": 50
}
]
],
"order": 30
}
}
@@ -62,6 +62,7 @@ class ImportService
$preserveIds = $options['preserveIds'] ?? true;
$skipDuplicates = $options['skipDuplicates'] ?? false;
$dryRun = $options['dryRun'] ?? false;
$noFiles = $options['noFiles'] ?? false;
$verbose = $options['verbose'] ?? false;
$dataDir = rtrim($inputDir, '/') . '/data';
@@ -100,6 +101,11 @@ class ImportService
continue;
}
if (!$this->entityTypeExists($entityType)) {
$io->writeLine("Skipping {$entityType} (module not installed)");
continue;
}
$io->writeLine("Importing {$entityType}...");
$result = $this->entityImporter->import(
@@ -133,7 +139,10 @@ class ImportService
}
// Restore attachment files
if (is_dir($filesDir)) {
if ($noFiles) {
$io->writeLine('');
$io->writeLine('Attachment files: SKIPPED (--no-files)');
} else if (is_dir($filesDir)) {
$io->writeLine('');
$io->writeLine('Restoring attachment files...');
@@ -171,4 +180,14 @@ class ImportService
}
}
}
private function entityTypeExists(string $entityType): bool
{
try {
$this->entityManager->getRDBRepository($entityType);
return true;
} catch (\Exception $e) {
return false;
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "DataMigration",
"version": "1.1.0",
"version": "1.1.4",
"acceptableVersions": [
">=8.0.0"
],