diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 1780d57835..94bebd0ba2 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -89,6 +89,7 @@ services: - MINIO_SECRET_KEY=minioadmin - TITLE=DevPlatform - DEFAULT_LANGUAGE=ru + - LAST_NAME_FIRST=true restart: unless-stopped collaborator: image: hardcoreeng/collaborator @@ -156,6 +157,7 @@ services: # - APM_SERVER_URL=http://apm-server:8200 - SERVER_PROVIDER=ws - ACCOUNTS_URL=http://account:3000 + - LAST_NAME_FIRST=true restart: unless-stopped rekoni: image: hardcoreeng/rekoni-service diff --git a/dev/prod/public/config.json b/dev/prod/public/config.json index e6e6909711..5670346aea 100644 --- a/dev/prod/public/config.json +++ b/dev/prod/public/config.json @@ -6,5 +6,6 @@ "GMAIL_URL": "http://localhost:8088", "CALENDAR_URL": "http://localhost:8095", "REKONI_URL": "http://localhost:4004", - "COLLABORATOR_URL": "ws://localhost:3078" + "COLLABORATOR_URL": "ws://localhost:3078", + "LAST_NAME_FIRST": "true" } \ No newline at end of file diff --git a/dev/storage/src/storage.ts b/dev/storage/src/storage.ts index 36d0fd789c..8218c05733 100644 --- a/dev/storage/src/storage.ts +++ b/dev/storage/src/storage.ts @@ -44,17 +44,11 @@ class InMemoryTxAdapter extends DummyDbAdapter implements TxAdapter { return await this.txdb.findAll(_class, query, options) } - async tx (...tx: Tx[]): Promise { + async tx (...tx: Tx[]): Promise { const r: TxResult[] = [] for (const t of tx) { r.push(await this.txdb.tx(t)) } - if (r.length === 1) { - return r[0] - } - if (r.length === 0) { - return {} - } return r } diff --git a/packages/core/src/memdb.ts b/packages/core/src/memdb.ts index b08c7f1944..2fa1e580b9 100644 --- a/packages/core/src/memdb.ts +++ b/packages/core/src/memdb.ts @@ -258,9 +258,9 @@ export class TxDb extends MemDb { throw new Error('Method not implemented.') } - async tx (tx: Tx): Promise { + async tx (tx: Tx): Promise { this.addDoc(tx) - return {} + return [] } } diff --git a/packages/core/src/operations.ts b/packages/core/src/operations.ts index 1e0efe7420..684ba74660 100644 --- a/packages/core/src/operations.ts +++ b/packages/core/src/operations.ts @@ -19,13 +19,13 @@ import type { DocumentQuery, FindOptions, FindResult, - SearchQuery, SearchOptions, + SearchQuery, SearchResult, TxResult, WithLookup } from './storage' -import { DocumentClassQuery, Tx, TxCUD, TxFactory, TxProcessor } from './tx' +import { DocumentClassQuery, Tx, TxApplyResult, TxCUD, TxFactory, TxProcessor } from './tx' /** * @public @@ -464,17 +464,19 @@ export class ApplyOperations extends TxOperations { async commit (notify: boolean = true, extraNotify: Ref>[] = []): Promise { if (this.txes.length > 0) { - return await ((await this.ops.tx( - this.ops.txFactory.createTxApplyIf( - core.space.Tx, - this.scope, - this.matches, - this.notMatches, - this.txes, - notify, - extraNotify - ) - )) as Promise) + return ( + await ((await this.ops.tx( + this.ops.txFactory.createTxApplyIf( + core.space.Tx, + this.scope, + this.matches, + this.notMatches, + this.txes, + notify, + extraNotify + ) + )) as Promise) + ).success } return true } diff --git a/packages/core/src/tx.ts b/packages/core/src/tx.ts index fa955cc514..7596321f7a 100644 --- a/packages/core/src/tx.ts +++ b/packages/core/src/tx.ts @@ -140,6 +140,11 @@ export interface TxApplyIf extends Tx { extraNotify?: Ref>[] } +export interface TxApplyResult { + success: boolean + derived: Tx[] // Some derived transactions to handle. +} + /** * @public */ @@ -318,14 +323,14 @@ export const DOMAIN_TX = 'tx' as Domain * @public */ export interface WithTx { - tx: (...txs: Tx[]) => Promise + tx: (...txs: Tx[]) => Promise } /** * @public */ export abstract class TxProcessor implements WithTx { - async tx (...txes: Tx[]): Promise { + async tx (...txes: Tx[]): Promise { const result: TxResult[] = [] for (const tx of txes) { switch (tx._class) { @@ -346,15 +351,9 @@ export abstract class TxProcessor implements WithTx { break case core.class.TxApplyIf: // Apply if processed on server - return await Promise.resolve({}) + return await Promise.resolve([]) } } - if (result.length === 0) { - return {} - } - if (result.length === 1) { - return result[0] - } return result } diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index 6c315beff9..33a0d17a8e 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -683,6 +683,18 @@ export class LiveQuery extends TxProcessor implements Client { } } + private triggerRefresh (q: Query): void { + const r: Promise> | FindResult = this.client.findAll(q._class, q.query, q.options) + q.result = r + void r.then(async (qr) => { + const oldResult = q.result + if (!deepEqual(qr, oldResult) || (qr.total !== q.total && q.options?.total === true)) { + q.total = qr.total + await this.callback(q) + } + }) + } + // Check if query is partially matched. private async matchQuery (q: Query, tx: TxUpdateDoc, docCache?: Map): Promise { const clazz = this.getHierarchy().isMixin(q._class) ? this.getHierarchy().getBaseClass(q._class) : q._class @@ -1056,24 +1068,31 @@ export class LiveQuery extends TxProcessor implements Client { return result } - async tx (tx: Tx): Promise { - if (tx._class === core.class.TxWorkspaceEvent) { - await this.checkUpdateFulltextQueries(tx) - await this.changePrivateHandler(tx) - return {} + async tx (...txes: Tx[]): Promise { + const result: TxResult[] = [] + for (const tx of txes) { + if (tx._class === core.class.TxWorkspaceEvent) { + await this.checkUpdateEvents(tx) + await this.changePrivateHandler(tx) + } + result.push(...(await super.tx(tx))) } - return await super.tx(tx) + return result } - private async checkUpdateFulltextQueries (tx: Tx): Promise { + private async checkUpdateEvents (tx: Tx): Promise { const evt = tx as TxWorkspaceEvent + const h = this.client.getHierarchy() + function hasClass (q: Query, classes: Ref>[]): boolean { + return classes.includes(q._class) || classes.some((it) => h.isDerived(q._class, it) || h.isDerived(it, q._class)) + } if (evt.event === WorkspaceEvent.IndexingUpdate) { const indexingParam = evt.params as IndexingUpdateEvent for (const q of [...this.queue]) { - if (indexingParam._class.includes(q._class) && q.query.$search !== undefined) { + if (hasClass(q, indexingParam._class) && q.query.$search !== undefined) { if (!this.removeFromQueue(q)) { try { - await this.refresh(q) + this.triggerRefresh(q) } catch (err) { console.error(err) } @@ -1082,9 +1101,9 @@ export class LiveQuery extends TxProcessor implements Client { } for (const v of this.queries.values()) { for (const q of v) { - if (indexingParam._class.includes(q._class) && q.query.$search !== undefined) { + if (hasClass(q, indexingParam._class) && q.query.$search !== undefined) { try { - await this.refresh(q) + this.triggerRefresh(q) } catch (err) { console.error(err) } @@ -1095,10 +1114,10 @@ export class LiveQuery extends TxProcessor implements Client { if (evt.event === WorkspaceEvent.BulkUpdate) { const params = evt.params as BulkUpdateEvent for (const q of [...this.queue]) { - if (params._class.includes(q._class)) { + if (hasClass(q, params._class)) { if (!this.removeFromQueue(q)) { try { - await this.refresh(q) + this.triggerRefresh(q) } catch (err) { console.error(err) } @@ -1107,9 +1126,9 @@ export class LiveQuery extends TxProcessor implements Client { } for (const v of this.queries.values()) { for (const q of v) { - if (params._class.includes(q._class)) { + if (hasClass(q, params._class)) { try { - await this.refresh(q) + this.triggerRefresh(q) } catch (err) { console.error(err) } diff --git a/plugins/client-resources/src/connection.ts b/plugins/client-resources/src/connection.ts index 72d87311ab..e630728971 100644 --- a/plugins/client-resources/src/connection.ts +++ b/plugins/client-resources/src/connection.ts @@ -27,19 +27,20 @@ import core, { FindOptions, FindResult, LoadModelResponse, + MeasureDoneOperation, Ref, + SearchOptions, + SearchQuery, + SearchResult, Timestamp, Tx, TxApplyIf, + TxApplyResult, TxHandler, TxResult, TxWorkspaceEvent, WorkspaceEvent, - generateId, - SearchQuery, - SearchOptions, - SearchResult, - MeasureDoneOperation + generateId } from '@hcengineering/core' import { PlatformError, UNAUTHORIZED, broadcastEvent, getMetadata, unknownError } from '@hcengineering/platform' @@ -57,7 +58,8 @@ class RequestPromise { reconnect?: () => void constructor ( readonly method: string, - readonly params: any[] + readonly params: any[], + readonly handleResult?: (result: any) => Promise ) { this.promise = new Promise((resolve, reject) => { this.resolve = resolve @@ -263,7 +265,13 @@ class Connection implements ClientConnection { ) promise.reject(new PlatformError(resp.error)) } else { - promise.resolve(resp.result) + if (request?.handleResult !== undefined) { + void request.handleResult(resp.result).then(() => { + promise.resolve(resp.result) + }) + } else { + promise.resolve(resp.result) + } } void broadcastEvent(client.event.NetworkRequests, this.requests.size) } else { @@ -336,13 +344,14 @@ class Connection implements ClientConnection { params: any[] // If not defined, on reconnect with timeout, will retry automatically. retry?: () => Promise + handleResult?: (result: any) => Promise }): Promise { if (this.closed) { throw new PlatformError(unknownError('connection closed')) } const id = this.lastId++ - const promise = new RequestPromise(data.method, data.params) + const promise = new RequestPromise(data.method, data.params, data.handleResult) const sendData = async (): Promise => { if (this.websocket instanceof Promise) { @@ -423,7 +432,19 @@ class Connection implements ClientConnection { return (await this.findAll(core.class.Tx, { _id: (tx as TxApplyIf).txes[0]._id }, { limit: 1 })).length === 0 } return (await this.findAll(core.class.Tx, { _id: tx._id }, { limit: 1 })).length === 0 - } + }, + handleResult: + tx._class === core.class.TxApplyIf + ? async (result) => { + if (tx._class === core.class.TxApplyIf) { + // We need to check extra broadcast's and perform them before + const r = result as TxApplyResult + for (const d of r?.derived ?? []) { + this.handler(d) + } + } + } + : undefined }) } diff --git a/plugins/contact-resources/src/components/DeleteConfirmationPopup.svelte b/plugins/contact-resources/src/components/DeleteConfirmationPopup.svelte index f546d182af..d772c13ca4 100644 --- a/plugins/contact-resources/src/components/DeleteConfirmationPopup.svelte +++ b/plugins/contact-resources/src/components/DeleteConfirmationPopup.svelte @@ -25,8 +25,9 @@ import { ObjectPresenter } from '@hcengineering/view-resources' export let object: Doc | Doc[] - export let deleteAction: () => void + export let deleteAction: () => void | Promise export let skipCheck: boolean = false + export let canDeleteExtra: boolean = true const objectArray = Array.isArray(object) ? object : [object] const owners: PersonAccount[] = Array.from($personAccountByIdStore.values()).filter( (acc) => acc.role === AccountRole.Owner @@ -34,9 +35,10 @@ const dispatch = createEventDispatcher() $: creators = [...new Set(objectArray.map((obj) => obj.createdBy as Ref))] $: canDelete = - skipCheck || - (creators.length === 1 && creators.includes(getCurrentAccount()._id as Ref)) || - getCurrentAccount().role === AccountRole.Owner + (skipCheck || + (creators.length === 1 && creators.includes(getCurrentAccount()._id as Ref)) || + getCurrentAccount().role === AccountRole.Owner) && + canDeleteExtra $: label = canDelete ? view.string.DeleteObject : view.string.DeletePopupNoPermissionTitle @@ -56,9 +58,11 @@ {#if canDelete}
{:else}
@@ -82,4 +86,5 @@
{/if} + diff --git a/plugins/contact-resources/src/index.ts b/plugins/contact-resources/src/index.ts index fc9e28a0e1..91d31355a5 100644 --- a/plugins/contact-resources/src/index.ts +++ b/plugins/contact-resources/src/index.ts @@ -139,6 +139,7 @@ export { EmployeeArrayEditor, EmployeeEditor, PersonAccountRefPresenter, + PersonAccountPresenter, MembersPresenter, EditPerson, EmployeeRefPresenter, @@ -168,7 +169,8 @@ export { UsersList, SelectUsersPopup, IconAddMember, - UserDetails + UserDetails, + DeleteConfirmationPopup } const toObjectSearchResult = (e: WithLookup): ObjectSearchResult => ({ diff --git a/plugins/recruit-resources/src/components/CreateVacancy.svelte b/plugins/recruit-resources/src/components/CreateVacancy.svelte index a27b816b66..3622d50db2 100644 --- a/plugins/recruit-resources/src/components/CreateVacancy.svelte +++ b/plugins/recruit-resources/src/components/CreateVacancy.svelte @@ -47,7 +47,7 @@ let appliedTemplateId: Ref | undefined let objectId: Ref = generateId() - let issueTemplates: FindResult + let issueTemplates: FindResult = [] let fullDescription: string = '' diff --git a/plugins/task-assets/lang/en.json b/plugins/task-assets/lang/en.json index 4423290033..29f4e8d730 100644 --- a/plugins/task-assets/lang/en.json +++ b/plugins/task-assets/lang/en.json @@ -95,6 +95,8 @@ "Type": "Type", "Group": "Group", "Color": "Color", - "Identifier": "Identifier" + "Identifier": "Identifier", + "RenameStatus": "Rename a status to new name", + "UpdateTasksStatusRequest": "Status is used with {total} tasks, it will require update of all of them. Please approve." } } \ No newline at end of file diff --git a/plugins/task-assets/lang/ru.json b/plugins/task-assets/lang/ru.json index c5d0fe28ca..7238400a9c 100644 --- a/plugins/task-assets/lang/ru.json +++ b/plugins/task-assets/lang/ru.json @@ -95,6 +95,8 @@ "Type": "Тип", "Group": "Группа", "Color": "Цвет", - "Identifier": "Идентификатор" + "Identifier": "Идентификатор", + "RenameStatus": "Переименование статуса в новое имя", + "UpdateTasksStatusRequest": "Статус сейчас используется в {total} задачах, потребуется обновление всеи их. Пожалуйста подтвердите." } } \ No newline at end of file diff --git a/plugins/task-resources/src/components/CreateStatePopup.svelte b/plugins/task-resources/src/components/CreateStatePopup.svelte deleted file mode 100644 index 9009fd2f7d..0000000000 --- a/plugins/task-resources/src/components/CreateStatePopup.svelte +++ /dev/null @@ -1,251 +0,0 @@ - - - - { - clearSettingsStore() - }} -> - - - - -
- -