import { fetchMetadataLocalStorage, setMetadataLocalStorage } from '@hcengineering/ui' import { deepEqual } from 'fast-equals' import { type Unsubscriber, writable } from 'svelte/store' import presentation from './plugin' migrateDrafts() export const draftsStore = writable>(fetchMetadataLocalStorage(presentation.metadata.Draft) ?? {}) let drafts: Record = fetchMetadataLocalStorage(presentation.metadata.Draft) ?? {} const activeDraftsKey = 'activeDrafts' export const activeDraftsStore = writable>(new Set()) const activeDrafts = new Set() window.addEventListener('storage', storageHandler) function storageHandler (evt: StorageEvent): void { if (evt.storageArea === localStorage) { if (evt.key !== presentation.metadata.Draft) return if (evt.newValue !== null) { drafts = JSON.parse(evt.newValue) draftsStore.set(drafts) } } } function syncDrafts (): void { draftsStore.set(drafts) setMetadataLocalStorage(presentation.metadata.Draft, drafts) } // #region Broadcast const bc = 'BroadcastChannel' in window ? new BroadcastChannel(activeDraftsKey) : undefined type BroadcastMessage = BroadcastGetMessage | BroadcastGetResp | BroadcastAddMessage | BroadcastRemoveMessage interface BroadcastGetMessage { type: 'get_all' } interface BroadcastGetResp { type: 'get_all_response' value: string[] } interface BroadcastRemoveMessage { type: 'remove' value: string } interface BroadcastAddMessage { type: 'add' value: string } function sendMessage (req: BroadcastMessage): void { bc?.postMessage(req) } function syncActive (): void { activeDraftsStore.set(activeDrafts) } function loadActiveDrafts (): void { activeDrafts.clear() syncActive() sendMessage({ type: 'get_all' }) } if (bc !== undefined) { bc.onmessage = (e: MessageEvent) => { if (e.data.type === 'get_all') { sendMessage({ type: 'get_all_response', value: Array.from(activeDrafts.values()) }) } if (e.data.type === 'get_all_response') { for (const val of e.data.value) { activeDrafts.add(val) } syncActive() } if (e.data.type === 'add') { activeDrafts.add(e.data.value) syncActive() } if (e.data.type === 'remove') { activeDrafts.delete(e.data.value) syncActive() } } } loadActiveDrafts() // #endregion // #region Active function addActive (id: string): void { if (!activeDrafts.has(id)) { activeDrafts.add(id) syncActive() sendMessage({ type: 'add', value: id }) } } function deleteActive (id: string): void { if (activeDrafts.has(id)) { activeDrafts.delete(id) syncActive() sendMessage({ type: 'remove', value: id }) } } // #endregion function isEmptyDraft (object: T, emptyObj: Partial | undefined): boolean { for (const key in object) { if (key === '_id') continue const value = object[key] let res: boolean = false if (Array.isArray(value)) { res = value.length > 0 if (res && emptyObj != null) { res = !deepEqual(value, emptyObj[key]) } } else { res = value != null if (res && typeof value === 'string') { res = value.trim() !== '' } if (res && typeof value === 'number') { res = value !== 0 } if (res && emptyObj != null) { res = !deepEqual(value, emptyObj[key]) } } if (res) { return false } } return true } function removeDraft (id: string, parentId: string | undefined = undefined): void { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete drafts[id] deleteActive(id) syncDrafts() if (parentId !== undefined) { MultipleDraftController.remove(parentId, id) } } export class DraftController { private unsub: Unsubscriber | undefined = undefined constructor ( private readonly id: string | undefined, private readonly parentId: string | undefined = undefined ) { if (this.id !== undefined) { addActive(this.id) } } static remove (id: string): void { removeDraft(id) } static save(id: string, object: T, emptyObj: Partial | undefined = undefined): void { if (emptyObj !== undefined && isEmptyDraft(object, emptyObj)) { DraftController.remove(id) return } drafts[id] = object addActive(id) syncDrafts() } destroy (): void { this.unsub?.() if (this.id !== undefined) { deleteActive(this.id) } } subscribe (callback: (val: T | undefined) => void): Unsubscriber { callback(this.get()) this.unsub = draftsStore.subscribe((p) => { callback(this.getValue(p)) }) return () => { this.destroy() } } private getValue (store: Record): T | undefined { if (this.id !== undefined) { const res = store[this.id] return res } } get (): T | undefined { return this.getValue(drafts) } save (object: T, emptyObj: Partial | undefined = undefined): void { if (emptyObj !== undefined && isEmptyDraft(object, emptyObj)) { this.remove() return } if (this.id !== undefined) { drafts[this.id] = object syncDrafts() addActive(this.id) if (this.parentId !== undefined) { MultipleDraftController.add(this.parentId, this.id) } } } remove (): void { if (this.id !== undefined) { removeDraft(this.id, this.parentId) } } } export class MultipleDraftController { constructor (private readonly id: string) {} static remove (id: string, value: string): void { const arr: string[] = drafts[id] ?? [] const index = arr.findIndex((p) => p === value) if (index !== -1) { arr.splice(index, 1) drafts[id] = arr syncDrafts() } } static add (id: string, value: string): void { const arr: string[] = drafts[id] ?? [] if (!arr.includes(value)) { arr.push(value) } drafts[id] = arr syncDrafts() } getNext (): string | undefined { const value = drafts[this.id] ?? [] for (const val of value) { if (!activeDrafts.has(val)) { return val } } } hasNext (callback: (value: boolean) => void): Unsubscriber { const next = this.getNext() // eslint-disable-next-line callback(next !== undefined) const draftSub = draftsStore.subscribe((drafts) => { const value = drafts[this.id] ?? [] for (const val of value) { if (!activeDrafts.has(val)) { // eslint-disable-next-line callback(true) return } } // eslint-disable-next-line callback(false) }) const activeSub = activeDraftsStore.subscribe((activeDrafts) => { const value = drafts[this.id] ?? [] for (const val of value) { if (!activeDrafts.has(val)) { // eslint-disable-next-line callback(true) return } } // eslint-disable-next-line callback(false) }) return () => { draftSub() activeSub() } } } function migrateDrafts (): void { const drafts = fetchMetadataLocalStorage(presentation.metadata.Draft) ?? {} const issues = drafts['tracker:ids:IssueDraft'] if (!Array.isArray(issues)) { drafts['tracker:ids:IssueDraft'] = [] } const candidates = drafts['recruit:mixin:Candidate'] if (!Array.isArray(candidates)) { drafts['recruit:mixin:Candidate'] = [] } setMetadataLocalStorage(presentation.metadata.Draft, drafts) }