Add multiple network maps, graph UX improvements, and full backup import.

Support per-map contact membership with scoped graph views, relation editing on edges, layout caching, and automatic detection of full JSON backups so contacts and relations import together.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-24 17:54:51 +03:00
co-authored by Cursor
parent 35ec4c81ec
commit 5155b3a37a
50 changed files with 3176 additions and 372 deletions
@@ -0,0 +1,129 @@
import { localDb, DEFAULT_MAP_NAME } from '../db/localDb'
import { generateId } from '../../lib/uuid'
function nowIso() {
return new Date().toISOString()
}
function withDefaults(payload = {}) {
return {
name: '',
description: '',
workspaceId: 'personal',
...payload,
}
}
async function findActiveMap(id) {
const direct = await localDb.networkMaps.get(id)
if (direct && !direct.deletedAt) return direct
const sid = String(id)
const all = await localDb.networkMaps.toArray()
return all.find((m) => !m.deletedAt && String(m.id) === sid) || null
}
async function membershipsCount(mapId) {
const all = await localDb.networkMapMemberships.toArray()
return all.filter((m) => !m.deletedAt && String(m.mapId) === String(mapId)).length
}
async function hydrate(map) {
if (!map || map.deletedAt) return null
return {
...map,
memberships_count: await membershipsCount(map.id),
}
}
async function ensureDefaultMap() {
const all = await localDb.networkMaps.toArray()
const active = all.filter((m) => !m.deletedAt)
if (active.length) return active.sort((a, b) => a.name.localeCompare(b.name, 'ru'))[0]
const ts = nowIso()
const id = generateId()
await localDb.networkMaps.put({
id,
name: DEFAULT_MAP_NAME,
description: '',
workspaceId: 'personal',
version: 1,
createdAt: ts,
updatedAt: ts,
deletedAt: null,
})
return findActiveMap(id)
}
export const localNetworkMapRepository = {
async list() {
const all = await localDb.networkMaps.toArray()
const filtered = all
.filter((m) => !m.deletedAt)
.sort((a, b) => a.name.localeCompare(b.name, 'ru'))
if (!filtered.length) {
const defaultMap = await ensureDefaultMap()
return [await hydrate(defaultMap)]
}
const hydrated = await Promise.all(filtered.map(hydrate))
return hydrated.filter(Boolean)
},
async getById(id) {
const map = await findActiveMap(id)
return hydrate(map)
},
async create(payload) {
const ts = nowIso()
const record = withDefaults(payload)
const id = generateId()
await localDb.networkMaps.put({
...record,
id,
version: 1,
createdAt: ts,
updatedAt: ts,
deletedAt: null,
})
return this.getById(id)
},
async update(id, payload) {
const existing = await findActiveMap(id)
if (!existing) {
throw new Error('Карта не найдена')
}
await localDb.networkMaps.update(existing.id, {
...payload,
updatedAt: nowIso(),
version: Number(existing.version || 1) + 1,
})
return this.getById(existing.id)
},
async remove(id) {
const map = await findActiveMap(id)
if (!map) {
throw new Error('Карта не найдена')
}
const ts = nowIso()
await localDb.networkMaps.update(map.id, {
deletedAt: ts,
updatedAt: ts,
version: Number(map.version || 1) + 1,
})
const memberships = await localDb.networkMapMemberships
.filter((m) => !m.deletedAt && String(m.mapId) === String(map.id))
.toArray()
await Promise.all(
memberships.map((m) =>
localDb.networkMapMemberships.update(m.id, {
deletedAt: ts,
updatedAt: ts,
version: Number(m.version || 1) + 1,
})
)
)
},
}