2022-12-08 01:17:53 +00:00
|
|
|
import { fetchMetadataLocalStorage, setMetadataLocalStorage } from '@hcengineering/ui'
|
2023-03-24 10:23:44 +00:00
|
|
|
import { deepEqual } from 'fast-equals'
|
2023-11-20 10:01:43 +00:00
|
|
|
import { type Unsubscriber, writable } from 'svelte/store'
|
2022-12-08 01:17:53 +00:00
|
|
|
import presentation from './plugin'
|
|
|
|
|
2023-04-27 16:52:31 +00:00
|
|
|
migrateDrafts()
|
2023-03-24 10:23:44 +00:00
|
|
|
export const draftsStore = writable<Record<string, any>>(fetchMetadataLocalStorage(presentation.metadata.Draft) ?? {})
|
2023-04-27 16:52:31 +00:00
|
|
|
let drafts: Record<string, any> = fetchMetadataLocalStorage(presentation.metadata.Draft) ?? {}
|
|
|
|
const activeDraftsKey = 'activeDrafts'
|
|
|
|
export const activeDraftsStore = writable<Set<string>>(new Set())
|
2023-11-20 10:01:43 +00:00
|
|
|
const activeDrafts = new Set<string>()
|
2023-04-27 16:52:31 +00:00
|
|
|
|
2023-03-24 10:23:44 +00:00
|
|
|
window.addEventListener('storage', storageHandler)
|
2022-12-08 01:17:53 +00:00
|
|
|
|
2023-03-24 10:23:44 +00:00
|
|
|
function storageHandler (evt: StorageEvent): void {
|
2023-04-27 16:52:31 +00:00
|
|
|
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
|
|
|
|
|
2023-06-08 09:57:11 +00:00
|
|
|
const bc = 'BroadcastChannel' in window ? new BroadcastChannel(activeDraftsKey) : undefined
|
2023-04-27 16:52:31 +00:00
|
|
|
|
|
|
|
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 {
|
2023-06-07 06:43:54 +00:00
|
|
|
bc?.postMessage(req)
|
2023-04-27 16:52:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function syncActive (): void {
|
|
|
|
activeDraftsStore.set(activeDrafts)
|
|
|
|
}
|
|
|
|
|
|
|
|
function loadActiveDrafts (): void {
|
|
|
|
activeDrafts.clear()
|
|
|
|
syncActive()
|
|
|
|
sendMessage({ type: 'get_all' })
|
|
|
|
}
|
|
|
|
|
2023-06-07 06:43:54 +00:00
|
|
|
if (bc !== undefined) {
|
|
|
|
bc.onmessage = (e: MessageEvent<BroadcastMessage>) => {
|
|
|
|
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()
|
2023-04-27 16:52:31 +00:00
|
|
|
}
|
2023-03-24 10:23:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-27 16:52:31 +00:00
|
|
|
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
|
|
|
|
|
2023-03-24 10:23:44 +00:00
|
|
|
function isEmptyDraft<T> (object: T, emptyObj: Partial<T> | 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])
|
|
|
|
}
|
2022-12-20 04:25:45 +00:00
|
|
|
} else {
|
2023-03-24 10:23:44 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-04-27 16:52:31 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-24 10:23:44 +00:00
|
|
|
export class DraftController<T> {
|
2023-04-27 16:52:31 +00:00
|
|
|
private unsub: Unsubscriber | undefined = undefined
|
2023-11-20 10:01:43 +00:00
|
|
|
constructor (
|
|
|
|
private readonly id: string | undefined,
|
|
|
|
private readonly parentId: string | undefined = undefined
|
|
|
|
) {
|
2023-04-27 16:52:31 +00:00
|
|
|
if (this.id !== undefined) {
|
|
|
|
addActive(this.id)
|
|
|
|
}
|
|
|
|
}
|
2023-03-24 10:23:44 +00:00
|
|
|
|
|
|
|
static remove (id: string): void {
|
2023-04-27 16:52:31 +00:00
|
|
|
removeDraft(id)
|
2023-03-24 10:23:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static save<T>(id: string, object: T, emptyObj: Partial<T> | undefined = undefined): void {
|
|
|
|
if (emptyObj !== undefined && isEmptyDraft(object, emptyObj)) {
|
2023-11-20 10:01:43 +00:00
|
|
|
DraftController.remove(id)
|
|
|
|
return
|
2023-04-27 16:52:31 +00:00
|
|
|
}
|
|
|
|
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<string, any>): T | undefined {
|
|
|
|
if (this.id !== undefined) {
|
|
|
|
const res = store[this.id]
|
|
|
|
return res
|
2022-12-20 04:25:45 +00:00
|
|
|
}
|
2023-03-24 10:23:44 +00:00
|
|
|
}
|
2022-12-08 01:17:53 +00:00
|
|
|
|
2023-03-24 10:23:44 +00:00
|
|
|
get (): T | undefined {
|
2023-04-27 16:52:31 +00:00
|
|
|
return this.getValue(drafts)
|
2023-03-24 10:23:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
save (object: T, emptyObj: Partial<T> | undefined = undefined): void {
|
|
|
|
if (emptyObj !== undefined && isEmptyDraft(object, emptyObj)) {
|
2023-11-20 10:01:43 +00:00
|
|
|
this.remove()
|
|
|
|
return
|
2023-04-27 16:52:31 +00:00
|
|
|
}
|
|
|
|
if (this.id !== undefined) {
|
2023-03-24 10:23:44 +00:00
|
|
|
drafts[this.id] = object
|
2023-04-27 16:52:31 +00:00
|
|
|
syncDrafts()
|
|
|
|
addActive(this.id)
|
|
|
|
if (this.parentId !== undefined) {
|
|
|
|
MultipleDraftController.add(this.parentId, this.id)
|
|
|
|
}
|
2023-03-24 10:23:44 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-08 01:17:53 +00:00
|
|
|
|
2023-04-27 16:52:31 +00:00
|
|
|
remove (): void {
|
|
|
|
if (this.id !== undefined) {
|
|
|
|
removeDraft(this.id, this.parentId)
|
|
|
|
}
|
2023-03-24 10:23:44 +00:00
|
|
|
}
|
2023-04-27 16:52:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export class MultipleDraftController {
|
|
|
|
constructor (private readonly id: string) {}
|
2022-12-08 01:17:53 +00:00
|
|
|
|
2023-04-27 16:52:31 +00:00
|
|
|
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()
|
2023-03-24 10:23:44 +00:00
|
|
|
}
|
2022-12-08 01:17:53 +00:00
|
|
|
}
|
2023-03-24 10:23:44 +00:00
|
|
|
|
2023-04-27 16:52:31 +00:00
|
|
|
static add (id: string, value: string): void {
|
|
|
|
const arr: string[] = drafts[id] ?? []
|
|
|
|
if (!arr.includes(value)) {
|
|
|
|
arr.push(value)
|
|
|
|
}
|
|
|
|
drafts[id] = arr
|
|
|
|
syncDrafts()
|
2023-03-24 10:23:44 +00:00
|
|
|
}
|
|
|
|
|
2023-04-27 16:52:31 +00:00
|
|
|
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'] = []
|
2022-12-08 01:17:53 +00:00
|
|
|
}
|
2023-04-27 16:52:31 +00:00
|
|
|
setMetadataLocalStorage(presentation.metadata.Draft, drafts)
|
2022-12-08 01:17:53 +00:00
|
|
|
}
|