mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-11 21:11:57 +00:00
UBERF-5564: rework groupping and support PersonAccount (#5525)
Signed-off-by: Vyacheslav Tumanov <me@slavatumanov.me>
This commit is contained in:
parent
4496530fed
commit
7f2a8779c6
@ -507,6 +507,16 @@ export function createModel (builder: Builder): void {
|
|||||||
pinned: true
|
pinned: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
builder.mixin(core.class.Account, core.class.Class, view.mixin.Aggregation, {
|
||||||
|
createAggregationManager: contact.aggregation.CreatePersonAggregationManager,
|
||||||
|
setStoreFunc: contact.function.SetPersonStore,
|
||||||
|
filterFunc: contact.function.PersonFilterFunction
|
||||||
|
})
|
||||||
|
|
||||||
|
builder.mixin(core.class.Account, core.class.Class, view.mixin.Groupping, {
|
||||||
|
grouppingManager: contact.aggregation.GrouppingPersonManager
|
||||||
|
})
|
||||||
|
|
||||||
builder.mixin(contact.mixin.Employee, core.class.Class, view.mixin.ObjectEditor, {
|
builder.mixin(contact.mixin.Employee, core.class.Class, view.mixin.ObjectEditor, {
|
||||||
editor: contact.component.EditEmployee,
|
editor: contact.component.EditEmployee,
|
||||||
pinned: true
|
pinned: true
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import { contactId } from '@hcengineering/contact'
|
import { contactId } from '@hcengineering/contact'
|
||||||
import contact from '@hcengineering/contact-resources/src/plugin'
|
import contact from '@hcengineering/contact-resources/src/plugin'
|
||||||
import type { Client, Doc, Ref } from '@hcengineering/core'
|
import type { Client, Doc, DocManager, Ref } from '@hcengineering/core'
|
||||||
import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/model-presentation'
|
import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/model-presentation'
|
||||||
import { type NotificationGroup } from '@hcengineering/notification'
|
import { type NotificationGroup } from '@hcengineering/notification'
|
||||||
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
|
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
|
||||||
@ -139,6 +139,8 @@ export default mergeIds(contactId, contact, {
|
|||||||
GetContactLastName: '' as Resource<TemplateFieldFunc>,
|
GetContactLastName: '' as Resource<TemplateFieldFunc>,
|
||||||
ContactTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
|
ContactTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
|
||||||
ChannelTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
|
ChannelTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
|
||||||
ChannelIdentifierProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
|
ChannelIdentifierProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
|
||||||
|
SetPersonStore: '' as Resource<(manager: DocManager<any>) => void>,
|
||||||
|
PersonFilterFunction: '' as Resource<(doc: Doc, target: Doc) => boolean>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -166,7 +166,7 @@ export class TSpacesTypeData extends TSpace implements RolesAssignment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Model(core.class.Account, core.class.Doc, DOMAIN_MODEL)
|
@Model(core.class.Account, core.class.Doc, DOMAIN_MODEL)
|
||||||
@UX(core.string.Account)
|
@UX(core.string.Account, undefined, undefined, 'name')
|
||||||
export class TAccount extends TDoc implements Account {
|
export class TAccount extends TDoc implements Account {
|
||||||
email!: string
|
email!: string
|
||||||
role!: AccountRole
|
role!: AccountRole
|
||||||
|
@ -94,7 +94,13 @@ function defineSortAndGrouping (builder: Builder): void {
|
|||||||
})
|
})
|
||||||
|
|
||||||
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.Aggregation, {
|
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.Aggregation, {
|
||||||
createAggregationManager: tracker.aggregation.CreateComponentAggregationManager
|
createAggregationManager: tracker.aggregation.CreateComponentAggregationManager,
|
||||||
|
setStoreFunc: tracker.function.SetComponentStore,
|
||||||
|
filterFunc: tracker.function.ComponentFilterFunction
|
||||||
|
})
|
||||||
|
|
||||||
|
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.Groupping, {
|
||||||
|
grouppingManager: tracker.aggregation.GrouppingComponentManager
|
||||||
})
|
})
|
||||||
|
|
||||||
builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AllValuesFunc, {
|
builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AllValuesFunc, {
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
//
|
//
|
||||||
import { type DocUpdateMessageViewlet } from '@hcengineering/activity'
|
import { type DocUpdateMessageViewlet } from '@hcengineering/activity'
|
||||||
import { type ChatMessageViewlet } from '@hcengineering/chunter'
|
import { type ChatMessageViewlet } from '@hcengineering/chunter'
|
||||||
import { type StatusCategory, type Doc, type Ref } from '@hcengineering/core'
|
import { type StatusCategory, type Doc, type Ref, type DocManager } from '@hcengineering/core'
|
||||||
import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/model-presentation'
|
import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/model-presentation'
|
||||||
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
|
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
|
||||||
import { mergeIds, type IntlString, type Resource } from '@hcengineering/platform'
|
import { mergeIds, type IntlString, type Resource } from '@hcengineering/platform'
|
||||||
@ -119,5 +119,10 @@ export default mergeIds(trackerId, tracker, {
|
|||||||
Started: '' as Ref<StatusCategory>,
|
Started: '' as Ref<StatusCategory>,
|
||||||
Completed: '' as Ref<StatusCategory>,
|
Completed: '' as Ref<StatusCategory>,
|
||||||
Canceled: '' as Ref<StatusCategory>
|
Canceled: '' as Ref<StatusCategory>
|
||||||
|
},
|
||||||
|
|
||||||
|
function: {
|
||||||
|
SetComponentStore: '' as Resource<(manager: DocManager<any>) => void>,
|
||||||
|
ComponentFilterFunction: '' as Resource<(doc: Doc, target: Doc) => boolean>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
DOMAIN_MODEL,
|
DOMAIN_MODEL,
|
||||||
type Data,
|
type Data,
|
||||||
type Doc,
|
type Doc,
|
||||||
|
type DocManager,
|
||||||
type DocumentQuery,
|
type DocumentQuery,
|
||||||
type Domain,
|
type Domain,
|
||||||
type Ref,
|
type Ref,
|
||||||
@ -274,6 +275,8 @@ export class TGroupping extends TClass implements Groupping {
|
|||||||
@Mixin(view.mixin.Aggregation, core.class.Class)
|
@Mixin(view.mixin.Aggregation, core.class.Class)
|
||||||
export class TAggregation extends TClass implements Aggregation {
|
export class TAggregation extends TClass implements Aggregation {
|
||||||
createAggregationManager!: CreateAggregationManagerFunc
|
createAggregationManager!: CreateAggregationManagerFunc
|
||||||
|
setStoreFunc!: Resource<(manager: DocManager<any>) => void>
|
||||||
|
filterFunc!: Resource<(doc: Doc, target: Doc) => boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mixin(view.mixin.ObjectIcon, core.class.Class)
|
@Mixin(view.mixin.ObjectIcon, core.class.Class)
|
||||||
|
@ -315,29 +315,36 @@ export class AggregateValue {
|
|||||||
*/
|
*/
|
||||||
export type CategoryType = number | string | undefined | Ref<Doc> | AggregateValue
|
export type CategoryType = number | string | undefined | Ref<Doc> | AggregateValue
|
||||||
|
|
||||||
|
export interface IDocManager<T extends Doc> {
|
||||||
|
get: (ref: Ref<T>) => T | undefined
|
||||||
|
getDocs: () => T[]
|
||||||
|
getIdMap: () => IdMap<T>
|
||||||
|
filter: (predicate: (value: T) => boolean) => T[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export class DocManager {
|
export class DocManager<T extends Doc> implements IDocManager<T> {
|
||||||
protected readonly byId: IdMap<Doc>
|
protected readonly byId: IdMap<T>
|
||||||
|
|
||||||
constructor (protected readonly docs: Doc[]) {
|
constructor (protected readonly docs: T[]) {
|
||||||
this.byId = toIdMap(docs)
|
this.byId = toIdMap(docs)
|
||||||
}
|
}
|
||||||
|
|
||||||
get (ref: Ref<Doc>): Doc | undefined {
|
get (ref: Ref<T>): T | undefined {
|
||||||
return this.byId.get(ref)
|
return this.byId.get(ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
getDocs (): Doc[] {
|
getDocs (): T[] {
|
||||||
return this.docs
|
return this.docs
|
||||||
}
|
}
|
||||||
|
|
||||||
getIdMap (): IdMap<Doc> {
|
getIdMap (): IdMap<T> {
|
||||||
return this.byId
|
return this.byId
|
||||||
}
|
}
|
||||||
|
|
||||||
filter (predicate: (value: Doc) => boolean): Doc[] {
|
filter (predicate: (value: T) => boolean): T[] {
|
||||||
return this.docs.filter(predicate)
|
return this.docs.filter(predicate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,8 +74,15 @@
|
|||||||
let limitedObjects: IdMap<DocWithRank> = new Map()
|
let limitedObjects: IdMap<DocWithRank> = new Map()
|
||||||
|
|
||||||
const docQuery = createQuery()
|
const docQuery = createQuery()
|
||||||
|
$: groupQuery = {
|
||||||
$: groupQuery = { ...query, [groupByKey]: typeof state === 'object' ? { $in: state.values } : state }
|
...query,
|
||||||
|
[groupByKey]:
|
||||||
|
typeof state === 'object'
|
||||||
|
? state.name !== undefined
|
||||||
|
? { $in: state.values.flatMap((x) => x._id) }
|
||||||
|
: undefined
|
||||||
|
: state
|
||||||
|
}
|
||||||
|
|
||||||
$: void limiter.add(async () => {
|
$: void limiter.add(async () => {
|
||||||
docQuery.query(
|
docQuery.query(
|
||||||
|
@ -15,12 +15,13 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PersonAccount } from '@hcengineering/contact'
|
import { PersonAccount } from '@hcengineering/contact'
|
||||||
import { Ref } from '@hcengineering/core'
|
import { AggregateValue, Ref } from '@hcengineering/core'
|
||||||
import { IconSize } from '@hcengineering/ui'
|
import { IconSize } from '@hcengineering/ui'
|
||||||
import { personAccountByIdStore } from '../utils'
|
import { personAccountByIdStore } from '../utils'
|
||||||
import PersonAccountPresenter from './PersonAccountPresenter.svelte'
|
import PersonAccountPresenter from './PersonAccountPresenter.svelte'
|
||||||
|
import { personStore } from '..'
|
||||||
|
|
||||||
export let value: Ref<PersonAccount>
|
export let value: Ref<PersonAccount> | AggregateValue
|
||||||
export let avatarSize: IconSize = 'x-small'
|
export let avatarSize: IconSize = 'x-small'
|
||||||
export let shouldShowAvatar: boolean = true
|
export let shouldShowAvatar: boolean = true
|
||||||
export let shouldShowName: boolean = true
|
export let shouldShowName: boolean = true
|
||||||
@ -30,7 +31,8 @@
|
|||||||
export let noUnderline: boolean = false
|
export let noUnderline: boolean = false
|
||||||
export let compact = false
|
export let compact = false
|
||||||
|
|
||||||
$: account = $personAccountByIdStore.get(value)
|
$: _value = $personStore.get(typeof value === 'string' ? value : (value?.values?.[0]?._id as Ref<PersonAccount>))
|
||||||
|
$: account = $personAccountByIdStore.get(_value?._id ?? (value as Ref<PersonAccount>))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if account}
|
{#if account}
|
||||||
|
@ -15,14 +15,16 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type Channel,
|
||||||
|
type AvatarInfo,
|
||||||
|
type Contact,
|
||||||
getGravatarUrl,
|
getGravatarUrl,
|
||||||
getName,
|
getName,
|
||||||
type AvatarInfo,
|
type Person,
|
||||||
type Channel,
|
type PersonAccount
|
||||||
type Contact,
|
|
||||||
type Person
|
|
||||||
} from '@hcengineering/contact'
|
} from '@hcengineering/contact'
|
||||||
import {
|
import {
|
||||||
|
DocManager,
|
||||||
type Class,
|
type Class,
|
||||||
type Client,
|
type Client,
|
||||||
type Data,
|
type Data,
|
||||||
@ -120,8 +122,9 @@ import NameChangedActivityMessage from './components/activity/NameChangedActivit
|
|||||||
import IconAddMember from './components/icons/AddMember.svelte'
|
import IconAddMember from './components/icons/AddMember.svelte'
|
||||||
import ExpandRightDouble from './components/icons/ExpandRightDouble.svelte'
|
import ExpandRightDouble from './components/icons/ExpandRightDouble.svelte'
|
||||||
import IconMembers from './components/icons/Members.svelte'
|
import IconMembers from './components/icons/Members.svelte'
|
||||||
|
import { AggregationManager } from '@hcengineering/view-resources'
|
||||||
|
|
||||||
import { get } from 'svelte/store'
|
import { get, writable } from 'svelte/store'
|
||||||
import contact from './plugin'
|
import contact from './plugin'
|
||||||
import {
|
import {
|
||||||
channelIdentifierProvider,
|
channelIdentifierProvider,
|
||||||
@ -140,6 +143,7 @@ import {
|
|||||||
getCurrentEmployeeName,
|
getCurrentEmployeeName,
|
||||||
getCurrentEmployeePosition,
|
getCurrentEmployeePosition,
|
||||||
getPersonTooltip,
|
getPersonTooltip,
|
||||||
|
grouppingPersonManager,
|
||||||
resolveLocation
|
resolveLocation
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
|
||||||
@ -293,6 +297,16 @@ async function openChannelURL (doc: Channel): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterPerson (doc: PersonAccount, target: PersonAccount): boolean {
|
||||||
|
return doc.person === target.person && doc._id !== target._id
|
||||||
|
}
|
||||||
|
|
||||||
|
export const personStore = writable<DocManager<PersonAccount>>(new DocManager([]))
|
||||||
|
|
||||||
|
function setStore (manager: DocManager<PersonAccount>): void {
|
||||||
|
personStore.set(manager)
|
||||||
|
}
|
||||||
|
|
||||||
export interface PersonLabelTooltip {
|
export interface PersonLabelTooltip {
|
||||||
personLabel?: IntlString
|
personLabel?: IntlString
|
||||||
placeholderLabel?: IntlString
|
placeholderLabel?: IntlString
|
||||||
@ -431,9 +445,16 @@ export default async (): Promise<Resources> => ({
|
|||||||
ContactTitleProvider: contactTitleProvider,
|
ContactTitleProvider: contactTitleProvider,
|
||||||
PersonTooltipProvider: getPersonTooltip,
|
PersonTooltipProvider: getPersonTooltip,
|
||||||
ChannelTitleProvider: channelTitleProvider,
|
ChannelTitleProvider: channelTitleProvider,
|
||||||
ChannelIdentifierProvider: channelIdentifierProvider
|
ChannelIdentifierProvider: channelIdentifierProvider,
|
||||||
|
SetPersonStore: setStore,
|
||||||
|
PersonFilterFunction: filterPerson
|
||||||
},
|
},
|
||||||
resolver: {
|
resolver: {
|
||||||
Location: resolveLocation
|
Location: resolveLocation
|
||||||
|
},
|
||||||
|
aggregation: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
|
CreatePersonAggregationManager: AggregationManager.create,
|
||||||
|
GrouppingPersonManager: grouppingPersonManager
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -18,7 +18,12 @@ import contact, { contactId } from '@hcengineering/contact'
|
|||||||
import { type Client, type Doc } from '@hcengineering/core'
|
import { type Client, type Doc } from '@hcengineering/core'
|
||||||
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
|
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
|
||||||
import { type LabelAndProps, type Location } from '@hcengineering/ui'
|
import { type LabelAndProps, type Location } from '@hcengineering/ui'
|
||||||
import { type FilterFunction, type SortFunc } from '@hcengineering/view'
|
import {
|
||||||
|
type CreateAggregationManagerFunc,
|
||||||
|
type GrouppingManagerResource,
|
||||||
|
type FilterFunction,
|
||||||
|
type SortFunc
|
||||||
|
} from '@hcengineering/view'
|
||||||
|
|
||||||
export default mergeIds(contactId, contact, {
|
export default mergeIds(contactId, contact, {
|
||||||
string: {
|
string: {
|
||||||
@ -86,5 +91,9 @@ export default mergeIds(contactId, contact, {
|
|||||||
FilterChannelHasMessagesResult: '' as FilterFunction,
|
FilterChannelHasMessagesResult: '' as FilterFunction,
|
||||||
FilterChannelHasNewMessagesResult: '' as FilterFunction,
|
FilterChannelHasNewMessagesResult: '' as FilterFunction,
|
||||||
PersonTooltipProvider: '' as Resource<(client: Client, doc?: Doc | null) => Promise<LabelAndProps | undefined>>
|
PersonTooltipProvider: '' as Resource<(client: Client, doc?: Doc | null) => Promise<LabelAndProps | undefined>>
|
||||||
|
},
|
||||||
|
aggregation: {
|
||||||
|
CreatePersonAggregationManager: '' as CreateAggregationManagerFunc,
|
||||||
|
GrouppingPersonManager: '' as GrouppingManagerResource
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -41,7 +41,13 @@ import core, {
|
|||||||
type Timestamp,
|
type Timestamp,
|
||||||
type TxOperations,
|
type TxOperations,
|
||||||
type UserStatus,
|
type UserStatus,
|
||||||
type WithLookup
|
type WithLookup,
|
||||||
|
AggregateValue,
|
||||||
|
type Space,
|
||||||
|
type Hierarchy,
|
||||||
|
type DocumentQuery,
|
||||||
|
AggregateValueData,
|
||||||
|
matchQuery
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
|
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
|
||||||
import { getEmbeddedLabel, getResource, translate } from '@hcengineering/platform'
|
import { getEmbeddedLabel, getResource, translate } from '@hcengineering/platform'
|
||||||
@ -55,11 +61,12 @@ import {
|
|||||||
type ResolvedLocation,
|
type ResolvedLocation,
|
||||||
type TabItem
|
type TabItem
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import view, { type Filter } from '@hcengineering/view'
|
import view, { type GrouppingManager, type Filter } from '@hcengineering/view'
|
||||||
import { FilterQuery, accessDeniedStore } from '@hcengineering/view-resources'
|
import { FilterQuery, accessDeniedStore } from '@hcengineering/view-resources'
|
||||||
import { derived, get, writable } from 'svelte/store'
|
import { derived, get, writable } from 'svelte/store'
|
||||||
|
|
||||||
import contact from './plugin'
|
import contact from './plugin'
|
||||||
|
import { personStore } from '.'
|
||||||
|
|
||||||
export function formatDate (dueDateMs: Timestamp): string {
|
export function formatDate (dueDateMs: Timestamp): string {
|
||||||
return new Date(dueDateMs).toLocaleString('default', {
|
return new Date(dueDateMs).toLocaleString('default', {
|
||||||
@ -431,3 +438,109 @@ export async function channelTitleProvider (client: Client, ref: Ref<Channel>, d
|
|||||||
|
|
||||||
return channel.value
|
return channel.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const grouppingPersonManager: GrouppingManager = {
|
||||||
|
groupByCategories: groupByPersonAccountCategories,
|
||||||
|
groupValues: groupPersonAccountValues,
|
||||||
|
groupValuesWithEmpty: groupPersonAccountValuesWithEmpty,
|
||||||
|
hasValue: hasPersonAccountValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function groupByPersonAccountCategories (categories: any[]): AggregateValue[] {
|
||||||
|
const mgr = get(personStore)
|
||||||
|
|
||||||
|
const existingCategories: AggregateValue[] = [new AggregateValue(undefined, [])]
|
||||||
|
const personMap = new Map<string, AggregateValue>()
|
||||||
|
|
||||||
|
const usedSpaces = new Set<Ref<Space>>()
|
||||||
|
const personAccountList: Array<WithLookup<PersonAccount>> = []
|
||||||
|
for (const v of categories) {
|
||||||
|
const personAccount = mgr.getIdMap().get(v)
|
||||||
|
if (personAccount !== undefined) {
|
||||||
|
personAccountList.push(personAccount)
|
||||||
|
usedSpaces.add(personAccount.space)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const personAccount of personAccountList) {
|
||||||
|
if (personAccount !== undefined) {
|
||||||
|
let fst = personMap.get(personAccount.person)
|
||||||
|
if (fst === undefined) {
|
||||||
|
const people = mgr
|
||||||
|
.getDocs()
|
||||||
|
.filter(
|
||||||
|
(it) => it.person === personAccount.person && (categories.includes(it._id) || usedSpaces.has(it.space))
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.email.localeCompare(b.email))
|
||||||
|
.map((it) => new AggregateValueData(it.person, it._id, it.space))
|
||||||
|
fst = new AggregateValue(personAccount.person, people)
|
||||||
|
personMap.set(personAccount.person, fst)
|
||||||
|
existingCategories.push(fst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return existingCategories
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function groupPersonAccountValues (val: Doc[], targets: Set<any>): Doc[] {
|
||||||
|
const values = val
|
||||||
|
const result: Doc[] = []
|
||||||
|
const unique = [...new Set(val.map((c) => (c as PersonAccount).person))]
|
||||||
|
unique.forEach((label, i) => {
|
||||||
|
let exists = false
|
||||||
|
values.forEach((c) => {
|
||||||
|
if ((c as PersonAccount).person === label) {
|
||||||
|
if (!exists) {
|
||||||
|
result[i] = c
|
||||||
|
exists = targets.has(c?._id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function hasPersonAccountValue (value: Doc | undefined | null, values: any[]): boolean {
|
||||||
|
const mgr = get(personStore)
|
||||||
|
const personSet = new Set(mgr.filter((it) => it.person === (value as PersonAccount)?.person).map((it) => it._id))
|
||||||
|
return values.some((it) => personSet.has(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function groupPersonAccountValuesWithEmpty (
|
||||||
|
hierarchy: Hierarchy,
|
||||||
|
_class: Ref<Class<Doc>>,
|
||||||
|
key: string,
|
||||||
|
query: DocumentQuery<Doc> | undefined
|
||||||
|
): Array<Ref<Doc>> {
|
||||||
|
const mgr = get(personStore)
|
||||||
|
let personAccountList = mgr.getDocs()
|
||||||
|
if (query !== undefined) {
|
||||||
|
const { [key]: st, space } = query
|
||||||
|
const resQuery: DocumentQuery<Doc> = {}
|
||||||
|
if (space !== undefined) {
|
||||||
|
resQuery.space = space
|
||||||
|
}
|
||||||
|
if (st !== undefined) {
|
||||||
|
resQuery._id = st
|
||||||
|
}
|
||||||
|
personAccountList = matchQuery<Doc>(personAccountList, resQuery, _class, hierarchy) as unknown as Array<
|
||||||
|
WithLookup<PersonAccount>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
return personAccountList.map((it) => it._id)
|
||||||
|
}
|
||||||
|
@ -16,104 +16,21 @@
|
|||||||
import {
|
import {
|
||||||
AggregateValue,
|
AggregateValue,
|
||||||
AggregateValueData,
|
AggregateValueData,
|
||||||
type AnyAttribute,
|
|
||||||
type Class,
|
type Class,
|
||||||
type Client,
|
|
||||||
type Doc,
|
type Doc,
|
||||||
|
DocManager,
|
||||||
type DocumentQuery,
|
type DocumentQuery,
|
||||||
type Hierarchy,
|
type Hierarchy,
|
||||||
type Ref,
|
type Ref,
|
||||||
SortingOrder,
|
|
||||||
type Space,
|
type Space,
|
||||||
type Tx,
|
|
||||||
type WithLookup,
|
type WithLookup,
|
||||||
matchQuery
|
matchQuery
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { LiveQuery } from '@hcengineering/query'
|
import { type Component } from '@hcengineering/tracker'
|
||||||
import tracker, { type Component, ComponentManager } from '@hcengineering/tracker'
|
import { type GrouppingManager } from '@hcengineering/view'
|
||||||
import { type AggregationManager, type GrouppingManager } from '@hcengineering/view'
|
|
||||||
import { get, writable } from 'svelte/store'
|
import { get, writable } from 'svelte/store'
|
||||||
|
|
||||||
export const componentStore = writable<ComponentManager>(new ComponentManager([]))
|
export const componentStore = writable<DocManager<Component>>(new DocManager([]))
|
||||||
|
|
||||||
/**
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export class ComponentAggregationManager implements AggregationManager {
|
|
||||||
docs: Doc[] | undefined
|
|
||||||
mgr: ComponentManager | Promise<ComponentManager> | undefined
|
|
||||||
query: (() => void) | undefined
|
|
||||||
lq: LiveQuery
|
|
||||||
lqCallback: () => void
|
|
||||||
|
|
||||||
private constructor (client: Client, lqCallback: () => void) {
|
|
||||||
this.lq = new LiveQuery(client)
|
|
||||||
this.lqCallback = lqCallback ?? (() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
static create (client: Client, lqCallback: () => void): ComponentAggregationManager {
|
|
||||||
return new ComponentAggregationManager(client, lqCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getManager (): Promise<ComponentManager> {
|
|
||||||
if (this.mgr !== undefined) {
|
|
||||||
if (this.mgr instanceof Promise) {
|
|
||||||
this.mgr = await this.mgr
|
|
||||||
}
|
|
||||||
return this.mgr
|
|
||||||
}
|
|
||||||
this.mgr = new Promise<ComponentManager>((resolve) => {
|
|
||||||
this.query = this.lq.query(
|
|
||||||
tracker.class.Component,
|
|
||||||
{},
|
|
||||||
(res) => {
|
|
||||||
const first = this.docs === undefined
|
|
||||||
this.docs = res
|
|
||||||
this.mgr = new ComponentManager(res)
|
|
||||||
componentStore.set(this.mgr)
|
|
||||||
if (!first) {
|
|
||||||
this.lqCallback()
|
|
||||||
}
|
|
||||||
resolve(this.mgr)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sort: {
|
|
||||||
label: SortingOrder.Ascending
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return await this.mgr
|
|
||||||
}
|
|
||||||
|
|
||||||
close (): void {
|
|
||||||
this.query?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
async notifyTx (...tx: Tx[]): Promise<void> {
|
|
||||||
await this.lq.tx(...tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
getAttrClass (): Ref<Class<Doc>> {
|
|
||||||
return tracker.class.Component
|
|
||||||
}
|
|
||||||
|
|
||||||
async categorize (target: Array<Ref<Doc>>, attr: AnyAttribute): Promise<Array<Ref<Doc>>> {
|
|
||||||
const mgr = await this.getManager()
|
|
||||||
for (const sid of [...target]) {
|
|
||||||
const c = mgr.getIdMap().get(sid as Ref<Component>) as WithLookup<Component>
|
|
||||||
if (c !== undefined) {
|
|
||||||
let components = mgr.getDocs()
|
|
||||||
components = components.filter(
|
|
||||||
(it) => it.label.toLowerCase().trim() === c.label.toLowerCase().trim() && it._id !== c._id
|
|
||||||
)
|
|
||||||
target.push(...components.map((it) => it._id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return target.filter((it, idx, arr) => arr.indexOf(it) === idx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -136,6 +53,7 @@ export function groupByComponentCategories (categories: any[]): AggregateValue[]
|
|||||||
|
|
||||||
const usedSpaces = new Set<Ref<Space>>()
|
const usedSpaces = new Set<Ref<Space>>()
|
||||||
const componentsList: Array<WithLookup<Component>> = []
|
const componentsList: Array<WithLookup<Component>> = []
|
||||||
|
// console.log('mgr docs', mgr.getDocs())
|
||||||
for (const v of categories) {
|
for (const v of categories) {
|
||||||
const component = mgr.getIdMap().get(v)
|
const component = mgr.getIdMap().get(v)
|
||||||
if (component !== undefined) {
|
if (component !== undefined) {
|
||||||
|
@ -30,12 +30,13 @@ import core, {
|
|||||||
type Ref,
|
type Ref,
|
||||||
type RelatedDocument,
|
type RelatedDocument,
|
||||||
type TxOperations,
|
type TxOperations,
|
||||||
|
type DocManager,
|
||||||
AccountRole
|
AccountRole
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import chunter, { type ChatMessage } from '@hcengineering/chunter'
|
import chunter, { type ChatMessage } from '@hcengineering/chunter'
|
||||||
import { type Status, translate, type Resources } from '@hcengineering/platform'
|
import { type Status, translate, type Resources } from '@hcengineering/platform'
|
||||||
import { getClient, MessageBox, type ObjectSearchResult } from '@hcengineering/presentation'
|
import { getClient, MessageBox, type ObjectSearchResult } from '@hcengineering/presentation'
|
||||||
import { type Issue, type Milestone, type Project } from '@hcengineering/tracker'
|
import { type Component, type Issue, type Milestone, type Project } from '@hcengineering/tracker'
|
||||||
import { getCurrentLocation, navigate, showPopup, themeStore } from '@hcengineering/ui'
|
import { getCurrentLocation, navigate, showPopup, themeStore } from '@hcengineering/ui'
|
||||||
import ComponentEditor from './components/components/ComponentEditor.svelte'
|
import ComponentEditor from './components/components/ComponentEditor.svelte'
|
||||||
import ComponentFilterValuePresenter from './components/components/ComponentFilterValuePresenter.svelte'
|
import ComponentFilterValuePresenter from './components/components/ComponentFilterValuePresenter.svelte'
|
||||||
@ -122,7 +123,7 @@ import ComponentSelector from './components/components/ComponentSelector.svelte'
|
|||||||
import IssueTemplatePresenter from './components/templates/IssueTemplatePresenter.svelte'
|
import IssueTemplatePresenter from './components/templates/IssueTemplatePresenter.svelte'
|
||||||
import IssueTemplates from './components/templates/IssueTemplates.svelte'
|
import IssueTemplates from './components/templates/IssueTemplates.svelte'
|
||||||
|
|
||||||
import { deleteObject, deleteObjects } from '@hcengineering/view-resources'
|
import { deleteObject, deleteObjects, AggregationManager } from '@hcengineering/view-resources'
|
||||||
import MoveAndDeleteMilestonePopup from './components/milestones/MoveAndDeleteMilestonePopup.svelte'
|
import MoveAndDeleteMilestonePopup from './components/milestones/MoveAndDeleteMilestonePopup.svelte'
|
||||||
import EditIssueTemplate from './components/templates/EditIssueTemplate.svelte'
|
import EditIssueTemplate from './components/templates/EditIssueTemplate.svelte'
|
||||||
import TemplateEstimationEditor from './components/templates/EstimationEditor.svelte'
|
import TemplateEstimationEditor from './components/templates/EstimationEditor.svelte'
|
||||||
@ -143,7 +144,7 @@ import {
|
|||||||
subIssueQuery
|
subIssueQuery
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
|
||||||
import { ComponentAggregationManager, grouppingComponentManager } from './component'
|
import { componentStore, grouppingComponentManager } from './component'
|
||||||
import PriorityIcon from './components/activity/PriorityIcon.svelte'
|
import PriorityIcon from './components/activity/PriorityIcon.svelte'
|
||||||
import StatusIcon from './components/activity/StatusIcon.svelte'
|
import StatusIcon from './components/activity/StatusIcon.svelte'
|
||||||
import DeleteComponentPresenter from './components/components/DeleteComponentPresenter.svelte'
|
import DeleteComponentPresenter from './components/components/DeleteComponentPresenter.svelte'
|
||||||
@ -591,6 +592,14 @@ export async function importTasks (tasks: File, space: Ref<Project>): Promise<vo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterComponents (doc: Component, target: Component): boolean {
|
||||||
|
return doc.label.toLowerCase().trim() === target.label.toLowerCase().trim() && doc._id !== target._id
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStore (manager: DocManager<Component>): void {
|
||||||
|
componentStore.set(manager)
|
||||||
|
}
|
||||||
|
|
||||||
export default async (): Promise<Resources> => ({
|
export default async (): Promise<Resources> => ({
|
||||||
activity: {
|
activity: {
|
||||||
PriorityIcon,
|
PriorityIcon,
|
||||||
@ -710,7 +719,9 @@ export default async (): Promise<Resources> => ({
|
|||||||
GetVisibleFilters: getVisibleFilters,
|
GetVisibleFilters: getVisibleFilters,
|
||||||
IssueChatTitleProvider: getIssueChatTitle,
|
IssueChatTitleProvider: getIssueChatTitle,
|
||||||
IsProjectJoined: async (project: Project) => project.members.includes(getCurrentAccount()._id),
|
IsProjectJoined: async (project: Project) => project.members.includes(getCurrentAccount()._id),
|
||||||
GetIssueStatusCategories: getIssueStatusCategories
|
GetIssueStatusCategories: getIssueStatusCategories,
|
||||||
|
SetComponentStore: setStore,
|
||||||
|
ComponentFilterFunction: filterComponents
|
||||||
},
|
},
|
||||||
actionImpl: {
|
actionImpl: {
|
||||||
Move: move,
|
Move: move,
|
||||||
@ -726,7 +737,7 @@ export default async (): Promise<Resources> => ({
|
|||||||
},
|
},
|
||||||
aggregation: {
|
aggregation: {
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
CreateComponentAggregationManager: ComponentAggregationManager.create,
|
CreateComponentAggregationManager: AggregationManager.create,
|
||||||
GrouppingComponentManager: grouppingComponentManager
|
GrouppingComponentManager: grouppingComponentManager
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -21,8 +21,6 @@ import {
|
|||||||
CollectionSize,
|
CollectionSize,
|
||||||
Data,
|
Data,
|
||||||
Doc,
|
Doc,
|
||||||
DocManager,
|
|
||||||
IdMap,
|
|
||||||
Markup,
|
Markup,
|
||||||
Mixin,
|
Mixin,
|
||||||
Ref,
|
Ref,
|
||||||
@ -30,8 +28,7 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Status,
|
Status,
|
||||||
Timestamp,
|
Timestamp,
|
||||||
Type,
|
Type
|
||||||
WithLookup
|
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { Asset, IntlString, Plugin, Resource, plugin } from '@hcengineering/platform'
|
import { Asset, IntlString, Plugin, Resource, plugin } from '@hcengineering/platform'
|
||||||
import { Preference } from '@hcengineering/preference'
|
import { Preference } from '@hcengineering/preference'
|
||||||
@ -351,29 +348,6 @@ export interface Component extends Doc {
|
|||||||
attachments?: number
|
attachments?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* Allow to query for status keys/values.
|
|
||||||
*/
|
|
||||||
export class ComponentManager extends DocManager {
|
|
||||||
get (ref: Ref<WithLookup<Component>>): WithLookup<Component> | undefined {
|
|
||||||
return this.getIdMap().get(ref) as WithLookup<Component>
|
|
||||||
}
|
|
||||||
|
|
||||||
getDocs (): Array<WithLookup<Component>> {
|
|
||||||
return this.docs as Component[]
|
|
||||||
}
|
|
||||||
|
|
||||||
getIdMap (): IdMap<WithLookup<Component>> {
|
|
||||||
return this.byId as IdMap<WithLookup<Component>>
|
|
||||||
}
|
|
||||||
|
|
||||||
filter (predicate: (value: Component) => boolean): Component[] {
|
|
||||||
return this.getDocs().filter(predicate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
|
@ -97,6 +97,7 @@
|
|||||||
queryNoLookup,
|
queryNoLookup,
|
||||||
(res) => {
|
(res) => {
|
||||||
fastDocs = res
|
fastDocs = res
|
||||||
|
// console.log('query, res', queryNoLookup, res)
|
||||||
fastQueryIds = new Set(res.map((it) => it._id))
|
fastQueryIds = new Set(res.map((it) => it._id))
|
||||||
},
|
},
|
||||||
{ ...categoryQueryOptions, limit: 1000 }
|
{ ...categoryQueryOptions, limit: 1000 }
|
||||||
|
@ -349,130 +349,149 @@
|
|||||||
|
|
||||||
const listCategory: SvelteComponentTyped[] = []
|
const listCategory: SvelteComponentTyped[] = []
|
||||||
const listListCategory: ListCategory[] = []
|
const listListCategory: ListCategory[] = []
|
||||||
|
function getGroupByKey (
|
||||||
|
docKeys: Partial<DocumentQuery<Doc<Space>>>,
|
||||||
|
category: CategoryType,
|
||||||
|
resultQuery: DocumentQuery<Doc<Space>>
|
||||||
|
): Partial<DocumentQuery<Doc>> {
|
||||||
|
return {
|
||||||
|
...docKeys,
|
||||||
|
[groupByKey]:
|
||||||
|
typeof category === 'object'
|
||||||
|
? category.name !== undefined
|
||||||
|
? { $in: category.values.flatMap((x) => x._id) }
|
||||||
|
: resultQuery[groupByKey]?.$in?.length !== 0
|
||||||
|
? undefined
|
||||||
|
: []
|
||||||
|
: category
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#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 }}
|
{@const categoryDocKeys = getGroupByKey(docKeys, category, resultQuery)}
|
||||||
<ListCategory
|
{#if items.length !== 0}
|
||||||
bind:this={listListCategory[i]}
|
<ListCategory
|
||||||
{extraHeaders}
|
bind:this={listListCategory[i]}
|
||||||
{space}
|
{extraHeaders}
|
||||||
{selectedObjectIds}
|
{space}
|
||||||
{headerComponent}
|
{selectedObjectIds}
|
||||||
{baseMenuClass}
|
{headerComponent}
|
||||||
{level}
|
{baseMenuClass}
|
||||||
{viewOptions}
|
{level}
|
||||||
{groupByKey}
|
{viewOptions}
|
||||||
{lookup}
|
{groupByKey}
|
||||||
{config}
|
{lookup}
|
||||||
{configurations}
|
{config}
|
||||||
{configurationsVersion}
|
{configurations}
|
||||||
{itemModels}
|
{configurationsVersion}
|
||||||
{_class}
|
{itemModels}
|
||||||
parentCategories={categories.length}
|
{_class}
|
||||||
groupPersistKey={`${groupPersistKey}_${level}_${typeof category === 'object' ? category.name : category}`}
|
parentCategories={categories.length}
|
||||||
singleCat={level === 0 && categories.length === 1}
|
groupPersistKey={`${groupPersistKey}_${level}_${typeof category === 'object' ? category.name : category}`}
|
||||||
oneCat={viewOptions.groupBy.length === 1}
|
singleCat={level === 0 && categories.length === 1}
|
||||||
lastCat={i === categories.length - 1}
|
oneCat={viewOptions.groupBy.length === 1}
|
||||||
{category}
|
lastCat={i === categories.length - 1}
|
||||||
itemProj={items}
|
{category}
|
||||||
docKeys={categoryDocKeys}
|
itemProj={items}
|
||||||
{newObjectProps}
|
docKeys={categoryDocKeys}
|
||||||
{createItemDialog}
|
{newObjectProps}
|
||||||
{createItemDialogProps}
|
{createItemDialog}
|
||||||
{createItemLabel}
|
{createItemDialogProps}
|
||||||
{viewOptionsConfig}
|
{createItemLabel}
|
||||||
{compactMode}
|
{viewOptionsConfig}
|
||||||
{resultQuery}
|
{compactMode}
|
||||||
{resultOptions}
|
{resultQuery}
|
||||||
{limiter}
|
{resultOptions}
|
||||||
{listProvider}
|
{limiter}
|
||||||
on:check
|
{listProvider}
|
||||||
on:uncheckAll
|
on:check
|
||||||
on:row-focus
|
on:uncheckAll
|
||||||
on:dragstart={(e) => {
|
on:row-focus
|
||||||
dispatch('dragstart', {
|
on:dragstart={(e) => {
|
||||||
target: e.detail.target,
|
dispatch('dragstart', {
|
||||||
index: e.detail.index + getInitIndex(categories, i)
|
target: e.detail.target,
|
||||||
})
|
index: e.detail.index + getInitIndex(categories, i)
|
||||||
}}
|
})
|
||||||
on:collapsed
|
}}
|
||||||
{flatHeaders}
|
on:collapsed
|
||||||
{disableHeader}
|
{flatHeaders}
|
||||||
{props}
|
{disableHeader}
|
||||||
{listDiv}
|
{props}
|
||||||
bind:dragItem
|
{listDiv}
|
||||||
>
|
bind:dragItem
|
||||||
<svelte:fragment
|
|
||||||
slot="category"
|
|
||||||
let:docs
|
|
||||||
let:_class
|
|
||||||
let:space
|
|
||||||
let:lookup
|
|
||||||
let:baseMenuClass
|
|
||||||
let:config
|
|
||||||
let:selectedObjectIds
|
|
||||||
let:createItemDialog
|
|
||||||
let:createItemLabel
|
|
||||||
let:viewOptions
|
|
||||||
let:newObjectProps
|
|
||||||
let:flatHeaders
|
|
||||||
let:props
|
|
||||||
let:level
|
|
||||||
let:viewOptionsConfig
|
|
||||||
let:listDiv
|
|
||||||
let:dragstart
|
|
||||||
>
|
>
|
||||||
<svelte:self
|
<svelte:fragment
|
||||||
{docs}
|
slot="category"
|
||||||
bind:this={listCategory[i]}
|
let:docs
|
||||||
{_class}
|
let:_class
|
||||||
{space}
|
let:space
|
||||||
{lookup}
|
let:lookup
|
||||||
{baseMenuClass}
|
let:baseMenuClass
|
||||||
{config}
|
let:config
|
||||||
{selectedObjectIds}
|
let:selectedObjectIds
|
||||||
{createItemDialog}
|
let:createItemDialog
|
||||||
{createItemLabel}
|
let:createItemLabel
|
||||||
{viewOptions}
|
let:viewOptions
|
||||||
{newObjectProps}
|
let:newObjectProps
|
||||||
{flatHeaders}
|
let:flatHeaders
|
||||||
{props}
|
let:props
|
||||||
{level}
|
let:level
|
||||||
docKeys={categoryDocKeys}
|
let:viewOptionsConfig
|
||||||
groupPersistKey={`${groupPersistKey}_${level}_${typeof category === 'object' ? category.name : category}`}
|
let:listDiv
|
||||||
{initIndex}
|
let:dragstart
|
||||||
{viewOptionsConfig}
|
>
|
||||||
{listDiv}
|
<svelte:self
|
||||||
{resultQuery}
|
{docs}
|
||||||
{resultOptions}
|
bind:this={listCategory[i]}
|
||||||
{limiter}
|
{_class}
|
||||||
{listProvider}
|
{space}
|
||||||
bind:dragItem
|
{lookup}
|
||||||
on:dragItem
|
{baseMenuClass}
|
||||||
on:check
|
{config}
|
||||||
on:uncheckAll
|
{selectedObjectIds}
|
||||||
on:row-focus
|
{createItemDialog}
|
||||||
on:dragstart={dragstart}
|
{createItemLabel}
|
||||||
on:select={(evt) => {
|
{viewOptions}
|
||||||
select(0, evt.detail)
|
{newObjectProps}
|
||||||
}}
|
{flatHeaders}
|
||||||
on:select-next={(evt) => {
|
{props}
|
||||||
if (level !== 0) {
|
{level}
|
||||||
dispatch('select-next', evt.detail)
|
docKeys={categoryDocKeys}
|
||||||
} else {
|
groupPersistKey={`${groupPersistKey}_${level}_${typeof category === 'object' ? category.name : category}`}
|
||||||
select(2, evt.detail)
|
{initIndex}
|
||||||
}
|
{viewOptionsConfig}
|
||||||
}}
|
{listDiv}
|
||||||
on:select-prev={(evt) => {
|
{resultQuery}
|
||||||
if (level !== 0) {
|
{resultOptions}
|
||||||
dispatch('select-prev', evt.detail)
|
{limiter}
|
||||||
} else {
|
{listProvider}
|
||||||
select(-2, evt.detail)
|
bind:dragItem
|
||||||
}
|
on:dragItem
|
||||||
}}
|
on:check
|
||||||
/>
|
on:uncheckAll
|
||||||
</svelte:fragment>
|
on:row-focus
|
||||||
</ListCategory>
|
on:dragstart={dragstart}
|
||||||
|
on:select={(evt) => {
|
||||||
|
select(0, evt.detail)
|
||||||
|
}}
|
||||||
|
on:select-next={(evt) => {
|
||||||
|
if (level !== 0) {
|
||||||
|
dispatch('select-next', evt.detail)
|
||||||
|
} else {
|
||||||
|
select(2, evt.detail)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:select-prev={(evt) => {
|
||||||
|
if (level !== 0) {
|
||||||
|
dispatch('select-prev', evt.detail)
|
||||||
|
} else {
|
||||||
|
select(-2, evt.detail)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</svelte:fragment>
|
||||||
|
</ListCategory>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -20,7 +20,7 @@ import core, {
|
|||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { getResource, translate } from '@hcengineering/platform'
|
import { getResource, translate } from '@hcengineering/platform'
|
||||||
import { BasePresentationMiddleware, type PresentationMiddleware } from '@hcengineering/presentation'
|
import { BasePresentationMiddleware, type PresentationMiddleware } from '@hcengineering/presentation'
|
||||||
import view, { type AggregationManager } from '@hcengineering/view'
|
import view, { type IAggregationManager } from '@hcengineering/view'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -39,7 +39,7 @@ export interface DocSubScriber<T extends Doc = Doc> {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export class AggregationMiddleware extends BasePresentationMiddleware implements PresentationMiddleware {
|
export class AggregationMiddleware extends BasePresentationMiddleware implements PresentationMiddleware {
|
||||||
mgrs: Map<Ref<Class<Doc>>, AggregationManager> = new Map<Ref<Class<Doc>>, AggregationManager>()
|
mgrs: Map<Ref<Class<Doc>>, IAggregationManager<any>> = new Map<Ref<Class<Doc>>, IAggregationManager<any>>()
|
||||||
docs: Doc[] | undefined
|
docs: Doc[] | undefined
|
||||||
|
|
||||||
subscribers: Map<string, DocSubScriber> = new Map<string, DocSubScriber>()
|
subscribers: Map<string, DocSubScriber> = new Map<string, DocSubScriber>()
|
||||||
@ -121,17 +121,30 @@ export class AggregationMiddleware extends BasePresentationMiddleware implements
|
|||||||
return { unsubscribe: ret.unsubscribe }
|
return { unsubscribe: ret.unsubscribe }
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAggregationManager (_class: Ref<Class<Doc>>): Promise<AggregationManager | undefined> {
|
private async getAggregationManager (_class: Ref<Class<Doc>>): Promise<IAggregationManager<any> | undefined> {
|
||||||
let mgr = this.mgrs.get(_class)
|
let mgr = this.mgrs.get(_class)
|
||||||
|
|
||||||
if (mgr === undefined) {
|
if (mgr === undefined) {
|
||||||
const h = this.client.getHierarchy()
|
const h = this.client.getHierarchy()
|
||||||
const mixin = h.classHierarchyMixin(_class, view.mixin.Aggregation)
|
const mixin = h.classHierarchyMixin(_class, view.mixin.Aggregation)
|
||||||
if (mixin?.createAggregationManager !== undefined) {
|
if (
|
||||||
|
mixin?.createAggregationManager !== undefined &&
|
||||||
|
mixin?.setStoreFunc !== undefined &&
|
||||||
|
mixin?.filterFunc !== undefined &&
|
||||||
|
mixin?._class !== undefined
|
||||||
|
) {
|
||||||
const f = await getResource(mixin.createAggregationManager)
|
const f = await getResource(mixin.createAggregationManager)
|
||||||
mgr = f(this.client, () => {
|
const storeFunc = await getResource(mixin.setStoreFunc)
|
||||||
this.refreshSubscribers()
|
const filterFunc = await getResource(mixin.filterFunc)
|
||||||
})
|
mgr = f(
|
||||||
|
this.client,
|
||||||
|
() => {
|
||||||
|
this.refreshSubscribers()
|
||||||
|
},
|
||||||
|
storeFunc,
|
||||||
|
filterFunc,
|
||||||
|
_class
|
||||||
|
)
|
||||||
this.mgrs.set(_class, mgr)
|
this.mgrs.set(_class, mgr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,12 @@ import core, {
|
|||||||
type TxOperations,
|
type TxOperations,
|
||||||
type TxUpdateDoc,
|
type TxUpdateDoc,
|
||||||
type TypeAny,
|
type TypeAny,
|
||||||
type TypedSpace
|
type TypedSpace,
|
||||||
|
type WithLookup,
|
||||||
|
type AnyAttribute,
|
||||||
|
DocManager,
|
||||||
|
SortingOrder,
|
||||||
|
type Tx
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { type Restrictions } from '@hcengineering/guest'
|
import { type Restrictions } from '@hcengineering/guest'
|
||||||
import type { Asset, IntlString } from '@hcengineering/platform'
|
import type { Asset, IntlString } from '@hcengineering/platform'
|
||||||
@ -63,6 +68,7 @@ import {
|
|||||||
isAdminUser,
|
isAdminUser,
|
||||||
type KeyedAttribute
|
type KeyedAttribute
|
||||||
} from '@hcengineering/presentation'
|
} from '@hcengineering/presentation'
|
||||||
|
import { LiveQuery } from '@hcengineering/query'
|
||||||
import { type CollaborationUser } from '@hcengineering/text-editor'
|
import { type CollaborationUser } from '@hcengineering/text-editor'
|
||||||
import {
|
import {
|
||||||
ErrorPresenter,
|
ErrorPresenter,
|
||||||
@ -79,6 +85,7 @@ import {
|
|||||||
type Location
|
type Location
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import view, {
|
import view, {
|
||||||
|
type IAggregationManager,
|
||||||
AttributeCategoryOrder,
|
AttributeCategoryOrder,
|
||||||
type AttributeCategory,
|
type AttributeCategory,
|
||||||
type AttributeModel,
|
type AttributeModel,
|
||||||
@ -105,6 +112,102 @@ export interface LoadingProps {
|
|||||||
length: number
|
length: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export class AggregationManager<T extends Doc> implements IAggregationManager<T> {
|
||||||
|
docs: T[] | undefined
|
||||||
|
mgr: DocManager<T> | Promise<DocManager<T>> | undefined
|
||||||
|
query: (() => void) | undefined
|
||||||
|
lq: LiveQuery
|
||||||
|
lqCallback: () => void
|
||||||
|
private readonly setStore: (manager: DocManager<T>) => void
|
||||||
|
private readonly filter: (doc: T, target: T) => boolean
|
||||||
|
private readonly _class: Ref<Class<T>>
|
||||||
|
|
||||||
|
private constructor (
|
||||||
|
client: Client,
|
||||||
|
lqCallback: () => void,
|
||||||
|
setStore: (manager: DocManager<T>) => void,
|
||||||
|
categorizingFunc: (doc: T, target: T) => boolean,
|
||||||
|
_class: Ref<Class<T>>
|
||||||
|
) {
|
||||||
|
this.lq = new LiveQuery(client)
|
||||||
|
this.lqCallback = lqCallback ?? (() => {})
|
||||||
|
this.setStore = setStore
|
||||||
|
this.filter = categorizingFunc
|
||||||
|
this._class = _class
|
||||||
|
void this.getManager()
|
||||||
|
}
|
||||||
|
|
||||||
|
static create<T extends Doc>(
|
||||||
|
client: Client,
|
||||||
|
lqCallback: () => void,
|
||||||
|
setStore: (manager: DocManager<T>) => void,
|
||||||
|
categorizingFunc: (doc: T, target: T) => boolean,
|
||||||
|
_class: Ref<Class<T>>
|
||||||
|
): AggregationManager<T> {
|
||||||
|
return new AggregationManager<T>(client, lqCallback, setStore, categorizingFunc, _class)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getManager (): Promise<DocManager<T>> {
|
||||||
|
if (this.mgr !== undefined) {
|
||||||
|
if (this.mgr instanceof Promise) {
|
||||||
|
this.mgr = await this.mgr
|
||||||
|
}
|
||||||
|
return this.mgr
|
||||||
|
}
|
||||||
|
this.mgr = new Promise<DocManager<T>>((resolve) => {
|
||||||
|
this.query = this.lq.query(
|
||||||
|
this._class,
|
||||||
|
{},
|
||||||
|
(res) => {
|
||||||
|
const first = this.docs === undefined
|
||||||
|
this.docs = res
|
||||||
|
this.mgr = new DocManager<T>(res as T[])
|
||||||
|
this.setStore(this.mgr)
|
||||||
|
if (!first) {
|
||||||
|
this.lqCallback()
|
||||||
|
}
|
||||||
|
resolve(this.mgr)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sort: {
|
||||||
|
label: SortingOrder.Ascending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return await this.mgr
|
||||||
|
}
|
||||||
|
|
||||||
|
close (): void {
|
||||||
|
this.query?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
async notifyTx (...tx: Tx[]): Promise<void> {
|
||||||
|
await this.lq.tx(...tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttrClass (): Ref<Class<T>> {
|
||||||
|
return this._class
|
||||||
|
}
|
||||||
|
|
||||||
|
async categorize (target: Array<Ref<T>>, attr: AnyAttribute): Promise<Array<Ref<T>>> {
|
||||||
|
const mgr = await this.getManager()
|
||||||
|
for (const sid of [...target]) {
|
||||||
|
const c = mgr.getIdMap().get(sid) as WithLookup<T>
|
||||||
|
if (c !== undefined) {
|
||||||
|
let docs = mgr.getDocs()
|
||||||
|
docs = docs.filter((it: T) => this.filter(it, c))
|
||||||
|
target.push(...docs.map((it) => it._id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target.filter((it, idx, arr) => arr.indexOf(it) === idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
Class,
|
Class,
|
||||||
Client,
|
Client,
|
||||||
Doc,
|
Doc,
|
||||||
|
DocManager,
|
||||||
DocumentQuery,
|
DocumentQuery,
|
||||||
FindOptions,
|
FindOptions,
|
||||||
Hierarchy,
|
Hierarchy,
|
||||||
@ -373,29 +374,39 @@ export interface Groupping extends Class<Doc> {
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface AggregationManager {
|
export interface IAggregationManager<T extends Doc> {
|
||||||
close: () => void
|
close: () => void
|
||||||
notifyTx: (...tx: Tx[]) => Promise<void>
|
notifyTx: (...tx: Tx[]) => Promise<void>
|
||||||
categorize: (target: Array<Ref<Doc>>, attr: AnyAttribute) => Promise<Array<Ref<Doc>>>
|
categorize: (target: Array<Ref<T>>, attr: AnyAttribute) => Promise<Array<Ref<T>>>
|
||||||
getAttrClass: () => Ref<Class<Doc>>
|
getAttrClass: () => Ref<Class<T>>
|
||||||
updateSorting?: (finalOptions: FindOptions<Doc>, attr: AnyAttribute) => Promise<void>
|
updateSorting?: (finalOptions: FindOptions<T>, attr: AnyAttribute) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export type AggregationManagerResource = Resource<AggregationManager>
|
export type AggregationManagerResource = Resource<IAggregationManager<any>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export type CreateAggregationManagerFunc = Resource<(client: Client, lqCallback: () => void) => AggregationManager>
|
export type CreateAggregationManagerFunc = Resource<
|
||||||
|
(
|
||||||
|
client: Client,
|
||||||
|
lqCallback: () => void,
|
||||||
|
setStore: (manager: DocManager<any>) => void,
|
||||||
|
categorizingFunc: (doc: any, target: any) => boolean,
|
||||||
|
_class: Ref<Class<any>>
|
||||||
|
) => IAggregationManager<any>
|
||||||
|
>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface Aggregation extends Class<Doc> {
|
export interface Aggregation extends Class<Doc> {
|
||||||
createAggregationManager: CreateAggregationManagerFunc
|
createAggregationManager: CreateAggregationManagerFunc
|
||||||
|
setStoreFunc: Resource<(manager: DocManager<any>) => void>
|
||||||
|
filterFunc: Resource<(doc: Doc, target: Doc) => boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user