feat: import

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-06-28 18:06:12 -04:00
parent 0880bba063
commit b7e12621cd
16 changed files with 968 additions and 11 deletions
+124
View File
@@ -0,0 +1,124 @@
/**
* Contact Import Store
*/
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import { entityService } from '../services/entityService'
import type { CollectionIdentifier } from '../types/common'
import type { EntityImportResponse } from '../types/entity'
import type {
ImportCounters,
ImportFileAdd,
ImportFileEntry,
ImportFileOptions,
ImportSession,
ImportSessionStage
} from '../types/import'
const RECENT_RESULTS_CAP = 25
const emptyCounters = (): ImportCounters => ({ discovered: 0, processed: 0, created: 0, updated: 0, exists: 0, error: 0 })
export const useImportStore = defineStore('chronoImportStore', () => {
// State
const nextId = ref(0)
const files = ref<ImportFileEntry[]>([])
const stage = ref<ImportSessionStage>('idle')
const running = ref(false)
const sessions = ref<Record<number, ImportSession>>({})
const order = ref<number[]>([])
// Computed
const totals = computed(() => order.value.reduce((total, id) => {
const counters = sessions.value[id]?.counters
if (counters) Object.keys(total).forEach(key => {
total[key as keyof ImportCounters] += counters[key as keyof ImportCounters]
})
return total
}, emptyCounters()))
// Actions
function addFile(file: ImportFileAdd): number {
const id = nextId.value++
files.value.push({
file: { id, ...file },
collectionId: null,
options: { supersede: false }
})
return id
}
function removeAllFiles() {
files.value = []
}
function reset() {
stage.value = 'idle';
running.value = false;
sessions.value = {};
order.value = [];
}
function entryFor(id: number) {
return files.value.find(entry => entry.file.id === id)
}
/** Set the destination collection for a queued file. */
function setCollectionForFile(id: number, collectionId: CollectionIdentifier | null) {
const entry = entryFor(id);
if (entry) entry.collectionId = collectionId;
}
/** Merge option changes for a queued file without dropping untouched keys. */
function setOptionsForFile(id: number, options: Partial<ImportFileOptions>) {
const entry = entryFor(id);
if (entry) entry.options = { ...entry.options, ...options };
}
async function startImport(): Promise<ImportCounters> {
const entries = [...files.value]
sessions.value = {}
order.value = entries.map(entry => entry.file.id)
for (const entry of entries) sessions.value[entry.file.id] = {
fileId: entry.file.id, fileName: entry.file.name, targetIdentifier: entry.collectionId,
status: 'pending', counters: emptyCounters(), recentResults: [], lastError: null,
}
running.value = true
stage.value = 'importing'
try {
for (const entry of entries) {
const session = sessions.value[entry.file.id]!
if (!entry.collectionId) throw new Error(`No calendar selected for "${entry.file.name}"`)
session.status = 'importing'
try {
await entityService.import(
{ target: entry.collectionId, data: entry.file.contents, options: entry.options },
(object: EntityImportResponse) => {
session.counters.processed++
session.counters[object.disposition]++
session.recentResults = [object, ...session.recentResults].slice(0, RECENT_RESULTS_CAP)
},
expected => { session.counters.discovered = expected },
)
session.status = session.counters.error ? 'error' : 'completed'
} catch (error) {
session.status = 'error'
session.lastError = error instanceof Error ? error.message : String(error)
throw error
}
}
stage.value = 'completed'
return totals.value
} catch (error) {
stage.value = 'error'
throw error
} finally { running.value = false }
}
return { files, stage, running, sessions, order, totals, addFile, removeAllFiles, reset, setCollectionForFile, setOptionsForFile, startImport }
})
+1
View File
@@ -2,3 +2,4 @@ export { useProvidersStore } from './providersStore';
export { useServicesStore } from './servicesStore';
export { useCollectionsStore } from './collectionsStore';
export { useEntitiesStore } from './entitiesStore';
export { useImportStore } from './importStore';