From 35981be705c6f7d5a11de2666ed90c909f259f1e Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Tue, 24 Dec 2024 17:39:51 +0700 Subject: [PATCH] UBERF-8993: Part2 (#7532) Signed-off-by: Andrey Sobolev --- .gitignore | 1 + packages/core/src/operator.ts | 26 ++++----- .../src/components/DocPopup.svelte | 9 ++-- .../src/components/ObjectSearchPopup.svelte | 16 +++--- packages/presentation/src/utils.ts | 54 ++++++++++++------- packages/ui/src/components/CodeForm.svelte | 3 ++ packages/ui/src/focus.ts | 4 +- packages/ui/src/popups.ts | 11 ++-- packages/ui/src/utils.ts | 8 ++- .../chat-message/ChatMessagePresenter.svelte | 3 +- .../src/components/AccountArrayEditor.svelte | 8 +-- .../src/components/UserBoxList.svelte | 11 ++-- .../src/components/Move.svelte | 2 +- plugins/login-resources/src/utils.ts | 6 ++- .../love-resources/src/components/Room.svelte | 4 +- .../src/components/EditVacancy.svelte | 12 ++++- .../src/components/KanbanCard.svelte | 12 ++--- .../src/components/ClassAttributes.svelte | 2 +- .../project/ProjectSpacePresenter.svelte | 6 ++- .../src/components/extensions.ts | 16 +++--- .../projects/ProjectSpacePresenter.svelte | 4 +- .../view-resources/src/components/Menu.svelte | 6 +-- .../components/filter/FilterTypePopup.svelte | 4 +- .../model/documents/document-content-page.ts | 1 + server/account/src/operations.ts | 7 +-- .../tests/documents/documents-content.spec.ts | 11 ++-- tests/sanity/tests/tracker/issues.spec.ts | 39 +++++++++----- .../sanity/tests/tracker/public-link.spec.ts | 6 ++- 28 files changed, 178 insertions(+), 114 deletions(-) diff --git a/.gitignore b/.gitignore index 4def1db2963..d6cf72d06fb 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,4 @@ tests/profiles dump **/logs/** dev/tool/history.json +.aider* diff --git a/packages/core/src/operator.ts b/packages/core/src/operator.ts index 612426b066f..ce72bc973e2 100644 --- a/packages/core/src/operator.ts +++ b/packages/core/src/operator.ts @@ -28,22 +28,23 @@ function $push (document: Doc, keyval: Record): void { if (doc[key] === undefined) { doc[key] = [] } - const val = keyval[key] - if (typeof val === 'object') { + const kvk = keyval[key] + if (typeof kvk === 'object' && kvk != null) { const arr = doc[key] as Array - const desc = val as Position + const desc = kvk as Position if ('$each' in desc) { - if (arr != null) { + if (arr != null && Array.isArray(arr)) { arr.splice(desc.$position ?? 0, 0, ...desc.$each) } } else { - arr.push(val) + arr.push(kvk) } } else { - if (doc[key] == null) { - doc[key] = [] + if (doc[key] === null || doc[key] === undefined) { + doc[key] = [kvk] + } else { + doc[key].push(kvk) } - doc[key].push(val) } } } @@ -55,15 +56,16 @@ function $pull (document: Doc, keyval: Record): void { doc[key] = [] } const arr = doc[key] as Array - if (typeof keyval[key] === 'object' && keyval[key] !== null) { - const { $in } = keyval[key] as PullArray + const kvk = keyval[key] + if (typeof kvk === 'object' && kvk !== null) { + const { $in } = kvk as PullArray doc[key] = (arr ?? []).filter((val) => { if ($in !== undefined) { return !$in.includes(val) } else { // We need to match all fields - for (const [kk, kv] of Object.entries(keyval[key])) { + for (const [kk, kv] of Object.entries(kvk)) { if (val[kk] !== kv) { return true } @@ -72,7 +74,7 @@ function $pull (document: Doc, keyval: Record): void { } }) } else { - doc[key] = (arr ?? []).filter((val) => val !== keyval[key]) + doc[key] = (arr ?? []).filter((val) => val !== kvk) } } } diff --git a/packages/presentation/src/components/DocPopup.svelte b/packages/presentation/src/components/DocPopup.svelte index 44e47e1d8a9..fe7eb715754 100644 --- a/packages/presentation/src/components/DocPopup.svelte +++ b/packages/presentation/src/components/DocPopup.svelte @@ -93,13 +93,14 @@ dispatch('update', selectedObjects) } - const client = getClient() - let selection = 0 let list: ListView async function handleSelection (evt: Event | undefined, objects: Doc[], selection: number): Promise { const item = objects[selection] + if (item === undefined) { + return + } if (!multiSelect) { if (allowDeselect) { @@ -140,7 +141,7 @@ showPopup(c.component, c.props ?? {}, 'top', async (res) => { if (res != null) { // We expect reference to new object. - const newPerson = await client.findOne(_class, { _id: res }) + const newPerson = await getClient().findOne(_class, { _id: res }) if (newPerson !== undefined) { search = c.update?.(newPerson) ?? '' dispatch('created', newPerson) @@ -163,7 +164,7 @@ } function findObjectPresenter (_class: Ref>): void { - const presenterMixin = client.getHierarchy().classHierarchyMixin(_class, view.mixin.ObjectPresenter) + const presenterMixin = getClient().getHierarchy().classHierarchyMixin(_class, view.mixin.ObjectPresenter) if (presenterMixin?.presenter !== undefined) { getResource(presenterMixin.presenter) .then((result) => { diff --git a/packages/presentation/src/components/ObjectSearchPopup.svelte b/packages/presentation/src/components/ObjectSearchPopup.svelte index 01655d2c175..90926438744 100644 --- a/packages/presentation/src/components/ObjectSearchPopup.svelte +++ b/packages/presentation/src/components/ObjectSearchPopup.svelte @@ -46,7 +46,6 @@ let categories: ObjectSearchCategory[] = [] let categoryStatus: Record, number> = {} - const client = getClient() let category: ObjectSearchCategory | undefined const categoryQuery: DocumentQuery = { @@ -56,10 +55,12 @@ categoryQuery._id = { $in: allowCategory } } - client.findAll(presentation.class.ObjectSearchCategory, categoryQuery).then((r) => { - categories = r.filter((it) => hasResource(it.query)) - category = categories[0] - }) + void getClient() + .findAll(presentation.class.ObjectSearchCategory, categoryQuery) + .then((r) => { + categories = r.filter((it) => hasResource(it.query)) + category = categories[0] + }) const dispatch = createEventDispatcher() @@ -96,7 +97,7 @@ key.preventDefault() key.stopPropagation() const item = items[selection] - if (item) { + if (item != null) { dispatchItem(item) return true } else { @@ -106,7 +107,7 @@ return false } - export function done () {} + export function done (): void {} const updateItems = reduceCalls(async function updateItems ( cat: ObjectSearchCategory | undefined, @@ -120,6 +121,7 @@ const newCategoryStatus: Record, number> = {} const f = await getResource(cat.query) + const client = getClient() const result = await f(client, query, { in: relatedDocuments, nin: ignore }) // We need to sure, results we return is for proper category. if (cat._id === category?._id) { diff --git a/packages/presentation/src/utils.ts b/packages/presentation/src/utils.ts index b92c77d8890..8abd129f4c9 100644 --- a/packages/presentation/src/utils.ts +++ b/packages/presentation/src/utils.ts @@ -53,7 +53,7 @@ import { getRawCurrentLocation, workspaceId, type AnyComponent, type AnySvelteCo import view, { type AttributeCategory, type AttributeEditor } from '@hcengineering/view' import { deepEqual } from 'fast-equals' import { onDestroy } from 'svelte' -import { get, writable, type Writable } from 'svelte/store' +import { get, writable } from 'svelte/store' import { type KeyedAttribute } from '..' import { OptimizeQueryMiddleware, PresentationPipelineImpl, type PresentationPipeline } from './pipeline' @@ -63,7 +63,7 @@ export { reduceCalls } from '@hcengineering/core' let liveQuery: LQ let rawLiveQuery: LQ -let client: TxOperations & Client & OptimisticTxes +let client: TxOperations & Client let pipeline: PresentationPipeline const txListeners: Array<(...tx: Tx[]) => void> = [] @@ -89,13 +89,11 @@ export function removeTxListener (l: (tx: Tx) => void): void { } } -export interface OptimisticTxes { - pendingCreatedDocs: Writable, boolean>> -} - export const uiContext = new MeasureMetricsContext('client-ui', {}) -class UIClient extends TxOperations implements Client, OptimisticTxes { +export const pendingCreatedDocs = writable, boolean>>({}) + +class UIClient extends TxOperations implements Client { hook = getMetadata(plugin.metadata.ClientHook) constructor ( client: Client, @@ -105,14 +103,9 @@ class UIClient extends TxOperations implements Client, OptimisticTxes { } protected pendingTxes = new Set>() - protected _pendingCreatedDocs = writable, boolean>>({}) - - get pendingCreatedDocs (): typeof this._pendingCreatedDocs { - return this._pendingCreatedDocs - } async doNotify (...tx: Tx[]): Promise { - const pending = get(this._pendingCreatedDocs) + const pending = get(pendingCreatedDocs) let pendingUpdated = false tx.forEach((t) => { if (this.pendingTxes.has(t._id)) { @@ -129,7 +122,7 @@ class UIClient extends TxOperations implements Client, OptimisticTxes { } }) if (pendingUpdated) { - this._pendingCreatedDocs.set(pending) + pendingCreatedDocs.set(pending) } // We still want to notify about all transactions because there might be queries created after @@ -214,9 +207,9 @@ class UIClient extends TxOperations implements Client, OptimisticTxes { } if (innerTx._class === core.class.TxCreateDoc) { - const pending = get(this._pendingCreatedDocs) + const pending = get(pendingCreatedDocs) pending[innerTx.objectId] = true - this._pendingCreatedDocs.set(pending) + pendingCreatedDocs.set(pending) } this.pendingTxes.add(tx._id) @@ -231,11 +224,33 @@ class UIClient extends TxOperations implements Client, OptimisticTxes { } } +const hierarchyProxy = new Proxy( + {}, + { + get (target, p, receiver) { + const h = client.getHierarchy() + return Reflect.get(h, p) + } + } +) as TxOperations & Client + +// We need a proxy to handle all the calls to the proper client. +const clientProxy = new Proxy( + {}, + { + get (target, p, receiver) { + if (p === 'getHierarchy') { + return () => hierarchyProxy + } + return Reflect.get(client, p) + } + } +) as TxOperations & Client /** * @public */ -export function getClient (): TxOperations & Client & OptimisticTxes { - return client +export function getClient (): TxOperations & Client { + return clientProxy } let txQueue: Tx[] = [] @@ -252,6 +267,7 @@ export function addRefreshListener (r: RefreshListener): void { * @public */ export async function setClient (_client: Client): Promise { + pendingCreatedDocs.set({}) if (liveQuery !== undefined) { await liveQuery.close() } @@ -700,7 +716,7 @@ export function isSpace (space: Doc): space is Space { } export function isSpaceClass (_class: Ref>): boolean { - return getClient().getHierarchy().isDerived(_class, core.class.Space) + return client.getHierarchy().isDerived(_class, core.class.Space) } export function setPresentationCookie (token: string, workspaceId: string): void { diff --git a/packages/ui/src/components/CodeForm.svelte b/packages/ui/src/components/CodeForm.svelte index 3481922172d..0c885e26cf5 100644 --- a/packages/ui/src/components/CodeForm.svelte +++ b/packages/ui/src/components/CodeForm.svelte @@ -59,6 +59,9 @@ } function onKeydown (e: KeyboardEvent): void { + if (e.key === undefined) { + return + } const key = e.key.toLowerCase() const target = e.target as HTMLInputElement if (key !== 'backspace' && key !== 'delete') return diff --git a/packages/ui/src/focus.ts b/packages/ui/src/focus.ts index 820a81b73c5..3a73ebecb67 100644 --- a/packages/ui/src/focus.ts +++ b/packages/ui/src/focus.ts @@ -63,7 +63,7 @@ class FocusManagerImpl implements FocusManager { return } this.current = this.elements.findIndex((it) => it.id === idx) ?? 0 - this.elements[Math.abs(this.current) % this.elements.length].focus() + this.elements[Math.abs(this.current) % this.elements.length]?.focus() } setFocusPos (order: number): void { @@ -73,7 +73,7 @@ class FocusManagerImpl implements FocusManager { const idx = this.elements.findIndex((it) => it.order === order) if (idx !== undefined) { this.current = idx - this.elements[Math.abs(this.current) % this.elements.length].focus() + this.elements[Math.abs(this.current) % this.elements.length]?.focus() } } diff --git a/packages/ui/src/popups.ts b/packages/ui/src/popups.ts index d5447490e5c..b2e53371fac 100644 --- a/packages/ui/src/popups.ts +++ b/packages/ui/src/popups.ts @@ -149,16 +149,17 @@ export function closePopup (category?: string): void { } else { for (let i = popups.length - 1; i >= 0; i--) { if (popups[i].type !== 'popup') continue - if ((popups[i] as CompAndProps).options.fixed !== true) { - const isClosing = (popups[i] as CompAndProps).closing ?? false + const popi = popups[i] as CompAndProps + if (popi.options.fixed !== true) { + const isClosing = popi.closing ?? false if (popups[i].type === 'popup') { - ;(popups[i] as CompAndProps).closing = true + popi.closing = true } if (!isClosing) { // To prevent possible recursion, we need to check if we call some code from popup close, to do close. - ;(popups[i] as CompAndProps).onClose?.(undefined) + popi.onClose?.(undefined) } - ;(popups[i] as CompAndProps).closing = false + popi.closing = false popups.splice(i, 1) break } diff --git a/packages/ui/src/utils.ts b/packages/ui/src/utils.ts index 60c88455e2d..1d3c88fd818 100644 --- a/packages/ui/src/utils.ts +++ b/packages/ui/src/utils.ts @@ -214,8 +214,12 @@ export function replaceURLs (text: string): string { * @returns {string} string with parsed URL */ export function parseURL (text: string): string { - const matches = autolinker.parse(text, { urls: true }) - return matches.length > 0 ? matches[0].getAnchorHref() : '' + try { + const matches = autolinker.parse(text ?? '', { urls: true }) + return matches.length > 0 ? matches[0].getAnchorHref() : '' + } catch (err: any) { + return '' + } } /** diff --git a/plugins/chunter-resources/src/components/chat-message/ChatMessagePresenter.svelte b/plugins/chunter-resources/src/components/chat-message/ChatMessagePresenter.svelte index 5dc8ed6605f..295f79e9759 100644 --- a/plugins/chunter-resources/src/components/chat-message/ChatMessagePresenter.svelte +++ b/plugins/chunter-resources/src/components/chat-message/ChatMessagePresenter.svelte @@ -16,7 +16,7 @@ import contact, { Person, PersonAccount } from '@hcengineering/contact' import { personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources' import { Class, Doc, getCurrentAccount, Markup, Ref, Space, WithLookup } from '@hcengineering/core' - import { getClient, MessageViewer } from '@hcengineering/presentation' + import { getClient, MessageViewer, pendingCreatedDocs } from '@hcengineering/presentation' import { AttachmentDocList, AttachmentImageSize } from '@hcengineering/attachment-resources' import { getDocLinkTitle } from '@hcengineering/view-resources' import { Action, Button, IconEdit, ShowMore } from '@hcengineering/ui' @@ -58,7 +58,6 @@ export let onReply: ((message: ActivityMessage) => void) | undefined = undefined const client = getClient() - const { pendingCreatedDocs } = client const hierarchy = client.getHierarchy() const STALE_TIMEOUT_MS = 5000 const currentAccount = getCurrentAccount() diff --git a/plugins/contact-resources/src/components/AccountArrayEditor.svelte b/plugins/contact-resources/src/components/AccountArrayEditor.svelte index c336ac027c6..edea6072c9b 100644 --- a/plugins/contact-resources/src/components/AccountArrayEditor.svelte +++ b/plugins/contact-resources/src/components/AccountArrayEditor.svelte @@ -82,9 +82,11 @@ included = [] } - $: employees = Array.from( - (value ?? []).map((it) => $personAccountByIdStore.get(it as Ref)?.person) - ).filter((it) => it !== undefined) as Ref[] + $: employees = Array.isArray(value) + ? (Array.from((value ?? []).map((it) => $personAccountByIdStore.get(it as Ref)?.person)).filter( + (it) => it !== undefined + ) as Ref[]) + : [] $: docQuery = excluded.length === 0 && included.length === 0 diff --git a/plugins/contact-resources/src/components/UserBoxList.svelte b/plugins/contact-resources/src/components/UserBoxList.svelte index c838460ab79..ee1b3a16dd6 100644 --- a/plugins/contact-resources/src/components/UserBoxList.svelte +++ b/plugins/contact-resources/src/components/UserBoxList.svelte @@ -13,7 +13,7 @@ // limitations under the License. -->