mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-09 09:20:54 +00:00
UBERF-7690: Performance fixes (#6336)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
d502ba86d9
commit
87ded4d797
@ -67,7 +67,7 @@ async function hydrateNotificationAsYouCan (lastNotification: InboxNotification)
|
|||||||
body: ''
|
body: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = await client.findOne(contact.class.PersonAccount, { _id: lastNotification.modifiedBy as Ref<PersonAccount> })
|
const account = await client.getModel().findOne(contact.class.PersonAccount, { _id: lastNotification.modifiedBy as Ref<PersonAccount> })
|
||||||
|
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
return noPersonData
|
return noPersonData
|
||||||
|
@ -362,7 +362,6 @@ export function createModel (builder: Builder): void {
|
|||||||
|
|
||||||
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
||||||
domain: DOMAIN_ACTIVITY,
|
domain: DOMAIN_ACTIVITY,
|
||||||
indexes: [{ keys: { attachedTo: 1, createdOn: 1 } }, { keys: { attachedTo: 1, createdOn: -1 } }],
|
|
||||||
disabled: [
|
disabled: [
|
||||||
{ modifiedOn: 1 },
|
{ modifiedOn: 1 },
|
||||||
{ createdOn: -1 },
|
{ createdOn: -1 },
|
||||||
|
@ -199,7 +199,8 @@ export function createModel (builder: Builder): void {
|
|||||||
{ modifiedBy: 1 },
|
{ modifiedBy: 1 },
|
||||||
{ createdBy: 1 },
|
{ createdBy: 1 },
|
||||||
{ createdOn: -1 },
|
{ createdOn: -1 },
|
||||||
{ state: 1 }
|
{ state: 1 },
|
||||||
|
{ _class: 1 }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -135,6 +135,14 @@ export function createModel (builder: Builder): void {
|
|||||||
)
|
)
|
||||||
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
||||||
domain: DOMAIN_BITRIX,
|
domain: DOMAIN_BITRIX,
|
||||||
disabled: [{ _id: 1 }, { _class: 1 }, { space: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }]
|
disabled: [
|
||||||
|
{ _id: 1 },
|
||||||
|
{ _class: 1 },
|
||||||
|
{ space: 1 },
|
||||||
|
{ modifiedBy: 1 },
|
||||||
|
{ createdBy: 1 },
|
||||||
|
{ createdOn: -1 },
|
||||||
|
{ modifiedOn: 1 }
|
||||||
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -223,6 +223,7 @@ export class TContactsTab extends TDoc implements ContactsTab {
|
|||||||
@Model(contact.class.PersonSpace, core.class.Space)
|
@Model(contact.class.PersonSpace, core.class.Space)
|
||||||
export class TPersonSpace extends TSpace implements PersonSpace {
|
export class TPersonSpace extends TSpace implements PersonSpace {
|
||||||
@Prop(TypeRef(contact.class.Person), contact.string.Person)
|
@Prop(TypeRef(contact.class.Person), contact.string.Person)
|
||||||
|
@Index(IndexKind.Indexed)
|
||||||
person!: Ref<Person>
|
person!: Ref<Person>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1170,6 +1171,14 @@ export function createModel (builder: Builder): void {
|
|||||||
})
|
})
|
||||||
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
||||||
domain: DOMAIN_CONTACT,
|
domain: DOMAIN_CONTACT,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
_class: 1,
|
||||||
|
[contact.mixin.Employee + '.active']: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
disabled: [{ attachedToClass: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }, { attachedTo: 1 }]
|
disabled: [{ attachedToClass: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }, { attachedTo: 1 }]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -197,6 +197,7 @@ export function createModel (builder: Builder): void {
|
|||||||
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
||||||
domain: DOMAIN_TX,
|
domain: DOMAIN_TX,
|
||||||
disabled: [
|
disabled: [
|
||||||
|
{ _class: 1 },
|
||||||
{ space: 1 },
|
{ space: 1 },
|
||||||
{ objectClass: 1 },
|
{ objectClass: 1 },
|
||||||
{ createdBy: 1 },
|
{ createdBy: 1 },
|
||||||
@ -267,7 +268,14 @@ export function createModel (builder: Builder): void {
|
|||||||
|
|
||||||
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
||||||
domain: DOMAIN_STATUS,
|
domain: DOMAIN_STATUS,
|
||||||
disabled: [{ modifiedOn: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdBy: -1 }, { createdOn: -1 }]
|
disabled: [
|
||||||
|
{ modifiedOn: 1 },
|
||||||
|
{ modifiedBy: 1 },
|
||||||
|
{ createdBy: 1 },
|
||||||
|
{ createdBy: -1 },
|
||||||
|
{ createdOn: -1 },
|
||||||
|
{ space: 1 }
|
||||||
|
]
|
||||||
})
|
})
|
||||||
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
||||||
domain: DOMAIN_SPACE,
|
domain: DOMAIN_SPACE,
|
||||||
@ -276,7 +284,15 @@ export function createModel (builder: Builder): void {
|
|||||||
|
|
||||||
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
||||||
domain: DOMAIN_BLOB,
|
domain: DOMAIN_BLOB,
|
||||||
disabled: [{ _class: 1 }, { space: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdBy: -1 }, { createdOn: -1 }]
|
disabled: [
|
||||||
|
{ _class: 1 },
|
||||||
|
{ space: 1 },
|
||||||
|
{ modifiedBy: 1 },
|
||||||
|
{ createdBy: 1 },
|
||||||
|
{ createdBy: -1 },
|
||||||
|
{ createdOn: -1 },
|
||||||
|
{ modifiedOn: 1 }
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
||||||
|
@ -14,22 +14,22 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
DOMAIN_MODEL,
|
||||||
|
IndexKind,
|
||||||
type Account,
|
type Account,
|
||||||
type AccountRole,
|
type AccountRole,
|
||||||
type Arr,
|
type Arr,
|
||||||
|
type Class,
|
||||||
|
type CollectionSize,
|
||||||
type Domain,
|
type Domain,
|
||||||
DOMAIN_MODEL,
|
type Permission,
|
||||||
IndexKind,
|
|
||||||
type Ref,
|
type Ref,
|
||||||
|
type Role,
|
||||||
|
type RolesAssignment,
|
||||||
type Space,
|
type Space,
|
||||||
type TypedSpace,
|
|
||||||
type SpaceType,
|
type SpaceType,
|
||||||
type SpaceTypeDescriptor,
|
type SpaceTypeDescriptor,
|
||||||
type Role,
|
type TypedSpace
|
||||||
type Class,
|
|
||||||
type Permission,
|
|
||||||
type CollectionSize,
|
|
||||||
type RolesAssignment
|
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import {
|
import {
|
||||||
ArrOf,
|
ArrOf,
|
||||||
@ -46,7 +46,7 @@ import {
|
|||||||
} from '@hcengineering/model'
|
} from '@hcengineering/model'
|
||||||
import { getEmbeddedLabel, type Asset, type IntlString } from '@hcengineering/platform'
|
import { getEmbeddedLabel, type Asset, type IntlString } from '@hcengineering/platform'
|
||||||
import core from './component'
|
import core from './component'
|
||||||
import { TDoc, TAttachedDoc } from './core'
|
import { TAttachedDoc, TDoc } from './core'
|
||||||
|
|
||||||
export const DOMAIN_SPACE = 'space' as Domain
|
export const DOMAIN_SPACE = 'space' as Domain
|
||||||
|
|
||||||
@ -67,6 +67,7 @@ export class TSpace extends TDoc implements Space {
|
|||||||
private!: boolean
|
private!: boolean
|
||||||
|
|
||||||
@Prop(TypeBoolean(), core.string.Archived)
|
@Prop(TypeBoolean(), core.string.Archived)
|
||||||
|
@Index(IndexKind.Indexed)
|
||||||
archived!: boolean
|
archived!: boolean
|
||||||
|
|
||||||
@Prop(ArrOf(TypeRef(core.class.Account)), core.string.Members)
|
@Prop(ArrOf(TypeRef(core.class.Account)), core.string.Members)
|
||||||
|
@ -254,7 +254,8 @@ export function createModel (builder: Builder): void {
|
|||||||
{ modifiedBy: 1 },
|
{ modifiedBy: 1 },
|
||||||
{ createdBy: 1 },
|
{ createdBy: 1 },
|
||||||
{ attachedToClass: 1 },
|
{ attachedToClass: 1 },
|
||||||
{ createdOn: -1 }
|
{ createdOn: -1 },
|
||||||
|
{ modifiedOn: 1 }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import activity, { type ActivityMessage } from '@hcengineering/activity'
|
import activity, { type ActivityMessage } from '@hcengineering/activity'
|
||||||
import chunter from '@hcengineering/chunter'
|
import chunter from '@hcengineering/chunter'
|
||||||
|
import { type PersonSpace } from '@hcengineering/contact'
|
||||||
import {
|
import {
|
||||||
AccountRole,
|
AccountRole,
|
||||||
DOMAIN_MODEL,
|
DOMAIN_MODEL,
|
||||||
@ -49,7 +50,6 @@ import {
|
|||||||
UX,
|
UX,
|
||||||
type Builder
|
type Builder
|
||||||
} from '@hcengineering/model'
|
} from '@hcengineering/model'
|
||||||
import { type PersonSpace } from '@hcengineering/contact'
|
|
||||||
import core, { TClass, TDoc } from '@hcengineering/model-core'
|
import core, { TClass, TDoc } from '@hcengineering/model-core'
|
||||||
import preference, { TPreference } from '@hcengineering/model-preference'
|
import preference, { TPreference } from '@hcengineering/model-preference'
|
||||||
import view, { createAction, template } from '@hcengineering/model-view'
|
import view, { createAction, template } from '@hcengineering/model-view'
|
||||||
@ -201,7 +201,6 @@ export class TDocNotifyContext extends TDoc implements DocNotifyContext {
|
|||||||
objectId!: Ref<Doc>
|
objectId!: Ref<Doc>
|
||||||
|
|
||||||
@Prop(TypeRef(core.class.Class), core.string.Class)
|
@Prop(TypeRef(core.class.Class), core.string.Class)
|
||||||
@Index(IndexKind.Indexed)
|
|
||||||
objectClass!: Ref<Class<Doc>>
|
objectClass!: Ref<Class<Doc>>
|
||||||
|
|
||||||
@Prop(TypeRef(core.class.Space), core.string.Space)
|
@Prop(TypeRef(core.class.Space), core.string.Space)
|
||||||
@ -632,7 +631,7 @@ export function createModel (builder: Builder): void {
|
|||||||
|
|
||||||
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
||||||
domain: DOMAIN_NOTIFICATION,
|
domain: DOMAIN_NOTIFICATION,
|
||||||
indexes: [{ keys: { user: 1, archived: 1 } }],
|
indexes: [{ keys: { user: 1, archived: 1, space: 1 } }],
|
||||||
disabled: [{ modifiedOn: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { isViewed: 1 }, { hidden: 1 }]
|
disabled: [{ modifiedOn: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { isViewed: 1 }, { hidden: 1 }]
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -647,7 +646,8 @@ export function createModel (builder: Builder): void {
|
|||||||
{ isViewed: 1 },
|
{ isViewed: 1 },
|
||||||
{ hidden: 1 },
|
{ hidden: 1 },
|
||||||
{ createdOn: -1 },
|
{ createdOn: -1 },
|
||||||
{ attachedTo: 1 }
|
{ attachedTo: 1 },
|
||||||
|
{ space: 1 }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
||||||
|
@ -480,7 +480,7 @@ export function createModel (builder: Builder): void {
|
|||||||
sortable: true
|
sortable: true
|
||||||
},
|
},
|
||||||
baseQuery: {
|
baseQuery: {
|
||||||
isDone: { $ne: true },
|
isDone: false,
|
||||||
'$lookup.space.archived': false
|
'$lookup.space.archived': false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -500,7 +500,7 @@ export function createModel (builder: Builder): void {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
baseQuery: {
|
baseQuery: {
|
||||||
isDone: { $ne: true },
|
isDone: false,
|
||||||
'$lookup.space.archived': false
|
'$lookup.space.archived': false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -796,7 +796,7 @@ export function createModel (builder: Builder): void {
|
|||||||
descriptor: task.viewlet.Kanban,
|
descriptor: task.viewlet.Kanban,
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
baseQuery: {
|
baseQuery: {
|
||||||
isDone: { $ne: true },
|
isDone: false,
|
||||||
'$lookup.space.archived': false
|
'$lookup.space.archived': false
|
||||||
},
|
},
|
||||||
viewOptions: {
|
viewOptions: {
|
||||||
|
@ -60,6 +60,16 @@ export const recruitOperation: MigrateOperation = {
|
|||||||
func: async (client: MigrationClient) => {
|
func: async (client: MigrationClient) => {
|
||||||
await migrateSpace(client, 'recruit:space:Reviews' as Ref<Space>, core.space.Workspace, [DOMAIN_CALENDAR])
|
await migrateSpace(client, 'recruit:space:Reviews' as Ref<Space>, core.space.Workspace, [DOMAIN_CALENDAR])
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: 'migrate-applicants',
|
||||||
|
func: async (client: MigrationClient) => {
|
||||||
|
await client.update(
|
||||||
|
DOMAIN_TASK,
|
||||||
|
{ _class: recruit.class.Applicant, isDone: { $nin: [false, true] } },
|
||||||
|
{ isDone: false }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
|
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
|
||||||
import { deepEqual } from 'fast-equals'
|
import { deepEqual } from 'fast-equals'
|
||||||
|
import { DOMAIN_BENCHMARK } from './benchmark'
|
||||||
import {
|
import {
|
||||||
Account,
|
Account,
|
||||||
AccountRole,
|
AccountRole,
|
||||||
@ -46,7 +47,6 @@ import { TxOperations } from './operations'
|
|||||||
import { isPredicate } from './predicate'
|
import { isPredicate } from './predicate'
|
||||||
import { DocumentQuery, FindResult } from './storage'
|
import { DocumentQuery, FindResult } from './storage'
|
||||||
import { DOMAIN_TX } from './tx'
|
import { DOMAIN_TX } from './tx'
|
||||||
import { DOMAIN_BENCHMARK } from './benchmark'
|
|
||||||
|
|
||||||
function toHex (value: number, chars: number): string {
|
function toHex (value: number, chars: number): string {
|
||||||
const result = value.toString(16)
|
const result = value.toString(16)
|
||||||
@ -604,9 +604,10 @@ export const isEnum =
|
|||||||
export async function checkPermission (
|
export async function checkPermission (
|
||||||
client: TxOperations,
|
client: TxOperations,
|
||||||
_id: Ref<Permission>,
|
_id: Ref<Permission>,
|
||||||
_space: Ref<TypedSpace>
|
_space: Ref<TypedSpace>,
|
||||||
|
space?: TypedSpace
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const space = await client.findOne(core.class.TypedSpace, { _id: _space })
|
space = space ?? (await client.findOne(core.class.TypedSpace, { _id: _space }))
|
||||||
const type = await client
|
const type = await client
|
||||||
.getModel()
|
.getModel()
|
||||||
.findOne(core.class.SpaceType, { _id: space?.type }, { lookup: { _id: { roles: core.class.Role } } })
|
.findOne(core.class.SpaceType, { _id: space?.type }, { lookup: { _id: { roles: core.class.Role } } })
|
||||||
|
@ -16,7 +16,7 @@ import type { AttachedDoc, Class, Doc, Markup, Mixin, Ref, SystemSpace, Timestam
|
|||||||
import { NotificationType } from '@hcengineering/notification'
|
import { NotificationType } from '@hcengineering/notification'
|
||||||
import type { Asset, IntlString, Metadata, Plugin } from '@hcengineering/platform'
|
import type { Asset, IntlString, Metadata, Plugin } from '@hcengineering/platform'
|
||||||
import { plugin } from '@hcengineering/platform'
|
import { plugin } from '@hcengineering/platform'
|
||||||
import type { Handler, IntegrationType } from '@hcengineering/setting'
|
import { Handler, IntegrationType } from '@hcengineering/setting'
|
||||||
import { AnyComponent, ComponentExtensionId } from '@hcengineering/ui'
|
import { AnyComponent, ComponentExtensionId } from '@hcengineering/ui'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -320,7 +320,7 @@ function fillStores (): void {
|
|||||||
const accountPersonQuery = createQuery(true)
|
const accountPersonQuery = createQuery(true)
|
||||||
|
|
||||||
const query = createQuery(true)
|
const query = createQuery(true)
|
||||||
query.query(contact.mixin.Employee, {}, (res) => {
|
query.query(contact.mixin.Employee, { [contact.mixin.Employee + '.active']: { $in: [true, false] } }, (res) => {
|
||||||
employeesStore.set(res)
|
employeesStore.set(res)
|
||||||
employeeByIdStore.set(toIdMap(res))
|
employeeByIdStore.set(toIdMap(res))
|
||||||
})
|
})
|
||||||
@ -331,13 +331,10 @@ function fillStores (): void {
|
|||||||
|
|
||||||
const persons = res.map((it) => it.person)
|
const persons = res.map((it) => it.person)
|
||||||
|
|
||||||
accountPersonQuery.query<Person>(
|
accountPersonQuery.query<Person>(contact.class.Person, { _id: { $in: persons } }, (res) => {
|
||||||
contact.class.Person,
|
const personIn = toIdMap(res)
|
||||||
{ _id: { $in: persons }, [contact.mixin.Employee]: { $exists: false } },
|
personAccountPersonByIdStore.set(personIn)
|
||||||
(res) => {
|
})
|
||||||
personAccountPersonByIdStore.set(toIdMap(res))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const providerQuery = createQuery(true)
|
const providerQuery = createQuery(true)
|
||||||
|
@ -112,12 +112,8 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
|
|||||||
user: getCurrentAccount()._id
|
user: getCurrentAccount()._id
|
||||||
},
|
},
|
||||||
(result: InboxNotification[]) => {
|
(result: InboxNotification[]) => {
|
||||||
|
result.sort((a, b) => (b.createdOn ?? b.modifiedOn) - (a.createdOn ?? a.modifiedOn))
|
||||||
this.otherInboxNotifications.set(result)
|
this.otherInboxNotifications.set(result)
|
||||||
},
|
|
||||||
{
|
|
||||||
sort: {
|
|
||||||
createdOn: SortingOrder.Descending
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DocumentQuery, Ref } from '@hcengineering/core'
|
import { DocumentQuery, Ref } from '@hcengineering/core'
|
||||||
import type { IntlString, Asset } from '@hcengineering/platform'
|
import type { Asset, IntlString } from '@hcengineering/platform'
|
||||||
import { createQuery } from '@hcengineering/presentation'
|
import { createQuery } from '@hcengineering/presentation'
|
||||||
import { Issue, IssueStatus, Project } from '@hcengineering/tracker'
|
import { Issue, IssueStatus, Project } from '@hcengineering/tracker'
|
||||||
import { IModeSelector, resolvedLocationStore } from '@hcengineering/ui'
|
import { IModeSelector, resolvedLocationStore } from '@hcengineering/ui'
|
||||||
@ -41,18 +41,20 @@
|
|||||||
let query: DocumentQuery<Issue> | undefined = undefined
|
let query: DocumentQuery<Issue> | undefined = undefined
|
||||||
let modeSelectorProps: IModeSelector | undefined = undefined
|
let modeSelectorProps: IModeSelector | undefined = undefined
|
||||||
|
|
||||||
const archivedProjectQuery = createQuery()
|
const allProjectQuery = createQuery()
|
||||||
let archived: Ref<Project>[] = []
|
let allProjects: Pick<Project, '_id' | '_class' | 'archived'>[] = []
|
||||||
|
|
||||||
archivedProjectQuery.query(
|
allProjectQuery.query(
|
||||||
tracker.class.Project,
|
tracker.class.Project,
|
||||||
{ archived: true },
|
{},
|
||||||
(res) => {
|
(res) => {
|
||||||
archived = res.map((it) => it._id)
|
allProjects = res
|
||||||
},
|
},
|
||||||
{ projection: { _id: 1 } }
|
{ projection: { _id: 1, archived: 1 } }
|
||||||
)
|
)
|
||||||
$: spaceQuery = currentSpace ? { space: currentSpace } : { space: { $nin: archived } }
|
$: spaceQuery = currentSpace
|
||||||
|
? { space: currentSpace }
|
||||||
|
: { space: { $in: allProjects.filter((it) => !it.archived).map((it) => it._id) } }
|
||||||
|
|
||||||
$: all = { ...baseQuery, ...spaceQuery }
|
$: all = { ...baseQuery, ...spaceQuery }
|
||||||
|
|
||||||
|
@ -75,16 +75,16 @@
|
|||||||
{ sort: { _id: 1 }, projection: { _id: 1 } }
|
{ sort: { _id: 1 }, projection: { _id: 1 } }
|
||||||
)
|
)
|
||||||
|
|
||||||
const archivedProjectQuery = createQuery()
|
const allProjectQuery = createQuery()
|
||||||
let archived: Ref<Project>[] = []
|
let allProjects: Pick<Project, '_class' | '_id' | 'archived'>[] = []
|
||||||
|
|
||||||
archivedProjectQuery.query(
|
allProjectQuery.query(
|
||||||
tracker.class.Project,
|
tracker.class.Project,
|
||||||
{ archived: true },
|
{},
|
||||||
(res) => {
|
(res) => {
|
||||||
archived = res.map((it) => it._id)
|
allProjects = res
|
||||||
},
|
},
|
||||||
{ projection: { _id: 1 } }
|
{ projection: { _id: 1, archived: 1 } }
|
||||||
)
|
)
|
||||||
|
|
||||||
$: queries = { assigned, active, backlog, created, subscribed }
|
$: queries = { assigned, active, backlog, created, subscribed }
|
||||||
@ -95,7 +95,7 @@
|
|||||||
$: if (mode !== undefined) {
|
$: if (mode !== undefined) {
|
||||||
query = { ...(queries as any)[mode] }
|
query = { ...(queries as any)[mode] }
|
||||||
if (query?.space === undefined) {
|
if (query?.space === undefined) {
|
||||||
query = { ...query, space: { $nin: archived } }
|
query = { ...query, space: { $in: allProjects.filter((it) => !it.archived).map((it) => it._id) } }
|
||||||
}
|
}
|
||||||
modeSelectorProps = {
|
modeSelectorProps = {
|
||||||
config,
|
config,
|
||||||
|
@ -1496,6 +1496,14 @@ export const permissionsStore = writable<PermissionsStore>({
|
|||||||
whitelist: new Set()
|
whitelist: new Set()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const spaceSpaceQuery = createQuery(true)
|
||||||
|
|
||||||
|
export const spaceSpace = writable<TypedSpace | undefined>(undefined)
|
||||||
|
|
||||||
|
spaceSpaceQuery.query(core.class.TypedSpace, { _id: core.space.Space }, (res) => {
|
||||||
|
spaceSpace.set(res[0])
|
||||||
|
})
|
||||||
|
|
||||||
const spaceTypesQuery = createQuery(true)
|
const spaceTypesQuery = createQuery(true)
|
||||||
const permissionsQuery = createQuery(true)
|
const permissionsQuery = createQuery(true)
|
||||||
type TargetClassesProjection = Record<Ref<Class<Space>>, number>
|
type TargetClassesProjection = Record<Ref<Class<Space>>, number>
|
||||||
|
@ -13,8 +13,17 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import core, { checkPermission, type Space, type Doc, type TypedSpace, getCurrentAccount } from '@hcengineering/core'
|
import core, {
|
||||||
|
checkPermission,
|
||||||
|
getCurrentAccount,
|
||||||
|
toIdMap,
|
||||||
|
type Doc,
|
||||||
|
type Space,
|
||||||
|
type TypedSpace
|
||||||
|
} from '@hcengineering/core'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
|
import { get } from 'svelte/store'
|
||||||
|
import { spaceSpace } from './utils'
|
||||||
|
|
||||||
function isTypedSpace (space: Space): space is TypedSpace {
|
function isTypedSpace (space: Space): space is TypedSpace {
|
||||||
return getClient().getHierarchy().isDerived(space._class, core.class.TypedSpace)
|
return getClient().getHierarchy().isDerived(space._class, core.class.TypedSpace)
|
||||||
@ -28,14 +37,14 @@ export async function canDeleteObject (doc?: Doc | Doc[]): Promise<boolean> {
|
|||||||
const client = getClient()
|
const client = getClient()
|
||||||
const targets = Array.isArray(doc) ? doc : [doc]
|
const targets = Array.isArray(doc) ? doc : [doc]
|
||||||
// Note: allow deleting objects in NOT typed spaces for now
|
// Note: allow deleting objects in NOT typed spaces for now
|
||||||
const targetSpaces = (await client.findAll(core.class.Space, { _id: { $in: targets.map((t) => t.space) } })).filter(
|
const targetSpaces = toIdMap(
|
||||||
isTypedSpace
|
(await client.findAll(core.class.Space, { _id: { $in: targets.map((t) => t.space) } })).filter(isTypedSpace)
|
||||||
)
|
)
|
||||||
|
|
||||||
return !(
|
return !(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from(new Set(targetSpaces.map((t) => t._id))).map(
|
Array.from(targetSpaces.entries()).map(
|
||||||
async (s) => await checkPermission(client, core.permission.ForbidDeleteObject, s)
|
async (s) => await checkPermission(client, core.permission.ForbidDeleteObject, s[0], s[1])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).some((r) => r)
|
).some((r) => r)
|
||||||
@ -54,11 +63,13 @@ export async function canEditSpace (doc?: Doc | Doc[]): Promise<boolean> {
|
|||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
|
||||||
if (await checkPermission(client, core.permission.UpdateObject, core.space.Space)) {
|
const _spaceSpace = get(spaceSpace) ?? (await client.findOne(core.class.TypedSpace, { _id: core.space.Space }))
|
||||||
|
|
||||||
|
if (await checkPermission(client, core.permission.UpdateObject, core.space.Space, _spaceSpace)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTypedSpace(space) && (await checkPermission(client, core.permission.UpdateSpace, space._id))) {
|
if (isTypedSpace(space) && (await checkPermission(client, core.permission.UpdateSpace, space._id, space))) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,11 +89,13 @@ export async function canArchiveSpace (doc?: Doc | Doc[]): Promise<boolean> {
|
|||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
|
||||||
if (await checkPermission(client, core.permission.DeleteObject, core.space.Space)) {
|
const _spaceSpace = get(spaceSpace) ?? (await client.findOne(core.class.TypedSpace, { _id: core.space.Space }))
|
||||||
|
|
||||||
|
if (await checkPermission(client, core.permission.DeleteObject, core.space.Space, _spaceSpace)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTypedSpace(space) && (await checkPermission(client, core.permission.ArchiveSpace, space._id))) {
|
if (isTypedSpace(space) && (await checkPermission(client, core.permission.ArchiveSpace, space._id, space))) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +115,9 @@ export async function canDeleteSpace (doc?: Doc | Doc[]): Promise<boolean> {
|
|||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
|
||||||
if (await checkPermission(client, core.permission.DeleteObject, core.space.Space)) {
|
const _spaceSpace = get(spaceSpace) ?? (await client.findOne(core.class.TypedSpace, { _id: core.space.Space }))
|
||||||
|
|
||||||
|
if (await checkPermission(client, core.permission.DeleteObject, core.space.Space, _spaceSpace)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,12 +41,14 @@
|
|||||||
let shownSpaces: Space[] = []
|
let shownSpaces: Space[] = []
|
||||||
|
|
||||||
$: if (model) {
|
$: if (model) {
|
||||||
const classes = getSpecialSpaceClass(model).flatMap((c) => hierarchy.getDescendants(c))
|
const classes = Array.from(new Set(getSpecialSpaceClass(model).flatMap((c) => hierarchy.getDescendants(c)))).filter(
|
||||||
|
(it) => !hierarchy.isMixin(it)
|
||||||
|
)
|
||||||
if (classes.length > 0) {
|
if (classes.length > 0) {
|
||||||
query.query(
|
query.query(
|
||||||
core.class.Space,
|
classes.length === 1 ? classes[0] : core.class.Space,
|
||||||
{
|
{
|
||||||
_class: classes.length === 1 ? classes[0] : { $in: classes },
|
...(classes.length === 1 ? {} : { _class: { $in: classes } }),
|
||||||
members: getCurrentAccount()._id
|
members: getCurrentAccount()._id
|
||||||
},
|
},
|
||||||
(result) => {
|
(result) => {
|
||||||
|
@ -2,7 +2,7 @@ FROM node:20
|
|||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
RUN npm install --ignore-scripts=false --verbose bufferutil utf-8-validate @mongodb-js/zstd snappy --unsafe-perm
|
RUN npm install --ignore-scripts=false --verbose bufferutil utf-8-validate @mongodb-js/zstd snappy --unsafe-perm
|
||||||
RUN npm install --ignore-scripts=false --verbose uNetworking/uWebSockets.js#v20.43.0
|
RUN npm install --ignore-scripts=false --verbose uNetworking/uWebSockets.js#v20.47.0
|
||||||
|
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install libjemalloc2
|
RUN apt-get install libjemalloc2
|
||||||
|
@ -122,10 +122,11 @@ async function handleVacancyUpdate (control: TriggerControl, cud: TxCUD<Doc>, re
|
|||||||
const updateTx = cud as TxUpdateDoc<Vacancy>
|
const updateTx = cud as TxUpdateDoc<Vacancy>
|
||||||
if (updateTx.operations.company !== undefined) {
|
if (updateTx.operations.company !== undefined) {
|
||||||
// It could be null or new value
|
// It could be null or new value
|
||||||
const txes = await control.findAll(core.class.TxCUD, {
|
const txes = (
|
||||||
objectId: updateTx.objectId,
|
await control.findAll(core.class.TxCUD, {
|
||||||
_id: { $nin: [updateTx._id] }
|
objectId: updateTx.objectId
|
||||||
})
|
})
|
||||||
|
).filter((it) => it._id !== updateTx._id)
|
||||||
const vacancy = TxProcessor.buildDoc2Doc(txes) as Vacancy
|
const vacancy = TxProcessor.buildDoc2Doc(txes) as Vacancy
|
||||||
if (vacancy.company != null) {
|
if (vacancy.company != null) {
|
||||||
// We have old value
|
// We have old value
|
||||||
@ -162,10 +163,11 @@ async function handleVacancyRemove (control: TriggerControl, cud: TxCUD<Doc>, ac
|
|||||||
if (control.hierarchy.isDerived(cud.objectClass, recruit.class.Vacancy)) {
|
if (control.hierarchy.isDerived(cud.objectClass, recruit.class.Vacancy)) {
|
||||||
const removeTx = actualTx as TxRemoveDoc<Vacancy>
|
const removeTx = actualTx as TxRemoveDoc<Vacancy>
|
||||||
// It could be null or new value
|
// It could be null or new value
|
||||||
const txes = await control.findAll(core.class.TxCUD, {
|
const txes = (
|
||||||
objectId: removeTx.objectId,
|
await control.findAll(core.class.TxCUD, {
|
||||||
_id: { $nin: [removeTx._id] }
|
objectId: removeTx.objectId
|
||||||
})
|
})
|
||||||
|
).filter((it) => it._id !== removeTx._id)
|
||||||
const vacancy = TxProcessor.buildDoc2Doc(txes) as Vacancy
|
const vacancy = TxProcessor.buildDoc2Doc(txes) as Vacancy
|
||||||
const res: Tx[] = []
|
const res: Tx[] = []
|
||||||
if (vacancy.company != null) {
|
if (vacancy.company != null) {
|
||||||
|
@ -44,11 +44,9 @@ export async function getValue (control: TriggerControl, context: Record<string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getEmployee (control: TriggerControl, _id: Ref<Account>): Promise<Person | undefined> {
|
async function getEmployee (control: TriggerControl, _id: Ref<Account>): Promise<Person | undefined> {
|
||||||
const employeeAccount = (
|
const employeeAccount = control.modelDb.findAllSync(contact.class.PersonAccount, {
|
||||||
await control.modelDb.findAll(contact.class.PersonAccount, {
|
_id: _id as Ref<PersonAccount>
|
||||||
_id: _id as Ref<PersonAccount>
|
})[0]
|
||||||
})
|
|
||||||
)[0]
|
|
||||||
if (employeeAccount !== undefined) {
|
if (employeeAccount !== undefined) {
|
||||||
const employee = (
|
const employee = (
|
||||||
await control.findAll(contact.class.Person, {
|
await control.findAll(contact.class.Person, {
|
||||||
|
@ -321,11 +321,12 @@ async function doTimeReportUpdate (cud: TxCUD<TimeSpendReport>, tx: Tx, control:
|
|||||||
if (upd.operations.value !== undefined) {
|
if (upd.operations.value !== undefined) {
|
||||||
const logTxes = Array.from(
|
const logTxes = Array.from(
|
||||||
await control.findAll(core.class.TxCollectionCUD, {
|
await control.findAll(core.class.TxCollectionCUD, {
|
||||||
'tx.objectId': cud.objectId,
|
'tx.objectId': cud.objectId
|
||||||
_id: { $nin: [parentTx._id] }
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
.filter((it) => it._id !== parentTx._id)
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
).map(TxProcessor.extractTx)
|
.map(TxProcessor.extractTx)
|
||||||
const doc: TimeSpendReport | undefined = TxProcessor.buildDoc2Doc(logTxes)
|
const doc: TimeSpendReport | undefined = TxProcessor.buildDoc2Doc(logTxes)
|
||||||
|
|
||||||
const res: Tx[] = []
|
const res: Tx[] = []
|
||||||
@ -357,11 +358,12 @@ async function doTimeReportUpdate (cud: TxCUD<TimeSpendReport>, tx: Tx, control:
|
|||||||
if (!control.removedMap.has(parentTx.objectId)) {
|
if (!control.removedMap.has(parentTx.objectId)) {
|
||||||
const logTxes = Array.from(
|
const logTxes = Array.from(
|
||||||
await control.findAll(core.class.TxCollectionCUD, {
|
await control.findAll(core.class.TxCollectionCUD, {
|
||||||
'tx.objectId': cud.objectId,
|
'tx.objectId': cud.objectId
|
||||||
_id: { $nin: [parentTx._id] }
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
.filter((it) => it._id !== parentTx._id)
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
).map(TxProcessor.extractTx)
|
.map(TxProcessor.extractTx)
|
||||||
const doc: TimeSpendReport | undefined = TxProcessor.buildDoc2Doc(logTxes)
|
const doc: TimeSpendReport | undefined = TxProcessor.buildDoc2Doc(logTxes)
|
||||||
if (doc !== undefined) {
|
if (doc !== undefined) {
|
||||||
const [currentIssue] = await control.findAll(tracker.class.Issue, { _id: parentTx.objectId }, { limit: 1 })
|
const [currentIssue] = await control.findAll(tracker.class.Issue, { _id: parentTx.objectId }, { limit: 1 })
|
||||||
|
@ -44,6 +44,10 @@ export class DomainIndexHelperImpl implements DomainHelper {
|
|||||||
const attrs = hierarchy.getAllAttributes(c._id)
|
const attrs = hierarchy.getAllAttributes(c._id)
|
||||||
const domainAttrs = this.domains.get(domain) ?? new Set<FieldIndexConfig<Doc>>()
|
const domainAttrs = this.domains.get(domain) ?? new Set<FieldIndexConfig<Doc>>()
|
||||||
for (const a of attrs.values()) {
|
for (const a of attrs.values()) {
|
||||||
|
if (a.isCustom === true) {
|
||||||
|
// Skip custom attribute indexes
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (a.index !== undefined && a.index !== IndexKind.FullText) {
|
if (a.index !== undefined && a.index !== IndexKind.FullText) {
|
||||||
domainAttrs.add({
|
domainAttrs.add({
|
||||||
keys: {
|
keys: {
|
||||||
|
@ -115,7 +115,19 @@ interface LookupStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function toArray<T> (cursor: AbstractCursor<T>): Promise<T[]> {
|
export async function toArray<T> (cursor: AbstractCursor<T>): Promise<T[]> {
|
||||||
const data = await cursor.toArray()
|
const data: T[] = []
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const d = await cursor.next()
|
||||||
|
if (d === null) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
data.push(d)
|
||||||
|
const batch = cursor.readBufferedDocuments()
|
||||||
|
if (batch.length > 0) {
|
||||||
|
data.push(...batch)
|
||||||
|
}
|
||||||
|
}
|
||||||
await cursor.close()
|
await cursor.close()
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
@ -125,14 +125,26 @@ export function getMongoClient (uri: string, options?: MongoClientOptions): Mong
|
|||||||
const key = `${uri}${process.env.MONGO_OPTIONS ?? '{}'}_${JSON.stringify(options ?? {})}`
|
const key = `${uri}${process.env.MONGO_OPTIONS ?? '{}'}_${JSON.stringify(options ?? {})}`
|
||||||
let existing = connections.get(key)
|
let existing = connections.get(key)
|
||||||
|
|
||||||
|
const allOptions: MongoClientOptions = {
|
||||||
|
...options,
|
||||||
|
...extraOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make poll size stable
|
||||||
|
if (allOptions.maxPoolSize !== undefined) {
|
||||||
|
allOptions.minPoolSize = allOptions.maxPoolSize
|
||||||
|
}
|
||||||
|
allOptions.monitorCommands = false
|
||||||
|
allOptions.noDelay = true
|
||||||
|
|
||||||
// If not created or closed
|
// If not created or closed
|
||||||
if (existing === undefined) {
|
if (existing === undefined) {
|
||||||
existing = new MongoClientReferenceImpl(
|
existing = new MongoClientReferenceImpl(
|
||||||
MongoClient.connect(uri, {
|
MongoClient.connect(uri, {
|
||||||
appName: 'transactor',
|
appName: 'transactor',
|
||||||
...options,
|
enableUtf8Validation: false,
|
||||||
...extraOptions,
|
|
||||||
enableUtf8Validation: false
|
...allOptions
|
||||||
}),
|
}),
|
||||||
() => {
|
() => {
|
||||||
connections.delete(key)
|
connections.delete(key)
|
||||||
|
@ -385,7 +385,7 @@ export async function upgradeModel (
|
|||||||
|
|
||||||
await tryMigrate(migrateClient, coreId, [
|
await tryMigrate(migrateClient, coreId, [
|
||||||
{
|
{
|
||||||
state: 'indexes-v3',
|
state: 'indexes-v4',
|
||||||
func: upgradeIndexes
|
func: upgradeIndexes
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
mkdir -p ./.build
|
mkdir -p ./.build
|
||||||
cd ./.build
|
cd ./.build
|
||||||
if ! test -f ./v20.43.0.zip; then
|
if ! test -f ./v20.47.0.zip; then
|
||||||
wget --quiet https://github.com/uNetworking/uWebSockets.js/archive/refs/tags/v20.43.0.zip
|
wget --quiet https://github.com/uNetworking/uWebSockets.js/archive/refs/tags/v20.47.0.zip
|
||||||
fi
|
fi
|
||||||
if ! test -f ../lib/uws.js; then
|
if ! test -f ../lib/uws.js; then
|
||||||
unzip -qq -j -o ./v20.43.0.zip -d ../lib
|
unzip -qq -j -o ./v20.47.0.zip -d ../lib
|
||||||
fi
|
fi
|
||||||
|
2
server/ws/.gitignore
vendored
2
server/ws/.gitignore
vendored
@ -1,2 +1,2 @@
|
|||||||
v20.43.0.zip
|
v*.zip
|
||||||
src/uws
|
src/uws
|
@ -80,12 +80,13 @@ export function startHttpServer (
|
|||||||
const token = req.query.token as string
|
const token = req.query.token as string
|
||||||
const payload = decodeToken(token)
|
const payload = decodeToken(token)
|
||||||
const admin = payload.extra?.admin === 'true'
|
const admin = payload.extra?.admin === 'true'
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' })
|
const jsonData = {
|
||||||
const json = JSON.stringify({
|
|
||||||
...getStatistics(ctx, sessions, admin),
|
...getStatistics(ctx, sessions, admin),
|
||||||
users: getUsers,
|
users: getUsers(),
|
||||||
admin
|
admin
|
||||||
})
|
}
|
||||||
|
const json = JSON.stringify(jsonData)
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' })
|
||||||
res.end(json)
|
res.end(json)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Analytics.handleError(err)
|
Analytics.handleError(err)
|
||||||
|
@ -21,10 +21,10 @@ import core, {
|
|||||||
type Class,
|
type Class,
|
||||||
type TxMixin
|
type TxMixin
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
|
import github, { DocSyncInfo, GithubProject } from '@hcengineering/github'
|
||||||
import { TriggerControl } from '@hcengineering/server-core'
|
import { TriggerControl } from '@hcengineering/server-core'
|
||||||
import time, { ToDo } from '@hcengineering/time'
|
import time, { ToDo } from '@hcengineering/time'
|
||||||
import tracker from '@hcengineering/tracker'
|
import tracker from '@hcengineering/tracker'
|
||||||
import github, { DocSyncInfo, GithubProject } from '@hcengineering/github'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -124,7 +124,7 @@ async function updateDocSyncInfo (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [account] = await control.modelDb.findAll(contact.class.PersonAccount, {
|
const [account] = control.modelDb.findAllSync(contact.class.PersonAccount, {
|
||||||
_id: tx.modifiedBy as Ref<PersonAccount>
|
_id: tx.modifiedBy as Ref<PersonAccount>
|
||||||
})
|
})
|
||||||
// Do not modify state if is modified by github service.
|
// Do not modify state if is modified by github service.
|
||||||
|
Loading…
Reference in New Issue
Block a user