From be1872abe5e48bf7ebcc77c3596015d97edc7fb8 Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Thu, 27 Apr 2023 22:52:31 +0600 Subject: [PATCH] TSK-1335 Improve draft (#3092) Signed-off-by: Denis Bykhov --- packages/presentation/src/drafts.ts | 291 ++++++++++++++---- .../src/components/CommentInput.svelte | 11 +- .../src/components/CreateCandidate.svelte | 40 +-- .../src/components/NewCandidateHeader.svelte | 14 +- .../src/components/CreateIssue.svelte | 43 +-- .../src/components/NewIssueHeader.svelte | 22 +- .../src/components/SubIssues.svelte | 4 +- .../src/components/issues/StatusEditor.svelte | 16 +- .../issues/edit/CreateSubIssue.svelte | 19 +- .../components/issues/edit/SubIssues.svelte | 2 +- .../templates/DraftIssueChildEditor.svelte | 22 +- 11 files changed, 343 insertions(+), 141 deletions(-) diff --git a/packages/presentation/src/drafts.ts b/packages/presentation/src/drafts.ts index 7ec8b1e73b..f63274845d 100644 --- a/packages/presentation/src/drafts.ts +++ b/packages/presentation/src/drafts.ts @@ -1,20 +1,115 @@ import { fetchMetadataLocalStorage, setMetadataLocalStorage } from '@hcengineering/ui' import { deepEqual } from 'fast-equals' -import { get, writable } from 'svelte/store' +import { 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: Set = new Set() + window.addEventListener('storage', storageHandler) -const saveInterval = 200 function storageHandler (evt: StorageEvent): void { - if (evt.storageArea !== localStorage) return - if (evt.key !== presentation.metadata.Draft) return - if (evt.newValue !== null) { - draftsStore.set(JSON.parse(evt.newValue)) + 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 = new BroadcastChannel(activeDraftsKey) + +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' }) +} + +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 @@ -44,73 +139,161 @@ function isEmptyDraft (object: T, emptyObj: Partial | undefined): boolean 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 timer: number | undefined = undefined - constructor (private readonly id: string) {} + 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 { - const drafts = get(draftsStore) - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete drafts[id] - draftsStore.set(drafts) - setMetadataLocalStorage(presentation.metadata.Draft, drafts) + removeDraft(id) } static save(id: string, object: T, emptyObj: Partial | undefined = undefined): void { - const drafts = get(draftsStore) if (emptyObj !== undefined && isEmptyDraft(object, emptyObj)) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete drafts[id] - } else { - drafts[id] = object + return DraftController.remove(id) + } + 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 } - draftsStore.set(drafts) - setMetadataLocalStorage(presentation.metadata.Draft, drafts) } get (): T | undefined { - const drafts = get(draftsStore) - const res = drafts[this.id] - return res + return this.getValue(drafts) } save (object: T, emptyObj: Partial | undefined = undefined): void { - const drafts = get(draftsStore) if (emptyObj !== undefined && isEmptyDraft(object, emptyObj)) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete drafts[this.id] - } else { + return this.remove() + } + if (this.id !== undefined) { drafts[this.id] = object + syncDrafts() + addActive(this.id) + if (this.parentId !== undefined) { + MultipleDraftController.add(this.parentId, this.id) + } } - draftsStore.set(drafts) - setMetadataLocalStorage(presentation.metadata.Draft, drafts) - } - - private update (object: T, emptyObj: Partial | undefined): void { - this.timer = window.setTimeout(() => { - this.save(object, emptyObj) - this.update(object, emptyObj) - }, saveInterval) - } - - unsubscribe (): void { - if (this?.timer !== undefined) { - clearTimeout(this.timer) - } - } - - watch (object: T, emptyObj: Partial | undefined = undefined): void { - this.unsubscribe() - this.save(object, emptyObj) - this.update(object, emptyObj) } remove (): void { - this.unsubscribe() - const drafts = get(draftsStore) - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete drafts[this.id] - setMetadataLocalStorage(presentation.metadata.Draft, drafts) - draftsStore.set(drafts) + 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) +} diff --git a/plugins/chunter-resources/src/components/CommentInput.svelte b/plugins/chunter-resources/src/components/CommentInput.svelte index eeb0846831..770d4a1878 100644 --- a/plugins/chunter-resources/src/components/CommentInput.svelte +++ b/plugins/chunter-resources/src/components/CommentInput.svelte @@ -14,13 +14,12 @@ // limitations under the License. -->