mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
Fix load of applications in tables/lists/kanban (#6231)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
943c01bf8c
commit
1c965209a7
1
.vscode/launch.json
vendored
1
.vscode/launch.json
vendored
@ -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",
|
||||
|
@ -1 +1 @@
|
||||
"0.6.270"
|
||||
"0.6.271"
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 }]
|
||||
})
|
||||
}
|
||||
|
@ -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 },
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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) => {
|
||||
|
@ -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 (
|
||||
|
@ -51,6 +51,7 @@
|
||||
preferenceQuery.query(
|
||||
view.class.ViewletPreference,
|
||||
{
|
||||
space: core.space.Workspace,
|
||||
attachedTo: viewlet._id
|
||||
},
|
||||
(res) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
@ -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'
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -55,6 +55,7 @@
|
||||
preferenceQuery.query(
|
||||
view.class.ViewletPreference,
|
||||
{
|
||||
space: core.space.Workspace,
|
||||
attachedTo: { $in: Array.from(viewlets.map((it) => it._id)) }
|
||||
},
|
||||
(res) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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[] = []
|
||||
|
@ -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 [
|
||||
|
@ -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) => {
|
||||
|
@ -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')
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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']
|
||||
|
@ -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 []
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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, {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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> {
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user