Fix load of applications in tables/lists/kanban (#6231)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-08-02 21:14:49 +07:00 committed by GitHub
parent 943c01bf8c
commit 1c965209a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 799 additions and 359 deletions

1
.vscode/launch.json vendored
View File

@ -100,6 +100,7 @@
"ACCOUNTS_URL": "http://localhost:3000",
"UPLOAD_URL": "/files",
"SERVER_PORT": "8087",
"VERSION": null,
"COLLABORATOR_URL": "ws://localhost:3078",
"COLLABORATOR_API_URL": "http://localhost:3078",
"CALENDAR_URL": "http://localhost:8095",

View File

@ -1 +1 @@
"0.6.270"
"0.6.271"

View File

@ -77,6 +77,7 @@ services:
- ACCOUNTS_URL=http://account:3000
- UPLOAD_URL=/files
- MONGO_URL=mongodb://mongodb:27017?compressors=snappy
- 'MONGO_OPTIONS={"appName":"collaborator","maxPoolSize":2}'
- STORAGE_CONFIG=${STORAGE_CONFIG}
restart: unless-stopped
front:
@ -95,6 +96,7 @@ services:
- SERVER_PORT=8080
- SERVER_SECRET=secret
- MONGO_URL=mongodb://mongodb:27017?compressors=snappy
- 'MONGO_OPTIONS={"appName":"front","maxPoolSize":1}'
- ACCOUNTS_URL=http://localhost:3000
- UPLOAD_URL=/files
- ELASTIC_URL=http://elastic:9200
@ -135,6 +137,7 @@ services:
- ENABLE_COMPRESSION=true
- ELASTIC_URL=http://elastic:9200
- MONGO_URL=mongodb://mongodb:27017?compressors=snappy
- 'MONGO_OPTIONS={"appName": "transactor", "maxPoolSize": 1}'
- METRICS_CONSOLE=false
- METRICS_FILE=metrics.txt
- STORAGE_CONFIG=${STORAGE_CONFIG}
@ -165,6 +168,7 @@ services:
environment:
- SECRET=secret
- MONGO_URL=mongodb://mongodb:27017?compressors=snappy
- 'MONGO_OPTIONS={"appName":"print","maxPoolSize":1}'
- STORAGE_CONFIG=${STORAGE_CONFIG}
deploy:
resources:
@ -181,6 +185,7 @@ services:
environment:
- SECRET=secret
- MONGO_URL=mongodb://mongodb:27017
- 'MONGO_OPTIONS={"appName":"sign","maxPoolSize":1}'
- MINIO_ENDPOINT=minio
- MINIO_ACCESS_KEY=minioadmin
- ACCOUNTS_URL=http://account:3000
@ -201,6 +206,7 @@ services:
- SECRET=secret
- PORT=4007
- MONGO_URL=mongodb://mongodb:27017
- 'MONGO_OPTIONS={"appName":"analytics","maxPoolSize":1}'
- SERVICE_ID=analytics-collector-service
- ACCOUNTS_URL=http://account:3000
- SUPPORT_WORKSPACE=support

View File

@ -70,6 +70,7 @@ export class TSpace extends TDoc implements Space {
archived!: boolean
@Prop(ArrOf(TypeRef(core.class.Account)), core.string.Members)
@Index(IndexKind.Indexed)
members!: Arr<Ref<Account>>
@Prop(ArrOf(TypeRef(core.class.Account)), core.string.Owners)

View File

@ -39,6 +39,6 @@ export function createModel (builder: Builder): void {
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
domain: DOMAIN_PREFERENCE,
disabled: [{ modifiedOn: 1 }, { createdOn: 1 }]
disabled: [{ modifiedOn: 1 }, { createdOn: 1 }, { attachedTo: 1 }, { createdOn: -1 }, { modifiedBy: 1 }]
})
}

View File

@ -148,6 +148,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
domain: DOMAIN_TAGS,
disabled: [
{ _class: 1 },
{ modifiedOn: 1 },
{ modifiedBy: 1 },
{ createdBy: 1 },

View File

@ -1,7 +1,6 @@
// Basic performance metrics suite.
import { MetricsData } from '.'
import { cutObjectArray } from '../utils'
import { FullParamsType, Metrics, ParamsType } from './types'
/**
@ -35,7 +34,7 @@ function getUpdatedTopResult (
const newValue = {
value: time,
params: cutObjectArray(params)
params
}
if (result.length > 6) {

View File

@ -487,9 +487,13 @@ export class ApplyOperations extends TxOperations {
extraNotify
)
)) as Promise<TxApplyResult>)
const dnow = Date.now()
if (typeof window === 'object' && window !== null) {
console.log(`measure ${this.measureName}`, dnow - st, 'server time', result.serverTime)
}
return {
result: result.success,
time: Date.now() - st,
time: dnow - st,
serverTime: result.serverTime
}
}

View File

@ -13,8 +13,9 @@
// limitations under the License.
//
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
import { getEmbeddedLabel, IntlString, PlatformError, unknownError } from '@hcengineering/platform'
import { deepEqual } from 'fast-equals'
import { DOMAIN_BENCHMARK } from './benchmark'
import {
Account,
AccountRole,
@ -46,7 +47,6 @@ import { TxOperations } from './operations'
import { isPredicate } from './predicate'
import { DocumentQuery, FindResult } from './storage'
import { DOMAIN_TX } from './tx'
import { DOMAIN_BENCHMARK } from './benchmark'
function toHex (value: number, chars: number): string {
const result = value.toString(16)
@ -355,7 +355,6 @@ export class DocManager<T extends Doc> implements IDocManager<T> {
export class RateLimiter {
idCounter: number = 0
processingQueue = new Map<number, Promise<void>>()
last: number = 0
rate: number
@ -366,21 +365,21 @@ export class RateLimiter {
}
notify: (() => void)[] = []
finished: boolean = false
async exec<T, B extends Record<string, any> = any>(op: (args?: B) => Promise<T>, args?: B): Promise<T> {
const processingId = this.idCounter++
while (this.processingQueue.size >= this.rate) {
if (this.finished) {
throw new PlatformError(unknownError('No Possible to add/exec on finished queue'))
}
while (this.notify.length >= this.rate) {
await new Promise<void>((resolve) => {
this.notify.push(resolve)
})
}
try {
const p = op(args)
this.processingQueue.set(processingId, p as Promise<void>)
return await p
} finally {
this.processingQueue.delete(processingId)
const n = this.notify.shift()
if (n !== undefined) {
n()
@ -389,7 +388,7 @@ export class RateLimiter {
}
async add<T, B extends Record<string, any> = any>(op: (args?: B) => Promise<T>, args?: B): Promise<void> {
if (this.processingQueue.size < this.rate) {
if (this.notify.length < this.rate) {
void this.exec(op, args)
} else {
await this.exec(op, args)
@ -397,7 +396,12 @@ export class RateLimiter {
}
async waitProcessing (): Promise<void> {
await Promise.all(this.processingQueue.values())
this.finished = true
while (this.notify.length > 0) {
await new Promise<void>((resolve) => {
this.notify.push(resolve)
})
}
}
}

View File

@ -9,6 +9,7 @@ import {
type FindResult,
type Hierarchy,
type ModelDb,
type QuerySelector,
type Ref,
type SearchOptions,
type SearchQuery,
@ -330,6 +331,22 @@ export class OptimizeQueryMiddleware extends BasePresentationMiddleware implemen
const fQuery = { ...query }
const fOptions = { ...options }
this.optimizeQuery<T>(fQuery, fOptions)
// Immidiate response queries, if have some $in with empty list.
for (const [k, v] of Object.entries(fQuery)) {
if (typeof v === 'object' && v != null) {
const vobj = v as QuerySelector<any>
if (vobj.$in != null && vobj.$in.length === 0) {
// Emopty in, will always return []
return toFindResult([], 0)
} else if (vobj.$in != null && vobj.$in.length === 1 && Object.keys(vobj).length === 1) {
;(fQuery as any)[k] = vobj.$in[0]
} else if (vobj.$nin != null && vobj.$nin.length === 1 && Object.keys(vobj).length === 1) {
;(fQuery as any)[k] = { $ne: vobj.$nin[0] }
}
}
}
return await this.provideFindAll(_class, fQuery, fOptions)
}

View File

@ -28,6 +28,10 @@ export function tooltip (node: HTMLElement, options?: LabelAndProps): any {
if (options === undefined) {
return {}
}
if (options.label === undefined && options.component === undefined) {
// No tooltip
return {}
}
let opt = options
const show = (): void => {
const shown = !!(storedValue.label !== undefined || storedValue.component !== undefined)
@ -113,7 +117,7 @@ export function showTooltip (
props,
anchor,
onUpdate,
kind,
kind: kind ?? 'tooltip',
keys,
type: 'tooltip'
}

View File

@ -13,23 +13,23 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher, onDestroy } from 'svelte'
import { Attachment } from '@hcengineering/attachment'
import core, { Account, Class, Doc, generateId, Markup, Ref, Space, toIdMap, type Blob } from '@hcengineering/core'
import { Account, Class, Doc, generateId, Markup, Ref, Space, toIdMap, type Blob } from '@hcengineering/core'
import { IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform'
import {
createQuery,
DraftController,
deleteFile,
DraftController,
draftsStore,
getClient,
getFileMetadata,
uploadFile
} from '@hcengineering/presentation'
import textEditor, { type RefAction } from '@hcengineering/text-editor'
import { EmptyMarkup } from '@hcengineering/text'
import textEditor, { type RefAction } from '@hcengineering/text-editor'
import { AttachIcon, StyledTextBox } from '@hcengineering/text-editor-resources'
import { ButtonSize } from '@hcengineering/ui'
import { createEventDispatcher, onDestroy } from 'svelte'
import attachment from '../plugin'
import AttachmentsGrid from './AttachmentsGrid.svelte'

View File

@ -76,9 +76,13 @@ const providerSettingsQuery = createQuery(true)
const typeSettingsQuery = createQuery(true)
export function loadNotificationSettings (): void {
providerSettingsQuery.query(notification.class.NotificationProviderSetting, {}, (res) => {
providersSettings.set(res)
})
providerSettingsQuery.query(
notification.class.NotificationProviderSetting,
{ space: core.space.Workspace },
(res) => {
providersSettings.set(res)
}
)
typeSettingsQuery.query(notification.class.NotificationTypeSetting, {}, (res) => {
typesSettings.set(res)
})

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import type { Product, ProductVersion } from '@hcengineering/products'
import { FindOptions, SortingOrder } from '@hcengineering/core'
import core, { FindOptions, SortingOrder } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Label, Loading } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
@ -45,6 +45,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: viewlet._id
},
(res) => {

View File

@ -20,6 +20,7 @@
import { EmployeeBox, ExpandRightDouble, UserBox } from '@hcengineering/contact-resources'
import {
Account,
AccountRole,
Class,
Client,
Doc,
@ -28,10 +29,9 @@
Ref,
SortingOrder,
Space,
Status as TaskStatus,
fillDefaults,
generateId,
Status as TaskStatus,
AccountRole,
getCurrentAccount,
hasAccountRole
} from '@hcengineering/core'
@ -43,10 +43,10 @@
createQuery,
getClient
} from '@hcengineering/presentation'
import type { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
import { recruitId, type Applicant, type Candidate, type Vacancy } from '@hcengineering/recruit'
import task, { TaskType, getStates, makeRank } from '@hcengineering/task'
import { TaskKindSelector, selectedTypeStore, typeStore } from '@hcengineering/task-resources'
import { EmptyMarkup } from '@hcengineering/text'
import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
import ui, {
Button,
ColorPopup,
@ -132,8 +132,11 @@
if (candidateInstance === undefined) {
throw new Error('contact not found')
}
const ops = client.apply(generateId(), recruitId + '.Create.CreateApplication')
if (!client.getHierarchy().hasMixin(candidateInstance, recruit.mixin.Candidate)) {
await client.createMixin<Contact, Candidate>(
await ops.createMixin<Contact, Candidate>(
candidateInstance._id,
candidateInstance._class,
candidateInstance.space,
@ -144,7 +147,7 @@
const number = (incResult as any).object.sequence
await client.addCollection(
await ops.addCollection(
recruit.class.Applicant,
_space,
candidateInstance._id,
@ -166,11 +169,12 @@
await descriptionBox.createAttachments()
if (_comment.trim().length > 0) {
await client.addCollection(chunter.class.ChatMessage, _space, doc._id, recruit.class.Applicant, 'comments', {
if (_comment.trim().length > 0 && !isEmptyMarkup(_comment)) {
await ops.addCollection(chunter.class.ChatMessage, _space, doc._id, recruit.class.Applicant, 'comments', {
message: _comment
})
}
await ops.commit()
}
async function invokeValidate (

View File

@ -51,6 +51,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: viewlet._id
},
(res) => {

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import core, { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Component } from '@hcengineering/tracker'
import { Loading, Component as ViewComponent } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
@ -34,6 +34,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: viewlet._id
},
(res) => {

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import core, { Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue } from '@hcengineering/tracker'
import { Button, Chevron, ExpandCollapse, IconAdd, closeTooltip, resizeObserver, showPopup } from '@hcengineering/ui'
@ -77,6 +77,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: { $in: configurationRaw.map((it) => it._id) }
},
(res) => {

View File

@ -0,0 +1,225 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import core, { SortingOrder, toIdMap, type IdMap, type Ref, type StatusCategory } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import tracker, { type Issue, type Project } from '@hcengineering/tracker'
import {
createFocusManager,
deviceOptionsStore,
EditWithIcon,
FocusHandler,
Icon,
IconCheck,
IconSearch,
Label,
ListView,
resizeObserver,
showPanel,
Spinner,
type SelectPopupValueType
} from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { subIssueListProvider, type IssueRef } from '../../../utils'
import RelatedIssuePresenter from './RelatedIssuePresenter.svelte'
export let refs: IssueRef[]
export let placeholder: IntlString | undefined = undefined
export let placeholderParam: any | undefined = undefined
export let searchable: boolean = false
export let width: 'medium' | 'large' | 'full' = 'medium'
export let showShadow: boolean = true
export let embedded: boolean = false
export let componentLink: boolean = false
export let loading: boolean = false
export let currentProject: Project
let popupElement: HTMLDivElement | undefined = undefined
let search: string = ''
const dispatch = createEventDispatcher()
let selection = 0
let list: ListView
let selected: any
let subIssues: Issue[] = []
const query = createQuery()
query.query(
tracker.class.Issue,
{ _id: { $in: refs.map((it) => it._id) } },
(res) => {
subIssues = res
},
{
sort: { rank: SortingOrder.Ascending }
}
)
let categories: IdMap<StatusCategory> = new Map()
void getClient()
.findAll(core.class.StatusCategory, {})
.then((res) => {
categories = toIdMap(res)
})
$: value = subIssues.map((iss) => {
const c = $statusStore.byId.get(iss.status)?.category
const category = c !== undefined ? categories.get(c) : undefined
return {
id: iss._id,
isSelected: false,
component: RelatedIssuePresenter,
props: { project: currentProject, issue: iss },
category:
category !== undefined
? {
label: category.label,
icon: category.icon
}
: undefined
}
})
$: hasSelected = value.some((v) => v.isSelected)
function openIssue (target: Ref<Issue>): void {
subIssueListProvider(subIssues, target)
showPanel(tracker.component.EditIssue, target, tracker.class.Issue, 'content')
}
function sendSelect (id: SelectPopupValueType['id']): void {
selected = id
openIssue(id as Ref<Issue>)
}
export function onKeydown (key: KeyboardEvent): boolean {
if (key.code === 'Tab') {
dispatch('close')
key.preventDefault()
key.stopPropagation()
return true
}
if (key.code === 'ArrowUp') {
key.stopPropagation()
key.preventDefault()
list.select(selection - 1)
return true
}
if (key.code === 'ArrowDown') {
key.stopPropagation()
key.preventDefault()
list.select(selection + 1)
return true
}
if (key.code === 'Enter') {
key.preventDefault()
key.stopPropagation()
sendSelect(value[selection].id)
return true
}
return false
}
const manager = createFocusManager()
$: if (popupElement) {
popupElement.focus()
}
</script>
<FocusHandler {manager} />
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="selectPopup"
bind:this={popupElement}
tabindex="0"
class:noShadow={!showShadow}
class:full-width={width === 'full'}
class:max-width-40={width === 'large'}
class:embedded
use:resizeObserver={() => {
dispatch('changeContent')
}}
on:keydown={onKeydown}
>
{#if searchable}
<div class="header">
<EditWithIcon
icon={IconSearch}
size={'large'}
width={'100%'}
autoFocus={!$deviceOptionsStore.isMobile}
bind:value={search}
{placeholder}
{placeholderParam}
on:change
/>
</div>
{:else}
<div class="menu-space" />
{/if}
<div class="scroll">
<div class="box">
<ListView bind:this={list} count={value.length} bind:selection on:changeContent={() => dispatch('changeContent')}>
<svelte:fragment slot="item" let:item={itemId}>
{@const item = value[itemId]}
<button
class="menu-item withList w-full"
on:click={() => {
sendSelect(item.id)
}}
disabled={loading}
>
<div class="flex-row-center flex-grow" class:pointer-events-none={!componentLink}>
{#if item.component}
<div class="flex-grow clear-mins"><svelte:component this={item.component} {...item.props} /></div>
{/if}
{#if hasSelected}
<div class="check">
{#if item.isSelected}
<Icon icon={IconCheck} size={'small'} />
{/if}
</div>
{/if}
{#if item.id === selected && loading}
<Spinner size={'small'} />
{/if}
</div>
</button>
</svelte:fragment>
<svelte:fragment slot="category" let:item={row}>
{@const obj = value[row]}
{#if obj.category && ((row === 0 && obj.category.label !== undefined) || obj.category.label !== value[row - 1]?.category?.label)}
{#if row > 0}<div class="menu-separator" />{/if}
<div class="menu-group__header flex-row-center">
<span class="overflow-label">
<Label label={obj.category.label} />
</span>
</div>
{/if}
</svelte:fragment>
</ListView>
</div>
</div>
{#if !embedded}<div class="menu-space" />{/if}
</div>

View File

@ -13,18 +13,16 @@
// limitations under the License.
-->
<script lang="ts">
import core, { Doc, IdMap, Ref, SortingOrder, StatusCategory, WithLookup, toIdMap } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Doc, Ref, WithLookup, type Status } from '@hcengineering/core'
import task from '@hcengineering/task'
import { Issue, Project } from '@hcengineering/tracker'
import { Button, ButtonKind, ButtonSize, ProgressCircle, SelectPopup, showPanel } from '@hcengineering/ui'
import { Project, type Issue } from '@hcengineering/tracker'
import { Button, ButtonKind, ButtonSize, ProgressCircle } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import { listIssueStatusOrder, subIssueListProvider } from '../../../utils'
import RelatedIssuePresenter from './RelatedIssuePresenter.svelte'
import { listIssueStatusOrder, relatedIssues, type IssueRef } from '../../../utils'
import RelatedIssuePopup from './RelatedIssuePopup.svelte'
export let object: WithLookup<Doc & { related: number }> | undefined
export let value: WithLookup<Doc & { related: number }> | undefined
export let object: WithLookup<Doc> | undefined
export let value: WithLookup<Doc> | undefined
export let currentProject: Project | undefined
export let kind: ButtonKind = 'link-bordered'
@ -33,44 +31,13 @@
export let width: string | undefined = 'min-contet'
export let compactMode: boolean = false
let _subIssues: Issue[] = []
let subIssues: Issue[] = []
let countComplete: number = 0
const query = createQuery()
$: _object = object ?? value
$: _object != null && update(_object)
$: subIssues = sortStatuses(_object?._id !== undefined ? [...($relatedIssues.get(_object?._id) ?? [])] : [])
let countComplete: number = 0
function update (value: WithLookup<Doc & { related: number }>): void {
if (value.$lookup?.related !== undefined) {
query.unsubscribe()
_subIssues = value.$lookup.related as Issue[]
} else {
query.query(
tracker.class.Issue,
{ 'relations._id': value._id, 'relations._class': value._class },
(res) => {
_subIssues = res
},
{
sort: { rank: SortingOrder.Ascending }
}
)
}
}
let categories: IdMap<StatusCategory> = new Map()
void getClient()
.findAll(core.class.StatusCategory, {})
.then((res) => {
categories = toIdMap(res)
})
$: {
_subIssues.sort((a, b) => {
function sortStatuses (statuses: IssueRef[]): { _id: Ref<Issue>, status: Ref<Status> }[] {
statuses.sort((a, b) => {
const aStatus = $statusStore.byId.get(a.status)
const bStatus = $statusStore.byId.get(b.status)
return (
@ -78,39 +45,16 @@
listIssueStatusOrder.indexOf(bStatus?.category ?? task.statusCategory.UnStarted)
)
})
subIssues = _subIssues
return statuses
}
$: if (subIssues != null) {
$: if (subIssues.length > 0) {
const doneStatuses = $statusStore.array
.filter((s) => s.category === task.statusCategory.Won || s.category === task.statusCategory.Lost)
.map((p) => p._id)
countComplete = subIssues.filter((si) => doneStatuses.includes(si.status)).length
}
$: hasSubIssues = (subIssues?.length ?? 0) > 0
function openIssue (target: Ref<Issue>): void {
subIssueListProvider(subIssues, target)
showPanel(tracker.component.EditIssue, target, tracker.class.Issue, 'content')
}
$: selectValue = subIssues.map((iss) => {
const c = $statusStore.byId.get(iss.status)?.category
const category = c !== undefined ? categories.get(c) : undefined
return {
id: iss._id,
isSelected: false,
component: RelatedIssuePresenter,
props: { project: currentProject, issue: iss },
category:
category !== undefined
? {
label: category.label,
icon: category.icon
}
: undefined
}
})
$: hasSubIssues = subIssues.length > 0
</script>
{#if hasSubIssues}
@ -121,10 +65,10 @@
{size}
{justify}
showTooltip={{
component: SelectPopup,
component: RelatedIssuePopup,
props: {
value: selectValue,
onSelect: openIssue,
refs: subIssues,
currentProject,
showShadow: false,
width: 'large'
}

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import core, { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Milestone } from '@hcengineering/tracker'
import { Component, Loading } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
@ -22,6 +22,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: viewlet._id
},
(res) => {

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import core, { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { IssueTemplate } from '@hcengineering/tracker'
import { Component } from '@hcengineering/ui'
@ -19,6 +19,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: viewlet._id
},
(res) => {

View File

@ -28,6 +28,7 @@ import core, {
type DocumentUpdate,
type Ref,
type Space,
type Status,
type StatusCategory,
type TxCollectionCUD,
type TxCreateDoc,
@ -52,7 +53,7 @@ import {
import { PaletteColorIndexes, areDatesEqual, isWeekend } from '@hcengineering/ui'
import { type KeyFilter, type ViewletDescriptor } from '@hcengineering/view'
import { CategoryQuery, ListSelectionProvider, statusStore, type SelectDirection } from '@hcengineering/view-resources'
import { derived, get } from 'svelte/store'
import { derived, get, writable } from 'svelte/store'
import tracker from './plugin'
import { defaultMilestoneStatuses, defaultPriorities } from './types'
@ -570,3 +571,44 @@ export async function getMilestoneTitle (client: TxOperations, ref: Ref<Mileston
return object?.label ?? ''
}
export interface IssueRef {
status: Ref<Status>
_id: Ref<Issue>
}
export type IssueReverseRevMap = Map<Ref<Doc>, IssueRef[]>
export const relatedIssues = writable<IssueReverseRevMap>(new Map())
function fillStores (): void {
const client = getClient()
if (client !== undefined) {
const relatedIssuesQuery = createQuery(true)
relatedIssuesQuery.query(
tracker.class.Issue,
{ 'relations._id': { $exists: true } },
(res) => {
const nMap: IssueReverseRevMap = new Map()
for (const r of res) {
for (const rr of r.relations ?? []) {
nMap.set(rr._id, [...(nMap.get(rr._id) ?? []), { _id: r._id, status: r.status }])
}
}
relatedIssues.set(nMap)
},
{
projection: {
relations: 1,
status: 1
}
}
)
} else {
setTimeout(() => {
fillStores()
}, 50)
}
}
fillStores()

View File

@ -154,10 +154,31 @@
query,
(result) => {
total = result.total
if (totalQuery === undefined) {
gtotal = total
}
},
{ limit: 1, ...options, sort: getSort(_sortKey), lookup, total: true }
)
const totalQueryQ = createQuery()
$: if (totalQuery !== undefined) {
totalQueryQ.query(
_class,
totalQuery,
(result) => {
gtotal = result.total === -1 ? 0 : result.total
},
{
lookup,
limit: 1,
total: true
}
)
} else {
gtotal = total
}
const showContextMenu = async (ev: MouseEvent, object: Doc, row: number): Promise<void> => {
selection = row
if (!checkedSet.has(object._id)) {
@ -258,20 +279,6 @@
let width: number
const totalQueryQ = createQuery()
$: totalQueryQ.query(
_class,
totalQuery ?? query ?? {},
(result) => {
gtotal = result.total === -1 ? 0 : result.total
},
{
lookup,
limit: 1,
total: true
}
)
let isBuildingModel = true
let model: AttributeModel[] | undefined
let modelOptions: BuildModelOptions | undefined

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { Class, Doc, DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import core, { Class, Doc, DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { AnySvelteComponent, Component, Loading } from '@hcengineering/ui'
@ -44,6 +44,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: { $in: configurationRaw.map((it) => it._id) }
},
(res) => {

View File

@ -3,7 +3,7 @@
import { activeViewlet, makeViewletKey, setActiveViewletId } from '../utils'
import { resolvedLocationStore, Switcher } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
import { DocumentQuery, Ref, WithLookup } from '@hcengineering/core'
import core, { DocumentQuery, Ref, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
export let viewlet: WithLookup<Viewlet> | undefined
@ -69,6 +69,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: viewlet._id
},
(res) => {

View File

@ -55,6 +55,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: { $in: Array.from(viewlets.map((it) => it._id)) }
},
(res) => {

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { DocumentQuery, WithLookup } from '@hcengineering/core'
import core, { DocumentQuery, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { ModernButton, showPopup, closeTooltip } from '@hcengineering/ui'
import { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
@ -72,6 +72,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: viewlet._id
},
(res) => {

View File

@ -474,7 +474,10 @@ export function buildConfigLookup<T extends Doc> (
...((existingLookup as ReverseLookups)._id ?? {}),
...((res as ReverseLookups)._id ?? {})
}
res = { ...existingLookup, ...res, _id }
res = { ...existingLookup, ...res }
if (Object.keys(_id).length > 0) {
;(res as any)._id = _id
}
}
return res
}

View File

@ -42,17 +42,21 @@
$: if (model) {
const classes = getSpecialSpaceClass(model).flatMap((c) => hierarchy.getDescendants(c))
query.query(
core.class.Space,
{
_class: { $in: classes },
members: getCurrentAccount()._id
},
(result) => {
spaces = result
},
{ sort: { name: SortingOrder.Ascending } }
)
if (classes.length > 0) {
query.query(
core.class.Space,
{
_class: classes.length === 1 ? classes[0] : { $in: classes },
members: getCurrentAccount()._id
},
(result) => {
spaces = result
},
{ sort: { name: SortingOrder.Ascending } }
)
} else {
query.unsubscribe()
}
}
let specials: SpecialNavModel[] = []

View File

@ -47,22 +47,28 @@
const filteredViewsQuery = createQuery()
let availableFilteredViews: FilteredView[] = []
let myFilteredViews: FilteredView[] = []
$: filteredViewsQuery.query<FilteredView>(
view.class.FilteredView,
{ attachedTo: currentApplication?.alias },
(result) => {
myFilteredViews = result.filter((p) => p.users.includes(me))
availableFilteredViews = result.filter((p) => p.sharable && !p.users.includes(me))
$: if (currentApplication?.alias !== undefined) {
filteredViewsQuery.query<FilteredView>(
view.class.FilteredView,
{ attachedTo: currentApplication?.alias },
(result) => {
myFilteredViews = result.filter((p) => p.users.includes(me))
availableFilteredViews = result.filter((p) => p.sharable && !p.users.includes(me))
const location = getLocation()
if (location.query?.filterViewId) {
const targetView = result.find((view) => view._id === location.query?.filterViewId)
if (targetView) {
load(targetView)
const location = getLocation()
if (location.query?.filterViewId) {
const targetView = result.find((view) => view._id === location.query?.filterViewId)
if (targetView) {
load(targetView)
}
}
}
}
)
)
} else {
filteredViewsQuery.unsubscribe()
myFilteredViews = []
availableFilteredViews = []
}
async function removeAction (filteredView: FilteredView): Promise<Action[]> {
return [

View File

@ -15,6 +15,7 @@
-->
<script lang="ts">
import type { Class, Doc, Ref, Space, WithLookup } from '@hcengineering/core'
import core from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { AnyComponent, Component, Loading } from '@hcengineering/ui'
@ -42,6 +43,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: viewlet._id
},
(res) => {

View File

@ -57,9 +57,14 @@
const prevSpaceId = spaceId
$: query.query(core.class.Space, { _id: spaceId }, (result) => {
space = result[0]
})
$: query.query(
core.class.Space,
{ _id: spaceId },
(result) => {
space = result[0]
},
{ limit: 1 }
)
function showCreateDialog (ev: Event) {
showPopup(createItemDialog as AnyComponent, { space: spaceId }, 'top')

View File

@ -36,9 +36,14 @@
const clazz = client.getHierarchy().getClass(_class)
const query = createQuery()
$: query.query(core.class.Space, { _id }, (result) => {
space = result[0]
})
$: query.query(
core.class.Space,
{ _id },
(result) => {
space = result[0]
},
{ limit: 1 }
)
function onNameChange (ev: Event) {
const value = (ev.target as HTMLInputElement).value
@ -46,9 +51,14 @@
client.updateDoc(_class, space.space, space._id, { name: value })
} else {
// Just refresh value
query.query(core.class.Space, { _id }, (result) => {
space = result[0]
})
query.query(
core.class.Space,
{ _id },
(result) => {
space = result[0]
},
{ limit: 1 }
)
}
}
</script>

View File

@ -9,7 +9,10 @@
export let level = 0
export let name: string = 'System'
$: haschilds = Object.keys(metrics.measurements).length > 0 || Object.keys(metrics.params).length > 0
$: haschilds =
Object.keys(metrics.measurements).length > 0 ||
Object.keys(metrics.params).length > 0 ||
(metrics.topResult?.length ?? 0) > 0
function showAvg (name: string, time: number, ops: number): string {
if (name.startsWith('#')) {
@ -76,13 +79,13 @@
{#each metrics.topResult ?? [] as r}
<Expandable>
<svelte:fragment slot="title">
<div class="flex-row-center flex-between flex-grow">
<div class="flex-row-center flex-between flex-grow select-text">
Time:{r.value}
</div>
</svelte:fragment>
<pre>
{JSON.stringify(r, null, 2)}
</pre>
<pre class="select-text">
{JSON.stringify(r, null, 2)}
</pre>
</Expandable>
{/each}
</Expandable>
@ -127,9 +130,9 @@
Time:{r.value}
</div>
</svelte:fragment>
<pre>
{JSON.stringify(r, null, 2)}
</pre>
<pre class="select-text">
{JSON.stringify(r, null, 2)}
</pre>
</Expandable>
{/each}
</div>

View File

@ -17,6 +17,7 @@
"bundle": "mkdir -p bundle && esbuild src/__start.ts --bundle --define:process.env.MODEL_VERSION=$(node ../../common/scripts/show_version.js) --minify --platform=node > bundle/bundle.js",
"docker:build": "../../common/scripts/docker_build.sh hardcoreeng/account",
"docker:tbuild": "docker build -t hardcoreeng/account . --platform=linux/amd64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/account",
"docker:abuild": "docker build -t hardcoreeng/account . --platform=linux/arm64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/account",
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/account staging",
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/account",
"run-local": "cross-env MONGO_URL=mongodb://localhost:27017 MINIO_ACCESS_KEY=minioadmi MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost SERVER_SECRET='secret' TRANSACTOR_URL=ws://localhost:3333 ts-node src/__start.ts",

View File

@ -15,6 +15,8 @@
"_phase:docker-staging": "rushx docker:staging",
"bundle": "mkdir -p bundle && esbuild src/__start.ts --bundle --platform=node --keep-names > bundle/bundle.js",
"docker:build": "../../common/scripts/docker_build.sh hardcoreeng/collaborator",
"docker:tbuild": "docker build -t hardcoreeng/collaborator . --platform=linux/amd64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/collaborator",
"docker:abuild": "docker build -t hardcoreeng/collaborator . --platform=linux/arm64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/collaborator",
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/collaborator staging",
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/collaborator",
"run-local": "cross-env MONGO_URL=mongodb://localhost:27017 SECRET=secret MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin ts-node src/__start.ts",

View File

@ -20,6 +20,7 @@
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/front staging",
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/front",
"docker:tbuild": "docker build -t hardcoreeng/front . --platform=linux/amd64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/front",
"docker:abuild": "docker build -t hardcoreeng/front . --platform=linux/arm64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/front",
"format": "format src",
"run-local": "cross-env MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost SERVER_SECRET='secret' ACCOUNTS_URL=http://localhost:3000 UPLOAD_URL=/files ELASTIC_URL=http://localhost:9200 MODEL_VERSION=$(node ../../common/scripts/show_version.js) VERSION=$(node ../../common/scripts/show_tag.js) PUBLIC_DIR='.' ts-node ./src/__start.ts",
"test": "jest --passWithNoTests --silent --forceExit",

View File

@ -8,9 +8,9 @@
"template": "@hcengineering/node-package",
"license": "EPL-2.0",
"scripts": {
"start": "rush bundle --to @hcengineering/server && cross-env NODE_ENV=production ELASTIC_INDEX_NAME=local_storage_index MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret node --inspect --enable-source-maps bundle/bundle.js",
"start-u": "rush bundle --to @hcengineering/server && cp ./node_modules/@hcengineering/uws/lib/*.node ./bundle/ && cross-env NODE_ENV=production SERVER_PROVIDER=uweb ELASTIC_INDEX_NAME=local_storage_index MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret node --inspect bundle/bundle.js",
"start-flame": "rush bundle --to @hcengineering/server && cross-env NODE_ENV=production ELASTIC_INDEX_NAME=local_storage_index MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret clinic flame --dest ./out -- node --nolazy -r ts-node/register --enable-source-maps src/__start.ts",
"start": "rush bundle --to @hcengineering/pod-server && cross-env NODE_ENV=production ELASTIC_INDEX_NAME=local_storage_index MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret node --inspect --enable-source-maps bundle/bundle.js",
"start-u": "rush bundle --to @hcengineering/pod-server && cp ./node_modules/@hcengineering/uws/lib/*.node ./bundle/ && cross-env NODE_ENV=production SERVER_PROVIDER=uweb ELASTIC_INDEX_NAME=local_storage_index MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret node --inspect bundle/bundle.js",
"start-flame": "rush bundle --to @hcengineering/pod-server && cross-env NODE_ENV=production ELASTIC_INDEX_NAME=local_storage_index MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret clinic flame --dest ./out -- node --nolazy -r ts-node/register --enable-source-maps src/__start.ts",
"build": "compile",
"_phase:bundle": "rushx bundle",
"_phase:docker-build": "rushx docker:build",
@ -18,6 +18,7 @@
"bundle": "mkdir -p bundle && esbuild src/__start.ts --minify --bundle --keep-names --platform=node --external:*.node --external:bufferutil --external:utf-8-validate --define:process.env.MODEL_VERSION=$(node ../../common/scripts/show_version.js) --define:process.env.GIT_REVISION=$(../../common/scripts/git_version.sh) --outfile=bundle/bundle.js --log-level=error --sourcemap=external",
"docker:build": "../../common/scripts/docker_build.sh hardcoreeng/transactor",
"docker:tbuild": "docker build -t hardcoreeng/transactor . --platform=linux/amd64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/transactor",
"docker:abuild": "docker build -t hardcoreeng/transactor . --platform=linux/arm64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/transactor",
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/transactor staging",
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/transactor",
"build:watch": "compile",

View File

@ -136,6 +136,7 @@ export async function createReactionNotifications (
res = res.concat(
await createCollabDocInfo(
control.ctx,
[user] as Ref<PersonAccount>[],
control,
tx.tx,
@ -389,7 +390,7 @@ async function ActivityMessagesHandler (tx: TxCUD<Doc>, control: TriggerControl)
const messages = txes.map((messageTx) => TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc<DocUpdateMessage>))
const notificationTxes = await control.ctx.with(
'createNotificationTxes',
'createCollaboratorNotifications',
{},
async (ctx) => await createCollaboratorNotifications(ctx, tx, control, messages)
)

View File

@ -248,7 +248,7 @@ async function checkSpace (
control: TriggerControl,
res: Tx[]
): Promise<boolean> {
const space = (await control.findAll<Space>(core.class.Space, { _id: spaceId }))[0]
const space = (await control.findAll<Space>(core.class.Space, { _id: spaceId }, { limit: 1 }))[0]
const isMember = space.members.includes(user._id)
if (space.private) {
return isMember

View File

@ -166,7 +166,10 @@ async function getCollaboratorsDiff (
prevValue = hierarchy.as(prevDoc, notification.mixin.Collaborators).collaborators ?? []
} else if (prevDoc !== undefined) {
const mixin = hierarchy.classHierarchyMixin(prevDoc._class, notification.mixin.ClassCollaborators)
prevValue = mixin !== undefined ? await getDocCollaborators(prevDoc, mixin, control as TriggerControl) : []
prevValue =
mixin !== undefined
? await getDocCollaborators((control as TriggerControl).ctx, prevDoc, mixin, control as TriggerControl)
: []
}
const added = value.filter((item) => !prevValue.includes(item)) as DocAttributeUpdates['added']

View File

@ -201,7 +201,7 @@ async function OnChatMessageCreated (tx: TxCUD<Doc>, control: TriggerControl): P
)
}
} else {
const collaborators = await getDocCollaborators(targetDoc, mixin, control)
const collaborators = await getDocCollaborators(control.ctx, targetDoc, mixin, control)
if (!collaborators.includes(message.modifiedBy)) {
collaborators.push(message.modifiedBy)
}
@ -542,12 +542,11 @@ async function hideOldChannels (
return res
}
async function updateChatInfo (control: TriggerControl, status: UserStatus, date: Timestamp): Promise<void> {
export async function updateChatInfo (control: TriggerControl, status: UserStatus, date: Timestamp): Promise<void> {
const account = await control.modelDb.findOne(contact.class.PersonAccount, { _id: status.user as Ref<PersonAccount> })
if (account === undefined) return
const chatUpdates = await control.queryFind(chunter.class.ChatInfo, {})
const update = chatUpdates.find(({ user }) => user === account.person)
const update = (await control.findAll(chunter.class.ChatInfo, { user: account.person })).shift()
const shouldUpdate = update === undefined || date - update.timestamp > updateChatInfoDelay
if (!shouldUpdate) return
@ -608,23 +607,23 @@ async function updateChatInfo (control: TriggerControl, status: UserStatus, date
}
async function OnUserStatus (originTx: TxCUD<UserStatus>, control: TriggerControl): Promise<Tx[]> {
const tx = TxProcessor.extractTx(originTx) as TxCUD<UserStatus>
if (tx.objectClass !== core.class.UserStatus) return []
if (tx._class === core.class.TxCreateDoc) {
const createTx = tx as TxCreateDoc<UserStatus>
const { online } = createTx.attributes
if (online) {
const status = TxProcessor.createDoc2Doc(createTx)
await updateChatInfo(control, status, originTx.modifiedOn)
}
} else if (tx._class === core.class.TxUpdateDoc) {
const updateTx = tx as TxUpdateDoc<UserStatus>
const { online } = updateTx.operations
if (online === true) {
const status = (await control.findAll(core.class.UserStatus, { _id: updateTx.objectId }))[0]
await updateChatInfo(control, status, originTx.modifiedOn)
}
}
// const tx = TxProcessor.extractTx(originTx) as TxCUD<UserStatus>
// if (tx.objectClass !== core.class.UserStatus) return []
// if (tx._class === core.class.TxCreateDoc) {
// const createTx = tx as TxCreateDoc<UserStatus>
// const { online } = createTx.attributes
// if (online) {
// const status = TxProcessor.createDoc2Doc(createTx)
// await updateChatInfo(control, status, originTx.modifiedOn)
// }
// } else if (tx._class === core.class.TxUpdateDoc) {
// const updateTx = tx as TxUpdateDoc<UserStatus>
// const { online } = updateTx.operations
// if (online === true) {
// const status = (await control.findAll(core.class.UserStatus, { _id: updateTx.objectId }))[0]
// await updateChatInfo(control, status, originTx.modifiedOn)
// }
// }
return []
}
@ -633,19 +632,17 @@ async function OnContextUpdate (tx: TxUpdateDoc<DocNotifyContext>, control: Trig
const hasUpdate = 'lastUpdateTimestamp' in tx.operations && tx.operations.lastUpdateTimestamp !== undefined
if (!hasUpdate) return []
const chatUpdates = await control.queryFind(chunter.class.ChatInfo, {})
for (const update of chatUpdates) {
if (update.hidden.includes(tx.objectId)) {
return [
control.txFactory.createTxMixin(tx.objectId, tx.objectClass, tx.objectSpace, chunter.mixin.ChannelInfo, {
hidden: false
}),
control.txFactory.createTxUpdateDoc(update._class, update.space, update._id, {
hidden: update.hidden.filter((id) => id !== tx.objectId)
})
]
}
}
// const update = (await control.findAll(notification.class.DocNotifyContext, { _id: tx.objectId }, { limit: 1 })).shift()
// if (update !== undefined) {
// const as = control.hierarchy.as(update, chunter.mixin.ChannelInfo)
// if (as.hidden) {
// return [
// control.txFactory.createTxMixin(tx.objectId, tx.objectClass, tx.objectSpace, chunter.mixin.ChannelInfo, {
// hidden: false
// })
// ]
// }
// }
return []
}

View File

@ -284,6 +284,7 @@ async function getKeyCollaborators (
* @public
*/
export async function getDocCollaborators (
ctx: MeasureContext,
doc: Doc,
mixin: ClassCollaborators,
control: TriggerControl
@ -291,7 +292,11 @@ export async function getDocCollaborators (
const collaborators = new Set<Ref<Account>>()
for (const field of mixin.fields) {
const value = (doc as any)[field]
const newCollaborators = await getKeyCollaborators(doc, value, field, control)
const newCollaborators = await ctx.with(
'getKeyCollaborators',
{},
async () => await getKeyCollaborators(doc, value, field, control)
)
if (newCollaborators !== undefined) {
for (const newCollaborator of newCollaborators) {
collaborators.add(newCollaborator)
@ -544,9 +549,7 @@ export async function createPushNotification (
const privateKey = getMetadata(serverNotification.metadata.PushPrivateKey)
const subject = getMetadata(serverNotification.metadata.PushSubject) ?? 'mailto:hey@huly.io'
if (privateKey === undefined || publicKey === undefined) return
const subscriptions = (await control.queryFind(notification.class.PushSubscription, {})).filter(
(p) => p.user === target
)
const subscriptions = await control.findAll(notification.class.PushSubscription, { user: target })
const data: PushData = {
title,
body
@ -784,6 +787,7 @@ async function updateContextsTimestamp (
}
export async function createCollabDocInfo (
ctx: MeasureContext,
collaborators: Ref<PersonAccount>[],
control: TriggerControl,
tx: TxCUD<Doc>,
@ -799,7 +803,7 @@ export async function createCollabDocInfo (
return res
}
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: object._id })
const notifyContexts = await control.findAllCtx(ctx, notification.class.DocNotifyContext, { attachedTo: object._id })
await updateContextsTimestamp(notifyContexts, originTx.modifiedOn, control, originTx.modifiedBy)
@ -822,7 +826,11 @@ export async function createCollabDocInfo (
return res
}
const usersInfo = await getUsersInfo([...Array.from(targets), originTx.modifiedBy as Ref<PersonAccount>], control)
const usersInfo = await ctx.with(
'get-user-info',
{},
async (ctx) => await getUsersInfo(ctx, [...Array.from(targets), originTx.modifiedBy as Ref<PersonAccount>], control)
)
const sender = usersInfo.find(({ _id }) => _id === originTx.modifiedBy) ?? {
_id: originTx.modifiedBy
}
@ -888,7 +896,7 @@ async function getSpaceCollabTxes (
return []
}
const space = cache.get(doc.space) ?? (await control.findAll(core.class.Space, { _id: doc.space }))[0]
const space = cache.get(doc.space) ?? (await control.findAll(core.class.Space, { _id: doc.space }, { limit: 1 }))[0]
if (space === undefined) return []
cache.set(space._id, space)
@ -901,6 +909,7 @@ async function getSpaceCollabTxes (
const collabs = control.hierarchy.as<Doc, Collaborators>(space, notification.mixin.Collaborators)
if (collabs.collaborators !== undefined) {
return await createCollabDocInfo(
control.ctx,
collabs.collaborators as Ref<PersonAccount>[],
control,
tx,
@ -916,6 +925,7 @@ async function getSpaceCollabTxes (
}
async function createCollaboratorDoc (
ctx: MeasureContext,
tx: TxCreateDoc<Doc>,
control: TriggerControl,
activityMessage: ActivityMessage[],
@ -931,28 +941,45 @@ async function createCollaboratorDoc (
}
const doc = TxProcessor.createDoc2Doc(tx)
const collaborators = await getDocCollaborators(doc, mixin, control)
const collaborators = await ctx.with(
'get-collaborators',
{},
async (ctx) => await getDocCollaborators(ctx, doc, mixin, control)
)
const mixinTx = getMixinTx(tx, control, collaborators)
const notificationTxes = await createCollabDocInfo(
collaborators as Ref<PersonAccount>[],
control,
tx,
originTx,
doc,
activityMessage,
{ isOwn: true, isSpace: false, shouldUpdateTimestamp: true },
cache
const notificationTxes = await ctx.with(
'create-collabdocinfo',
{},
async () =>
await createCollabDocInfo(
ctx,
collaborators as Ref<PersonAccount>[],
control,
tx,
originTx,
doc,
activityMessage,
{ isOwn: true, isSpace: false, shouldUpdateTimestamp: true },
cache
)
)
res.push(mixinTx)
res.push(...notificationTxes)
res.push(...(await getSpaceCollabTxes(control, doc, tx, originTx, activityMessage, cache)))
res.push(
...(await ctx.with(
'get-space-collabtxes',
{},
async () => await getSpaceCollabTxes(control, doc, tx, originTx, activityMessage, cache)
))
)
return res
}
async function updateCollaboratorsMixin (
ctx: MeasureContext,
tx: TxMixin<Doc, Collaborators>,
control: TriggerControl,
activityMessages: ActivityMessage[],
@ -969,17 +996,17 @@ async function updateCollaboratorsMixin (
if (tx.attributes.collaborators !== undefined) {
const createTx = hierarchy.isDerived(tx.objectClass, core.class.AttachedDoc)
? (
await control.findAll(core.class.TxCollectionCUD, {
await control.findAllCtx(ctx, core.class.TxCollectionCUD, {
'tx.objectId': tx.objectId,
'tx._class': core.class.TxCreateDoc
})
)[0]
: (
await control.findAll(core.class.TxCreateDoc, {
await control.findAllCtx(ctx, core.class.TxCreateDoc, {
objectId: tx.objectId
})
)[0]
const mixinTxes = await control.findAll(core.class.TxMixin, {
const mixinTxes = await control.findAllCtx(ctx, core.class.TxMixin, {
objectId: tx.objectId
})
const prevDoc = TxProcessor.buildDoc2Doc([createTx, ...mixinTxes].filter((t) => t._id !== tx._id)) as Doc
@ -992,7 +1019,7 @@ async function updateCollaboratorsMixin (
prevCollabs = new Set(prevDocMixin.collaborators ?? [])
} else {
const mixin = hierarchy.classHierarchyMixin(prevDoc._class, notification.mixin.ClassCollaborators)
prevCollabs = mixin !== undefined ? new Set(await getDocCollaborators(prevDoc, mixin, control)) : new Set()
prevCollabs = mixin !== undefined ? new Set(await getDocCollaborators(ctx, prevDoc, mixin, control)) : new Set()
}
const type = await control.modelDb.findOne(notification.class.BaseNotificationType, {
@ -1017,12 +1044,16 @@ async function updateCollaboratorsMixin (
}
if (newCollabs.length > 0) {
const docNotifyContexts = await control.findAll(notification.class.DocNotifyContext, {
const docNotifyContexts = await control.findAllCtx(ctx, notification.class.DocNotifyContext, {
user: { $in: newCollabs },
attachedTo: tx.objectId
})
const infos = await getUsersInfo([...newCollabs, originTx.modifiedBy] as Ref<PersonAccount>[], control)
const infos = await ctx.with(
'get-user-info',
{},
async (ctx) => await getUsersInfo(ctx, [...newCollabs, originTx.modifiedBy] as Ref<PersonAccount>[], control)
)
const sender = infos.find(({ _id }) => _id === originTx.modifiedBy) ?? { _id: originTx.modifiedBy }
for (const collab of newCollabs) {
@ -1050,13 +1081,14 @@ async function updateCollaboratorsMixin (
}
async function collectionCollabDoc (
ctx: MeasureContext,
tx: TxCollectionCUD<Doc, AttachedDoc>,
control: TriggerControl,
activityMessages: ActivityMessage[],
cache: Map<Ref<Doc>, Doc>
): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<Doc>
let res = await createCollaboratorNotifications(control.ctx, actualTx, control, activityMessages, tx, cache)
let res = await createCollaboratorNotifications(ctx, actualTx, control, activityMessages, tx, cache)
if (![core.class.TxCreateDoc, core.class.TxRemoveDoc, core.class.TxUpdateDoc].includes(actualTx._class)) {
return res
@ -1068,7 +1100,12 @@ async function collectionCollabDoc (
return res
}
const doc = cache.get(tx.objectId) ?? (await control.findAll(tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
const doc = await ctx.with(
'get-doc',
{},
async (ctx) =>
cache.get(tx.objectId) ?? (await control.findAllCtx(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
)
if (doc === undefined) {
return res
@ -1076,18 +1113,28 @@ async function collectionCollabDoc (
cache.set(doc._id, doc)
const collaborators = await getCollaborators(doc, control, tx, res)
const collaborators = await ctx.with(
'get-collaborators',
{},
async () => await getCollaborators(doc, control, tx, res)
)
res = res.concat(
await createCollabDocInfo(
collaborators as Ref<PersonAccount>[],
control,
actualTx,
tx,
doc,
activityMessages,
{ isOwn: false, isSpace: false, shouldUpdateTimestamp: true },
cache
await ctx.with(
'create-collab-doc-info',
{},
async (ctx) =>
await createCollabDocInfo(
ctx,
collaborators as Ref<PersonAccount>[],
control,
actualTx,
tx,
doc,
activityMessages,
{ isOwn: false, isSpace: false, shouldUpdateTimestamp: true },
cache
)
)
)
@ -1184,6 +1231,7 @@ async function getNewCollaborators (
}
async function updateCollaboratorDoc (
ctx: MeasureContext,
tx: TxUpdateDoc<Doc> | TxMixin<Doc, Doc>,
control: TriggerControl,
originTx: TxCUD<Doc>,
@ -1194,7 +1242,11 @@ async function updateCollaboratorDoc (
let res: Tx[] = []
const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators)
if (mixin === undefined) return []
const doc = (await control.findAll(tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
const doc = await ctx.with(
'find-doc',
{ _class: tx.objectClass },
async () => (await control.findAllCtx(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
)
if (doc === undefined) return []
const params: NotifyParams = { isOwn: true, isSpace: false, shouldUpdateTimestamp: true }
if (hierarchy.hasMixin(doc, notification.mixin.Collaborators)) {
@ -1202,7 +1254,9 @@ async function updateCollaboratorDoc (
const collabMixin = hierarchy.as(doc, notification.mixin.Collaborators)
const collabs = new Set(collabMixin.collaborators)
const ops = isMixinTx(tx) ? tx.attributes : tx.operations
const newCollaborators = (await getNewCollaborators(ops, mixin, doc, control)).filter((p) => !collabs.has(p))
const newCollaborators = await ctx.with('get-new-collaborators', {}, async () =>
(await getNewCollaborators(ops, mixin, doc, control)).filter((p) => !collabs.has(p))
)
if (newCollaborators.length > 0) {
res.push(
@ -1217,22 +1271,33 @@ async function updateCollaboratorDoc (
)
}
res = res.concat(
await createCollabDocInfo(
[...collabMixin.collaborators, ...newCollaborators] as Ref<PersonAccount>[],
control,
tx,
originTx,
doc,
activityMessages,
params,
cache
await ctx.with(
'create-collab-docinfo',
{},
async () =>
await createCollabDocInfo(
ctx,
[...collabMixin.collaborators, ...newCollaborators] as Ref<PersonAccount>[],
control,
tx,
originTx,
doc,
activityMessages,
params,
cache
)
)
)
} else {
const collaborators = await getDocCollaborators(doc, mixin, control)
const collaborators = await ctx.with(
'get-doc-collaborators',
{},
async () => await getDocCollaborators(ctx, doc, mixin, control)
)
res.push(getMixinTx(tx, control, collaborators))
res = res.concat(
await createCollabDocInfo(
ctx,
collaborators as Ref<PersonAccount>[],
control,
tx,
@ -1245,8 +1310,16 @@ async function updateCollaboratorDoc (
)
}
res = res.concat(await getSpaceCollabTxes(control, doc, tx, originTx, activityMessages, cache))
res = res.concat(await updateNotifyContextsSpace(control, tx))
res = res.concat(
await ctx.with(
'get-space-collabtxes',
{},
async () => await getSpaceCollabTxes(control, doc, tx, originTx, activityMessages, cache)
)
)
res = res.concat(
await ctx.with('update-notify-context-space', {}, async () => await updateNotifyContextsSpace(control, tx))
)
return res
}
@ -1305,6 +1378,7 @@ export async function OnAttributeUpdate (tx: Tx, control: TriggerControl): Promi
}
async function applyUserTxes (
ctx: MeasureContext,
control: TriggerControl,
txes: Tx[],
cache: Map<Ref<Doc>, Doc> = new Map<Ref<Doc>, Doc>()
@ -1342,7 +1416,9 @@ async function applyUserTxes (
}
for (const [user, txs] of map.entries()) {
const account = (cache.get(user) as PersonAccount) ?? (await getPersonAccountById(user, control))
const account =
(cache.get(user) as PersonAccount) ??
(await ctx.with('get-person-account', {}, async () => await getPersonAccountById(user, control)))
if (account !== undefined) {
cache.set(account._id, account)
@ -1378,21 +1454,47 @@ export async function createCollaboratorNotifications (
switch (tx._class) {
case core.class.TxCreateDoc: {
const res = await createCollaboratorDoc(tx as TxCreateDoc<Doc>, control, activityMessages, originTx ?? tx, cache)
const res = await ctx.with(
'createCollaboratorDoc',
{},
async () =>
await createCollaboratorDoc(ctx, tx as TxCreateDoc<Doc>, control, activityMessages, originTx ?? tx, cache)
)
return await applyUserTxes(control, res)
return await applyUserTxes(ctx, control, res)
}
case core.class.TxUpdateDoc:
case core.class.TxMixin: {
let res = await updateCollaboratorDoc(tx as TxUpdateDoc<Doc>, control, originTx ?? tx, activityMessages, cache)
res = res.concat(
await updateCollaboratorsMixin(tx as TxMixin<Doc, Collaborators>, control, activityMessages, originTx ?? tx)
let res = await ctx.with(
'updateCollaboratorDoc',
{},
async () =>
await updateCollaboratorDoc(ctx, tx as TxUpdateDoc<Doc>, control, originTx ?? tx, activityMessages, cache)
)
return await applyUserTxes(control, res)
res = res.concat(
await ctx.with(
'updateCollaboratorMixin',
{},
async () =>
await updateCollaboratorsMixin(
ctx,
tx as TxMixin<Doc, Collaborators>,
control,
activityMessages,
originTx ?? tx
)
)
)
return await applyUserTxes(ctx, control, res)
}
case core.class.TxCollectionCUD: {
const res = await collectionCollabDoc(tx as TxCollectionCUD<Doc, AttachedDoc>, control, activityMessages, cache)
return await applyUserTxes(control, res)
const res = await ctx.with(
'collectionCollabDoc',
{},
async (ctx) =>
await collectionCollabDoc(ctx, tx as TxCollectionCUD<Doc, AttachedDoc>, control, activityMessages, cache)
)
return await applyUserTxes(ctx, control, res)
}
}
@ -1481,7 +1583,7 @@ export async function getCollaborators (
if (control.hierarchy.hasMixin(doc, notification.mixin.Collaborators)) {
return control.hierarchy.as(doc, notification.mixin.Collaborators).collaborators
} else {
const collaborators = await getDocCollaborators(doc, mixin, control)
const collaborators = await getDocCollaborators(control.ctx, doc, mixin, control)
res.push(getMixinTx(tx, control, collaborators))
return collaborators

View File

@ -12,14 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import notification, {
BaseNotificationType,
CommonNotificationType,
NotificationContent,
NotificationProvider,
NotificationType
} from '@hcengineering/notification'
import type { TriggerControl } from '@hcengineering/server-core'
import { DocUpdateMessage } from '@hcengineering/activity'
import { Analytics } from '@hcengineering/analytics'
import contact, { formatName, PersonAccount } from '@hcengineering/contact'
import core, {
Account,
Class,
@ -29,14 +24,25 @@ import core, {
matchQuery,
MixinUpdate,
Ref,
toIdMap,
Tx,
TxCreateDoc,
TxCUD,
TxMixin,
TxProcessor,
TxRemoveDoc,
TxUpdateDoc
TxUpdateDoc,
type MeasureContext
} from '@hcengineering/core'
import notification, {
BaseNotificationType,
CommonNotificationType,
NotificationContent,
NotificationProvider,
NotificationType
} from '@hcengineering/notification'
import { getResource, IntlString, translate } from '@hcengineering/platform'
import type { TriggerControl } from '@hcengineering/server-core'
import serverNotification, {
getPersonAccountById,
HTMLPresenter,
@ -44,10 +50,6 @@ import serverNotification, {
TextPresenter,
UserInfo
} from '@hcengineering/server-notification'
import { getResource, IntlString, translate } from '@hcengineering/platform'
import contact, { formatName, PersonAccount } from '@hcengineering/contact'
import { DocUpdateMessage } from '@hcengineering/activity'
import { Analytics } from '@hcengineering/analytics'
import { NotifyResult } from './types'
@ -134,7 +136,9 @@ export async function isAllowed (
type: BaseNotificationType,
provider: NotificationProvider
): Promise<boolean> {
const providersSettings = await control.queryFind(notification.class.NotificationProviderSetting, {})
const providersSettings = await control.queryFind(notification.class.NotificationProviderSetting, {
space: core.space.Workspace
})
const providerSetting = providersSettings.find(
({ attachedTo, modifiedBy }) => attachedTo === provider._id && modifiedBy === receiver
)
@ -446,13 +450,23 @@ export async function getNotificationContent (
return content
}
export async function getUsersInfo (ids: Ref<PersonAccount>[], control: TriggerControl): Promise<UserInfo[]> {
export async function getUsersInfo (
ctx: MeasureContext,
ids: Ref<PersonAccount>[],
control: TriggerControl
): Promise<UserInfo[]> {
const accounts = await control.modelDb.findAll(contact.class.PersonAccount, { _id: { $in: ids } })
const persons = await control.queryFind(contact.class.Person, {})
const persons = toIdMap(
await ctx.with(
'query-find',
{},
async () => await control.findAll(contact.class.Person, { _id: { $in: accounts.map((it) => it.person) } })
)
)
return accounts.map((account) => ({
_id: account._id,
account,
person: persons.find(({ _id }) => _id === account.person)
person: persons.get(account.person)
}))
}

View File

@ -146,7 +146,7 @@ async function getRequestNotificationTx (tx: TxCollectionCUD<Doc, Request>, cont
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, {
attachedTo: doc._id
})
const usersInfo = await getUsersInfo([...collaborators, tx.modifiedBy] as Ref<PersonAccount>[], control)
const usersInfo = await getUsersInfo(control.ctx, [...collaborators, tx.modifiedBy] as Ref<PersonAccount>[], control)
const senderInfo = usersInfo.find(({ _id }) => _id === tx.modifiedBy) ?? {
_id: tx.modifiedBy
}

View File

@ -27,7 +27,10 @@ export async function OnCustomAttributeRemove (tx: Tx, control: TriggerControl):
const txes = await control.findAll<TxCUD<AnyAttribute>>(core.class.TxCUD, { objectId: ptx.objectId })
const attribute = TxProcessor.buildDoc2Doc<AnyAttribute>(txes)
if (attribute === undefined) return []
const preferences = await control.findAll(view.class.ViewletPreference, { config: attribute.name })
const preferences = await control.findAll(view.class.ViewletPreference, {
config: attribute.name,
space: core.space.Workspace
})
const res: Tx[] = []
for (const preference of preferences) {
const tx = control.txFactory.createTxUpdateDoc(preference._class, preference.space, preference._id, {

View File

@ -128,11 +128,10 @@ export interface DbAdapterOptions {
abstract class MongoAdapterBase implements DbAdapter {
_db: DBCollectionHelper
findRateLimit = new RateLimiter(parseInt(process.env.FIND_RLIMIT ?? '10'))
rateLimit = new RateLimiter(parseInt(process.env.TX_RLIMIT ?? '1'))
findRateLimit = new RateLimiter(parseInt(process.env.FIND_RLIMIT ?? '1000'))
rateLimit = new RateLimiter(parseInt(process.env.TX_RLIMIT ?? '5'))
constructor (
readonly globalCtx: MeasureContext,
protected readonly db: Db,
protected readonly hierarchy: Hierarchy,
protected readonly modelDb: ModelDb,
@ -216,14 +215,14 @@ abstract class MongoAdapterBase implements DbAdapter {
}
const baseClass = this.hierarchy.getBaseClass(clazz)
if (baseClass !== core.class.Doc) {
const classes = this.hierarchy.getDescendants(baseClass)
const classes = this.hierarchy.getDescendants(baseClass).filter((it) => !this.hierarchy.isMixin(it))
// Only replace if not specified
if (translated._class === undefined) {
translated._class = { $in: classes }
} else if (typeof translated._class === 'string') {
if (!classes.includes(translated._class)) {
translated._class = { $in: classes.filter((it) => !this.hierarchy.isMixin(it)) }
translated._class = classes.length === 1 ? classes[0] : { $in: classes }
}
} else if (typeof translated._class === 'object' && translated._class !== null) {
let descendants: Ref<Class<Doc>>[] = classes
@ -238,7 +237,8 @@ abstract class MongoAdapterBase implements DbAdapter {
descendants = descendants.filter((c) => !excludedClassesIds.has(c))
}
translated._class = { $in: descendants.filter((it: any) => !this.hierarchy.isMixin(it as Ref<Class<Doc>>)) }
const desc = descendants.filter((it: any) => !this.hierarchy.isMixin(it as Ref<Class<Doc>>))
translated._class = desc.length === 1 ? desc[0] : { $in: desc }
}
if (baseClass !== clazz) {
@ -279,8 +279,7 @@ abstract class MongoAdapterBase implements DbAdapter {
from: domain,
localField: fullKey,
foreignField: '_id',
as: fullKey.split('.').join('') + '_lookup',
pipeline: [{ $project: { '%hash%': 0 } }]
as: fullKey.split('.').join('') + '_lookup'
})
}
await this.getLookupValue(_class, nested, result, fullKey + '_lookup')
@ -294,8 +293,7 @@ abstract class MongoAdapterBase implements DbAdapter {
from: domain,
localField: fullKey,
foreignField: '_id',
as: fullKey.split('.').join('') + '_lookup',
pipeline: [{ $project: { '%hash%': 0 } }]
as: fullKey.split('.').join('') + '_lookup'
})
}
}
@ -333,10 +331,9 @@ abstract class MongoAdapterBase implements DbAdapter {
pipeline: [
{
$match: {
_class: { $in: desc }
_class: desc.length === 1 ? desc[0] : { $in: desc }
}
},
{ $project: { '%hash%': 0 } }
}
],
as: asVal
}
@ -483,7 +480,7 @@ abstract class MongoAdapterBase implements DbAdapter {
const pipeline: any[] = []
const match = { $match: this.translateQuery(clazz, query) }
const slowPipeline = isLookupQuery(query) || isLookupSort(options?.sort)
const steps = await this.getLookups(clazz, options?.lookup)
const steps = await ctx.with('get-lookups', {}, async () => await this.getLookups(clazz, options?.lookup))
if (slowPipeline) {
for (const step of steps) {
pipeline.push({ $lookup: step })
@ -507,8 +504,6 @@ abstract class MongoAdapterBase implements DbAdapter {
projection[ckey] = options.projection[key]
}
pipeline.push({ $project: projection })
} else {
pipeline.push({ $project: { '%hash%': 0 } })
}
// const domain = this.hierarchy.getDomain(clazz)
@ -519,15 +514,16 @@ abstract class MongoAdapterBase implements DbAdapter {
let total = options?.total === true ? 0 : -1
try {
await ctx.with(
'toArray',
{},
'aggregate',
{ clazz },
async (ctx) => {
result = await toArray(cursor)
},
() => ({
size: result.length,
domain,
pipeline
pipeline,
clazz
})
)
} catch (e) {
@ -538,6 +534,11 @@ abstract class MongoAdapterBase implements DbAdapter {
await ctx.with('fill-lookup', {}, async (ctx) => {
await this.fillLookupValue(ctx, clazz, options?.lookup, row)
})
if (row.$lookup !== undefined) {
for (const [, v] of Object.entries(row.$lookup)) {
this.stripHash(v)
}
}
this.clearExtraLookups(row)
}
if (options?.total === true) {
@ -545,10 +546,19 @@ abstract class MongoAdapterBase implements DbAdapter {
const totalCursor = this.collection(domain).aggregate(totalPipeline, {
checkKeys: false
})
const arr = await toArray(totalCursor)
const arr = await ctx.with(
'aggregate-total',
{},
async (ctx) => await toArray(totalCursor),
() => ({
domain,
pipeline,
clazz
})
)
total = arr?.[0]?.total ?? 0
}
return toFindResult(this.stripHash(result), total)
return toFindResult(this.stripHash(result) as T[], total)
}
private translateKey<T extends Doc>(key: string, clazz: Ref<Class<T>>): string {
@ -634,7 +644,7 @@ abstract class MongoAdapterBase implements DbAdapter {
@withContext('groupBy')
async groupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
const result = await this.globalCtx.with(
const result = await ctx.with(
'groupBy',
{ domain },
async (ctx) => {
@ -697,7 +707,6 @@ abstract class MongoAdapterBase implements DbAdapter {
return result
}
@withContext('find-all')
async findAll<T extends Doc>(
ctx: MeasureContext,
_class: Ref<Class<T>>,
@ -708,26 +717,17 @@ abstract class MongoAdapterBase implements DbAdapter {
return await this.findRateLimit.exec(async () => {
const st = Date.now()
const result = await this.collectOps(
this.globalCtx,
ctx,
this.hierarchy.findDomain(_class),
'find',
async (ctx) => {
const domain = options?.domain ?? this.hierarchy.getDomain(_class)
if (
options != null &&
(options?.lookup != null || this.isEnumSort(_class, options) || this.isRulesSort(options))
) {
return await ctx.with(
'pipeline',
{},
async (ctx) => await this.findWithPipeline(ctx, _class, query, options),
{
_class,
query,
options
}
)
return await this.findWithPipeline(ctx, _class, query, options)
}
const domain = options?.domain ?? this.hierarchy.getDomain(_class)
const coll = this.collection(domain)
const mongoQuery = this.translateQuery(_class, query)
@ -735,7 +735,7 @@ abstract class MongoAdapterBase implements DbAdapter {
// Skip sort/projection/etc.
return await ctx.with(
'find-one',
{},
{ domain },
async (ctx) => {
const findOptions: MongoFindOptions = {}
@ -744,8 +744,6 @@ abstract class MongoAdapterBase implements DbAdapter {
}
if (options?.projection !== undefined) {
findOptions.projection = this.calcProjection<T>(options, _class)
} else {
findOptions.projection = { '%hash%': 0 }
}
const doc = await coll.findOne(mongoQuery, findOptions)
@ -758,7 +756,7 @@ abstract class MongoAdapterBase implements DbAdapter {
}
return toFindResult([], total)
},
{ mongoQuery }
{ domain, mongoQuery }
)
}
@ -769,8 +767,6 @@ abstract class MongoAdapterBase implements DbAdapter {
if (projection != null) {
cursor = cursor.project(projection)
}
} else {
cursor = cursor.project({ '%hash%': 0 })
}
let total: number = -1
if (options != null) {
@ -792,7 +788,7 @@ abstract class MongoAdapterBase implements DbAdapter {
try {
let res: T[] = []
await ctx.with(
'toArray',
'find-all',
{},
async (ctx) => {
res = await toArray(cursor)
@ -807,7 +803,7 @@ abstract class MongoAdapterBase implements DbAdapter {
if (options?.total === true && options?.limit === undefined) {
total = res.length
}
return toFindResult(this.stripHash(res), total)
return toFindResult(this.stripHash(res) as T[], total)
} catch (e) {
console.error('error during executing cursor in findAll', _class, cutObjectArray(query), options, e)
throw e
@ -882,13 +878,19 @@ abstract class MongoAdapterBase implements DbAdapter {
return projection
}
stripHash<T extends Doc>(docs: T[]): T[] {
docs.forEach((it) => {
if ('%hash%' in it) {
delete it['%hash%']
stripHash<T extends Doc>(docs: T | T[]): T | T[] {
if (Array.isArray(docs)) {
docs.forEach((it) => {
if ('%hash%' in it) {
delete it['%hash%']
}
return it
})
} else if (typeof docs === 'object' && docs != null) {
if ('%hash%' in docs) {
delete docs['%hash%']
}
return it
})
}
return docs
}
@ -1000,7 +1002,7 @@ abstract class MongoAdapterBase implements DbAdapter {
}
const cursor = this.db.collection<Doc>(domain).find<Doc>({ _id: { $in: docs } }, { limit: docs.length })
const result = await toArray(cursor)
return this.stripHash(result)
return this.stripHash(result) as Doc[]
})
}
@ -1108,7 +1110,6 @@ class MongoAdapter extends MongoAdapterBase {
}
}
@withContext('tx')
async tx (ctx: MeasureContext, ...txes: Tx[]): Promise<TxResult[]> {
const result: TxResult[] = []
@ -1147,7 +1148,7 @@ class MongoAdapter extends MongoAdapterBase {
}
domains.push(
this.collectOps(
this.globalCtx,
ctx,
domain,
'tx',
async (ctx) => {
@ -1454,13 +1455,12 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter {
await this._db.init(DOMAIN_TX)
}
@withContext('tx')
override async tx (ctx: MeasureContext, ...tx: Tx[]): Promise<TxResult[]> {
if (tx.length === 0) {
return []
}
await this.collectOps(
this.globalCtx,
ctx,
DOMAIN_TX,
'tx',
async () => {
@ -1490,9 +1490,6 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter {
sort: {
_id: 1,
modifiedOn: 1
},
projection: {
'%hash%': 0
}
}
)
@ -1650,7 +1647,7 @@ export async function createMongoAdapter (
const client = getMongoClient(url)
const db = getWorkspaceDB(await client.getClient(), workspaceId)
return new MongoAdapter(ctx.newChild('mongoDb', {}), db, hierarchy, modelDb, client, options)
return new MongoAdapter(db, hierarchy, modelDb, client, options)
}
/**
@ -1666,5 +1663,5 @@ export async function createMongoTxAdapter (
const client = getMongoClient(url)
const db = getWorkspaceDB(await client.getClient(), workspaceId)
return new MongoTxAdapter(ctx.newChild('mongoDbTx', {}), db, hierarchy, modelDb, client)
return new MongoTxAdapter(db, hierarchy, modelDb, client)
}

View File

@ -131,7 +131,8 @@ export function getMongoClient (uri: string, options?: MongoClientOptions): Mong
MongoClient.connect(uri, {
appName: 'transactor',
...options,
...extraOptions
...extraOptions,
enableUtf8Validation: false
}),
() => {
connections.delete(key)

View File

@ -2,6 +2,8 @@ import { type Locator, type Page, expect } from '@playwright/test'
import { NewToDo, Slot } from './types'
import { CalendarPage } from '../calendar-page'
const retryOptions = { intervals: [1000, 1500, 2500], timeout: 60000 }
export class PlanningPage extends CalendarPage {
readonly page: Page
@ -81,14 +83,17 @@ export class PlanningPage extends CalendarPage {
async dragdropTomorrow (title: string, time: string): Promise<void> {
await this.toDosContainer().getByRole('button', { name: title }).hover()
await this.page.mouse.down()
const boundingBox = await this.selectTomorrow(time).boundingBox()
expect(boundingBox).toBeTruthy()
if (boundingBox != null) {
await this.page.mouse.move(boundingBox.x + 10, boundingBox.y + 10)
await this.page.mouse.move(boundingBox.x + 10, boundingBox.y + 20)
await this.page.mouse.up()
}
await expect(async () => {
await this.page.mouse.down()
const boundingBox = await this.selectTomorrow(time).boundingBox()
expect(boundingBox).toBeTruthy()
if (boundingBox != null) {
await this.page.mouse.move(boundingBox.x + 10, boundingBox.y + 10)
await this.page.mouse.move(boundingBox.x + 10, boundingBox.y + 20)
await this.page.mouse.up()
}
}).toPass(retryOptions)
}
async checkInSchedule (title: string): Promise<void> {

View File

@ -1,4 +1,4 @@
import { test } from '@playwright/test'
import { test, expect } from '@playwright/test'
import { generateId, PlatformSetting, PlatformURI } from '../utils'
import { PlanningPage } from '../model/planning/planning-page'
import { NewToDo } from '../model/planning/types'
@ -8,6 +8,8 @@ test.use({
storageState: PlatformSetting
})
const retryOptions = { intervals: [1000, 1500, 2500], timeout: 60000 }
test.describe('Planning ToDo tests', () => {
test.beforeEach(async ({ page }) => {
await (await page.goto(`${PlatformURI}/workbench/sanity-ws/time`))?.finished()
@ -42,9 +44,13 @@ test.describe('Planning ToDo tests', () => {
const planningPage = new PlanningPage(page)
const planningNavigationMenuPage = new PlanningNavigationMenuPage(page)
await planningNavigationMenuPage.clickOnButtonUnplanned()
await planningNavigationMenuPage.compareCountersUnplannedToDos()
await expect(async () => {
await planningNavigationMenuPage.compareCountersUnplannedToDos()
}).toPass(retryOptions)
await planningPage.createNewToDo(newToDo)
await planningNavigationMenuPage.compareCountersUnplannedToDos()
await expect(async () => {
await planningNavigationMenuPage.compareCountersUnplannedToDos()
}).toPass(retryOptions)
await planningNavigationMenuPage.clickOnButtonToDoAll()
await planningPage.checkToDoExist(newToDo.title)