mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 11:01:54 +03:00
UBER-834: Improve list speed (#3692)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
af94b640c5
commit
61cc74d7a0
@ -676,7 +676,7 @@ export function createModel (builder: Builder): void {
|
|||||||
|
|
||||||
const applicantViewOptions = (colors: boolean): ViewOptionsModel => {
|
const applicantViewOptions = (colors: boolean): ViewOptionsModel => {
|
||||||
const model: ViewOptionsModel = {
|
const model: ViewOptionsModel = {
|
||||||
groupBy: ['status', 'assignee', 'space', 'createdBy', 'modifiedBy'],
|
groupBy: ['status', 'doneState', 'assignee', 'space', 'createdBy', 'modifiedBy'],
|
||||||
orderBy: [
|
orderBy: [
|
||||||
['status', SortingOrder.Ascending],
|
['status', SortingOrder.Ascending],
|
||||||
['modifiedOn', SortingOrder.Descending],
|
['modifiedOn', SortingOrder.Descending],
|
||||||
@ -784,6 +784,105 @@ export function createModel (builder: Builder): void {
|
|||||||
recruit.viewlet.ListApplicant
|
recruit.viewlet.ListApplicant
|
||||||
)
|
)
|
||||||
|
|
||||||
|
builder.createDoc(
|
||||||
|
view.class.Viewlet,
|
||||||
|
core.space.Model,
|
||||||
|
{
|
||||||
|
attachTo: recruit.mixin.Candidate,
|
||||||
|
descriptor: view.viewlet.List,
|
||||||
|
config: [
|
||||||
|
{ key: '', displayProps: { fixed: 'left', key: 'app' } },
|
||||||
|
{
|
||||||
|
key: 'title',
|
||||||
|
props: { kind: 'list', size: 'small', shouldShowName: false }
|
||||||
|
},
|
||||||
|
{ key: 'comments', displayProps: { key: 'comments', suffix: true } },
|
||||||
|
{ key: '', displayProps: { grow: true } },
|
||||||
|
{
|
||||||
|
key: '$lookup.channels',
|
||||||
|
label: contact.string.ContactInfo,
|
||||||
|
sortingKey: ['$lookup.channels.lastMessage', '$lookup.attachedTo.channels'],
|
||||||
|
props: {
|
||||||
|
length: 'full',
|
||||||
|
size: 'small',
|
||||||
|
kind: 'list'
|
||||||
|
},
|
||||||
|
displayProps: { compression: true }
|
||||||
|
},
|
||||||
|
{ key: 'modifiedOn', displayProps: { key: 'modified', fixed: 'right', dividerBefore: true } }
|
||||||
|
],
|
||||||
|
configOptions: {
|
||||||
|
strict: true,
|
||||||
|
hiddenKeys: ['name']
|
||||||
|
},
|
||||||
|
viewOptions: {
|
||||||
|
groupBy: ['createdBy', 'modifiedBy'],
|
||||||
|
orderBy: [
|
||||||
|
['modifiedOn', SortingOrder.Descending],
|
||||||
|
['createdOn', SortingOrder.Descending],
|
||||||
|
['rank', SortingOrder.Ascending]
|
||||||
|
],
|
||||||
|
other: [showColorsViewOption]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
recruit.viewlet.ListTalent
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.createDoc(
|
||||||
|
view.class.Viewlet,
|
||||||
|
core.space.Model,
|
||||||
|
{
|
||||||
|
attachTo: recruit.mixin.VacancyList,
|
||||||
|
descriptor: view.viewlet.List,
|
||||||
|
config: [
|
||||||
|
{ key: '', displayProps: { fixed: 'left', key: 'app' } },
|
||||||
|
{
|
||||||
|
key: '@vacancies',
|
||||||
|
label: recruit.string.Vacancies,
|
||||||
|
props: { kind: 'list', size: 'small', shouldShowName: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '@applications',
|
||||||
|
label: recruit.string.Applications,
|
||||||
|
props: { kind: 'list', size: 'small', shouldShowName: false }
|
||||||
|
},
|
||||||
|
{ key: 'comments', displayProps: { key: 'comments', suffix: true } },
|
||||||
|
{
|
||||||
|
key: '$lookup.channels',
|
||||||
|
label: contact.string.ContactInfo,
|
||||||
|
sortingKey: ['$lookup.channels.lastMessage', '$lookup.attachedTo.channels'],
|
||||||
|
props: {
|
||||||
|
length: 'full',
|
||||||
|
size: 'small',
|
||||||
|
kind: 'list'
|
||||||
|
},
|
||||||
|
displayProps: { compression: true }
|
||||||
|
},
|
||||||
|
{ key: '', displayProps: { grow: true } },
|
||||||
|
{
|
||||||
|
key: '@applications.modifiedOn',
|
||||||
|
label: core.string.ModifiedDate,
|
||||||
|
displayProps: { key: 'modified', fixed: 'right', dividerBefore: true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
configOptions: {
|
||||||
|
strict: true,
|
||||||
|
sortable: true,
|
||||||
|
hiddenKeys: ['name', 'space', 'modifiedOn']
|
||||||
|
},
|
||||||
|
viewOptions: {
|
||||||
|
groupBy: ['createdBy', 'modifiedBy'],
|
||||||
|
orderBy: [
|
||||||
|
['modifiedOn', SortingOrder.Descending],
|
||||||
|
['createdOn', SortingOrder.Descending],
|
||||||
|
['rank', SortingOrder.Ascending]
|
||||||
|
],
|
||||||
|
other: [showColorsViewOption]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
recruit.viewlet.ListCompanies
|
||||||
|
)
|
||||||
|
|
||||||
builder.createDoc(
|
builder.createDoc(
|
||||||
view.class.Viewlet,
|
view.class.Viewlet,
|
||||||
core.space.Model,
|
core.space.Model,
|
||||||
|
@ -120,6 +120,8 @@ export default mergeIds(recruitId, recruit, {
|
|||||||
ApplicantTable: '' as Ref<Viewlet>,
|
ApplicantTable: '' as Ref<Viewlet>,
|
||||||
ApplicantKanban: '' as Ref<Viewlet>,
|
ApplicantKanban: '' as Ref<Viewlet>,
|
||||||
ListApplicant: '' as Ref<Viewlet>,
|
ListApplicant: '' as Ref<Viewlet>,
|
||||||
|
ListTalent: '' as Ref<Viewlet>,
|
||||||
|
ListCompanies: '' as Ref<Viewlet>,
|
||||||
TableApplicant: '' as Ref<Viewlet>,
|
TableApplicant: '' as Ref<Viewlet>,
|
||||||
TableApplicantMatch: '' as Ref<Viewlet>,
|
TableApplicantMatch: '' as Ref<Viewlet>,
|
||||||
CalendarReview: '' as Ref<Viewlet>,
|
CalendarReview: '' as Ref<Viewlet>,
|
||||||
|
@ -280,3 +280,47 @@ export class DocManager {
|
|||||||
return this.docs.filter(predicate)
|
return this.docs.filter(predicate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class RateLimitter {
|
||||||
|
idCounter: number = 0
|
||||||
|
processingQueue = new Map<string, Promise<void>>()
|
||||||
|
last: number = 0
|
||||||
|
|
||||||
|
queue: (() => Promise<void>)[] = []
|
||||||
|
|
||||||
|
constructor (readonly config: () => { rate: number, perSecond?: number }) {}
|
||||||
|
|
||||||
|
async exec<T, B extends Record<string, any> = {}>(op: (args?: B) => Promise<T>, args?: B): Promise<T> {
|
||||||
|
const processingId = `${this.idCounter++}`
|
||||||
|
const cfg = this.config()
|
||||||
|
|
||||||
|
while (this.processingQueue.size > cfg.rate) {
|
||||||
|
await Promise.race(this.processingQueue.values())
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const p = op(args)
|
||||||
|
this.processingQueue.set(processingId, p as Promise<void>)
|
||||||
|
return await p
|
||||||
|
} finally {
|
||||||
|
this.processingQueue.delete(processingId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async add<T, B extends Record<string, any> = {}>(op: (args?: B) => Promise<T>, args?: B): Promise<void> {
|
||||||
|
const cfg = this.config()
|
||||||
|
|
||||||
|
if (this.processingQueue.size < cfg.rate) {
|
||||||
|
void this.exec(op, args)
|
||||||
|
} else {
|
||||||
|
await this.exec(op, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitProcessing (): Promise<void> {
|
||||||
|
await await Promise.race(this.processingQueue.values())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'tiny' | 'small' | 'medium' | 'large'
|
||||||
export let fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -14,18 +14,12 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Organization } from '@hcengineering/contact'
|
import { Organization } from '@hcengineering/contact'
|
||||||
import core, { Doc, DocumentQuery, Ref } from '@hcengineering/core'
|
import core, { Doc, DocumentQuery, Ref, WithLookup } from '@hcengineering/core'
|
||||||
import { createQuery } from '@hcengineering/presentation'
|
import { createQuery } from '@hcengineering/presentation'
|
||||||
import { Applicant, Vacancy } from '@hcengineering/recruit'
|
import { Applicant, Vacancy } from '@hcengineering/recruit'
|
||||||
import { Button, IconAdd, Label, Loading, SearchEdit, showPopup } from '@hcengineering/ui'
|
import { Button, Component, IconAdd, Label, Loading, SearchEdit, showPopup } from '@hcengineering/ui'
|
||||||
import view, { BuildModelKey, Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
|
import view, { BuildModelKey, ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||||
import {
|
import { FilterBar, FilterButton, ViewletSelector, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||||
FilterBar,
|
|
||||||
FilterButton,
|
|
||||||
TableBrowser,
|
|
||||||
ViewletSelector,
|
|
||||||
ViewletSettingButton
|
|
||||||
} from '@hcengineering/view-resources'
|
|
||||||
import recruit from '../plugin'
|
import recruit from '../plugin'
|
||||||
import CreateOrganization from './CreateOrganization.svelte'
|
import CreateOrganization from './CreateOrganization.svelte'
|
||||||
import VacancyListApplicationsPopup from './organizations/VacancyListApplicationsPopup.svelte'
|
import VacancyListApplicationsPopup from './organizations/VacancyListApplicationsPopup.svelte'
|
||||||
@ -175,14 +169,17 @@
|
|||||||
]
|
]
|
||||||
])
|
])
|
||||||
|
|
||||||
let viewlet: Viewlet | undefined
|
let viewlet: WithLookup<Viewlet> | undefined
|
||||||
let loading = true
|
let loading = true
|
||||||
|
|
||||||
let preference: ViewletPreference | undefined
|
let preference: ViewletPreference | undefined
|
||||||
let viewOptions: ViewOptions | undefined
|
let viewOptions: ViewOptions | undefined
|
||||||
|
|
||||||
function createConfig (descr: Viewlet, preference: ViewletPreference | undefined): (string | BuildModelKey)[] {
|
function createConfig (
|
||||||
const base = preference?.config ?? descr.config
|
descr: Viewlet | undefined,
|
||||||
|
preference: ViewletPreference | undefined
|
||||||
|
): (string | BuildModelKey)[] {
|
||||||
|
const base = preference?.config ?? descr?.config ?? []
|
||||||
const result: (string | BuildModelKey)[] = []
|
const result: (string | BuildModelKey)[] = []
|
||||||
for (const key of base) {
|
for (const key of base) {
|
||||||
if (typeof key === 'string') {
|
if (typeof key === 'string') {
|
||||||
@ -193,13 +190,24 @@
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: finalConfig = createConfig(viewlet, preference)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="ac-header full divide">
|
<div class="ac-header full divide">
|
||||||
<div class="ac-header__wrap-title mr-3">
|
<div class="ac-header__wrap-title mr-3">
|
||||||
<span class="ac-header__title"><Label label={recruit.string.Organizations} /></span>
|
<span class="ac-header__title"><Label label={recruit.string.Organizations} /></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="clear-mins mb-1">
|
<div class="ac-header-full medium-gap mb-1">
|
||||||
|
<ViewletSelector
|
||||||
|
bind:loading
|
||||||
|
bind:viewlet
|
||||||
|
bind:preference
|
||||||
|
viewletQuery={{
|
||||||
|
attachTo: recruit.mixin.VacancyList,
|
||||||
|
descriptor: { $in: [view.viewlet.Table, view.viewlet.List] }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Button icon={IconAdd} label={recruit.string.CompanyCreateLabel} kind={'accented'} on:click={showCreateDialog} />
|
<Button icon={IconAdd} label={recruit.string.CompanyCreateLabel} kind={'accented'} on:click={showCreateDialog} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -211,16 +219,6 @@
|
|||||||
<FilterButton _class={recruit.mixin.VacancyList} />
|
<FilterButton _class={recruit.mixin.VacancyList} />
|
||||||
</div>
|
</div>
|
||||||
<div class="ac-header-full medium-gap">
|
<div class="ac-header-full medium-gap">
|
||||||
<ViewletSelector
|
|
||||||
hidden
|
|
||||||
viewletQuery={{
|
|
||||||
attachTo: recruit.mixin.VacancyList,
|
|
||||||
descriptor: view.viewlet.Table
|
|
||||||
}}
|
|
||||||
bind:preference
|
|
||||||
bind:loading
|
|
||||||
bind:viewlet
|
|
||||||
/>
|
|
||||||
<ViewletSettingButton bind:viewOptions bind:viewlet />
|
<ViewletSettingButton bind:viewOptions bind:viewlet />
|
||||||
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
|
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
|
||||||
</div>
|
</div>
|
||||||
@ -234,19 +232,22 @@
|
|||||||
on:change={(e) => (resultQuery = e.detail)}
|
on:change={(e) => (resultQuery = e.detail)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if viewlet}
|
{#if loading}
|
||||||
{#if loading}
|
<Loading />
|
||||||
<Loading />
|
{:else if viewlet && viewlet?.$lookup?.descriptor?.component}
|
||||||
{:else}
|
<Component
|
||||||
<TableBrowser
|
is={viewlet.$lookup.descriptor.component}
|
||||||
_class={recruit.mixin.VacancyList}
|
props={{
|
||||||
config={createConfig(viewlet, preference)}
|
_class: recruit.mixin.VacancyList,
|
||||||
options={viewlet.options}
|
options: viewlet.options,
|
||||||
query={{
|
config: finalConfig,
|
||||||
|
viewlet,
|
||||||
|
viewOptions,
|
||||||
|
viewOptionsConfig: viewlet.viewOptions?.other,
|
||||||
|
query: {
|
||||||
...resultQuery
|
...resultQuery
|
||||||
}}
|
},
|
||||||
totalQuery={{}}
|
totalQuery: {}
|
||||||
showNotification
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Class, Doc, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core'
|
import { Class, Doc, DocumentQuery, FindOptions, Ref, Space, RateLimitter } from '@hcengineering/core'
|
||||||
import { IntlString, getResource } from '@hcengineering/platform'
|
import { IntlString, getResource } from '@hcengineering/platform'
|
||||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
|
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
|
||||||
@ -43,11 +43,16 @@
|
|||||||
|
|
||||||
export let documents: Doc[] | undefined = undefined
|
export let documents: Doc[] | undefined = undefined
|
||||||
|
|
||||||
|
const limiter = new RateLimitter(() => ({ rate: 10 }))
|
||||||
|
|
||||||
let docs: Doc[] = []
|
let docs: Doc[] = []
|
||||||
|
let fastDocs: Doc[] = []
|
||||||
|
let slowDocs: Doc[] = []
|
||||||
|
|
||||||
$: orderBy = viewOptions.orderBy
|
$: orderBy = viewOptions.orderBy
|
||||||
|
|
||||||
const docsQuery = createQuery()
|
const docsQuery = createQuery()
|
||||||
|
const docsQuerySlow = createQuery()
|
||||||
|
|
||||||
$: lookup = buildConfigLookup(client.getHierarchy(), _class, config, options?.lookup)
|
$: lookup = buildConfigLookup(client.getHierarchy(), _class, config, options?.lookup)
|
||||||
$: resultOptions = { ...options, lookup, ...(orderBy !== undefined ? { sort: { [orderBy[0]]: orderBy[1] } } : {}) }
|
$: resultOptions = { ...options, lookup, ...(orderBy !== undefined ? { sort: { [orderBy[0]]: orderBy[1] } } : {}) }
|
||||||
@ -59,12 +64,35 @@
|
|||||||
|
|
||||||
$: queryNoLookup = noLookup(resultQuery)
|
$: queryNoLookup = noLookup(resultQuery)
|
||||||
|
|
||||||
|
let fastQueryIds: Ref<Doc>[] = []
|
||||||
$: if (documents === undefined) {
|
$: if (documents === undefined) {
|
||||||
docsQuery.query(
|
docsQuery.query(
|
||||||
_class,
|
_class,
|
||||||
queryNoLookup,
|
queryNoLookup,
|
||||||
(res) => {
|
(res) => {
|
||||||
docs = res
|
fastDocs = res
|
||||||
|
fastQueryIds = res.map((it) => it._id)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...resultOptions,
|
||||||
|
projection: {
|
||||||
|
...resultOptions.projection,
|
||||||
|
_id: 1,
|
||||||
|
_class: 1,
|
||||||
|
...getProjection(viewOptions.groupBy, queryNoLookup)
|
||||||
|
},
|
||||||
|
limit: 1000
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
docsQuery.unsubscribe()
|
||||||
|
}
|
||||||
|
$: if (fastQueryIds.length > 0) {
|
||||||
|
docsQuerySlow.query(
|
||||||
|
_class,
|
||||||
|
{ ...queryNoLookup, _id: { $nin: fastQueryIds } },
|
||||||
|
(res) => {
|
||||||
|
slowDocs = res
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...resultOptions,
|
...resultOptions,
|
||||||
@ -77,7 +105,12 @@
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
docsQuery.unsubscribe()
|
docsQuerySlow.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (documents === undefined) {
|
||||||
|
docs = [...fastDocs, ...slowDocs]
|
||||||
|
} else {
|
||||||
docs = documents
|
docs = documents
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +203,7 @@
|
|||||||
{viewOptions}
|
{viewOptions}
|
||||||
{viewOptionsConfig}
|
{viewOptionsConfig}
|
||||||
{selectedObjectIds}
|
{selectedObjectIds}
|
||||||
|
{limiter}
|
||||||
level={0}
|
level={0}
|
||||||
groupPersistKey={''}
|
groupPersistKey={''}
|
||||||
{createItemDialog}
|
{createItemDialog}
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
FindOptions,
|
FindOptions,
|
||||||
generateId,
|
generateId,
|
||||||
Lookup,
|
Lookup,
|
||||||
|
RateLimitter,
|
||||||
Ref,
|
Ref,
|
||||||
Space
|
Space
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
@ -50,6 +51,7 @@
|
|||||||
import ListCategory from './ListCategory.svelte'
|
import ListCategory from './ListCategory.svelte'
|
||||||
|
|
||||||
export let docs: Doc[]
|
export let docs: Doc[]
|
||||||
|
export let docKeys: Partial<DocumentQuery<Doc>> = {}
|
||||||
export let _class: Ref<Class<Doc>>
|
export let _class: Ref<Class<Doc>>
|
||||||
export let space: Ref<Space> | undefined
|
export let space: Ref<Space> | undefined
|
||||||
export let query: DocumentQuery<Doc> | undefined
|
export let query: DocumentQuery<Doc> | undefined
|
||||||
@ -80,6 +82,7 @@
|
|||||||
|
|
||||||
export let resultQuery: DocumentQuery<Doc>
|
export let resultQuery: DocumentQuery<Doc>
|
||||||
export let resultOptions: FindOptions<Doc>
|
export let resultOptions: FindOptions<Doc>
|
||||||
|
export let limiter: RateLimitter
|
||||||
|
|
||||||
$: groupByKey = viewOptions.groupBy[level] ?? noCategory
|
$: groupByKey = viewOptions.groupBy[level] ?? noCategory
|
||||||
let categories: CategoryType[] = []
|
let categories: CategoryType[] = []
|
||||||
@ -138,7 +141,7 @@
|
|||||||
$: getHeader(_class, groupByKey)
|
$: getHeader(_class, groupByKey)
|
||||||
|
|
||||||
let updateCounter = 0
|
let updateCounter = 0
|
||||||
|
let configurationsVersion = 0
|
||||||
async function buildModels (
|
async function buildModels (
|
||||||
_class: Ref<Class<Doc>>,
|
_class: Ref<Class<Doc>>,
|
||||||
config: (string | BuildModelKey)[],
|
config: (string | BuildModelKey)[],
|
||||||
@ -161,6 +164,7 @@
|
|||||||
|
|
||||||
if (id === updateCounter) {
|
if (id === updateCounter) {
|
||||||
itemModels = newItemModels
|
itemModels = newItemModels
|
||||||
|
configurationsVersion = updateCounter
|
||||||
for (const [, v] of Object.entries(newItemModels)) {
|
for (const [, v] of Object.entries(newItemModels)) {
|
||||||
// itemModels = itemModels
|
// itemModels = itemModels
|
||||||
;(v as AttributeModel[]).forEach((m: AttributeModel) => {
|
;(v as AttributeModel[]).forEach((m: AttributeModel) => {
|
||||||
@ -341,6 +345,7 @@
|
|||||||
|
|
||||||
{#each categories as category, i (typeof category === 'object' ? category.name : category)}
|
{#each categories as category, i (typeof category === 'object' ? category.name : category)}
|
||||||
{@const items = groupByKey === noCategory ? docs : getGroupByValues(groupByDocs, category)}
|
{@const items = groupByKey === noCategory ? docs : getGroupByValues(groupByDocs, category)}
|
||||||
|
{@const categoryDocKeys = { ...docKeys, [groupByKey]: category }}
|
||||||
<ListCategory
|
<ListCategory
|
||||||
bind:this={listListCategory[i]}
|
bind:this={listListCategory[i]}
|
||||||
{extraHeaders}
|
{extraHeaders}
|
||||||
@ -355,14 +360,17 @@
|
|||||||
index={i}
|
index={i}
|
||||||
{config}
|
{config}
|
||||||
{configurations}
|
{configurations}
|
||||||
|
{configurationsVersion}
|
||||||
{itemModels}
|
{itemModels}
|
||||||
{_class}
|
{_class}
|
||||||
|
parentCategories={categories.length}
|
||||||
groupPersistKey={`${groupPersistKey}_${level}_${typeof category === 'object' ? category.name : category}`}
|
groupPersistKey={`${groupPersistKey}_${level}_${typeof category === 'object' ? category.name : category}`}
|
||||||
singleCat={level === 0 && categories.length === 1}
|
singleCat={level === 0 && categories.length === 1}
|
||||||
oneCat={viewOptions.groupBy.length === 1}
|
oneCat={viewOptions.groupBy.length === 1}
|
||||||
lastCat={i === categories.length - 1}
|
lastCat={i === categories.length - 1}
|
||||||
{category}
|
{category}
|
||||||
itemProj={items}
|
itemProj={items}
|
||||||
|
docKeys={categoryDocKeys}
|
||||||
{newObjectProps}
|
{newObjectProps}
|
||||||
{createItemDialog}
|
{createItemDialog}
|
||||||
{createItemDialogProps}
|
{createItemDialogProps}
|
||||||
@ -371,6 +379,7 @@
|
|||||||
{compactMode}
|
{compactMode}
|
||||||
{resultQuery}
|
{resultQuery}
|
||||||
{resultOptions}
|
{resultOptions}
|
||||||
|
{limiter}
|
||||||
on:check
|
on:check
|
||||||
on:uncheckAll
|
on:uncheckAll
|
||||||
on:row-focus
|
on:row-focus
|
||||||
@ -423,12 +432,14 @@
|
|||||||
{flatHeaders}
|
{flatHeaders}
|
||||||
{props}
|
{props}
|
||||||
{level}
|
{level}
|
||||||
|
docKeys={categoryDocKeys}
|
||||||
groupPersistKey={`${groupPersistKey}_${level}_${typeof category === 'object' ? category.name : category}`}
|
groupPersistKey={`${groupPersistKey}_${level}_${typeof category === 'object' ? category.name : category}`}
|
||||||
{initIndex}
|
{initIndex}
|
||||||
{viewOptionsConfig}
|
{viewOptionsConfig}
|
||||||
{listDiv}
|
{listDiv}
|
||||||
{resultQuery}
|
{resultQuery}
|
||||||
{resultOptions}
|
{resultOptions}
|
||||||
|
{limiter}
|
||||||
bind:dragItem
|
bind:dragItem
|
||||||
on:dragItem
|
on:dragItem
|
||||||
on:check
|
on:check
|
||||||
|
@ -20,8 +20,10 @@
|
|||||||
DocumentQuery,
|
DocumentQuery,
|
||||||
DocumentUpdate,
|
DocumentUpdate,
|
||||||
FindOptions,
|
FindOptions,
|
||||||
|
Hierarchy,
|
||||||
Lookup,
|
Lookup,
|
||||||
PrimitiveType,
|
PrimitiveType,
|
||||||
|
RateLimitter,
|
||||||
Ref,
|
Ref,
|
||||||
Space
|
Space
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
@ -37,7 +39,7 @@
|
|||||||
mouseAttractor,
|
mouseAttractor,
|
||||||
showPopup
|
showPopup
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { AttributeModel, BuildModelKey, Viewlet, ViewOptionModel, ViewOptions } from '@hcengineering/view'
|
import { AttributeModel, BuildModelKey, ViewOptionModel, ViewOptions, Viewlet } from '@hcengineering/view'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import { fade } from 'svelte/transition'
|
import { fade } from 'svelte/transition'
|
||||||
import { FocusSelection, focusStore } from '../../selection'
|
import { FocusSelection, focusStore } from '../../selection'
|
||||||
@ -54,6 +56,7 @@
|
|||||||
export let space: Ref<Space> | undefined
|
export let space: Ref<Space> | undefined
|
||||||
export let baseMenuClass: Ref<Class<Doc>> | undefined
|
export let baseMenuClass: Ref<Class<Doc>> | undefined
|
||||||
export let itemProj: Doc[]
|
export let itemProj: Doc[]
|
||||||
|
export let docKeys: Partial<DocumentQuery<Doc>> = {}
|
||||||
export let createItemDialog: AnyComponent | AnySvelteComponent | undefined
|
export let createItemDialog: AnyComponent | AnySvelteComponent | undefined
|
||||||
export let createItemDialogProps: Record<string, any> | undefined
|
export let createItemDialogProps: Record<string, any> | undefined
|
||||||
export let createItemLabel: IntlString | undefined
|
export let createItemLabel: IntlString | undefined
|
||||||
@ -68,6 +71,7 @@
|
|||||||
export let _class: Ref<Class<Doc>>
|
export let _class: Ref<Class<Doc>>
|
||||||
export let config: (string | BuildModelKey)[]
|
export let config: (string | BuildModelKey)[]
|
||||||
export let configurations: Record<Ref<Class<Doc>>, Viewlet['config']> | undefined
|
export let configurations: Record<Ref<Class<Doc>>, Viewlet['config']> | undefined
|
||||||
|
export let configurationsVersion: number
|
||||||
export let viewOptions: ViewOptions
|
export let viewOptions: ViewOptions
|
||||||
export let newObjectProps: (doc: Doc | undefined) => Record<string, any> | undefined
|
export let newObjectProps: (doc: Doc | undefined) => Record<string, any> | undefined
|
||||||
export let viewOptionsConfig: ViewOptionModel[] | undefined
|
export let viewOptionsConfig: ViewOptionModel[] | undefined
|
||||||
@ -81,6 +85,8 @@
|
|||||||
export let compactMode: boolean = false
|
export let compactMode: boolean = false
|
||||||
export let resultQuery: DocumentQuery<Doc>
|
export let resultQuery: DocumentQuery<Doc>
|
||||||
export let resultOptions: FindOptions<Doc>
|
export let resultOptions: FindOptions<Doc>
|
||||||
|
export let parentCategories: number = 0
|
||||||
|
export let limiter: RateLimitter
|
||||||
|
|
||||||
$: lastLevel = level + 1 >= viewOptions.groupBy.length
|
$: lastLevel = level + 1 >= viewOptions.groupBy.length
|
||||||
|
|
||||||
@ -95,14 +101,16 @@
|
|||||||
$: limit = initialLimit
|
$: limit = initialLimit
|
||||||
|
|
||||||
$: if (lastLevel) {
|
$: if (lastLevel) {
|
||||||
docsQuery.query(
|
limiter.add(async () => {
|
||||||
_class,
|
docsQuery.query(
|
||||||
{ ...resultQuery, _id: { $in: itemProj.map((it) => it._id) } },
|
_class,
|
||||||
(res) => {
|
{ ...resultQuery, ...docKeys },
|
||||||
items = res
|
(res) => {
|
||||||
},
|
items = res
|
||||||
{ ...resultOptions, limit: limit ?? 200 }
|
},
|
||||||
)
|
{ ...resultOptions, limit: limit ?? 200 }
|
||||||
|
)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
docsQuery.unsubscribe()
|
docsQuery.unsubscribe()
|
||||||
}
|
}
|
||||||
@ -120,13 +128,18 @@
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
function initCollapsed (singleCat: boolean, lastLevel: boolean): void {
|
function initCollapsed (singleCat: boolean, lastLevel: boolean, level: number): void {
|
||||||
if (localStorage.getItem(categoryCollapseKey) === null) {
|
if (localStorage.getItem(categoryCollapseKey) === null) {
|
||||||
collapsed = !disableHeader && !singleCat && itemProj.length > (lastLevel ? autoFoldLimit : singleCategoryLimit)
|
collapsed =
|
||||||
|
(!disableHeader &&
|
||||||
|
!singleCat &&
|
||||||
|
itemProj.length > (lastLevel ? autoFoldLimit : singleCategoryLimit) / (level + 1)) ||
|
||||||
|
parentCategories > 10 ||
|
||||||
|
(level > 1 && parentCategories > 5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: initCollapsed(singleCat, lastLevel)
|
$: initCollapsed(singleCat, lastLevel, level)
|
||||||
|
|
||||||
const handleRowFocused = (object: Doc) => {
|
const handleRowFocused = (object: Doc) => {
|
||||||
dispatch('row-focus', object)
|
dispatch('row-focus', object)
|
||||||
@ -463,6 +476,7 @@
|
|||||||
{createItemDialog}
|
{createItemDialog}
|
||||||
{createItemLabel}
|
{createItemLabel}
|
||||||
{viewOptions}
|
{viewOptions}
|
||||||
|
{docKeys}
|
||||||
newObjectProps={_newObjectProps}
|
newObjectProps={_newObjectProps}
|
||||||
{flatHeaders}
|
{flatHeaders}
|
||||||
{props}
|
{props}
|
||||||
@ -475,38 +489,40 @@
|
|||||||
/>
|
/>
|
||||||
{:else if itemModels && itemModels.size > 0 && (!collapsed || wasLoaded || dragItemIndex !== undefined)}
|
{:else if itemModels && itemModels.size > 0 && (!collapsed || wasLoaded || dragItemIndex !== undefined)}
|
||||||
{#if limited && !loading}
|
{#if limited && !loading}
|
||||||
{#each limited as docObject, i (docObject._id)}
|
{#key configurationsVersion}
|
||||||
<ListItem
|
{#each limited as docObject, i (docObject._id)}
|
||||||
bind:this={listItems[i]}
|
<ListItem
|
||||||
{docObject}
|
bind:this={listItems[i]}
|
||||||
model={getDocItemModel(docObject._class)}
|
{docObject}
|
||||||
{groupByKey}
|
model={getDocItemModel(Hierarchy.mixinOrClass(docObject))}
|
||||||
selected={isSelected(docObject, $focusStore)}
|
{groupByKey}
|
||||||
checked={selectedObjectIdsSet.has(docObject._id)}
|
selected={isSelected(docObject, $focusStore)}
|
||||||
last={i === limited.length - 1}
|
checked={selectedObjectIdsSet.has(docObject._id)}
|
||||||
lastCat={i === limited.length - 1 && (oneCat || lastCat)}
|
last={i === limited.length - 1}
|
||||||
on:dragstart={(e) => dragStart(e, docObject, i)}
|
lastCat={i === limited.length - 1 && (oneCat || lastCat)}
|
||||||
on:dragenter={(e) => {
|
on:dragstart={(e) => dragStart(e, docObject, i)}
|
||||||
if (dragItemIndex !== undefined) {
|
on:dragenter={(e) => {
|
||||||
e.stopPropagation()
|
if (dragItemIndex !== undefined) {
|
||||||
e.preventDefault()
|
e.stopPropagation()
|
||||||
}
|
e.preventDefault()
|
||||||
}}
|
}
|
||||||
on:dragleave={(e) => dragItemLeave(e, i)}
|
}}
|
||||||
on:dragover={(e) => dragover(e, i)}
|
on:dragleave={(e) => dragItemLeave(e, i)}
|
||||||
on:drop={dropItemHandle}
|
on:dragover={(e) => dragover(e, i)}
|
||||||
on:check={(ev) => dispatch('check', { docs: ev.detail.docs, value: ev.detail.value })}
|
on:drop={dropItemHandle}
|
||||||
on:contextmenu={(event) => handleMenuOpened(event, docObject)}
|
on:check={(ev) => dispatch('check', { docs: ev.detail.docs, value: ev.detail.value })}
|
||||||
on:focus={() => {}}
|
on:contextmenu={(event) => handleMenuOpened(event, docObject)}
|
||||||
on:mouseover={mouseAttractor(() => handleRowFocused(docObject))}
|
on:focus={() => {}}
|
||||||
on:mouseenter={mouseAttractor(() => handleRowFocused(docObject))}
|
on:mouseover={mouseAttractor(() => handleRowFocused(docObject))}
|
||||||
{props}
|
on:mouseenter={mouseAttractor(() => handleRowFocused(docObject))}
|
||||||
{compactMode}
|
{props}
|
||||||
on:on-mount={() => {
|
{compactMode}
|
||||||
wasLoaded = true
|
on:on-mount={() => {
|
||||||
}}
|
wasLoaded = true
|
||||||
/>
|
}}
|
||||||
{/each}
|
/>
|
||||||
|
{/each}
|
||||||
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
{:else if loading}
|
{:else if loading}
|
||||||
<Spinner size="small" />
|
<Spinner size="small" />
|
||||||
|
@ -111,9 +111,9 @@
|
|||||||
on:click={() => dispatch('collapse')}
|
on:click={() => dispatch('collapse')}
|
||||||
>
|
>
|
||||||
<div class="flex-row-center flex-grow" style:color={headerComponent ? headerTextColor : 'inherit'}>
|
<div class="flex-row-center flex-grow" style:color={headerComponent ? headerTextColor : 'inherit'}>
|
||||||
{#if level === 0}
|
<!-- {#if level === 0} -->
|
||||||
<div class="chevron"><IconCollapseArrow size={'small'} /></div>
|
<div class="chevron"><IconCollapseArrow size={level === 0 ? 'small' : 'tiny'} /></div>
|
||||||
{/if}
|
<!-- {/if} -->
|
||||||
{#if groupByKey === noCategory}
|
{#if groupByKey === noCategory}
|
||||||
<span class="text-base fs-bold overflow-label pointer-events-none">
|
<span class="text-base fs-bold overflow-label pointer-events-none">
|
||||||
<Label label={view.string.NoGrouping} />
|
<Label label={view.string.NoGrouping} />
|
||||||
|
@ -1,43 +1 @@
|
|||||||
/**
|
export { RateLimitter } from '@hcengineering/core'
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class RateLimitter {
|
|
||||||
idCounter: number = 0
|
|
||||||
processingQueue = new Map<string, Promise<void>>()
|
|
||||||
last: number = 0
|
|
||||||
|
|
||||||
queue: (() => Promise<void>)[] = []
|
|
||||||
|
|
||||||
constructor (readonly config: () => { rate: number, perSecond?: number }) {}
|
|
||||||
|
|
||||||
async exec<T, B extends Record<string, any> = {}>(op: (args?: B) => Promise<T>, args?: B): Promise<T> {
|
|
||||||
const processingId = `${this.idCounter++}`
|
|
||||||
const cfg = this.config()
|
|
||||||
|
|
||||||
while (this.processingQueue.size > cfg.rate) {
|
|
||||||
await Promise.race(this.processingQueue.values())
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const p = op(args)
|
|
||||||
this.processingQueue.set(processingId, p as Promise<void>)
|
|
||||||
return await p
|
|
||||||
} finally {
|
|
||||||
this.processingQueue.delete(processingId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async add<T, B extends Record<string, any> = {}>(op: (args?: B) => Promise<T>, args?: B): Promise<void> {
|
|
||||||
const cfg = this.config()
|
|
||||||
|
|
||||||
if (this.processingQueue.size < cfg.rate) {
|
|
||||||
void this.exec(op, args)
|
|
||||||
} else {
|
|
||||||
await this.exec(op, args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitProcessing (): Promise<void> {
|
|
||||||
await await Promise.race(this.processingQueue.values())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user