diff --git a/backend/import_export/dump_services.py b/backend/import_export/dump_services.py
new file mode 100644
index 0000000..064b759
--- /dev/null
+++ b/backend/import_export/dump_services.py
@@ -0,0 +1,387 @@
+import json
+from datetime import datetime, timezone
+
+from django.db import transaction
+
+from contacts.models import (
+ Contact,
+ NetworkMap,
+ NetworkMapMembership,
+ NetworkMapType,
+ Relation,
+)
+from core.choices import ALL_RELATION_TYPES, INTERACTION_INTENSITY
+
+VALID_RELATION_TYPES = {choice[0] for choice in ALL_RELATION_TYPES}
+VALID_INTENSITY = {choice[0] for choice in INTERACTION_INTENSITY}
+
+
+def is_data_dump(data):
+ return (
+ isinstance(data, dict)
+ and isinstance(data.get('contacts'), list)
+ and isinstance(data.get('relations'), list)
+ )
+
+
+def _owner_filter(owner):
+ return {'owner': owner} if owner is not None else {}
+
+
+def _contacts_qs(owner):
+ qs = Contact.objects.all()
+ if owner is not None:
+ qs = qs.filter(owner=owner)
+ return qs.order_by('name')
+
+
+def _relations_qs(owner):
+ qs = Relation.objects.select_related('source', 'target').all()
+ if owner is not None:
+ qs = qs.filter(owner=owner)
+ return qs.order_by('id')
+
+
+def _map_types_qs(owner):
+ qs = NetworkMapType.objects.all()
+ if owner is not None:
+ qs = qs.filter(owner=owner)
+ return qs.order_by('name')
+
+
+def _maps_qs(owner):
+ qs = NetworkMap.objects.select_related('map_type').all()
+ if owner is not None:
+ qs = qs.filter(owner=owner)
+ return qs.order_by('name')
+
+
+def _serialize_contact(contact):
+ return {
+ 'id': contact.id,
+ 'name': contact.name,
+ 'email': contact.email or '',
+ 'phone': contact.phone or '',
+ 'organization': contact.organization or '',
+ 'position': contact.position or '',
+ 'notes': contact.notes or '',
+ 'created_at': contact.created_at.isoformat() if contact.created_at else None,
+ 'updated_at': contact.updated_at.isoformat() if contact.updated_at else None,
+ }
+
+
+def _serialize_relation(relation):
+ return {
+ 'id': relation.id,
+ 'source': relation.source_id,
+ 'source_name': relation.source.name if relation.source_id else '',
+ 'target': relation.target_id,
+ 'target_name': relation.target.name if relation.target_id else '',
+ 'relation_type': relation.relation_type,
+ 'description': relation.description or '',
+ 'interaction_intensity': relation.interaction_intensity,
+ 'created_at': relation.created_at.isoformat() if relation.created_at else None,
+ }
+
+
+def _serialize_map_type(map_type):
+ return {
+ 'id': map_type.id,
+ 'name': map_type.name,
+ 'sectors': map_type.sectors or [],
+ 'circles': map_type.circles or [],
+ 'isDefault': map_type.is_default,
+ 'conflictologyEnabled': map_type.conflictology_enabled,
+ 'createdAt': map_type.created_at.isoformat() if map_type.created_at else None,
+ 'updatedAt': map_type.updated_at.isoformat() if map_type.updated_at else None,
+ }
+
+
+def _serialize_map(network_map):
+ return {
+ 'id': network_map.id,
+ 'name': network_map.name,
+ 'description': network_map.description or '',
+ 'mapTypeId': network_map.map_type_id,
+ 'conflictSubject': network_map.conflict_subject or '',
+ 'createdAt': network_map.created_at.isoformat() if network_map.created_at else None,
+ 'updatedAt': network_map.updated_at.isoformat() if network_map.updated_at else None,
+ }
+
+
+def _serialize_membership(membership):
+ return {
+ 'id': membership.id,
+ 'mapId': membership.map_id,
+ 'contactId': membership.contact_id,
+ 'life_sphere': membership.life_sphere,
+ 'network_circle': membership.network_circle,
+ 'importance': membership.importance,
+ 'conflict_involvement': membership.conflict_involvement,
+ 'map_angle': membership.map_angle,
+ 'map_radius_ratio': membership.map_radius_ratio,
+ 'createdAt': membership.created_at.isoformat() if membership.created_at else None,
+ 'updatedAt': membership.updated_at.isoformat() if membership.updated_at else None,
+ }
+
+
+def build_owner_dump(owner):
+ contacts = [_serialize_contact(c) for c in _contacts_qs(owner)]
+ relations = [_serialize_relation(r) for r in _relations_qs(owner)]
+ map_types = [_serialize_map_type(t) for t in _map_types_qs(owner)]
+ maps = [_serialize_map(m) for m in _maps_qs(owner)]
+
+ map_ids = [m.id for m in _maps_qs(owner)]
+ memberships = NetworkMapMembership.objects.filter(map_id__in=map_ids).select_related('map', 'contact')
+ membership_rows = [_serialize_membership(m) for m in memberships]
+
+ return {
+ 'version': 2,
+ 'exportedAt': datetime.now(timezone.utc).isoformat(),
+ 'contacts': contacts,
+ 'relations': relations,
+ 'networkMapTypes': map_types,
+ 'networkMaps': maps,
+ 'networkMapMemberships': membership_rows,
+ }
+
+
+def _read_map_types(data):
+ raw = data.get('networkMapTypes') or data.get('network_map_types') or []
+ return raw if isinstance(raw, list) else []
+
+
+def _read_maps(data):
+ raw = data.get('networkMaps') or data.get('network_maps') or []
+ return raw if isinstance(raw, list) else []
+
+
+def _read_memberships(data):
+ raw = data.get('networkMapMemberships') or data.get('network_map_memberships') or []
+ return raw if isinstance(raw, list) else []
+
+
+def _contact_payload(row):
+ return {
+ 'name': str(row.get('name') or '').strip() or 'Без имени',
+ 'email': str(row.get('email') or '').strip(),
+ 'phone': str(row.get('phone') or '').strip(),
+ 'organization': str(row.get('organization') or '').strip(),
+ 'position': str(row.get('position') or '').strip(),
+ 'notes': str(row.get('notes') or '').strip(),
+ }
+
+
+def _normalize_relation_type(value):
+ raw = str(value or 'acquaintance').strip()
+ return raw if raw in VALID_RELATION_TYPES else 'other'
+
+
+def _normalize_intensity(value):
+ raw = str(value or 'intense').strip()
+ return raw if raw in VALID_INTENSITY else 'intense'
+
+
+def _relation_payload(row, source_id, target_id):
+ return {
+ 'source_id': source_id,
+ 'target_id': target_id,
+ 'relation_type': _normalize_relation_type(row.get('relation_type')),
+ 'description': str(row.get('description') or '')[:255],
+ 'interaction_intensity': _normalize_intensity(row.get('interaction_intensity')),
+ }
+
+
+def _map_type_payload(row):
+ return {
+ 'name': str(row.get('name') or '').strip() or 'Тип карты',
+ 'sectors': row.get('sectors') or [],
+ 'circles': row.get('circles') or [],
+ 'is_default': bool(row.get('isDefault') if 'isDefault' in row else row.get('is_default')),
+ 'conflictology_enabled': bool(
+ row.get('conflictologyEnabled') if 'conflictologyEnabled' in row
+ else row.get('conflictology_enabled')
+ ),
+ }
+
+
+def _map_payload(row, map_type_id):
+ return {
+ 'name': str(row.get('name') or '').strip() or 'Карта',
+ 'description': str(row.get('description') or '').strip(),
+ 'map_type_id': map_type_id,
+ 'conflict_subject': str(
+ row.get('conflictSubject') if 'conflictSubject' in row else row.get('conflict_subject') or ''
+ ).strip(),
+ }
+
+
+def _membership_payload(row, map_id, contact_id):
+ return {
+ 'map_id': map_id,
+ 'contact_id': contact_id,
+ 'life_sphere': str(row.get('life_sphere') or 'other'),
+ 'network_circle': str(row.get('network_circle') or 'productivity'),
+ 'importance': int(row.get('importance') or 3),
+ 'conflict_involvement': int(row.get('conflict_involvement') or 3),
+ 'map_angle': row.get('map_angle'),
+ 'map_radius_ratio': row.get('map_radius_ratio'),
+ }
+
+
+def clear_owner_data(owner):
+ maps_qs = _maps_qs(owner)
+ map_ids = list(maps_qs.values_list('id', flat=True))
+ deleted_memberships = NetworkMapMembership.objects.filter(map_id__in=map_ids).count()
+ deleted_maps = maps_qs.count()
+ deleted_relations = _relations_qs(owner).count()
+ deleted_contacts = _contacts_qs(owner).count()
+ deleted_map_types = _map_types_qs(owner).count()
+
+ NetworkMapMembership.objects.filter(map_id__in=map_ids).delete()
+ maps_qs.delete()
+ _relations_qs(owner).delete()
+ _contacts_qs(owner).delete()
+ _map_types_qs(owner).delete()
+
+ return {
+ 'deletedContacts': deleted_contacts,
+ 'deletedRelations': deleted_relations,
+ 'deletedMaps': deleted_maps,
+ 'deletedMemberships': deleted_memberships,
+ 'deletedMapTypes': deleted_map_types,
+ }
+
+
+@transaction.atomic
+def import_owner_dump(owner, data, replace=False):
+ if not is_data_dump(data):
+ raise ValueError('Некорректный формат бэкапа: ожидаются массивы contacts и relations.')
+
+ cleared = None
+ if replace:
+ cleared = clear_owner_data(owner)
+
+ contacts = data.get('contacts') or []
+ relations = data.get('relations') or []
+ map_types = _read_map_types(data)
+ maps = _read_maps(data)
+ memberships = _read_memberships(data)
+
+ contact_id_map = {}
+ contacts_created = 0
+ contacts_skipped = 0
+
+ for row in contacts:
+ payload = _contact_payload(row)
+ if not payload['name']:
+ contacts_skipped += 1
+ continue
+ create_kwargs = {**payload, **_owner_filter(owner)}
+ contact = Contact.objects.create(**create_kwargs)
+ old_id = row.get('id')
+ if old_id is not None:
+ contact_id_map[str(old_id)] = contact.id
+ contacts_created += 1
+
+ if not contact_id_map and contacts:
+ raise ValueError('Не удалось импортировать ни одного контакта.')
+
+ type_id_map = {}
+ existing_types = {t.name: t for t in _map_types_qs(owner)}
+ default_type_id = None
+
+ for row in map_types:
+ payload = _map_type_payload(row)
+ found = existing_types.get(payload['name'])
+ if found:
+ type_id_map[str(row.get('id'))] = found.id
+ if payload['is_default']:
+ default_type_id = found.id
+ continue
+ create_kwargs = {**payload, **_owner_filter(owner)}
+ created = NetworkMapType.objects.create(**create_kwargs)
+ existing_types[payload['name']] = created
+ type_id_map[str(row.get('id'))] = created.id
+ if payload['is_default']:
+ default_type_id = created.id
+
+ if not default_type_id:
+ fallback = _map_types_qs(owner).filter(is_default=True).first() or _map_types_qs(owner).first()
+ default_type_id = fallback.id if fallback else None
+
+ map_id_map = {}
+ maps_created = 0
+
+ for row in maps:
+ old_type_id = row.get('mapTypeId') if 'mapTypeId' in row else row.get('map_type')
+ map_type_id = type_id_map.get(str(old_type_id)) or default_type_id
+ if not map_type_id:
+ continue
+ payload = _map_payload(row, map_type_id)
+ create_kwargs = {**payload, **_owner_filter(owner)}
+ created = NetworkMap.objects.create(**create_kwargs)
+ map_id_map[str(row.get('id'))] = created.id
+ maps_created += 1
+
+ relation_pairs = set()
+ relations_created = 0
+ relations_skipped = 0
+
+ for row in relations:
+ source = contact_id_map.get(str(row.get('source')))
+ target = contact_id_map.get(str(row.get('target')))
+ if not source or not target or source == target:
+ relations_skipped += 1
+ continue
+ pair_key = f'{source}:{target}'
+ if pair_key in relation_pairs:
+ relations_skipped += 1
+ continue
+ payload = _relation_payload(row, source, target)
+ create_kwargs = {**payload, **_owner_filter(owner)}
+ Relation.objects.create(**create_kwargs)
+ relation_pairs.add(pair_key)
+ relations_created += 1
+
+ memberships_created = 0
+ memberships_skipped = 0
+
+ for row in memberships:
+ map_id = map_id_map.get(str(row.get('mapId') if 'mapId' in row else row.get('map')))
+ contact_id = contact_id_map.get(str(row.get('contactId') if 'contactId' in row else row.get('contact')))
+ if not map_id or not contact_id:
+ memberships_skipped += 1
+ continue
+ payload = _membership_payload(row, map_id, contact_id)
+ NetworkMapMembership.objects.create(**payload)
+ memberships_created += 1
+
+ return {
+ 'replaced': bool(replace),
+ 'cleared': cleared,
+ 'importedContacts': contacts_created,
+ 'contactsSkipped': contacts_skipped,
+ 'importedRelations': relations_created,
+ 'relationsSkipped': relations_skipped,
+ 'importedMaps': maps_created,
+ 'importedMemberships': memberships_created,
+ 'membershipsSkipped': memberships_skipped,
+ 'importedMapTypes': len(type_id_map),
+ }
+
+
+def parse_dump_request(request):
+ if request.content_type and 'application/json' in request.content_type:
+ try:
+ body = request.body.decode('utf-8') if request.body else '{}'
+ return json.loads(body or '{}'), None
+ except json.JSONDecodeError as exc:
+ return None, f'Некорректный JSON: {exc}'
+ dump_file = request.FILES.get('file')
+ if dump_file:
+ try:
+ return json.loads(dump_file.read().decode('utf-8')), None
+ except json.JSONDecodeError as exc:
+ return None, f'Некорректный JSON в файле: {exc}'
+ return None, 'Передайте JSON-бэкап в теле запроса или файлом (поле file).'
diff --git a/backend/import_export/urls.py b/backend/import_export/urls.py
index 7ae7b80..b9ab549 100644
--- a/backend/import_export/urls.py
+++ b/backend/import_export/urls.py
@@ -4,4 +4,5 @@ from . import views
urlpatterns = [
path('import/', views.ImportContactsView.as_view(), name='import-contacts'),
+ path('export/', views.ExportDumpView.as_view(), name='export-dump'),
]
diff --git a/backend/import_export/views.py b/backend/import_export/views.py
index 21d3bc7..373108e 100644
--- a/backend/import_export/views.py
+++ b/backend/import_export/views.py
@@ -4,9 +4,38 @@ from rest_framework.views import APIView
from core.access import use_jwt_auth
from core.drf_mixins import JwtAuthMixin
+from .dump_services import build_owner_dump, import_owner_dump, is_data_dump, parse_dump_request
from .services import parse_upload_file, import_contacts_from_rows
+class ExportDumpView(JwtAuthMixin, APIView):
+ def get(self, request):
+ owner = request.user if use_jwt_auth() and request.user.is_authenticated else None
+ return Response(build_owner_dump(owner))
+
+ def post(self, request):
+ data, error = parse_dump_request(request)
+ if error:
+ return Response({'error': error}, status=status.HTTP_400_BAD_REQUEST)
+ if not is_data_dump(data):
+ return Response(
+ {'error': 'Некорректный формат бэкапа: ожидаются массивы contacts и relations.'},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+ replace = str(request.query_params.get('replace', '')).lower() in ('1', 'true', 'yes')
+ owner = request.user if use_jwt_auth() and request.user.is_authenticated else None
+ try:
+ summary = import_owner_dump(owner, data, replace=replace)
+ return Response(summary)
+ except ValueError as exc:
+ return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
+ except Exception as exc:
+ return Response(
+ {'error': f'Ошибка импорта бэкапа: {exc}'},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+
class ImportContactsView(JwtAuthMixin, APIView):
def post(self, request):
file = request.FILES.get('file')
diff --git a/backend/tests/test_export_dump_api.py b/backend/tests/test_export_dump_api.py
new file mode 100644
index 0000000..d6565e3
--- /dev/null
+++ b/backend/tests/test_export_dump_api.py
@@ -0,0 +1,94 @@
+import pytest
+
+from contacts.models import Contact, NetworkMap, NetworkMapMembership, NetworkMapType, Relation
+
+
+@pytest.mark.django_db
+def test_export_dump_empty(api_client):
+ response = api_client.get('/api/v1/export/')
+ assert response.status_code == 200
+ data = response.json()
+ assert data['version'] == 2
+ assert data['contacts'] == []
+ assert data['relations'] == []
+ assert isinstance(data['networkMapTypes'], list)
+ assert isinstance(data['networkMaps'], list)
+ assert isinstance(data['networkMapMemberships'], list)
+
+
+@pytest.mark.django_db
+def test_export_dump_with_data(api_client, two_contacts, sample_relation):
+ response = api_client.get('/api/v1/export/')
+ assert response.status_code == 200
+ data = response.json()
+ assert len(data['contacts']) == 2
+ assert len(data['relations']) == 1
+ assert data['relations'][0]['source'] == sample_relation.source_id
+ assert data['relations'][0]['target'] == sample_relation.target_id
+
+
+@pytest.mark.django_db
+def test_import_dump_replace(api_client, two_contacts, sample_relation):
+ export_response = api_client.get('/api/v1/export/')
+ dump = export_response.json()
+ assert len(dump['contacts']) == 2
+
+ Contact.objects.all().delete()
+ assert Contact.objects.count() == 0
+
+ import_response = api_client.post('/api/v1/export/?replace=true', dump, format='json')
+ assert import_response.status_code == 200
+ result = import_response.json()
+ assert result['importedContacts'] == 2
+ assert result['importedRelations'] == 1
+ assert Contact.objects.count() == 2
+ assert Relation.objects.count() == 1
+
+
+@pytest.mark.django_db
+def test_import_dump_invalid(api_client):
+ response = api_client.post('/api/v1/export/', {'contacts': []}, format='json')
+ assert response.status_code == 400
+ assert 'error' in response.json()
+
+
+@pytest.mark.django_db
+def test_import_dump_with_maps(api_client, db):
+ NetworkMapMembership.objects.all().delete()
+ NetworkMap.objects.all().delete()
+ NetworkMapType.objects.all().delete()
+
+ map_type = NetworkMapType.objects.create(
+ name='Базовый',
+ sectors=[{'id': 'work', 'label': 'Работа'}],
+ circles=[{'id': 'close', 'label': 'Близкий'}],
+ is_default=True,
+ )
+ contact = Contact.objects.create(name='Алиса')
+ network_map = NetworkMap.objects.create(
+ name='Основная',
+ description='Тест',
+ map_type=map_type,
+ )
+ network_map.memberships.create(
+ contact=contact,
+ life_sphere='work',
+ network_circle='close',
+ importance=4,
+ )
+
+ dump = api_client.get('/api/v1/export/').json()
+ assert len(dump['contacts']) == 1
+ assert len(dump['networkMaps']) == 1
+ assert len(dump['networkMapMemberships']) == 1
+
+ Contact.objects.all().delete()
+ NetworkMap.objects.all().delete()
+ NetworkMapType.objects.all().delete()
+
+ result = api_client.post('/api/v1/export/?replace=true', dump, format='json').json()
+ assert result['importedContacts'] == 1
+ assert result['importedMaps'] == 1
+ assert result['importedMemberships'] == 1
+ assert NetworkMapType.objects.count() == 1
+ assert NetworkMap.objects.count() == 1
diff --git a/frontend/src/application/usecases/importExport.js b/frontend/src/application/usecases/importExport.js
index 8951c75..96fd0fd 100644
--- a/frontend/src/application/usecases/importExport.js
+++ b/frontend/src/application/usecases/importExport.js
@@ -5,9 +5,12 @@ import { generateId } from '../../lib/uuid'
import { createDefaultMapTypeRecord } from '../../domain/mapTypeDefaults'
import { parseVcf } from '../../lib/import/vcard'
import { serializeContactsExport } from '../../lib/export/contacts'
+import { serializeRelationsExport } from '../../lib/export/relations'
import api from '../../api'
import { localContactRepository } from '../../infrastructure/repositories/contactRepository.local'
import { remoteContactRepository } from '../../infrastructure/repositories/contactRepository.remote'
+import { localRelationRepository } from '../../infrastructure/repositories/relationRepository.local'
+import { remoteRelationRepository } from '../../infrastructure/repositories/relationRepository.remote'
function isLikelyEmail(value) {
return value.includes('@') && value.includes('.')
@@ -157,11 +160,12 @@ async function parseContactRowsFromFile(file, { allowLocalDump = false } = {}) {
throw new Error('Поддерживаются только CSV, JSON и vCard (.vcf) файлы.')
}
-const ALLOWED_EXPORT_FORMATS = new Set(['csv', 'json', 'vcf'])
+const ALLOWED_CONTACT_EXPORT_FORMATS = new Set(['csv', 'json', 'vcf'])
+const ALLOWED_RELATION_EXPORT_FORMATS = new Set(['csv', 'json'])
function contactsExportResult(contacts, format) {
const normalized = String(format || 'csv').toLowerCase()
- if (!ALLOWED_EXPORT_FORMATS.has(normalized)) {
+ if (!ALLOWED_CONTACT_EXPORT_FORMATS.has(normalized)) {
throw new Error('Поддерживаются только CSV, JSON и vCard (.vcf).')
}
const { filename, mime, content } = serializeContactsExport(contacts, normalized)
@@ -173,6 +177,20 @@ function contactsExportResult(contacts, format) {
}
}
+function relationsExportResult(relations, format) {
+ const normalized = String(format || 'csv').toLowerCase()
+ if (!ALLOWED_RELATION_EXPORT_FORMATS.has(normalized)) {
+ throw new Error('Поддерживаются только CSV и JSON.')
+ }
+ const { filename, mime, content } = serializeRelationsExport(relations, normalized)
+ return {
+ filename,
+ blob: new Blob([content], { type: mime }),
+ count: relations.length,
+ format: normalized,
+ }
+}
+
export async function importContactsToLocalFromFile(file) {
const parsed = await parseContactRowsFromFile(file, { allowLocalDump: true })
if (parsed.kind === 'dump') {
@@ -213,9 +231,21 @@ export async function exportContactsFromRemote({ format = 'csv' } = {}) {
return contactsExportResult(contacts, format)
}
+export async function exportRelationsFromLocal({ format = 'csv' } = {}) {
+ const relations = await localRelationRepository.list()
+ if (!relations.length) throw new Error('Нет связей в локальной базе.')
+ return relationsExportResult(relations, format)
+}
+
+export async function exportRelationsFromRemote({ format = 'csv' } = {}) {
+ const relations = await remoteRelationRepository.list()
+ if (!relations.length) throw new Error('Нет связей на сервере.')
+ return relationsExportResult(relations, format)
+}
+
export async function exportContacts({ format = 'csv' } = {}) {
const normalized = String(format || 'csv').toLowerCase()
- if (!ALLOWED_EXPORT_FORMATS.has(normalized)) {
+ if (!ALLOWED_CONTACT_EXPORT_FORMATS.has(normalized)) {
throw new Error('Поддерживаются только CSV, JSON и vCard (.vcf).')
}
@@ -399,6 +429,38 @@ function migrateV1ToV2(dump) {
}
}
+export async function exportRemoteData() {
+ const { data } = await api.get('/v1/export/', { timeout: 120000 })
+ return {
+ filename: `social-graph-export-${Date.now()}.json`,
+ blob: new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }),
+ }
+}
+
+async function parseDumpFile(file, passphrase = '') {
+ const raw = JSON.parse(await file.text())
+ if (isEncryptedLocalDump(raw)) {
+ if (!passphrase) {
+ throw new Error('Для зашифрованного бэкапа укажите пароль.')
+ }
+ return decryptPayload(raw, passphrase)
+ }
+ return raw
+}
+
+export async function importRemoteDump(file, { replace = true, passphrase = '' } = {}) {
+ const dump = await parseDumpFile(file, passphrase)
+ if (!isLocalDataDump(dump)) {
+ throw new Error('Некорректный формат бэкапа: ожидаются массивы contacts и relations.')
+ }
+ const { data } = await api.post('/v1/export/', dump, {
+ params: { replace: replace ? 'true' : 'false' },
+ timeout: 300000,
+ })
+ if (data?.error) throw new Error(data.error)
+ return data
+}
+
export async function exportLocalData({ passphrase = '' } = {}) {
const payload = {
version: 2,
diff --git a/frontend/src/components/import/ExportFormatRow.vue b/frontend/src/components/import/ExportFormatRow.vue
index 42a3840..8daf79a 100644
--- a/frontend/src/components/import/ExportFormatRow.vue
+++ b/frontend/src/components/import/ExportFormatRow.vue
@@ -5,7 +5,7 @@